diff --git a/foundations/communication/.gitattributes b/foundations/communication/.gitattributes deleted file mode 100644 index 79a85db5c2..0000000000 --- a/foundations/communication/.gitattributes +++ /dev/null @@ -1,14 +0,0 @@ -# Don't allow people to merge changes to these generated files, because the result -# may be invalid. You need to run "rush update" again. -pnpm-lock.yaml merge=text -shrinkwrap.yaml merge=binary -npm-shrinkwrap.json merge=binary -yarn.lock merge=binary - -# Rush's JSON config files use JavaScript-style code comments. The rule below prevents pedantic -# syntax highlighters such as GitHub's from highlighting these comments as errors. Your text editor -# may also require a special configuration to allow comments in JSON. -# -# For more information, see this issue: https://github.com/microsoft/rushstack/issues/1088 -# -*.json linguist-language=JSON-with-Comments diff --git a/foundations/communication/.github/workflows/ci.yml b/foundations/communication/.github/workflows/ci.yml deleted file mode 100644 index 63bb264241..0000000000 --- a/foundations/communication/.github/workflows/ci.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: CI - -on: - push: - branches: ['main'] - tags: - - 'v0.7.*' - - 's0.7.*' - pull_request: - branches: ['main'] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 2 - - uses: actions/setup-node@v3 - with: - node-version: 22 - - name: Verify Change Logs - run: node common/scripts/install-run-rush.js change --verify - - name: Rush Install - run: node common/scripts/install-run-rush.js install - - name: Rush validate - run: node common/scripts/install-run-rush.js validate --verbose - - name: Rush test - run: node common/scripts/install-run-rush.js test --verbose - - name: Publish packages - if: startsWith(github.ref, 'refs/tags/v0.7.') || startsWith(github.ref, 'refs/tags/s0.7.') - env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - run: node common/scripts/install-run-rush.js publish --include-all --publish diff --git a/foundations/communication/.gitignore b/foundations/communication/.gitignore deleted file mode 100644 index 97b0f884d9..0000000000 --- a/foundations/communication/.gitignore +++ /dev/null @@ -1,140 +0,0 @@ -# Logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* -rush-logs/ - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov/ - -# Coverage directory used by tools like istanbul -coverage/ - -# nyc test coverage -.nyc_output/ - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt/ - -# Bower dependency directory (https://bower.io/) -bower_components/ - -# node-waf configuration -.lock-wscript/ - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release/ - -# Dependency directories -node_modules/ -jspm_packages/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm/ - -# Optional eslint cache -.eslintcache/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# next.js build output -.next/ - -# Docusaurus cache and generated files -.docusaurus/ - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# yarn v2 -.yarn/cache/ -.yarn/unplugged/ -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* - -# OS X temporary files -.DS_Store - -# IntelliJ IDEA project files; if you want to commit IntelliJ settings, this recipe may be helpful: -# https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -.idea/ -*.iml - -# Visual Studio Code -.vscode/ -!.vscode/tasks.json -!.vscode/launch.json - -# Rush temporary files -common/deploy/ -common/temp/ -common/autoinstallers/*/.npmrc -**/.rush/temp/ -*.lock - -# Common toolchain intermediate files -temp/ -lib/ -lib-amd/ -lib-es6/ -lib-esnext/ -lib-commonjs/ -lib-shim/ -dist/ -dist-storybook/ - -# Heft temporary files -.cache/ -.heft/ -.rush/ -.validate - -# build output -.turbo/ -**/types/ - -!packages/types/ - -# VS Code settings -.vscode/settings.json - -# logs -pnpm-debug.log* - -# environment variables -.env.production - diff --git a/foundations/communication/.npmrc b/foundations/communication/.npmrc deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/foundations/communication/.prettierrc b/foundations/communication/.prettierrc deleted file mode 100644 index d0f0f53741..0000000000 --- a/foundations/communication/.prettierrc +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "trailingComma": "none", - "tabWidth": 2, - "semi": false, - "singleQuote": true, - "printWidth": 120, - "useTabs": false, - "bracketSpacing": true, - "proseWrap": "preserve" -} \ No newline at end of file diff --git a/foundations/communication/.version b/foundations/communication/.version deleted file mode 100644 index 9868d567ed..0000000000 --- a/foundations/communication/.version +++ /dev/null @@ -1 +0,0 @@ -0.1.195 diff --git a/foundations/communication/README.md b/foundations/communication/README.md deleted file mode 100644 index a046e7bb0e..0000000000 --- a/foundations/communication/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# Important Notice - -**This repository is not a standalone project** and is intended to be used **only** as a submodule within https://github.com/hcengineering/platform. - -There is no separate CI, release pipeline, or independent build in this directory. -All install and build commands must be run from the root of the main repository (for example: rush install && rush build). -Changes in this folder will not take effect unless the parent repository initializes and updates this submodule. - - -Cloning this repository by itself will not provide the full dependency structure. To set everything up correctly, clone and initialize https://github.com/hcengineering/platform. diff --git a/foundations/communication/common/changes/@hcengineering/communication-rest-client/main_2025-10-29-08-20.json b/foundations/communication/common/changes/@hcengineering/communication-rest-client/main_2025-10-29-08-20.json deleted file mode 100644 index 6ef798c1dc..0000000000 --- a/foundations/communication/common/changes/@hcengineering/communication-rest-client/main_2025-10-29-08-20.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@hcengineering/communication-rest-client", - "comment": "update deps", - "type": "patch" - } - ], - "packageName": "@hcengineering/communication-rest-client" -} \ No newline at end of file diff --git a/foundations/communication/common/changes/@hcengineering/communication-sdk-types/main_2025-10-29-08-20.json b/foundations/communication/common/changes/@hcengineering/communication-sdk-types/main_2025-10-29-08-20.json deleted file mode 100644 index 4fe5123b5c..0000000000 --- a/foundations/communication/common/changes/@hcengineering/communication-sdk-types/main_2025-10-29-08-20.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@hcengineering/communication-sdk-types", - "comment": "update deps", - "type": "patch" - } - ], - "packageName": "@hcengineering/communication-sdk-types" -} \ No newline at end of file diff --git a/foundations/communication/common/changes/@hcengineering/communication-server/main_2025-10-29-08-20.json b/foundations/communication/common/changes/@hcengineering/communication-server/main_2025-10-29-08-20.json deleted file mode 100644 index 7a62a00716..0000000000 --- a/foundations/communication/common/changes/@hcengineering/communication-server/main_2025-10-29-08-20.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@hcengineering/communication-server", - "comment": "update deps", - "type": "patch" - } - ], - "packageName": "@hcengineering/communication-server" -} \ No newline at end of file diff --git a/foundations/communication/common/changes/@hcengineering/communication-types/main_2025-10-29-08-20.json b/foundations/communication/common/changes/@hcengineering/communication-types/main_2025-10-29-08-20.json deleted file mode 100644 index dbb851e6cb..0000000000 --- a/foundations/communication/common/changes/@hcengineering/communication-types/main_2025-10-29-08-20.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@hcengineering/communication-types", - "comment": "update deps", - "type": "patch" - } - ], - "packageName": "@hcengineering/communication-types" -} \ No newline at end of file diff --git a/foundations/communication/common/config/rush/.npmrc b/foundations/communication/common/config/rush/.npmrc deleted file mode 100644 index 4bb3c57a4e..0000000000 --- a/foundations/communication/common/config/rush/.npmrc +++ /dev/null @@ -1,33 +0,0 @@ -# Rush uses this file to configure the NPM package registry during installation. It is applicable -# to PNPM, NPM, and Yarn package managers. It is used by operations such as "rush install", -# "rush update", and the "install-run.js" scripts. -# -# NOTE: The "rush publish" command uses .npmrc-publish instead. -# -# Before invoking the package manager, Rush will generate an .npmrc in the folder where installation -# is performed. This generated file will omit any config lines that reference environment variables -# that are undefined in that session; this avoids problems that would otherwise result due to -# a missing variable being replaced by an empty string. -# -# If "subspacesEnabled" is true in subspaces.json, the generated file will merge settings from -# "common/config/rush/.npmrc" and "common/config/subspaces//.npmrc", with the latter taking -# precedence. -# -# * * * SECURITY WARNING * * * -# -# It is NOT recommended to store authentication tokens in a text file on a lab machine, because -# other unrelated processes may be able to read that file. Also, the file may persist indefinitely, -# for example if the machine loses power. A safer practice is to pass the token via an -# environment variable, which can be referenced from .npmrc using ${} expansion. For example: -# -# //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN} -# - -# Explicitly specify the NPM registry that "rush install" and "rush update" will use by default: -registry=https://registry.npmjs.org/ - -# Optionally provide an authentication token for the above registry URL (if it is a private registry): -//registry.npmjs.org/:_authToken=${NPM_TOKEN} - -# Change this to "true" if your registry requires authentication for read-only operations: -always-auth=false diff --git a/foundations/communication/common/config/rush/.npmrc-publish b/foundations/communication/common/config/rush/.npmrc-publish deleted file mode 100644 index 951a4918cf..0000000000 --- a/foundations/communication/common/config/rush/.npmrc-publish +++ /dev/null @@ -1,29 +0,0 @@ -# This config file is very similar to common/config/rush/.npmrc, except that .npmrc-publish -# is used by the "rush publish" command, as publishing often involves different credentials -# and registries than other operations. -# -# Before invoking the package manager, Rush will copy this file to "common/temp/publish-home/.npmrc" -# and then temporarily map that folder as the "home directory" for the current user account. -# This enables the same settings to apply for each project folder that gets published. The copied file -# will omit any config lines that reference environment variables that are undefined in that session; -# this avoids problems that would otherwise result due to a missing variable being replaced by -# an empty string. -# -# * * * SECURITY WARNING * * * -# -# It is NOT recommended to store authentication tokens in a text file on a lab machine, because -# other unrelated processes may be able to read the file. Also, the file may persist indefinitely, -# for example if the machine loses power. A safer practice is to pass the token via an -# environment variable, which can be referenced from .npmrc using ${} expansion. For example: -# -# //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN} -# - -# Explicitly specify the NPM registry that "rush publish" will use by default: -registry=https://registry.npmjs.org/ - -# Optionally provide an authentication token for the above registry URL (if it is a private registry): -//registry.npmjs.org/:_authToken=${NPM_TOKEN} - -# Change this to "true" if your registry requires authentication for read-only operations: -always-auth=false diff --git a/foundations/communication/common/config/rush/.pnpmfile.cjs b/foundations/communication/common/config/rush/.pnpmfile.cjs deleted file mode 100644 index 98cf3279ec..0000000000 --- a/foundations/communication/common/config/rush/.pnpmfile.cjs +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -/** - * When using the PNPM package manager, you can use pnpmfile.js to workaround - * dependencies that have mistakes in their package.json file. (This feature is - * functionally similar to Yarn's "resolutions".) - * - * For details, see the PNPM documentation: - * https://pnpm.io/pnpmfile#hooks - * - * IMPORTANT: SINCE THIS FILE CONTAINS EXECUTABLE CODE, MODIFYING IT IS LIKELY TO INVALIDATE - * ANY CACHED DEPENDENCY ANALYSIS. After any modification to pnpmfile.js, it's recommended to run - * "rush update --full" so that PNPM will recalculate all version selections. - */ -module.exports = { - hooks: { - readPackage - } -}; - -/** - * This hook is invoked during installation before a package's dependencies - * are selected. - * The `packageJson` parameter is the deserialized package.json - * contents for the package that is about to be installed. - * The `context` parameter provides a log() function. - * The return value is the updated object. - */ -function readPackage(packageJson, context) { - - // // The karma types have a missing dependency on typings from the log4js package. - // if (packageJson.name === '@types/karma') { - // context.log('Fixed up dependencies for @types/karma'); - // packageJson.dependencies['log4js'] = '0.6.38'; - // } - - return packageJson; -} diff --git a/foundations/communication/common/config/rush/artifactory.json b/foundations/communication/common/config/rush/artifactory.json deleted file mode 100644 index 268065478a..0000000000 --- a/foundations/communication/common/config/rush/artifactory.json +++ /dev/null @@ -1,109 +0,0 @@ -/** - * This configuration file manages Rush integration with JFrog Artifactory services. - * More documentation is available on the Rush website: https://rushjs.io - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/artifactory.schema.json", - - "packageRegistry": { - /** - * (Required) Set this to "true" to enable Rush to manage tokens for an Artifactory NPM registry. - * When enabled, "rush install" will automatically detect when the user's ~/.npmrc - * authentication token is missing or expired. And "rush setup" will prompt the user to - * renew their token. - * - * The default value is false. - */ - "enabled": false, - - /** - * (Required) Specify the URL of your NPM registry. This is the same URL that appears in - * your .npmrc file. It should look something like this example: - * - * https://your-company.jfrog.io/your-project/api/npm/npm-private/ - */ - "registryUrl": "", - - /** - * A list of custom strings that "rush setup" should add to the user's ~/.npmrc file at the time - * when the token is updated. This could be used for example to configure the company registry - * to be used whenever NPM is invoked as a standalone command (but it's not needed for Rush - * operations like "rush add" and "rush install", which get their mappings from the monorepo's - * common/config/rush/.npmrc file). - * - * NOTE: The ~/.npmrc settings are global for the user account on a given machine, so be careful - * about adding settings that may interfere with other work outside the monorepo. - */ - "userNpmrcLinesToAdd": [ - // "@example:registry=https://your-company.jfrog.io/your-project/api/npm/npm-private/" - ], - - /** - * (Required) Specifies the URL of the Artifactory control panel where the user can generate - * an API key. This URL is printed after the "visitWebsite" message. - * It should look something like this example: https://your-company.jfrog.io/ - * Specify an empty string to suppress this line entirely. - */ - "artifactoryWebsiteUrl": "", - - /** - * Uncomment this line to specify the type of credential to save in the user's ~/.npmrc file. - * The default is "password", which means the user's API token will be traded in for an - * npm password specific to that registry. Optionally you can specify "authToken", which - * will save the user's API token as credentials instead. - */ - // "credentialType": "password", - - /** - * These settings allow the "rush setup" interactive prompts to be customized, for - * example with messages specific to your team or configuration. Specify an empty string - * to suppress that message entirely. - */ - "messageOverrides": { - /** - * Overrides the message that normally says: - * "This monorepo consumes packages from an Artifactory private NPM registry." - */ - // "introduction": "", - - /** - * Overrides the message that normally says: - * "Please contact the repository maintainers for help with setting up an Artifactory user account." - */ - // "obtainAnAccount": "", - - /** - * Overrides the message that normally says: - * "Please open this URL in your web browser:" - * - * The "artifactoryWebsiteUrl" string is printed after this message. - */ - // "visitWebsite": "", - - /** - * Overrides the message that normally says: - * "Your user name appears in the upper-right corner of the JFrog website." - */ - // "locateUserName": "", - - /** - * Overrides the message that normally says: - * "Click 'Edit Profile' on the JFrog website. Click the 'Generate API Key' - * button if you haven't already done so previously." - */ - // "locateApiKey": "" - - /** - * Overrides the message that normally prompts: - * "What is your Artifactory user name?" - */ - // "userNamePrompt": "" - - /** - * Overrides the message that normally prompts: - * "What is your Artifactory API key?" - */ - // "apiKeyPrompt": "" - } - } -} diff --git a/foundations/communication/common/config/rush/build-cache.json b/foundations/communication/common/config/rush/build-cache.json deleted file mode 100644 index 072e9f7d49..0000000000 --- a/foundations/communication/common/config/rush/build-cache.json +++ /dev/null @@ -1,160 +0,0 @@ -/** - * This configuration file manages Rush's build cache feature. - * More documentation is available on the Rush website: https://rushjs.io - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/build-cache.schema.json", - - /** - * (Required) EXPERIMENTAL - Set this to true to enable the build cache feature. - * - * See https://rushjs.io/pages/maintainer/build_cache/ for details about this experimental feature. - */ - "buildCacheEnabled": false, - - /** - * (Required) Choose where project build outputs will be cached. - * - * Possible values: "local-only", "azure-blob-storage", "amazon-s3" - */ - "cacheProvider": "local-only", - - /** - * Setting this property overrides the cache entry ID. If this property is set, it must contain - * a [hash] token. - * - * Other available tokens: - * - [projectName] Example: "@my-scope/my-project" - * - [projectName:normalize] Example: "my-scope+my-project" - * - [phaseName] Example: "_phase:test/api" - * - [phaseName:normalize] Example: "_phase:test+api" - * - [phaseName:trimPrefix] Example: "test/api" - * - [os] Example: "win32" - * - [arch] Example: "x64" - */ - // "cacheEntryNamePattern": "[projectName:normalize]-[phaseName:normalize]-[hash]" - - /** - * (Optional) Salt to inject during calculation of the cache key. This can be used to invalidate the cache for all projects when the salt changes. - */ - // "cacheHashSalt": "1", - - /** - * Use this configuration with "cacheProvider"="azure-blob-storage" - */ - "azureBlobStorageConfiguration": { - /** - * (Required) The name of the the Azure storage account to use for build cache. - */ - // "storageAccountName": "example", - - /** - * (Required) The name of the container in the Azure storage account to use for build cache. - */ - // "storageContainerName": "my-container", - - /** - * The Azure environment the storage account exists in. Defaults to AzurePublicCloud. - * - * Possible values: "AzurePublicCloud", "AzureChina", "AzureGermany", "AzureGovernment" - */ - // "azureEnvironment": "AzurePublicCloud", - - /** - * An optional prefix for cache item blob names. - */ - // "blobPrefix": "my-prefix", - - /** - * If set to true, allow writing to the cache. Defaults to false. - */ - // "isCacheWriteAllowed": true, - - /** - * The Entra ID login flow to use. Defaults to 'AdoCodespacesAuth' on GitHub Codespaces, 'InteractiveBrowser' otherwise. - */ - // "loginFlow": "InteractiveBrowser", - - /** - * If set to true, reading the cache requires authentication. Defaults to false. - */ - // "readRequiresAuthentication": true - }, - - /** - * Use this configuration with "cacheProvider"="amazon-s3" - */ - "amazonS3Configuration": { - /** - * (Required unless s3Endpoint is specified) The name of the bucket to use for build cache. - * Example: "my-bucket" - */ - // "s3Bucket": "my-bucket", - - /** - * (Required unless s3Bucket is specified) The Amazon S3 endpoint of the bucket to use for build cache. - * This should not include any path; use the s3Prefix to set the path. - * Examples: "my-bucket.s3.us-east-2.amazonaws.com" or "http://localhost:9000" - */ - // "s3Endpoint": "https://my-bucket.s3.us-east-2.amazonaws.com", - - /** - * (Required) The Amazon S3 region of the bucket to use for build cache. - * Example: "us-east-1" - */ - // "s3Region": "us-east-1", - - /** - * An optional prefix ("folder") for cache items. It should not start with "/". - */ - // "s3Prefix": "my-prefix", - - /** - * If set to true, allow writing to the cache. Defaults to false. - */ - // "isCacheWriteAllowed": true - }, - - /** - * Use this configuration with "cacheProvider"="http" - */ - "httpConfiguration": { - /** - * (Required) The URL of the server that stores the caches. - * Example: "https://build-cacches.example.com/" - */ - // "url": "https://build-cacches.example.com/", - - /** - * (Optional) The HTTP method to use when writing to the cache (defaults to PUT). - * Should be one of PUT, POST, or PATCH. - * Example: "PUT" - */ - // "uploadMethod": "PUT", - - /** - * (Optional) HTTP headers to pass to the cache server. - * Example: { "X-HTTP-Company-Id": "109283" } - */ - // "headers": {}, - - /** - * (Optional) Shell command that prints the authorization token needed to communicate with the - * cache server, and exits with exit code 0. This command will be executed from the root of - * the monorepo. - * Example: { "exec": "node", "args": ["common/scripts/auth.js"] } - */ - // "tokenHandler": { "exec": "node", "args": ["common/scripts/auth.js"] }, - - /** - * (Optional) Prefix for cache keys. - * Example: "my-company-" - */ - // "cacheKeyPrefix": "", - - /** - * (Optional) If set to true, allow writing to the cache. Defaults to false. - */ - // "isCacheWriteAllowed": true - } -} diff --git a/foundations/communication/common/config/rush/cobuild.json b/foundations/communication/common/config/rush/cobuild.json deleted file mode 100644 index a47fad18d5..0000000000 --- a/foundations/communication/common/config/rush/cobuild.json +++ /dev/null @@ -1,22 +0,0 @@ -/** - * This configuration file manages Rush's cobuild feature. - * More documentation is available on the Rush website: https://rushjs.io - */ - { - "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/cobuild.schema.json", - - /** - * (Required) EXPERIMENTAL - Set this to true to enable the cobuild feature. - * RUSH_COBUILD_CONTEXT_ID should always be specified as an environment variable with an non-empty string, - * otherwise the cobuild feature will be disabled. - */ - "cobuildFeatureEnabled": false, - - /** - * (Required) Choose where cobuild lock will be acquired. - * - * The lock provider is registered by the rush plugins. - * For example, @rushstack/rush-redis-cobuild-plugin registers the "redis" lock provider. - */ - "cobuildLockProvider": "redis" -} diff --git a/foundations/communication/common/config/rush/command-line.json b/foundations/communication/common/config/rush/command-line.json deleted file mode 100644 index 5064b33ef9..0000000000 --- a/foundations/communication/common/config/rush/command-line.json +++ /dev/null @@ -1,262 +0,0 @@ -/** - * This configuration file defines custom commands for the "rush" command-line. - * More documentation is available on the Rush website: https://rushjs.io - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/command-line.schema.json", - - "phases": [ - { - "name": "_phase:build", - "dependencies": { - "upstream": ["_phase:build"] - }, - "ignoreMissingScript": true, - "allowWarningsOnSuccess": false - }, - { - "name": "_phase:validate", - "dependencies": { - "self": ["_phase:build"], - "upstream": ["_phase:validate", "_phase:build"] - }, - "ignoreMissingScript": true, - "allowWarningsOnSuccess": false - }, - { - "name": "_phase:bundle", - "dependencies": { - "self": ["_phase:build"] - }, - "ignoreMissingScript": true, - "allowWarningsOnSuccess": false - }, - { - "name": "_phase:test", - "dependencies": { - "self": ["_phase:build"], - "upstream": ["_phase:validate"] - }, - "ignoreMissingScript": true, - "allowWarningsOnSuccess": true - } - ], - "commands": [ - { - "commandKind": "global", - "name": "coverage", - "summary": "Run tests, merge LCOV and generate HTML coverage", - "description": "Run 'rush test', then merge per-package LCOV files and generate HTML coverage in coverage/html", - "safeForSimultaneousRushProcesses": true, - "shellCommand": "rush test && node scripts/merge-coverage.js && node scripts/generate-coverage-html.js coverage/lcov.info coverage/html" - }, - { - "commandKind": "phased", - "summary": "Do testing", - "name": "test", - "phases": ["_phase:build", "_phase:test"], - "enableParallelism": true, - "incremental": true - }, - { - "commandKind": "phased", - "name": "build", - "summary": "build", - "phases": ["_phase:build"], - "enableParallelism": true, - "incremental": true, - "watchOptions": { - "alwaysWatch": false, - "watchPhases": ["_phase:build"] - } - }, - { - "commandKind": "phased", - "name": "validate", - "phases": ["_phase:validate"], - "summary": "validate", - "enableParallelism": true, - "incremental": true - }, - { - "commandKind": "bulk", - "name": "format", - "summary": "Format", - "description": "Perform a formatting", - "enableParallelism": true, - "incremental": false, - "ignoreMissingScript": true, - "safeForSimultaneousRushProcesses": true, - "disableBuildCache": true - }, - { - "commandKind": "bulk", - "name": "lint", - "summary": "Lint", - "description": "Linting", - "enableParallelism": true, - "incremental": false, - "ignoreMissingScript": true, - "safeForSimultaneousRushProcesses": true, - "disableBuildCache": true - }, - { - "commandKind": "bulk", - "name": "lint:fix", - "summary": "Lint & fix", - "description": "Linting and fixing", - "enableParallelism": true, - "incremental": false, - "ignoreMissingScript": true, - "safeForSimultaneousRushProcesses": true, - "disableBuildCache": true - }, - { - "commandKind": "global", - "name": "bump-changes", - "summary": "Bump changes from tag", - "description": "Bump changes from previous tag", - "safeForSimultaneousRushProcesses": true, - "shellCommand": "./common/scripts/node_modules/.bin/bump-changes-from-tag" - } - ], - /** - * Custom "parameters" introduce new parameters for specified Rush command-line commands. - * For example, you might define a "--production" parameter for the "rush build" command. - */ - "parameters": [ - { - "parameterKind": "flag", - "longName": "--lite", - "shortName": "-l", - "description": "Enable Heft lite building option, will skip some phases.", - "associatedCommands": ["build"] - }, - { - "parameterKind": "flag", - "longName": "--clean", - "description": "Enable Heft clean building option", - "associatedCommands": ["build"] - } - // { - // /** - // * (Required) Determines the type of custom parameter. - // * A "flag" is a custom command-line parameter whose presence acts as an on/off switch. - // */ - // "parameterKind": "flag", - // - // /** - // * (Required) The long name of the parameter. It must be lower-case and use dash delimiters. - // */ - // "longName": "--my-flag", - // - // /** - // * An optional alternative short name for the parameter. It must be a dash followed by a single - // * lower-case or upper-case letter, which is case-sensitive. - // * - // * NOTE: The Rush developers recommend that automation scripts should always use the long name - // * to improve readability. The short name is only intended as a convenience for humans. - // * The alphabet letters run out quickly, and are difficult to memorize, so *only* use - // * a short name if you expect the parameter to be needed very often in everyday operations. - // */ - // "shortName": "-m", - // - // /** - // * (Required) A long description to be shown in the command-line help. - // * - // * Whenever you introduce commands/parameters, taking a little time to write meaningful - // * documentation can make a big difference for the developer experience in your repo. - // */ - // "description": "A custom flag parameter that is passed to the scripts that are invoked when building projects", - // - // /** - // * (Required) A list of custom commands and/or built-in Rush commands that this parameter may - // * be used with. The parameter will be appended to the shell command that Rush invokes. - // */ - // "associatedCommands": ["build", "rebuild"] - // }, - // - // { - // /** - // * (Required) Determines the type of custom parameter. - // * A "string" is a custom command-line parameter whose value is a simple text string. - // */ - // "parameterKind": "string", - // "longName": "--my-string", - // "description": "A custom string parameter for the \"my-global-command\" custom command", - // - // "associatedCommands": ["my-global-command"], - // - // /** - // * The name of the argument, which will be shown in the command-line help. - // * - // * For example, if the parameter name is '--count" and the argument name is "NUMBER", - // * then the command-line help would display "--count NUMBER". The argument name must - // * be comprised of upper-case letters, numbers, and underscores. It should be kept short. - // */ - // "argumentName": "SOME_TEXT", - // - // /** - // * If true, this parameter must be included with the command. The default is false. - // */ - // "required": false - // }, - // - // { - // /** - // * (Required) Determines the type of custom parameter. - // * A "choice" is a custom command-line parameter whose argument must be chosen from a list of - // * allowable alternatives. - // */ - // "parameterKind": "choice", - // "longName": "--my-choice", - // "description": "A custom choice parameter for the \"my-global-command\" custom command", - // - // "associatedCommands": ["my-global-command"], - // - // /** - // * If true, this parameter must be included with the command. The default is false. - // */ - // "required": false, - // - // /** - // * Normally if a parameter is omitted from the command line, it will not be passed - // * to the shell command. this value will be inserted by default. Whereas if a "defaultValue" - // * is defined, the parameter will always be passed to the shell command, and will use the - // * default value if unspecified. The value must be one of the defined alternatives. - // */ - // "defaultValue": "vanilla", - // - // /** - // * (Required) A list of alternative argument values that can be chosen for this parameter. - // */ - // "alternatives": [ - // { - // /** - // * A token that is one of the alternatives that can be used with the choice parameter, - // * e.g. "vanilla" in "--flavor vanilla". - // */ - // "name": "vanilla", - // - // /** - // * A detailed description for the alternative that can be shown in the command-line help. - // * - // * Whenever you introduce commands/parameters, taking a little time to write meaningful - // * documentation can make a big difference for the developer experience in your repo. - // */ - // "description": "Use the vanilla flavor (the default)" - // }, - // - // { - // "name": "chocolate", - // "description": "Use the chocolate flavor" - // }, - // - // { - // "name": "strawberry", - // "description": "Use the strawberry flavor" - // } - // ] - // } - ] -} diff --git a/foundations/communication/common/config/rush/common-versions.json b/foundations/communication/common/config/rush/common-versions.json deleted file mode 100644 index 4a3ecbd1ee..0000000000 --- a/foundations/communication/common/config/rush/common-versions.json +++ /dev/null @@ -1,77 +0,0 @@ -/** - * This configuration file specifies NPM dependency version selections that affect all projects - * in a Rush repo. More documentation is available on the Rush website: https://rushjs.io - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/common-versions.schema.json", - - /** - * A table that specifies a "preferred version" for a given NPM package. This feature is typically used - * to hold back an indirect dependency to a specific older version, or to reduce duplication of indirect dependencies. - * - * The "preferredVersions" value can be any SemVer range specifier (e.g. "~1.2.3"). Rush injects these values into - * the "dependencies" field of the top-level common/temp/package.json, which influences how the package manager - * will calculate versions. The specific effect depends on your package manager. Generally it will have no - * effect on an incompatible or already constrained SemVer range. If you are using PNPM, similar effects can be - * achieved using the pnpmfile.js hook. See the Rush documentation for more details. - * - * After modifying this field, it's recommended to run "rush update --full" so that the package manager - * will recalculate all version selections. - */ - "preferredVersions": { - /** - * When someone asks for "^1.0.0" make sure they get "1.2.3" when working in this repo, - * instead of the latest version. - */ - // "some-library": "1.2.3" - }, - - /** - * When set to true, for all projects in the repo, all dependencies will be automatically added as preferredVersions, - * except in cases where different projects specify different version ranges for a given dependency. For older - * package managers, this tended to reduce duplication of indirect dependencies. However, it can sometimes cause - * trouble for indirect dependencies with incompatible peerDependencies ranges. - * - * The default value is true. If you're encountering installation errors related to peer dependencies, - * it's recommended to set this to false. - * - * After modifying this field, it's recommended to run "rush update --full" so that the package manager - * will recalculate all version selections. - */ - // "implicitlyPreferredVersions": false, - - /** - * If you would like the version specifiers for your dependencies to be consistent, then - * uncomment this line. This is effectively similar to running "rush check" before any - * of the following commands: - * - * rush install, rush update, rush link, rush version, rush publish - * - * In some cases you may want this turned on, but need to allow certain packages to use a different - * version. In those cases, you will need to add an entry to the "allowedAlternativeVersions" - * section of the common-versions.json. - * - * In the case that subspaces is enabled, this setting will take effect at a subspace level. - */ - // "ensureConsistentVersions": true, - - /** - * The "rush check" command can be used to enforce that every project in the repo must specify - * the same SemVer range for a given dependency. However, sometimes exceptions are needed. - * The allowedAlternativeVersions table allows you to list other SemVer ranges that will be - * accepted by "rush check" for a given dependency. - * - * IMPORTANT: THIS TABLE IS FOR *ADDITIONAL* VERSION RANGES THAT ARE ALTERNATIVES TO THE - * USUAL VERSION (WHICH IS INFERRED BY LOOKING AT ALL PROJECTS IN THE REPO). - * This design avoids unnecessary churn in this file. - */ - "allowedAlternativeVersions": { - /** - * For example, allow some projects to use an older TypeScript compiler - * (in addition to whatever "usual" version is being used by other projects in the repo): - */ - // "typescript": [ - // "~2.4.0" - // ] - } -} \ No newline at end of file diff --git a/foundations/communication/common/config/rush/custom-tips.json b/foundations/communication/common/config/rush/custom-tips.json deleted file mode 100644 index 31e540d6ad..0000000000 --- a/foundations/communication/common/config/rush/custom-tips.json +++ /dev/null @@ -1,29 +0,0 @@ -/** - * This configuration file allows repo maintainers to configure extra details to be - * printed alongside certain Rush messages. More documentation is available on the - * Rush website: https://rushjs.io - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/custom-tips.schema.json", - - /** - * Custom tips allow you to annotate Rush's console messages with advice tailored for - * your specific monorepo. - */ - "customTips": [ - // { - // /** - // * (REQUIRED) An identifier indicating a message that may be printed by Rush. - // * If that message is printed, then this custom tip will be shown. - // * The list of available tip identifiers can be found on this page: - // * https://rushjs.io/pages/maintainer/custom_tips/ - // */ - // "tipId": "TIP_RUSH_INCONSISTENT_VERSIONS", - // - // /** - // * (REQUIRED) The message text to be displayed for this tip. - // */ - // "message": "For additional troubleshooting information, refer this wiki article:\n\nhttps://intranet.contoso.com/docs/pnpm-mismatch" - // } - ] -} diff --git a/foundations/communication/common/config/rush/experiments.json b/foundations/communication/common/config/rush/experiments.json deleted file mode 100644 index 01f8f8f902..0000000000 --- a/foundations/communication/common/config/rush/experiments.json +++ /dev/null @@ -1,121 +0,0 @@ -/** - * This configuration file allows repo maintainers to enable and disable experimental - * Rush features. More documentation is available on the Rush website: https://rushjs.io - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/experiments.schema.json", - - /** - * By default, 'rush install' passes --no-prefer-frozen-lockfile to 'pnpm install'. - * Set this option to true to pass '--frozen-lockfile' instead for faster installs. - */ - // "usePnpmFrozenLockfileForRushInstall": true, - - /** - * By default, 'rush update' passes --no-prefer-frozen-lockfile to 'pnpm install'. - * Set this option to true to pass '--prefer-frozen-lockfile' instead to minimize shrinkwrap changes. - */ - // "usePnpmPreferFrozenLockfileForRushUpdate": true, - - /** - * By default, 'rush update' runs as a single operation. - * Set this option to true to instead update the lockfile with `--lockfile-only`, then perform a `--frozen-lockfile` install. - * Necessary when using the `afterAllResolved` hook in .pnpmfile.cjs. - */ - // "usePnpmLockfileOnlyThenFrozenLockfileForRushUpdate": true, - - /** - * If using the 'preventManualShrinkwrapChanges' option, restricts the hash to only include the layout of external dependencies. - * Used to allow links between workspace projects or the addition/removal of references to existing dependency versions to not - * cause hash changes. - */ - // "omitImportersFromPreventManualShrinkwrapChanges": true, - - /** - * If true, the chmod field in temporary project tar headers will not be normalized. - * This normalization can help ensure consistent tarball integrity across platforms. - */ - // "noChmodFieldInTarHeaderNormalization": true, - - /** - * If true, build caching will respect the allowWarningsInSuccessfulBuild flag and cache builds with warnings. - * This will not replay warnings from the cached build. - */ - // "buildCacheWithAllowWarningsInSuccessfulBuild": true, - - /** - * If true, build skipping will respect the allowWarningsInSuccessfulBuild flag and skip builds with warnings. - * This will not replay warnings from the skipped build. - */ - // "buildSkipWithAllowWarningsInSuccessfulBuild": true, - - /** - * If true, perform a clean install after when running `rush install` or `rush update` if the - * `.npmrc` file has changed since the last install. - */ - // "cleanInstallAfterNpmrcChanges": true, - - /** - * If true, print the outputs of shell commands defined in event hooks to the console. - */ - // "printEventHooksOutputToConsole": true, - - /** - * If true, Rush will not allow node_modules in the repo folder or in parent folders. - */ - // "forbidPhantomResolvableNodeModulesFolders": true, - - /** - * (UNDER DEVELOPMENT) For certain installation problems involving peer dependencies, PNPM cannot - * correctly satisfy versioning requirements without installing duplicate copies of a package inside the - * node_modules folder. This poses a problem for "workspace:*" dependencies, as they are normally - * installed by making a symlink to the local project source folder. PNPM's "injected dependencies" - * feature provides a model for copying the local project folder into node_modules, however copying - * must occur AFTER the dependency project is built and BEFORE the consuming project starts to build. - * The "pnpm-sync" tool manages this operation; see its documentation for details. - * Enable this experiment if you want "rush" and "rushx" commands to resync injected dependencies - * by invoking "pnpm-sync" during the build. - */ - // "usePnpmSyncForInjectedDependencies": true, - - /** - * If set to true, Rush will generate a `project-impact-graph.yaml` file in the repository root during `rush update`. - */ - // "generateProjectImpactGraphDuringRushUpdate": true, - - /** - * If true, when running in watch mode, Rush will check for phase scripts named `_phase::ipc` and run them instead - * of `_phase:` if they exist. The created child process will be provided with an IPC channel and expected to persist - * across invocations. - */ - // "useIPCScriptsInWatchMode": true, - - /** - * (UNDER DEVELOPMENT) The Rush alerts feature provides a way to send announcements to engineers - * working in the monorepo, by printing directly in the user's shell window when they invoke Rush commands. - * This ensures that important notices will be seen by anyone doing active development, since people often - * ignore normal discussion group messages or don't know to subscribe. - */ - // "rushAlerts": true, - - - /** - * When using cobuilds, this experiment allows uncacheable operations to benefit from cobuild orchestration without using the build cache. - */ - // "allowCobuildWithoutCache": true, - - /** - * By default, rush perform a full scan of the entire repository. For example, Rush runs `git status` to check for local file changes. - * When this toggle is enabled, Rush will only scan specific paths, significantly speeding up Git operations. - */ - // "enableSubpathScan": true, - - /** - * Rush has a policy that normally requires Rush projects to specify `workspace:*` in package.json when depending - * on other projects in the workspace, unless they are explicitly declared as `decoupledLocalDependencies` - * in rush.json. Enabling this experiment will remove that requirement for dependencies belonging to a different - * subspace. This is useful for large product groups who work in separate subspaces and generally prefer to consume - * each other's packages via the NPM registry. - */ - // "exemptDecoupledDependenciesBetweenSubspaces": false -} diff --git a/foundations/communication/common/config/rush/pnpm-config.json b/foundations/communication/common/config/rush/pnpm-config.json deleted file mode 100644 index f09b56ba1d..0000000000 --- a/foundations/communication/common/config/rush/pnpm-config.json +++ /dev/null @@ -1,331 +0,0 @@ -/** - * This configuration file provides settings specific to the PNPM package manager. - * More documentation is available on the Rush website: https://rushjs.io - * - * Rush normally looks for this file in `common/config/rush/pnpm-config.json`. However, - * if `subspacesEnabled` is true in subspaces.json, then Rush will instead first look - * for `common/config/subspaces//pnpm-config.json`. (If the file exists in both places, - * then the file under `common/config/rush` is ignored.) - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/pnpm-config.schema.json", - - /** - * If true, then `rush install` and `rush update` will use the PNPM workspaces feature - * to perform the install, instead of the old model where Rush generated the symlinks - * for each projects's node_modules folder. - * - * When using workspaces, Rush will generate a `common/temp/pnpm-workspace.yaml` file referencing - * all local projects to install. Rush will also generate a `.pnpmfile.cjs` shim which implements - * Rush-specific features such as preferred versions. The user's `common/config/rush/.pnpmfile.cjs` - * is invoked by the shim. - * - * This option is strongly recommended. The default value is false. - */ - "useWorkspaces": true, - - /** - * This setting determines how PNPM chooses version numbers during `rush update`. - * For example, suppose `lib-x@3.0.0` depends on `"lib-y": "^1.2.3"` whose latest major - * releases are `1.8.9` and `2.3.4`. The resolution mode `lowest-direct` might choose - * `lib-y@1.2.3`, wheres `highest` will choose 1.8.9, and `time-based` will pick the - * highest compatible version at the time when `lib-x@3.0.0` itself was published (ensuring - * that the version could have been tested by the maintainer of "lib-x"). For local workspace - * projects, `time-based` instead works like `lowest-direct`, avoiding upgrades unless - * they are explicitly requested. Although `time-based` is the most robust option, it may be - * slightly slower with registries such as npmjs.com that have not implemented an optimization. - * - * IMPORTANT: Be aware that PNPM 8.0.0 initially defaulted to `lowest-direct` instead of - * `highest`, but PNPM reverted this decision in 8.6.12 because it caused confusion for users. - * Rush version 5.106.0 and newer avoids this confusion by consistently defaulting to - * `highest` when `resolutionMode` is not explicitly set in pnpm-config.json or .npmrc, - * regardless of your PNPM version. - * - * PNPM documentation: https://pnpm.io/npmrc#resolution-mode - * - * Possible values are: `highest`, `time-based`, and `lowest-direct`. - * The default is `highest`. - */ - // "resolutionMode": "time-based", - - /** - * This setting determines whether PNPM will automatically install (non-optional) - * missing peer dependencies instead of reporting an error. Doing so conveniently - * avoids the need to specify peer versions in package.json, but in a large monorepo - * this often creates worse problems. The reason is that peer dependency behavior - * is inherently complicated, and it is easier to troubleshoot consequences of an explicit - * version than an invisible heuristic. The original NPM RFC discussion pointed out - * some other problems with this feature: https://github.com/npm/rfcs/pull/43 - - * IMPORTANT: Without Rush, the setting defaults to true for PNPM 8 and newer; however, - * as of Rush version 5.109.0 the default is always false unless `autoInstallPeers` - * is specified in pnpm-config.json or .npmrc, regardless of your PNPM version. - - * PNPM documentation: https://pnpm.io/npmrc#auto-install-peers - - * The default value is false. - */ - // "autoInstallPeers": false, - - /** - * If true, then Rush will add the `--strict-peer-dependencies` command-line parameter when - * invoking PNPM. This causes `rush update` to fail if there are unsatisfied peer dependencies, - * which is an invalid state that can cause build failures or incompatible dependency versions. - * (For historical reasons, JavaScript package managers generally do not treat this invalid - * state as an error.) - * - * PNPM documentation: https://pnpm.io/npmrc#strict-peer-dependencies - * - * The default value is false to avoid legacy compatibility issues. - * It is strongly recommended to set `strictPeerDependencies=true`. - */ - // "strictPeerDependencies": true, - - /** - * Environment variables that will be provided to PNPM. - */ - // "environmentVariables": { - // "NODE_OPTIONS": { - // "value": "--max-old-space-size=4096", - // "override": false - // } - // }, - - /** - * Specifies the location of the PNPM store. There are two possible values: - * - * - `local` - use the `pnpm-store` folder in the current configured temp folder: - * `common/temp/pnpm-store` by default. - * - `global` - use PNPM's global store, which has the benefit of being shared - * across multiple repo folders, but the disadvantage of less isolation for builds - * (for example, bugs or incompatibilities when two repos use different releases of PNPM) - * - * In both cases, the store path can be overridden by the environment variable `RUSH_PNPM_STORE_PATH`. - * - * The default value is `local`. - */ - // "pnpmStore": "global", - - /** - * If true, then `rush install` will report an error if manual modifications - * were made to the PNPM shrinkwrap file without running `rush update` afterwards. - * - * This feature protects against accidental inconsistencies that may be introduced - * if the PNPM shrinkwrap file (`pnpm-lock.yaml`) is manually edited. When this - * feature is enabled, `rush update` will append a hash to the file as a YAML comment, - * and then `rush update` and `rush install` will validate the hash. Note that this - * does not prohibit manual modifications, but merely requires `rush update` be run - * afterwards, ensuring that PNPM can report or repair any potential inconsistencies. - * - * To temporarily disable this validation when invoking `rush install`, use the - * `--bypass-policy` command-line parameter. - * - * The default value is false. - */ - // "preventManualShrinkwrapChanges": true, - - /** - * When a project uses `workspace:` to depend on another Rush project, PNPM normally installs - * it by creating a symlink under `node_modules`. This generally works well, but in certain - * cases such as differing `peerDependencies` versions, symlinking may cause trouble - * such as incorrectly satisfied versions. For such cases, the dependency can be declared - * as "injected", causing PNPM to copy its built output into `node_modules` like a real - * install from a registry. Details here: https://rushjs.io/pages/advanced/injected_deps/ - * - * When using Rush subspaces, these sorts of versioning problems are much more likely if - * `workspace:` refers to a project from a different subspace. This is because the symlink - * would point to a separate `node_modules` tree installed by a different PNPM lockfile. - * A comprehensive solution is to enable `alwaysInjectDependenciesFromOtherSubspaces`, - * which automatically treats all projects from other subspaces as injected dependencies - * without having to manually configure them. - * - * NOTE: Use carefully -- excessive file copying can slow down the `rush install` and - * `pnpm-sync` operations if too many dependencies become injected. - * - * The default value is false. - */ - // "alwaysInjectDependenciesFromOtherSubspaces": false, - - /** - * Defines the policies to be checked for the `pnpm-lock.yaml` file. - */ - "pnpmLockfilePolicies": { - - /** - * This policy will cause "rush update" to report an error if `pnpm-lock.yaml` contains - * any SHA1 integrity hashes. - * - * For each NPM dependency, `pnpm-lock.yaml` normally stores an `integrity` hash. Although - * its main purpose is to detect corrupted or truncated network requests, this hash can also - * serve as a security fingerprint to protect against attacks that would substitute a - * malicious tarball, for example if a misconfigured .npmrc caused a machine to accidentally - * download a matching package name+version from npmjs.com instead of the private NPM registry. - * NPM originally used a SHA1 hash; this was insecure because an attacker can too easily craft - * a tarball with a matching fingerprint. For this reason, NPM later deprecated SHA1 and - * instead adopted a cryptographically strong SHA512 hash. Nonetheless, SHA1 hashes can - * occasionally reappear during "rush update", for example due to missing metadata fallbacks - * (https://github.com/orgs/pnpm/discussions/6194) or an incompletely migrated private registry. - * The `disallowInsecureSha1` policy prevents this, avoiding potential security/compliance alerts. - */ - // "disallowInsecureSha1": { - // /** - // * Enables the "disallowInsecureSha1" policy. The default value is false. - // */ - // "enabled": true, - // - // /** - // * In rare cases, a private NPM registry may continue to serve SHA1 hashes for very old - // * package versions, perhaps due to a caching issue or database migration glitch. To avoid - // * having to disable the "disallowInsecureSha1" policy for the entire monorepo, the problematic - // * package versions can be individually ignored. The "exemptPackageVersions" key is the - // * package name, and the array value lists exact version numbers to be ignored. - // */ - // "exemptPackageVersions": { - // "example1": ["1.0.0"], - // "example2": ["2.0.0", "2.0.1"] - // } - // } - }, - - /** - * The "globalOverrides" setting provides a simple mechanism for overriding version selections - * for all dependencies of all projects in the monorepo workspace. The settings are copied - * into the `pnpm.overrides` field of the `common/temp/package.json` file that is generated - * by Rush during installation. - * - * Order of precedence: `.pnpmfile.cjs` has the highest precedence, followed by - * `unsupportedPackageJsonSettings`, `globalPeerDependencyRules`, `globalPackageExtensions`, - * and `globalOverrides` has lowest precedence. - * - * PNPM documentation: https://pnpm.io/package_json#pnpmoverrides - */ - "globalOverrides": { - // "example1": "^1.0.0", - // "example2": "npm:@company/example2@^1.0.0" - }, - - /** - * The `globalPeerDependencyRules` setting provides various settings for suppressing validation errors - * that are reported during installation with `strictPeerDependencies=true`. The settings are copied - * into the `pnpm.peerDependencyRules` field of the `common/temp/package.json` file that is generated - * by Rush during installation. - * - * Order of precedence: `.pnpmfile.cjs` has the highest precedence, followed by - * `unsupportedPackageJsonSettings`, `globalPeerDependencyRules`, `globalPackageExtensions`, - * and `globalOverrides` has lowest precedence. - * - * https://pnpm.io/package_json#pnpmpeerdependencyrules - */ - "globalPeerDependencyRules": { - // "ignoreMissing": ["@eslint/*"], - // "allowedVersions": { "react": "17" }, - // "allowAny": ["@babel/*"] - }, - - /** - * The `globalPackageExtension` setting provides a way to patch arbitrary package.json fields - * for any PNPM dependency of the monorepo. The settings are copied into the `pnpm.packageExtensions` - * field of the `common/temp/package.json` file that is generated by Rush during installation. - * The `globalPackageExtension` setting has similar capabilities as `.pnpmfile.cjs` but without - * the downsides of an executable script (nondeterminism, unreliable caching, performance concerns). - * - * Order of precedence: `.pnpmfile.cjs` has the highest precedence, followed by - * `unsupportedPackageJsonSettings`, `globalPeerDependencyRules`, `globalPackageExtensions`, - * and `globalOverrides` has lowest precedence. - * - * PNPM documentation: https://pnpm.io/package_json#pnpmpackageextensions - */ - "globalPackageExtensions": { - // "fork-ts-checker-webpack-plugin": { - // "dependencies": { - // "@babel/core": "1" - // }, - // "peerDependencies": { - // "eslint": ">= 6" - // }, - // "peerDependenciesMeta": { - // "eslint": { - // "optional": true - // } - // } - // } - }, - - /** - * The `globalNeverBuiltDependencies` setting suppresses the `preinstall`, `install`, and `postinstall` - * lifecycle events for the specified NPM dependencies. This is useful for scripts with poor practices - * such as downloading large binaries without retries or attempting to invoke OS tools such as - * a C++ compiler. (PNPM's terminology refers to these lifecycle events as "building" a package; - * it has nothing to do with build system operations such as `rush build` or `rushx build`.) - * The settings are copied into the `pnpm.neverBuiltDependencies` field of the `common/temp/package.json` - * file that is generated by Rush during installation. - * - * PNPM documentation: https://pnpm.io/package_json#pnpmneverbuiltdependencies - */ - "globalNeverBuiltDependencies": [ - // "fsevents" - ], - - /** - * The `globalIgnoredOptionalDependencies` setting suppresses the installation of optional NPM - * dependencies specified in the list. This is useful when certain optional dependencies are - * not needed in your environment, such as platform-specific packages or dependencies that - * fail during installation but are not critical to your project. - * These settings are copied into the `pnpm.overrides` field of the `common/temp/package.json` - * file that is generated by Rush during installation, instructing PNPM to ignore the specified - * optional dependencies. - * - * PNPM documentation: https://pnpm.io/package_json#pnpmignoredoptionaldependencies - */ - "globalIgnoredOptionalDependencies": [ - // "fsevents" - ], - - /** - * The `globalAllowedDeprecatedVersions` setting suppresses installation warnings for package - * versions that the NPM registry reports as being deprecated. This is useful if the - * deprecated package is an indirect dependency of an external package that has not released a fix. - * The settings are copied into the `pnpm.allowedDeprecatedVersions` field of the `common/temp/package.json` - * file that is generated by Rush during installation. - * - * PNPM documentation: https://pnpm.io/package_json#pnpmalloweddeprecatedversions - * - * If you are working to eliminate a deprecated version, it's better to specify `allowedDeprecatedVersions` - * in the package.json file for individual Rush projects. - */ - "globalAllowedDeprecatedVersions": { - // "request": "*" - }, - - - /** - * (THIS FIELD IS MACHINE GENERATED) The "globalPatchedDependencies" field is updated automatically - * by the `rush-pnpm patch-commit` command. It is a dictionary, where the key is an NPM package name - * and exact version, and the value is a relative path to the associated patch file. - * - * PNPM documentation: https://pnpm.io/package_json#pnpmpatcheddependencies - */ - "globalPatchedDependencies": { }, - - /** - * (USE AT YOUR OWN RISK) This is a free-form property bag that will be copied into - * the `common/temp/package.json` file that is generated by Rush during installation. - * This provides a way to experiment with new PNPM features. These settings will override - * any other Rush configuration associated with a given JSON field except for `.pnpmfile.cjs`. - * - * USAGE OF THIS SETTING IS NOT SUPPORTED BY THE RUSH MAINTAINERS AND MAY CAUSE RUSH - * TO MALFUNCTION. If you encounter a missing PNPM setting that you believe should - * be supported, please create a GitHub issue or PR. Note that Rush does not aim to - * support every possible PNPM setting, but rather to promote a battle-tested installation - * strategy that is known to provide a good experience for large teams with lots of projects. - */ - "unsupportedPackageJsonSettings": { - // "dependencies": { - // "not-a-good-practice": "*" - // }, - // "scripts": { - // "do-something": "echo Also not a good practice" - // }, - // "pnpm": { "futurePnpmFeature": true } - } -} diff --git a/foundations/communication/common/config/rush/pnpm-lock.yaml b/foundations/communication/common/config/rush/pnpm-lock.yaml deleted file mode 100644 index 8182ddd7de..0000000000 --- a/foundations/communication/common/config/rush/pnpm-lock.yaml +++ /dev/null @@ -1,6444 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: false - excludeLinksFromLockfile: false - -pnpmfileChecksum: avk5jcsa6uxo2n5cahoirjiuw4 - -importers: - - .: {} - - ../../packages/client-query: - dependencies: - '@hcengineering/communication-query': - specifier: workspace:^0.7.11 - version: link:../query - '@hcengineering/communication-sdk-types': - specifier: workspace:^0.7.12 - version: link:../sdk-types - '@hcengineering/communication-types': - specifier: workspace:^0.7.12 - version: link:../types - '@hcengineering/hulylake-client': - specifier: ^0.7.17 - version: 0.7.17 - fast-equals: - specifier: ^5.2.2 - version: 5.3.2 - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6))(postcss@8.5.6) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - esbuild: - specifier: ^0.25.9 - version: 0.25.10 - esbuild-plugin-copy: - specifier: ~2.1.1 - version: 2.1.1(esbuild@0.25.10) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10) - prettier: - specifier: ^3.1.0 - version: 3.6.2 - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - ../../packages/cockroach: - dependencies: - '@hcengineering/communication-sdk-types': - specifier: workspace:^0.7.12 - version: link:../sdk-types - '@hcengineering/communication-shared': - specifier: workspace:^0.7.11 - version: link:../shared - '@hcengineering/communication-types': - specifier: workspace:^0.7.12 - version: link:../types - postgres: - specifier: ^3.4.7 - version: 3.4.7 - uuid: - specifier: ^8.3.2 - version: 8.3.2 - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6))(postcss@8.5.6) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@types/node': - specifier: ^22.15.29 - version: 22.18.10 - '@types/uuid': - specifier: ^8.3.1 - version: 8.3.4 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - esbuild: - specifier: ^0.25.9 - version: 0.25.10 - esbuild-plugin-copy: - specifier: ~2.1.1 - version: 2.1.1(esbuild@0.25.10) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10) - prettier: - specifier: ^3.1.0 - version: 3.6.2 - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - ../../packages/query: - dependencies: - '@hcengineering/communication-sdk-types': - specifier: workspace:^0.7.12 - version: link:../sdk-types - '@hcengineering/communication-shared': - specifier: workspace:^0.7.11 - version: link:../shared - '@hcengineering/communication-types': - specifier: workspace:^0.7.12 - version: link:../types - '@hcengineering/hulylake-client': - specifier: ^0.7.17 - version: 0.7.17 - fast-equals: - specifier: ^5.2.2 - version: 5.3.2 - uuid: - specifier: ^8.3.2 - version: 8.3.2 - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6))(postcss@8.5.6) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@types/uuid': - specifier: ^8.3.1 - version: 8.3.4 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - esbuild: - specifier: ^0.25.9 - version: 0.25.10 - esbuild-plugin-copy: - specifier: ~2.1.1 - version: 2.1.1(esbuild@0.25.10) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10) - prettier: - specifier: ^3.1.0 - version: 3.6.2 - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - ../../packages/rest-client: - dependencies: - '@hcengineering/communication-sdk-types': - specifier: workspace:^0.7.12 - version: link:../sdk-types - '@hcengineering/communication-shared': - specifier: workspace:^0.7.11 - version: link:../shared - '@hcengineering/communication-types': - specifier: workspace:^0.7.12 - version: link:../types - '@hcengineering/core': - specifier: ^0.7.19 - version: 0.7.19 - snappyjs: - specifier: ^0.7.0 - version: 0.7.0 - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6))(postcss@8.5.6) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@types/snappyjs': - specifier: ^0.7.1 - version: 0.7.1 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - esbuild: - specifier: ^0.25.9 - version: 0.25.10 - esbuild-plugin-copy: - specifier: ~2.1.1 - version: 2.1.1(esbuild@0.25.10) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10) - prettier: - specifier: ^3.1.0 - version: 3.6.2 - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - ../../packages/sdk-types: - dependencies: - '@hcengineering/communication-types': - specifier: workspace:^0.7.12 - version: link:../types - '@hcengineering/core': - specifier: ^0.7.19 - version: 0.7.19 - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6))(postcss@8.5.6) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - esbuild: - specifier: ^0.25.9 - version: 0.25.10 - esbuild-plugin-copy: - specifier: ~2.1.1 - version: 2.1.1(esbuild@0.25.10) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10) - prettier: - specifier: ^3.1.0 - version: 3.6.2 - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - ../../packages/server: - dependencies: - '@hcengineering/account-client': - specifier: ^0.7.18 - version: 0.7.18 - '@hcengineering/communication-cockroach': - specifier: workspace:^0.7.11 - version: link:../cockroach - '@hcengineering/communication-sdk-types': - specifier: workspace:^0.7.12 - version: link:../sdk-types - '@hcengineering/communication-shared': - specifier: workspace:^0.7.11 - version: link:../shared - '@hcengineering/communication-types': - specifier: workspace:^0.7.12 - version: link:../types - '@hcengineering/core': - specifier: ^0.7.19 - version: 0.7.19 - '@hcengineering/hulylake-client': - specifier: ^0.7.17 - version: 0.7.17 - '@hcengineering/server-token': - specifier: ^0.7.17 - version: 0.7.17 - '@hcengineering/text-core': - specifier: ^0.7.18 - version: 0.7.18 - '@hcengineering/text-markdown': - specifier: ^0.7.18 - version: 0.7.18 - uuid: - specifier: ^8.3.2 - version: 8.3.2 - zod: - specifier: ^3.22.4 - version: 3.25.76 - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6))(postcss@8.5.6) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@types/node': - specifier: ^22.15.29 - version: 22.18.10 - '@types/uuid': - specifier: ^8.3.1 - version: 8.3.4 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - esbuild: - specifier: ^0.25.9 - version: 0.25.10 - esbuild-plugin-copy: - specifier: ~2.1.1 - version: 2.1.1(esbuild@0.25.10) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10) - prettier: - specifier: ^3.1.0 - version: 3.6.2 - ts-jest: - specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.10)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10))(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - ../../packages/shared: - dependencies: - '@hcengineering/communication-sdk-types': - specifier: workspace:^0.7.12 - version: link:../sdk-types - '@hcengineering/communication-types': - specifier: workspace:^0.7.12 - version: link:../types - '@hcengineering/hulylake-client': - specifier: ^0.7.17 - version: 0.7.17 - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6))(postcss@8.5.6) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - esbuild: - specifier: ^0.25.9 - version: 0.25.10 - esbuild-plugin-copy: - specifier: ~2.1.1 - version: 2.1.1(esbuild@0.25.10) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10) - prettier: - specifier: ^3.1.0 - version: 3.6.2 - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - ../../packages/types: - dependencies: - '@hcengineering/core': - specifier: ^0.7.19 - version: 0.7.19 - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6))(postcss@8.5.6) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - esbuild: - specifier: ^0.25.9 - version: 0.25.10 - esbuild-plugin-copy: - specifier: ~2.1.1 - version: 2.1.1(esbuild@0.25.10) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10) - prettier: - specifier: ^3.1.0 - version: 3.6.2 - typescript: - specifier: ^5.8.3 - version: 5.9.3 - - ../scripts: - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6))(postcss@8.5.6) - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - esbuild: - specifier: ^0.24.2 - version: 0.24.2 - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - -packages: - - '@ampproject/remapping@2.3.0': - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} - - '@babel/code-frame@7.27.1': - resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} - engines: {node: '>=6.9.0'} - - '@babel/compat-data@7.28.4': - resolution: {integrity: sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.28.4': - resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.28.3': - resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-compilation-targets@7.27.2': - resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-globals@7.28.0': - resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-imports@7.27.1': - resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-transforms@7.28.3': - resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-plugin-utils@7.27.1': - resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.27.1': - resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-option@7.27.1': - resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} - engines: {node: '>=6.9.0'} - - '@babel/helpers@7.28.4': - resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.28.4': - resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/plugin-syntax-async-generators@7.8.4': - resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-bigint@7.8.3': - resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-class-properties@7.12.13': - resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-class-static-block@7.14.5': - resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-import-attributes@7.27.1': - resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-import-meta@7.10.4': - resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-json-strings@7.8.3': - resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-jsx@7.27.1': - resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-logical-assignment-operators@7.10.4': - resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': - resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-numeric-separator@7.10.4': - resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-object-rest-spread@7.8.3': - resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-optional-catch-binding@7.8.3': - resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-optional-chaining@7.8.3': - resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-private-property-in-object@7.14.5': - resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-top-level-await@7.14.5': - resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-typescript@7.27.1': - resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/template@7.27.2': - resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.28.4': - resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.28.4': - resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} - engines: {node: '>=6.9.0'} - - '@bcoe/v8-coverage@0.2.3': - resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - - '@esbuild/aix-ppc64@0.24.2': - resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/aix-ppc64@0.25.10': - resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.24.2': - resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm64@0.25.10': - resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.24.2': - resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-arm@0.25.10': - resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.24.2': - resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/android-x64@0.25.10': - resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.24.2': - resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-arm64@0.25.10': - resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.24.2': - resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/darwin-x64@0.25.10': - resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.24.2': - resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-arm64@0.25.10': - resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.24.2': - resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.25.10': - resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.24.2': - resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm64@0.25.10': - resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.24.2': - resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-arm@0.25.10': - resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.24.2': - resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-ia32@0.25.10': - resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.24.2': - resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-loong64@0.25.10': - resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.24.2': - resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-mips64el@0.25.10': - resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.24.2': - resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-ppc64@0.25.10': - resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.24.2': - resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-riscv64@0.25.10': - resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.24.2': - resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-s390x@0.25.10': - resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.24.2': - resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/linux-x64@0.25.10': - resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.24.2': - resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-arm64@0.25.10': - resolution: {integrity: sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.24.2': - resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.25.10': - resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.24.2': - resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-arm64@0.25.10': - resolution: {integrity: sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.24.2': - resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.25.10': - resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openharmony-arm64@0.25.10': - resolution: {integrity: sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/sunos-x64@0.24.2': - resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/sunos-x64@0.25.10': - resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.24.2': - resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-arm64@0.25.10': - resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.24.2': - resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-ia32@0.25.10': - resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.24.2': - resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@esbuild/win32-x64@0.25.10': - resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@eslint-community/eslint-utils@4.9.0': - resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - '@eslint-community/regexpp@4.12.1': - resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - '@eslint/eslintrc@2.1.4': - resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - '@eslint/js@8.57.1': - resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - '@formatjs/ecma402-abstract@2.3.5': - resolution: {integrity: sha512-1HTESOq1IUa23g1lFZEGIXsfZKZOwWmB9RROwGn+xariiQnd++wwTMvlRAbZ8wtXRHFUamJPxsKcxpSzeCvFWQ==} - - '@formatjs/fast-memoize@2.2.7': - resolution: {integrity: sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==} - - '@formatjs/icu-messageformat-parser@2.11.3': - resolution: {integrity: sha512-H/KfWSosaiDiOaW4nHe1Fn4Cgzm+oFQ8giTmB5RJzTBNSMmd+j2NVrvvZHAmlxJHcuOelzKBLjQ2EDcyH4NSWw==} - - '@formatjs/icu-skeleton-parser@1.8.15': - resolution: {integrity: sha512-qNrKxWJmnWxin5U4A4Evy7C0rgRiNw3IqXu9OGuT31B8lDxBGl+OgT8kcq0ZVKK0gqA4l4SQB9x+SFAvLT5hcQ==} - - '@formatjs/intl-localematcher@0.6.2': - resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==} - - '@hcengineering/account-client@0.7.18': - resolution: {integrity: sha512-o7iU2cx2rRtkv3ntvbMaw4f8Wk6of7Cot+y6MAsHXTlELLStcKjCQcLRj7abne0sdN/N3luaNVkJBqfu4JpHHw==} - - '@hcengineering/analytics@0.7.17': - resolution: {integrity: sha512-AzB2DvmDctEJge8VeAHS3YyRYij/QQlARxa2OCNiY+Vv+2+OsQk0NZsw1yqZqsMkVJ40FTUrzOkI7UdM6RPO1Q==} - - '@hcengineering/core@0.7.19': - resolution: {integrity: sha512-BMkVkVXMU8qmbR2KAeMZ3YIMfa07/AzdCEY7PNn7dfAZyc63vrLz+Jl0CPvN7cq9p6x/WBkMfS+eKFwtvhZIUA==} - - '@hcengineering/hulylake-client@0.7.17': - resolution: {integrity: sha512-TPqdc5nd7xV73Y1B2EJ7GxpWazp9ACV1AqHlm6ebYwU1CzXQvbuJxF68itbHmPiNiCmmuiDruJmjnyLGwL18Kw==} - - '@hcengineering/measurements@0.7.18': - resolution: {integrity: sha512-4MxYthJhNWPfR389lN5cIFtrhzgmhi39Fx/dqIOOYF+6g0WWA6GtWXi+ORRM8l/AWKSastZqsWxMdBbT+0htdg==} - - '@hcengineering/platform-rig@0.7.19': - resolution: {integrity: sha512-3Fi5nU+nEPjzzcm3FIfEZU+ZgdkiSfL46ojbj42jdXnWMkxew2aZZVtfmIpPnMUP9AMZlj2/EA+yOcMzfY/VmA==} - hasBin: true - - '@hcengineering/platform@0.7.17': - resolution: {integrity: sha512-PJH38bQ3yZgP52vHV7r8Y+JkCuH0nV9kic/CNt4XuNXQp9nTjzBlDnl+1IrCo6hU+x3QweKVIEzMXzU9J4hhEA==} - - '@hcengineering/retry@0.7.17': - resolution: {integrity: sha512-JPIGoWP3RHTET0laQV4rqlNxjn3tT9O54XNxCxsxhTFF5xeC02XxmW/xV4Gh6QWE5/QCXY5DD3Sz3ild3Cr7Uw==} - - '@hcengineering/server-token@0.7.17': - resolution: {integrity: sha512-MDoGzVOTScJXnx2jHL63szclhiqPOKEXk7bmUj6i/HLUQoU6UAl9TTq1Zx/TomyzFQUx8Vkuj0j0bJMUcmLXmQ==} - - '@hcengineering/text-core@0.7.18': - resolution: {integrity: sha512-UZ/ZMwVcf60WpnYYcsA7RqYE4JlHnAGVdmUDHX2SYzXXvDIK7deGWD9AKyK4yqI1mb19ccyCr5fGyGoLxx9k/g==} - - '@hcengineering/text-html@0.7.18': - resolution: {integrity: sha512-hGPgptTk2Kfvlg8lAniaFe8jpf3RD4mZD0eL6CoPlXTKylUOklOGHFGWId+QjUYGPLBVkODPErmZxrZKbkRSWg==} - - '@hcengineering/text-markdown@0.7.18': - resolution: {integrity: sha512-GEslsa/gPp5NH+VPg/G8vOVzt9XdEUr8myoFDBz0LGlQVs6veAEv1psUdOynKeHV/ntF3qmN3hSHg0LrMyoGfA==} - - '@humanwhocodes/config-array@0.13.0': - resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} - engines: {node: '>=10.10.0'} - deprecated: Use @eslint/config-array instead - - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - - '@humanwhocodes/object-schema@2.0.3': - resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} - deprecated: Use @eslint/object-schema instead - - '@istanbuljs/load-nyc-config@1.1.0': - resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} - engines: {node: '>=8'} - - '@istanbuljs/schema@0.1.3': - resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} - engines: {node: '>=8'} - - '@jest/console@29.7.0': - resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/core@29.7.0': - resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - - '@jest/environment@29.7.0': - resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/expect-utils@29.7.0': - resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/expect@29.7.0': - resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/fake-timers@29.7.0': - resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/globals@29.7.0': - resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/reporters@29.7.0': - resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - - '@jest/schemas@29.6.3': - resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/source-map@29.6.3': - resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/test-result@29.7.0': - resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/test-sequencer@29.7.0': - resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/transform@29.7.0': - resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/types@29.6.3': - resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jridgewell/gen-mapping@0.3.13': - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - - '@rtsao/scc@1.1.0': - resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - - '@sinclair/typebox@0.27.8': - resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} - - '@sinonjs/commons@3.0.1': - resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} - - '@sinonjs/fake-timers@10.3.0': - resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} - - '@types/babel__core@7.20.5': - resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} - - '@types/babel__generator@7.27.0': - resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} - - '@types/babel__template@7.4.4': - resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} - - '@types/babel__traverse@7.28.0': - resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} - - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - - '@types/graceful-fs@4.1.9': - resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} - - '@types/istanbul-lib-coverage@2.0.6': - resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} - - '@types/istanbul-lib-report@3.0.3': - resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} - - '@types/istanbul-reports@3.0.4': - resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} - - '@types/jest@29.5.14': - resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} - - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - - '@types/json5@0.0.29': - resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - - '@types/node@22.18.10': - resolution: {integrity: sha512-anNG/V/Efn/YZY4pRzbACnKxNKoBng2VTFydVu8RRs5hQjikP8CQfaeAV59VFSCzKNp90mXiVXW2QzV56rwMrg==} - - '@types/pug@2.0.10': - resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==} - - '@types/semver@7.7.1': - resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} - - '@types/snappyjs@0.7.1': - resolution: {integrity: sha512-OxjzJ6cQZstysMh6PEwZWmK9qlKZyezHJKOkcUkZDooSFuog2votUEKkxMaTq51UQF3cJkXKQ+XGlj4FSl8JQQ==} - - '@types/stack-utils@2.0.3': - resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} - - '@types/uuid@8.3.4': - resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==} - - '@types/yargs-parser@21.0.3': - resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} - - '@types/yargs@17.0.33': - resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} - - '@typescript-eslint/eslint-plugin@6.21.0': - resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/parser@6.21.0': - resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/scope-manager@6.21.0': - resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} - engines: {node: ^16.0.0 || >=18.0.0} - - '@typescript-eslint/type-utils@6.21.0': - resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/types@6.21.0': - resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} - engines: {node: ^16.0.0 || >=18.0.0} - - '@typescript-eslint/typescript-estree@6.21.0': - resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/utils@6.21.0': - resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - - '@typescript-eslint/visitor-keys@6.21.0': - resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} - engines: {node: ^16.0.0 || >=18.0.0} - - '@ungap/structured-clone@1.3.0': - resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - - acorn@8.15.0: - resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} - engines: {node: '>=0.4.0'} - hasBin: true - - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - - ansi-escapes@4.3.2: - resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} - engines: {node: '>=8'} - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - - argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - - aria-query@5.3.2: - resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} - engines: {node: '>= 0.4'} - - array-buffer-byte-length@1.0.2: - resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} - engines: {node: '>= 0.4'} - - array-includes@3.1.9: - resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} - engines: {node: '>= 0.4'} - - array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} - - array.prototype.findlastindex@1.2.6: - resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} - engines: {node: '>= 0.4'} - - array.prototype.flat@1.3.3: - resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} - engines: {node: '>= 0.4'} - - array.prototype.flatmap@1.3.3: - resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} - engines: {node: '>= 0.4'} - - arraybuffer.prototype.slice@1.0.4: - resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} - engines: {node: '>= 0.4'} - - async-function@1.0.0: - resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} - engines: {node: '>= 0.4'} - - available-typed-arrays@1.0.7: - resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} - engines: {node: '>= 0.4'} - - axobject-query@4.1.0: - resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} - engines: {node: '>= 0.4'} - - babel-jest@29.7.0: - resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@babel/core': ^7.8.0 - - babel-plugin-istanbul@6.1.1: - resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} - engines: {node: '>=8'} - - babel-plugin-jest-hoist@29.6.3: - resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - babel-preset-current-node-syntax@1.2.0: - resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} - peerDependencies: - '@babel/core': ^7.0.0 || ^8.0.0-0 - - babel-preset-jest@29.6.3: - resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@babel/core': ^7.0.0 - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - baseline-browser-mapping@2.8.12: - resolution: {integrity: sha512-vAPMQdnyKCBtkmQA6FMCBvU9qFIppS3nzyXnEM+Lo2IAhG4Mpjv9cCxMudhgV3YdNNJv6TNqXy97dfRVL2LmaQ==} - hasBin: true - - binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} - - brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - - brace-expansion@2.0.2: - resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - - browserslist@4.26.3: - resolution: {integrity: sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - bs-logger@0.2.6: - resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} - engines: {node: '>= 6'} - - bser@2.1.1: - resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} - - buffer-crc32@1.0.0: - resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} - engines: {node: '>=8.0.0'} - - buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - - builtins@5.1.0: - resolution: {integrity: sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==} - - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - - call-bind@1.0.8: - resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} - engines: {node: '>= 0.4'} - - call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} - - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - camelcase@5.3.1: - resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} - engines: {node: '>=6'} - - camelcase@6.3.0: - resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} - engines: {node: '>=10'} - - caniuse-lite@1.0.30001748: - resolution: {integrity: sha512-5P5UgAr0+aBmNiplks08JLw+AW/XG/SurlgZLgB1dDLfAw7EfRGxIwzPHxdSCGY/BTKDqIVyJL87cCN6s0ZR0w==} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - char-regex@1.0.2: - resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} - engines: {node: '>=10'} - - chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} - - ci-info@3.9.0: - resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} - engines: {node: '>=8'} - - cjs-module-lexer@1.4.3: - resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} - - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - - co@4.6.0: - resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} - engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} - - code-red@1.0.4: - resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==} - - collect-v8-coverage@1.0.2: - resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - - create-jest@29.7.0: - resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - - css-tree@2.3.1: - resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} - engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} - - cssesc@3.0.0: - resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} - engines: {node: '>=4'} - hasBin: true - - data-view-buffer@1.0.2: - resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} - engines: {node: '>= 0.4'} - - data-view-byte-length@1.0.2: - resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} - engines: {node: '>= 0.4'} - - data-view-byte-offset@1.0.1: - resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} - engines: {node: '>= 0.4'} - - debug@3.2.7: - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - decimal.js@10.6.0: - resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} - - dedent-js@1.0.1: - resolution: {integrity: sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==} - - dedent@1.7.0: - resolution: {integrity: sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==} - peerDependencies: - babel-plugin-macros: ^3.1.0 - peerDependenciesMeta: - babel-plugin-macros: - optional: true - - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - - deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} - - define-data-property@1.1.4: - resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} - engines: {node: '>= 0.4'} - - define-properties@1.2.1: - resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} - engines: {node: '>= 0.4'} - - detect-indent@6.1.0: - resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} - engines: {node: '>=8'} - - detect-newline@3.1.0: - resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} - engines: {node: '>=8'} - - diff-sequences@29.6.3: - resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} - - doctrine@2.1.0: - resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} - engines: {node: '>=0.10.0'} - - doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} - - dom-serializer@2.0.0: - resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} - - domelementtype@2.3.0: - resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - - domhandler@5.0.3: - resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} - engines: {node: '>= 4'} - - domutils@3.2.2: - resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} - - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - - electron-to-chromium@1.5.232: - resolution: {integrity: sha512-ENirSe7wf8WzyPCibqKUG1Cg43cPaxH4wRR7AJsX7MCABCHBIOFqvaYODSLKUuZdraxUTHRE/0A2Aq8BYKEHOg==} - - emittery@0.13.1: - resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} - engines: {node: '>=12'} - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} - - error-ex@1.3.4: - resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} - - es-abstract@1.24.0: - resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} - engines: {node: '>= 0.4'} - - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - - es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} - engines: {node: '>= 0.4'} - - es-set-tostringtag@2.1.0: - resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} - engines: {node: '>= 0.4'} - - es-shim-unscopables@1.1.0: - resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} - engines: {node: '>= 0.4'} - - es-to-primitive@1.3.0: - resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} - engines: {node: '>= 0.4'} - - es6-promise@3.3.1: - resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} - - esbuild-plugin-copy@2.1.1: - resolution: {integrity: sha512-Bk66jpevTcV8KMFzZI1P7MZKZ+uDcrZm2G2egZ2jNIvVnivDpodZI+/KnpL3Jnap0PBdIHU7HwFGB8r+vV5CVw==} - peerDependencies: - esbuild: '>= 0.14.0' - - esbuild-svelte@0.9.3: - resolution: {integrity: sha512-CgEcGY1r/d16+aggec3czoFBEBaYIrFOnMxpsO6fWNaNEqHregPN5DLAPZDqrL7rXDNplW+WMu8s3GMq9FqgJA==} - engines: {node: '>=18'} - peerDependencies: - esbuild: '>=0.17.0' - svelte: '>=4.2.1 <6' - - esbuild@0.24.2: - resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} - engines: {node: '>=18'} - hasBin: true - - esbuild@0.25.10: - resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==} - engines: {node: '>=18'} - hasBin: true - - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - - escape-string-regexp@2.0.0: - resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} - engines: {node: '>=8'} - - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - - eslint-compat-utils@0.5.1: - resolution: {integrity: sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==} - engines: {node: '>=12'} - peerDependencies: - eslint: '>=6.0.0' - - eslint-config-standard-with-typescript@40.0.0: - resolution: {integrity: sha512-GXUJcwIXiTQaS3H4etv8a1lejVVdZYaxZNz3g7vt6GoJosQqMTurbmSC4FVGyHiGT/d1TjFr3+47A3xsHhsG+Q==} - deprecated: Please use eslint-config-love, instead. - peerDependencies: - '@typescript-eslint/eslint-plugin': ^6.4.0 - eslint: ^8.0.1 - eslint-plugin-import: ^2.25.2 - eslint-plugin-n: '^15.0.0 || ^16.0.0 ' - eslint-plugin-promise: ^6.0.0 - typescript: '*' - - eslint-config-standard@17.1.0: - resolution: {integrity: sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==} - engines: {node: '>=12.0.0'} - peerDependencies: - eslint: ^8.0.1 - eslint-plugin-import: ^2.25.2 - eslint-plugin-n: '^15.0.0 || ^16.0.0 ' - eslint-plugin-promise: ^6.0.0 - - eslint-import-resolver-node@0.3.9: - resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} - - eslint-module-utils@2.12.1: - resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} - engines: {node: '>=4'} - peerDependencies: - eslint: '*' - peerDependenciesMeta: - eslint: - optional: true - - eslint-plugin-es@4.1.0: - resolution: {integrity: sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==} - engines: {node: '>=8.10.0'} - peerDependencies: - eslint: '>=4.19.1' - - eslint-plugin-import@2.32.0: - resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} - engines: {node: '>=4'} - peerDependencies: - eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 - - eslint-plugin-n@15.7.0: - resolution: {integrity: sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==} - engines: {node: '>=12.22.0'} - peerDependencies: - eslint: '>=7.0.0' - - eslint-plugin-promise@6.6.0: - resolution: {integrity: sha512-57Zzfw8G6+Gq7axm2Pdo3gW/Rx3h9Yywgn61uE/3elTCOePEHVrn2i5CdfBwA1BLK0Q0WqctICIUSqXZW/VprQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 - - eslint-plugin-svelte@2.46.1: - resolution: {integrity: sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw==} - engines: {node: ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0-0 || ^9.0.0-0 - svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 - peerDependenciesMeta: - svelte: - optional: true - - eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-utils@2.1.0: - resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==} - engines: {node: '>=6'} - - eslint-utils@3.0.0: - resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} - engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} - peerDependencies: - eslint: '>=5' - - eslint-visitor-keys@1.3.0: - resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} - engines: {node: '>=4'} - - eslint-visitor-keys@2.1.0: - resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} - engines: {node: '>=10'} - - eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint@8.57.1: - resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. - hasBin: true - - espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - - esquery@1.6.0: - resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} - engines: {node: '>=0.10'} - - esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - - execa@5.1.1: - resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} - engines: {node: '>=10'} - - exit@0.1.2: - resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} - engines: {node: '>= 0.8.0'} - - expect@29.7.0: - resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-equals@5.3.2: - resolution: {integrity: sha512-6rxyATwPCkaFIL3JLqw8qXqMpIZ942pTX/tbQFkRsDGblS8tNGtlUauA/+mt6RUfqn/4MoEr+WDkYoIQbibWuQ==} - engines: {node: '>=6.0.0'} - - fast-glob@3.3.3: - resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} - engines: {node: '>=8.6.0'} - - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - - fastq@1.19.1: - resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} - - fb-watchman@2.0.2: - resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} - - file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} - - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - - find-up@4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} - engines: {node: '>=8'} - - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - - flat-cache@3.2.0: - resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} - engines: {node: ^10.12.0 || >=12.0.0} - - flatted@3.3.3: - resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - - for-each@0.3.5: - resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} - engines: {node: '>= 0.4'} - - fs-extra@10.1.0: - resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} - engines: {node: '>=12'} - - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - function.prototype.name@1.1.8: - resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} - engines: {node: '>= 0.4'} - - functions-have-names@1.2.3: - resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - - generator-function@2.0.1: - resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} - engines: {node: '>= 0.4'} - - gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} - - get-package-type@0.1.0: - resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} - engines: {node: '>=8.0.0'} - - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} - - get-stream@6.0.1: - resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} - engines: {node: '>=10'} - - get-symbol-description@1.1.0: - resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} - engines: {node: '>= 0.4'} - - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - - glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported - - globals@13.24.0: - resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} - engines: {node: '>=8'} - - globalthis@1.0.4: - resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} - engines: {node: '>= 0.4'} - - globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} - - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - - graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - - handlebars@4.7.8: - resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} - engines: {node: '>=0.4.7'} - hasBin: true - - has-bigints@1.1.0: - resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} - engines: {node: '>= 0.4'} - - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - - has-property-descriptors@1.0.2: - resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} - - has-proto@1.2.0: - resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} - engines: {node: '>= 0.4'} - - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - - has-tostringtag@1.0.2: - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} - engines: {node: '>= 0.4'} - - hash-it@6.0.0: - resolution: {integrity: sha512-KHzmSFx1KwyMPw0kXeeUD752q/Kfbzhy6dAZrjXV9kAIXGqzGvv8vhkUqj+2MGZldTo0IBpw6v7iWE7uxsvH0w==} - - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - - html-escaper@2.0.2: - resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - - htmlparser2@9.1.0: - resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} - - human-signals@2.1.0: - resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} - engines: {node: '>=10.17.0'} - - ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} - engines: {node: '>= 4'} - - import-fresh@3.3.1: - resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} - engines: {node: '>=6'} - - import-local@3.2.0: - resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} - engines: {node: '>=8'} - hasBin: true - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - internal-slot@1.1.0: - resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} - engines: {node: '>= 0.4'} - - intl-messageformat@10.7.17: - resolution: {integrity: sha512-0Ugaf65B2J76rb31drgNF1l6bGEDkbIiYc2Glx6jaZINHnwa5kDRGy8KXYuA+/8P4G0c9prAFhfVhQJJfzUuvQ==} - - is-array-buffer@3.0.5: - resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} - engines: {node: '>= 0.4'} - - is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - - is-async-function@2.1.1: - resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} - engines: {node: '>= 0.4'} - - is-bigint@1.1.0: - resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} - engines: {node: '>= 0.4'} - - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - - is-boolean-object@1.2.2: - resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} - engines: {node: '>= 0.4'} - - is-callable@1.2.7: - resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} - engines: {node: '>= 0.4'} - - is-core-module@2.16.1: - resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} - engines: {node: '>= 0.4'} - - is-data-view@1.0.2: - resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} - engines: {node: '>= 0.4'} - - is-date-object@1.1.0: - resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} - engines: {node: '>= 0.4'} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-finalizationregistry@1.1.1: - resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} - engines: {node: '>= 0.4'} - - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - - is-generator-fn@2.1.0: - resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} - engines: {node: '>=6'} - - is-generator-function@1.1.2: - resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} - engines: {node: '>= 0.4'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - is-map@2.0.3: - resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} - engines: {node: '>= 0.4'} - - is-negative-zero@2.0.3: - resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} - engines: {node: '>= 0.4'} - - is-number-object@1.1.1: - resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} - engines: {node: '>= 0.4'} - - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - - is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - - is-reference@3.0.3: - resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} - - is-regex@1.2.1: - resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} - engines: {node: '>= 0.4'} - - is-set@2.0.3: - resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} - engines: {node: '>= 0.4'} - - is-shared-array-buffer@1.0.4: - resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} - engines: {node: '>= 0.4'} - - is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - - is-string@1.1.1: - resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} - engines: {node: '>= 0.4'} - - is-symbol@1.1.1: - resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} - engines: {node: '>= 0.4'} - - is-typed-array@1.1.15: - resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} - engines: {node: '>= 0.4'} - - is-weakmap@2.0.2: - resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} - engines: {node: '>= 0.4'} - - is-weakref@1.1.1: - resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} - engines: {node: '>= 0.4'} - - is-weakset@2.0.4: - resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} - engines: {node: '>= 0.4'} - - isarray@2.0.5: - resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - istanbul-lib-coverage@3.2.2: - resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} - engines: {node: '>=8'} - - istanbul-lib-instrument@5.2.1: - resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} - engines: {node: '>=8'} - - istanbul-lib-instrument@6.0.3: - resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} - engines: {node: '>=10'} - - istanbul-lib-report@3.0.1: - resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} - engines: {node: '>=10'} - - istanbul-lib-source-maps@4.0.1: - resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} - engines: {node: '>=10'} - - istanbul-reports@3.2.0: - resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} - engines: {node: '>=8'} - - jest-changed-files@29.7.0: - resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-circus@29.7.0: - resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-cli@29.7.0: - resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - - jest-config@29.7.0: - resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@types/node': '*' - ts-node: '>=9.0.0' - peerDependenciesMeta: - '@types/node': - optional: true - ts-node: - optional: true - - jest-diff@29.7.0: - resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-docblock@29.7.0: - resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-each@29.7.0: - resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-environment-node@29.7.0: - resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-get-type@29.6.3: - resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-haste-map@29.7.0: - resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-leak-detector@29.7.0: - resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-matcher-utils@29.7.0: - resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-message-util@29.7.0: - resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-mock@29.7.0: - resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-pnp-resolver@1.2.3: - resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} - engines: {node: '>=6'} - peerDependencies: - jest-resolve: '*' - peerDependenciesMeta: - jest-resolve: - optional: true - - jest-regex-util@29.6.3: - resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-resolve-dependencies@29.7.0: - resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-resolve@29.7.0: - resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-runner@29.7.0: - resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-runtime@29.7.0: - resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-snapshot@29.7.0: - resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-util@29.7.0: - resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-validate@29.7.0: - resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-watcher@29.7.0: - resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-worker@29.7.0: - resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest@29.7.0: - resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - js-yaml@3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} - hasBin: true - - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - - jsesc@3.1.0: - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} - engines: {node: '>=6'} - hasBin: true - - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - - json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - - json5@1.0.2: - resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} - hasBin: true - - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - - jsonfile@6.2.0: - resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} - - jwt-simple@0.5.6: - resolution: {integrity: sha512-40aUybvhH9t2h71ncA1/1SbtTNCVZHgsTsTgqPUxGWDmUDrXyDf2wMNQKEbdBjbf4AI+fQhbECNTV6lWxQKUzg==} - engines: {node: '>= 0.4.0'} - - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - - kleur@3.0.3: - resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} - engines: {node: '>=6'} - - known-css-properties@0.35.0: - resolution: {integrity: sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==} - - leven@3.1.0: - resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} - engines: {node: '>=6'} - - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - - lilconfig@2.1.0: - resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} - engines: {node: '>=10'} - - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - - linkify-it@5.0.0: - resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} - - locate-character@3.0.0: - resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} - - locate-path@5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} - engines: {node: '>=8'} - - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - - lodash.memoize@4.1.2: - resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} - - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - - lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - - magic-string@0.30.19: - resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} - - make-dir@4.0.0: - resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} - engines: {node: '>=10'} - - make-error@1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - - makeerror@1.0.12: - resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} - - markdown-it@14.1.0: - resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} - hasBin: true - - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - - mdn-data@2.0.30: - resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} - - mdurl@2.0.0: - resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} - - merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} - - mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} - - min-indent@1.0.1: - resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} - engines: {node: '>=4'} - - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - - minimatch@9.0.3: - resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} - engines: {node: '>=16 || 14 >=14.17'} - - minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - - mkdirp@0.5.6: - resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} - hasBin: true - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - - neo-async@2.6.2: - resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - - node-int64@0.4.0: - resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - - node-releases@2.0.23: - resolution: {integrity: sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==} - - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - - npm-run-path@4.0.1: - resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} - engines: {node: '>=8'} - - object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} - - object-keys@1.1.1: - resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} - engines: {node: '>= 0.4'} - - object.assign@4.1.7: - resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} - engines: {node: '>= 0.4'} - - object.fromentries@2.0.8: - resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} - engines: {node: '>= 0.4'} - - object.groupby@1.0.3: - resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} - engines: {node: '>= 0.4'} - - object.values@1.2.1: - resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} - engines: {node: '>= 0.4'} - - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - - onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} - - optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} - - own-keys@1.0.1: - resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} - engines: {node: '>= 0.4'} - - p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} - - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - - p-locate@4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} - engines: {node: '>=8'} - - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - - p-try@2.2.0: - resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} - engines: {node: '>=6'} - - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - - parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} - - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - - path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - - path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} - - periscopic@3.1.0: - resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - - pirates@4.0.7: - resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} - engines: {node: '>= 6'} - - pkg-dir@4.2.0: - resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} - engines: {node: '>=8'} - - possible-typed-array-names@1.1.0: - resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} - engines: {node: '>= 0.4'} - - postcss-load-config@3.1.4: - resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} - engines: {node: '>= 10'} - peerDependencies: - postcss: '>=8.0.9' - ts-node: '>=9.0.0' - peerDependenciesMeta: - postcss: - optional: true - ts-node: - optional: true - - postcss-safe-parser@6.0.0: - resolution: {integrity: sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==} - engines: {node: '>=12.0'} - peerDependencies: - postcss: ^8.3.3 - - postcss-scss@4.0.9: - resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} - engines: {node: '>=12.0'} - peerDependencies: - postcss: ^8.4.29 - - postcss-selector-parser@6.1.2: - resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} - engines: {node: '>=4'} - - postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} - engines: {node: ^10 || ^12 || >=14} - - postgres@3.4.7: - resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} - engines: {node: '>=12'} - - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - - prettier-plugin-svelte@3.4.0: - resolution: {integrity: sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==} - peerDependencies: - prettier: ^3.0.0 - svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 - - prettier@3.6.2: - resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} - engines: {node: '>=14'} - hasBin: true - - pretty-format@29.7.0: - resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - prompts@2.4.2: - resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} - engines: {node: '>= 6'} - - punycode.js@2.3.1: - resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} - engines: {node: '>=6'} - - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - - pure-rand@6.1.0: - resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} - - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - - react-is@18.3.1: - resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - - readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - - reflect.getprototypeof@1.0.10: - resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} - engines: {node: '>= 0.4'} - - regexp.prototype.flags@1.5.4: - resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} - engines: {node: '>= 0.4'} - - regexpp@3.2.0: - resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} - engines: {node: '>=8'} - - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - - resolve-cwd@3.0.0: - resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} - engines: {node: '>=8'} - - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - - resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} - - resolve.exports@2.0.3: - resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} - engines: {node: '>=10'} - - resolve@1.22.10: - resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} - engines: {node: '>= 0.4'} - hasBin: true - - reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - - rimraf@2.7.1: - resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - - rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - - safe-array-concat@1.1.3: - resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} - engines: {node: '>=0.4'} - - safe-push-apply@1.0.0: - resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} - engines: {node: '>= 0.4'} - - safe-regex-test@1.1.0: - resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} - engines: {node: '>= 0.4'} - - sander@0.5.1: - resolution: {integrity: sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==} - - scule@1.3.0: - resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} - - semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - - semver@7.7.2: - resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} - engines: {node: '>=10'} - hasBin: true - - semver@7.7.3: - resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} - engines: {node: '>=10'} - hasBin: true - - set-function-length@1.2.2: - resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} - engines: {node: '>= 0.4'} - - set-function-name@2.0.2: - resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} - engines: {node: '>= 0.4'} - - set-proto@1.0.0: - resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} - engines: {node: '>= 0.4'} - - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - side-channel-list@1.0.0: - resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} - engines: {node: '>= 0.4'} - - side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} - engines: {node: '>= 0.4'} - - side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} - engines: {node: '>= 0.4'} - - side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} - engines: {node: '>= 0.4'} - - signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - - sisteransi@1.0.5: - resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - - slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} - - snappyjs@0.7.0: - resolution: {integrity: sha512-u5iEEXkMe2EInQio6Wv9LWHOQYRDbD2O9hzS27GpT/lwfIQhTCnHCTqedqHIHe9ZcvQo+9au6vngQayipz1NYw==} - - sorcery@0.11.1: - resolution: {integrity: sha512-o7npfeJE6wi6J9l0/5LKshFzZ2rMatRiCDwYeDQaOzqdzRJwALhX7mk/A/ecg6wjMu7wdZbmXfD2S/vpOg0bdQ==} - hasBin: true - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - source-map-support@0.5.13: - resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} - - source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - - sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - - stack-utils@2.0.6: - resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} - engines: {node: '>=10'} - - stop-iteration-iterator@1.1.0: - resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} - engines: {node: '>= 0.4'} - - string-length@4.0.2: - resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} - engines: {node: '>=10'} - - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - string.prototype.trim@1.2.10: - resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} - engines: {node: '>= 0.4'} - - string.prototype.trimend@1.0.9: - resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} - engines: {node: '>= 0.4'} - - string.prototype.trimstart@1.0.8: - resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} - engines: {node: '>= 0.4'} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - strip-bom@3.0.0: - resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} - engines: {node: '>=4'} - - strip-bom@4.0.0: - resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} - engines: {node: '>=8'} - - strip-final-newline@2.0.0: - resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} - engines: {node: '>=6'} - - strip-indent@3.0.0: - resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} - engines: {node: '>=8'} - - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} - - supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - - svelte-eslint-parser@0.33.1: - resolution: {integrity: sha512-vo7xPGTlKBGdLH8T5L64FipvTrqv3OQRx9d2z5X05KKZDlF4rQk8KViZO4flKERY+5BiVdOh7zZ7JGJWo5P0uA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - svelte: ^3.37.0 || ^4.0.0 - peerDependenciesMeta: - svelte: - optional: true - - svelte-eslint-parser@0.43.0: - resolution: {integrity: sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 - peerDependenciesMeta: - svelte: - optional: true - - svelte-preprocess@5.1.4: - resolution: {integrity: sha512-IvnbQ6D6Ao3Gg6ftiM5tdbR6aAETwjhHV+UKGf5bHGYR69RQvF1ho0JKPcbUON4vy4R7zom13jPjgdOWCQ5hDA==} - engines: {node: '>= 16.0.0'} - peerDependencies: - '@babel/core': ^7.10.2 - coffeescript: ^2.5.1 - less: ^3.11.3 || ^4.0.0 - postcss: ^7 || ^8 - postcss-load-config: ^2.1.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 - pug: ^3.0.0 - sass: ^1.26.8 - stylus: ^0.55.0 - sugarss: ^2.0.0 || ^3.0.0 || ^4.0.0 - svelte: ^3.23.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0 - typescript: '>=3.9.5 || ^4.0.0 || ^5.0.0' - peerDependenciesMeta: - '@babel/core': - optional: true - coffeescript: - optional: true - less: - optional: true - postcss: - optional: true - postcss-load-config: - optional: true - pug: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - typescript: - optional: true - - svelte2tsx@0.7.45: - resolution: {integrity: sha512-cSci+mYGygYBHIZLHlm/jYlEc1acjAHqaQaDFHdEBpUueM9kSTnPpvPtSl5VkJOU1qSJ7h1K+6F/LIUYiqC8VA==} - peerDependencies: - svelte: ^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0 - typescript: ^4.9.4 || ^5.0.0 - - svelte@4.2.20: - resolution: {integrity: sha512-eeEgGc2DtiUil5ANdtd8vPwt9AgaMdnuUFnPft9F5oMvU/FHu5IHFic+p1dR/UOB7XU2mX2yHW+NcTch4DCh5Q==} - engines: {node: '>=16'} - - test-exclude@6.0.0: - resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} - engines: {node: '>=8'} - - text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - - tmpl@1.0.5: - resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} - - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - - ts-api-utils@1.4.3: - resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} - engines: {node: '>=16'} - peerDependencies: - typescript: '>=4.2.0' - - ts-jest@29.4.5: - resolution: {integrity: sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q==} - engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@babel/core': '>=7.0.0-beta.0 <8' - '@jest/transform': ^29.0.0 || ^30.0.0 - '@jest/types': ^29.0.0 || ^30.0.0 - babel-jest: ^29.0.0 || ^30.0.0 - esbuild: '*' - jest: ^29.0.0 || ^30.0.0 - jest-util: ^29.0.0 || ^30.0.0 - typescript: '>=4.3 <6' - peerDependenciesMeta: - '@babel/core': - optional: true - '@jest/transform': - optional: true - '@jest/types': - optional: true - babel-jest: - optional: true - esbuild: - optional: true - jest-util: - optional: true - - tsconfig-paths@3.15.0: - resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} - - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - - type-detect@4.0.8: - resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} - engines: {node: '>=4'} - - type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - - type-fest@0.21.3: - resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} - engines: {node: '>=10'} - - type-fest@4.41.0: - resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} - engines: {node: '>=16'} - - typed-array-buffer@1.0.3: - resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} - engines: {node: '>= 0.4'} - - typed-array-byte-length@1.0.3: - resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} - engines: {node: '>= 0.4'} - - typed-array-byte-offset@1.0.4: - resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} - engines: {node: '>= 0.4'} - - typed-array-length@1.0.7: - resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} - engines: {node: '>= 0.4'} - - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - - uc.micro@2.1.0: - resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} - - uglify-js@3.19.3: - resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} - engines: {node: '>=0.8.0'} - hasBin: true - - unbox-primitive@1.1.0: - resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} - engines: {node: '>= 0.4'} - - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - - universalify@2.0.1: - resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} - engines: {node: '>= 10.0.0'} - - update-browserslist-db@1.1.3: - resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - - uuid@8.3.2: - resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} - hasBin: true - - v8-to-istanbul@9.3.0: - resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} - engines: {node: '>=10.12.0'} - - walker@1.0.8: - resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} - - which-boxed-primitive@1.1.1: - resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} - engines: {node: '>= 0.4'} - - which-builtin-type@1.2.1: - resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} - engines: {node: '>= 0.4'} - - which-collection@1.0.2: - resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} - engines: {node: '>= 0.4'} - - which-typed-array@1.1.19: - resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} - engines: {node: '>= 0.4'} - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} - - wordwrap@1.0.0: - resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} - - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - - write-file-atomic@4.0.2: - resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - - yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - - yaml@1.10.2: - resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} - engines: {node: '>= 6'} - - yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} - - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - - zod@3.25.76: - resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - -snapshots: - - '@ampproject/remapping@2.3.0': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - - '@babel/code-frame@7.27.1': - dependencies: - '@babel/helper-validator-identifier': 7.27.1 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/compat-data@7.28.4': {} - - '@babel/core@7.28.4': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.3 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) - '@babel/helpers': 7.28.4 - '@babel/parser': 7.28.4 - '@babel/template': 7.27.2 - '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 - '@jridgewell/remapping': 2.3.5 - convert-source-map: 2.0.0 - debug: 4.4.3 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/generator@7.28.3': - dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 - - '@babel/helper-compilation-targets@7.27.2': - dependencies: - '@babel/compat-data': 7.28.4 - '@babel/helper-validator-option': 7.27.1 - browserslist: 4.26.3 - lru-cache: 5.1.1 - semver: 6.3.1 - - '@babel/helper-globals@7.28.0': {} - - '@babel/helper-module-imports@7.27.1': - dependencies: - '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 - transitivePeerDependencies: - - supports-color - - '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.28.4 - transitivePeerDependencies: - - supports-color - - '@babel/helper-plugin-utils@7.27.1': {} - - '@babel/helper-string-parser@7.27.1': {} - - '@babel/helper-validator-identifier@7.27.1': {} - - '@babel/helper-validator-option@7.27.1': {} - - '@babel/helpers@7.28.4': - dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.28.4 - - '@babel/parser@7.28.4': - dependencies: - '@babel/types': 7.28.4 - - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/template@7.27.2': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 - - '@babel/traverse@7.28.4': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.3 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.4 - '@babel/template': 7.27.2 - '@babel/types': 7.28.4 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - '@babel/types@7.28.4': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - - '@bcoe/v8-coverage@0.2.3': {} - - '@esbuild/aix-ppc64@0.24.2': - optional: true - - '@esbuild/aix-ppc64@0.25.10': - optional: true - - '@esbuild/android-arm64@0.24.2': - optional: true - - '@esbuild/android-arm64@0.25.10': - optional: true - - '@esbuild/android-arm@0.24.2': - optional: true - - '@esbuild/android-arm@0.25.10': - optional: true - - '@esbuild/android-x64@0.24.2': - optional: true - - '@esbuild/android-x64@0.25.10': - optional: true - - '@esbuild/darwin-arm64@0.24.2': - optional: true - - '@esbuild/darwin-arm64@0.25.10': - optional: true - - '@esbuild/darwin-x64@0.24.2': - optional: true - - '@esbuild/darwin-x64@0.25.10': - optional: true - - '@esbuild/freebsd-arm64@0.24.2': - optional: true - - '@esbuild/freebsd-arm64@0.25.10': - optional: true - - '@esbuild/freebsd-x64@0.24.2': - optional: true - - '@esbuild/freebsd-x64@0.25.10': - optional: true - - '@esbuild/linux-arm64@0.24.2': - optional: true - - '@esbuild/linux-arm64@0.25.10': - optional: true - - '@esbuild/linux-arm@0.24.2': - optional: true - - '@esbuild/linux-arm@0.25.10': - optional: true - - '@esbuild/linux-ia32@0.24.2': - optional: true - - '@esbuild/linux-ia32@0.25.10': - optional: true - - '@esbuild/linux-loong64@0.24.2': - optional: true - - '@esbuild/linux-loong64@0.25.10': - optional: true - - '@esbuild/linux-mips64el@0.24.2': - optional: true - - '@esbuild/linux-mips64el@0.25.10': - optional: true - - '@esbuild/linux-ppc64@0.24.2': - optional: true - - '@esbuild/linux-ppc64@0.25.10': - optional: true - - '@esbuild/linux-riscv64@0.24.2': - optional: true - - '@esbuild/linux-riscv64@0.25.10': - optional: true - - '@esbuild/linux-s390x@0.24.2': - optional: true - - '@esbuild/linux-s390x@0.25.10': - optional: true - - '@esbuild/linux-x64@0.24.2': - optional: true - - '@esbuild/linux-x64@0.25.10': - optional: true - - '@esbuild/netbsd-arm64@0.24.2': - optional: true - - '@esbuild/netbsd-arm64@0.25.10': - optional: true - - '@esbuild/netbsd-x64@0.24.2': - optional: true - - '@esbuild/netbsd-x64@0.25.10': - optional: true - - '@esbuild/openbsd-arm64@0.24.2': - optional: true - - '@esbuild/openbsd-arm64@0.25.10': - optional: true - - '@esbuild/openbsd-x64@0.24.2': - optional: true - - '@esbuild/openbsd-x64@0.25.10': - optional: true - - '@esbuild/openharmony-arm64@0.25.10': - optional: true - - '@esbuild/sunos-x64@0.24.2': - optional: true - - '@esbuild/sunos-x64@0.25.10': - optional: true - - '@esbuild/win32-arm64@0.24.2': - optional: true - - '@esbuild/win32-arm64@0.25.10': - optional: true - - '@esbuild/win32-ia32@0.24.2': - optional: true - - '@esbuild/win32-ia32@0.25.10': - optional: true - - '@esbuild/win32-x64@0.24.2': - optional: true - - '@esbuild/win32-x64@0.25.10': - optional: true - - '@eslint-community/eslint-utils@4.9.0(eslint@8.57.1)': - dependencies: - eslint: 8.57.1 - eslint-visitor-keys: 3.4.3 - - '@eslint-community/regexpp@4.12.1': {} - - '@eslint/eslintrc@2.1.4': - dependencies: - ajv: 6.12.6 - debug: 4.4.3 - espree: 9.6.1 - globals: 13.24.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - - '@eslint/js@8.57.1': {} - - '@formatjs/ecma402-abstract@2.3.5': - dependencies: - '@formatjs/fast-memoize': 2.2.7 - '@formatjs/intl-localematcher': 0.6.2 - decimal.js: 10.6.0 - tslib: 2.8.1 - - '@formatjs/fast-memoize@2.2.7': - dependencies: - tslib: 2.8.1 - - '@formatjs/icu-messageformat-parser@2.11.3': - dependencies: - '@formatjs/ecma402-abstract': 2.3.5 - '@formatjs/icu-skeleton-parser': 1.8.15 - tslib: 2.8.1 - - '@formatjs/icu-skeleton-parser@1.8.15': - dependencies: - '@formatjs/ecma402-abstract': 2.3.5 - tslib: 2.8.1 - - '@formatjs/intl-localematcher@0.6.2': - dependencies: - tslib: 2.8.1 - - '@hcengineering/account-client@0.7.18': - dependencies: - '@hcengineering/core': 0.7.19 - '@hcengineering/platform': 0.7.17 - - '@hcengineering/analytics@0.7.17': - dependencies: - '@hcengineering/platform': 0.7.17 - - '@hcengineering/core@0.7.19': - dependencies: - '@hcengineering/analytics': 0.7.17 - '@hcengineering/measurements': 0.7.18 - '@hcengineering/platform': 0.7.17 - fast-equals: 5.3.2 - - '@hcengineering/hulylake-client@0.7.17': - dependencies: - '@hcengineering/core': 0.7.19 - '@hcengineering/retry': 0.7.17 - - '@hcengineering/measurements@0.7.18': {} - - '@hcengineering/platform-rig@0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6))(postcss@8.5.6)': - dependencies: - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) - esbuild: 0.25.10 - esbuild-plugin-copy: 2.1.1(esbuild@0.25.10) - esbuild-svelte: 0.9.3(esbuild@0.25.10)(svelte@4.2.20) - eslint: 8.57.1 - eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: 2.32.0(eslint@8.57.1) - eslint-plugin-n: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: 6.6.0(eslint@8.57.1) - eslint-plugin-svelte: 2.46.1(eslint@8.57.1)(svelte@4.2.20) - prettier: 3.6.2 - prettier-plugin-svelte: 3.4.0(prettier@3.6.2)(svelte@4.2.20) - svelte: 4.2.20 - svelte-eslint-parser: 0.33.1(svelte@4.2.20) - svelte-preprocess: 5.1.4(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6))(postcss@8.5.6)(svelte@4.2.20)(typescript@5.9.3) - svelte2tsx: 0.7.45(svelte@4.2.20)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - '@babel/core' - - coffeescript - - less - - postcss - - postcss-load-config - - pug - - sass - - stylus - - sugarss - - supports-color - - ts-node - - '@hcengineering/platform@0.7.17': - dependencies: - intl-messageformat: 10.7.17 - - '@hcengineering/retry@0.7.17': {} - - '@hcengineering/server-token@0.7.17': - dependencies: - '@hcengineering/core': 0.7.19 - '@hcengineering/platform': 0.7.17 - jwt-simple: 0.5.6 - uuid: 8.3.2 - - '@hcengineering/text-core@0.7.18': - dependencies: - '@hcengineering/core': 0.7.19 - fast-equals: 5.3.2 - hash-it: 6.0.0 - - '@hcengineering/text-html@0.7.18': - dependencies: - '@hcengineering/text-core': 0.7.18 - htmlparser2: 9.1.0 - - '@hcengineering/text-markdown@0.7.18': - dependencies: - '@hcengineering/text-core': 0.7.18 - '@hcengineering/text-html': 0.7.18 - fast-equals: 5.3.2 - markdown-it: 14.1.0 - - '@humanwhocodes/config-array@0.13.0': - dependencies: - '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.3 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - - '@humanwhocodes/module-importer@1.0.1': {} - - '@humanwhocodes/object-schema@2.0.3': {} - - '@istanbuljs/load-nyc-config@1.1.0': - dependencies: - camelcase: 5.3.1 - find-up: 4.1.0 - get-package-type: 0.1.0 - js-yaml: 3.14.1 - resolve-from: 5.0.0 - - '@istanbuljs/schema@0.1.3': {} - - '@jest/console@29.7.0': - dependencies: - '@jest/types': 29.6.3 - '@types/node': 22.18.10 - chalk: 4.1.2 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - slash: 3.0.0 - - '@jest/core@29.7.0': - dependencies: - '@jest/console': 29.7.0 - '@jest/reporters': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.18.10 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 3.9.0 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.18.10) - jest-haste-map: 29.7.0 - jest-message-util: 29.7.0 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-resolve-dependencies: 29.7.0 - jest-runner: 29.7.0 - jest-runtime: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - jest-watcher: 29.7.0 - micromatch: 4.0.8 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-ansi: 6.0.1 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - ts-node - - '@jest/environment@29.7.0': - dependencies: - '@jest/fake-timers': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.18.10 - jest-mock: 29.7.0 - - '@jest/expect-utils@29.7.0': - dependencies: - jest-get-type: 29.6.3 - - '@jest/expect@29.7.0': - dependencies: - expect: 29.7.0 - jest-snapshot: 29.7.0 - transitivePeerDependencies: - - supports-color - - '@jest/fake-timers@29.7.0': - dependencies: - '@jest/types': 29.6.3 - '@sinonjs/fake-timers': 10.3.0 - '@types/node': 22.18.10 - jest-message-util: 29.7.0 - jest-mock: 29.7.0 - jest-util: 29.7.0 - - '@jest/globals@29.7.0': - dependencies: - '@jest/environment': 29.7.0 - '@jest/expect': 29.7.0 - '@jest/types': 29.6.3 - jest-mock: 29.7.0 - transitivePeerDependencies: - - supports-color - - '@jest/reporters@29.7.0': - dependencies: - '@bcoe/v8-coverage': 0.2.3 - '@jest/console': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.31 - '@types/node': 22.18.10 - chalk: 4.1.2 - collect-v8-coverage: 1.0.2 - exit: 0.1.2 - glob: 7.2.3 - graceful-fs: 4.2.11 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-instrument: 6.0.3 - istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 4.0.1 - istanbul-reports: 3.2.0 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - jest-worker: 29.7.0 - slash: 3.0.0 - string-length: 4.0.2 - strip-ansi: 6.0.1 - v8-to-istanbul: 9.3.0 - transitivePeerDependencies: - - supports-color - - '@jest/schemas@29.6.3': - dependencies: - '@sinclair/typebox': 0.27.8 - - '@jest/source-map@29.6.3': - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - callsites: 3.1.0 - graceful-fs: 4.2.11 - - '@jest/test-result@29.7.0': - dependencies: - '@jest/console': 29.7.0 - '@jest/types': 29.6.3 - '@types/istanbul-lib-coverage': 2.0.6 - collect-v8-coverage: 1.0.2 - - '@jest/test-sequencer@29.7.0': - dependencies: - '@jest/test-result': 29.7.0 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - slash: 3.0.0 - - '@jest/transform@29.7.0': - dependencies: - '@babel/core': 7.28.4 - '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.31 - babel-plugin-istanbul: 6.1.1 - chalk: 4.1.2 - convert-source-map: 2.0.0 - fast-json-stable-stringify: 2.1.0 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - jest-regex-util: 29.6.3 - jest-util: 29.7.0 - micromatch: 4.0.8 - pirates: 4.0.7 - slash: 3.0.0 - write-file-atomic: 4.0.2 - transitivePeerDependencies: - - supports-color - - '@jest/types@29.6.3': - dependencies: - '@jest/schemas': 29.6.3 - '@types/istanbul-lib-coverage': 2.0.6 - '@types/istanbul-reports': 3.0.4 - '@types/node': 22.18.10 - '@types/yargs': 17.0.33 - chalk: 4.1.2 - - '@jridgewell/gen-mapping@0.3.13': - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/remapping@2.3.5': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/sourcemap-codec@1.5.5': {} - - '@jridgewell/trace-mapping@0.3.31': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.19.1 - - '@rtsao/scc@1.1.0': {} - - '@sinclair/typebox@0.27.8': {} - - '@sinonjs/commons@3.0.1': - dependencies: - type-detect: 4.0.8 - - '@sinonjs/fake-timers@10.3.0': - dependencies: - '@sinonjs/commons': 3.0.1 - - '@types/babel__core@7.20.5': - dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 - '@types/babel__generator': 7.27.0 - '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.28.0 - - '@types/babel__generator@7.27.0': - dependencies: - '@babel/types': 7.28.4 - - '@types/babel__template@7.4.4': - dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 - - '@types/babel__traverse@7.28.0': - dependencies: - '@babel/types': 7.28.4 - - '@types/estree@1.0.8': {} - - '@types/graceful-fs@4.1.9': - dependencies: - '@types/node': 22.18.10 - - '@types/istanbul-lib-coverage@2.0.6': {} - - '@types/istanbul-lib-report@3.0.3': - dependencies: - '@types/istanbul-lib-coverage': 2.0.6 - - '@types/istanbul-reports@3.0.4': - dependencies: - '@types/istanbul-lib-report': 3.0.3 - - '@types/jest@29.5.14': - dependencies: - expect: 29.7.0 - pretty-format: 29.7.0 - - '@types/json-schema@7.0.15': {} - - '@types/json5@0.0.29': {} - - '@types/node@22.18.10': - dependencies: - undici-types: 6.21.0 - - '@types/pug@2.0.10': {} - - '@types/semver@7.7.1': {} - - '@types/snappyjs@0.7.1': {} - - '@types/stack-utils@2.0.3': {} - - '@types/uuid@8.3.4': {} - - '@types/yargs-parser@21.0.3': {} - - '@types/yargs@17.0.33': - dependencies: - '@types/yargs-parser': 21.0.3 - - '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': - dependencies: - '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.3 - eslint: 8.57.1 - graphemer: 1.4.0 - ignore: 5.3.2 - natural-compare: 1.4.0 - semver: 7.7.2 - ts-api-utils: 1.4.3(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3)': - dependencies: - '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.3 - eslint: 8.57.1 - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/scope-manager@6.21.0': - dependencies: - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/visitor-keys': 6.21.0 - - '@typescript-eslint/type-utils@6.21.0(eslint@8.57.1)(typescript@5.9.3)': - dependencies: - '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.9.3) - '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.9.3) - debug: 4.4.3 - eslint: 8.57.1 - ts-api-utils: 1.4.3(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/types@6.21.0': {} - - '@typescript-eslint/typescript-estree@6.21.0(typescript@5.9.3)': - dependencies: - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.3 - globby: 11.1.0 - is-glob: 4.0.3 - minimatch: 9.0.3 - semver: 7.7.2 - ts-api-utils: 1.4.3(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@6.21.0(eslint@8.57.1)(typescript@5.9.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1) - '@types/json-schema': 7.0.15 - '@types/semver': 7.7.1 - '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.9.3) - eslint: 8.57.1 - semver: 7.7.2 - transitivePeerDependencies: - - supports-color - - typescript - - '@typescript-eslint/visitor-keys@6.21.0': - dependencies: - '@typescript-eslint/types': 6.21.0 - eslint-visitor-keys: 3.4.3 - - '@ungap/structured-clone@1.3.0': {} - - acorn-jsx@5.3.2(acorn@8.15.0): - dependencies: - acorn: 8.15.0 - - acorn@8.15.0: {} - - ajv@6.12.6: - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - - ansi-escapes@4.3.2: - dependencies: - type-fest: 0.21.3 - - ansi-regex@5.0.1: {} - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - ansi-styles@5.2.0: {} - - anymatch@3.1.3: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - - argparse@1.0.10: - dependencies: - sprintf-js: 1.0.3 - - argparse@2.0.1: {} - - aria-query@5.3.2: {} - - array-buffer-byte-length@1.0.2: - dependencies: - call-bound: 1.0.4 - is-array-buffer: 3.0.5 - - array-includes@3.1.9: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - is-string: 1.1.1 - math-intrinsics: 1.1.0 - - array-union@2.1.0: {} - - array.prototype.findlastindex@1.2.6: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-shim-unscopables: 1.1.0 - - array.prototype.flat@1.3.3: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-shim-unscopables: 1.1.0 - - array.prototype.flatmap@1.3.3: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-shim-unscopables: 1.1.0 - - arraybuffer.prototype.slice@1.0.4: - dependencies: - array-buffer-byte-length: 1.0.2 - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - is-array-buffer: 3.0.5 - - async-function@1.0.0: {} - - available-typed-arrays@1.0.7: - dependencies: - possible-typed-array-names: 1.1.0 - - axobject-query@4.1.0: {} - - babel-jest@29.7.0(@babel/core@7.28.4): - dependencies: - '@babel/core': 7.28.4 - '@jest/transform': 29.7.0 - '@types/babel__core': 7.20.5 - babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.6.3(@babel/core@7.28.4) - chalk: 4.1.2 - graceful-fs: 4.2.11 - slash: 3.0.0 - transitivePeerDependencies: - - supports-color - - babel-plugin-istanbul@6.1.1: - dependencies: - '@babel/helper-plugin-utils': 7.27.1 - '@istanbuljs/load-nyc-config': 1.1.0 - '@istanbuljs/schema': 0.1.3 - istanbul-lib-instrument: 5.2.1 - test-exclude: 6.0.0 - transitivePeerDependencies: - - supports-color - - babel-plugin-jest-hoist@29.6.3: - dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.28.4 - '@types/babel__core': 7.20.5 - '@types/babel__traverse': 7.28.0 - - babel-preset-current-node-syntax@1.2.0(@babel/core@7.28.4): - dependencies: - '@babel/core': 7.28.4 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.28.4) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.28.4) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.28.4) - '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.4) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.28.4) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.28.4) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.4) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.4) - - babel-preset-jest@29.6.3(@babel/core@7.28.4): - dependencies: - '@babel/core': 7.28.4 - babel-plugin-jest-hoist: 29.6.3 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.4) - - balanced-match@1.0.2: {} - - baseline-browser-mapping@2.8.12: {} - - binary-extensions@2.3.0: {} - - brace-expansion@1.1.12: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - brace-expansion@2.0.2: - dependencies: - balanced-match: 1.0.2 - - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - - browserslist@4.26.3: - dependencies: - baseline-browser-mapping: 2.8.12 - caniuse-lite: 1.0.30001748 - electron-to-chromium: 1.5.232 - node-releases: 2.0.23 - update-browserslist-db: 1.1.3(browserslist@4.26.3) - - bs-logger@0.2.6: - dependencies: - fast-json-stable-stringify: 2.1.0 - - bser@2.1.1: - dependencies: - node-int64: 0.4.0 - - buffer-crc32@1.0.0: {} - - buffer-from@1.1.2: {} - - builtins@5.1.0: - dependencies: - semver: 7.7.2 - - call-bind-apply-helpers@1.0.2: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - - call-bind@1.0.8: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - get-intrinsic: 1.3.0 - set-function-length: 1.2.2 - - call-bound@1.0.4: - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 - - callsites@3.1.0: {} - - camelcase@5.3.1: {} - - camelcase@6.3.0: {} - - caniuse-lite@1.0.30001748: {} - - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - char-regex@1.0.2: {} - - chokidar@3.6.0: - dependencies: - anymatch: 3.1.3 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 - - ci-info@3.9.0: {} - - cjs-module-lexer@1.4.3: {} - - cliui@8.0.1: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - - co@4.6.0: {} - - code-red@1.0.4: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@types/estree': 1.0.8 - acorn: 8.15.0 - estree-walker: 3.0.3 - periscopic: 3.1.0 - - collect-v8-coverage@1.0.2: {} - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - - concat-map@0.0.1: {} - - convert-source-map@2.0.0: {} - - create-jest@29.7.0(@types/node@22.18.10): - dependencies: - '@jest/types': 29.6.3 - chalk: 4.1.2 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.18.10) - jest-util: 29.7.0 - prompts: 2.4.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - css-tree@2.3.1: - dependencies: - mdn-data: 2.0.30 - source-map-js: 1.2.1 - - cssesc@3.0.0: {} - - data-view-buffer@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - data-view-byte-length@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - data-view-byte-offset@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - debug@3.2.7: - dependencies: - ms: 2.1.3 - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - decimal.js@10.6.0: {} - - dedent-js@1.0.1: {} - - dedent@1.7.0: {} - - deep-is@0.1.4: {} - - deepmerge@4.3.1: {} - - define-data-property@1.1.4: - dependencies: - es-define-property: 1.0.1 - es-errors: 1.3.0 - gopd: 1.2.0 - - define-properties@1.2.1: - dependencies: - define-data-property: 1.1.4 - has-property-descriptors: 1.0.2 - object-keys: 1.1.1 - - detect-indent@6.1.0: {} - - detect-newline@3.1.0: {} - - diff-sequences@29.6.3: {} - - dir-glob@3.0.1: - dependencies: - path-type: 4.0.0 - - doctrine@2.1.0: - dependencies: - esutils: 2.0.3 - - doctrine@3.0.0: - dependencies: - esutils: 2.0.3 - - dom-serializer@2.0.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - entities: 4.5.0 - - domelementtype@2.3.0: {} - - domhandler@5.0.3: - dependencies: - domelementtype: 2.3.0 - - domutils@3.2.2: - dependencies: - dom-serializer: 2.0.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - - electron-to-chromium@1.5.232: {} - - emittery@0.13.1: {} - - emoji-regex@8.0.0: {} - - entities@4.5.0: {} - - error-ex@1.3.4: - dependencies: - is-arrayish: 0.2.1 - - es-abstract@1.24.0: - dependencies: - array-buffer-byte-length: 1.0.2 - arraybuffer.prototype.slice: 1.0.4 - available-typed-arrays: 1.0.7 - call-bind: 1.0.8 - call-bound: 1.0.4 - data-view-buffer: 1.0.2 - data-view-byte-length: 1.0.2 - data-view-byte-offset: 1.0.1 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-set-tostringtag: 2.1.0 - es-to-primitive: 1.3.0 - function.prototype.name: 1.1.8 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - get-symbol-description: 1.1.0 - globalthis: 1.0.4 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - has-proto: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - internal-slot: 1.1.0 - is-array-buffer: 3.0.5 - is-callable: 1.2.7 - is-data-view: 1.0.2 - is-negative-zero: 2.0.3 - is-regex: 1.2.1 - is-set: 2.0.3 - is-shared-array-buffer: 1.0.4 - is-string: 1.1.1 - is-typed-array: 1.1.15 - is-weakref: 1.1.1 - math-intrinsics: 1.1.0 - object-inspect: 1.13.4 - object-keys: 1.1.1 - object.assign: 4.1.7 - own-keys: 1.0.1 - regexp.prototype.flags: 1.5.4 - safe-array-concat: 1.1.3 - safe-push-apply: 1.0.0 - safe-regex-test: 1.1.0 - set-proto: 1.0.0 - stop-iteration-iterator: 1.1.0 - string.prototype.trim: 1.2.10 - string.prototype.trimend: 1.0.9 - string.prototype.trimstart: 1.0.8 - typed-array-buffer: 1.0.3 - typed-array-byte-length: 1.0.3 - typed-array-byte-offset: 1.0.4 - typed-array-length: 1.0.7 - unbox-primitive: 1.1.0 - which-typed-array: 1.1.19 - - es-define-property@1.0.1: {} - - es-errors@1.3.0: {} - - es-object-atoms@1.1.1: - dependencies: - es-errors: 1.3.0 - - es-set-tostringtag@2.1.0: - dependencies: - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - - es-shim-unscopables@1.1.0: - dependencies: - hasown: 2.0.2 - - es-to-primitive@1.3.0: - dependencies: - is-callable: 1.2.7 - is-date-object: 1.1.0 - is-symbol: 1.1.1 - - es6-promise@3.3.1: {} - - esbuild-plugin-copy@2.1.1(esbuild@0.25.10): - dependencies: - chalk: 4.1.2 - chokidar: 3.6.0 - esbuild: 0.25.10 - fs-extra: 10.1.0 - globby: 11.1.0 - - esbuild-svelte@0.9.3(esbuild@0.25.10)(svelte@4.2.20): - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - esbuild: 0.25.10 - svelte: 4.2.20 - - esbuild@0.24.2: - optionalDependencies: - '@esbuild/aix-ppc64': 0.24.2 - '@esbuild/android-arm': 0.24.2 - '@esbuild/android-arm64': 0.24.2 - '@esbuild/android-x64': 0.24.2 - '@esbuild/darwin-arm64': 0.24.2 - '@esbuild/darwin-x64': 0.24.2 - '@esbuild/freebsd-arm64': 0.24.2 - '@esbuild/freebsd-x64': 0.24.2 - '@esbuild/linux-arm': 0.24.2 - '@esbuild/linux-arm64': 0.24.2 - '@esbuild/linux-ia32': 0.24.2 - '@esbuild/linux-loong64': 0.24.2 - '@esbuild/linux-mips64el': 0.24.2 - '@esbuild/linux-ppc64': 0.24.2 - '@esbuild/linux-riscv64': 0.24.2 - '@esbuild/linux-s390x': 0.24.2 - '@esbuild/linux-x64': 0.24.2 - '@esbuild/netbsd-arm64': 0.24.2 - '@esbuild/netbsd-x64': 0.24.2 - '@esbuild/openbsd-arm64': 0.24.2 - '@esbuild/openbsd-x64': 0.24.2 - '@esbuild/sunos-x64': 0.24.2 - '@esbuild/win32-arm64': 0.24.2 - '@esbuild/win32-ia32': 0.24.2 - '@esbuild/win32-x64': 0.24.2 - - esbuild@0.25.10: - optionalDependencies: - '@esbuild/aix-ppc64': 0.25.10 - '@esbuild/android-arm': 0.25.10 - '@esbuild/android-arm64': 0.25.10 - '@esbuild/android-x64': 0.25.10 - '@esbuild/darwin-arm64': 0.25.10 - '@esbuild/darwin-x64': 0.25.10 - '@esbuild/freebsd-arm64': 0.25.10 - '@esbuild/freebsd-x64': 0.25.10 - '@esbuild/linux-arm': 0.25.10 - '@esbuild/linux-arm64': 0.25.10 - '@esbuild/linux-ia32': 0.25.10 - '@esbuild/linux-loong64': 0.25.10 - '@esbuild/linux-mips64el': 0.25.10 - '@esbuild/linux-ppc64': 0.25.10 - '@esbuild/linux-riscv64': 0.25.10 - '@esbuild/linux-s390x': 0.25.10 - '@esbuild/linux-x64': 0.25.10 - '@esbuild/netbsd-arm64': 0.25.10 - '@esbuild/netbsd-x64': 0.25.10 - '@esbuild/openbsd-arm64': 0.25.10 - '@esbuild/openbsd-x64': 0.25.10 - '@esbuild/openharmony-arm64': 0.25.10 - '@esbuild/sunos-x64': 0.25.10 - '@esbuild/win32-arm64': 0.25.10 - '@esbuild/win32-ia32': 0.25.10 - '@esbuild/win32-x64': 0.25.10 - - escalade@3.2.0: {} - - escape-string-regexp@2.0.0: {} - - escape-string-regexp@4.0.0: {} - - eslint-compat-utils@0.5.1(eslint@8.57.1): - dependencies: - eslint: 8.57.1 - semver: 7.7.2 - - eslint-config-standard-with-typescript@40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3): - dependencies: - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) - eslint: 8.57.1 - eslint-config-standard: 17.1.0(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.32.0(eslint@8.57.1) - eslint-plugin-n: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: 6.6.0(eslint@8.57.1) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - eslint-config-standard@17.1.0(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1): - dependencies: - eslint: 8.57.1 - eslint-plugin-import: 2.32.0(eslint@8.57.1) - eslint-plugin-n: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: 6.6.0(eslint@8.57.1) - - eslint-import-resolver-node@0.3.9: - dependencies: - debug: 3.2.7 - is-core-module: 2.16.1 - resolve: 1.22.10 - - eslint-module-utils@2.12.1(eslint@8.57.1): - dependencies: - debug: 3.2.7 - optionalDependencies: - eslint: 8.57.1 - - eslint-plugin-es@4.1.0(eslint@8.57.1): - dependencies: - eslint: 8.57.1 - eslint-utils: 2.1.0 - regexpp: 3.2.0 - - eslint-plugin-import@2.32.0(eslint@8.57.1): - dependencies: - '@rtsao/scc': 1.1.0 - array-includes: 3.1.9 - array.prototype.findlastindex: 1.2.6 - array.prototype.flat: 1.3.3 - array.prototype.flatmap: 1.3.3 - debug: 3.2.7 - doctrine: 2.1.0 - eslint: 8.57.1 - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(eslint@8.57.1) - hasown: 2.0.2 - is-core-module: 2.16.1 - is-glob: 4.0.3 - minimatch: 3.1.2 - object.fromentries: 2.0.8 - object.groupby: 1.0.3 - object.values: 1.2.1 - semver: 6.3.1 - string.prototype.trimend: 1.0.9 - tsconfig-paths: 3.15.0 - - eslint-plugin-n@15.7.0(eslint@8.57.1): - dependencies: - builtins: 5.1.0 - eslint: 8.57.1 - eslint-plugin-es: 4.1.0(eslint@8.57.1) - eslint-utils: 3.0.0(eslint@8.57.1) - ignore: 5.3.2 - is-core-module: 2.16.1 - minimatch: 3.1.2 - resolve: 1.22.10 - semver: 7.7.2 - - eslint-plugin-promise@6.6.0(eslint@8.57.1): - dependencies: - eslint: 8.57.1 - - eslint-plugin-svelte@2.46.1(eslint@8.57.1)(svelte@4.2.20): - dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1) - '@jridgewell/sourcemap-codec': 1.5.5 - eslint: 8.57.1 - eslint-compat-utils: 0.5.1(eslint@8.57.1) - esutils: 2.0.3 - known-css-properties: 0.35.0 - postcss: 8.5.6 - postcss-load-config: 3.1.4(postcss@8.5.6) - postcss-safe-parser: 6.0.0(postcss@8.5.6) - postcss-selector-parser: 6.1.2 - semver: 7.7.2 - svelte-eslint-parser: 0.43.0(svelte@4.2.20) - optionalDependencies: - svelte: 4.2.20 - transitivePeerDependencies: - - ts-node - - eslint-scope@7.2.2: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-utils@2.1.0: - dependencies: - eslint-visitor-keys: 1.3.0 - - eslint-utils@3.0.0(eslint@8.57.1): - dependencies: - eslint: 8.57.1 - eslint-visitor-keys: 2.1.0 - - eslint-visitor-keys@1.3.0: {} - - eslint-visitor-keys@2.1.0: {} - - eslint-visitor-keys@3.4.3: {} - - eslint@8.57.1: - dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1) - '@eslint-community/regexpp': 4.12.1 - '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.57.1 - '@humanwhocodes/config-array': 0.13.0 - '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - '@ungap/structured-clone': 1.3.0 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.4.3 - doctrine: 3.0.0 - escape-string-regexp: 4.0.0 - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 - esquery: 1.6.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 - find-up: 5.0.0 - glob-parent: 6.0.2 - globals: 13.24.0 - graphemer: 1.4.0 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - is-path-inside: 3.0.3 - js-yaml: 4.1.0 - json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.4 - strip-ansi: 6.0.1 - text-table: 0.2.0 - transitivePeerDependencies: - - supports-color - - espree@9.6.1: - dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) - eslint-visitor-keys: 3.4.3 - - esprima@4.0.1: {} - - esquery@1.6.0: - dependencies: - estraverse: 5.3.0 - - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@5.3.0: {} - - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.8 - - esutils@2.0.3: {} - - execa@5.1.1: - dependencies: - cross-spawn: 7.0.6 - get-stream: 6.0.1 - human-signals: 2.1.0 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 - - exit@0.1.2: {} - - expect@29.7.0: - dependencies: - '@jest/expect-utils': 29.7.0 - jest-get-type: 29.6.3 - jest-matcher-utils: 29.7.0 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - - fast-deep-equal@3.1.3: {} - - fast-equals@5.3.2: {} - - fast-glob@3.3.3: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - - fast-json-stable-stringify@2.1.0: {} - - fast-levenshtein@2.0.6: {} - - fastq@1.19.1: - dependencies: - reusify: 1.1.0 - - fb-watchman@2.0.2: - dependencies: - bser: 2.1.1 - - file-entry-cache@6.0.1: - dependencies: - flat-cache: 3.2.0 - - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - - find-up@4.1.0: - dependencies: - locate-path: 5.0.0 - path-exists: 4.0.0 - - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - - flat-cache@3.2.0: - dependencies: - flatted: 3.3.3 - keyv: 4.5.4 - rimraf: 3.0.2 - - flatted@3.3.3: {} - - for-each@0.3.5: - dependencies: - is-callable: 1.2.7 - - fs-extra@10.1.0: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.2.0 - universalify: 2.0.1 - - fs.realpath@1.0.0: {} - - fsevents@2.3.3: - optional: true - - function-bind@1.1.2: {} - - function.prototype.name@1.1.8: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - functions-have-names: 1.2.3 - hasown: 2.0.2 - is-callable: 1.2.7 - - functions-have-names@1.2.3: {} - - generator-function@2.0.1: {} - - gensync@1.0.0-beta.2: {} - - get-caller-file@2.0.5: {} - - get-intrinsic@1.3.0: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - math-intrinsics: 1.1.0 - - get-package-type@0.1.0: {} - - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 - - get-stream@6.0.1: {} - - get-symbol-description@1.1.0: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - - glob@7.2.3: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 - - globals@13.24.0: - dependencies: - type-fest: 0.20.2 - - globalthis@1.0.4: - dependencies: - define-properties: 1.2.1 - gopd: 1.2.0 - - globby@11.1.0: - dependencies: - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.3.3 - ignore: 5.3.2 - merge2: 1.4.1 - slash: 3.0.0 - - gopd@1.2.0: {} - - graceful-fs@4.2.11: {} - - graphemer@1.4.0: {} - - handlebars@4.7.8: - dependencies: - minimist: 1.2.8 - neo-async: 2.6.2 - source-map: 0.6.1 - wordwrap: 1.0.0 - optionalDependencies: - uglify-js: 3.19.3 - - has-bigints@1.1.0: {} - - has-flag@4.0.0: {} - - has-property-descriptors@1.0.2: - dependencies: - es-define-property: 1.0.1 - - has-proto@1.2.0: - dependencies: - dunder-proto: 1.0.1 - - has-symbols@1.1.0: {} - - has-tostringtag@1.0.2: - dependencies: - has-symbols: 1.1.0 - - hash-it@6.0.0: {} - - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - - html-escaper@2.0.2: {} - - htmlparser2@9.1.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.2.2 - entities: 4.5.0 - - human-signals@2.1.0: {} - - ignore@5.3.2: {} - - import-fresh@3.3.1: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - - import-local@3.2.0: - dependencies: - pkg-dir: 4.2.0 - resolve-cwd: 3.0.0 - - imurmurhash@0.1.4: {} - - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - - inherits@2.0.4: {} - - internal-slot@1.1.0: - dependencies: - es-errors: 1.3.0 - hasown: 2.0.2 - side-channel: 1.1.0 - - intl-messageformat@10.7.17: - dependencies: - '@formatjs/ecma402-abstract': 2.3.5 - '@formatjs/fast-memoize': 2.2.7 - '@formatjs/icu-messageformat-parser': 2.11.3 - tslib: 2.8.1 - - is-array-buffer@3.0.5: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - - is-arrayish@0.2.1: {} - - is-async-function@2.1.1: - dependencies: - async-function: 1.0.0 - call-bound: 1.0.4 - get-proto: 1.0.1 - has-tostringtag: 1.0.2 - safe-regex-test: 1.1.0 - - is-bigint@1.1.0: - dependencies: - has-bigints: 1.1.0 - - is-binary-path@2.1.0: - dependencies: - binary-extensions: 2.3.0 - - is-boolean-object@1.2.2: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-callable@1.2.7: {} - - is-core-module@2.16.1: - dependencies: - hasown: 2.0.2 - - is-data-view@1.0.2: - dependencies: - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - is-typed-array: 1.1.15 - - is-date-object@1.1.0: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-extglob@2.1.1: {} - - is-finalizationregistry@1.1.1: - dependencies: - call-bound: 1.0.4 - - is-fullwidth-code-point@3.0.0: {} - - is-generator-fn@2.1.0: {} - - is-generator-function@1.1.2: - dependencies: - call-bound: 1.0.4 - generator-function: 2.0.1 - get-proto: 1.0.1 - has-tostringtag: 1.0.2 - safe-regex-test: 1.1.0 - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - is-map@2.0.3: {} - - is-negative-zero@2.0.3: {} - - is-number-object@1.1.1: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-number@7.0.0: {} - - is-path-inside@3.0.3: {} - - is-reference@3.0.3: - dependencies: - '@types/estree': 1.0.8 - - is-regex@1.2.1: - dependencies: - call-bound: 1.0.4 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - - is-set@2.0.3: {} - - is-shared-array-buffer@1.0.4: - dependencies: - call-bound: 1.0.4 - - is-stream@2.0.1: {} - - is-string@1.1.1: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-symbol@1.1.1: - dependencies: - call-bound: 1.0.4 - has-symbols: 1.1.0 - safe-regex-test: 1.1.0 - - is-typed-array@1.1.15: - dependencies: - which-typed-array: 1.1.19 - - is-weakmap@2.0.2: {} - - is-weakref@1.1.1: - dependencies: - call-bound: 1.0.4 - - is-weakset@2.0.4: - dependencies: - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - - isarray@2.0.5: {} - - isexe@2.0.0: {} - - istanbul-lib-coverage@3.2.2: {} - - istanbul-lib-instrument@5.2.1: - dependencies: - '@babel/core': 7.28.4 - '@babel/parser': 7.28.4 - '@istanbuljs/schema': 0.1.3 - istanbul-lib-coverage: 3.2.2 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - istanbul-lib-instrument@6.0.3: - dependencies: - '@babel/core': 7.28.4 - '@babel/parser': 7.28.4 - '@istanbuljs/schema': 0.1.3 - istanbul-lib-coverage: 3.2.2 - semver: 7.7.2 - transitivePeerDependencies: - - supports-color - - istanbul-lib-report@3.0.1: - dependencies: - istanbul-lib-coverage: 3.2.2 - make-dir: 4.0.0 - supports-color: 7.2.0 - - istanbul-lib-source-maps@4.0.1: - dependencies: - debug: 4.4.3 - istanbul-lib-coverage: 3.2.2 - source-map: 0.6.1 - transitivePeerDependencies: - - supports-color - - istanbul-reports@3.2.0: - dependencies: - html-escaper: 2.0.2 - istanbul-lib-report: 3.0.1 - - jest-changed-files@29.7.0: - dependencies: - execa: 5.1.1 - jest-util: 29.7.0 - p-limit: 3.1.0 - - jest-circus@29.7.0: - dependencies: - '@jest/environment': 29.7.0 - '@jest/expect': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.18.10 - chalk: 4.1.2 - co: 4.6.0 - dedent: 1.7.0 - is-generator-fn: 2.1.0 - jest-each: 29.7.0 - jest-matcher-utils: 29.7.0 - jest-message-util: 29.7.0 - jest-runtime: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - p-limit: 3.1.0 - pretty-format: 29.7.0 - pure-rand: 6.1.0 - slash: 3.0.0 - stack-utils: 2.0.6 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-cli@29.7.0(@types/node@22.18.10): - dependencies: - '@jest/core': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - chalk: 4.1.2 - create-jest: 29.7.0(@types/node@22.18.10) - exit: 0.1.2 - import-local: 3.2.0 - jest-config: 29.7.0(@types/node@22.18.10) - jest-util: 29.7.0 - jest-validate: 29.7.0 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - - jest-config@29.7.0(@types/node@22.18.10): - dependencies: - '@babel/core': 7.28.4 - '@jest/test-sequencer': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.28.4) - chalk: 4.1.2 - ci-info: 3.9.0 - deepmerge: 4.3.1 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-circus: 29.7.0 - jest-environment-node: 29.7.0 - jest-get-type: 29.6.3 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-runner: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - micromatch: 4.0.8 - parse-json: 5.2.0 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 22.18.10 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-diff@29.7.0: - dependencies: - chalk: 4.1.2 - diff-sequences: 29.6.3 - jest-get-type: 29.6.3 - pretty-format: 29.7.0 - - jest-docblock@29.7.0: - dependencies: - detect-newline: 3.1.0 - - jest-each@29.7.0: - dependencies: - '@jest/types': 29.6.3 - chalk: 4.1.2 - jest-get-type: 29.6.3 - jest-util: 29.7.0 - pretty-format: 29.7.0 - - jest-environment-node@29.7.0: - dependencies: - '@jest/environment': 29.7.0 - '@jest/fake-timers': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.18.10 - jest-mock: 29.7.0 - jest-util: 29.7.0 - - jest-get-type@29.6.3: {} - - jest-haste-map@29.7.0: - dependencies: - '@jest/types': 29.6.3 - '@types/graceful-fs': 4.1.9 - '@types/node': 22.18.10 - anymatch: 3.1.3 - fb-watchman: 2.0.2 - graceful-fs: 4.2.11 - jest-regex-util: 29.6.3 - jest-util: 29.7.0 - jest-worker: 29.7.0 - micromatch: 4.0.8 - walker: 1.0.8 - optionalDependencies: - fsevents: 2.3.3 - - jest-leak-detector@29.7.0: - dependencies: - jest-get-type: 29.6.3 - pretty-format: 29.7.0 - - jest-matcher-utils@29.7.0: - dependencies: - chalk: 4.1.2 - jest-diff: 29.7.0 - jest-get-type: 29.6.3 - pretty-format: 29.7.0 - - jest-message-util@29.7.0: - dependencies: - '@babel/code-frame': 7.27.1 - '@jest/types': 29.6.3 - '@types/stack-utils': 2.0.3 - chalk: 4.1.2 - graceful-fs: 4.2.11 - micromatch: 4.0.8 - pretty-format: 29.7.0 - slash: 3.0.0 - stack-utils: 2.0.6 - - jest-mock@29.7.0: - dependencies: - '@jest/types': 29.6.3 - '@types/node': 22.18.10 - jest-util: 29.7.0 - - jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): - optionalDependencies: - jest-resolve: 29.7.0 - - jest-regex-util@29.6.3: {} - - jest-resolve-dependencies@29.7.0: - dependencies: - jest-regex-util: 29.6.3 - jest-snapshot: 29.7.0 - transitivePeerDependencies: - - supports-color - - jest-resolve@29.7.0: - dependencies: - chalk: 4.1.2 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) - jest-util: 29.7.0 - jest-validate: 29.7.0 - resolve: 1.22.10 - resolve.exports: 2.0.3 - slash: 3.0.0 - - jest-runner@29.7.0: - dependencies: - '@jest/console': 29.7.0 - '@jest/environment': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.18.10 - chalk: 4.1.2 - emittery: 0.13.1 - graceful-fs: 4.2.11 - jest-docblock: 29.7.0 - jest-environment-node: 29.7.0 - jest-haste-map: 29.7.0 - jest-leak-detector: 29.7.0 - jest-message-util: 29.7.0 - jest-resolve: 29.7.0 - jest-runtime: 29.7.0 - jest-util: 29.7.0 - jest-watcher: 29.7.0 - jest-worker: 29.7.0 - p-limit: 3.1.0 - source-map-support: 0.5.13 - transitivePeerDependencies: - - supports-color - - jest-runtime@29.7.0: - dependencies: - '@jest/environment': 29.7.0 - '@jest/fake-timers': 29.7.0 - '@jest/globals': 29.7.0 - '@jest/source-map': 29.6.3 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.18.10 - chalk: 4.1.2 - cjs-module-lexer: 1.4.3 - collect-v8-coverage: 1.0.2 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - jest-message-util: 29.7.0 - jest-mock: 29.7.0 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - slash: 3.0.0 - strip-bom: 4.0.0 - transitivePeerDependencies: - - supports-color - - jest-snapshot@29.7.0: - dependencies: - '@babel/core': 7.28.4 - '@babel/generator': 7.28.3 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.4) - '@babel/types': 7.28.4 - '@jest/expect-utils': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.4) - chalk: 4.1.2 - expect: 29.7.0 - graceful-fs: 4.2.11 - jest-diff: 29.7.0 - jest-get-type: 29.6.3 - jest-matcher-utils: 29.7.0 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - natural-compare: 1.4.0 - pretty-format: 29.7.0 - semver: 7.7.2 - transitivePeerDependencies: - - supports-color - - jest-util@29.7.0: - dependencies: - '@jest/types': 29.6.3 - '@types/node': 22.18.10 - chalk: 4.1.2 - ci-info: 3.9.0 - graceful-fs: 4.2.11 - picomatch: 2.3.1 - - jest-validate@29.7.0: - dependencies: - '@jest/types': 29.6.3 - camelcase: 6.3.0 - chalk: 4.1.2 - jest-get-type: 29.6.3 - leven: 3.1.0 - pretty-format: 29.7.0 - - jest-watcher@29.7.0: - dependencies: - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.18.10 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - emittery: 0.13.1 - jest-util: 29.7.0 - string-length: 4.0.2 - - jest-worker@29.7.0: - dependencies: - '@types/node': 22.18.10 - jest-util: 29.7.0 - merge-stream: 2.0.0 - supports-color: 8.1.1 - - jest@29.7.0(@types/node@22.18.10): - dependencies: - '@jest/core': 29.7.0 - '@jest/types': 29.6.3 - import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@22.18.10) - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - - js-tokens@4.0.0: {} - - js-yaml@3.14.1: - dependencies: - argparse: 1.0.10 - esprima: 4.0.1 - - js-yaml@4.1.0: - dependencies: - argparse: 2.0.1 - - jsesc@3.1.0: {} - - json-buffer@3.0.1: {} - - json-parse-even-better-errors@2.3.1: {} - - json-schema-traverse@0.4.1: {} - - json-stable-stringify-without-jsonify@1.0.1: {} - - json5@1.0.2: - dependencies: - minimist: 1.2.8 - - json5@2.2.3: {} - - jsonfile@6.2.0: - dependencies: - universalify: 2.0.1 - optionalDependencies: - graceful-fs: 4.2.11 - - jwt-simple@0.5.6: {} - - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 - - kleur@3.0.3: {} - - known-css-properties@0.35.0: {} - - leven@3.1.0: {} - - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - - lilconfig@2.1.0: {} - - lines-and-columns@1.2.4: {} - - linkify-it@5.0.0: - dependencies: - uc.micro: 2.1.0 - - locate-character@3.0.0: {} - - locate-path@5.0.0: - dependencies: - p-locate: 4.1.0 - - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - - lodash.memoize@4.1.2: {} - - lodash.merge@4.6.2: {} - - lru-cache@5.1.1: - dependencies: - yallist: 3.1.1 - - magic-string@0.30.19: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - - make-dir@4.0.0: - dependencies: - semver: 7.7.2 - - make-error@1.3.6: {} - - makeerror@1.0.12: - dependencies: - tmpl: 1.0.5 - - markdown-it@14.1.0: - dependencies: - argparse: 2.0.1 - entities: 4.5.0 - linkify-it: 5.0.0 - mdurl: 2.0.0 - punycode.js: 2.3.1 - uc.micro: 2.1.0 - - math-intrinsics@1.1.0: {} - - mdn-data@2.0.30: {} - - mdurl@2.0.0: {} - - merge-stream@2.0.0: {} - - merge2@1.4.1: {} - - micromatch@4.0.8: - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 - - mimic-fn@2.1.0: {} - - min-indent@1.0.1: {} - - minimatch@3.1.2: - dependencies: - brace-expansion: 1.1.12 - - minimatch@9.0.3: - dependencies: - brace-expansion: 2.0.2 - - minimist@1.2.8: {} - - mkdirp@0.5.6: - dependencies: - minimist: 1.2.8 - - ms@2.1.3: {} - - nanoid@3.3.11: {} - - natural-compare@1.4.0: {} - - neo-async@2.6.2: {} - - node-int64@0.4.0: {} - - node-releases@2.0.23: {} - - normalize-path@3.0.0: {} - - npm-run-path@4.0.1: - dependencies: - path-key: 3.1.1 - - object-inspect@1.13.4: {} - - object-keys@1.1.1: {} - - object.assign@4.1.7: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - has-symbols: 1.1.0 - object-keys: 1.1.1 - - object.fromentries@2.0.8: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-object-atoms: 1.1.1 - - object.groupby@1.0.3: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - - object.values@1.2.1: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - - once@1.4.0: - dependencies: - wrappy: 1.0.2 - - onetime@5.1.2: - dependencies: - mimic-fn: 2.1.0 - - optionator@0.9.4: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - - own-keys@1.0.1: - dependencies: - get-intrinsic: 1.3.0 - object-keys: 1.1.1 - safe-push-apply: 1.0.0 - - p-limit@2.3.0: - dependencies: - p-try: 2.2.0 - - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - - p-locate@4.1.0: - dependencies: - p-limit: 2.3.0 - - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - - p-try@2.2.0: {} - - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - - parse-json@5.2.0: - dependencies: - '@babel/code-frame': 7.27.1 - error-ex: 1.3.4 - json-parse-even-better-errors: 2.3.1 - lines-and-columns: 1.2.4 - - path-exists@4.0.0: {} - - path-is-absolute@1.0.1: {} - - path-key@3.1.1: {} - - path-parse@1.0.7: {} - - path-type@4.0.0: {} - - periscopic@3.1.0: - dependencies: - '@types/estree': 1.0.8 - estree-walker: 3.0.3 - is-reference: 3.0.3 - - picocolors@1.1.1: {} - - picomatch@2.3.1: {} - - pirates@4.0.7: {} - - pkg-dir@4.2.0: - dependencies: - find-up: 4.1.0 - - possible-typed-array-names@1.1.0: {} - - postcss-load-config@3.1.4(postcss@8.5.6): - dependencies: - lilconfig: 2.1.0 - yaml: 1.10.2 - optionalDependencies: - postcss: 8.5.6 - - postcss-safe-parser@6.0.0(postcss@8.5.6): - dependencies: - postcss: 8.5.6 - - postcss-scss@4.0.9(postcss@8.5.6): - dependencies: - postcss: 8.5.6 - - postcss-selector-parser@6.1.2: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - - postcss@8.5.6: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - postgres@3.4.7: {} - - prelude-ls@1.2.1: {} - - prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@4.2.20): - dependencies: - prettier: 3.6.2 - svelte: 4.2.20 - - prettier@3.6.2: {} - - pretty-format@29.7.0: - dependencies: - '@jest/schemas': 29.6.3 - ansi-styles: 5.2.0 - react-is: 18.3.1 - - prompts@2.4.2: - dependencies: - kleur: 3.0.3 - sisteransi: 1.0.5 - - punycode.js@2.3.1: {} - - punycode@2.3.1: {} - - pure-rand@6.1.0: {} - - queue-microtask@1.2.3: {} - - react-is@18.3.1: {} - - readdirp@3.6.0: - dependencies: - picomatch: 2.3.1 - - reflect.getprototypeof@1.0.10: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - which-builtin-type: 1.2.1 - - regexp.prototype.flags@1.5.4: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-errors: 1.3.0 - get-proto: 1.0.1 - gopd: 1.2.0 - set-function-name: 2.0.2 - - regexpp@3.2.0: {} - - require-directory@2.1.1: {} - - resolve-cwd@3.0.0: - dependencies: - resolve-from: 5.0.0 - - resolve-from@4.0.0: {} - - resolve-from@5.0.0: {} - - resolve.exports@2.0.3: {} - - resolve@1.22.10: - dependencies: - is-core-module: 2.16.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - reusify@1.1.0: {} - - rimraf@2.7.1: - dependencies: - glob: 7.2.3 - - rimraf@3.0.2: - dependencies: - glob: 7.2.3 - - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - - safe-array-concat@1.1.3: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - has-symbols: 1.1.0 - isarray: 2.0.5 - - safe-push-apply@1.0.0: - dependencies: - es-errors: 1.3.0 - isarray: 2.0.5 - - safe-regex-test@1.1.0: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-regex: 1.2.1 - - sander@0.5.1: - dependencies: - es6-promise: 3.3.1 - graceful-fs: 4.2.11 - mkdirp: 0.5.6 - rimraf: 2.7.1 - - scule@1.3.0: {} - - semver@6.3.1: {} - - semver@7.7.2: {} - - semver@7.7.3: {} - - set-function-length@1.2.2: - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - function-bind: 1.1.2 - get-intrinsic: 1.3.0 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - - set-function-name@2.0.2: - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - functions-have-names: 1.2.3 - has-property-descriptors: 1.0.2 - - set-proto@1.0.0: - dependencies: - dunder-proto: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - side-channel-list@1.0.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - - side-channel-map@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - - side-channel-weakmap@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - side-channel-map: 1.0.1 - - side-channel@1.1.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - side-channel-list: 1.0.0 - side-channel-map: 1.0.1 - side-channel-weakmap: 1.0.2 - - signal-exit@3.0.7: {} - - sisteransi@1.0.5: {} - - slash@3.0.0: {} - - snappyjs@0.7.0: {} - - sorcery@0.11.1: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - buffer-crc32: 1.0.0 - minimist: 1.2.8 - sander: 0.5.1 - - source-map-js@1.2.1: {} - - source-map-support@0.5.13: - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - - source-map@0.6.1: {} - - sprintf-js@1.0.3: {} - - stack-utils@2.0.6: - dependencies: - escape-string-regexp: 2.0.0 - - stop-iteration-iterator@1.1.0: - dependencies: - es-errors: 1.3.0 - internal-slot: 1.1.0 - - string-length@4.0.2: - dependencies: - char-regex: 1.0.2 - strip-ansi: 6.0.1 - - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - string.prototype.trim@1.2.10: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-data-property: 1.1.4 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-object-atoms: 1.1.1 - has-property-descriptors: 1.0.2 - - string.prototype.trimend@1.0.9: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - - string.prototype.trimstart@1.0.8: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - strip-bom@3.0.0: {} - - strip-bom@4.0.0: {} - - strip-final-newline@2.0.0: {} - - strip-indent@3.0.0: - dependencies: - min-indent: 1.0.1 - - strip-json-comments@3.1.1: {} - - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - supports-color@8.1.1: - dependencies: - has-flag: 4.0.0 - - supports-preserve-symlinks-flag@1.0.0: {} - - svelte-eslint-parser@0.33.1(svelte@4.2.20): - dependencies: - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 - postcss: 8.5.6 - postcss-scss: 4.0.9(postcss@8.5.6) - optionalDependencies: - svelte: 4.2.20 - - svelte-eslint-parser@0.43.0(svelte@4.2.20): - dependencies: - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 - postcss: 8.5.6 - postcss-scss: 4.0.9(postcss@8.5.6) - optionalDependencies: - svelte: 4.2.20 - - svelte-preprocess@5.1.4(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6))(postcss@8.5.6)(svelte@4.2.20)(typescript@5.9.3): - dependencies: - '@types/pug': 2.0.10 - detect-indent: 6.1.0 - magic-string: 0.30.19 - sorcery: 0.11.1 - strip-indent: 3.0.0 - svelte: 4.2.20 - optionalDependencies: - '@babel/core': 7.28.4 - postcss: 8.5.6 - postcss-load-config: 3.1.4(postcss@8.5.6) - typescript: 5.9.3 - - svelte2tsx@0.7.45(svelte@4.2.20)(typescript@5.9.3): - dependencies: - dedent-js: 1.0.1 - scule: 1.3.0 - svelte: 4.2.20 - typescript: 5.9.3 - - svelte@4.2.20: - dependencies: - '@ampproject/remapping': 2.3.0 - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 - '@types/estree': 1.0.8 - acorn: 8.15.0 - aria-query: 5.3.2 - axobject-query: 4.1.0 - code-red: 1.0.4 - css-tree: 2.3.1 - estree-walker: 3.0.3 - is-reference: 3.0.3 - locate-character: 3.0.0 - magic-string: 0.30.19 - periscopic: 3.1.0 - - test-exclude@6.0.0: - dependencies: - '@istanbuljs/schema': 0.1.3 - glob: 7.2.3 - minimatch: 3.1.2 - - text-table@0.2.0: {} - - tmpl@1.0.5: {} - - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - - ts-api-utils@1.4.3(typescript@5.9.3): - dependencies: - typescript: 5.9.3 - - ts-jest@29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.10)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10))(typescript@5.9.3): - dependencies: - bs-logger: 0.2.6 - fast-json-stable-stringify: 2.1.0 - handlebars: 4.7.8 - jest: 29.7.0(@types/node@22.18.10) - json5: 2.2.3 - lodash.memoize: 4.1.2 - make-error: 1.3.6 - semver: 7.7.3 - type-fest: 4.41.0 - typescript: 5.9.3 - yargs-parser: 21.1.1 - optionalDependencies: - '@babel/core': 7.28.4 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.28.4) - esbuild: 0.25.10 - jest-util: 29.7.0 - - tsconfig-paths@3.15.0: - dependencies: - '@types/json5': 0.0.29 - json5: 1.0.2 - minimist: 1.2.8 - strip-bom: 3.0.0 - - tslib@2.8.1: {} - - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - - type-detect@4.0.8: {} - - type-fest@0.20.2: {} - - type-fest@0.21.3: {} - - type-fest@4.41.0: {} - - typed-array-buffer@1.0.3: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-typed-array: 1.1.15 - - typed-array-byte-length@1.0.3: - dependencies: - call-bind: 1.0.8 - for-each: 0.3.5 - gopd: 1.2.0 - has-proto: 1.2.0 - is-typed-array: 1.1.15 - - typed-array-byte-offset@1.0.4: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.8 - for-each: 0.3.5 - gopd: 1.2.0 - has-proto: 1.2.0 - is-typed-array: 1.1.15 - reflect.getprototypeof: 1.0.10 - - typed-array-length@1.0.7: - dependencies: - call-bind: 1.0.8 - for-each: 0.3.5 - gopd: 1.2.0 - is-typed-array: 1.1.15 - possible-typed-array-names: 1.1.0 - reflect.getprototypeof: 1.0.10 - - typescript@5.9.3: {} - - uc.micro@2.1.0: {} - - uglify-js@3.19.3: - optional: true - - unbox-primitive@1.1.0: - dependencies: - call-bound: 1.0.4 - has-bigints: 1.1.0 - has-symbols: 1.1.0 - which-boxed-primitive: 1.1.1 - - undici-types@6.21.0: {} - - universalify@2.0.1: {} - - update-browserslist-db@1.1.3(browserslist@4.26.3): - dependencies: - browserslist: 4.26.3 - escalade: 3.2.0 - picocolors: 1.1.1 - - uri-js@4.4.1: - dependencies: - punycode: 2.3.1 - - util-deprecate@1.0.2: {} - - uuid@8.3.2: {} - - v8-to-istanbul@9.3.0: - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - '@types/istanbul-lib-coverage': 2.0.6 - convert-source-map: 2.0.0 - - walker@1.0.8: - dependencies: - makeerror: 1.0.12 - - which-boxed-primitive@1.1.1: - dependencies: - is-bigint: 1.1.0 - is-boolean-object: 1.2.2 - is-number-object: 1.1.1 - is-string: 1.1.1 - is-symbol: 1.1.1 - - which-builtin-type@1.2.1: - dependencies: - call-bound: 1.0.4 - function.prototype.name: 1.1.8 - has-tostringtag: 1.0.2 - is-async-function: 2.1.1 - is-date-object: 1.1.0 - is-finalizationregistry: 1.1.1 - is-generator-function: 1.1.2 - is-regex: 1.2.1 - is-weakref: 1.1.1 - isarray: 2.0.5 - which-boxed-primitive: 1.1.1 - which-collection: 1.0.2 - which-typed-array: 1.1.19 - - which-collection@1.0.2: - dependencies: - is-map: 2.0.3 - is-set: 2.0.3 - is-weakmap: 2.0.2 - is-weakset: 2.0.4 - - which-typed-array@1.1.19: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.8 - call-bound: 1.0.4 - for-each: 0.3.5 - get-proto: 1.0.1 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - word-wrap@1.2.5: {} - - wordwrap@1.0.0: {} - - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - wrappy@1.0.2: {} - - write-file-atomic@4.0.2: - dependencies: - imurmurhash: 0.1.4 - signal-exit: 3.0.7 - - y18n@5.0.8: {} - - yallist@3.1.1: {} - - yaml@1.10.2: {} - - yargs-parser@21.1.1: {} - - yargs@17.7.2: - dependencies: - cliui: 8.0.1 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 - - yocto-queue@0.1.0: {} - - zod@3.25.76: {} diff --git a/foundations/communication/common/config/rush/repo-state.json b/foundations/communication/common/config/rush/repo-state.json deleted file mode 100644 index 0e7b144099..0000000000 --- a/foundations/communication/common/config/rush/repo-state.json +++ /dev/null @@ -1,4 +0,0 @@ -// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. -{ - "preferredVersionsHash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f" -} diff --git a/foundations/communication/common/config/rush/rush-plugins.json b/foundations/communication/common/config/rush/rush-plugins.json deleted file mode 100644 index 752e373213..0000000000 --- a/foundations/communication/common/config/rush/rush-plugins.json +++ /dev/null @@ -1,29 +0,0 @@ -/** - * This configuration file manages Rush's plugin feature. - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush-plugins.schema.json", - "plugins": [ - /** - * Each item configures a plugin to be loaded by Rush. - */ - // { - // /** - // * The name of the NPM package that provides the plugin. - // */ - // "packageName": "@scope/my-rush-plugin", - // /** - // * The name of the plugin. This can be found in the "pluginName" - // * field of the "rush-plugin-manifest.json" file in the NPM package folder. - // */ - // "pluginName": "my-plugin-name", - // /** - // * The name of a Rush autoinstaller that will be used for installation, which - // * can be created using "rush init-autoinstaller". Add the plugin's NPM package - // * to the package.json "dependencies" of your autoinstaller, then run - // * "rush update-autoinstaller". - // */ - // "autoinstallerName": "rush-plugins" - // } - ] -} \ No newline at end of file diff --git a/foundations/communication/common/config/rush/subspaces.json b/foundations/communication/common/config/rush/subspaces.json deleted file mode 100644 index d3c3ae8c51..0000000000 --- a/foundations/communication/common/config/rush/subspaces.json +++ /dev/null @@ -1,35 +0,0 @@ -/** - * This configuration file manages the experimental "subspaces" feature for Rush, - * which allows multiple PNPM lockfiles to be used in a single Rush workspace. - * For full documentation, please see https://rushjs.io - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/subspaces.schema.json", - - /** - * Set this flag to "true" to enable usage of subspaces. - */ - "subspacesEnabled": false, - - /** - * (DEPRECATED) This is a temporary workaround for migrating from an earlier prototype - * of this feature: https://github.com/microsoft/rushstack/pull/3481 - * It allows subspaces with only one project to store their config files in the project folder. - */ - "splitWorkspaceCompatibility": false, - - /** - * When a command such as "rush update" is invoked without the "--subspace" or "--to" - * parameters, Rush will install all subspaces. In a huge monorepo with numerous subspaces, - * this would be extremely slow. Set "preventSelectingAllSubspaces" to true to avoid this - * mistake by always requiring selection parameters for commands such as "rush update". - */ - "preventSelectingAllSubspaces": false, - - /** - * The list of subspace names, which should be lowercase alphanumeric words separated by - * hyphens, for example "my-subspace". The corresponding config files will have paths - * such as "common/config/subspaces/my-subspace/package-lock.yaml". - */ - "subspaceNames": [] -} diff --git a/foundations/communication/common/config/rush/version-policies.json b/foundations/communication/common/config/rush/version-policies.json deleted file mode 100644 index d03b73a3b6..0000000000 --- a/foundations/communication/common/config/rush/version-policies.json +++ /dev/null @@ -1,102 +0,0 @@ -/** - * This is configuration file is used for advanced publishing configurations with Rush. - * More documentation is available on the Rush website: https://rushjs.io - */ - -/** - * A list of version policy definitions. A "version policy" is a custom package versioning - * strategy that affects "rush change", "rush version", and "rush publish". The strategy applies - * to a set of projects that are specified using the "versionPolicyName" field in rush.json. - */ -[ - // { - // /** - // * (Required) Indicates the kind of version policy being defined ("lockStepVersion" or "individualVersion"). - // * - // * The "lockStepVersion" mode specifies that the projects will use "lock-step versioning". This - // * strategy is appropriate for a set of packages that act as selectable components of a - // * unified product. The entire set of packages are always published together, and always share - // * the same NPM version number. When the packages depend on other packages in the set, the - // * SemVer range is usually restricted to a single version. - // */ - // "definitionName": "lockStepVersion", - // - // /** - // * (Required) The name that will be used for the "versionPolicyName" field in rush.json. - // * This name is also used command-line parameters such as "--version-policy" - // * and "--to-version-policy". - // */ - // "policyName": "MyBigFramework", - // - // /** - // * (Required) The current version. All packages belonging to the set should have this version - // * in the current branch. When bumping versions, Rush uses this to determine the next version. - // * (The "version" field in package.json is NOT considered.) - // */ - // "version": "1.0.0", - // - // /** - // * (Required) The type of bump that will be performed when publishing the next release. - // * When creating a release branch in Git, this field should be updated according to the - // * type of release. - // * - // * Valid values are: "prerelease", "preminor", "minor", "patch", "major" - // */ - // "nextBump": "prerelease", - // - // /** - // * (Optional) If specified, all packages in the set share a common CHANGELOG.md file. - // * This file is stored with the specified "main" project, which must be a member of the set. - // * - // * If this field is omitted, then a separate CHANGELOG.md file will be maintained for each - // * package in the set. - // */ - // "mainProject": "my-app", - // - // /** - // * (Optional) If enabled, the "rush change" command will prompt the user for their email address - // * and include it in the JSON change files. If an organization maintains multiple repos, tracking - // * this contact information may be useful for a service that automatically upgrades packages and - // * needs to notify engineers whose change may be responsible for a downstream build break. It might - // * also be useful for crediting contributors. Rush itself does not do anything with the collected - // * email addresses. The default value is "false". - // */ - // // "includeEmailInChangeFile": true - // }, - // - // { - // /** - // * (Required) Indicates the kind of version policy being defined ("lockStepVersion" or "individualVersion"). - // * - // * The "individualVersion" mode specifies that the projects will use "individual versioning". - // * This is the typical NPM model where each package has an independent version number - // * and CHANGELOG.md file. Although a single CI definition is responsible for publishing the - // * packages, they otherwise don't have any special relationship. The version bumping will - // * depend on how developers answer the "rush change" questions for each package that - // * is changed. - // */ - // "definitionName": "individualVersion", - // - // "policyName": "MyRandomLibraries", - // - // /** - // * (Optional) This can be used to enforce that all packages in the set must share a common - // * major version number, e.g. because they are from the same major release branch. - // * It can also be used to discourage people from accidentally making "MAJOR" SemVer changes - // * inappropriately. The minor/patch version parts will be bumped independently according - // * to the types of changes made to each project, according to the "rush change" command. - // */ - // "lockedMajor": 3, - // - // /** - // * (Optional) When publishing is managed by Rush, by default the "rush change" command will - // * request changes for any projects that are modified by a pull request. These change entries - // * will produce a CHANGELOG.md file. If you author your CHANGELOG.md manually or announce updates - // * in some other way, set "exemptFromRushChange" to true to tell "rush change" to ignore the projects - // * belonging to this version policy. - // */ - // "exemptFromRushChange": false, - // - // // "includeEmailInChangeFile": true - // } -] diff --git a/foundations/communication/common/git-hooks/commit-msg.sample b/foundations/communication/common/git-hooks/commit-msg.sample deleted file mode 100644 index 59cacb80ca..0000000000 --- a/foundations/communication/common/git-hooks/commit-msg.sample +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/sh -# -# This is an example Git hook for use with Rush. To enable this hook, rename this file -# to "commit-msg" and then run "rush install", which will copy it from common/git-hooks -# to the .git/hooks folder. -# -# TO LEARN MORE ABOUT GIT HOOKS -# -# The Git documentation is here: https://git-scm.com/docs/githooks -# Some helpful resources: https://githooks.com -# -# ABOUT THIS EXAMPLE -# -# The commit-msg hook is called by "git commit" with one argument, the name of the file -# that has the commit message. The hook should exit with non-zero status after issuing -# an appropriate message if it wants to stop the commit. The hook is allowed to edit -# the commit message file. - -# This example enforces that commit message should contain a minimum amount of -# description text. -if [ `cat $1 | wc -w` -lt 3 ]; then - echo "" - echo "Invalid commit message: The message must contain at least 3 words." - exit 1 -fi diff --git a/foundations/communication/common/scripts/install-run-rush-pnpm.js b/foundations/communication/common/scripts/install-run-rush-pnpm.js deleted file mode 100644 index 2356649f4e..0000000000 --- a/foundations/communication/common/scripts/install-run-rush-pnpm.js +++ /dev/null @@ -1,31 +0,0 @@ -// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED. -// -// This script is intended for usage in an automated build environment where the Rush command may not have -// been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush -// specified in the rush.json configuration file (if not already installed), and then pass a command-line to the -// rush-pnpm command. -// -// An example usage would be: -// -// node common/scripts/install-run-rush-pnpm.js pnpm-command -// -// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/ -// -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See the @microsoft/rush package's LICENSE file for details. - -/******/ (() => { // webpackBootstrap -/******/ "use strict"; -var __webpack_exports__ = {}; -/*!*****************************************************!*\ - !*** ./lib-esnext/scripts/install-run-rush-pnpm.js ***! - \*****************************************************/ - -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. -require('./install-run-rush'); -//# sourceMappingURL=install-run-rush-pnpm.js.map -module.exports = __webpack_exports__; -/******/ })() -; -//# sourceMappingURL=install-run-rush-pnpm.js.map \ No newline at end of file diff --git a/foundations/communication/common/scripts/install-run-rush.js b/foundations/communication/common/scripts/install-run-rush.js deleted file mode 100644 index ef1d697f9c..0000000000 --- a/foundations/communication/common/scripts/install-run-rush.js +++ /dev/null @@ -1,218 +0,0 @@ -// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED. -// -// This script is intended for usage in an automated build environment where the Rush command may not have -// been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush -// specified in the rush.json configuration file (if not already installed), and then pass a command-line to it. -// An example usage would be: -// -// node common/scripts/install-run-rush.js install -// -// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/ -// -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See the @microsoft/rush package's LICENSE file for details. - -/******/ (() => { // webpackBootstrap -/******/ "use strict"; -/******/ var __webpack_modules__ = ({ - -/***/ 16928: -/*!***********************!*\ - !*** external "path" ***! - \***********************/ -/***/ ((module) => { - -module.exports = require("path"); - -/***/ }), - -/***/ 179896: -/*!*********************!*\ - !*** external "fs" ***! - \*********************/ -/***/ ((module) => { - -module.exports = require("fs"); - -/***/ }) - -/******/ }); -/************************************************************************/ -/******/ // The module cache -/******/ var __webpack_module_cache__ = {}; -/******/ -/******/ // The require function -/******/ function __webpack_require__(moduleId) { -/******/ // Check if module is in cache -/******/ var cachedModule = __webpack_module_cache__[moduleId]; -/******/ if (cachedModule !== undefined) { -/******/ return cachedModule.exports; -/******/ } -/******/ // Create a new module (and put it into the cache) -/******/ var module = __webpack_module_cache__[moduleId] = { -/******/ // no module.id needed -/******/ // no module.loaded needed -/******/ exports: {} -/******/ }; -/******/ -/******/ // Execute the module function -/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); -/******/ -/******/ // Return the exports of the module -/******/ return module.exports; -/******/ } -/******/ -/************************************************************************/ -/******/ /* webpack/runtime/compat get default export */ -/******/ (() => { -/******/ // getDefaultExport function for compatibility with non-harmony modules -/******/ __webpack_require__.n = (module) => { -/******/ var getter = module && module.__esModule ? -/******/ () => (module['default']) : -/******/ () => (module); -/******/ __webpack_require__.d(getter, { a: getter }); -/******/ return getter; -/******/ }; -/******/ })(); -/******/ -/******/ /* webpack/runtime/define property getters */ -/******/ (() => { -/******/ // define getter functions for harmony exports -/******/ __webpack_require__.d = (exports, definition) => { -/******/ for(var key in definition) { -/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { -/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); -/******/ } -/******/ } -/******/ }; -/******/ })(); -/******/ -/******/ /* webpack/runtime/hasOwnProperty shorthand */ -/******/ (() => { -/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) -/******/ })(); -/******/ -/******/ /* webpack/runtime/make namespace object */ -/******/ (() => { -/******/ // define __esModule on exports -/******/ __webpack_require__.r = (exports) => { -/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { -/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); -/******/ } -/******/ Object.defineProperty(exports, '__esModule', { value: true }); -/******/ }; -/******/ })(); -/******/ -/************************************************************************/ -var __webpack_exports__ = {}; -// This entry needs to be wrapped in an IIFE because it needs to be isolated against other modules in the chunk. -(() => { -/*!************************************************!*\ - !*** ./lib-esnext/scripts/install-run-rush.js ***! - \************************************************/ -__webpack_require__.r(__webpack_exports__); -/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! path */ 16928); -/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! fs */ 179896); -/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_1__); -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. -/* eslint-disable no-console */ - - -const { installAndRun, findRushJsonFolder, RUSH_JSON_FILENAME, runWithErrorAndStatusCode } = require('./install-run'); -const PACKAGE_NAME = '@microsoft/rush'; -const RUSH_PREVIEW_VERSION = 'RUSH_PREVIEW_VERSION'; -const INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE = 'INSTALL_RUN_RUSH_LOCKFILE_PATH'; -function _getRushVersion(logger) { - const rushPreviewVersion = process.env[RUSH_PREVIEW_VERSION]; - if (rushPreviewVersion !== undefined) { - logger.info(`Using Rush version from environment variable ${RUSH_PREVIEW_VERSION}=${rushPreviewVersion}`); - return rushPreviewVersion; - } - const rushJsonFolder = findRushJsonFolder(); - const rushJsonPath = path__WEBPACK_IMPORTED_MODULE_0__.join(rushJsonFolder, RUSH_JSON_FILENAME); - try { - const rushJsonContents = fs__WEBPACK_IMPORTED_MODULE_1__.readFileSync(rushJsonPath, 'utf-8'); - // Use a regular expression to parse out the rushVersion value because rush.json supports comments, - // but JSON.parse does not and we don't want to pull in more dependencies than we need to in this script. - const rushJsonMatches = rushJsonContents.match(/\"rushVersion\"\s*\:\s*\"([0-9a-zA-Z.+\-]+)\"/); - return rushJsonMatches[1]; - } - catch (e) { - throw new Error(`Unable to determine the required version of Rush from ${RUSH_JSON_FILENAME} (${rushJsonFolder}). ` + - `The 'rushVersion' field is either not assigned in ${RUSH_JSON_FILENAME} or was specified ` + - 'using an unexpected syntax.'); - } -} -function _getBin(scriptName) { - switch (scriptName.toLowerCase()) { - case 'install-run-rush-pnpm.js': - return 'rush-pnpm'; - case 'install-run-rushx.js': - return 'rushx'; - default: - return 'rush'; - } -} -function _run() { - const [nodePath /* Ex: /bin/node */, scriptPath /* /repo/common/scripts/install-run-rush.js */, ...packageBinArgs /* [build, --to, myproject] */] = process.argv; - // Detect if this script was directly invoked, or if the install-run-rushx script was invokved to select the - // appropriate binary inside the rush package to run - const scriptName = path__WEBPACK_IMPORTED_MODULE_0__.basename(scriptPath); - const bin = _getBin(scriptName); - if (!nodePath || !scriptPath) { - throw new Error('Unexpected exception: could not detect node path or script path'); - } - let commandFound = false; - let logger = { info: console.log, error: console.error }; - for (const arg of packageBinArgs) { - if (arg === '-q' || arg === '--quiet') { - // The -q/--quiet flag is supported by both `rush` and `rushx`, and will suppress - // any normal informational/diagnostic information printed during startup. - // - // To maintain the same user experience, the install-run* scripts pass along this - // flag but also use it to suppress any diagnostic information normally printed - // to stdout. - logger = { - info: () => { }, - error: console.error - }; - } - else if (!arg.startsWith('-') || arg === '-h' || arg === '--help') { - // We either found something that looks like a command (i.e. - doesn't start with a "-"), - // or we found the -h/--help flag, which can be run without a command - commandFound = true; - } - } - if (!commandFound) { - console.log(`Usage: ${scriptName} [args...]`); - if (scriptName === 'install-run-rush-pnpm.js') { - console.log(`Example: ${scriptName} pnpm-command`); - } - else if (scriptName === 'install-run-rush.js') { - console.log(`Example: ${scriptName} build --to myproject`); - } - else { - console.log(`Example: ${scriptName} custom-command`); - } - process.exit(1); - } - runWithErrorAndStatusCode(logger, () => { - const version = _getRushVersion(logger); - logger.info(`The ${RUSH_JSON_FILENAME} configuration requests Rush version ${version}`); - const lockFilePath = process.env[INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE]; - if (lockFilePath) { - logger.info(`Found ${INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE}="${lockFilePath}", installing with lockfile.`); - } - return installAndRun(logger, PACKAGE_NAME, version, bin, packageBinArgs, lockFilePath); - }); -} -_run(); -//# sourceMappingURL=install-run-rush.js.map -})(); - -module.exports = __webpack_exports__; -/******/ })() -; -//# sourceMappingURL=install-run-rush.js.map \ No newline at end of file diff --git a/foundations/communication/common/scripts/install-run-rushx.js b/foundations/communication/common/scripts/install-run-rushx.js deleted file mode 100644 index 6581521f3c..0000000000 --- a/foundations/communication/common/scripts/install-run-rushx.js +++ /dev/null @@ -1,31 +0,0 @@ -// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED. -// -// This script is intended for usage in an automated build environment where the Rush command may not have -// been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush -// specified in the rush.json configuration file (if not already installed), and then pass a command-line to the -// rushx command. -// -// An example usage would be: -// -// node common/scripts/install-run-rushx.js custom-command -// -// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/ -// -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See the @microsoft/rush package's LICENSE file for details. - -/******/ (() => { // webpackBootstrap -/******/ "use strict"; -var __webpack_exports__ = {}; -/*!*************************************************!*\ - !*** ./lib-esnext/scripts/install-run-rushx.js ***! - \*************************************************/ - -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. -require('./install-run-rush'); -//# sourceMappingURL=install-run-rushx.js.map -module.exports = __webpack_exports__; -/******/ })() -; -//# sourceMappingURL=install-run-rushx.js.map \ No newline at end of file diff --git a/foundations/communication/common/scripts/install-run.js b/foundations/communication/common/scripts/install-run.js deleted file mode 100644 index a35726bf16..0000000000 --- a/foundations/communication/common/scripts/install-run.js +++ /dev/null @@ -1,778 +0,0 @@ -// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED. -// -// This script is intended for usage in an automated build environment where a Node tool may not have -// been preinstalled, or may have an unpredictable version. This script will automatically install the specified -// version of the specified tool (if not already installed), and then pass a command-line to it. -// An example usage would be: -// -// node common/scripts/install-run.js qrcode@1.2.2 qrcode https://rushjs.io -// -// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/ -// -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See the @microsoft/rush package's LICENSE file for details. - -/******/ (() => { // webpackBootstrap -/******/ "use strict"; -/******/ var __webpack_modules__ = ({ - -/***/ 16928: -/*!***********************!*\ - !*** external "path" ***! - \***********************/ -/***/ ((module) => { - -module.exports = require("path"); - -/***/ }), - -/***/ 179896: -/*!*********************!*\ - !*** external "fs" ***! - \*********************/ -/***/ ((module) => { - -module.exports = require("fs"); - -/***/ }), - -/***/ 370857: -/*!*********************!*\ - !*** external "os" ***! - \*********************/ -/***/ ((module) => { - -module.exports = require("os"); - -/***/ }), - -/***/ 535317: -/*!********************************!*\ - !*** external "child_process" ***! - \********************************/ -/***/ ((module) => { - -module.exports = require("child_process"); - -/***/ }), - -/***/ 832286: -/*!************************************************!*\ - !*** ./lib-esnext/utilities/npmrcUtilities.js ***! - \************************************************/ -/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { - -__webpack_require__.r(__webpack_exports__); -/* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ isVariableSetInNpmrcFile: () => (/* binding */ isVariableSetInNpmrcFile), -/* harmony export */ syncNpmrc: () => (/* binding */ syncNpmrc), -/* harmony export */ trimNpmrcFileLines: () => (/* binding */ trimNpmrcFileLines) -/* harmony export */ }); -/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! fs */ 179896); -/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! path */ 16928); -/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_1__); -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. -// IMPORTANT - do not use any non-built-in libraries in this file - - -/** - * This function reads the content for given .npmrc file path, and also trims - * unusable lines from the .npmrc file. - * - * @returns - * The text of the the .npmrc. - */ -// create a global _combinedNpmrc for cache purpose -const _combinedNpmrcMap = new Map(); -function _trimNpmrcFile(options) { - const { sourceNpmrcPath, linesToPrepend, linesToAppend, supportEnvVarFallbackSyntax } = options; - const combinedNpmrcFromCache = _combinedNpmrcMap.get(sourceNpmrcPath); - if (combinedNpmrcFromCache !== undefined) { - return combinedNpmrcFromCache; - } - let npmrcFileLines = []; - if (linesToPrepend) { - npmrcFileLines.push(...linesToPrepend); - } - if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(sourceNpmrcPath)) { - npmrcFileLines.push(...fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(sourceNpmrcPath).toString().split('\n')); - } - if (linesToAppend) { - npmrcFileLines.push(...linesToAppend); - } - npmrcFileLines = npmrcFileLines.map((line) => (line || '').trim()); - const resultLines = trimNpmrcFileLines(npmrcFileLines, process.env, supportEnvVarFallbackSyntax); - const combinedNpmrc = resultLines.join('\n'); - //save the cache - _combinedNpmrcMap.set(sourceNpmrcPath, combinedNpmrc); - return combinedNpmrc; -} -/** - * - * @param npmrcFileLines The npmrc file's lines - * @param env The environment variables object - * @param supportEnvVarFallbackSyntax Whether to support fallback values in the form of `${VAR_NAME:-fallback}` - * @returns - */ -function trimNpmrcFileLines(npmrcFileLines, env, supportEnvVarFallbackSyntax) { - var _a; - const resultLines = []; - // This finds environment variable tokens that look like "${VAR_NAME}" - const expansionRegExp = /\$\{([^\}]+)\}/g; - // Comment lines start with "#" or ";" - const commentRegExp = /^\s*[#;]/; - // Trim out lines that reference environment variables that aren't defined - for (let line of npmrcFileLines) { - let lineShouldBeTrimmed = false; - //remove spaces before or after key and value - line = line - .split('=') - .map((lineToTrim) => lineToTrim.trim()) - .join('='); - // Ignore comment lines - if (!commentRegExp.test(line)) { - const environmentVariables = line.match(expansionRegExp); - if (environmentVariables) { - for (const token of environmentVariables) { - /** - * Remove the leading "${" and the trailing "}" from the token - * - * ${nameString} -> nameString - * ${nameString-fallbackString} -> name-fallbackString - * ${nameString:-fallbackString} -> name:-fallbackString - */ - const nameWithFallback = token.substring(2, token.length - 1); - let environmentVariableName; - let fallback; - if (supportEnvVarFallbackSyntax) { - /** - * Get the environment variable name and fallback value. - * - * name fallback - * nameString -> nameString undefined - * nameString-fallbackString -> nameString fallbackString - * nameString:-fallbackString -> nameString fallbackString - */ - const matched = nameWithFallback.match(/^([^:-]+)(?:\:?-(.+))?$/); - // matched: [originStr, variableName, fallback] - environmentVariableName = (_a = matched === null || matched === void 0 ? void 0 : matched[1]) !== null && _a !== void 0 ? _a : nameWithFallback; - fallback = matched === null || matched === void 0 ? void 0 : matched[2]; - } - else { - environmentVariableName = nameWithFallback; - } - // Is the environment variable and fallback value defined. - if (!env[environmentVariableName] && !fallback) { - // No, so trim this line - lineShouldBeTrimmed = true; - break; - } - } - } - } - if (lineShouldBeTrimmed) { - // Example output: - // "; MISSING ENVIRONMENT VARIABLE: //my-registry.com/npm/:_authToken=${MY_AUTH_TOKEN}" - resultLines.push('; MISSING ENVIRONMENT VARIABLE: ' + line); - } - else { - resultLines.push(line); - } - } - return resultLines; -} -function _copyAndTrimNpmrcFile(options) { - const { logger, sourceNpmrcPath, targetNpmrcPath } = options; - logger.info(`Transforming ${sourceNpmrcPath}`); // Verbose - logger.info(` --> "${targetNpmrcPath}"`); - const combinedNpmrc = _trimNpmrcFile(options); - fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(targetNpmrcPath, combinedNpmrc); - return combinedNpmrc; -} -function syncNpmrc(options) { - const { sourceNpmrcFolder, targetNpmrcFolder, useNpmrcPublish, logger = { - // eslint-disable-next-line no-console - info: console.log, - // eslint-disable-next-line no-console - error: console.error - }, createIfMissing = false } = options; - const sourceNpmrcPath = path__WEBPACK_IMPORTED_MODULE_1__.join(sourceNpmrcFolder, !useNpmrcPublish ? '.npmrc' : '.npmrc-publish'); - const targetNpmrcPath = path__WEBPACK_IMPORTED_MODULE_1__.join(targetNpmrcFolder, '.npmrc'); - try { - if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(sourceNpmrcPath) || createIfMissing) { - // Ensure the target folder exists - if (!fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(targetNpmrcFolder)) { - fs__WEBPACK_IMPORTED_MODULE_0__.mkdirSync(targetNpmrcFolder, { recursive: true }); - } - return _copyAndTrimNpmrcFile({ - sourceNpmrcPath, - targetNpmrcPath, - logger, - ...options - }); - } - else if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(targetNpmrcPath)) { - // If the source .npmrc doesn't exist and there is one in the target, delete the one in the target - logger.info(`Deleting ${targetNpmrcPath}`); // Verbose - fs__WEBPACK_IMPORTED_MODULE_0__.unlinkSync(targetNpmrcPath); - } - } - catch (e) { - throw new Error(`Error syncing .npmrc file: ${e}`); - } -} -function isVariableSetInNpmrcFile(sourceNpmrcFolder, variableKey, supportEnvVarFallbackSyntax) { - const sourceNpmrcPath = `${sourceNpmrcFolder}/.npmrc`; - //if .npmrc file does not exist, return false directly - if (!fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(sourceNpmrcPath)) { - return false; - } - const trimmedNpmrcFile = _trimNpmrcFile({ sourceNpmrcPath, supportEnvVarFallbackSyntax }); - const variableKeyRegExp = new RegExp(`^${variableKey}=`, 'm'); - return trimmedNpmrcFile.match(variableKeyRegExp) !== null; -} -//# sourceMappingURL=npmrcUtilities.js.map - -/***/ }) - -/******/ }); -/************************************************************************/ -/******/ // The module cache -/******/ var __webpack_module_cache__ = {}; -/******/ -/******/ // The require function -/******/ function __webpack_require__(moduleId) { -/******/ // Check if module is in cache -/******/ var cachedModule = __webpack_module_cache__[moduleId]; -/******/ if (cachedModule !== undefined) { -/******/ return cachedModule.exports; -/******/ } -/******/ // Create a new module (and put it into the cache) -/******/ var module = __webpack_module_cache__[moduleId] = { -/******/ // no module.id needed -/******/ // no module.loaded needed -/******/ exports: {} -/******/ }; -/******/ -/******/ // Execute the module function -/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); -/******/ -/******/ // Return the exports of the module -/******/ return module.exports; -/******/ } -/******/ -/************************************************************************/ -/******/ /* webpack/runtime/compat get default export */ -/******/ (() => { -/******/ // getDefaultExport function for compatibility with non-harmony modules -/******/ __webpack_require__.n = (module) => { -/******/ var getter = module && module.__esModule ? -/******/ () => (module['default']) : -/******/ () => (module); -/******/ __webpack_require__.d(getter, { a: getter }); -/******/ return getter; -/******/ }; -/******/ })(); -/******/ -/******/ /* webpack/runtime/define property getters */ -/******/ (() => { -/******/ // define getter functions for harmony exports -/******/ __webpack_require__.d = (exports, definition) => { -/******/ for(var key in definition) { -/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { -/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); -/******/ } -/******/ } -/******/ }; -/******/ })(); -/******/ -/******/ /* webpack/runtime/hasOwnProperty shorthand */ -/******/ (() => { -/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) -/******/ })(); -/******/ -/******/ /* webpack/runtime/make namespace object */ -/******/ (() => { -/******/ // define __esModule on exports -/******/ __webpack_require__.r = (exports) => { -/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { -/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); -/******/ } -/******/ Object.defineProperty(exports, '__esModule', { value: true }); -/******/ }; -/******/ })(); -/******/ -/************************************************************************/ -var __webpack_exports__ = {}; -// This entry needs to be wrapped in an IIFE because it needs to be isolated against other modules in the chunk. -(() => { -/*!*******************************************!*\ - !*** ./lib-esnext/scripts/install-run.js ***! - \*******************************************/ -__webpack_require__.r(__webpack_exports__); -/* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ RUSH_JSON_FILENAME: () => (/* binding */ RUSH_JSON_FILENAME), -/* harmony export */ findRushJsonFolder: () => (/* binding */ findRushJsonFolder), -/* harmony export */ getNpmPath: () => (/* binding */ getNpmPath), -/* harmony export */ installAndRun: () => (/* binding */ installAndRun), -/* harmony export */ runWithErrorAndStatusCode: () => (/* binding */ runWithErrorAndStatusCode) -/* harmony export */ }); -/* harmony import */ var child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! child_process */ 535317); -/* harmony import */ var child_process__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(child_process__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! fs */ 179896); -/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var os__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! os */ 370857); -/* harmony import */ var os__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(os__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! path */ 16928); -/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_3__); -/* harmony import */ var _utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../utilities/npmrcUtilities */ 832286); -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. -/* eslint-disable no-console */ - - - - - -const RUSH_JSON_FILENAME = 'rush.json'; -const RUSH_TEMP_FOLDER_ENV_VARIABLE_NAME = 'RUSH_TEMP_FOLDER'; -const INSTALL_RUN_LOCKFILE_PATH_VARIABLE = 'INSTALL_RUN_LOCKFILE_PATH'; -const INSTALLED_FLAG_FILENAME = 'installed.flag'; -const NODE_MODULES_FOLDER_NAME = 'node_modules'; -const PACKAGE_JSON_FILENAME = 'package.json'; -/** - * Parse a package specifier (in the form of name\@version) into name and version parts. - */ -function _parsePackageSpecifier(rawPackageSpecifier) { - rawPackageSpecifier = (rawPackageSpecifier || '').trim(); - const separatorIndex = rawPackageSpecifier.lastIndexOf('@'); - let name; - let version = undefined; - if (separatorIndex === 0) { - // The specifier starts with a scope and doesn't have a version specified - name = rawPackageSpecifier; - } - else if (separatorIndex === -1) { - // The specifier doesn't have a version - name = rawPackageSpecifier; - } - else { - name = rawPackageSpecifier.substring(0, separatorIndex); - version = rawPackageSpecifier.substring(separatorIndex + 1); - } - if (!name) { - throw new Error(`Invalid package specifier: ${rawPackageSpecifier}`); - } - return { name, version }; -} -let _npmPath = undefined; -/** - * Get the absolute path to the npm executable - */ -function getNpmPath() { - if (!_npmPath) { - try { - if (_isWindows()) { - // We're on Windows - const whereOutput = child_process__WEBPACK_IMPORTED_MODULE_0__.execSync('where npm', { stdio: [] }).toString(); - const lines = whereOutput.split(os__WEBPACK_IMPORTED_MODULE_2__.EOL).filter((line) => !!line); - // take the last result, we are looking for a .cmd command - // see https://github.com/microsoft/rushstack/issues/759 - _npmPath = lines[lines.length - 1]; - } - else { - // We aren't on Windows - assume we're on *NIX or Darwin - _npmPath = child_process__WEBPACK_IMPORTED_MODULE_0__.execSync('command -v npm', { stdio: [] }).toString(); - } - } - catch (e) { - throw new Error(`Unable to determine the path to the NPM tool: ${e}`); - } - _npmPath = _npmPath.trim(); - if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(_npmPath)) { - throw new Error('The NPM executable does not exist'); - } - } - return _npmPath; -} -function _ensureFolder(folderPath) { - if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(folderPath)) { - const parentDir = path__WEBPACK_IMPORTED_MODULE_3__.dirname(folderPath); - _ensureFolder(parentDir); - fs__WEBPACK_IMPORTED_MODULE_1__.mkdirSync(folderPath); - } -} -/** - * Create missing directories under the specified base directory, and return the resolved directory. - * - * Does not support "." or ".." path segments. - * Assumes the baseFolder exists. - */ -function _ensureAndJoinPath(baseFolder, ...pathSegments) { - let joinedPath = baseFolder; - try { - for (let pathSegment of pathSegments) { - pathSegment = pathSegment.replace(/[\\\/]/g, '+'); - joinedPath = path__WEBPACK_IMPORTED_MODULE_3__.join(joinedPath, pathSegment); - if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(joinedPath)) { - fs__WEBPACK_IMPORTED_MODULE_1__.mkdirSync(joinedPath); - } - } - } - catch (e) { - throw new Error(`Error building local installation folder (${path__WEBPACK_IMPORTED_MODULE_3__.join(baseFolder, ...pathSegments)}): ${e}`); - } - return joinedPath; -} -function _getRushTempFolder(rushCommonFolder) { - const rushTempFolder = process.env[RUSH_TEMP_FOLDER_ENV_VARIABLE_NAME]; - if (rushTempFolder !== undefined) { - _ensureFolder(rushTempFolder); - return rushTempFolder; - } - else { - return _ensureAndJoinPath(rushCommonFolder, 'temp'); - } -} -/** - * Compare version strings according to semantic versioning. - * Returns a positive integer if "a" is a later version than "b", - * a negative integer if "b" is later than "a", - * and 0 otherwise. - */ -function _compareVersionStrings(a, b) { - const aParts = a.split(/[.-]/); - const bParts = b.split(/[.-]/); - const numberOfParts = Math.max(aParts.length, bParts.length); - for (let i = 0; i < numberOfParts; i++) { - if (aParts[i] !== bParts[i]) { - return (Number(aParts[i]) || 0) - (Number(bParts[i]) || 0); - } - } - return 0; -} -/** - * Resolve a package specifier to a static version - */ -function _resolvePackageVersion(logger, rushCommonFolder, { name, version }) { - if (!version) { - version = '*'; // If no version is specified, use the latest version - } - if (version.match(/^[a-zA-Z0-9\-\+\.]+$/)) { - // If the version contains only characters that we recognize to be used in static version specifiers, - // pass the version through - return version; - } - else { - // version resolves to - try { - const rushTempFolder = _getRushTempFolder(rushCommonFolder); - const sourceNpmrcFolder = path__WEBPACK_IMPORTED_MODULE_3__.join(rushCommonFolder, 'config', 'rush'); - (0,_utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__.syncNpmrc)({ - sourceNpmrcFolder, - targetNpmrcFolder: rushTempFolder, - logger, - supportEnvVarFallbackSyntax: false - }); - const npmPath = getNpmPath(); - // This returns something that looks like: - // ``` - // [ - // "3.0.0", - // "3.0.1", - // ... - // "3.0.20" - // ] - // ``` - // - // if multiple versions match the selector, or - // - // ``` - // "3.0.0" - // ``` - // - // if only a single version matches. - const spawnSyncOptions = { - cwd: rushTempFolder, - stdio: [], - shell: _isWindows() - }; - const platformNpmPath = _getPlatformPath(npmPath); - const npmVersionSpawnResult = child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync(platformNpmPath, ['view', `${name}@${version}`, 'version', '--no-update-notifier', '--json'], spawnSyncOptions); - if (npmVersionSpawnResult.status !== 0) { - throw new Error(`"npm view" returned error code ${npmVersionSpawnResult.status}`); - } - const npmViewVersionOutput = npmVersionSpawnResult.stdout.toString(); - const parsedVersionOutput = JSON.parse(npmViewVersionOutput); - const versions = Array.isArray(parsedVersionOutput) - ? parsedVersionOutput - : [parsedVersionOutput]; - let latestVersion = versions[0]; - for (let i = 1; i < versions.length; i++) { - const latestVersionCandidate = versions[i]; - if (_compareVersionStrings(latestVersionCandidate, latestVersion) > 0) { - latestVersion = latestVersionCandidate; - } - } - if (!latestVersion) { - throw new Error('No versions found for the specified version range.'); - } - return latestVersion; - } - catch (e) { - throw new Error(`Unable to resolve version ${version} of package ${name}: ${e}`); - } - } -} -let _rushJsonFolder; -/** - * Find the absolute path to the folder containing rush.json - */ -function findRushJsonFolder() { - if (!_rushJsonFolder) { - let basePath = __dirname; - let tempPath = __dirname; - do { - const testRushJsonPath = path__WEBPACK_IMPORTED_MODULE_3__.join(basePath, RUSH_JSON_FILENAME); - if (fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(testRushJsonPath)) { - _rushJsonFolder = basePath; - break; - } - else { - basePath = tempPath; - } - } while (basePath !== (tempPath = path__WEBPACK_IMPORTED_MODULE_3__.dirname(basePath))); // Exit the loop when we hit the disk root - if (!_rushJsonFolder) { - throw new Error(`Unable to find ${RUSH_JSON_FILENAME}.`); - } - } - return _rushJsonFolder; -} -/** - * Detects if the package in the specified directory is installed - */ -function _isPackageAlreadyInstalled(packageInstallFolder) { - try { - const flagFilePath = path__WEBPACK_IMPORTED_MODULE_3__.join(packageInstallFolder, INSTALLED_FLAG_FILENAME); - if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(flagFilePath)) { - return false; - } - const fileContents = fs__WEBPACK_IMPORTED_MODULE_1__.readFileSync(flagFilePath).toString(); - return fileContents.trim() === process.version; - } - catch (e) { - return false; - } -} -/** - * Delete a file. Fail silently if it does not exist. - */ -function _deleteFile(file) { - try { - fs__WEBPACK_IMPORTED_MODULE_1__.unlinkSync(file); - } - catch (err) { - if (err.code !== 'ENOENT' && err.code !== 'ENOTDIR') { - throw err; - } - } -} -/** - * Removes the following files and directories under the specified folder path: - * - installed.flag - * - - * - node_modules - */ -function _cleanInstallFolder(rushTempFolder, packageInstallFolder, lockFilePath) { - try { - const flagFile = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, INSTALLED_FLAG_FILENAME); - _deleteFile(flagFile); - const packageLockFile = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, 'package-lock.json'); - if (lockFilePath) { - fs__WEBPACK_IMPORTED_MODULE_1__.copyFileSync(lockFilePath, packageLockFile); - } - else { - // Not running `npm ci`, so need to cleanup - _deleteFile(packageLockFile); - const nodeModulesFolder = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME); - if (fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(nodeModulesFolder)) { - const rushRecyclerFolder = _ensureAndJoinPath(rushTempFolder, 'rush-recycler'); - fs__WEBPACK_IMPORTED_MODULE_1__.renameSync(nodeModulesFolder, path__WEBPACK_IMPORTED_MODULE_3__.join(rushRecyclerFolder, `install-run-${Date.now().toString()}`)); - } - } - } - catch (e) { - throw new Error(`Error cleaning the package install folder (${packageInstallFolder}): ${e}`); - } -} -function _createPackageJson(packageInstallFolder, name, version) { - try { - const packageJsonContents = { - name: 'ci-rush', - version: '0.0.0', - dependencies: { - [name]: version - }, - description: "DON'T WARN", - repository: "DON'T WARN", - license: 'MIT' - }; - const packageJsonPath = path__WEBPACK_IMPORTED_MODULE_3__.join(packageInstallFolder, PACKAGE_JSON_FILENAME); - fs__WEBPACK_IMPORTED_MODULE_1__.writeFileSync(packageJsonPath, JSON.stringify(packageJsonContents, undefined, 2)); - } - catch (e) { - throw new Error(`Unable to create package.json: ${e}`); - } -} -/** - * Run "npm install" in the package install folder. - */ -function _installPackage(logger, packageInstallFolder, name, version, command) { - try { - logger.info(`Installing ${name}...`); - const npmPath = getNpmPath(); - const platformNpmPath = _getPlatformPath(npmPath); - const result = child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync(platformNpmPath, [command], { - stdio: 'inherit', - cwd: packageInstallFolder, - env: process.env, - shell: _isWindows() - }); - if (result.status !== 0) { - throw new Error(`"npm ${command}" encountered an error`); - } - logger.info(`Successfully installed ${name}@${version}`); - } - catch (e) { - throw new Error(`Unable to install package: ${e}`); - } -} -/** - * Get the ".bin" path for the package. - */ -function _getBinPath(packageInstallFolder, binName) { - const binFolderPath = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME, '.bin'); - const resolvedBinName = _isWindows() ? `${binName}.cmd` : binName; - return path__WEBPACK_IMPORTED_MODULE_3__.resolve(binFolderPath, resolvedBinName); -} -/** - * Returns a cross-platform path - windows must enclose any path containing spaces within double quotes. - */ -function _getPlatformPath(platformPath) { - return _isWindows() && platformPath.includes(' ') ? `"${platformPath}"` : platformPath; -} -function _isWindows() { - return os__WEBPACK_IMPORTED_MODULE_2__.platform() === 'win32'; -} -/** - * Write a flag file to the package's install directory, signifying that the install was successful. - */ -function _writeFlagFile(packageInstallFolder) { - try { - const flagFilePath = path__WEBPACK_IMPORTED_MODULE_3__.join(packageInstallFolder, INSTALLED_FLAG_FILENAME); - fs__WEBPACK_IMPORTED_MODULE_1__.writeFileSync(flagFilePath, process.version); - } - catch (e) { - throw new Error(`Unable to create installed.flag file in ${packageInstallFolder}`); - } -} -function installAndRun(logger, packageName, packageVersion, packageBinName, packageBinArgs, lockFilePath = process.env[INSTALL_RUN_LOCKFILE_PATH_VARIABLE]) { - const rushJsonFolder = findRushJsonFolder(); - const rushCommonFolder = path__WEBPACK_IMPORTED_MODULE_3__.join(rushJsonFolder, 'common'); - const rushTempFolder = _getRushTempFolder(rushCommonFolder); - const packageInstallFolder = _ensureAndJoinPath(rushTempFolder, 'install-run', `${packageName}@${packageVersion}`); - if (!_isPackageAlreadyInstalled(packageInstallFolder)) { - // The package isn't already installed - _cleanInstallFolder(rushTempFolder, packageInstallFolder, lockFilePath); - const sourceNpmrcFolder = path__WEBPACK_IMPORTED_MODULE_3__.join(rushCommonFolder, 'config', 'rush'); - (0,_utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__.syncNpmrc)({ - sourceNpmrcFolder, - targetNpmrcFolder: packageInstallFolder, - logger, - supportEnvVarFallbackSyntax: false - }); - _createPackageJson(packageInstallFolder, packageName, packageVersion); - const command = lockFilePath ? 'ci' : 'install'; - _installPackage(logger, packageInstallFolder, packageName, packageVersion, command); - _writeFlagFile(packageInstallFolder); - } - const statusMessage = `Invoking "${packageBinName} ${packageBinArgs.join(' ')}"`; - const statusMessageLine = new Array(statusMessage.length + 1).join('-'); - logger.info('\n' + statusMessage + '\n' + statusMessageLine + '\n'); - const binPath = _getBinPath(packageInstallFolder, packageBinName); - const binFolderPath = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME, '.bin'); - // Windows environment variables are case-insensitive. Instead of using SpawnSyncOptions.env, we need to - // assign via the process.env proxy to ensure that we append to the right PATH key. - const originalEnvPath = process.env.PATH || ''; - let result; - try { - // `npm` bin stubs on Windows are `.cmd` files - // Node.js will not directly invoke a `.cmd` file unless `shell` is set to `true` - const platformBinPath = _getPlatformPath(binPath); - process.env.PATH = [binFolderPath, originalEnvPath].join(path__WEBPACK_IMPORTED_MODULE_3__.delimiter); - result = child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync(platformBinPath, packageBinArgs, { - stdio: 'inherit', - windowsVerbatimArguments: false, - shell: _isWindows(), - cwd: process.cwd(), - env: process.env - }); - } - finally { - process.env.PATH = originalEnvPath; - } - if (result.status !== null) { - return result.status; - } - else { - throw result.error || new Error('An unknown error occurred.'); - } -} -function runWithErrorAndStatusCode(logger, fn) { - process.exitCode = 1; - try { - const exitCode = fn(); - process.exitCode = exitCode; - } - catch (e) { - logger.error('\n\n' + e.toString() + '\n\n'); - } -} -function _run() { - const [nodePath /* Ex: /bin/node */, scriptPath /* /repo/common/scripts/install-run-rush.js */, rawPackageSpecifier /* qrcode@^1.2.0 */, packageBinName /* qrcode */, ...packageBinArgs /* [-f, myproject/lib] */] = process.argv; - if (!nodePath) { - throw new Error('Unexpected exception: could not detect node path'); - } - if (path__WEBPACK_IMPORTED_MODULE_3__.basename(scriptPath).toLowerCase() !== 'install-run.js') { - // If install-run.js wasn't directly invoked, don't execute the rest of this function. Return control - // to the script that (presumably) imported this file - return; - } - if (process.argv.length < 4) { - console.log('Usage: install-run.js @ [args...]'); - console.log('Example: install-run.js qrcode@1.2.2 qrcode https://rushjs.io'); - process.exit(1); - } - const logger = { info: console.log, error: console.error }; - runWithErrorAndStatusCode(logger, () => { - const rushJsonFolder = findRushJsonFolder(); - const rushCommonFolder = _ensureAndJoinPath(rushJsonFolder, 'common'); - const packageSpecifier = _parsePackageSpecifier(rawPackageSpecifier); - const name = packageSpecifier.name; - const version = _resolvePackageVersion(logger, rushCommonFolder, packageSpecifier); - if (packageSpecifier.version !== version) { - console.log(`Resolved to ${name}@${version}`); - } - return installAndRun(logger, name, version, packageBinName, packageBinArgs); - }); -} -_run(); -//# sourceMappingURL=install-run.js.map -})(); - -module.exports = __webpack_exports__; -/******/ })() -; -//# sourceMappingURL=install-run.js.map \ No newline at end of file diff --git a/foundations/communication/common/scripts/package.json b/foundations/communication/common/scripts/package.json deleted file mode 100644 index bb9cc54cda..0000000000 --- a/foundations/communication/common/scripts/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "@hcengineering/scripts", - "version": "0.7.11", - "scripts": { - "format": "echo \"No format specified\"" - }, - "devDependencies": { - "esbuild": "^0.25.10", - "@hcengineering/platform-rig": "workspace:^0.7.19", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "@typescript-eslint/parser": "^6.21.0", - "eslint-config-standard-with-typescript": "^40.0.0" - }, - "private": true -} diff --git a/foundations/communication/common/scripts/run-tests-with-coverage.js b/foundations/communication/common/scripts/run-tests-with-coverage.js deleted file mode 100755 index 150cd21ff7..0000000000 --- a/foundations/communication/common/scripts/run-tests-with-coverage.js +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env node - -/** - * Run tests with coverage for all packages - * This is a replacement for show-coverage.sh - */ - -const fs = require('fs') -const path = require('path') -const { spawn } = require('child_process') - -const root = process.cwd() -const packagesDir = path.join(root, 'packages') - -if (!fs.existsSync(packagesDir)) { - console.error('Error: packages directory not found') - process.exit(1) -} - -console.log('=== RUNNING TESTS WITH COVERAGE ===') -console.log('') - -const packages = fs - .readdirSync(packagesDir, { withFileTypes: true }) - .filter((dirent) => dirent.isDirectory()) - .map((dirent) => dirent.name) - .sort() - -async function runTestsForPackage(pkgName) { - const pkgDir = path.join(packagesDir, pkgName) - const packageJson = path.join(pkgDir, 'package.json') - - if (!fs.existsSync(packageJson)) { - return null - } - - const pkg = JSON.parse(fs.readFileSync(packageJson, 'utf8')) - if (!pkg.scripts || !pkg.scripts.test) { - return null - } - - console.log(`📦 Package: ${pkgName}`) - console.log('---') - - return new Promise((resolve) => { - const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm' - const child = spawn(npm, ['test', '--', '--coverage', '--silent'], { - cwd: pkgDir, - stdio: 'pipe', - shell: true - }) - - let output = '' - let inSummary = false - - child.stdout.on('data', (data) => { - const text = data.toString() - output += text - - // Extract coverage summary - const lines = text.split('\n') - for (const line of lines) { - if (line.includes('Coverage summary') || line.includes('----------')) { - inSummary = true - } - if ( - inSummary && - (line.includes('Statements') || - line.includes('Branches') || - line.includes('Functions') || - line.includes('Lines')) - ) { - console.log(line.trim()) - } - if (inSummary && line.trim() === '') { - inSummary = false - } - } - }) - - child.stderr.on('data', (data) => { - // Ignore stderr for cleaner output - }) - - child.on('close', (code) => { - console.log('') - resolve({ package: pkgName, code, output }) - }) - }) -} - -async function main() { - const results = [] - - for (const pkg of packages) { - const result = await runTestsForPackage(pkg) - if (result) { - results.push(result) - } - } - - console.log('=== END OF COVERAGE REPORT ===') - console.log('') - console.log(`Tested ${results.length} packages`) - - const failed = results.filter((r) => r.code !== 0) - if (failed.length > 0) { - console.log(`⚠️ ${failed.length} package(s) had test failures:`) - failed.forEach((r) => console.log(` - ${r.package}`)) - } -} - -main().catch((err) => { - console.error('Error:', err) - process.exit(1) -}) diff --git a/foundations/communication/common/scripts/show-coverage-summary.js b/foundations/communication/common/scripts/show-coverage-summary.js deleted file mode 100755 index f2ca9e98b8..0000000000 --- a/foundations/communication/common/scripts/show-coverage-summary.js +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/env node - -/** - * Display coverage summary from lcov.info file - * Usage: node show-coverage-summary.js [lcov-file-path] - */ - -const fs = require('fs') -const path = require('path') - -const lcovFile = process.argv[2] || 'coverage/lcov.info' -const root = process.cwd() -const lcovPath = path.isAbsolute(lcovFile) ? lcovFile : path.join(root, lcovFile) - -if (!fs.existsSync(lcovPath)) { - console.error(`Error: LCOV file not found: ${lcovPath}`) - process.exit(1) -} - -console.log('==============================================') -console.log('COVERAGE SUMMARY BY PACKAGE') -console.log('==============================================') -console.log('') - -const data = fs.readFileSync(lcovPath, 'utf8') -const lines = data.split(/\r?\n/) - -const fileStats = new Map() -const filePkg = new Map() -let currentFile = '' - -// Parse lcov.info -for (const line of lines) { - if (line.startsWith('SF:')) { - currentFile = line.substring(3) - fileStats.set(currentFile, { total: 0, covered: 0 }) - - // Extract package name from path - const parts = currentFile.split(path.sep) - const pkgIndex = parts.indexOf('packages') - if (pkgIndex !== -1 && pkgIndex + 1 < parts.length) { - filePkg.set(currentFile, parts[pkgIndex + 1]) - } - } else if (line.startsWith('DA:')) { - const [lineNum, hitCount] = line.substring(3).split(',') - const stats = fileStats.get(currentFile) - if (stats) { - stats.total++ - if (parseInt(hitCount) > 0) { - stats.covered++ - } - } - } -} - -// Aggregate by package -const pkgStats = new Map() -for (const [file, stats] of fileStats) { - const pkg = filePkg.get(file) - if (pkg) { - if (!pkgStats.has(pkg)) { - pkgStats.set(pkg, { total: 0, covered: 0 }) - } - const pkgStat = pkgStats.get(pkg) - pkgStat.total += stats.total - pkgStat.covered += stats.covered - } -} - -// Sort packages alphabetically -const sortedPackages = Array.from(pkgStats.keys()).sort() - -// Display header -console.log(padRight('Package', 25) + padLeft('Covered', 10) + padLeft('Total', 10) + padLeft('Coverage', 10)) -console.log('----------------------------------------------') - -// Display package stats -let overallCovered = 0 -let overallTotal = 0 - -for (const pkg of sortedPackages) { - const stats = pkgStats.get(pkg) - const pct = (stats.covered / stats.total) * 100 - console.log( - padRight(pkg, 25) + - padLeft(stats.covered.toString(), 10) + - padLeft(stats.total.toString(), 10) + - padLeft(pct.toFixed(2) + '%', 10) - ) - overallCovered += stats.covered - overallTotal += stats.total -} - -// Display total -console.log('----------------------------------------------') -const overallPct = (overallCovered / overallTotal) * 100 -console.log( - padRight('TOTAL', 25) + - padLeft(overallCovered.toString(), 10) + - padLeft(overallTotal.toString(), 10) + - padLeft(overallPct.toFixed(2) + '%', 10) -) -console.log('') - -console.log('==============================================') -console.log('') -console.log('HTML report available at: coverage/html/index.html') -console.log(`Merged LCOV file available at: ${lcovFile}`) - -// Helper functions -function padRight(str, width) { - return str + ' '.repeat(Math.max(0, width - str.length)) -} - -function padLeft(str, width) { - return ' '.repeat(Math.max(0, width - str.length)) + str -} diff --git a/foundations/communication/common/scripts/show-coverage-summary.sh b/foundations/communication/common/scripts/show-coverage-summary.sh deleted file mode 100755 index 7ae68f8391..0000000000 --- a/foundations/communication/common/scripts/show-coverage-summary.sh +++ /dev/null @@ -1,76 +0,0 @@ -git#!/bin/bash - -# Script to display coverage summary from lcov.info file - -LCOV_FILE="${1:-coverage/lcov.info}" - -if [ ! -f "$LCOV_FILE" ]; then - echo "Error: LCOV file not found: $LCOV_FILE" - exit 1 -fi - -echo "==============================================" -echo "COVERAGE SUMMARY BY PACKAGE" -echo "==============================================" -echo "" - -# Parse lcov.info and aggregate by package -awk ' -BEGIN { - current_file = ""; -} -/^SF:/ { - current_file = $0; - sub(/^SF:/, "", current_file); - # Extract package name from path - split(current_file, parts, "/"); - pkg = ""; - for (i=1; i<=length(parts); i++) { - if (parts[i] == "packages" && i+1 <= length(parts)) { - pkg = parts[i+1]; - break; - } - } - total_lines[current_file] = 0; - covered_lines[current_file] = 0; - file_pkg[current_file] = pkg; -} -/^DA:/ { - split($0, parts, ","); - total_lines[current_file]++; - if (parts[2] > 0) covered_lines[current_file]++; -} -END { - # Aggregate by package - for (file in total_lines) { - pkg = file_pkg[file]; - if (pkg != "") { - pkg_total[pkg] += total_lines[file]; - pkg_covered[pkg] += covered_lines[file]; - } - } - - printf "%-25s %10s %10s %10s\n", "Package", "Covered", "Total", "Coverage"; - print "----------------------------------------------"; - - overall_covered = 0; - overall_total = 0; - - # Display packages - for (pkg in pkg_total) { - pct = (pkg_covered[pkg] / pkg_total[pkg]) * 100; - printf "%-25s %10d %10d %9.2f%%\n", pkg, pkg_covered[pkg], pkg_total[pkg], pct; - overall_covered += pkg_covered[pkg]; - overall_total += pkg_total[pkg]; - } - - print "----------------------------------------------"; - overall_pct = (overall_covered / overall_total) * 100; - printf "%-25s %10d %10d %9.2f%%\n", "TOTAL", overall_covered, overall_total, overall_pct; - print ""; -}' "$LCOV_FILE" - -echo "==============================================" -echo "" -echo "HTML report available at: coverage/html/index.html" -echo "Merged LCOV file available at: $LCOV_FILE" diff --git a/foundations/communication/common/scripts/show-coverage.sh b/foundations/communication/common/scripts/show-coverage.sh deleted file mode 100755 index cc99b27499..0000000000 --- a/foundations/communication/common/scripts/show-coverage.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -echo "=== FINAL COVERAGE REPORT ===" -echo "" - -# Iterate through each package directory -for pkg in packages/*/; do - pkgname=$(basename "$pkg") - - echo "📦 Package: $pkgname" - echo "---" - - # Change to package directory - cd "$pkg" || continue - - # Run tests with coverage and extract summary - npm test -- --coverage --silent 2>&1 | \ - grep -A 4 "Coverage summary" | \ - grep -E "Statements|Branches|Functions|Lines" - - # Return to root directory - cd ../.. || exit - - echo "" -done - -echo "=== END OF COVERAGE REPORT ===" \ No newline at end of file diff --git a/foundations/communication/example.json b/foundations/communication/example.json deleted file mode 100644 index ac066a5e22..0000000000 --- a/foundations/communication/example.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "cardId": "card-000", - "fromDate": "2025-09-02T10:00:00.000Z", - "toDate": "2025-09-02T12:30:00.000Z", - "language": "original", - - "messages": { - "msg-001": { - "id": "msg-001", - "cardId": "card-000", - "created": "2025-09-02T10:00:00.000Z", - "creator": "user-456", - "type": "message", - "content": "Привет! Это первое сообщение.", - "extra": {}, - "modified": "2025-09-02T12:00:00.000Z", - "reactions": { - "👍": { - "anonymous": { - "count": 100, - "created": "2025-09-02T12:00:00.000Z" - }, - "user-222": { - "count": 1, - "created": "2025-09-02T12:00:00.000Z" - }, - "user-333": { - "count": 1, - "created": "2025-09-02T12:00:00.000Z" - } - } - }, - "attachments": { - "att-001": { - "id": "att-001", - "mimeType": "image/png", - "params": { - "blobId": "blob-001", - "fileName": "image.png", - "size": 1024, - "metadata": {} - }, - "creator": "user-111", - "created": "2025-09-02T12:00:00.000Z" - }, - "att-002": { - "id": "att-002", - "mimeType": "image/png", - "params": { - "blobId": "blob-002", - "fileName": "image.png", - "size": 1024, - "metadata": {} - }, - "creator": "user-111", - "created": "2025-09-02T12:00:00.000Z" - } - }, - "threads": { - "thread-001": { - "threadId": "thread-001", - "repliesCount": 0, - "lastReply": null, - "repliedPersons": {} - }, - "thread-002": { - "threadId": "thread-002", - "repliesCount": 1, - "lastReply": "2025-09-02T12:30:00.000Z", - "repliedPersons": { - "user-222": 1 - } - } - } - } - } -} - diff --git a/foundations/communication/packages/client-query/.eslintrc.cjs b/foundations/communication/packages/client-query/.eslintrc.cjs deleted file mode 100644 index 72235dc283..0000000000 --- a/foundations/communication/packages/client-query/.eslintrc.cjs +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/foundations/communication/packages/client-query/CHANGELOG.json b/foundations/communication/packages/client-query/CHANGELOG.json deleted file mode 100644 index d93647f948..0000000000 --- a/foundations/communication/packages/client-query/CHANGELOG.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "name": "@hcengineering/communication-client-query", - "entries": [ - { - "version": "0.7.11", - "tag": "@hcengineering/communication-client-query_v0.7.11", - "date": "Mon, 27 Oct 2025 16:28:25 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/communication-types\" from `^0.7.11` to `0.7.12`" - }, - { - "comment": "Updating dependency \"@hcengineering/communication-sdk-types\" from `^0.7.11` to `0.7.12`" - } - ] - } - }, - { - "version": "0.7.8", - "tag": "@hcengineering/communication-client-query_v0.7.8", - "date": "Sat, 25 Oct 2025 22:20:35 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/communication-types\" from `^0.7.8` to `0.7.9`" - }, - { - "comment": "Updating dependency \"@hcengineering/communication-sdk-types\" from `^0.7.8` to `0.7.9`" - } - ] - } - }, - { - "version": "0.7.4", - "tag": "@hcengineering/communication-client-query_v0.7.4", - "date": "Tue, 14 Oct 2025 10:12:38 GMT", - "comments": { - "patch": [ - { - "comment": "update hulylake client" - }, - { - "comment": "update deps" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/communication-types\" from `^0.7.3` to `0.7.4`" - }, - { - "comment": "Updating dependency \"@hcengineering/communication-sdk-types\" from `^0.7.3` to `0.7.4`" - }, - { - "comment": "Updating dependency \"@hcengineering/communication-query\" from `^0.7.3` to `0.7.4`" - } - ] - } - } - ] -} diff --git a/foundations/communication/packages/client-query/CHANGELOG.md b/foundations/communication/packages/client-query/CHANGELOG.md deleted file mode 100644 index 39021f5c31..0000000000 --- a/foundations/communication/packages/client-query/CHANGELOG.md +++ /dev/null @@ -1,22 +0,0 @@ -# Change Log - @hcengineering/communication-client-query - -This log was last generated on Mon, 27 Oct 2025 16:28:25 GMT and should not be manually modified. - -## 0.7.11 -Mon, 27 Oct 2025 16:28:25 GMT - -_Version update only_ - -## 0.7.8 -Sat, 25 Oct 2025 22:20:35 GMT - -_Version update only_ - -## 0.7.4 -Tue, 14 Oct 2025 10:12:38 GMT - -### Patches - -- update hulylake client -- update deps - diff --git a/foundations/communication/packages/client-query/config/rig.json b/foundations/communication/packages/client-query/config/rig.json deleted file mode 100644 index 0110930f55..0000000000 --- a/foundations/communication/packages/client-query/config/rig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig" -} diff --git a/foundations/communication/packages/client-query/package.json b/foundations/communication/packages/client-query/package.json deleted file mode 100644 index 7a63da4b3a..0000000000 --- a/foundations/communication/packages/client-query/package.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "name": "@hcengineering/communication-client-query", - "version": "0.7.11", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "files": [ - "lib/**/*", - "types/**/*", - "src/**/*", - "tsconfig.json" - ], - "scripts": { - "build": "compile", - "_phase:build": "compile transpile src", - "_phase:validate": "compile validate", - "lint": "eslint \"src/**/*.ts\"", - "lint:fix": "eslint --fix \"src/**/*.ts\"", - "format": "prettier --write src/**/*.ts && eslint --fix \"src/**/*.ts\" && echo 'Formatted'", - "clean": "rm -rf lib && rm -rf types rm -rf node_modules" - }, - "devDependencies": { - "@hcengineering/platform-rig": "workspace:^0.7.19", - "@types/jest": "^29.5.5", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "@typescript-eslint/parser": "^6.21.0", - "esbuild": "^0.25.10", - "esbuild-plugin-copy": "^2.1.1", - "eslint": "^8.54.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-n": "^15.4.0", - "eslint-plugin-promise": "^6.1.1", - "jest": "^29.7.0", - "prettier": "^3.6.2", - "typescript": "^5.9.3" - }, - "dependencies": { - "@hcengineering/communication-types": "workspace:^0.7.12", - "@hcengineering/communication-sdk-types": "workspace:^0.7.12", - "@hcengineering/communication-query": "workspace:^0.7.11", - "@hcengineering/hulylake-client": "workspace:^0.7.17", - "fast-equals": "^5.2.2" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/hcengineering/communication.git" - }, - "publishConfig": { - "access": "public" - }, - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.js" - } - } -} diff --git a/foundations/communication/packages/client-query/src/index.ts b/foundations/communication/packages/client-query/src/index.ts deleted file mode 100644 index 45fd04a344..0000000000 --- a/foundations/communication/packages/client-query/src/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { CollaboratorsQuery, LabelsQuery, MessagesQuery, NotificationContextsQuery, NotificationsQuery } from './query' - -export type { MessageQueryParams } from '@hcengineering/communication-query' -export { initLiveQueries, refreshLiveQueries, closeLiveQueries } from './init' - -export function createMessagesQuery (dontDestroy?: boolean): MessagesQuery { - return new MessagesQuery(dontDestroy) -} - -export function createNotificationsQuery (dontDestroy?: boolean): NotificationsQuery { - return new NotificationsQuery(dontDestroy) -} - -export function createNotificationContextsQuery (dontDestroy?: boolean): NotificationContextsQuery { - return new NotificationContextsQuery(dontDestroy) -} - -export function createLabelsQuery (dontDestroy?: boolean): LabelsQuery { - return new LabelsQuery(dontDestroy) -} - -export function createCollaboratorsQuery (dontDestroy?: boolean): CollaboratorsQuery { - return new CollaboratorsQuery(dontDestroy) -} diff --git a/foundations/communication/packages/client-query/src/init.ts b/foundations/communication/packages/client-query/src/init.ts deleted file mode 100644 index f7e47affa0..0000000000 --- a/foundations/communication/packages/client-query/src/init.ts +++ /dev/null @@ -1,58 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { LiveQueries } from '@hcengineering/communication-query' -import type { FindClient } from '@hcengineering/communication-sdk-types' -import { type HulylakeWorkspaceClient } from '@hcengineering/hulylake-client' - -export type { MessageQueryParams } from '@hcengineering/communication-query' - -let lq: LiveQueries -let onDestroy: (fn: () => void) => void = () => {} - -export function getLiveQueries (): LiveQueries { - return lq -} - -export function getOnDestroy (): (fn: () => void) => void { - return onDestroy -} - -export function initLiveQueries ( - client: FindClient, - hulylake: HulylakeWorkspaceClient, - destroyFn?: (fn: () => void) => void -): void { - if (lq != null) { - lq.close() - } - - if (destroyFn != null) { - onDestroy = destroyFn - } - - lq = new LiveQueries(client, hulylake) -} - -export async function refreshLiveQueries (): Promise { - if (lq != null) { - await lq.refresh() - } -} -export function closeLiveQueries (): void { - if (lq != null) { - lq.close() - } -} diff --git a/foundations/communication/packages/client-query/src/query.ts b/foundations/communication/packages/client-query/src/query.ts deleted file mode 100644 index c2be11c5c0..0000000000 --- a/foundations/communication/packages/client-query/src/query.ts +++ /dev/null @@ -1,154 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - MessageQueryParams, - NotificationContextQueryOptions, - NotificationQueryParams, - MessageQueryOptions, - QueryOptions -} from '@hcengineering/communication-query' -import type { PagedQueryCallback, QueryCallback } from '@hcengineering/communication-sdk-types' -import { - type FindLabelsParams, - type FindNotificationContextParams, - type Label, - type Message, - type NotificationContext, - type Notification, - FindCollaboratorsParams, - Collaborator -} from '@hcengineering/communication-types' -import { deepEqual } from 'fast-equals' -import { getLiveQueries, getOnDestroy } from './init' - -class BaseQuery

, C extends (r: any) => void, O extends QueryOptions = QueryOptions> { - private oldParams: P | undefined - private oldOptions: O | undefined - private oldCallback: C | undefined - - constructor (dontDestroy?: boolean) { - if (dontDestroy !== true) { - const destroyFn = getOnDestroy() - destroyFn(() => { - this.unsubscribe() - }) - } - } - - private _unsubscribe: (isUpdate: boolean) => void = () => {} - - public unsubscribe (): void { - this._unsubscribe(false) - } - - query (params: P, callback: C, options?: O): boolean { - if (!this.needUpdate(params, callback, options)) { - return false - } - this.doQuery(params, callback, options) - return true - } - - private doQuery (params: P, callback: C, options?: O): void { - const isUpdate = this.oldParams !== undefined - this._unsubscribe(isUpdate) - this.oldCallback = callback - this.oldParams = params - this.oldOptions = options - - const { unsubscribe } = this.createQuery(params, callback, options) - this._unsubscribe = (isUpdate) => { - unsubscribe(isUpdate) - this.oldCallback = undefined - this.oldParams = undefined - this.oldOptions = undefined - this._unsubscribe = () => {} - } - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - createQuery (params: P, callback: C, options?: O): { unsubscribe: (isUpdate: boolean) => void } { - return { - unsubscribe: () => {} - } - } - - private needUpdate (params: P, callback: C, options?: O): boolean { - if (!deepEqual(params, this.oldParams)) return true - if (!deepEqual(options, this.oldOptions)) return true - if (!deepEqual(callback.toString(), this.oldCallback?.toString())) return true - return false - } -} - -export class MessagesQuery extends BaseQuery, MessageQueryOptions> { - override createQuery ( - params: MessageQueryParams, - callback: PagedQueryCallback, - options?: MessageQueryOptions - ): { unsubscribe: (isUpdate: boolean) => void } { - return getLiveQueries().queryMessages(params, callback, options) - } -} - -export class NotificationsQuery extends BaseQuery> { - override createQuery ( - params: NotificationQueryParams, - callback: PagedQueryCallback - ): { - unsubscribe: (isUpdate: boolean) => void - } { - return getLiveQueries().queryNotifications(params, callback) - } -} - -export class NotificationContextsQuery extends BaseQuery< -FindNotificationContextParams, -PagedQueryCallback, -NotificationContextQueryOptions -> { - override createQuery ( - params: FindNotificationContextParams, - callback: PagedQueryCallback, - options?: NotificationContextQueryOptions - ): { - unsubscribe: (isUpdate: boolean) => void - } { - return getLiveQueries().queryNotificationContexts(params, callback, options) - } -} - -export class LabelsQuery extends BaseQuery> { - override createQuery ( - params: FindLabelsParams, - callback: QueryCallback

( - cardId: CardID, - messageId: MessageID, - data: AttachmentData

[], - socialId: SocialID, - date?: Date - ) => Promise - removeAttachments: ( - cardId: CardID, - messageId: MessageID, - ids: AttachmentID[], - socialId: SocialID, - date?: Date - ) => Promise - setAttachments:

( - cardId: CardID, - messageId: MessageID, - data: AttachmentData

[], - socialId: SocialID, - date?: Date - ) => Promise -} diff --git a/foundations/communication/packages/rest-client/src/utils.ts b/foundations/communication/packages/rest-client/src/utils.ts deleted file mode 100644 index 29314845dc..0000000000 --- a/foundations/communication/packages/rest-client/src/utils.ts +++ /dev/null @@ -1,41 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { uncompress } from 'snappyjs' - -function isDateString (value: any): boolean { - if (typeof value !== 'string') return false - const dateStringRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/ - - return dateStringRegex.test(value) -} - -function reviver (key: string, value: any): Date { - if (isDateString(value)) return new Date(value) - return value -} - -export async function extractJson (response: Response): Promise { - const encoding = response.headers.get('content-encoding') - if (encoding === 'snappy') { - const buffer = await response.arrayBuffer() - const decompressed = uncompress(buffer) - const decoder = new TextDecoder() - const jsonString = decoder.decode(decompressed) - return JSON.parse(jsonString, reviver) as T - } - const jsonString = await response.text() - return JSON.parse(jsonString, reviver) as T -} diff --git a/foundations/communication/packages/rest-client/tsconfig.json b/foundations/communication/packages/rest-client/tsconfig.json deleted file mode 100644 index b5ae22f6e4..0000000000 --- a/foundations/communication/packages/rest-client/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/foundations/communication/packages/sdk-types/.eslintrc.cjs b/foundations/communication/packages/sdk-types/.eslintrc.cjs deleted file mode 100644 index 72235dc283..0000000000 --- a/foundations/communication/packages/sdk-types/.eslintrc.cjs +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/foundations/communication/packages/sdk-types/CHANGELOG.json b/foundations/communication/packages/sdk-types/CHANGELOG.json deleted file mode 100644 index 396910c6d9..0000000000 --- a/foundations/communication/packages/sdk-types/CHANGELOG.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "name": "@hcengineering/communication-sdk-types", - "entries": [ - { - "version": "0.7.12", - "tag": "@hcengineering/communication-sdk-types_v0.7.12", - "date": "Mon, 27 Oct 2025 16:28:25 GMT", - "comments": { - "patch": [ - { - "comment": "update deps" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/communication-types\" from `^0.7.11` to `0.7.12`" - } - ] - } - }, - { - "version": "0.7.9", - "tag": "@hcengineering/communication-sdk-types_v0.7.9", - "date": "Sat, 25 Oct 2025 22:20:35 GMT", - "comments": { - "patch": [ - { - "comment": "Fix peer create event" - }, - { - "comment": "Bump core" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/communication-types\" from `^0.7.8` to `0.7.9`" - } - ] - } - }, - { - "version": "0.7.4", - "tag": "@hcengineering/communication-sdk-types_v0.7.4", - "date": "Tue, 14 Oct 2025 10:12:38 GMT", - "comments": { - "patch": [ - { - "comment": "update deps" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/communication-types\" from `^0.7.3` to `0.7.4`" - } - ] - } - } - ] -} diff --git a/foundations/communication/packages/sdk-types/CHANGELOG.md b/foundations/communication/packages/sdk-types/CHANGELOG.md deleted file mode 100644 index 9d8560a863..0000000000 --- a/foundations/communication/packages/sdk-types/CHANGELOG.md +++ /dev/null @@ -1,26 +0,0 @@ -# Change Log - @hcengineering/communication-sdk-types - -This log was last generated on Mon, 27 Oct 2025 16:28:25 GMT and should not be manually modified. - -## 0.7.12 -Mon, 27 Oct 2025 16:28:25 GMT - -### Patches - -- update deps - -## 0.7.9 -Sat, 25 Oct 2025 22:20:35 GMT - -### Patches - -- Fix peer create event -- Bump core - -## 0.7.4 -Tue, 14 Oct 2025 10:12:38 GMT - -### Patches - -- update deps - diff --git a/foundations/communication/packages/sdk-types/config/rig.json b/foundations/communication/packages/sdk-types/config/rig.json deleted file mode 100644 index 0110930f55..0000000000 --- a/foundations/communication/packages/sdk-types/config/rig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig" -} diff --git a/foundations/communication/packages/sdk-types/package.json b/foundations/communication/packages/sdk-types/package.json deleted file mode 100644 index 80a6167a25..0000000000 --- a/foundations/communication/packages/sdk-types/package.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "name": "@hcengineering/communication-sdk-types", - "version": "0.7.12", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "files": [ - "lib/**/*", - "types/**/*", - "src/**/*", - "tsconfig.json" - ], - "scripts": { - "build": "compile", - "_phase:build": "compile transpile src", - "_phase:validate": "compile validate", - "lint": "eslint \"src/**/*.ts\"", - "lint:fix": "eslint --fix \"src/**/*.ts\"", - "format": "prettier --write src/**/*.ts && eslint --fix \"src/**/*.ts\" && echo 'Formatted'", - "clean": "rm -rf lib && rm -rf types rm -rf node_modules" - }, - "devDependencies": { - "@hcengineering/platform-rig": "workspace:^0.7.19", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "@typescript-eslint/parser": "^6.21.0", - "esbuild": "^0.25.10", - "esbuild-plugin-copy": "^2.1.1", - "eslint": "^8.54.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-n": "^15.4.0", - "eslint-plugin-promise": "^6.1.1", - "jest": "^29.7.0", - "@types/jest": "^29.5.5", - "prettier": "^3.6.2", - "typescript": "^5.9.3" - }, - "dependencies": { - "@hcengineering/core": "workspace:^0.7.22", - "@hcengineering/communication-types": "workspace:^0.7.12" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/hcengineering/communication.git" - }, - "publishConfig": { - "access": "public" - }, - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.js" - } - } -} diff --git a/foundations/communication/packages/sdk-types/src/client.ts b/foundations/communication/packages/sdk-types/src/client.ts deleted file mode 100644 index b44f7511d9..0000000000 --- a/foundations/communication/packages/sdk-types/src/client.ts +++ /dev/null @@ -1,43 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { - CardID, - Collaborator, FindCollaboratorsParams, - FindLabelsParams, - FindMessagesMetaParams, - FindNotificationContextParams, - FindNotificationsParams, - Label, MessageMeta, - Notification, - NotificationContext, WithTotal -} from '@hcengineering/communication-types' - -import type { EventResult, Event } from './events/event' - -export interface FindClient { - onEvent: (event: Event) => void - onRequest: (event: Event, promise: Promise) => void - - findMessagesMeta: (params: FindMessagesMetaParams) => Promise - - findNotificationContexts: (params: FindNotificationContextParams, queryId?: number) => Promise - findNotifications: (params: FindNotificationsParams, queryId?: number) => Promise> - findLabels: (params: FindLabelsParams, queryId?: number) => Promise - findCollaborators: (params: FindCollaboratorsParams, queryId?: number) => Promise - - subscribeCard: (cardId: CardID, subscription: string | number) => Promise - unsubscribeCard: (cardId: CardID, subscription: string | number) => Promise -} diff --git a/foundations/communication/packages/sdk-types/src/db.ts b/foundations/communication/packages/sdk-types/src/db.ts deleted file mode 100644 index 35648a8aaf..0000000000 --- a/foundations/communication/packages/sdk-types/src/db.ts +++ /dev/null @@ -1,147 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - CardID, - ContextID, - FindNotificationContextParams, - FindNotificationsParams, - MessageID, - NotificationContext, - SocialID, - Notification, - AccountUuid, - Collaborator, - FindCollaboratorsParams, - NotificationID, - Label, - FindLabelsParams, - LabelID, - CardType, - NotificationContent, - NotificationType, - WithTotal, - WorkspaceUuid, - PeerKind, - PeerExtra, - FindPeersParams, - Peer, - FindThreadMetaParams, - MessageMeta, - ThreadMeta, - FindMessagesMetaParams, - BlobID -} from '@hcengineering/communication-types' - -export interface DbAdapter { - // MessageMeta - createMessageMeta: ( - cardId: CardID, - id: MessageID, - creator: SocialID, - created: Date, - blob: BlobID - ) => Promise - removeMessageMeta: (cardId: CardID, messageId: MessageID) => Promise - findMessagesMeta: (params: FindMessagesMetaParams) => Promise - - // ThreadsIndex - attachThreadMeta: (cardId: CardID, messageId: MessageID, threadId: CardID, threadType: CardType, socialId: SocialID, date: Date) => Promise - removeThreadMeta: (query: ThreadMetaQuery) => Promise - updateThreadMeta: (query: ThreadMetaQuery, update: ThreadMetaUpdate) => Promise - findThreadMeta: (params: FindThreadMetaParams) => Promise - - // Peers - createPeer: ( - workspaceId: WorkspaceUuid, - cardId: CardID, - kind: PeerKind, - value: string, - extra: PeerExtra, - date: Date, - options?: { newValue?: boolean } - ) => Promise - removePeer: (workspaceId: WorkspaceUuid, - cardId: CardID, - kind: PeerKind, - value: string) => Promise - findPeers: (params: FindPeersParams) => Promise - - // Collaborators - addCollaborators: (cardId: CardID, cardType: CardType, collaborators: AccountUuid[], date: Date) => Promise - removeCollaborators: (query: CollaboratorQuery) => Promise - updateCollaborators: (query: CollaboratorQuery, update: CollaboratorUpdate) => Promise - getCollaboratorsCursor: (cardId: CardID, date: Date, size?: number) => AsyncIterable - findCollaborators: (params: FindCollaboratorsParams) => Promise - - // Notifications - createNotification: ( - contextId: ContextID, - messageId: MessageID, - blobId: BlobID, - type: NotificationType, - read: boolean, - content: NotificationContent, - creator: SocialID, - created: Date - ) => Promise - updateNotification: (query: NotificationQuery, updates: NotificationUpdate) => Promise - removeNotifications: (query: NotificationQuery) => Promise - findNotifications: (params: FindNotificationsParams) => Promise> - - // NotificationContext - createNotificationContext: ( - account: AccountUuid, - cardId: CardID, - lastUpdate: Date, - lastView: Date, - lastNotify: Date - ) => Promise - updateContext: (query: NotificationContextQuery, updates: NotificationContextUpdate) => Promise - removeContext: (query: NotificationContextQuery) => Promise - findNotificationContexts: (params: FindNotificationContextParams) => Promise - - // Labels - createLabel: (cardId: CardID, cardType: CardType, labelId: LabelID, account: AccountUuid, created: Date) => Promise - removeLabels: (query: LabelQuery) => Promise - updateLabels: (query: LabelQuery, update: LabelUpdate) => Promise - findLabels: (params: FindLabelsParams) => Promise - - // Other - getCardTitle: (cardId: CardID) => Promise - getCardSpaceMembers: (cardId: CardID) => Promise - getAccountsByPersonIds: (ids: string[]) => Promise - getNameByAccount: (id: AccountUuid) => Promise - - close: () => void -} - -export type ThreadMetaQuery = Partial> -export type ThreadMetaUpdate = Partial> - -export type LabelQuery = Partial> -export type LabelUpdate = Partial> - -export type NotificationContextQuery = Partial> -export type NotificationContextUpdate = Partial> - -export type NotificationQuery = Partial> & { - id?: NotificationID | NotificationID[] - untilDate?: Date -} -export type NotificationUpdate = Pick - -export type CollaboratorQuery = Pick & { account?: AccountUuid | AccountUuid[] } -export type CollaboratorUpdate = Partial diff --git a/foundations/communication/packages/sdk-types/src/domain.ts b/foundations/communication/packages/sdk-types/src/domain.ts deleted file mode 100644 index 98aaf5cde6..0000000000 --- a/foundations/communication/packages/sdk-types/src/domain.ts +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. - -export enum Domain { - MessageIndex = 'communication.message_index', - ThreadIndex = 'communication.thread_index', - - Notification = 'communication.notification', - NotificationContext = 'communication.notification_context', - - Collaborator = 'communication.collaborator', - Label = 'communication.label', - Peer = 'communication.peer' -} - -export const Domains = Object.values(Domain) diff --git a/foundations/communication/packages/sdk-types/src/events/card.ts b/foundations/communication/packages/sdk-types/src/events/card.ts deleted file mode 100644 index e63e342e6c..0000000000 --- a/foundations/communication/packages/sdk-types/src/events/card.ts +++ /dev/null @@ -1,42 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { CardID, CardType, SocialID } from '@hcengineering/communication-types' - -import type { BaseEvent } from './common' - -export enum CardEventType { - UpdateCardType = 'updateCardType', - RemoveCard = 'removeCard' -} - -export type CardEvent = UpdateCardTypeEvent | RemoveCardEvent - -export interface UpdateCardTypeEvent extends BaseEvent { - type: CardEventType.UpdateCardType - cardId: CardID - cardType: CardType - - socialId: SocialID - date?: Date -} - -export interface RemoveCardEvent extends BaseEvent { - type: CardEventType.RemoveCard - cardId: CardID - - socialId: SocialID - date?: Date -} diff --git a/foundations/communication/packages/sdk-types/src/events/common.ts b/foundations/communication/packages/sdk-types/src/events/common.ts deleted file mode 100644 index ee911d0fac..0000000000 --- a/foundations/communication/packages/sdk-types/src/events/common.ts +++ /dev/null @@ -1,19 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export interface BaseEvent { - _id?: string - _eventExtra?: Record -} diff --git a/foundations/communication/packages/sdk-types/src/events/event.ts b/foundations/communication/packages/sdk-types/src/events/event.ts deleted file mode 100644 index 506d4df622..0000000000 --- a/foundations/communication/packages/sdk-types/src/events/event.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. - -import type { LabelEvent, LabelEventType } from './label' -import type { MessageEventResult, MessageEventType, MessageEvent } from './message' -import type { NotificationEventResult, NotificationEvent, NotificationEventType } from './notification' -import type { CardEvent, CardEventType } from './card' -import { PeerEvent, PeerEventType } from './peer' - -export * from './message' -export * from './notification' -export * from './label' -export * from './card' -export * from './peer' - -export type EventType = MessageEventType | NotificationEventType | LabelEventType | CardEventType | PeerEventType -export type Event = MessageEvent | NotificationEvent | LabelEvent | CardEvent | PeerEvent -// eslint-disable-next-line @typescript-eslint/ban-types -export type EventResult = MessageEventResult | NotificationEventResult | {} diff --git a/foundations/communication/packages/sdk-types/src/events/label.ts b/foundations/communication/packages/sdk-types/src/events/label.ts deleted file mode 100644 index 0c1735caac..0000000000 --- a/foundations/communication/packages/sdk-types/src/events/label.ts +++ /dev/null @@ -1,45 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { CardID, LabelID, CardType, AccountUuid } from '@hcengineering/communication-types' -import type { BaseEvent } from './common' - -export enum LabelEventType { - // Internal - CreateLabel = 'createLabel', - RemoveLabel = 'removeLabel' -} - -export type LabelEvent = CreateLabelEvent | RemoveLabelEvent - -// Internal -export interface CreateLabelEvent extends BaseEvent { - type: LabelEventType.CreateLabel - labelId: LabelID - cardId: CardID - cardType: CardType - account: AccountUuid - - date?: Date -} - -export interface RemoveLabelEvent extends BaseEvent { - type: LabelEventType.RemoveLabel - labelId: LabelID - cardId: CardID - account: AccountUuid - - date?: Date -} diff --git a/foundations/communication/packages/sdk-types/src/events/message.ts b/foundations/communication/packages/sdk-types/src/events/message.ts deleted file mode 100644 index bf491cf747..0000000000 --- a/foundations/communication/packages/sdk-types/src/events/message.ts +++ /dev/null @@ -1,262 +0,0 @@ -import type { - CardID, - MessageID, - Markdown, - SocialID, - BlobID, - MessageType, - CardType, - MessageExtra, - BlobParams, - AttachmentData, - AttachmentID, - AttachmentUpdateData, - Emoji, - PersonUuid -} from '@hcengineering/communication-types' - -import type { BaseEvent } from './common' - -export enum MessageEventType { - // Public events - CreateMessage = 'createMessage', - UpdatePatch = 'updatePatch', - RemovePatch = 'removePatch', - ReactionPatch = 'reactionPatch', - /** - * @deprecated Use AttachmentPatch instead - */ - BlobPatch = 'blobPatch', - AttachmentPatch = 'attachmentPatch', - ThreadPatch = 'threadPatch', - TranslateMessage = 'translateMessage' -} - -export type PatchEvent = - | UpdatePatchEvent - | RemovePatchEvent - | ReactionPatchEvent - | BlobPatchEvent - | AttachmentPatchEvent - | ThreadPatchEvent - -export type MessageEvent = CreateMessageEvent | PatchEvent | TranslateMessageEvent - -export interface CreateMessageOptions { - // Available for regular users (Not implemented yet) - skipLinkPreviews?: boolean - // Available only for system - noNotify?: boolean - // Dont add to collaborators mentioned users - ignoreMentions?: boolean -} -export interface UpdatePatchOptions { - // Available for regular users (Not implemented yet) - skipLinkPreviewsUpdate?: boolean - // Dont add to collaborators mentioned users - ignoreMentions?: boolean -} - -export interface CreateMessageEvent extends BaseEvent { - type: MessageEventType.CreateMessage - - cardId: CardID - cardType: CardType - - messageId?: MessageID - messageType: MessageType - - content: Markdown - language?: string - extra?: MessageExtra - - socialId: SocialID - date?: Date - - options?: CreateMessageOptions -} - -export interface TranslateMessageEvent extends BaseEvent { - type: MessageEventType.TranslateMessage - - cardId: CardID - messageId: MessageID - content: Markdown - language: string -} - -// Available for author and system -export interface UpdatePatchEvent extends BaseEvent { - type: MessageEventType.UpdatePatch - - cardId: CardID - messageId: MessageID - - content?: Markdown - extra?: MessageExtra - language?: string - - socialId: SocialID - date?: Date - - options?: UpdatePatchOptions -} - -// Available for author and system -export interface RemovePatchEvent extends BaseEvent { - type: MessageEventType.RemovePatch - - cardId: CardID - messageId: MessageID - - socialId: SocialID - date?: Date -} - -export interface AddReactionOperation { - opcode: 'add' - reaction: Emoji -} - -export interface RemoveReactionOperation { - opcode: 'remove' - reaction: Emoji -} - -// For any user -export interface ReactionPatchEvent extends BaseEvent { - type: MessageEventType.ReactionPatch - - cardId: CardID - messageId: MessageID - - operation: AddReactionOperation | RemoveReactionOperation - - socialId: SocialID - personUuid?: PersonUuid // Set by server - date?: Date -} - -export interface AttachBlobsOperation { - opcode: 'attach' - blobs: (BlobParams & { mimeType: string })[] -} - -export interface DetachBlobsOperation { - opcode: 'detach' - blobIds: BlobID[] -} - -export interface SetBlobsOperation { - opcode: 'set' - blobs: (BlobParams & { mimeType: string })[] -} - -export interface UpdateBlobsOperation { - opcode: 'update' - blobs: BlobUpdateData[] -} - -export type BlobUpdateData = { blobId: BlobID, mimeType?: string } & Partial - -/** - * @deprecated Use AttachmentPatch instead - */ -export interface BlobPatchEvent extends BaseEvent { - type: MessageEventType.BlobPatch - - cardId: CardID - messageId: MessageID - - operations: (AttachBlobsOperation | DetachBlobsOperation | SetBlobsOperation | UpdateBlobsOperation)[] - - socialId: SocialID - date?: Date -} - -export interface AddAttachmentsOperation { - opcode: 'add' - attachments: AttachmentData[] -} - -export interface RemoveAttachmentsOperation { - opcode: 'remove' - ids: AttachmentID[] -} - -export interface SetAttachmentsOperation { - opcode: 'set' - attachments: AttachmentData[] -} - -export interface UpdateAttachmentsOperation { - opcode: 'update' - attachments: AttachmentUpdateData[] -} - -// For system and message author -export interface AttachmentPatchEvent extends BaseEvent { - type: MessageEventType.AttachmentPatch - - cardId: CardID - messageId: MessageID - - operations: ( - | AddAttachmentsOperation - | RemoveAttachmentsOperation - | SetAttachmentsOperation - | UpdateAttachmentsOperation - )[] - - socialId: SocialID - date?: Date -} - -// For any user -export interface AttachThreadOperation { - opcode: 'attach' - threadId: CardID - threadType: CardType -} - -// For system -export interface UpdateThreadOperation { - opcode: 'update' - threadId: CardID - update: { - threadType: CardType - } -} - -// For system -export interface AddReplyOperation { - opcode: 'addReply' - threadId: CardID -} - -// For system -export interface RemoveReplyOperation { - opcode: 'removeReply' - threadId: CardID -} - -export interface ThreadPatchEvent extends BaseEvent { - type: MessageEventType.ThreadPatch - - cardId: CardID - messageId: MessageID - - operation: AttachThreadOperation | UpdateThreadOperation | AddReplyOperation | RemoveReplyOperation - - socialId: SocialID - personUuid?: PersonUuid // Set by server - date?: Date -} - -export interface CreateMessageResult { - messageId: MessageID - created: Date - blobId: BlobID -} - -export type MessageEventResult = CreateMessageResult diff --git a/foundations/communication/packages/sdk-types/src/events/notification.ts b/foundations/communication/packages/sdk-types/src/events/notification.ts deleted file mode 100644 index 04d04ecee6..0000000000 --- a/foundations/communication/packages/sdk-types/src/events/notification.ts +++ /dev/null @@ -1,156 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - CardID, - ContextID, - MessageID, - AccountUuid, - CardType, - NotificationType, - NotificationContent, - NotificationID, - BlobID, - SocialID -} from '@hcengineering/communication-types' - -import type { BaseEvent } from './common' - -export enum NotificationEventType { - AddCollaborators = 'addCollaborators', - RemoveCollaborators = 'removeCollaborators', - - CreateNotification = 'createNotification', - RemoveNotifications = 'removeNotifications', - UpdateNotification = 'updateNotification', - - CreateNotificationContext = 'createNotificationContext', - RemoveNotificationContext = 'removeNotificationContext', - UpdateNotificationContext = 'updateNotificationContext' -} - -export type NotificationEvent = - | AddCollaboratorsEvent - | CreateNotificationContextEvent - | CreateNotificationEvent - | UpdateNotificationEvent - | RemoveCollaboratorsEvent - | RemoveNotificationContextEvent - | RemoveNotificationsEvent - | UpdateNotificationContextEvent - -export interface CreateNotificationEvent extends BaseEvent { - type: NotificationEventType.CreateNotification - - notificationId?: NotificationID - notificationType: NotificationType - read: boolean - content: NotificationContent - cardId: CardID - contextId: ContextID - messageId: MessageID - creator: SocialID - blobId: BlobID - account: AccountUuid - - date?: Date -} - -export interface UpdateNotificationEvent extends BaseEvent { - type: NotificationEventType.UpdateNotification - contextId: ContextID - account: AccountUuid - query: { - type?: NotificationType - id?: NotificationID - untilDate?: Date - } - updates: { - read: boolean - } - - date?: Date - updated?: number -} - -export interface RemoveNotificationsEvent extends BaseEvent { - type: NotificationEventType.RemoveNotifications - contextId: ContextID - account: AccountUuid - ids: NotificationID[] - - date?: Date -} - -export interface CreateNotificationContextEvent extends BaseEvent { - type: NotificationEventType.CreateNotificationContext - contextId?: ContextID - cardId: CardID - account: AccountUuid - - lastView: Date - lastUpdate: Date - lastNotify: Date - - date?: Date -} - -export interface RemoveNotificationContextEvent extends BaseEvent { - type: NotificationEventType.RemoveNotificationContext - contextId: ContextID - account: AccountUuid - - date?: Date -} - -export interface UpdateNotificationContextEvent extends BaseEvent { - type: NotificationEventType.UpdateNotificationContext - contextId: ContextID - account: AccountUuid - updates: { - lastView?: Date - lastUpdate?: Date - lastNotify?: Date - } - - date?: Date -} - -export interface AddCollaboratorsEvent extends BaseEvent { - type: NotificationEventType.AddCollaborators - cardId: CardID - cardType: CardType - collaborators: AccountUuid[] - - socialId: SocialID - date?: Date -} - -export interface RemoveCollaboratorsEvent extends BaseEvent { - type: NotificationEventType.RemoveCollaborators - cardId: CardID - cardType: CardType - collaborators: AccountUuid[] - - socialId: SocialID - date?: Date -} - -// eslint-disable-next-line @typescript-eslint/ban-types -export type NotificationEventResult = CreateNotificationContextResult | {} - -export interface CreateNotificationContextResult { - id: ContextID -} diff --git a/foundations/communication/packages/sdk-types/src/events/peer.ts b/foundations/communication/packages/sdk-types/src/events/peer.ts deleted file mode 100644 index 0664469f04..0000000000 --- a/foundations/communication/packages/sdk-types/src/events/peer.ts +++ /dev/null @@ -1,47 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { BaseEvent } from './common' -import { CardID, PeerKind, PeerExtra, WorkspaceUuid } from '@hcengineering/communication-types' - -// Peer events only for system -export enum PeerEventType { - CreatePeer = 'createPeer', - RemovePeer = 'removePeer' -} - -export type PeerEvent = CreatePeerEvent | RemovePeerEvent - -export interface CreatePeerEvent extends BaseEvent { - type: PeerEventType.CreatePeer - workspaceId: WorkspaceUuid - cardId: CardID - kind: PeerKind - value: string - extra?: PeerExtra - options?: { - newValue?: boolean - } - date?: Date -} - -export interface RemovePeerEvent extends BaseEvent { - type: PeerEventType.RemovePeer - workspaceId: WorkspaceUuid - cardId: CardID - kind: PeerKind - value: string - date?: Date -} diff --git a/foundations/communication/packages/sdk-types/src/index.ts b/foundations/communication/packages/sdk-types/src/index.ts deleted file mode 100644 index 901d8da521..0000000000 --- a/foundations/communication/packages/sdk-types/src/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export type * from './client' -export * from './db' -export type * from './query' -export * from './events/event' -export type * from './serverApi' -export * from './domain' diff --git a/foundations/communication/packages/sdk-types/src/query.ts b/foundations/communication/packages/sdk-types/src/query.ts deleted file mode 100644 index 2275ebd1ee..0000000000 --- a/foundations/communication/packages/sdk-types/src/query.ts +++ /dev/null @@ -1,19 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { Window } from '@hcengineering/communication-types' - -export type PagedQueryCallback = (window: Window) => void -export type QueryCallback = (result: T[]) => void diff --git a/foundations/communication/packages/sdk-types/src/serverApi.ts b/foundations/communication/packages/sdk-types/src/serverApi.ts deleted file mode 100644 index b38d582b0d..0000000000 --- a/foundations/communication/packages/sdk-types/src/serverApi.ts +++ /dev/null @@ -1,70 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { - FindNotificationContextParams, - FindNotificationsParams, - NotificationContext, - Notification, - FindLabelsParams, - Label, - FindCollaboratorsParams, - Collaborator, - FindPeersParams, Peer, CardID, FindMessagesMetaParams, MessageMeta, MessagesGroup, FindMessagesGroupParams -} from '@hcengineering/communication-types' -import type { Account, MeasureContext } from '@hcengineering/core' - -import type { EventResult, Event } from './events/event' - -export type ContextData = { - asyncRequests?: ((ctx: MeasureContext) => Promise)[] -} & Record - -export interface SessionData { - sessionId?: string - account: Account - derived?: boolean - isAsyncContext?: boolean - contextData?: ContextData - asyncData: Event[] -} - -export interface ServerApi { - findMessagesGroups: (session: SessionData, params: FindMessagesGroupParams) => Promise - findMessagesMeta: (session: SessionData, params: FindMessagesMetaParams) => Promise - - findNotificationContexts: ( - session: SessionData, - params: FindNotificationContextParams, - subscription?: number | string - ) => Promise - findNotifications: ( - session: SessionData, - params: FindNotificationsParams, - subscription?: number | string - ) => Promise - - findLabels: (session: SessionData, params: FindLabelsParams) => Promise - findCollaborators: (session: SessionData, params: FindCollaboratorsParams) => Promise - findPeers: (session: SessionData, params: FindPeersParams) => Promise - - event: (session: SessionData, event: Event) => Promise - - subscribeCard: (session: SessionData, cardId: CardID, subscription: number | string) => void - unsubscribeCard: (session: SessionData, cardId: CardID, subscription: number | string) => void - - closeSession: (sessionId: string) => Promise - close: () => Promise -} diff --git a/foundations/communication/packages/sdk-types/tsconfig.json b/foundations/communication/packages/sdk-types/tsconfig.json deleted file mode 100644 index b5ae22f6e4..0000000000 --- a/foundations/communication/packages/sdk-types/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/foundations/communication/packages/server/.eslintrc.cjs b/foundations/communication/packages/server/.eslintrc.cjs deleted file mode 100644 index 72235dc283..0000000000 --- a/foundations/communication/packages/server/.eslintrc.cjs +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/foundations/communication/packages/server/CHANGELOG.json b/foundations/communication/packages/server/CHANGELOG.json deleted file mode 100644 index 8bfd469579..0000000000 --- a/foundations/communication/packages/server/CHANGELOG.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "name": "@hcengineering/communication-server", - "entries": [ - { - "version": "0.7.12", - "tag": "@hcengineering/communication-server_v0.7.12", - "date": "Mon, 27 Oct 2025 16:28:25 GMT", - "comments": { - "patch": [ - { - "comment": "update deps" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/communication-sdk-types\" from `^0.7.11` to `0.7.12`" - }, - { - "comment": "Updating dependency \"@hcengineering/communication-types\" from `^0.7.11` to `0.7.12`" - } - ] - } - }, - { - "version": "0.7.9", - "tag": "@hcengineering/communication-server_v0.7.9", - "date": "Sat, 25 Oct 2025 22:20:35 GMT", - "comments": { - "patch": [ - { - "comment": "Fix peer create" - }, - { - "comment": "Remove reaction notification on reaction unset" - }, - { - "comment": "Improve validation of find methods params" - }, - { - "comment": "Add tests" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/communication-cockroach\" from `^0.7.8` to `0.7.9`" - }, - { - "comment": "Updating dependency \"@hcengineering/communication-sdk-types\" from `^0.7.8` to `0.7.9`" - }, - { - "comment": "Updating dependency \"@hcengineering/communication-types\" from `^0.7.8` to `0.7.9`" - } - ] - } - }, - { - "version": "0.7.4", - "tag": "@hcengineering/communication-server_v0.7.4", - "date": "Tue, 14 Oct 2025 10:12:38 GMT", - "comments": { - "patch": [ - { - "comment": "fix language store" - }, - { - "comment": "Do not update message.modified date if content is not changed " - }, - { - "comment": "update hulylake client" - }, - { - "comment": "update deps" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/communication-cockroach\" from `^0.7.3` to `0.7.4`" - }, - { - "comment": "Updating dependency \"@hcengineering/communication-sdk-types\" from `^0.7.3` to `0.7.4`" - }, - { - "comment": "Updating dependency \"@hcengineering/communication-shared\" from `^0.7.3` to `0.7.4`" - }, - { - "comment": "Updating dependency \"@hcengineering/communication-types\" from `^0.7.3` to `0.7.4`" - } - ] - } - } - ] -} diff --git a/foundations/communication/packages/server/CHANGELOG.md b/foundations/communication/packages/server/CHANGELOG.md deleted file mode 100644 index 629d6be9cc..0000000000 --- a/foundations/communication/packages/server/CHANGELOG.md +++ /dev/null @@ -1,31 +0,0 @@ -# Change Log - @hcengineering/communication-server - -This log was last generated on Mon, 27 Oct 2025 16:28:25 GMT and should not be manually modified. - -## 0.7.12 -Mon, 27 Oct 2025 16:28:25 GMT - -### Patches - -- update deps - -## 0.7.9 -Sat, 25 Oct 2025 22:20:35 GMT - -### Patches - -- Fix peer create -- Remove reaction notification on reaction unset -- Improve validation of find methods params -- Add tests - -## 0.7.4 -Tue, 14 Oct 2025 10:12:38 GMT - -### Patches - -- fix language store -- Do not update message.modified date if content is not changed -- update hulylake client -- update deps - diff --git a/foundations/communication/packages/server/config/rig.json b/foundations/communication/packages/server/config/rig.json deleted file mode 100644 index 0110930f55..0000000000 --- a/foundations/communication/packages/server/config/rig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig" -} diff --git a/foundations/communication/packages/server/jest.config.js b/foundations/communication/packages/server/jest.config.js deleted file mode 100644 index c137fcf3e0..0000000000 --- a/foundations/communication/packages/server/jest.config.js +++ /dev/null @@ -1,16 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ['./src'], - collectCoverage: true, - coverageReporters: ['text-summary', 'html', 'lcov'], - coverageDirectory: 'coverage', - moduleNameMapper: { - '^franc-min$': '/src/__mocks__/franc-min.ts' - }, - transformIgnorePatterns: [ - 'node_modules/(?!(franc-min|trigram-utils)/)' - ], - moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'] -} diff --git a/foundations/communication/packages/server/package.json b/foundations/communication/packages/server/package.json deleted file mode 100644 index 62fce803e7..0000000000 --- a/foundations/communication/packages/server/package.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "name": "@hcengineering/communication-server", - "version": "0.7.12", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "files": [ - "lib/**/*", - "types/**/*", - "src/**/*", - "tsconfig.json" - ], - "scripts": { - "build": "compile", - "_phase:build": "compile transpile src", - "_phase:validate": "compile validate", - "lint": "eslint \"src/**/*.ts\"", - "lint:fix": "eslint --fix \"src/**/*.ts\"", - "format": "prettier --write src/**/*.ts && eslint --fix \"src/**/*.ts\" && echo 'Formatted'", - "clean": "rm -rf lib && rm -rf types rm -rf node_modules", - "test": "jest --passWithNoTests --silent --forceExit", - "_phase:test": "jest --passWithNoTests --silent --forceExit" - }, - "devDependencies": { - "@hcengineering/platform-rig": "workspace:^0.7.19", - "@types/jest": "^29.5.5", - "@types/node": "^22.18.1", - "@types/uuid": "^8.3.1", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "@typescript-eslint/parser": "^6.21.0", - "esbuild": "^0.25.10", - "esbuild-plugin-copy": "^2.1.1", - "eslint": "^8.54.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-n": "^15.4.0", - "eslint-plugin-promise": "^6.1.1", - "jest": "^29.7.0", - "prettier": "^3.6.2", - "ts-jest": "^29.1.1", - "typescript": "^5.9.3" - }, - "dependencies": { - "@hcengineering/account-client": "workspace:^0.7.19", - "@hcengineering/communication-cockroach": "workspace:^0.7.11", - "@hcengineering/communication-sdk-types": "workspace:^0.7.12", - "@hcengineering/communication-shared": "workspace:^0.7.11", - "@hcengineering/communication-types": "workspace:^0.7.12", - "@hcengineering/core": "workspace:^0.7.22", - "@hcengineering/server-token": "workspace:^0.7.17", - "@hcengineering/text-core": "workspace:^0.7.18", - "@hcengineering/text-markdown": "workspace:^0.7.20", - "@hcengineering/hulylake-client": "workspace:^0.7.17", - "uuid": "^8.3.2", - "zod": "^3.22.4" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/hcengineering/communication.git" - }, - "publishConfig": { - "access": "public" - }, - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.js" - } - } -} diff --git a/foundations/communication/packages/server/src/__mocks__/notification/notification.ts b/foundations/communication/packages/server/src/__mocks__/notification/notification.ts deleted file mode 100644 index 7101e0dfbc..0000000000 --- a/foundations/communication/packages/server/src/__mocks__/notification/notification.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Mock for notification/notification -export const notify = jest.fn().mockResolvedValue([]) diff --git a/foundations/communication/packages/server/src/__mocks__/triggers/all.ts b/foundations/communication/packages/server/src/__mocks__/triggers/all.ts deleted file mode 100644 index faf1ab60ac..0000000000 --- a/foundations/communication/packages/server/src/__mocks__/triggers/all.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Mock for triggers/all -export default [] diff --git a/foundations/communication/packages/server/src/__tests__/blob.test.ts b/foundations/communication/packages/server/src/__tests__/blob.test.ts deleted file mode 100644 index 892940b66e..0000000000 --- a/foundations/communication/packages/server/src/__tests__/blob.test.ts +++ /dev/null @@ -1,947 +0,0 @@ -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. - -import { MeasureContext, SortingOrder, WorkspaceUuid, PersonUuid } from '@hcengineering/core' -import { HulylakeWorkspaceClient, getWorkspaceClient } from '@hcengineering/hulylake-client' -import { - CardID, - BlobID, - MessageID, - MessagesGroup, - Message, - Markdown, - MessageExtra, - Attachment, - AttachmentID, - Thread, - CardType, - AttachmentUpdateData, - MessageType, - SocialID -} from '@hcengineering/communication-types' -import { Blob } from '../blob' -import { Metadata } from '../types' - -// Mock dependencies -jest.mock('@hcengineering/hulylake-client') -jest.mock('@hcengineering/server-token', () => ({ - generateToken: jest.fn(() => 'mock-token') -})) -jest.mock('uuid', () => ({ - v4: jest.fn(() => 'mock-uuid') -})) - -describe('Blob', () => { - let blob: Blob - let mockClient: jest.Mocked - let mockCtx: jest.Mocked - let mockMetadata: Metadata - - const workspace = 'test-workspace' as WorkspaceUuid - const cardId = 'test-card-id' as CardID - const blobId = 'test-blob-id' as BlobID - const messageId = 'test-message-id' as MessageID - - beforeEach(() => { - // Setup mock context - mockCtx = { - warn: jest.fn(), - error: jest.fn(), - info: jest.fn() - } as any - - // Setup mock metadata - mockMetadata = { - accountsUrl: 'http://accounts.test', - hulylakeUrl: 'http://hulylake.test', - secret: 'test-secret', - messagesPerBlob: 100 - } - - // Setup mock client - mockClient = { - getJson: jest.fn(), - putJson: jest.fn(), - patchJson: jest.fn() - } as any - - // Mock getWorkspaceClient to return our mock client - ;(getWorkspaceClient as jest.Mock).mockReturnValue(mockClient) - - blob = new Blob(mockCtx, workspace, mockMetadata) - }) - - afterEach(() => { - jest.clearAllMocks() - }) - - describe('findMessagesGroups', () => { - beforeEach(() => { - mockClient.getJson.mockResolvedValue({ - status: 200, - body: { - 'blob-1': { - cardId, - blobId: 'blob-1' as BlobID, - fromDate: '2025-01-01T00:00:00.000Z', - toDate: '2025-01-02T00:00:00.000Z', - count: 10 - }, - 'blob-2': { - cardId, - blobId: 'blob-2' as BlobID, - fromDate: '2025-01-03T00:00:00.000Z', - toDate: '2025-01-04T00:00:00.000Z', - count: 20 - }, - 'blob-3': { - cardId, - blobId: 'blob-3' as BlobID, - fromDate: '2025-01-05T00:00:00.000Z', - toDate: '2025-01-06T00:00:00.000Z', - count: 30 - } - } - } as any) - }) - - it('should return all groups when no filters are provided', async () => { - const result = await blob.findMessagesGroups({ cardId }) - - expect(result).toHaveLength(3) - expect(result[0].blobId).toBe('blob-1') - expect(result[1].blobId).toBe('blob-2') - expect(result[2].blobId).toBe('blob-3') - }) - - it('should sort groups in ascending order', async () => { - const result = await blob.findMessagesGroups({ - cardId, - order: SortingOrder.Ascending - }) - - expect(result[0].fromDate.getTime()).toBeLessThan(result[1].fromDate.getTime()) - expect(result[1].fromDate.getTime()).toBeLessThan(result[2].fromDate.getTime()) - }) - - it('should sort groups in descending order', async () => { - const result = await blob.findMessagesGroups({ - cardId, - order: SortingOrder.Descending - }) - - expect(result[0].fromDate.getTime()).toBeGreaterThan(result[1].fromDate.getTime()) - expect(result[1].fromDate.getTime()).toBeGreaterThan(result[2].fromDate.getTime()) - }) - - it('should filter groups by blobId', async () => { - const result = await blob.findMessagesGroups({ - cardId, - blobId: 'blob-2' as BlobID - }) - - expect(result).toHaveLength(1) - expect(result[0].blobId).toBe('blob-2') - }) - - it('should filter groups by fromDate', async () => { - const result = await blob.findMessagesGroups({ - cardId, - fromDate: new Date('2025-01-03') - }) - - expect(result).toHaveLength(1) - expect(result[0].blobId).toBe('blob-2') - }) - - it('should filter groups by toDate', async () => { - const result = await blob.findMessagesGroups({ - cardId, - toDate: new Date('2025-01-04') - }) - - expect(result).toHaveLength(1) - expect(result[0].blobId).toBe('blob-2') - }) - - it('should limit the number of returned groups', async () => { - const result = await blob.findMessagesGroups({ - cardId, - limit: 2 - }) - - expect(result).toHaveLength(2) - }) - - it('should create groups blob when not found (404)', async () => { - mockClient.getJson.mockResolvedValue({ - status: 404, - body: null - } as any) - - const result = await blob.findMessagesGroups({ cardId }) - - expect(result).toHaveLength(0) - expect(mockClient.putJson).toHaveBeenCalledWith(`${cardId}/messages/groups`, {}, undefined, expect.any(Object)) - }) - }) - - describe('getMessageGroupByDate', () => { - beforeEach(() => { - mockClient.getJson.mockResolvedValue({ - status: 200, - body: { - 'blob-1': { - cardId, - blobId: 'blob-1' as BlobID, - fromDate: '2025-01-01T00:00:00.000Z', - toDate: '2025-01-10T00:00:00.000Z', - count: 10 - } - } - } as any) - }) - - it('should return matching group when date is within range', async () => { - const result = await blob.getMessageGroupByDate(cardId, new Date('2025-01-05'), false) - - expect(result).toBeDefined() - expect(result?.blobId).toBe('blob-1') - }) - - it('should return last group if date is after and group is not full', async () => { - const result = await blob.getMessageGroupByDate(cardId, new Date('2025-01-15'), false) - - expect(result).toBeDefined() - expect(result?.blobId).toBe('blob-1') - }) - - it('should return undefined when no match and create is false', async () => { - mockClient.getJson.mockResolvedValue({ - status: 200, - body: { - 'blob-1': { - cardId, - blobId: 'blob-1' as BlobID, - fromDate: '2025-01-01T00:00:00.000Z', - toDate: '2025-01-10T00:00:00.000Z', - count: 100 // Full group - } - } - } as any) - - const result = await blob.getMessageGroupByDate(cardId, new Date('2025-01-15'), false) - - expect(result).toBeUndefined() - }) - - it('should create new group when no match and create is true', async () => { - mockClient.getJson.mockResolvedValue({ - status: 200, - body: {} - } as any) - - mockClient.patchJson.mockResolvedValue({ status: 200 } as any) - mockClient.putJson.mockResolvedValue({ status: 200 } as any) - - const result = await blob.getMessageGroupByDate(cardId, new Date('2025-01-15'), true) - - expect(result).toBeDefined() - expect(mockClient.patchJson).toHaveBeenCalled() - expect(mockClient.putJson).toHaveBeenCalled() - }) - }) - - describe('insertMessage', () => { - const mockGroup: MessagesGroup = { - cardId, - blobId, - fromDate: new Date('2025-01-01'), - toDate: new Date('2025-01-10'), - count: 5 - } - - const mockMessage: Message = { - id: messageId, - created: new Date('2025-01-05'), - creator: 'user-1' as SocialID, - content: 'Test message' as Markdown, - cardId, - type: MessageType.Text, - reactions: {}, - attachments: [], - threads: [] - } - - beforeEach(() => { - mockClient.patchJson.mockResolvedValue({ status: 200 } as any) - mockClient.getJson.mockResolvedValue({ - status: 200, - body: { - [blobId]: { - cardId, - blobId, - fromDate: '2025-01-01T00:00:00.000Z', - toDate: '2025-01-10T00:00:00.000Z', - count: 5 - } - } - } as any) - }) - - it('should insert message with correct patches', async () => { - await blob.insertMessage(cardId, mockGroup, mockMessage) - - expect(mockClient.patchJson).toHaveBeenCalledWith( - `${cardId}/messages/${blobId}`, - expect.arrayContaining([ - expect.objectContaining({ - hop: 'add', - path: `/messages/${messageId}` - }) - ]), - undefined, - expect.any(Object) - ) - }) - - it('should update toDate when message is newer', async () => { - const newerMessage = { - ...mockMessage, - created: new Date('2025-01-15') - } - - await blob.insertMessage(cardId, mockGroup, newerMessage) - - const patches = mockClient.patchJson.mock.calls[0][1] - const toDatePatch = patches.find((p) => p.path === '/toDate') - expect(toDatePatch).toBeDefined() - }) - - it('should update fromDate when message is older', async () => { - const olderMessage = { - ...mockMessage, - created: new Date('2024-12-31') - } - - await blob.insertMessage(cardId, mockGroup, olderMessage) - - const patches = mockClient.patchJson.mock.calls[0][1] - const fromDatePatch = patches.find((p) => p.path === '/fromDate') - expect(fromDatePatch).toBeDefined() - }) - }) - - describe('updateMessage', () => { - const date = new Date('2025-01-05') - - beforeEach(() => { - mockClient.patchJson.mockResolvedValue({ status: 200 } as any) - }) - - it('should update message content', async () => { - await blob.updateMessage(cardId, blobId, messageId, { content: 'Updated content' as Markdown }, date) - - const patches = mockClient.patchJson.mock.calls[0][1] - expect(patches.some((p) => p.path === `/messages/${messageId}/content`)).toBe(true) - expect(patches.some((p) => p.path === `/messages/${messageId}/modified`)).toBe(true) - }) - - it('should update message extra', async () => { - const extra: MessageExtra = { key: 'value' } - await blob.updateMessage(cardId, blobId, messageId, { extra }, date) - - const patches = mockClient.patchJson.mock.calls[0][1] - expect(patches.some((p) => p.path === `/messages/${messageId}/extra`)).toBe(true) - }) - - it('should update message language without modifying modified field', async () => { - await blob.updateMessage(cardId, blobId, messageId, { language: 'en' }, date) - - const patches = mockClient.patchJson.mock.calls[0][1] - expect(patches.some((p) => p.path === `/messages/${messageId}/language`)).toBe(true) - expect(patches.some((p) => p.path === `/messages/${messageId}/modified`)).toBe(false) - }) - - it('should not call patchJson when no updates provided', async () => { - await blob.updateMessage(cardId, blobId, messageId, {}, date) - - expect(mockClient.patchJson).not.toHaveBeenCalled() - }) - }) - - describe('removeMessage', () => { - beforeEach(() => { - mockClient.patchJson.mockResolvedValue({ status: 200 } as any) - mockClient.getJson.mockResolvedValue({ - status: 200, - body: { - [blobId]: { - cardId, - blobId, - fromDate: '2025-01-01T00:00:00.000Z', - toDate: '2025-01-10T00:00:00.000Z', - count: 5 - } - } - } as any) - }) - - it('should remove message with correct patch', async () => { - await blob.removeMessage(cardId, blobId, messageId) - - expect(mockClient.patchJson).toHaveBeenCalledWith( - `${cardId}/messages/${blobId}`, - expect.arrayContaining([ - expect.objectContaining({ - hop: 'remove', - path: `/messages/${messageId}`, - safe: true - }) - ]), - undefined, - expect.any(Object) - ) - }) - }) - - describe('addReaction', () => { - const person = 'user-1' as PersonUuid - const emoji = '👍' - const date = new Date('2025-01-05') - - beforeEach(() => { - mockClient.patchJson.mockResolvedValue({ status: 200 } as any) - }) - - it('should add reaction with correct patches', async () => { - await blob.addReaction(cardId, blobId, messageId, emoji, person, date) - - expect(mockClient.patchJson).toHaveBeenCalledWith( - `${cardId}/messages/${blobId}`, - expect.arrayContaining([ - expect.objectContaining({ - hop: 'add', - path: `/messages/${messageId}/reactions/${emoji}` - }), - expect.objectContaining({ - hop: 'add', - path: `/messages/${messageId}/reactions/${emoji}/${person}` - }) - ]), - undefined, - expect.any(Object) - ) - }) - }) - - describe('removeReaction', () => { - const person = 'user-1' as PersonUuid - const emoji = '👍' - - beforeEach(() => { - mockClient.patchJson.mockResolvedValue({ status: 200 } as any) - }) - - it('should remove reaction with correct patch', async () => { - await blob.removeReaction(cardId, blobId, messageId, emoji, person) - - expect(mockClient.patchJson).toHaveBeenCalledWith( - `${cardId}/messages/${blobId}`, - expect.arrayContaining([ - expect.objectContaining({ - hop: 'remove', - path: `/messages/${messageId}/reactions/${emoji}/${person}`, - safe: true - }) - ]), - undefined, - expect.any(Object) - ) - }) - }) - - describe('addAttachments', () => { - const attachments: Attachment[] = [ - { - id: 'att-1' as AttachmentID, - mimeType: 'text/plain', - params: { blobId: 'blob-1' as BlobID, fileName: 'file1.txt', size: 1024 }, - creator: 'user-1' as SocialID, - created: new Date('2025-01-05') - }, - { - id: 'att-2' as AttachmentID, - mimeType: 'text/plain', - params: { blobId: 'blob-2' as BlobID, fileName: 'file2.txt', size: 2048 }, - creator: 'user-1' as SocialID, - created: new Date('2025-01-05') - } - ] - - beforeEach(() => { - mockClient.patchJson.mockResolvedValue({ status: 200 } as any) - }) - - it('should add multiple attachments', async () => { - await blob.addAttachments(cardId, blobId, messageId, attachments) - - const patches = mockClient.patchJson.mock.calls[0][1] - expect(patches).toHaveLength(2) - expect(patches[0].path).toBe(`/messages/${messageId}/attachments/att-1`) - expect(patches[1].path).toBe(`/messages/${messageId}/attachments/att-2`) - }) - }) - - describe('removeAttachments', () => { - const attachmentIds: AttachmentID[] = ['att-1' as AttachmentID, 'att-2' as AttachmentID] - - beforeEach(() => { - mockClient.patchJson.mockResolvedValue({ status: 200 } as any) - }) - - it('should remove multiple attachments', async () => { - await blob.removeAttachments(cardId, blobId, messageId, attachmentIds) - - const patches = mockClient.patchJson.mock.calls[0][1] - expect(patches).toHaveLength(2) - expect(patches[0].path).toBe(`/messages/${messageId}/attachments/att-1`) - expect(patches[1].path).toBe(`/messages/${messageId}/attachments/att-2`) - }) - }) - - describe('setAttachments', () => { - const attachments: Attachment[] = [ - { - id: 'att-1' as AttachmentID, - mimeType: 'text/plain', - params: { blobId: 'blob-1' as BlobID, fileName: 'file1.txt', size: 1024 }, - creator: 'user-1' as SocialID, - created: new Date('2025-01-05') - } - ] - - beforeEach(() => { - mockClient.patchJson.mockResolvedValue({ status: 200 } as any) - }) - - it('should replace all attachments', async () => { - await blob.setAttachments(cardId, blobId, messageId, attachments) - - const patches = mockClient.patchJson.mock.calls[0][1] - expect(patches[0].path).toBe(`/messages/${messageId}/attachments`) - }) - }) - - describe('updateAttachments', () => { - const updates: AttachmentUpdateData[] = [ - { - id: 'att-1' as AttachmentID, - params: { status: 'processed' } - } - ] - const date = new Date('2025-01-05') - - beforeEach(() => { - mockClient.patchJson.mockResolvedValue({ status: 200 } as any) - }) - - it('should update attachment parameters', async () => { - await blob.updateAttachments(cardId, blobId, messageId, updates, date) - - const patches = mockClient.patchJson.mock.calls[0][1] - expect(patches.some((p) => p.path.includes('/params/status'))).toBe(true) - expect(patches.some((p) => p.path.includes('/modified'))).toBe(true) - }) - - it('should skip updates with empty params', async () => { - const emptyUpdates: AttachmentUpdateData[] = [ - { - id: 'att-1' as AttachmentID, - params: {} - } - ] - - await blob.updateAttachments(cardId, blobId, messageId, emptyUpdates, date) - - expect(mockClient.patchJson).toHaveBeenCalledWith(expect.any(String), [], undefined, expect.any(Object)) - }) - }) - - describe('attachThread', () => { - const thread: Thread = { - cardId, - messageId, - threadId: 'thread-1' as CardID, - threadType: 'discussion' as CardType, - repliesCount: 0, - lastReplyDate: undefined, - repliedPersons: {} - } - - beforeEach(() => { - mockClient.patchJson.mockResolvedValue({ status: 200 } as any) - }) - - it('should attach thread to message', async () => { - await blob.attachThread(cardId, blobId, messageId, thread) - - expect(mockClient.patchJson).toHaveBeenCalledWith( - `${cardId}/messages/${blobId}`, - expect.arrayContaining([ - expect.objectContaining({ - op: 'add', - path: `/messages/${messageId}/threads/${thread.threadId}` - }) - ]), - undefined, - expect.any(Object) - ) - }) - }) - - describe('updateThread', () => { - const threadId = 'thread-1' as CardID - - beforeEach(() => { - mockClient.patchJson.mockResolvedValue({ status: 200 } as any) - }) - - it('should update thread type', async () => { - await blob.updateThread(cardId, blobId, messageId, threadId, { - threadType: 'task' as CardType - }) - - expect(mockClient.patchJson).toHaveBeenCalledWith( - `${cardId}/messages/${blobId}`, - expect.arrayContaining([ - expect.objectContaining({ - op: 'add', - path: `/messages/${messageId}/threads/${threadId}/threadType`, - value: 'task' - }) - ]), - undefined, - expect.any(Object) - ) - }) - }) - - describe('addThreadReply', () => { - const threadId = 'thread-1' as CardID - const person = 'user-1' as PersonUuid - const date = new Date('2025-01-05') - - beforeEach(() => { - mockClient.patchJson.mockResolvedValue({ status: 200 } as any) - }) - - it('should increment replies count and update last reply', async () => { - await blob.addThreadReply(cardId, blobId, messageId, threadId, person, date) - - const patches = mockClient.patchJson.mock.calls[0][1] - expect(patches.some((p) => p.path.includes('/repliesCount'))).toBe(true) - expect(patches.some((p) => p.path.includes('/lastReply'))).toBe(true) - expect(patches.some((p) => p.path.includes('/repliedPersons'))).toBe(true) - }) - }) - - describe('removeThreadReply', () => { - const threadId = 'thread-1' as CardID - const person = 'user-1' as PersonUuid - - beforeEach(() => { - mockClient.patchJson.mockResolvedValue({ status: 200 } as any) - }) - - it('should decrement replies count', async () => { - await blob.removeThreadReply(cardId, blobId, messageId, threadId, person) - - const patches = mockClient.patchJson.mock.calls[0][1] - const repliesCountPatch = patches.find((p) => p.path.includes('/repliesCount')) - expect(repliesCountPatch).toBeDefined() - expect((repliesCountPatch as any).value).toBe(-1) - }) - }) - - describe('removeThread', () => { - const threadId = 'thread-1' as CardID - - beforeEach(() => { - mockClient.patchJson.mockResolvedValue({ status: 200 } as any) - }) - - it('should remove thread from message', async () => { - await blob.removeThread(cardId, blobId, messageId, threadId) - - expect(mockClient.patchJson).toHaveBeenCalledWith( - `${cardId}/messages/${blobId}`, - expect.arrayContaining([ - expect.objectContaining({ - hop: 'remove', - path: `/messages/${messageId}/threads/${threadId}`, - safe: true - }) - ]), - undefined, - expect.any(Object) - ) - }) - }) - - describe('Message group selection logic', () => { - beforeEach(() => { - mockClient.patchJson.mockResolvedValue({ status: 200 } as any) - mockClient.putJson.mockResolvedValue({ status: 200 } as any) - }) - - it('should insert message into group that matches date range', async () => { - // Setup: three groups with different date ranges - mockClient.getJson.mockResolvedValue({ - status: 200, - body: { - 'blob-1': { - cardId, - blobId: 'blob-1' as BlobID, - fromDate: '2025-01-01T00:00:00.000Z', - toDate: '2025-01-05T00:00:00.000Z', - count: 10 - }, - 'blob-2': { - cardId, - blobId: 'blob-2' as BlobID, - fromDate: '2025-01-06T00:00:00.000Z', - toDate: '2025-01-10T00:00:00.000Z', - count: 20 - }, - 'blob-3': { - cardId, - blobId: 'blob-3' as BlobID, - fromDate: '2025-01-11T00:00:00.000Z', - toDate: '2025-01-15T00:00:00.000Z', - count: 30 - } - } - } as any) - - // Message with date in the range of the second group - const messageDate = new Date('2025-01-08T12:00:00.000Z') - const group = await blob.getMessageGroupByDate(cardId, messageDate, false) - - expect(group).toBeDefined() - expect(group?.blobId).toBe('blob-2') - expect(group?.fromDate.getTime()).toBeLessThanOrEqual(messageDate.getTime()) - expect(group?.toDate.getTime()).toBeGreaterThanOrEqual(messageDate.getTime()) - }) - - it('should insert message into last group if date is after all groups and group is not full', async () => { - mockClient.getJson.mockResolvedValue({ - status: 200, - body: { - 'blob-1': { - cardId, - blobId: 'blob-1' as BlobID, - fromDate: '2025-01-01T00:00:00.000Z', - toDate: '2025-01-05T00:00:00.000Z', - count: 50 // Not full (< 100) - } - } - } as any) - - // Message with date after the last group - const messageDate = new Date('2025-01-10T12:00:00.000Z') - const group = await blob.getMessageGroupByDate(cardId, messageDate, false) - - expect(group).toBeDefined() - expect(group?.blobId).toBe('blob-1') - expect(group?.count).toBeLessThan(mockMetadata.messagesPerBlob) - }) - - it('should insert message into first group if date is before all groups and group is not full', async () => { - mockClient.getJson.mockResolvedValue({ - status: 200, - body: { - 'blob-1': { - cardId, - blobId: 'blob-1' as BlobID, - fromDate: '2025-01-10T00:00:00.000Z', - toDate: '2025-01-15T00:00:00.000Z', - count: 50 // Not full - } - } - } as any) - - // Message with date before the first group - const messageDate = new Date('2025-01-05T12:00:00.000Z') - const group = await blob.getMessageGroupByDate(cardId, messageDate, false) - - expect(group).toBeDefined() - expect(group?.blobId).toBe('blob-1') - expect(group?.count).toBeLessThan(mockMetadata.messagesPerBlob) - }) - - it('should NOT use last group if it is full', async () => { - mockClient.getJson.mockResolvedValue({ - status: 200, - body: { - 'blob-1': { - cardId, - blobId: 'blob-1' as BlobID, - fromDate: '2025-01-01T00:00:00.000Z', - toDate: '2025-01-05T00:00:00.000Z', - count: 100 // Full group - } - } - } as any) - - // Message with date after the last group - const messageDate = new Date('2025-01-10T12:00:00.000Z') - const group = await blob.getMessageGroupByDate(cardId, messageDate, false) - - // Should not find a group since the only group is full - expect(group).toBeUndefined() - }) - - it('should NOT use first group if it is full', async () => { - mockClient.getJson.mockResolvedValue({ - status: 200, - body: { - 'blob-1': { - cardId, - blobId: 'blob-1' as BlobID, - fromDate: '2025-01-10T00:00:00.000Z', - toDate: '2025-01-15T00:00:00.000Z', - count: 100 // Full group - } - } - } as any) - - // Message with date before the first group - const messageDate = new Date('2025-01-05T12:00:00.000Z') - const group = await blob.getMessageGroupByDate(cardId, messageDate, false) - - // Should not find a group since the only group is full - expect(group).toBeUndefined() - }) - - it('should create new group when no suitable group exists and create=true', async () => { - mockClient.getJson.mockResolvedValue({ - status: 200, - body: { - 'blob-1': { - cardId, - blobId: 'blob-1' as BlobID, - fromDate: '2025-01-01T00:00:00.000Z', - toDate: '2025-01-05T00:00:00.000Z', - count: 100 // Full group - } - } - } as any) - - const messageDate = new Date('2025-01-10T12:00:00.000Z') - const group = await blob.getMessageGroupByDate(cardId, messageDate, true) - - expect(group).toBeDefined() - expect(mockClient.patchJson).toHaveBeenCalled() - expect(mockClient.putJson).toHaveBeenCalled() - }) - - it('should select correct group among multiple groups', async () => { - mockClient.getJson.mockResolvedValue({ - status: 200, - body: { - 'blob-1': { - cardId, - blobId: 'blob-1' as BlobID, - fromDate: '2025-01-01T00:00:00.000Z', - toDate: '2025-01-03T00:00:00.000Z', - count: 100 // Full - }, - 'blob-2': { - cardId, - blobId: 'blob-2' as BlobID, - fromDate: '2025-01-04T00:00:00.000Z', - toDate: '2025-01-06T00:00:00.000Z', - count: 50 // Not full - }, - 'blob-3': { - cardId, - blobId: 'blob-3' as BlobID, - fromDate: '2025-01-07T00:00:00.000Z', - toDate: '2025-01-09T00:00:00.000Z', - count: 100 // Full - } - } - } as any) - - // Message should go into blob-2 since its date is in range - const messageDate = new Date('2025-01-05T12:00:00.000Z') - const group = await blob.getMessageGroupByDate(cardId, messageDate, false) - - expect(group).toBeDefined() - expect(group?.blobId).toBe('blob-2') - }) - - it('should use last non-full group for date after all groups', async () => { - mockClient.getJson.mockResolvedValue({ - status: 200, - body: { - 'blob-1': { - cardId, - blobId: 'blob-1' as BlobID, - fromDate: '2025-01-01T00:00:00.000Z', - toDate: '2025-01-03T00:00:00.000Z', - count: 100 // Full - }, - 'blob-2': { - cardId, - blobId: 'blob-2' as BlobID, - fromDate: '2025-01-04T00:00:00.000Z', - toDate: '2025-01-06T00:00:00.000Z', - count: 50 // Not full - last group - } - } - } as any) - - // Message with date after all groups - const messageDate = new Date('2025-01-10T12:00:00.000Z') - const group = await blob.getMessageGroupByDate(cardId, messageDate, false) - - expect(group).toBeDefined() - expect(group?.blobId).toBe('blob-2') - expect(group?.count).toBeLessThan(mockMetadata.messagesPerBlob) - }) - - it('should handle exact boundary dates correctly', async () => { - mockClient.getJson.mockResolvedValue({ - status: 200, - body: { - 'blob-1': { - cardId, - blobId: 'blob-1' as BlobID, - fromDate: '2025-01-01T00:00:00.000Z', - toDate: '2025-01-05T23:59:59.999Z', - count: 50 - } - } - } as any) - - // Message exactly at the toDate boundary - const messageDate = new Date('2025-01-05T23:59:59.999Z') - const group = await blob.getMessageGroupByDate(cardId, messageDate, false) - - expect(group).toBeDefined() - expect(group?.blobId).toBe('blob-1') - }) - }) -}) diff --git a/foundations/communication/packages/server/src/__tests__/client.test.ts b/foundations/communication/packages/server/src/__tests__/client.test.ts deleted file mode 100644 index 5e75e4f7af..0000000000 --- a/foundations/communication/packages/server/src/__tests__/client.test.ts +++ /dev/null @@ -1,458 +0,0 @@ -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. - -import { MeasureContext, WorkspaceUuid, PersonUuid, Account } from '@hcengineering/core' -import { HulylakeWorkspaceClient, getWorkspaceClient } from '@hcengineering/hulylake-client' -import { getClient as getAccountClient } from '@hcengineering/account-client' -import { loadMessages } from '@hcengineering/communication-shared' -import { - CardID, - MessageID, - MessageMeta, - Message, - SocialID, - FindMessagesOptions, - BlobID -} from '@hcengineering/communication-types' -import { DbAdapter } from '@hcengineering/communication-sdk-types' -import { LowLevelClient } from '../client' -import { Blob } from '../blob' -import { Metadata } from '../types' - -// Mock dependencies -jest.mock('@hcengineering/hulylake-client') -jest.mock('@hcengineering/account-client') -jest.mock('@hcengineering/communication-shared') -jest.mock('@hcengineering/server-token', () => ({ - generateToken: jest.fn(() => 'mock-token') -})) - -describe('LowLevelClient', () => { - let client: LowLevelClient - let mockDbAdapter: jest.Mocked - let mockBlob: jest.Mocked - let mockMetadata: Metadata - let mockLakeClient: jest.Mocked - let mockAccountClient: any - let mockCtx: jest.Mocked - let mockAccount: Account - - const workspace = 'test-workspace' as WorkspaceUuid - const cardId = 'test-card-id' as CardID - const messageId = 'test-message-id' as MessageID - const blobId = 'test-blob-id' as BlobID - const socialId = 'social-id-123' as SocialID - const personUuid = 'person-uuid-123' as PersonUuid - - beforeEach(() => { - // Reset all mocks - jest.clearAllMocks() - - // Mock DbAdapter - mockDbAdapter = { - findMessagesMeta: jest.fn(), - removeMessageMeta: jest.fn() - } as any - - // Mock Blob - mockBlob = {} as any - - // Mock Metadata - mockMetadata = { - accountsUrl: 'http://accounts-url', - hulylakeUrl: 'http://hulylake-url', - secret: 'test-secret', - messagesPerBlob: 100 - } - - // Mock HulylakeWorkspaceClient - mockLakeClient = { - find: jest.fn(), - update: jest.fn() - } as any - - // Mock getWorkspaceClient - ;(getWorkspaceClient as jest.Mock).mockReturnValue(mockLakeClient) - - // Mock AccountClient - mockAccountClient = { - findPersonBySocialId: jest.fn() - } - ;(getAccountClient as jest.Mock).mockReturnValue(mockAccountClient) - - // Mock loadMessages - ;(loadMessages as jest.Mock).mockResolvedValue([]) - - // Mock MeasureContext - mockCtx = { - warn: jest.fn(), - error: jest.fn(), - info: jest.fn() - } as any - - // Mock Account - mockAccount = { - uuid: personUuid, - socialIds: [] - } as any - - // Create client instance - client = new LowLevelClient(mockDbAdapter, mockBlob, mockMetadata, workspace) - }) - - describe('constructor', () => { - it('should initialize LowLevelClient with correct parameters', () => { - expect(client.db).toBe(mockDbAdapter) - expect(client.blob).toBe(mockBlob) - expect(getWorkspaceClient).toHaveBeenCalledWith(mockMetadata.hulylakeUrl, workspace, 'mock-token') - }) - }) - - describe('findMessage', () => { - it('should return undefined when message meta is not found', async () => { - mockDbAdapter.findMessagesMeta.mockResolvedValue([]) - - const result = await client.findMessage(cardId, messageId) - - expect(result).toBeUndefined() - expect(mockDbAdapter.findMessagesMeta).toHaveBeenCalledWith({ - cardId, - id: messageId - }) - expect(loadMessages).not.toHaveBeenCalled() - }) - - it('should return message when meta is found', async () => { - const mockMeta: MessageMeta = { - cardId, - id: messageId, - blobId, - createdOn: Date.now() - } as any - - const mockMessage: Message = { - cardId, - id: messageId, - createdOn: Date.now(), - createdBy: personUuid - } as any - - mockDbAdapter.findMessagesMeta.mockResolvedValue([mockMeta]) - ;(loadMessages as jest.Mock).mockResolvedValue([mockMessage]) - - const result = await client.findMessage(cardId, messageId) - - expect(result).toEqual(mockMessage) - expect(mockDbAdapter.findMessagesMeta).toHaveBeenCalledWith({ - cardId, - id: messageId - }) - expect(loadMessages).toHaveBeenCalledWith(mockLakeClient, blobId, { cardId, id: messageId }, undefined) - }) - - it('should pass options to loadMessages', async () => { - const mockMeta: MessageMeta = { - cardId, - id: messageId, - blobId, - createdOn: Date.now() - } as any - - const options: FindMessagesOptions = { - attachments: true - } - - mockDbAdapter.findMessagesMeta.mockResolvedValue([mockMeta]) - ;(loadMessages as jest.Mock).mockResolvedValue([]) - - await client.findMessage(cardId, messageId, options) - - expect(loadMessages).toHaveBeenCalledWith(mockLakeClient, blobId, { cardId, id: messageId }, options) - }) - - it('should use cached message meta on subsequent calls', async () => { - const mockMeta: MessageMeta = { - cardId, - id: messageId, - blobId, - createdOn: Date.now() - } as any - - mockDbAdapter.findMessagesMeta.mockResolvedValue([mockMeta]) - ;(loadMessages as jest.Mock).mockResolvedValue([]) - - // First call - await client.findMessage(cardId, messageId) - expect(mockDbAdapter.findMessagesMeta).toHaveBeenCalledTimes(1) - - // Second call - should use cache - await client.findMessage(cardId, messageId) - expect(mockDbAdapter.findMessagesMeta).toHaveBeenCalledTimes(1) - }) - }) - - describe('findPersonUuid', () => { - it('should return account uuid if socialId is in account socialIds', async () => { - mockAccount.socialIds = [socialId] - - const result = await client.findPersonUuid({ ctx: mockCtx, account: mockAccount }, socialId) - - expect(result).toBe(personUuid) - expect(mockAccountClient.findPersonBySocialId).not.toHaveBeenCalled() - }) - - it('should return cached person uuid if available', async () => { - // First call to populate cache - mockAccountClient.findPersonBySocialId.mockResolvedValue(personUuid) - - await client.findPersonUuid({ ctx: mockCtx, account: mockAccount }, socialId) - - expect(mockAccountClient.findPersonBySocialId).toHaveBeenCalledTimes(1) - - // Second call - should use cache - const result = await client.findPersonUuid({ ctx: mockCtx, account: mockAccount }, socialId) - - expect(result).toBe(personUuid) - expect(mockAccountClient.findPersonBySocialId).toHaveBeenCalledTimes(1) - }) - - it('should return undefined if accountsUrl is empty', async () => { - const clientWithoutUrl = new LowLevelClient( - mockDbAdapter, - mockBlob, - { ...mockMetadata, accountsUrl: '' }, - workspace - ) - - const result = await clientWithoutUrl.findPersonUuid({ ctx: mockCtx, account: mockAccount }, socialId) - - expect(result).toBeUndefined() - expect(mockAccountClient.findPersonBySocialId).not.toHaveBeenCalled() - }) - - it('should fetch person uuid from account client', async () => { - mockAccountClient.findPersonBySocialId.mockResolvedValue(personUuid) - - const result = await client.findPersonUuid({ ctx: mockCtx, account: mockAccount }, socialId) - - expect(result).toBe(personUuid) - expect(getAccountClient).toHaveBeenCalledWith(mockMetadata.accountsUrl, 'mock-token') - expect(mockAccountClient.findPersonBySocialId).toHaveBeenCalledWith(socialId, false) - }) - - it('should pass requireAccount parameter to account client', async () => { - mockAccountClient.findPersonBySocialId.mockResolvedValue(personUuid) - - await client.findPersonUuid({ ctx: mockCtx, account: mockAccount }, socialId, true) - - expect(mockAccountClient.findPersonBySocialId).toHaveBeenCalledWith(socialId, true) - }) - - it('should cache person uuid when found', async () => { - mockAccountClient.findPersonBySocialId.mockResolvedValue(personUuid) - - await client.findPersonUuid({ ctx: mockCtx, account: mockAccount }, socialId) - - // Second call should use cache - await client.findPersonUuid({ ctx: mockCtx, account: mockAccount }, socialId) - - expect(mockAccountClient.findPersonBySocialId).toHaveBeenCalledTimes(1) - }) - - it('should not cache when person uuid is undefined', async () => { - mockAccountClient.findPersonBySocialId.mockResolvedValue(undefined) - - await client.findPersonUuid({ ctx: mockCtx, account: mockAccount }, socialId) - - // Second call should try again - await client.findPersonUuid({ ctx: mockCtx, account: mockAccount }, socialId) - - expect(mockAccountClient.findPersonBySocialId).toHaveBeenCalledTimes(2) - }) - - it('should handle errors and log warning', async () => { - const error = new Error('Network error') - mockAccountClient.findPersonBySocialId.mockRejectedValue(error) - - const result = await client.findPersonUuid({ ctx: mockCtx, account: mockAccount }, socialId) - - expect(result).toBeUndefined() - expect(mockCtx.warn).toHaveBeenCalledWith('Cannot find person uuid', { - socialString: socialId, - err: error - }) - }) - }) - - describe('getMessageMeta', () => { - it('should return cached message meta if available', async () => { - const mockMeta: MessageMeta = { - cardId, - id: messageId, - blobId, - createdOn: Date.now() - } as any - - mockDbAdapter.findMessagesMeta.mockResolvedValue([mockMeta]) - - // First call - const result1 = await client.getMessageMeta(cardId, messageId) - expect(result1).toEqual(mockMeta) - expect(mockDbAdapter.findMessagesMeta).toHaveBeenCalledTimes(1) - - // Second call - should use cache - const result2 = await client.getMessageMeta(cardId, messageId) - expect(result2).toEqual(mockMeta) - expect(mockDbAdapter.findMessagesMeta).toHaveBeenCalledTimes(1) - }) - - it('should fetch message meta from db if not cached', async () => { - const mockMeta: MessageMeta = { - cardId, - id: messageId, - blobId, - createdOn: Date.now() - } as any - - mockDbAdapter.findMessagesMeta.mockResolvedValue([mockMeta]) - - const result = await client.getMessageMeta(cardId, messageId) - - expect(result).toEqual(mockMeta) - expect(mockDbAdapter.findMessagesMeta).toHaveBeenCalledWith({ - cardId, - id: messageId - }) - }) - - it('should return undefined if message meta not found', async () => { - mockDbAdapter.findMessagesMeta.mockResolvedValue([]) - - const result = await client.getMessageMeta(cardId, messageId) - - expect(result).toBeUndefined() - }) - - it('should cache message meta after fetching', async () => { - const mockMeta: MessageMeta = { - cardId, - id: messageId, - blobId, - createdOn: Date.now() - } as any - - mockDbAdapter.findMessagesMeta.mockResolvedValue([mockMeta]) - - await client.getMessageMeta(cardId, messageId) - await client.getMessageMeta(cardId, messageId) - - expect(mockDbAdapter.findMessagesMeta).toHaveBeenCalledTimes(1) - }) - - it('should handle different cardId and messageId combinations separately', async () => { - const cardId2 = 'card-2' as CardID - const messageId2 = 'message-2' as MessageID - - const mockMeta1: MessageMeta = { - cardId, - id: messageId, - blobId, - createdOn: Date.now() - } as any - - const mockMeta2: MessageMeta = { - cardId: cardId2, - id: messageId2, - blobId: 'blob-2' as BlobID, - createdOn: Date.now() - } as any - - mockDbAdapter.findMessagesMeta.mockResolvedValueOnce([mockMeta1]).mockResolvedValueOnce([mockMeta2]) - - const result1 = await client.getMessageMeta(cardId, messageId) - const result2 = await client.getMessageMeta(cardId2, messageId2) - - expect(result1).toEqual(mockMeta1) - expect(result2).toEqual(mockMeta2) - expect(mockDbAdapter.findMessagesMeta).toHaveBeenCalledTimes(2) - }) - }) - - describe('removeMessageMeta', () => { - it('should remove message meta from db and cache', async () => { - const mockMeta: MessageMeta = { - cardId, - id: messageId, - blobId, - createdOn: Date.now() - } as any - - // First, add to cache - mockDbAdapter.findMessagesMeta.mockResolvedValue([mockMeta]) - await client.getMessageMeta(cardId, messageId) - - // Remove - await client.removeMessageMeta(cardId, messageId) - - expect(mockDbAdapter.removeMessageMeta).toHaveBeenCalledWith(cardId, messageId) - - // Verify cache is cleared by checking if db is called again - mockDbAdapter.findMessagesMeta.mockResolvedValue([mockMeta]) - await client.getMessageMeta(cardId, messageId) - expect(mockDbAdapter.findMessagesMeta).toHaveBeenCalledTimes(2) - }) - - it('should call db removeMessageMeta even if not in cache', async () => { - await client.removeMessageMeta(cardId, messageId) - - expect(mockDbAdapter.removeMessageMeta).toHaveBeenCalledWith(cardId, messageId) - }) - - it('should only remove specific message from cache', async () => { - const cardId2 = 'card-2' as CardID - const messageId2 = 'message-2' as MessageID - - const mockMeta1: MessageMeta = { - cardId, - id: messageId, - blobId, - createdOn: Date.now() - } as any - - const mockMeta2: MessageMeta = { - cardId: cardId2, - id: messageId2, - blobId: 'blob-2' as BlobID, - createdOn: Date.now() - } as any - - // Add both to cache - mockDbAdapter.findMessagesMeta.mockResolvedValueOnce([mockMeta1]).mockResolvedValueOnce([mockMeta2]) - - await client.getMessageMeta(cardId, messageId) - await client.getMessageMeta(cardId2, messageId2) - - // Remove first one - await client.removeMessageMeta(cardId, messageId) - - // First should be removed from cache - mockDbAdapter.findMessagesMeta.mockResolvedValueOnce([mockMeta1]) - await client.getMessageMeta(cardId, messageId) - expect(mockDbAdapter.findMessagesMeta).toHaveBeenCalledTimes(3) - - // Second should still be cached - await client.getMessageMeta(cardId2, messageId2) - expect(mockDbAdapter.findMessagesMeta).toHaveBeenCalledTimes(3) - }) - }) -}) diff --git a/foundations/communication/packages/server/src/__tests__/error.test.ts b/foundations/communication/packages/server/src/__tests__/error.test.ts deleted file mode 100644 index bba6553b99..0000000000 --- a/foundations/communication/packages/server/src/__tests__/error.test.ts +++ /dev/null @@ -1,307 +0,0 @@ -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. - -import { ApiError } from '../error' - -describe('ApiError', () => { - describe('badRequest', () => { - it('should create an error with 400 status code', () => { - const message = 'Invalid input' - const error = ApiError.badRequest(message) - - expect(error).toBeInstanceOf(ApiError) - expect(error).toBeInstanceOf(Error) - expect(error.code).toBe(400) - expect(error.message).toBe(`Bad Request: ${message}`) - }) - - it('should include the custom message in the error message', () => { - const customMessage = 'Missing required field: userId' - const error = ApiError.badRequest(customMessage) - - expect(error.message).toContain(customMessage) - expect(error.message).toBe(`Bad Request: ${customMessage}`) - }) - - it('should have correct prototype chain', () => { - const error = ApiError.badRequest('test') - - expect(Object.getPrototypeOf(error)).toBe(ApiError.prototype) - }) - }) - - describe('forbidden', () => { - it('should create an error with 403 status code', () => { - const message = 'Access denied' - const error = ApiError.forbidden(message) - - expect(error).toBeInstanceOf(ApiError) - expect(error).toBeInstanceOf(Error) - expect(error.code).toBe(403) - expect(error.message).toBe(`Forbidden: ${message}`) - }) - - it('should include the custom message in the error message', () => { - const customMessage = 'Insufficient permissions' - const error = ApiError.forbidden(customMessage) - - expect(error.message).toContain(customMessage) - expect(error.message).toBe(`Forbidden: ${customMessage}`) - }) - - it('should have correct prototype chain', () => { - const error = ApiError.forbidden('test') - - expect(Object.getPrototypeOf(error)).toBe(ApiError.prototype) - }) - }) - - describe('notFound', () => { - it('should create an error with 404 status code', () => { - const message = 'Resource not found' - const error = ApiError.notFound(message) - - expect(error).toBeInstanceOf(ApiError) - expect(error).toBeInstanceOf(Error) - expect(error.code).toBe(404) - expect(error.message).toBe(`Not Found: ${message}`) - }) - - it('should include the custom message in the error message', () => { - const customMessage = 'User with id 123 not found' - const error = ApiError.notFound(customMessage) - - expect(error.message).toContain(customMessage) - expect(error.message).toBe(`Not Found: ${customMessage}`) - }) - - it('should have correct prototype chain', () => { - const error = ApiError.notFound('test') - - expect(Object.getPrototypeOf(error)).toBe(ApiError.prototype) - }) - }) - - describe('toJSON', () => { - it('should return object with code and message for badRequest', () => { - const message = 'Invalid data' - const error = ApiError.badRequest(message) - const json = error.toJSON() - - expect(json).toEqual({ - code: 400, - message: `Bad Request: ${message}` - }) - }) - - it('should return object with code and message for forbidden', () => { - const message = 'Access denied' - const error = ApiError.forbidden(message) - const json = error.toJSON() - - expect(json).toEqual({ - code: 403, - message: `Forbidden: ${message}` - }) - }) - - it('should return object with code and message for notFound', () => { - const message = 'Resource missing' - const error = ApiError.notFound(message) - const json = error.toJSON() - - expect(json).toEqual({ - code: 404, - message: `Not Found: ${message}` - }) - }) - - it('should return a plain object', () => { - const error = ApiError.badRequest('test') - const json = error.toJSON() - - expect(typeof json).toBe('object') - expect(json.constructor).toBe(Object) - }) - }) - - describe('toString', () => { - it('should return JSON string representation for badRequest', () => { - const message = 'Invalid input' - const error = ApiError.badRequest(message) - const stringified = error.toString() - - expect(stringified).toBe( - JSON.stringify({ - code: 400, - message: `Bad Request: ${message}` - }) - ) - }) - - it('should return JSON string representation for forbidden', () => { - const message = 'No access' - const error = ApiError.forbidden(message) - const stringified = error.toString() - - expect(stringified).toBe( - JSON.stringify({ - code: 403, - message: `Forbidden: ${message}` - }) - ) - }) - - it('should return JSON string representation for notFound', () => { - const message = 'Page not found' - const error = ApiError.notFound(message) - const stringified = error.toString() - - expect(stringified).toBe( - JSON.stringify({ - code: 404, - message: `Not Found: ${message}` - }) - ) - }) - - it('should return valid JSON string', () => { - const error = ApiError.badRequest('test') - const stringified = error.toString() - - expect(() => JSON.parse(stringified)).not.toThrow() - const parsed = JSON.parse(stringified) - expect(parsed).toHaveProperty('code') - expect(parsed).toHaveProperty('message') - }) - }) - - describe('Error behavior', () => { - it('should be catchable as Error', () => { - try { - throw ApiError.badRequest('test error') - } catch (err) { - expect(err).toBeInstanceOf(Error) - expect(err).toBeInstanceOf(ApiError) - } - }) - - it('should preserve error message in stack trace', () => { - const message = 'Test error message' - const error = ApiError.badRequest(message) - - expect(error.stack).toBeDefined() - expect(error.stack).toContain('Bad Request: Test error message') - }) - - it('should have name property inherited from Error', () => { - const error = ApiError.badRequest('test') - - expect(error.name).toBe('Error') - }) - - it('should allow instanceof checks', () => { - const badRequestError = ApiError.badRequest('test') - const forbiddenError = ApiError.forbidden('test') - const notFoundError = ApiError.notFound('test') - - expect(badRequestError instanceof ApiError).toBe(true) - expect(forbiddenError instanceof ApiError).toBe(true) - expect(notFoundError instanceof ApiError).toBe(true) - expect(badRequestError instanceof Error).toBe(true) - expect(forbiddenError instanceof Error).toBe(true) - expect(notFoundError instanceof Error).toBe(true) - }) - }) - - describe('Edge cases', () => { - it('should handle empty string message', () => { - const error = ApiError.badRequest('') - - expect(error.message).toBe('Bad Request: ') - expect(error.code).toBe(400) - }) - - it('should handle special characters in message', () => { - const message = 'Error with "quotes" and \'apostrophes\' and \n newlines' - const error = ApiError.notFound(message) - - expect(error.message).toContain(message) - }) - - it('should handle unicode characters in message', () => { - const message = 'Ошибка с юникодом 🚀 и эмодзи' - const error = ApiError.forbidden(message) - - expect(error.message).toContain(message) - expect((error.toJSON() as any).message).toContain(message) - }) - - it('should handle very long messages', () => { - const longMessage = 'A'.repeat(1000) - const error = ApiError.badRequest(longMessage) - - expect(error.message).toContain(longMessage) - expect(error.message.length).toBeGreaterThan(1000) - }) - }) - - describe('Serialization', () => { - it('should be JSON.stringify compatible', () => { - const error = ApiError.badRequest('test') - - expect(() => JSON.stringify(error)).not.toThrow() - }) - - it('should produce same result from toJSON and JSON.stringify', () => { - const error = ApiError.forbidden('test') - const jsonObject = error.toJSON() - const stringified = JSON.stringify(jsonObject) - - expect(stringified).toBe(error.toString()) - }) - - it('should maintain data integrity through serialization', () => { - const message = 'Original message' - const error = ApiError.notFound(message) - const serialized = error.toString() - const deserialized = JSON.parse(serialized) - - expect(deserialized.code).toBe(404) - expect(deserialized.message).toBe(`Not Found: ${message}`) - }) - }) - - describe('Different error types comparison', () => { - it('should create distinct errors with different codes', () => { - const badRequest = ApiError.badRequest('test') - const forbidden = ApiError.forbidden('test') - const notFound = ApiError.notFound('test') - - expect(badRequest.code).not.toBe(forbidden.code) - expect(badRequest.code).not.toBe(notFound.code) - expect(forbidden.code).not.toBe(notFound.code) - }) - - it('should create distinct messages with different prefixes', () => { - const message = 'same message' - const badRequest = ApiError.badRequest(message) - const forbidden = ApiError.forbidden(message) - const notFound = ApiError.notFound(message) - - expect(badRequest.message).toContain('Bad Request') - expect(forbidden.message).toContain('Forbidden') - expect(notFound.message).toContain('Not Found') - }) - }) -}) diff --git a/foundations/communication/packages/server/src/__tests__/index.test.ts b/foundations/communication/packages/server/src/__tests__/index.test.ts deleted file mode 100644 index 36b331d037..0000000000 --- a/foundations/communication/packages/server/src/__tests__/index.test.ts +++ /dev/null @@ -1,478 +0,0 @@ -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. - -import { MeasureContext, WorkspaceUuid } from '@hcengineering/core' -import { - CardID, - Collaborator, - FindCollaboratorsParams, - FindLabelsParams, - FindMessagesGroupParams, - FindMessagesMetaParams, - FindNotificationContextParams, - FindNotificationsParams, - FindPeersParams, - Label, - MessageMeta, - MessagesGroup, - Notification, - NotificationContext, - Peer -} from '@hcengineering/communication-types' -import { Event, EventResult, SessionData } from '@hcengineering/communication-sdk-types' -import { createDbAdapter } from '@hcengineering/communication-cockroach' -import { Api } from '../index' -import { getMetadata } from '../metadata' -import { buildMiddlewares } from '../middlewares' -import { Blob } from '../blob' -import { LowLevelClient } from '../client' - -// Mock dependencies -jest.mock('@hcengineering/communication-cockroach') -jest.mock('../metadata') -jest.mock('../middlewares') -jest.mock('../blob') -jest.mock('../client') - -describe('Api', () => { - let mockCtx: jest.Mocked - let mockMiddlewares: any - let mockSession: SessionData - let mockDbAdapter: any - let mockBlob: jest.Mocked - let mockClient: jest.Mocked - let mockMetadata: any - - const workspace = 'test-workspace' as WorkspaceUuid - const dbUrl = 'postgresql://test-db' - const cardId = 'test-card-id' as CardID - - const createMockCallbacks = (): { registerAsyncRequest: jest.Mock, broadcast: jest.Mock, enqueue: jest.Mock } => ({ - registerAsyncRequest: jest.fn(), - broadcast: jest.fn(), - enqueue: jest.fn() - }) - - beforeEach(() => { - jest.clearAllMocks() - - // Mock MeasureContext - mockCtx = { - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - newChild: jest.fn().mockReturnThis() - } as any - - // Mock SessionData - mockSession = { - sessionId: 'session-123', - account: { - uuid: 'account-uuid', - socialIds: [] - } - } as any - - // Mock metadata - mockMetadata = { - accountsUrl: 'http://accounts', - hulylakeUrl: 'http://hulylake', - secret: 'test-secret', - messagesPerBlob: 200 - } - ;(getMetadata as jest.Mock).mockReturnValue(mockMetadata) - - // Mock DbAdapter - mockDbAdapter = { - findMessagesMeta: jest.fn(), - close: jest.fn() - } - ;(createDbAdapter as jest.Mock).mockResolvedValue(mockDbAdapter) - - // Mock Blob - mockBlob = { - createMessage: jest.fn() - } as any - ;(Blob as jest.Mock).mockImplementation(() => mockBlob) - - // Mock LowLevelClient - mockClient = { - db: mockDbAdapter, - blob: mockBlob - } as any - ;(LowLevelClient as jest.Mock).mockImplementation(() => mockClient) - - // Mock middlewares - mockMiddlewares = { - findMessagesMeta: jest.fn(), - findMessagesGroups: jest.fn(), - findNotificationContexts: jest.fn(), - findNotifications: jest.fn(), - findLabels: jest.fn(), - findCollaborators: jest.fn(), - findPeers: jest.fn(), - subscribeCard: jest.fn(), - unsubscribeCard: jest.fn(), - event: jest.fn(), - closeSession: jest.fn(), - close: jest.fn() - } - ;(buildMiddlewares as jest.Mock).mockResolvedValue(mockMiddlewares) - }) - - describe('create', () => { - it('should create Api instance with all dependencies', async () => { - const callbacks = createMockCallbacks() - const api = await Api.create(mockCtx, workspace, dbUrl, callbacks) - - expect(api).toBeInstanceOf(Api) - expect(getMetadata).toHaveBeenCalled() - expect(createDbAdapter).toHaveBeenCalledWith(dbUrl, workspace, mockCtx, { withLogs: false }) - expect(Blob).toHaveBeenCalledWith(mockCtx, workspace, mockMetadata) - expect(LowLevelClient).toHaveBeenCalledWith(mockDbAdapter, mockBlob, mockMetadata, workspace) - expect(buildMiddlewares).toHaveBeenCalled() - }) - - it('should initialize middlewares with correct context', async () => { - const callbacks = createMockCallbacks() - await Api.create(mockCtx, workspace, dbUrl, callbacks) - - expect(buildMiddlewares).toHaveBeenCalledWith(mockCtx, workspace, mockMetadata, mockClient, callbacks) - }) - - it('should handle callback functions', async () => { - const callbacks = { - onCardUpdated: jest.fn(), - onMessageCreated: jest.fn(), - registerAsyncRequest: jest.fn(), - broadcast: jest.fn(), - enqueue: jest.fn() - } - - await Api.create(mockCtx, workspace, dbUrl, callbacks) - - expect(buildMiddlewares).toHaveBeenCalled() - }) - }) - - describe('findMessagesMeta', () => { - it('should delegate to middlewares.findMessagesMeta', async () => { - const params: FindMessagesMetaParams = { cardId } - const expectedResult: MessageMeta[] = [ - { cardId, id: 'msg-1' as any, blobId: 'blob-1' as any, createdOn: Date.now() } - ] as any - - mockMiddlewares.findMessagesMeta.mockResolvedValue(expectedResult) - const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) - - const result = await api.findMessagesMeta(mockSession, params) - - expect(result).toEqual(expectedResult) - expect(mockMiddlewares.findMessagesMeta).toHaveBeenCalledWith(mockSession, params) - }) - }) - - describe('findMessagesGroups', () => { - it('should delegate to middlewares.findMessagesGroups', async () => { - const params: FindMessagesGroupParams = { cardId } - const expectedResult: MessagesGroup[] = [{ cardId, blobId: 'blob-1' as any, count: 10 }] as any - - mockMiddlewares.findMessagesGroups.mockResolvedValue(expectedResult) - const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) - - const result = await api.findMessagesGroups(mockSession, params) - - expect(result).toEqual(expectedResult) - expect(mockMiddlewares.findMessagesGroups).toHaveBeenCalledWith(mockSession, params) - }) - }) - - describe('findNotificationContexts', () => { - it('should delegate to middlewares.findNotificationContexts without subscription', async () => { - const params: FindNotificationContextParams = { limit: 10 } - const expectedResult: NotificationContext[] = [{ id: 'ctx-1', cardId, unreadCount: 5 }] as any - - mockMiddlewares.findNotificationContexts.mockResolvedValue(expectedResult) - const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) - - const result = await api.findNotificationContexts(mockSession, params) - - expect(result).toEqual(expectedResult) - expect(mockMiddlewares.findNotificationContexts).toHaveBeenCalledWith(mockSession, params, undefined) - }) - - it('should pass subscription to middlewares.findNotificationContexts', async () => { - const params: FindNotificationContextParams = { limit: 10 } - const subscription = 'sub-123' - const expectedResult: NotificationContext[] = [] - - mockMiddlewares.findNotificationContexts.mockResolvedValue(expectedResult) - const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) - - await api.findNotificationContexts(mockSession, params, subscription) - - expect(mockMiddlewares.findNotificationContexts).toHaveBeenCalledWith(mockSession, params, subscription) - }) - }) - - describe('findNotifications', () => { - it('should delegate to middlewares.findNotifications without subscription', async () => { - const params: FindNotificationsParams = { cardId } - const expectedResult: Notification[] = [{ id: 'notif-1', cardId, isRead: false }] as any - - mockMiddlewares.findNotifications.mockResolvedValue(expectedResult) - const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) - - const result = await api.findNotifications(mockSession, params) - - expect(result).toEqual(expectedResult) - expect(mockMiddlewares.findNotifications).toHaveBeenCalledWith(mockSession, params, undefined) - }) - - it('should pass subscription to middlewares.findNotifications', async () => { - const params: FindNotificationsParams = { cardId } - const subscription = 'sub-456' - const expectedResult: Notification[] = [] - - mockMiddlewares.findNotifications.mockResolvedValue(expectedResult) - const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) - - await api.findNotifications(mockSession, params, subscription) - - expect(mockMiddlewares.findNotifications).toHaveBeenCalledWith(mockSession, params, subscription) - }) - }) - - describe('findLabels', () => { - it('should delegate to middlewares.findLabels', async () => { - const params: FindLabelsParams = { cardId } - const expectedResult: Label[] = [{ id: 'label-1', name: 'Bug', color: 'red' }] as any - - mockMiddlewares.findLabels.mockResolvedValue(expectedResult) - const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) - - const result = await api.findLabels(mockSession, params) - - expect(result).toEqual(expectedResult) - expect(mockMiddlewares.findLabels).toHaveBeenCalledWith(mockSession, params) - }) - }) - - describe('findCollaborators', () => { - it('should delegate to middlewares.findCollaborators', async () => { - const params: FindCollaboratorsParams = { cardId } - const expectedResult: Collaborator[] = [{ personUuid: 'person-1' as any, role: 'owner' }] as any - - mockMiddlewares.findCollaborators.mockResolvedValue(expectedResult) - const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) - - const result = await api.findCollaborators(mockSession, params) - - expect(result).toEqual(expectedResult) - expect(mockMiddlewares.findCollaborators).toHaveBeenCalledWith(mockSession, params) - }) - }) - - describe('findPeers', () => { - it('should delegate to middlewares.findPeers', async () => { - const params: FindPeersParams = { cardId } - const expectedResult: Peer[] = [{ personUuid: 'person-1' as any, name: 'John Doe' }] as any - - mockMiddlewares.findPeers.mockResolvedValue(expectedResult) - const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) - - const result = await api.findPeers(mockSession, params) - - expect(result).toEqual(expectedResult) - expect(mockMiddlewares.findPeers).toHaveBeenCalledWith(mockSession, params) - }) - }) - - describe('subscribeCard', () => { - it('should delegate to middlewares.subscribeCard', async () => { - const subscription = 'sub-789' - const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) - - api.subscribeCard(mockSession, cardId, subscription) - - expect(mockMiddlewares.subscribeCard).toHaveBeenCalledWith(mockSession, cardId, subscription) - }) - - it('should not return a value', async () => { - const subscription = 'sub-789' - const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) - - api.subscribeCard(mockSession, cardId, subscription) - - expect(mockMiddlewares.subscribeCard).toHaveBeenCalled() - }) - }) - - describe('unsubscribeCard', () => { - it('should delegate to middlewares.unsubscribeCard', async () => { - const subscription = 'sub-999' - const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) - - api.unsubscribeCard(mockSession, cardId, subscription) - - expect(mockMiddlewares.unsubscribeCard).toHaveBeenCalledWith(mockSession, cardId, subscription) - }) - - it('should not return a value', async () => { - const subscription = 'sub-999' - const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) - - api.unsubscribeCard(mockSession, cardId, subscription) - - expect(mockMiddlewares.unsubscribeCard).toHaveBeenCalled() - }) - }) - - describe('event', () => { - it('should delegate to middlewares.event', async () => { - const event: Event = { - type: 'message.create', - data: { cardId, content: 'Test message' } - } as any - - const expectedResult: EventResult = { - success: true - } as any - - mockMiddlewares.event.mockResolvedValue(expectedResult) - const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) - - const result = await api.event(mockSession, event) - - expect(result).toEqual(expectedResult) - expect(mockMiddlewares.event).toHaveBeenCalledWith(mockSession, event) - }) - - it('should handle different event types', async () => { - const events: Event[] = [ - { type: 'message.update', data: {} }, - { type: 'message.delete', data: {} }, - { type: 'notification.read', data: {} } - ] as any - - const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) - - for (const event of events) { - mockMiddlewares.event.mockResolvedValue({ success: true }) - await api.event(mockSession, event) - expect(mockMiddlewares.event).toHaveBeenCalledWith(mockSession, event) - } - }) - }) - - describe('closeSession', () => { - it('should delegate to middlewares.closeSession', async () => { - const sessionId = 'session-to-close' - const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) - - await api.closeSession(sessionId) - - expect(mockMiddlewares.closeSession).toHaveBeenCalledWith(sessionId) - }) - - it('should complete without errors', async () => { - const sessionId = 'session-123' - mockMiddlewares.closeSession.mockResolvedValue(undefined) - const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) - - await expect(api.closeSession(sessionId)).resolves.toBeUndefined() - }) - }) - - describe('close', () => { - it('should delegate to middlewares.close', async () => { - const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) - - await api.close() - - expect(mockMiddlewares.close).toHaveBeenCalled() - }) - - it('should complete without errors', async () => { - mockMiddlewares.close.mockResolvedValue(undefined) - const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) - - await expect(api.close()).resolves.toBeUndefined() - }) - }) - - describe('Error handling', () => { - it('should propagate errors from middlewares', async () => { - const error = new Error('Middleware error') - mockMiddlewares.findMessagesMeta.mockRejectedValue(error) - - const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) - const params: FindMessagesMetaParams = { cardId } - - await expect(api.findMessagesMeta(mockSession, params)).rejects.toThrow(error) - }) - - it('should handle errors during creation', async () => { - const error = new Error('DB connection failed') - ;(createDbAdapter as jest.Mock).mockRejectedValue(error) - - await expect(Api.create(mockCtx, workspace, dbUrl, createMockCallbacks())).rejects.toThrow(error) - }) - }) - - describe('Integration scenarios', () => { - it('should handle multiple operations in sequence', async () => { - const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) - - mockMiddlewares.findMessagesMeta.mockResolvedValue([]) - mockMiddlewares.findLabels.mockResolvedValue([]) - mockMiddlewares.findCollaborators.mockResolvedValue([]) - - await api.findMessagesMeta(mockSession, { cardId }) - await api.findLabels(mockSession, { cardId }) - await api.findCollaborators(mockSession, { cardId }) - - expect(mockMiddlewares.findMessagesMeta).toHaveBeenCalledTimes(1) - expect(mockMiddlewares.findLabels).toHaveBeenCalledTimes(1) - expect(mockMiddlewares.findCollaborators).toHaveBeenCalledTimes(1) - }) - - it('should handle subscription and unsubscription flow', async () => { - const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) - const subscription = 'sub-flow' - - api.subscribeCard(mockSession, cardId, subscription) - expect(mockMiddlewares.subscribeCard).toHaveBeenCalledWith(mockSession, cardId, subscription) - - api.unsubscribeCard(mockSession, cardId, subscription) - expect(mockMiddlewares.unsubscribeCard).toHaveBeenCalledWith(mockSession, cardId, subscription) - }) - - it('should handle session lifecycle', async () => { - const api = await Api.create(mockCtx, workspace, dbUrl, createMockCallbacks()) - const sessionId = 'lifecycle-session' - - // Perform operations - mockMiddlewares.findMessagesMeta.mockResolvedValue([]) - await api.findMessagesMeta(mockSession, { cardId }) - - // Close session - await api.closeSession(sessionId) - expect(mockMiddlewares.closeSession).toHaveBeenCalledWith(sessionId) - - // Close API - await api.close() - expect(mockMiddlewares.close).toHaveBeenCalled() - }) - }) -}) diff --git a/foundations/communication/packages/server/src/__tests__/messageId.test.ts b/foundations/communication/packages/server/src/__tests__/messageId.test.ts deleted file mode 100644 index dd622f0b57..0000000000 --- a/foundations/communication/packages/server/src/__tests__/messageId.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. - -import { generateMessageId } from '../messageId' - -describe('messageId', () => { - describe('generateMessageId', () => { - it('should generate a valid MessageID', () => { - const id = generateMessageId() - - expect(id).toBeDefined() - expect(typeof id).toBe('string') - expect(id.length).toBeGreaterThan(0) - }) - - it('should generate numeric string IDs', () => { - const id = generateMessageId() - - expect(/^\d+$/.test(id)).toBe(true) - }) - - it('should generate unique IDs on consecutive calls', () => { - const id1 = generateMessageId() - const id2 = generateMessageId() - const id3 = generateMessageId() - - expect(id1).not.toBe(id2) - expect(id2).not.toBe(id3) - expect(id1).not.toBe(id3) - }) - - it('should generate monotonically increasing IDs', () => { - const id1 = BigInt(generateMessageId()) - const id2 = BigInt(generateMessageId()) - const id3 = BigInt(generateMessageId()) - - expect(id2).toBeGreaterThan(id1) - expect(id3).toBeGreaterThan(id2) - }) - - it('should generate IDs in sequence even when called rapidly', () => { - const ids = [] - const count = 100 - - for (let i = 0; i < count; i++) { - ids.push(generateMessageId()) - } - - const uniqueIds = new Set(ids) - expect(uniqueIds.size).toBe(count) - - for (let i = 1; i < ids.length; i++) { - expect(BigInt(ids[i])).toBeGreaterThan(BigInt(ids[i - 1])) - } - }) - - it('should generate IDs that can be parsed as BigInt', () => { - const id = generateMessageId() - - expect(() => BigInt(id)).not.toThrow() - const bigIntId = BigInt(id) - expect(bigIntId).toBeGreaterThan(0n) - }) - - it('should maintain order across multiple calls', () => { - const batch1 = Array.from({ length: 10 }, () => generateMessageId()) - const batch2 = Array.from({ length: 10 }, () => generateMessageId()) - - const lastFromBatch1 = BigInt(batch1[batch1.length - 1]) - const firstFromBatch2 = BigInt(batch2[0]) - - expect(firstFromBatch2).toBeGreaterThan(lastFromBatch1) - }) - - it('should not generate negative IDs', () => { - for (let i = 0; i < 20; i++) { - const id = generateMessageId() - expect(id.startsWith('-')).toBe(false) - expect(BigInt(id)).toBeGreaterThanOrEqual(0n) - } - }) - }) -}) diff --git a/foundations/communication/packages/server/src/__tests__/metadata.test.ts b/foundations/communication/packages/server/src/__tests__/metadata.test.ts deleted file mode 100644 index 54166941d2..0000000000 --- a/foundations/communication/packages/server/src/__tests__/metadata.test.ts +++ /dev/null @@ -1,287 +0,0 @@ -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. - -import { getMetadata } from '../metadata' -import { MessageID } from '@hcengineering/communication-types' -import { generateMessageId } from '../messageId' - -describe('metadata', () => { - let originalEnv: NodeJS.ProcessEnv - - beforeEach(() => { - // Save original environment - originalEnv = { ...process.env } - }) - - afterEach(() => { - // Restore original environment - process.env = originalEnv - }) - - describe('getMetadata', () => { - it('should return default values when environment variables are not set', () => { - delete process.env.ACCOUNTS_URL - delete process.env.SERVER_SECRET - delete process.env.HULYLAKE_URL - delete process.env.MESSAGES_PER_BLOB - - const metadata = getMetadata() - - expect(metadata).toEqual({ - accountsUrl: '', - secret: 'secret', - hulylakeUrl: 'http://huly.local:8096', - messagesPerBlob: 200 - }) - }) - - it('should use ACCOUNTS_URL from environment', () => { - process.env.ACCOUNTS_URL = 'http://custom-accounts-url' - - const metadata = getMetadata() - - expect(metadata.accountsUrl).toBe('http://custom-accounts-url') - }) - - it('should use SERVER_SECRET from environment', () => { - process.env.SERVER_SECRET = 'custom-secret-key' - - const metadata = getMetadata() - - expect(metadata.secret).toBe('custom-secret-key') - }) - - it('should use HULYLAKE_URL from environment', () => { - process.env.HULYLAKE_URL = 'http://custom-hulylake:9000' - - const metadata = getMetadata() - - expect(metadata.hulylakeUrl).toBe('http://custom-hulylake:9000') - }) - - it('should use MESSAGES_PER_BLOB from environment', () => { - process.env.MESSAGES_PER_BLOB = '500' - - const metadata = getMetadata() - - expect(metadata.messagesPerBlob).toBe(500) - }) - - it('should use all custom environment variables', () => { - process.env.ACCOUNTS_URL = 'http://accounts' - process.env.SERVER_SECRET = 'my-secret' - process.env.HULYLAKE_URL = 'http://hulylake:8080' - process.env.MESSAGES_PER_BLOB = '1000' - - const metadata = getMetadata() - - expect(metadata).toEqual({ - accountsUrl: 'http://accounts', - secret: 'my-secret', - hulylakeUrl: 'http://hulylake:8080', - messagesPerBlob: 1000 - }) - }) - - it('should handle empty string for ACCOUNTS_URL', () => { - process.env.ACCOUNTS_URL = '' - - const metadata = getMetadata() - - expect(metadata.accountsUrl).toBe('') - }) - - it('should convert MESSAGES_PER_BLOB to number', () => { - process.env.MESSAGES_PER_BLOB = '750' - - const metadata = getMetadata() - - expect(typeof metadata.messagesPerBlob).toBe('number') - expect(metadata.messagesPerBlob).toBe(750) - }) - - it('should handle invalid MESSAGES_PER_BLOB as NaN', () => { - process.env.MESSAGES_PER_BLOB = 'invalid' - - const metadata = getMetadata() - - expect(metadata.messagesPerBlob).toBeNaN() - }) - - it('should return a new object on each call', () => { - const metadata1 = getMetadata() - const metadata2 = getMetadata() - - expect(metadata1).not.toBe(metadata2) - expect(metadata1).toEqual(metadata2) - }) - - it('should have correct type structure', () => { - const metadata = getMetadata() - - expect(metadata).toHaveProperty('accountsUrl') - expect(metadata).toHaveProperty('secret') - expect(metadata).toHaveProperty('hulylakeUrl') - expect(metadata).toHaveProperty('messagesPerBlob') - - expect(typeof metadata.accountsUrl).toBe('string') - expect(typeof metadata.secret).toBe('string') - expect(typeof metadata.hulylakeUrl).toBe('string') - expect(typeof metadata.messagesPerBlob).toBe('number') - }) - - it('should handle zero MESSAGES_PER_BLOB', () => { - process.env.MESSAGES_PER_BLOB = '0' - - const metadata = getMetadata() - - expect(metadata.messagesPerBlob).toBe(0) - }) - - it('should handle negative MESSAGES_PER_BLOB', () => { - process.env.MESSAGES_PER_BLOB = '-100' - - const metadata = getMetadata() - - expect(metadata.messagesPerBlob).toBe(-100) - }) - - it('should handle floating point MESSAGES_PER_BLOB', () => { - process.env.MESSAGES_PER_BLOB = '123.45' - - const metadata = getMetadata() - - expect(metadata.messagesPerBlob).toBe(123.45) - }) - - it('should handle URLs with different protocols', () => { - process.env.ACCOUNTS_URL = 'https://secure-accounts.com' - process.env.HULYLAKE_URL = 'wss://hulylake.example.com' - - const metadata = getMetadata() - - expect(metadata.accountsUrl).toBe('https://secure-accounts.com') - expect(metadata.hulylakeUrl).toBe('wss://hulylake.example.com') - }) - - it('should handle URLs with ports', () => { - process.env.ACCOUNTS_URL = 'http://localhost:3000' - process.env.HULYLAKE_URL = 'http://localhost:8096' - - const metadata = getMetadata() - - expect(metadata.accountsUrl).toBe('http://localhost:3000') - expect(metadata.hulylakeUrl).toBe('http://localhost:8096') - }) - }) -}) - -describe('messageId', () => { - describe('generateMessageId', () => { - it('should generate a valid MessageID', () => { - const id = generateMessageId() - - expect(id).toBeDefined() - expect(typeof id).toBe('string') - expect(id.length).toBeGreaterThan(0) - }) - - it('should generate numeric string IDs', () => { - const id = generateMessageId() - - expect(/^\d+$/.test(id)).toBe(true) - }) - - it('should generate unique IDs on consecutive calls', () => { - const id1 = generateMessageId() - const id2 = generateMessageId() - const id3 = generateMessageId() - - expect(id1).not.toBe(id2) - expect(id2).not.toBe(id3) - expect(id1).not.toBe(id3) - }) - - it('should generate monotonically increasing IDs', () => { - const id1 = BigInt(generateMessageId()) - const id2 = BigInt(generateMessageId()) - const id3 = BigInt(generateMessageId()) - - expect(id2).toBeGreaterThan(id1) - expect(id3).toBeGreaterThan(id2) - }) - - it('should generate IDs in sequence even when called rapidly', () => { - const ids: MessageID[] = [] - const count = 100 - - for (let i = 0; i < count; i++) { - ids.push(generateMessageId()) - } - - // Check all IDs are unique - const uniqueIds = new Set(ids) - expect(uniqueIds.size).toBe(count) - - // Check IDs are monotonically increasing - for (let i = 1; i < ids.length; i++) { - expect(BigInt(ids[i])).toBeGreaterThan(BigInt(ids[i - 1])) - } - }) - - it('should handle concurrent generation correctly', () => { - const ids = new Set() - const iterations = 50 - - for (let i = 0; i < iterations; i++) { - ids.add(generateMessageId()) - } - - expect(ids.size).toBe(iterations) - }) - - it('should generate IDs that can be parsed as BigInt', () => { - const id = generateMessageId() - - expect(() => BigInt(id)).not.toThrow() - const bigIntId = BigInt(id) - expect(bigIntId).toBeGreaterThan(0n) - }) - - it('should maintain increasing order across multiple batches', () => { - const batch1 = Array.from({ length: 10 }, () => generateMessageId()) - const batch2 = Array.from({ length: 10 }, () => generateMessageId()) - - const lastFromBatch1 = BigInt(batch1[batch1.length - 1]) - const firstFromBatch2 = BigInt(batch2[0]) - - expect(firstFromBatch2).toBeGreaterThan(lastFromBatch1) - }) - - it('should generate IDs with reasonable length', () => { - const id = generateMessageId() - - // Should be a reasonable length (not too short, not too long) - expect(id.length).toBeGreaterThan(10) - expect(id.length).toBeLessThan(30) - }) - - it('should not generate negative IDs', () => { - for (let i = 0; i < 20; i++) { - const id = generateMessageId() - expect(id.startsWith('-')).toBe(false) - expect(BigInt(id)).toBeGreaterThanOrEqual(0n) - } - }) - }) -}) diff --git a/foundations/communication/packages/server/src/__tests__/middleware/base.test.ts b/foundations/communication/packages/server/src/__tests__/middleware/base.test.ts deleted file mode 100644 index 81d5291ed3..0000000000 --- a/foundations/communication/packages/server/src/__tests__/middleware/base.test.ts +++ /dev/null @@ -1,375 +0,0 @@ -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. - -import { type MeasureContext, type WorkspaceUuid } from '@hcengineering/core' -import { CreateMessageEvent, MessageEventType, type SessionData } from '@hcengineering/communication-sdk-types' -import { - type AccountUuid, - CardID, - type CardType, - type ContextID, - type Markdown, - MessageID, - MessageType, - type SocialID -} from '@hcengineering/communication-types' - -import { BaseMiddleware } from '../../middleware/base' -import { type Enriched, type Middleware, type MiddlewareContext } from '../../types' -import { type LowLevelClient } from '../../client' - -describe('BaseMiddleware', () => { - let mockContext: MiddlewareContext - let mockClient: jest.Mocked - let mockMeasureCtx: jest.Mocked - let mockNext: jest.Mocked - let session: SessionData - let middleware: BaseMiddleware - - const workspace = 'test-workspace' as WorkspaceUuid - const accountUuid = 'account-123' as AccountUuid - const socialId = 'social-123' as SocialID - - const basicEvent = { - _id: 'event-123', - _eventExtra: {}, - socialId, - date: new Date() - } as const - - beforeEach(() => { - jest.clearAllMocks() - - mockMeasureCtx = { - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - newChild: jest.fn().mockReturnThis() - } as any as jest.Mocked - - mockClient = { - db: {}, - blob: {} - } as unknown as jest.Mocked - - mockNext = { - event: jest.fn().mockResolvedValue({ success: true }), - findNotificationContexts: jest.fn().mockResolvedValue([{ id: 'ctx-1' }]), - findNotifications: jest.fn().mockResolvedValue([{ id: 'notif-1' }]), - findLabels: jest.fn().mockResolvedValue([{ labelId: 'label-1' }]), - findCollaborators: jest.fn().mockResolvedValue([{ account: accountUuid }]), - findPeers: jest.fn().mockResolvedValue([{ cardId: 'card-1' }]), - findMessagesMeta: jest.fn().mockResolvedValue([{ messageId: 'msg-1' }]), - findMessagesGroups: jest.fn().mockResolvedValue([{ blobId: 'blob-1' }]), - handleBroadcast: jest.fn(), - subscribeCard: jest.fn(), - unsubscribeCard: jest.fn(), - close: jest.fn(), - closeSession: jest.fn() - } as any as jest.Mocked - - mockContext = { - ctx: mockMeasureCtx, - client: mockClient, - workspace, - metadata: { - accountsUrl: 'http://accounts', - hulylakeUrl: 'http://hulylake', - secret: 'secret', - messagesPerBlob: 100 - }, - cadsWithPeers: new Set() - } - - session = { - account: { - uuid: accountUuid, - socialIds: [socialId] - }, - sessionId: 'session-123' - } as any as SessionData - - middleware = new BaseMiddleware(mockContext, mockNext) - }) - - describe('event', () => { - it('should delegate to next middleware', async () => { - const event: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId: 'card-123' as CardID, - messageId: 'msg-123' as MessageID, - cardType: 'task' as CardType, - messageType: MessageType.Text, - content: 'Test' as Markdown, - socialId - } - - const result = await middleware.event(session, event, false) - - expect(mockNext.event).toHaveBeenCalledWith(session, event, false) - expect(result).toEqual({ success: true }) - }) - - it('should return empty object if no next middleware', async () => { - const middlewareWithoutNext = new BaseMiddleware(mockContext) - const event: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId: 'card-123' as CardID, - messageId: 'msg-123' as MessageID, - cardType: 'task' as CardType, - messageType: MessageType.Text, - content: 'Test' as Markdown, - socialId - } - - const result = await middlewareWithoutNext.event(session, event, false) - - expect(result).toEqual({}) - }) - }) - - describe('findMessagesMeta', () => { - it('should delegate to next middleware', async () => { - const params = { cardId: 'card-123' as CardID } - - const result = await middleware.findMessagesMeta(session, params) - - expect(mockNext.findMessagesMeta).toHaveBeenCalledWith(session, params) - expect(result).toEqual([{ messageId: 'msg-1' }]) - }) - - it('should return empty array if no next middleware', async () => { - const middlewareWithoutNext = new BaseMiddleware(mockContext) - const params = { cardId: 'card-123' as CardID } - - const result = await middlewareWithoutNext.findMessagesMeta(session, params) - - expect(result).toEqual([]) - }) - }) - - describe('findMessagesGroups', () => { - it('should delegate to next middleware', async () => { - const params = { cardId: 'card-123' as CardID } - - const result = await middleware.findMessagesGroups(session, params) - - expect(mockNext.findMessagesGroups).toHaveBeenCalledWith(session, params) - expect(result).toEqual([{ blobId: 'blob-1' }]) - }) - - it('should return empty array if no next middleware', async () => { - const middlewareWithoutNext = new BaseMiddleware(mockContext) - const params = { cardId: 'card-123' as CardID } - - const result = await middlewareWithoutNext.findMessagesGroups(session, params) - - expect(result).toEqual([]) - }) - }) - - describe('findNotificationContexts', () => { - it('should delegate to next middleware', async () => { - const params = { cardId: 'card-123' as CardID, account: accountUuid } - const subscription = 'sub-123' - - const result = await middleware.findNotificationContexts(session, params, subscription) - - expect(mockNext.findNotificationContexts).toHaveBeenCalledWith(session, params, subscription) - expect(result).toEqual([{ id: 'ctx-1' }]) - }) - - it('should return empty array if no next middleware', async () => { - const middlewareWithoutNext = new BaseMiddleware(mockContext) - const params = { cardId: 'card-123' as CardID } - - const result = await middlewareWithoutNext.findNotificationContexts(session, params) - - expect(result).toEqual([]) - }) - }) - - describe('findNotifications', () => { - it('should delegate to next middleware', async () => { - const params = { contextId: 'ctx-123' as ContextID } - const subscription = 'sub-123' - - const result = await middleware.findNotifications(session, params, subscription) - - expect(mockNext.findNotifications).toHaveBeenCalledWith(session, params, subscription) - expect(result).toEqual([{ id: 'notif-1' }]) - }) - - it('should return empty array if no next middleware', async () => { - const middlewareWithoutNext = new BaseMiddleware(mockContext) - const params = { contextId: 'ctx-123' as ContextID } - - const result = await middlewareWithoutNext.findNotifications(session, params) - - expect(result).toEqual([]) - }) - }) - - describe('findLabels', () => { - it('should delegate to next middleware', async () => { - const params = { cardId: 'card-123' as CardID } - const subscription = 'sub-123' - - const result = await middleware.findLabels(session, params, subscription) - - expect(mockNext.findLabels).toHaveBeenCalledWith(session, params, subscription) - expect(result).toEqual([{ labelId: 'label-1' }]) - }) - - it('should return empty array if no next middleware', async () => { - const middlewareWithoutNext = new BaseMiddleware(mockContext) - const params = { cardId: 'card-123' as CardID } - - const result = await middlewareWithoutNext.findLabels(session, params) - - expect(result).toEqual([]) - }) - }) - - describe('findCollaborators', () => { - it('should delegate to next middleware', async () => { - const params = { cardId: 'card-123' as CardID } - - const result = await middleware.findCollaborators(session, params) - - expect(mockNext.findCollaborators).toHaveBeenCalledWith(session, params) - expect(result).toEqual([{ account: accountUuid }]) - }) - - it('should return empty array if no next middleware', async () => { - const middlewareWithoutNext = new BaseMiddleware(mockContext) - const params = { cardId: 'card-123' as CardID } - - const result = await middlewareWithoutNext.findCollaborators(session, params) - - expect(result).toEqual([]) - }) - }) - - describe('findPeers', () => { - it('should delegate to next middleware', async () => { - const params = { workspaceId: workspace, cardId: 'card-123' as CardID } - - const result = await middleware.findPeers(session, params) - - expect(mockNext.findPeers).toHaveBeenCalledWith(session, params) - expect(result).toEqual([{ cardId: 'card-1' }]) - }) - - it('should return empty array if no next middleware', async () => { - const middlewareWithoutNext = new BaseMiddleware(mockContext) - const params = { workspaceId: workspace, cardId: 'card-123' as CardID } - - const result = await middlewareWithoutNext.findPeers(session, params) - - expect(result).toEqual([]) - }) - }) - - describe('handleBroadcast', () => { - it('should delegate to next middleware', () => { - const events: Enriched[] = [ - { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - date: new Date(), - _eventExtra: {} - } - ] - - middleware.handleBroadcast(session, events) - - expect(mockNext.handleBroadcast).toHaveBeenCalledWith(session, events) - }) - - it('should do nothing if no next middleware', () => { - const middlewareWithoutNext = new BaseMiddleware(mockContext) - const events: Enriched[] = [ - { - type: MessageEventType.CreateMessage, - date: new Date(), - _eventExtra: {} - } - ] - - expect(() => { - middlewareWithoutNext.handleBroadcast(session, events) - }).not.toThrow() - }) - }) - - describe('subscribeCard', () => { - it('should delegate to next middleware', () => { - const cardId = 'card-123' as CardID - const subscription = 'sub-123' - - middleware.subscribeCard(session, cardId, subscription) - - expect(mockNext.subscribeCard).toHaveBeenCalledWith(session, cardId, subscription) - }) - - it('should do nothing if no next middleware', () => { - const middlewareWithoutNext = new BaseMiddleware(mockContext) - const cardId = 'card-123' as CardID - const subscription = 'sub-123' - - expect(() => { - middlewareWithoutNext.subscribeCard(session, cardId, subscription) - }).not.toThrow() - }) - }) - - describe('unsubscribeCard', () => { - it('should delegate to next middleware', () => { - const cardId = 'card-123' as CardID - const subscription = 'sub-123' - - middleware.unsubscribeCard(session, cardId, subscription) - - expect(mockNext.unsubscribeCard).toHaveBeenCalledWith(session, cardId, subscription) - }) - - it('should do nothing if no next middleware', () => { - const middlewareWithoutNext = new BaseMiddleware(mockContext) - const cardId = 'card-123' as CardID - const subscription = 'sub-123' - - expect(() => { - middlewareWithoutNext.unsubscribeCard(session, cardId, subscription) - }).not.toThrow() - }) - }) - - describe('close', () => { - it('should call close without errors', () => { - expect(() => { - middleware.close() - }).not.toThrow() - }) - }) - - describe('closeSession', () => { - it('should call closeSession without errors', () => { - expect(() => { - middleware.closeSession('session-123') - }).not.toThrow() - }) - }) -}) diff --git a/foundations/communication/packages/server/src/__tests__/middleware/broadcast.test.ts b/foundations/communication/packages/server/src/__tests__/middleware/broadcast.test.ts deleted file mode 100644 index 20b1e05151..0000000000 --- a/foundations/communication/packages/server/src/__tests__/middleware/broadcast.test.ts +++ /dev/null @@ -1,1071 +0,0 @@ -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. - -import { type MeasureContext, type WorkspaceUuid } from '@hcengineering/core' -import { - AddCollaboratorsEvent, - AttachmentPatchEvent, - BlobPatchEvent, - CardEventType, - CreateLabelEvent, - CreateMessageEvent, - CreateNotificationContextEvent, - CreateNotificationEvent, - CreatePeerEvent, - LabelEventType, - MessageEventType, - NotificationEventType, - PeerEventType, - ReactionPatchEvent, - RemoveCardEvent, - RemoveCollaboratorsEvent, - RemoveLabelEvent, - RemoveNotificationContextEvent, - RemoveNotificationsEvent, - RemovePatchEvent, - RemovePeerEvent, - type SessionData, - ThreadPatchEvent, - TranslateMessageEvent, - UpdateCardTypeEvent, - UpdateNotificationContextEvent, - UpdateNotificationEvent, - UpdatePatchEvent -} from '@hcengineering/communication-sdk-types' -import { - type AccountUuid, - CardID, - type CardType, - ContextID, - Label, - LabelID, - type Markdown, - MessageType, - NotificationContext, - type SocialID -} from '@hcengineering/communication-types' - -import { BroadcastMiddleware } from '../../middleware/broadcast' -import { type CommunicationCallbacks, type Enriched, Middleware, type MiddlewareContext } from '../../types' -import { type LowLevelClient } from '../../client' - -describe('BroadcastMiddleware', () => { - let mockContext: MiddlewareContext - let mockClient: jest.Mocked - let mockMeasureCtx: jest.Mocked - let mockCallbacks: jest.Mocked - let mockNext: any - let session: SessionData - let session2: SessionData - let middleware: BroadcastMiddleware - - const workspace = 'test-workspace' as WorkspaceUuid - const accountUuid = 'account-123' as AccountUuid - const accountUuid2 = 'account-456' as AccountUuid - const socialId = 'social-123' as SocialID - const socialId2 = 'social-456' as SocialID - const cardId = 'card-123' as CardID - const cardId2 = 'card-456' as CardID - - const basicEvent = { - _id: 'event-123', - _eventExtra: {}, - socialId, - date: new Date() - } as const - - // Helper function to create a complete CreateMessage event for session initialization - const createSessionEvent = (_cardId: CardID = cardId): Enriched => ({ - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId: _cardId, - cardType: 'task' as CardType, - messageType: MessageType.Text, - content: 'Test' as Markdown, - socialId, - date: new Date() - }) - - beforeEach(() => { - jest.clearAllMocks() - - mockMeasureCtx = { - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - newChild: jest.fn().mockReturnThis(), - contextData: {} - } as any as jest.Mocked - - mockClient = { - db: {}, - blob: {} - } as unknown as jest.Mocked - - mockCallbacks = { - broadcast: jest.fn(), - enqueue: jest.fn(), - registerAsyncRequest: jest.fn() - } as any as jest.Mocked - - mockNext = { - event: jest.fn().mockResolvedValue({}), - findNotificationContexts: jest.fn().mockResolvedValue([]), - findNotifications: jest.fn().mockResolvedValue([]), - findLabels: jest.fn().mockResolvedValue([]), - findMessagesMeta: jest.fn().mockResolvedValue([]), - findMessagesGroups: jest.fn().mockResolvedValue([]), - findCollaborators: jest.fn().mockResolvedValue([]), - findPeers: jest.fn().mockResolvedValue([]), - subscribeCard: jest.fn(), - unsubscribeCard: jest.fn(), - handleBroadcast: jest.fn(), - closeSession: jest.fn(), - close: jest.fn() - } as any as jest.Mocked - - mockContext = { - ctx: mockMeasureCtx, - client: mockClient, - workspace, - metadata: { - accountsUrl: 'http://accounts', - hulylakeUrl: 'http://hulylake', - secret: 'secret', - messagesPerBlob: 100 - }, - cadsWithPeers: new Set() - } - - session = { - account: { - uuid: accountUuid, - socialIds: [socialId] - }, - sessionId: 'session-123', - contextData: {} - } as any as SessionData - - session2 = { - account: { - uuid: accountUuid2, - socialIds: [socialId2] - }, - sessionId: 'session-456', - contextData: {} - } as any as SessionData - - middleware = new BroadcastMiddleware(mockCallbacks, mockContext, mockNext) - }) - - describe('event', () => { - it('should create session and process event', async () => { - const event: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId, - cardType: 'task' as CardType, - messageType: MessageType.Text, - content: 'Test' as Markdown, - socialId, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockNext.event).toHaveBeenCalledWith(session, event, false) - }) - - it('should handle derived events', async () => { - const event: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId, - cardType: 'task' as CardType, - messageType: MessageType.Text, - content: 'Test' as Markdown, - socialId - } - - await middleware.event(session, event, true) - - expect(mockNext.event).toHaveBeenCalledWith(session, event, true) - }) - - it('should create session for multiple users', async () => { - const event: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId, - cardType: 'task' as CardType, - messageType: MessageType.Text, - content: 'Test' as Markdown, - socialId - } - - await middleware.event(session, event, false) - await middleware.event(session2, event, false) - - expect(mockNext.event).toHaveBeenCalledTimes(2) - }) - }) - - describe('findNotificationContexts', () => { - it('should create session and find notification contexts', async () => { - const params = { cardId } - const contexts = [{ id: 'ctx-1', cardId, account: accountUuid }] - mockNext.findNotificationContexts.mockResolvedValue(contexts) - - const result = await middleware.findNotificationContexts(session, params) - - expect(result).toEqual(contexts) - expect(mockNext.findNotificationContexts).toHaveBeenCalledWith(session, params, undefined) - }) - - it('should subscribe to contexts cards when subscription provided', async () => { - const params = { cardId } - const subscription = 'sub-123' - const contexts = [ - { id: 'ctx-1' as ContextID, cardId: 'card-1' as CardID, account: accountUuid }, - { id: 'ctx-2' as ContextID, cardId: 'card-2' as CardID, account: accountUuid } - ] - mockNext.findNotificationContexts.mockResolvedValue(contexts) - - await middleware.findNotificationContexts(session, params, subscription) - - expect(mockNext.findNotificationContexts).toHaveBeenCalledWith(session, params, subscription) - }) - - it('should auto-subscribe to all returned context cards', async () => { - const params = { cardId } - const subscription = 'sub-123' - const contexts = [ - { id: 'ctx-1' as ContextID, cardId: 'card-1' as CardID, account: accountUuid }, - { id: 'ctx-2' as ContextID, cardId: 'card-2' as CardID, account: accountUuid }, - { id: 'ctx-3' as ContextID, cardId: 'card-3' as CardID, account: accountUuid } - ] - mockNext.findNotificationContexts.mockResolvedValue(contexts) - - await middleware.findNotificationContexts(session, params, subscription) - - // Verify that subscription tracking is working by checking broadcast behavior - const event: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId: 'card-1' as CardID, - cardType: 'task' as CardType, - messageType: MessageType.Text, - content: 'Test' as Markdown - } - - middleware.handleBroadcast(session, [event]) - expect(mockCallbacks.broadcast).toHaveBeenCalled() - }) - - it('should not subscribe when no subscription provided', async () => { - const params = { cardId } - const contexts = [{ id: 'ctx-1' as ContextID, cardId, account: accountUuid }] - mockNext.findNotificationContexts.mockResolvedValue(contexts) - - await middleware.findNotificationContexts(session, params) - - expect(mockNext.findNotificationContexts).toHaveBeenCalled() - }) - - it('should not subscribe when sessionId is empty', async () => { - const sessionWithoutId: SessionData = { - ...session, - sessionId: '' - } as any - - const params = { cardId } - const subscription = 'sub-123' - const contexts: NotificationContext[] = [{ id: 'ctx-1' as ContextID, cardId, account: accountUuid }] as any[] - mockNext.findNotificationContexts.mockResolvedValue(contexts) - - await middleware.findNotificationContexts(sessionWithoutId, params, subscription) - - expect(mockNext.findNotificationContexts).toHaveBeenCalled() - }) - - it('should not subscribe when sessionId is null', async () => { - const sessionWithoutId: SessionData = { - ...session, - sessionId: null - } as any - - const params = { cardId } - const subscription = 'sub-123' - const contexts: NotificationContext[] = [{ id: 'ctx-1' as ContextID, cardId, account: accountUuid }] as any[] - mockNext.findNotificationContexts.mockResolvedValue(contexts) - - await middleware.findNotificationContexts(sessionWithoutId, params, subscription) - - expect(mockNext.findNotificationContexts).toHaveBeenCalled() - }) - }) - - describe('findNotifications', () => { - it('should create session and find notifications', async () => { - const params = { contextId: 'ctx-123' as ContextID } - const notifications = [{ id: 'notif-1' }] - mockNext.findNotifications.mockResolvedValue(notifications) - - const result = await middleware.findNotifications(session, params) - - expect(result).toEqual(notifications) - expect(mockNext.findNotifications).toHaveBeenCalledWith(session, params, undefined) - }) - - it('should pass query id when provided', async () => { - const params = { contextId: 'ctx-123' as ContextID } - const queryId = 'query-456' - const notifications = [{ id: 'notif-1' }] - mockNext.findNotifications.mockResolvedValue(notifications) - - await middleware.findNotifications(session, params, queryId) - - expect(mockNext.findNotifications).toHaveBeenCalledWith(session, params, queryId) - }) - }) - - describe('findLabels', () => { - it('should create session and find labels', async () => { - const params = { cardId } - const labels: Label[] = [{ labelId: 'label-1' as LabelID, cardId }] as any[] - mockNext.findLabels.mockResolvedValue(labels) - - const result = await middleware.findLabels(session, params) - - expect(result).toEqual(labels) - expect(mockNext.findLabels).toHaveBeenCalledWith(session, params, undefined) - }) - - it('should pass query id when provided', async () => { - const params = { cardId } - const queryId = 'query-789' - mockNext.findLabels.mockResolvedValue([]) - - await middleware.findLabels(session, params, queryId) - - expect(mockNext.findLabels).toHaveBeenCalledWith(session, params, queryId) - }) - }) - - describe('subscribeCard', () => { - it('should track subscription for card', () => { - const subscription = 'sub-123' - - middleware.subscribeCard(session, cardId, subscription) - - expect(() => { - middleware.subscribeCard(session, cardId, subscription) - }).not.toThrow() - }) - - it('should handle multiple subscriptions for same card', () => { - const subscription1 = 'sub-1' - const subscription2 = 'sub-2' - - middleware.subscribeCard(session, cardId, subscription1) - middleware.subscribeCard(session, cardId, subscription2) - - expect(() => { - middleware.subscribeCard(session, cardId, subscription1) - }).not.toThrow() - }) - - it('should handle subscriptions for different cards', () => { - const subscription = 'sub-123' - - middleware.subscribeCard(session, cardId, subscription) - middleware.subscribeCard(session, cardId2, subscription) - - expect(() => { - middleware.subscribeCard(session, cardId, subscription) - }).not.toThrow() - }) - - it('should handle subscription with numeric ID', () => { - const subscription = 12345 - - middleware.subscribeCard(session, cardId, subscription) - - expect(() => { - middleware.subscribeCard(session, cardId, subscription) - }).not.toThrow() - }) - - it('should not subscribe when session has no sessionId', () => { - const sessionWithoutId: SessionData = { - ...session, - sessionId: null - } as any - const subscription = 'sub-123' - - expect(() => { - middleware.subscribeCard(sessionWithoutId, cardId, subscription) - }).not.toThrow() - }) - }) - - describe('unsubscribeCard', () => { - it('should remove subscription from card', () => { - const subscription = 'sub-123' - - middleware.subscribeCard(session, cardId, subscription) - middleware.unsubscribeCard(session, cardId, subscription) - - expect(() => { - middleware.unsubscribeCard(session, cardId, subscription) - }).not.toThrow() - }) - - it('should handle unsubscribe when not subscribed', () => { - const subscription = 'sub-123' - - middleware.unsubscribeCard(session, cardId, subscription) - - expect(() => { - middleware.unsubscribeCard(session, cardId, subscription) - }).not.toThrow() - }) - - it('should handle unsubscribe without sessionId', () => { - const sessionWithoutId: SessionData = { - ...session, - sessionId: null - } as any - const subscription = 'sub-123' - - middleware.unsubscribeCard(sessionWithoutId, cardId, subscription) - - expect(() => { - middleware.unsubscribeCard(sessionWithoutId, cardId, subscription) - }).not.toThrow() - }) - - it('should handle unsubscribe for non-existent session', () => { - const subscription = 'sub-123' - - expect(() => { - middleware.unsubscribeCard(session, cardId, subscription) - }).not.toThrow() - }) - - it('should handle unsubscribe for non-existent card', () => { - const subscription = 'sub-123' - - // Create session first - middleware.subscribeCard(session, cardId, subscription) - - // Try to unsubscribe from different card - expect(() => { - middleware.unsubscribeCard(session, cardId2, subscription) - }).not.toThrow() - }) - - it('should only remove specific subscription', async () => { - const subscription1 = 'sub-1' - const subscription2 = 'sub-2' - - // Create session first - await middleware.event(session, createSessionEvent(cardId), false) - - middleware.subscribeCard(session, cardId, subscription1) - middleware.subscribeCard(session, cardId, subscription2) - middleware.unsubscribeCard(session, cardId, subscription1) - - // Verify subscription2 is still active by checking broadcast - const event: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId, - cardType: 'task' as CardType, - messageType: MessageType.Text, - content: 'Test' as Markdown - } - - middleware.handleBroadcast(session, [event]) - expect(mockCallbacks.broadcast).toHaveBeenCalled() - }) - }) - - describe('handleBroadcast', () => { - it('should call broadcast and enqueue', async () => { - // Create session first - await middleware.event(session, createSessionEvent(cardId), false) - - const events: Enriched[] = [ - { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId, - cardType: 'task' as CardType, - messageType: MessageType.Text, - content: 'Test' as Markdown - } - ] - - middleware.subscribeCard(session, cardId, 'sub-123') - middleware.handleBroadcast(session, events) - - expect(mockCallbacks.broadcast).toHaveBeenCalled() - expect(mockCallbacks.enqueue).toHaveBeenCalledWith(expect.anything(), events) - }) - - it('should handle empty events array', () => { - middleware.handleBroadcast(session, []) - - expect(mockCallbacks.broadcast).not.toHaveBeenCalled() - expect(mockCallbacks.enqueue).not.toHaveBeenCalled() - }) - - it('should filter events by subscription', async () => { - // Create session first - await middleware.event(session, createSessionEvent(cardId), false) - - middleware.subscribeCard(session, cardId, 'sub-123') - - const subscribedEvent: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId, - cardType: 'task' as CardType, - messageType: MessageType.Text, - content: 'Test' as Markdown - } - - const unsubscribedEvent: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId: cardId2, - cardType: 'task' as CardType, - messageType: MessageType.Text, - content: 'Test' as Markdown - } - - middleware.handleBroadcast(session, [subscribedEvent, unsubscribedEvent]) - - expect(mockCallbacks.broadcast).toHaveBeenCalled() - const broadcastCall = mockCallbacks.broadcast.mock.calls[0][1] - expect(broadcastCall[session.sessionId ?? '']).toEqual([subscribedEvent]) - }) - - it('should broadcast to multiple sessions', async () => { - // Create sessions first - await middleware.event(session, createSessionEvent(cardId), false) - await middleware.event(session2, createSessionEvent(cardId), false) - - middleware.subscribeCard(session, cardId, 'sub-1') - middleware.subscribeCard(session2, cardId, 'sub-2') - - const event: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId, - cardType: 'task' as CardType, - messageType: MessageType.Text, - content: 'Test' as Markdown - } - - middleware.handleBroadcast(session, [event]) - - expect(mockCallbacks.broadcast).toHaveBeenCalled() - const broadcastCall = mockCallbacks.broadcast.mock.calls[0][1] - expect(broadcastCall[session.sessionId ?? '']).toEqual([event]) - expect(broadcastCall[session2.sessionId ?? '']).toEqual([event]) - }) - - it('should handle MessageEventType.ThreadPatch', async () => { - await middleware.event(session, createSessionEvent(), false) - middleware.subscribeCard(session, cardId, 'sub-123') - - const event: Enriched = { - ...basicEvent, - type: MessageEventType.ThreadPatch, - cardId - } as any - - middleware.handleBroadcast(session, [event]) - expect(mockCallbacks.broadcast).toHaveBeenCalled() - }) - - it('should handle MessageEventType.ReactionPatch', async () => { - await middleware.event(session, createSessionEvent(), false) - middleware.subscribeCard(session, cardId, 'sub-123') - - const event: Enriched = { - ...basicEvent, - type: MessageEventType.ReactionPatch, - cardId - } as any - - middleware.handleBroadcast(session, [event]) - expect(mockCallbacks.broadcast).toHaveBeenCalled() - }) - - it('should handle MessageEventType.BlobPatch', async () => { - await middleware.event(session, createSessionEvent(), false) - middleware.subscribeCard(session, cardId, 'sub-123') - - const event: Enriched = { - ...basicEvent, - type: MessageEventType.BlobPatch, - cardId - } as any - - middleware.handleBroadcast(session, [event]) - expect(mockCallbacks.broadcast).toHaveBeenCalled() - }) - - it('should handle MessageEventType.AttachmentPatch', async () => { - await middleware.event(session, createSessionEvent(), false) - middleware.subscribeCard(session, cardId, 'sub-123') - - const event: Enriched = { - ...basicEvent, - type: MessageEventType.AttachmentPatch, - cardId - } as any - - middleware.handleBroadcast(session, [event]) - expect(mockCallbacks.broadcast).toHaveBeenCalled() - }) - - it('should handle MessageEventType.RemovePatch', async () => { - await middleware.event(session, createSessionEvent(), false) - middleware.subscribeCard(session, cardId, 'sub-123') - - const event: Enriched = { - ...basicEvent, - type: MessageEventType.RemovePatch, - cardId - } as any - - middleware.handleBroadcast(session, [event]) - expect(mockCallbacks.broadcast).toHaveBeenCalled() - }) - - it('should handle MessageEventType.UpdatePatch', async () => { - await middleware.event(session, createSessionEvent(), false) - middleware.subscribeCard(session, cardId, 'sub-123') - - const event: Enriched = { - type: MessageEventType.UpdatePatch, - cardId - } as any - - middleware.handleBroadcast(session, [event]) - expect(mockCallbacks.broadcast).toHaveBeenCalled() - }) - - it('should handle MessageEventType.TranslateMessage', async () => { - await middleware.event(session, createSessionEvent(), false) - middleware.subscribeCard(session, cardId, 'sub-123') - - const event: Enriched = { - type: MessageEventType.TranslateMessage, - cardId - } as any - - middleware.handleBroadcast(session, [event]) - expect(mockCallbacks.broadcast).toHaveBeenCalled() - }) - - it('should handle NotificationEventType.CreateNotification for matching account', async () => { - await middleware.event(session, createSessionEvent(), false) - middleware.subscribeCard(session, cardId, 'sub-123') - - const event: Enriched = { - type: NotificationEventType.CreateNotification, - account: accountUuid - } as any - - middleware.handleBroadcast(session, [event]) - - expect(mockCallbacks.broadcast).toHaveBeenCalled() - const broadcastCall = mockCallbacks.broadcast.mock.calls[0][1] - expect(broadcastCall[session.sessionId ?? '']).toEqual([event]) - }) - - it('should not broadcast NotificationEventType.CreateNotification for different account', async () => { - await middleware.event(session, createSessionEvent(), false) - middleware.subscribeCard(session, cardId, 'sub-123') - - const event: Enriched = { - type: NotificationEventType.CreateNotification, - account: accountUuid2 - } as any - - middleware.handleBroadcast(session, [event]) - - // Enqueue is still called - expect(mockCallbacks.enqueue).toHaveBeenCalled() - // No sessions should receive this event (either undefined or empty array) - const broadcastCall = mockCallbacks.broadcast.mock.calls[0]?.[1] - const sessionEvents = broadcastCall?.[session.sessionId ?? ''] - expect(sessionEvents === undefined || sessionEvents.length === 0).toBe(true) - }) - - it('should handle NotificationEventType.UpdateNotification', async () => { - await middleware.event(session, createSessionEvent(), false) - middleware.subscribeCard(session, cardId, 'sub-123') - - const event: Enriched = { - type: NotificationEventType.UpdateNotification, - account: accountUuid - } as any - - middleware.handleBroadcast(session, [event]) - expect(mockCallbacks.broadcast).toHaveBeenCalled() - }) - - it('should handle NotificationEventType.RemoveNotifications', async () => { - await middleware.event(session, createSessionEvent(), false) - middleware.subscribeCard(session, cardId, 'sub-123') - - const event: Enriched = { - type: NotificationEventType.RemoveNotifications, - account: accountUuid - } as any - - middleware.handleBroadcast(session, [event]) - expect(mockCallbacks.broadcast).toHaveBeenCalled() - }) - - it('should handle NotificationEventType.CreateNotificationContext', async () => { - await middleware.event(session, createSessionEvent(), false) - middleware.subscribeCard(session, cardId, 'sub-123') - - const event: Enriched = { - type: NotificationEventType.CreateNotificationContext, - account: accountUuid - } as any - - middleware.handleBroadcast(session, [event]) - expect(mockCallbacks.broadcast).toHaveBeenCalled() - }) - - it('should handle NotificationEventType.UpdateNotificationContext', async () => { - await middleware.event(session, createSessionEvent(), false) - middleware.subscribeCard(session, cardId, 'sub-123') - - const event: Enriched = { - type: NotificationEventType.UpdateNotificationContext, - account: accountUuid - } as any - - middleware.handleBroadcast(session, [event]) - expect(mockCallbacks.broadcast).toHaveBeenCalled() - }) - - it('should handle NotificationEventType.RemoveNotificationContext', async () => { - await middleware.event(session, createSessionEvent(), false) - middleware.subscribeCard(session, cardId, 'sub-123') - - const event: Enriched = { - type: NotificationEventType.RemoveNotificationContext, - account: accountUuid - } as any - - middleware.handleBroadcast(session, [event]) - expect(mockCallbacks.broadcast).toHaveBeenCalled() - }) - - it('should broadcast NotificationEventType.AddCollaborators to all sessions', async () => { - await middleware.event(session, createSessionEvent(), false) - middleware.subscribeCard(session, cardId, 'sub-123') - - const event: Enriched = { - type: NotificationEventType.AddCollaborators - } as any - - middleware.handleBroadcast(session, [event]) - - expect(mockCallbacks.broadcast).toHaveBeenCalled() - const broadcastCall = mockCallbacks.broadcast.mock.calls[0][1] - expect(broadcastCall[session.sessionId ?? '']).toEqual([event]) - }) - - it('should broadcast NotificationEventType.RemoveCollaborators to all sessions', async () => { - await middleware.event(session, createSessionEvent(), false) - middleware.subscribeCard(session, cardId, 'sub-123') - - const event: Enriched = { - ...basicEvent, - type: NotificationEventType.RemoveCollaborators - } as any - - middleware.handleBroadcast(session, [event]) - expect(mockCallbacks.broadcast).toHaveBeenCalled() - }) - - it('should handle LabelEventType.CreateLabel for matching account', async () => { - await middleware.event(session, createSessionEvent(), false) - middleware.subscribeCard(session, cardId, 'sub-123') - - const event: Enriched = { - ...basicEvent, - type: LabelEventType.CreateLabel, - account: accountUuid - } as any - - middleware.handleBroadcast(session, [event]) - expect(mockCallbacks.broadcast).toHaveBeenCalled() - }) - - it('should handle LabelEventType.RemoveLabel for matching account', async () => { - await middleware.event(session, createSessionEvent(), false) - middleware.subscribeCard(session, cardId, 'sub-123') - - const event: Enriched = { - ...basicEvent, - type: LabelEventType.RemoveLabel, - account: accountUuid - } as any - - middleware.handleBroadcast(session, [event]) - expect(mockCallbacks.broadcast).toHaveBeenCalled() - }) - - it('should broadcast CardEventType.UpdateCardType to all sessions', async () => { - await middleware.event(session, createSessionEvent(), false) - middleware.subscribeCard(session, cardId, 'sub-123') - - const event: Enriched = { - ...basicEvent, - type: CardEventType.UpdateCardType - } as any - - middleware.handleBroadcast(session, [event]) - expect(mockCallbacks.broadcast).toHaveBeenCalled() - }) - - it('should broadcast CardEventType.RemoveCard to all sessions', async () => { - await middleware.event(session, createSessionEvent(), false) - middleware.subscribeCard(session, cardId, 'sub-123') - - const event: Enriched = { - ...basicEvent, - type: CardEventType.RemoveCard - } as any - - middleware.handleBroadcast(session, [event]) - expect(mockCallbacks.broadcast).toHaveBeenCalled() - }) - - it('should not broadcast PeerEventType.CreatePeer', async () => { - await middleware.event(session, createSessionEvent(), false) - middleware.subscribeCard(session, cardId, 'sub-123') - - const event: Enriched = { - ...basicEvent, - type: PeerEventType.CreatePeer - } as any - - middleware.handleBroadcast(session, [event]) - - // Enqueue is still called - expect(mockCallbacks.enqueue).toHaveBeenCalled() - // No sessions should receive this event (either undefined or empty array) - const broadcastCall = mockCallbacks.broadcast.mock.calls[0]?.[1] - const sessionEvents = broadcastCall?.[session.sessionId ?? ''] - expect(sessionEvents === undefined || sessionEvents.length === 0).toBe(true) - }) - - it('should not broadcast PeerEventType.RemovePeer', async () => { - await middleware.event(session, createSessionEvent(), false) - middleware.subscribeCard(session, cardId, 'sub-123') - - const event: Enriched = { - ...basicEvent, - type: PeerEventType.RemovePeer - } as any - - middleware.handleBroadcast(session, [event]) - - // Enqueue is still called - expect(mockCallbacks.enqueue).toHaveBeenCalled() - // No sessions should receive this event (either undefined or empty array) - const broadcastCall = mockCallbacks.broadcast.mock.calls[0]?.[1] - const sessionEvents = broadcastCall?.[session.sessionId ?? ''] - expect(sessionEvents === undefined || sessionEvents.length === 0).toBe(true) - }) - - it('should handle broadcast errors gracefully', async () => { - mockCallbacks.broadcast.mockImplementation(() => { - throw new Error('Broadcast error') - }) - - await middleware.event(session, createSessionEvent(cardId), false) - middleware.subscribeCard(session, cardId, 'sub-123') - - const event: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId, - cardType: 'task' as CardType, - messageType: MessageType.Text, - content: 'Test' as Markdown, - socialId, - date: new Date(), - _eventExtra: {} - } - - expect(() => { - middleware.handleBroadcast(session, [event]) - }).not.toThrow() - - expect(mockMeasureCtx.error).toHaveBeenCalledWith( - 'Failed to broadcast event', - expect.objectContaining({ error: expect.any(Error) }) - ) - }) - - it('should handle enqueue errors gracefully', async () => { - mockCallbacks.enqueue.mockImplementation(() => { - throw new Error('Enqueue error') - }) - - await middleware.event(session, createSessionEvent(cardId), false) - middleware.subscribeCard(session, cardId, 'sub-123') - - const event: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId, - cardType: 'task' as CardType, - messageType: MessageType.Text, - content: 'Test' as Markdown - } - - expect(() => { - middleware.handleBroadcast(session, [event]) - }).not.toThrow() - - expect(mockMeasureCtx.error).toHaveBeenCalledWith( - 'Failed to broadcast event', - expect.objectContaining({ error: expect.any(Error) }) - ) - }) - - it('should pass context data to broadcast', async () => { - await middleware.event(session, createSessionEvent(cardId), false) - middleware.subscribeCard(session, cardId, 'sub-123') - - const event: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId, - cardType: 'task' as CardType, - messageType: MessageType.Text, - content: 'Test' as Markdown - } - - middleware.handleBroadcast(session, [event]) - - expect(mockMeasureCtx.newChild).toHaveBeenCalledWith('enqueue', {}) - expect(mockCallbacks.broadcast).toHaveBeenCalled() - expect(mockCallbacks.enqueue).toHaveBeenCalled() - }) - }) - - describe('close and closeSession', () => { - it('should handle close', async () => { - await middleware.event(session, createSessionEvent(cardId), false) - await middleware.event(session2, createSessionEvent(cardId), false) - - middleware.subscribeCard(session, cardId, 'sub-123') - middleware.subscribeCard(session2, cardId, 'sub-456') - - expect(() => { - middleware.close() - }).not.toThrow() - }) - - it('should clear all sessions on close', async () => { - await middleware.event(session, createSessionEvent(cardId), false) - await middleware.event(session2, createSessionEvent(cardId), false) - - middleware.subscribeCard(session, cardId, 'sub-123') - middleware.subscribeCard(session2, cardId, 'sub-456') - - middleware.close() - - // After close, broadcast should not have any sessions - const event: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId, - cardType: 'task' as CardType, - messageType: MessageType.Text, - content: 'Test' as Markdown - } - - middleware.handleBroadcast(session, [event]) - - // Enqueue is still called but broadcast shouldn't be (no sessions) - expect(mockCallbacks.enqueue).toHaveBeenCalled() - // broadcast should not be called since there are no sessions - const broadcastCalls = mockCallbacks.broadcast.mock.calls - if (broadcastCalls.length > 0) { - const lastBroadcastCall = broadcastCalls[broadcastCalls.length - 1][1] - expect(Object.keys(lastBroadcastCall)).toHaveLength(0) - } - }) - - it('should handle closeSession', async () => { - await middleware.event(session, createSessionEvent(cardId), false) - middleware.subscribeCard(session, cardId, 'sub-123') - - expect(() => { - middleware.closeSession('session-123') - }).not.toThrow() - }) - - it('should remove specific session on closeSession', async () => { - await middleware.event(session, createSessionEvent(cardId), false) - await middleware.event(session2, createSessionEvent(cardId), false) - - middleware.subscribeCard(session, cardId, 'sub-123') - middleware.subscribeCard(session2, cardId, 'sub-456') - - middleware.closeSession('session-123') - - // After closeSession, only session2 should receive broadcasts - const event: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId, - cardType: 'task' as CardType, - messageType: MessageType.Text, - content: 'Test' as Markdown - } - - middleware.handleBroadcast(session, [event]) - - expect(mockCallbacks.broadcast).toHaveBeenCalled() - const broadcastCall = mockCallbacks.broadcast.mock.calls[0][1] - expect(broadcastCall[session.sessionId ?? '']).toBeUndefined() - expect(broadcastCall[session2.sessionId ?? '']).toEqual([event]) - }) - - it('should handle closeSession for non-existent session', () => { - expect(() => { - middleware.closeSession('non-existent-session') - }).not.toThrow() - }) - }) -}) diff --git a/foundations/communication/packages/server/src/__tests__/middleware/date.test.ts b/foundations/communication/packages/server/src/__tests__/middleware/date.test.ts deleted file mode 100644 index 252ac632fc..0000000000 --- a/foundations/communication/packages/server/src/__tests__/middleware/date.test.ts +++ /dev/null @@ -1,249 +0,0 @@ -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. - -import { MeasureContext, systemAccountUuid, WorkspaceUuid } from '@hcengineering/core' -import { Event, MessageEventType, SessionData } from '@hcengineering/communication-sdk-types' -import { - AccountUuid, - CardID, - CardType, - Markdown, - MessageID, - MessageType, - SocialID -} from '@hcengineering/communication-types' - -import { DateMiddleware } from '../../middleware/date' -import { Enriched, Middleware, MiddlewareContext } from '../../types' -import { LowLevelClient } from '../../client' - -describe('DateMiddleware', () => { - let mockContext: MiddlewareContext - let mockClient: jest.Mocked - let mockMeasureCtx: jest.Mocked - let mockNext: jest.Mocked - let session: SessionData - let middleware: DateMiddleware - - const workspace = 'test-workspace' as WorkspaceUuid - const accountUuid = 'account-123' as AccountUuid - const socialId = 'social-123' as SocialID - - const basicEvent = { - _id: 'event-123', - _eventExtra: {}, - socialId, - date: new Date() - } as const - - beforeEach(() => { - jest.clearAllMocks() - - mockMeasureCtx = { - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - newChild: jest.fn().mockReturnThis() - } as any as jest.Mocked - - mockClient = { - db: {}, - blob: {}, - findPersonUuid: jest.fn() - } as any as jest.Mocked - - mockNext = { - event: jest.fn().mockResolvedValue({}), - findMessagesMeta: jest.fn().mockResolvedValue([]), - findMessagesGroups: jest.fn().mockResolvedValue([]), - findNotificationContexts: jest.fn().mockResolvedValue([]), - findNotifications: jest.fn().mockResolvedValue([]), - findLabels: jest.fn().mockResolvedValue([]), - findCollaborators: jest.fn().mockResolvedValue([]), - findPeers: jest.fn().mockResolvedValue([]), - subscribeCard: jest.fn(), - unsubscribeCard: jest.fn(), - handleBroadcast: jest.fn(), - closeSession: jest.fn(), - close: jest.fn() - } as any as jest.Mocked - - mockContext = { - ctx: mockMeasureCtx, - client: mockClient, - workspace, - metadata: { - accountsUrl: 'http://accounts', - hulylakeUrl: 'http://hulylake', - secret: 'secret', - messagesPerBlob: 100 - }, - cadsWithPeers: new Set() - } - - session = { - account: { - uuid: accountUuid, - socialIds: [socialId] - }, - sessionId: 'session-123', - asyncData: [] - } as any as SessionData - - middleware = new DateMiddleware(mockContext, mockNext) - }) - - describe('event', () => { - it('should set date when not provided and not derived', async () => { - const event: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId: 'card-123' as CardID, - cardType: 'task' as CardType, - content: 'Test' as Markdown, - messageType: MessageType.Text - } - - const beforeTime = new Date().getTime() - await middleware.event(session, event, false) - const afterTime = new Date().getTime() - - expect(event.date).toBeDefined() - expect(event.date).toBeInstanceOf(Date) - expect(event.date.getTime()).toBeGreaterThanOrEqual(beforeTime) - expect(event.date.getTime()).toBeLessThanOrEqual(afterTime) - expect(event._eventExtra).toBeDefined() - expect(mockNext.event).toHaveBeenCalledWith(session, event, false) - }) - - it('should preserve date when derived is true and date is provided', async () => { - const customDate = new Date('2025-01-01T00:00:00Z') - const event: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId: 'card-123' as CardID, - cardType: 'task' as CardType, - content: 'Test' as Markdown, - messageType: MessageType.Text, - socialId, - date: customDate - } - - await middleware.event(session, event, true) - - expect(event.date).toEqual(customDate) - expect(mockNext.event).toHaveBeenCalledWith(session, event, true) - }) - - it('should preserve date when user is system account', async () => { - const customDate = new Date('2025-01-01T00:00:00Z') - const systemSession: SessionData = { - ...session, - account: { - uuid: systemAccountUuid, - socialIds: [socialId] - } - } as any as SessionData - - const event: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId: 'card-123' as CardID, - cardType: 'task' as CardType, - content: 'Test' as Markdown, - messageType: MessageType.Text, - socialId, - date: customDate - } - - await middleware.event(systemSession, event, false) - - expect(event.date).toEqual(customDate) - expect(mockNext.event).toHaveBeenCalledWith(systemSession, event, false) - }) - - it('should set date even when null is provided for non-system account', async () => { - const event: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId: 'card-123' as CardID, - cardType: 'task' as CardType, - content: 'Test' as Markdown, - messageType: MessageType.Text, - socialId, - date: null as any - } - - await middleware.event(session, event, false) - - expect(event.date).toBeDefined() - expect(event.date).toBeInstanceOf(Date) - expect(mockNext.event).toHaveBeenCalledWith(session, event, false) - }) - - it('should initialize _eventExtra if not present', async () => { - const event: Enriched = { - ...basicEvent, - _eventExtra: undefined as any, - type: MessageEventType.CreateMessage, - cardId: 'card-123' as CardID, - cardType: 'task' as CardType, - content: 'Test' as Markdown, - messageType: MessageType.Text, - socialId - } - - await middleware.event(session, event, false) - - expect(event._eventExtra).toBeDefined() - expect(event._eventExtra).toEqual({}) - expect(mockNext.event).toHaveBeenCalledWith(session, event, false) - }) - - it('should preserve existing _eventExtra', async () => { - const existingExtra = { someData: 'value' } - const event: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId: 'card-123' as CardID, - cardType: 'task' as CardType, - content: 'Test' as Markdown, - messageType: MessageType.Text, - socialId, - _eventExtra: existingExtra - } - - await middleware.event(session, event, false) - - expect(event._eventExtra).toEqual(existingExtra) - expect(mockNext.event).toHaveBeenCalledWith(session, event, false) - }) - - it('should handle events without date property', async () => { - const event: Enriched = { - ...basicEvent, - type: MessageEventType.RemovePatch, - cardId: 'card-123' as CardID, - messageId: 'msg-123' as MessageID, - socialId, - date: undefined as any - } - - await middleware.event(session, event, false) - - expect(event.date).toBeDefined() - expect(event.date).toBeInstanceOf(Date) - expect(mockNext.event).toHaveBeenCalledWith(session, event, false) - }) - }) -}) diff --git a/foundations/communication/packages/server/src/__tests__/middleware/id.test.ts b/foundations/communication/packages/server/src/__tests__/middleware/id.test.ts deleted file mode 100644 index 8f88e8b13c..0000000000 --- a/foundations/communication/packages/server/src/__tests__/middleware/id.test.ts +++ /dev/null @@ -1,236 +0,0 @@ -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. - -import { MeasureContext, WorkspaceUuid } from '@hcengineering/core' -import { Event, MessageEventType, ReactionPatchEvent, SessionData } from '@hcengineering/communication-sdk-types' -import { - AccountUuid, - CardID, - CardType, Emoji, - Markdown, - MessageID, - MessageType, - SocialID -} from '@hcengineering/communication-types' - -import { IdMiddleware } from '../../middleware/id' -import { Enriched, MiddlewareContext, Middleware } from '../../types' -import { LowLevelClient } from '../../client' - -describe('IdMiddleware', () => { - let mockContext: MiddlewareContext - let mockClient: jest.Mocked - let mockMeasureCtx: jest.Mocked - let mockNext: jest.Mocked - let session: SessionData - let middleware: IdMiddleware - - const workspace = 'test-workspace' as WorkspaceUuid - const accountUuid = 'account-123' as AccountUuid - const socialId = 'social-123' as SocialID - - const basicEvent = { - _id: 'event-123', - _eventExtra: {}, - socialId, - date: new Date() - } as const - - beforeEach(() => { - jest.clearAllMocks() - - mockMeasureCtx = { - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - newChild: jest.fn().mockReturnThis() - } as any as jest.Mocked - - mockClient = { - db: {}, - blob: {} - } as any as jest.Mocked - - mockNext = { - event: jest.fn().mockResolvedValue({}), - findMessagesMeta: jest.fn().mockResolvedValue([]), - findMessagesGroups: jest.fn().mockResolvedValue([]), - findNotificationContexts: jest.fn().mockResolvedValue([]), - findNotifications: jest.fn().mockResolvedValue([]), - findLabels: jest.fn().mockResolvedValue([]), - findCollaborators: jest.fn().mockResolvedValue([]), - findPeers: jest.fn().mockResolvedValue([]), - subscribeCard: jest.fn(), - unsubscribeCard: jest.fn(), - handleBroadcast: jest.fn(), - closeSession: jest.fn(), - close: jest.fn() - } as any as jest.Mocked - - mockContext = { - ctx: mockMeasureCtx, - client: mockClient, - workspace, - metadata: { - accountsUrl: 'http://accounts', - hulylakeUrl: 'http://hulylake', - secret: 'secret', - messagesPerBlob: 100 - }, - cadsWithPeers: new Set() - } - - session = { - account: { - uuid: accountUuid, - socialIds: [socialId] - }, - sessionId: 'session-123', - asyncData: [] - } as any as SessionData - - middleware = new IdMiddleware(mockContext, mockNext) - }) - - describe('CreateMessage events', () => { - it('should generate messageId when not provided', async () => { - const event: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId: 'card-123' as CardID, - cardType: 'task' as CardType, - content: 'Test message' as Markdown, - messageType: MessageType.Text - } - - await middleware.event(session, event, false) - - expect(event.messageId).toBeDefined() - expect(typeof event.messageId).toBe('string') - // @ts-expect-error check messageId - expect(event.messageId.length).toBeGreaterThan(0) - expect(mockNext.event).toHaveBeenCalledWith(session, event, false) - }) - - it('should generate unique messageIds for different events', async () => { - const event1: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId: 'card-123' as CardID, - cardType: 'task' as CardType, - content: 'First message' as Markdown, - messageType: MessageType.Text - } - - const event2: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId: 'card-123' as CardID, - cardType: 'task' as CardType, - content: 'Second message' as Markdown, - messageType: MessageType.Text - } - - await middleware.event(session, event1, false) - await middleware.event(session, event2, false) - - expect(event1.messageId).toBeDefined() - expect(event2.messageId).toBeDefined() - expect(event1.messageId).not.toBe(event2.messageId) - }) - - it('should preserve existing messageId', async () => { - const existingMessageId = 'existing-msg-id' as MessageID - const event: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId: 'card-123' as CardID, - cardType: 'task' as CardType, - content: 'Test message' as Markdown, - messageType: MessageType.Text, - messageId: existingMessageId - } - - await middleware.event(session, event, false) - - expect(event.messageId).toBe(existingMessageId) - expect(mockNext.event).toHaveBeenCalledWith(session, event, false) - }) - - it('should generate messageId for derived CreateMessage events', async () => { - const event: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId: 'card-123' as CardID, - cardType: 'task' as CardType, - content: 'Test' as Markdown, - messageType: MessageType.Text - } - - await middleware.event(session, event, true) - - expect(event.messageId).toBeDefined() - expect(typeof event.messageId).toBe('string') - expect(mockNext.event).toHaveBeenCalledWith(session, event, true) - }) - }) - - describe('Non-CreateMessage events', () => { - it('should not modify messageId for UpdatePatch events', async () => { - const messageId = 'msg-123' as MessageID - const event: Enriched = { - ...basicEvent, - type: MessageEventType.UpdatePatch, - cardId: 'card-123' as CardID, - messageId, - content: 'Updated' as Markdown - } - - await middleware.event(session, event, false) - - expect(event.messageId).toBe(messageId) - expect(mockNext.event).toHaveBeenCalledWith(session, event, false) - }) - - it('should not modify messageId for RemovePatch events', async () => { - const messageId = 'msg-123' as MessageID - const event: Enriched = { - ...basicEvent, - type: MessageEventType.RemovePatch, - cardId: 'card-123' as CardID, - messageId - } - - await middleware.event(session, event, false) - - expect(event.messageId).toBe(messageId) - expect(mockNext.event).toHaveBeenCalledWith(session, event, false) - }) - - it('should not modify messageId for ReactionPatch events', async () => { - const messageId = 'msg-123' as MessageID - const event: Enriched = { - ...basicEvent, - type: MessageEventType.ReactionPatch, - cardId: 'card-123' as CardID, - messageId, - operation: { opcode: 'add', reaction: '👍' as Emoji } - } - - await middleware.event(session, event, false) - - expect(event.messageId).toBe(messageId) - expect(mockNext.event).toHaveBeenCalledWith(session, event, false) - }) - }) -}) diff --git a/foundations/communication/packages/server/src/__tests__/middleware/indentity.test.ts b/foundations/communication/packages/server/src/__tests__/middleware/indentity.test.ts deleted file mode 100644 index 81b843dfd6..0000000000 --- a/foundations/communication/packages/server/src/__tests__/middleware/indentity.test.ts +++ /dev/null @@ -1,332 +0,0 @@ -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. - -import { MeasureContext, systemAccountUuid, WorkspaceUuid } from '@hcengineering/core' -import { Event, MessageEventType, SessionData } from '@hcengineering/communication-sdk-types' -import { - AccountUuid, - CardID, - CardType, - Emoji, - Markdown, - MessageID, - MessageType, - SocialID -} from '@hcengineering/communication-types' -import { IdentityMiddleware } from '../../middleware/indentity' -import { Enriched, Middleware, MiddlewareContext } from '../../types' -import { LowLevelClient } from '../../client' - -describe('IdentityMiddleware', () => { - let mockContext: MiddlewareContext - let mockClient: jest.Mocked - let mockMeasureCtx: jest.Mocked - let mockNext: any - let session: SessionData - let middleware: IdentityMiddleware - let findPersonUuidMock: jest.MockedFunction - - const workspace = 'test-workspace' as WorkspaceUuid - const accountUuid = 'account-123' as AccountUuid - const socialId = 'social-123' as SocialID - - const basicEvent = { - _id: 'event-123', - _eventExtra: {}, - socialId, - date: new Date() - } as const - - beforeEach(() => { - jest.clearAllMocks() - - mockMeasureCtx = { - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - newChild: jest.fn().mockReturnThis() - } as any as jest.Mocked - - const findPersonUuidFn = jest.fn() - - mockClient = { - db: {}, - blob: {}, - findPersonUuid: findPersonUuidFn - } as any as jest.Mocked - - findPersonUuidMock = findPersonUuidFn - - mockNext = { - event: jest.fn().mockResolvedValue({}), - findNotificationContexts: jest.fn().mockResolvedValue([]), - findNotifications: jest.fn().mockResolvedValue([]), - findLabels: jest.fn().mockResolvedValue([]), - findMessagesMeta: jest.fn().mockResolvedValue([]), - findMessagesGroups: jest.fn().mockResolvedValue([]), - findCollaborators: jest.fn().mockResolvedValue([]), - findPeers: jest.fn().mockResolvedValue([]), - subscribeCard: jest.fn(), - unsubscribeCard: jest.fn(), - handleBroadcast: jest.fn(), - closeSession: jest.fn(), - close: jest.fn() - } as any as jest.Mocked - - mockContext = { - ctx: mockMeasureCtx, - client: mockClient, - workspace, - metadata: { - accountsUrl: 'http://accounts', - hulylakeUrl: 'http://hulylake', - secret: 'secret', - messagesPerBlob: 100 - }, - cadsWithPeers: new Set() - } - - session = { - account: { - uuid: accountUuid, - socialIds: [socialId] - }, - sessionId: 'session-123' - } as any as SessionData - - middleware = new IdentityMiddleware(mockContext, mockNext) - }) - - describe('event - personUuid enrichment', () => { - it('should set personUuid for ThreadPatch event', async () => { - const personUuid = 'person-123' - findPersonUuidMock.mockResolvedValue(personUuid as any) - - const event: Enriched = { - ...basicEvent, - type: MessageEventType.ThreadPatch, - cardId: 'card-123' as CardID, - messageId: 'msg-123' as MessageID, - operation: { opcode: 'attach', threadId: 'thread-123' as CardID, threadType: 'threadType' as CardType }, - socialId, - date: new Date() - } - - await middleware.event(session, event, false) - - expect(event.personUuid).toBe(personUuid) - expect(findPersonUuidMock).toHaveBeenCalledWith( - { ctx: mockMeasureCtx, account: session.account }, - socialId - ) - expect(mockNext.event).toHaveBeenCalledWith(session, event, false) - }) - - it('should set personUuid for ReactionPatch event', async () => { - const personUuid = 'person-456' - findPersonUuidMock.mockResolvedValue(personUuid as any) - - const event: Enriched = { - ...basicEvent, - type: MessageEventType.ReactionPatch, - cardId: 'card-123' as CardID, - messageId: 'msg-123' as MessageID, - operation: { opcode: 'add', reaction: '👍' as Emoji } - } - - await middleware.event(session, event, false) - - expect(event.personUuid).toBe(personUuid) - expect(findPersonUuidMock).toHaveBeenCalledWith( - { ctx: mockMeasureCtx, account: session.account }, - socialId - ) - expect(mockNext.event).toHaveBeenCalledWith(session, event, false) - }) - - it('should not set personUuid for CreateMessage event', async () => { - const event: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId: 'card-123' as CardID, - cardType: 'task' as CardType, - content: 'Test' as Markdown, - messageType: MessageType.Text - } - - await middleware.event(session, event, false) - - expect((event as any).personUuid).toBeUndefined() - expect(findPersonUuidMock).not.toHaveBeenCalled() - expect(mockNext.event).toHaveBeenCalledWith(session, event, false) - }) - - it('should handle personUuid as undefined when not found', async () => { - findPersonUuidMock.mockResolvedValue(undefined) - - const event: Enriched = { - ...basicEvent, - type: MessageEventType.ThreadPatch, - cardId: 'card-123' as CardID, - messageId: 'msg-123' as MessageID, - operation: { opcode: 'update', threadId: 'thread-123' as CardID, update: { threadType: 'threadType' as CardType } } - } - - await middleware.event(session, event, false) - - expect(event.personUuid).toBeUndefined() - expect(mockNext.event).toHaveBeenCalledWith(session, event, false) - }) - - it('should work with derived events', async () => { - const personUuid = 'person-789' - findPersonUuidMock.mockResolvedValue(personUuid as any) - - const event: Enriched = { - ...basicEvent, - type: MessageEventType.ReactionPatch, - cardId: 'card-123' as CardID, - messageId: 'msg-123' as MessageID, - operation: { opcode: 'remove', reaction: '❤️' as Emoji } - } - - await middleware.event(session, event, true) - - expect(event.personUuid).toBe(personUuid) - expect(mockNext.event).toHaveBeenCalledWith(session, event, true) - }) - }) - - describe('findNotificationContexts - account enrichment', () => { - it('should enrich params with account for regular user', async () => { - const params = { cardId: 'card-123' as CardID } - await middleware.findNotificationContexts(session, params) - - expect(mockNext.findNotificationContexts).toHaveBeenCalledWith( - session, - { ...params, account: accountUuid }, - undefined - ) - }) - - it('should not enrich params for system account', async () => { - const systemSession: SessionData = { - ...session, - account: { uuid: systemAccountUuid, socialIds: [socialId] } - } as any as SessionData - - const params = { cardId: 'card-123' as CardID } - await middleware.findNotificationContexts(systemSession, params) - - expect(mockNext.findNotificationContexts).toHaveBeenCalledWith(systemSession, params, undefined) - }) - - it('should overwrite existing account in params with session account', async () => { - const otherAccount = 'other-account' as AccountUuid - const params = { cardId: 'card-123' as CardID, account: otherAccount } - await middleware.findNotificationContexts(session, params) - - expect(mockNext.findNotificationContexts).toHaveBeenCalledWith( - session, - { ...params, account: accountUuid }, // Session account overwrites the other account - undefined - ) - }) - - it('should pass subscription to next middleware', async () => { - const params = { cardId: 'card-123' as CardID } - const subscription = 'sub-123' - await middleware.findNotificationContexts(session, params, subscription) - - expect(mockNext.findNotificationContexts).toHaveBeenCalledWith( - session, - { ...params, account: accountUuid }, - subscription - ) - }) - }) - - describe('findNotifications - account enrichment', () => { - it('should enrich params with account for regular user', async () => { - const params = { contextId: 'ctx-123' as any } - await middleware.findNotifications(session, params) - - expect(mockNext.findNotifications).toHaveBeenCalledWith( - session, - { ...params, account: accountUuid }, - undefined - ) - }) - - it('should not enrich params for system account', async () => { - const systemSession: SessionData = { - ...session, - account: { uuid: systemAccountUuid, socialIds: [socialId] } - } as any as SessionData - - const params = { contextId: 'ctx-123' as any } - await middleware.findNotifications(systemSession, params) - - expect(mockNext.findNotifications).toHaveBeenCalledWith(systemSession, params, undefined) - }) - - it('should overwrite existing account in params with session account', async () => { - const otherAccount = 'other-account' as AccountUuid - const params = { contextId: 'ctx-123' as any, account: otherAccount } - await middleware.findNotifications(session, params) - - expect(mockNext.findNotifications).toHaveBeenCalledWith( - session, - { ...params, account: accountUuid }, // Session account overwrites the other account - undefined - ) - }) - }) - - describe('findLabels - account enrichment', () => { - it('should enrich params with account for regular user', async () => { - const params = { cardId: 'card-123' as CardID } - await middleware.findLabels(session, params) - - expect(mockNext.findLabels).toHaveBeenCalledWith( - session, - { ...params, account: accountUuid }, - undefined - ) - }) - - it('should not enrich params for system account', async () => { - const systemSession: SessionData = { - ...session, - account: { uuid: systemAccountUuid, socialIds: [socialId] } - } as any as SessionData - - const params = { cardId: 'card-123' as CardID } - await middleware.findLabels(systemSession, params) - - expect(mockNext.findLabels).toHaveBeenCalledWith(systemSession, params, undefined) - }) - - it('should overwrite existing account in params with session account', async () => { - const otherAccount = 'other-account' as AccountUuid - const params = { cardId: 'card-123' as CardID, account: otherAccount } - await middleware.findLabels(session, params) - - expect(mockNext.findLabels).toHaveBeenCalledWith( - session, - { ...params, account: accountUuid }, // Session account overwrites the other account - undefined - ) - }) - }) -}) diff --git a/foundations/communication/packages/server/src/__tests__/middleware/peer.test.ts b/foundations/communication/packages/server/src/__tests__/middleware/peer.test.ts deleted file mode 100644 index e2723959ff..0000000000 --- a/foundations/communication/packages/server/src/__tests__/middleware/peer.test.ts +++ /dev/null @@ -1,323 +0,0 @@ -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. - -import { MeasureContext, WorkspaceUuid } from '@hcengineering/core' -import { - AttachmentPatchEvent, - CreateMessageEvent, - CreatePeerEvent, - Event, - MessageEventType, - NotificationEventType, - PeerEventType, RemovePatchEvent, - SessionData, UpdatePatchEvent -} from '@hcengineering/communication-sdk-types' -import { - AccountUuid, - CardID, - CardType, - Markdown, - MessageID, - MessageType, - SocialID -} from '@hcengineering/communication-types' -import { PeerMiddleware } from '../../middleware/peer' -import { Enriched, Middleware, MiddlewareContext } from '../../types' -import { LowLevelClient } from '../../client' - -describe('PeerMiddleware', () => { - let mockContext: MiddlewareContext - let mockClient: jest.Mocked - let mockMeasureCtx: jest.Mocked - let mockNext: any - let mockHead: any - let session: SessionData - let middleware: PeerMiddleware - - const workspace = 'test-workspace' as WorkspaceUuid - const accountUuid = 'account-123' as AccountUuid - const socialId = 'social-123' as SocialID - const cardId = 'card-123' as CardID - - const basicEvent = { - _id: 'event-123', - _eventExtra: {}, - socialId, - date: new Date() - } as const - - beforeEach(() => { - jest.clearAllMocks() - - mockMeasureCtx = { - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - newChild: jest.fn().mockReturnThis() - } as any as jest.Mocked - - mockClient = { - db: {}, - blob: {} - } as any as jest.Mocked - - mockNext = { - event: jest.fn().mockResolvedValue({}), - findMessagesMeta: jest.fn().mockResolvedValue([]), - findMessagesGroups: jest.fn().mockResolvedValue([]), - findNotificationContexts: jest.fn().mockResolvedValue([]), - findNotifications: jest.fn().mockResolvedValue([]), - findLabels: jest.fn().mockResolvedValue([]), - findCollaborators: jest.fn().mockResolvedValue([]), - findPeers: jest.fn().mockResolvedValue([]), - subscribeCard: jest.fn(), - unsubscribeCard: jest.fn(), - handleBroadcast: jest.fn(), - closeSession: jest.fn(), - close: jest.fn() - } as any as jest.Mocked - - mockHead = { - findPeers: jest.fn().mockResolvedValue([]), - event: jest.fn().mockResolvedValue({}), - findMessagesMeta: jest.fn().mockResolvedValue([]), - findMessagesGroups: jest.fn().mockResolvedValue([]), - findNotificationContexts: jest.fn().mockResolvedValue([]), - findNotifications: jest.fn().mockResolvedValue([]), - findLabels: jest.fn().mockResolvedValue([]), - findCollaborators: jest.fn().mockResolvedValue([]), - subscribeCard: jest.fn(), - unsubscribeCard: jest.fn(), - handleBroadcast: jest.fn(), - closeSession: jest.fn(), - close: jest.fn() - } as any as jest.Mocked - - mockContext = { - ctx: mockMeasureCtx, - client: mockClient, - workspace, - metadata: { - accountsUrl: 'http://accounts', - hulylakeUrl: 'http://hulylake', - secret: 'secret', - messagesPerBlob: 100 - }, - cadsWithPeers: new Set(), - head: mockHead - } - - session = { - account: { - uuid: accountUuid, - socialIds: [socialId] - }, - sessionId: 'session-123' - } as any as SessionData - - middleware = new PeerMiddleware(mockContext, mockNext) - }) - - describe('CreatePeer events', () => { - it('should add cardId to cadsWithPeers on CreatePeer event', async () => { - const event: Enriched = { - ...basicEvent, - workspaceId: workspace, - kind: 'card', - type: PeerEventType.CreatePeer, - cardId, - value: '123' - } - - expect(mockContext.cadsWithPeers.has(cardId as any)).toBe(false) - - await middleware.event(session, event, false) - - expect(mockContext.cadsWithPeers.has(cardId as any)).toBe(true) - expect(mockNext.event).toHaveBeenCalledWith(session, event, false) - }) - - it('should work with derived CreatePeer events', async () => { - const event: Enriched = { - ...basicEvent, - workspaceId: workspace, - kind: 'card', - type: PeerEventType.CreatePeer, - cardId, - value: '123' - } - - await middleware.event(session, event, true) - - expect(mockContext.cadsWithPeers.has(cardId as any)).toBe(true) - expect(mockNext.event).toHaveBeenCalledWith(session, event, true) - }) - }) - - describe('Message events with peers', () => { - beforeEach(() => { - mockContext.cadsWithPeers.add(cardId as any) - }) - - it('should fetch and attach peers for CreateMessage event', async () => { - const peers = [ - { kind: 'card', members: [{ workspaceId: workspace, cardId: 'peer-card' }] } - ] - mockHead.findPeers.mockResolvedValue(peers) - - const event: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId, - cardType: 'task' as CardType, - content: 'Test' as Markdown, - messageType: MessageType.Text - } - - await middleware.event(session, event, false) - - expect(mockHead.findPeers).toHaveBeenCalledWith(session, { - workspaceId: workspace, - cardId - }) - expect(event._eventExtra?.peers).toEqual(peers) - expect(mockNext.event).toHaveBeenCalledWith(session, event, false) - }) - - it('should fetch and attach peers for UpdatePatch event', async () => { - const peers = [{ kind: 'card', members: [] }] - mockHead.findPeers.mockResolvedValue(peers) - - const event: Enriched = { - ...basicEvent, - type: MessageEventType.UpdatePatch, - cardId, - messageId: 'msg-123' as MessageID, - content: 'Updated' as Markdown - } - - await middleware.event(session, event, false) - - expect(mockHead.findPeers).toHaveBeenCalledWith(session, { - workspaceId: workspace, - cardId - }) - expect(event._eventExtra?.peers).toEqual(peers) - expect(mockNext.event).toHaveBeenCalledWith(session, event, false) - }) - - it('should fetch and attach peers for RemovePatch event', async () => { - const peers = [{ kind: 'card', members: [] }] - mockHead.findPeers.mockResolvedValue(peers) - - const event: Enriched = { - ...basicEvent, - type: MessageEventType.RemovePatch, - cardId, - messageId: 'msg-123' as MessageID - } - - await middleware.event(session, event, false) - - expect(event._eventExtra?.peers).toEqual(peers) - expect(mockNext.event).toHaveBeenCalledWith(session, event, false) - }) - - it('should fetch and attach peers for AttachmentPatch event', async () => { - const peers = [{ kind: 'card', members: [] }] - mockHead.findPeers.mockResolvedValue(peers) - - const event: Enriched = { - ...basicEvent, - type: MessageEventType.AttachmentPatch, - cardId, - messageId: 'msg-123' as MessageID, - operations: [] - } - - await middleware.event(session, event, false) - - expect(event._eventExtra?.peers).toEqual(peers) - expect(mockNext.event).toHaveBeenCalledWith(session, event, false) - }) - - it('should initialize _eventExtra if not present', async () => { - const peers = [{ kind: 'card', members: [] }] - mockHead.findPeers.mockResolvedValue(peers) - - const event: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId, - cardType: 'task' as CardType, - content: 'Test' as Markdown, - messageType: MessageType.Text - } - - await middleware.event(session, event, false) - - expect(event._eventExtra).toBeDefined() - expect(event._eventExtra?.peers).toEqual(peers) - expect(mockNext.event).toHaveBeenCalledWith(session, event, false) - }) - - it('should handle errors when fetching peers', async () => { - mockHead.findPeers.mockRejectedValue(new Error('Fetch error')) - - const event: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId, - cardType: 'task' as CardType, - content: 'Test' as Markdown, - messageType: MessageType.Text - } - - await expect(middleware.event(session, event, false)).rejects.toThrow('Fetch error') - }) - }) - - describe('Message events without peers', () => { - it('should not fetch peers when cardId not in cadsWithPeers', async () => { - const event: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId: 'card-456' as CardID, - cardType: 'task' as CardType, - content: 'Test' as Markdown, - messageType: MessageType.Text - } - - await middleware.event(session, event, false) - - expect(mockHead.findPeers).not.toHaveBeenCalled() - expect(event._eventExtra?.peers).toEqual([{ kind: 'card', members: [] }]) - expect(mockNext.event).toHaveBeenCalledWith(session, event, false) - }) - }) - - describe('Non-message events', () => { - it('should not fetch peers for notification events', async () => { - const event: Enriched = { - ...basicEvent, - type: NotificationEventType.CreateNotification, - account: accountUuid - } as any as Enriched - - await middleware.event(session, event, false) - - expect(mockHead.findPeers).not.toHaveBeenCalled() - expect(mockNext.event).toHaveBeenCalledWith(session, event, false) - }) - }) -}) diff --git a/foundations/communication/packages/server/src/__tests__/middleware/permissions.test.ts b/foundations/communication/packages/server/src/__tests__/middleware/permissions.test.ts deleted file mode 100644 index 6adb9cbd32..0000000000 --- a/foundations/communication/packages/server/src/__tests__/middleware/permissions.test.ts +++ /dev/null @@ -1,759 +0,0 @@ -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. - -import { AccountRole, MeasureContext, systemAccountUuid, WorkspaceUuid } from '@hcengineering/core' -import { - Event, - MessageEventType, - NotificationEventType, - PeerEventType, - SessionData -} from '@hcengineering/communication-sdk-types' -import { - AccountUuid, - CardID, - CardType, ContextID, Emoji, - Markdown, - MessageID, - MessageType, NotificationID, - SocialID -} from '@hcengineering/communication-types' - -import { PermissionsMiddleware } from '../../middleware/permissions' -import { Enriched, Middleware, MiddlewareContext } from '../../types' -import { LowLevelClient } from '../../client' - -describe('PermissionsMiddleware', () => { - let mockContext: MiddlewareContext - let mockClient: jest.Mocked - let mockMeasureCtx: jest.Mocked - let mockNext: any - let session: SessionData - let middleware: PermissionsMiddleware - let getMessageMetaMock: jest.MockedFunction - - const workspace = 'test-workspace' as WorkspaceUuid - const accountUuid = 'account-123' as AccountUuid - const socialId = 'social-123' as SocialID - const cardId = 'card-123' as CardID - - const basicEvent = { - _id: 'event-123', - _eventExtra: {}, - socialId, - date: new Date() - } as const - - beforeEach(() => { - jest.clearAllMocks() - - mockMeasureCtx = { - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - newChild: jest.fn().mockReturnThis() - } as any as jest.Mocked - - const getMessageMetaFn = jest.fn() - - mockClient = { - db: {}, - blob: {}, - getMessageMeta: getMessageMetaFn - } as any as jest.Mocked - - getMessageMetaMock = getMessageMetaFn - - mockNext = { - event: jest.fn().mockResolvedValue({}), - findMessagesMeta: jest.fn().mockResolvedValue([]), - findMessagesGroups: jest.fn().mockResolvedValue([]), - findNotificationContexts: jest.fn().mockResolvedValue([]), - findNotifications: jest.fn().mockResolvedValue([]), - findLabels: jest.fn().mockResolvedValue([]), - findCollaborators: jest.fn().mockResolvedValue([]), - findPeers: jest.fn().mockResolvedValue([]), - subscribeCard: jest.fn(), - unsubscribeCard: jest.fn(), - handleBroadcast: jest.fn(), - closeSession: jest.fn(), - close: jest.fn() - } as any as jest.Mocked - - mockContext = { - ctx: mockMeasureCtx, - client: mockClient, - workspace, - metadata: { - accountsUrl: 'http://accounts', - hulylakeUrl: 'http://hulylake', - secret: 'secret', - messagesPerBlob: 100 - }, - cadsWithPeers: new Set() - } - - session = { - account: { - uuid: accountUuid, - socialIds: [socialId], - role: AccountRole.User - }, - sessionId: 'session-123' - } as any as SessionData - - middleware = new PermissionsMiddleware(mockContext, mockNext) - }) - - describe('Derived events', () => { - it('should skip permission checks for derived events', async () => { - const event: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId, - cardType: 'task' as CardType, - content: 'Test' as Markdown, - messageType: MessageType.Text, - socialId: 'other-social' as SocialID - } - - await middleware.event(session, event, true) - - expect(mockNext.event).toHaveBeenCalledWith(session, event, true) - }) - - it('should allow any operation for derived events', async () => { - const event: Enriched = { - ...basicEvent, - type: MessageEventType.UpdatePatch, - cardId, - messageId: 'msg-123' as MessageID, - content: 'Updated' as Markdown, - socialId: 'other-social' as SocialID - } - - await middleware.event(session, event, true) - - expect(getMessageMetaMock).not.toHaveBeenCalled() - expect(mockNext.event).toHaveBeenCalledWith(session, event, true) - }) - }) - - describe('System account', () => { - it('should allow all operations for system account', async () => { - const systemSession: SessionData = { - ...session, - account: { - uuid: systemAccountUuid, - socialIds: [socialId], - role: AccountRole.User - } - } as any as SessionData - - const event: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId, - cardType: 'task' as CardType, - content: 'Test' as Markdown, - messageType: MessageType.Text, - socialId: 'any-social' as SocialID - } - - await middleware.event(systemSession, event, false) - - expect(mockNext.event).toHaveBeenCalledWith(systemSession, event, false) - }) - - it('should allow system account to use any socialId', async () => { - const systemSession: SessionData = { - ...session, - account: { - uuid: systemAccountUuid, - socialIds: [socialId], - role: AccountRole.User - } - } as any as SessionData - - const event: Enriched = { - ...basicEvent, - type: MessageEventType.ReactionPatch, - cardId, - messageId: 'msg-123' as MessageID, - operation: { opcode: 'add', reaction: '👍' as Emoji }, - socialId: 'different-social' as SocialID - } - - await middleware.event(systemSession, event, false) - - expect(mockNext.event).toHaveBeenCalled() - }) - }) - - describe('CreateMessage events', () => { - it('should allow CreateMessage with correct socialId', async () => { - const event: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId, - cardType: 'task' as CardType, - content: 'Test' as Markdown, - messageType: MessageType.Text - } - - await middleware.event(session, event, false) - - expect(mockNext.event).toHaveBeenCalledWith(session, event, false) - }) - - it('should reject CreateMessage with incorrect socialId', async () => { - const event: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId, - cardType: 'task' as CardType, - content: 'Test' as Markdown, - messageType: MessageType.Text, - socialId: 'other-social' as SocialID - } - - await expect(middleware.event(session, event, false)).rejects.toThrow() - }) - - it('should force noNotify to false for non-system accounts', async () => { - const event: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId, - cardType: 'task' as CardType, - content: 'Test' as Markdown, - messageType: MessageType.Text, - options: { noNotify: true } - } - - await middleware.event(session, event, false) - - expect(event.options?.noNotify).toBe(false) - expect(mockNext.event).toHaveBeenCalledWith(session, event, false) - }) - - it('should allow noNotify for system account', async () => { - const systemSession: SessionData = { - ...session, - account: { - uuid: systemAccountUuid, - socialIds: [socialId], - role: AccountRole.User - } - } as any as SessionData - - const event: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId, - cardType: 'task' as CardType, - content: 'Test' as Markdown, - messageType: MessageType.Text, - options: { noNotify: true } - } - - await middleware.event(systemSession, event, false) - - expect(event.options?.noNotify).toBe(true) - expect(mockNext.event).toHaveBeenCalledWith(systemSession, event, false) - }) - }) - - describe('UpdatePatch events', () => { - it('should allow UpdatePatch when user is message author', async () => { - const messageId = 'msg-123' as MessageID - getMessageMetaMock.mockResolvedValue({ - creator: socialId, - cardId, - id: messageId - } as any) - - const event: Enriched = { - ...basicEvent, - type: MessageEventType.UpdatePatch, - cardId, - messageId, - content: 'Updated' as Markdown - } - - await middleware.event(session, event, false) - - expect(getMessageMetaMock).toHaveBeenCalledWith(cardId, messageId) - expect(mockNext.event).toHaveBeenCalledWith(session, event, false) - }) - - it('should reject UpdatePatch when user is not message author', async () => { - const messageId = 'msg-123' as MessageID - getMessageMetaMock.mockResolvedValue({ - creator: 'other-social' as SocialID, - cardId, - id: messageId - } as any) - - const event: Enriched = { - ...basicEvent, - type: MessageEventType.UpdatePatch, - cardId, - messageId, - content: 'Updated' as Markdown - } - - await expect(middleware.event(session, event, false)).rejects.toThrow('message author is not allowed') - }) - - it('should reject UpdatePatch when message not found', async () => { - getMessageMetaMock.mockResolvedValue(undefined) - - const event: Enriched = { - ...basicEvent, - type: MessageEventType.UpdatePatch, - cardId, - messageId: 'msg-123' as MessageID, - content: 'Updated' as Markdown - } - - await expect(middleware.event(session, event, false)).rejects.toThrow('message not found') - }) - - it('should reject UpdatePatch with incorrect socialId', async () => { - const event: Enriched = { - ...basicEvent, - type: MessageEventType.UpdatePatch, - cardId, - messageId: 'msg-123' as MessageID, - content: 'Updated' as Markdown, - socialId: 'other-social' as SocialID - } - - await expect(middleware.event(session, event, false)).rejects.toThrow() - }) - }) - - describe('RemovePatch events', () => { - it('should allow RemovePatch when user is message author', async () => { - const messageId = 'msg-123' as MessageID - getMessageMetaMock.mockResolvedValue({ - creator: socialId, - cardId, - id: messageId - } as any) - - const event: Enriched = { - ...basicEvent, - type: MessageEventType.RemovePatch, - cardId, - messageId - } - - await middleware.event(session, event, false) - - expect(getMessageMetaMock).toHaveBeenCalledWith(cardId, messageId) - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should reject RemovePatch when user is not message author', async () => { - const messageId = 'msg-123' as MessageID - getMessageMetaMock.mockResolvedValue({ - creator: 'other-social' as SocialID, - cardId, - id: messageId - } as any) - - const event: Enriched = { - ...basicEvent, - type: MessageEventType.RemovePatch, - cardId, - messageId - } - - await expect(middleware.event(session, event, false)).rejects.toThrow('message author is not allowed') - }) - }) - - describe('AttachmentPatch and BlobPatch events', () => { - it('should allow AttachmentPatch when user is message author', async () => { - const messageId = 'msg-123' as MessageID - getMessageMetaMock.mockResolvedValue({ - creator: socialId, - cardId, - id: messageId - } as any) - - const event: Enriched = { - ...basicEvent, - type: MessageEventType.AttachmentPatch, - cardId, - messageId, - operations: [] - } - - await middleware.event(session, event, false) - - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should allow BlobPatch when user is message author', async () => { - const messageId = 'msg-123' as MessageID - getMessageMetaMock.mockResolvedValue({ - creator: socialId, - cardId, - id: messageId - } as any) - - const event: Enriched = { - ...basicEvent, - type: MessageEventType.BlobPatch, - cardId, - messageId, - operations: [] - } - - await middleware.event(session, event, false) - - expect(mockNext.event).toHaveBeenCalled() - }) - }) - - describe('ReactionPatch and ThreadPatch events', () => { - it('should allow ReactionPatch with correct socialId', async () => { - const event: Enriched = { - ...basicEvent, - type: MessageEventType.ReactionPatch, - cardId, - messageId: 'msg-123' as MessageID, - operation: { opcode: 'add', reaction: '👍' as Emoji } - } - - await middleware.event(session, event, false) - - expect(mockNext.event).toHaveBeenCalledWith(session, event, false) - }) - - it('should reject ReactionPatch with incorrect socialId', async () => { - const event: Enriched = { - ...basicEvent, - type: MessageEventType.ReactionPatch, - cardId, - messageId: 'msg-123' as MessageID, - operation: { opcode: 'add', reaction: '👍' as Emoji }, - socialId: 'other-social' as SocialID - } - - await expect(middleware.event(session, event, false)).rejects.toThrow() - }) - - it('should allow ThreadPatch with correct socialId', async () => { - const event: Enriched = { - ...basicEvent, - type: MessageEventType.ThreadPatch, - cardId, - messageId: 'msg-123' as MessageID, - operation: { opcode: 'attach', threadId: 'thread-123' as CardID, threadType: 'threadType' as CardType } - } - - await middleware.event(session, event, false) - - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should reject ThreadPatch with incorrect socialId', async () => { - const event: Enriched = { - ...basicEvent, - type: MessageEventType.ThreadPatch, - cardId, - messageId: 'msg-123' as MessageID, - operation: { opcode: 'attach', threadId: 'thread-123' as CardID, threadType: 'threadType' as CardType }, - socialId: 'other-social' as SocialID - } - - await expect(middleware.event(session, event, false)).rejects.toThrow() - }) - }) - - describe('Notification events', () => { - it('should allow UpdateNotificationContext for own account', async () => { - const event: Enriched = { - ...basicEvent, - type: NotificationEventType.UpdateNotificationContext, - contextId: 'ctx-123' as ContextID, - account: accountUuid, - updates: {} - } - - await middleware.event(session, event, false) - - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should reject UpdateNotificationContext for other account', async () => { - const event: Enriched = { - ...basicEvent, - type: NotificationEventType.UpdateNotificationContext, - contextId: 'ctx-123' as ContextID, - account: 'other-account' as AccountUuid, - updates: {} - } - - await expect(middleware.event(session, event, false)).rejects.toThrow() - }) - - it('should allow RemoveNotifications for own account', async () => { - const event: Enriched = { - ...basicEvent, - type: NotificationEventType.RemoveNotifications, - contextId: 'ctx-123' as ContextID, - account: accountUuid, - ids: ['notif-1' as NotificationID] - } - - await middleware.event(session, event, false) - - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should reject RemoveNotifications for other account', async () => { - const event: Enriched = { - ...basicEvent, - type: NotificationEventType.RemoveNotifications, - contextId: 'ctx-123' as ContextID, - account: 'other-account' as AccountUuid, - ids: ['notif-1' as NotificationID] - } - - await expect(middleware.event(session, event, false)).rejects.toThrow() - }) - - it('should allow UpdateNotification for own account', async () => { - const event: Enriched = { - ...basicEvent, - type: NotificationEventType.UpdateNotification, - contextId: 'ctx-123' as ContextID, - account: accountUuid, - updates: { read: true }, - query: {} - } - - await middleware.event(session, event, false) - - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should allow RemoveNotificationContext for own account', async () => { - const event: Enriched = { - ...basicEvent, - type: NotificationEventType.RemoveNotificationContext, - contextId: 'ctx-123' as ContextID, - account: accountUuid - } - - await middleware.event(session, event, false) - - expect(mockNext.event).toHaveBeenCalled() - }) - }) - - describe('System-only operations', () => { - it('should reject TranslateMessage for non-system account', async () => { - const event: Enriched = { - ...basicEvent, - type: MessageEventType.TranslateMessage, - cardId, - content: '', - messageId: 'msg-123' as MessageID, - language: 'en' - } - - await expect(middleware.event(session, event, false)).rejects.toThrow() - }) - - it('should allow TranslateMessage for system account', async () => { - const systemSession: SessionData = { - ...session, - account: { - uuid: systemAccountUuid, - socialIds: [socialId], - role: AccountRole.User - } - } as any as SessionData - - const event: Enriched = { - ...basicEvent, - type: MessageEventType.TranslateMessage, - cardId, - messageId: 'msg-123' as MessageID, - language: 'en', - content: '' - } - - await middleware.event(systemSession, event, false) - - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should reject CreatePeer for non-system account', async () => { - const event: Enriched = { - ...basicEvent, - type: PeerEventType.CreatePeer, - workspaceId: workspace, - cardId, - value: '123', - kind: 'card' - } - - await expect(middleware.event(session, event, false)).rejects.toThrow() - }) - - it('should allow CreatePeer for system account', async () => { - const systemSession: SessionData = { - ...session, - account: { - uuid: systemAccountUuid, - socialIds: [socialId], - role: AccountRole.User - } - } as any as SessionData - - const event: Enriched = { - ...basicEvent, - type: PeerEventType.CreatePeer, - workspaceId: workspace, - cardId, - value: '123', - kind: 'card' - } - - await middleware.event(systemSession, event, false) - - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should reject RemovePeer for non-system account', async () => { - const event: Enriched = { - ...basicEvent, - type: PeerEventType.RemovePeer, - workspaceId: workspace, - cardId, - kind: 'card', - value: '123' - } - - await expect(middleware.event(session, event, false)).rejects.toThrow() - }) - - it('should allow RemovePeer for system account', async () => { - const systemSession: SessionData = { - ...session, - account: { - uuid: systemAccountUuid, - socialIds: [socialId], - role: AccountRole.User - } - } as any as SessionData - - const event: Enriched = { - ...basicEvent, - workspaceId: workspace, - type: PeerEventType.RemovePeer, - cardId, - kind: 'card', - value: '123' - } - - await middleware.event(systemSession, event, false) - - expect(mockNext.event).toHaveBeenCalled() - }) - }) - - describe('Collaborators events', () => { - it('should allow AddCollaborators with correct socialId', async () => { - const event: Enriched = { - ...basicEvent, - type: NotificationEventType.AddCollaborators, - cardId, - cardType: 'task' as CardType, - collaborators: [accountUuid] - } - - await middleware.event(session, event, false) - - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should reject AddCollaborators with incorrect socialId', async () => { - const event: Enriched = { - ...basicEvent, - type: NotificationEventType.AddCollaborators, - cardId, - cardType: 'task' as CardType, - collaborators: [accountUuid], - socialId: 'other-social' as SocialID - } - - await expect(middleware.event(session, event, false)).rejects.toThrow() - }) - - it('should allow RemoveCollaborators with correct socialId', async () => { - const event: Enriched = { - ...basicEvent, - type: NotificationEventType.RemoveCollaborators, - cardId, - cardType: 'task' as CardType, - collaborators: [accountUuid] - } - - await middleware.event(session, event, false) - - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should reject RemoveCollaborators with incorrect socialId', async () => { - const event: Enriched = { - ...basicEvent, - type: NotificationEventType.RemoveCollaborators, - cardId, - cardType: 'task' as CardType, - collaborators: [accountUuid], - socialId: 'other-social' as SocialID - } - - await expect(middleware.event(session, event, false)).rejects.toThrow() - }) - }) - - describe('Middleware chaining', () => { - it('should call next middleware and return its result', async () => { - const event: Enriched = { - ...basicEvent, - type: MessageEventType.CreateMessage, - cardId, - cardType: 'task' as CardType, - content: 'Test' as Markdown, - messageType: MessageType.Text - } - - const expectedResult = { success: true, eventId: 'event-123' } - mockNext.event.mockResolvedValue(expectedResult) - - const result = await middleware.event(session, event, false) - - expect(result).toEqual(expectedResult) - expect(mockNext.event).toHaveBeenCalledTimes(1) - }) - }) -}) diff --git a/foundations/communication/packages/server/src/__tests__/middleware/storage.test.ts b/foundations/communication/packages/server/src/__tests__/middleware/storage.test.ts deleted file mode 100644 index 9d48a40ab0..0000000000 --- a/foundations/communication/packages/server/src/__tests__/middleware/storage.test.ts +++ /dev/null @@ -1,1156 +0,0 @@ -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. - -import { MeasureContext, WorkspaceUuid } from '@hcengineering/core' -import { MessageEventType, NotificationEventType, SessionData, LabelEventType, CardEventType, PeerEventType } from '@hcengineering/communication-sdk-types' -import { AccountUuid, CardType, Markdown, SocialID } from '@hcengineering/communication-types' -import { StorageMiddleware } from '../../middleware/storage' -import { Enriched, Middleware, MiddlewareContext } from '../../types' -import { LowLevelClient } from '../../client' - -describe('StorageMiddleware', () => { - let mockContext: MiddlewareContext - let mockClient: { - db: Record - blob: Record - getMessageMeta: jest.Mock - findPersonUuid: jest.Mock - removeMessageMeta: jest.Mock - } - let mockMeasureCtx: jest.Mocked - let mockNext: jest.Mocked - let session: SessionData - let middleware: StorageMiddleware - - const workspace = 'test-workspace' as WorkspaceUuid - const accountUuid = 'account-123' as AccountUuid - const socialId = 'social-123' as SocialID - - beforeEach(() => { - jest.clearAllMocks() - - mockMeasureCtx = { - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - newChild: jest.fn().mockReturnThis() - } as any as jest.Mocked - - mockClient = { - db: { - findMessagesMeta: jest.fn().mockResolvedValue([]), - findNotificationContexts: jest.fn().mockResolvedValue([]), - findNotifications: jest.fn().mockResolvedValue([]), - findLabels: jest.fn().mockResolvedValue([]), - findCollaborators: jest.fn().mockResolvedValue([]), - findPeers: jest.fn().mockResolvedValue([]), - findThreadMeta: jest.fn().mockResolvedValue([]), - createMessageMeta: jest.fn().mockResolvedValue(true), - removeMessageMeta: jest.fn().mockResolvedValue(undefined), - addCollaborators: jest.fn().mockResolvedValue([]), - removeCollaborators: jest.fn().mockResolvedValue(undefined), - createNotification: jest.fn().mockResolvedValue('notif-123'), - updateNotification: jest.fn().mockResolvedValue(1), - removeNotifications: jest.fn().mockResolvedValue([]), - createNotificationContext: jest.fn().mockResolvedValue('ctx-123'), - removeContext: jest.fn().mockResolvedValue('ctx-123'), - updateContext: jest.fn().mockResolvedValue(undefined), - createLabel: jest.fn().mockResolvedValue(undefined), - removeLabels: jest.fn().mockResolvedValue(undefined), - createPeer: jest.fn().mockResolvedValue(undefined), - removePeer: jest.fn().mockResolvedValue(undefined), - attachThreadMeta: jest.fn().mockResolvedValue(undefined), - close: jest.fn() - }, - blob: { - findMessagesGroups: jest.fn().mockResolvedValue([]), - getMessageGroupByDate: jest.fn().mockResolvedValue({ blobId: 'blob-123' }), - insertMessage: jest.fn().mockResolvedValue(undefined), - updateMessage: jest.fn().mockResolvedValue(undefined), - removeMessage: jest.fn().mockResolvedValue(undefined), - addReaction: jest.fn().mockResolvedValue(undefined), - removeReaction: jest.fn().mockResolvedValue(undefined), - addAttachments: jest.fn().mockResolvedValue(undefined), - removeAttachments: jest.fn().mockResolvedValue(undefined), - setAttachments: jest.fn().mockResolvedValue(undefined), - updateAttachments: jest.fn().mockResolvedValue(undefined), - attachThread: jest.fn().mockResolvedValue(undefined), - detachThread: jest.fn().mockResolvedValue(undefined), - addBlobs: jest.fn().mockResolvedValue(undefined), - removeBlobs: jest.fn().mockResolvedValue(undefined), - updateBlobs: jest.fn().mockResolvedValue(undefined), - setBlobs: jest.fn().mockResolvedValue(undefined), - updateThread: jest.fn().mockResolvedValue(undefined), - addThreadReply: jest.fn().mockResolvedValue(undefined), - removeThreadReply: jest.fn().mockResolvedValue(undefined) - }, - getMessageMeta: jest.fn().mockResolvedValue({ blobId: 'blob-123', messageId: 'msg-123' }), - findPersonUuid: jest.fn().mockResolvedValue('person-123'), - removeMessageMeta: jest.fn().mockResolvedValue(undefined) - } - - mockNext = { - event: jest.fn().mockResolvedValue({}), - findMessagesMeta: jest.fn().mockResolvedValue([]), - findMessagesGroups: jest.fn().mockResolvedValue([]), - findNotificationContexts: jest.fn().mockResolvedValue([]), - findNotifications: jest.fn().mockResolvedValue([]), - findLabels: jest.fn().mockResolvedValue([]), - findCollaborators: jest.fn().mockResolvedValue([]), - findPeers: jest.fn().mockResolvedValue([]), - subscribeCard: jest.fn(), - unsubscribeCard: jest.fn(), - handleBroadcast: jest.fn(), - closeSession: jest.fn(), - close: jest.fn() - } as any as jest.Mocked - - mockContext = { - ctx: mockMeasureCtx, - client: mockClient as any as LowLevelClient, - workspace, - metadata: { - accountsUrl: 'http://accounts', - hulylakeUrl: 'http://hulylake', - secret: 'secret', - messagesPerBlob: 100 - }, - cadsWithPeers: new Set() - } - - session = { - account: { - uuid: accountUuid, - socialIds: [socialId] - }, - sessionId: 'session-123' - } as any as SessionData - - middleware = new StorageMiddleware(mockContext, mockNext) - }) - - describe('findMessagesMeta', () => { - it('should query database for messages meta', async () => { - const params = { cardId: 'card-123' } - const expectedResult = [{ messageId: 'msg-1', blobId: 'blob-1' }] - mockClient.db.findMessagesMeta.mockResolvedValue(expectedResult as any) - - const result = await middleware.findMessagesMeta(session, params as any) - - expect(mockClient.db.findMessagesMeta).toHaveBeenCalledWith(params) - expect(result).toEqual(expectedResult) - }) - }) - - describe('findMessagesGroups', () => { - it('should query blob storage for messages groups', async () => { - const params = { cardId: 'card-123' } - const expectedResult = [{ blobId: 'blob-1', from: new Date(), to: new Date() }] - mockClient.blob.findMessagesGroups.mockResolvedValue(expectedResult as any) - - const result = await middleware.findMessagesGroups(session, params as any) - - expect(mockClient.blob.findMessagesGroups).toHaveBeenCalledWith(params) - expect(result).toEqual(expectedResult) - }) - - it('should use message meta blobId when id is provided', async () => { - const params = { cardId: 'card-123', id: 'msg-123' } - mockClient.getMessageMeta.mockResolvedValue({ blobId: 'blob-456', messageId: 'msg-123' } as any) - - await middleware.findMessagesGroups(session, params as any) - - expect(mockClient.getMessageMeta).toHaveBeenCalledWith('card-123', 'msg-123') - expect(mockClient.blob.findMessagesGroups).toHaveBeenCalledWith({ - ...params, - blobId: 'blob-456' - }) - }) - - it('should return empty array if message meta not found', async () => { - const params = { cardId: 'card-123', id: 'msg-123' } - mockClient.getMessageMeta.mockResolvedValue(undefined) - - const result = await middleware.findMessagesGroups(session, params as any) - - expect(result).toEqual([]) - }) - }) - - describe('findNotificationContexts', () => { - it('should query database for notification contexts', async () => { - const params = { cardId: 'card-123', account: accountUuid } - const expectedResult = [{ id: 'ctx-1', cardId: 'card-123' }] - mockClient.db.findNotificationContexts.mockResolvedValue(expectedResult as any) - - const result = await middleware.findNotificationContexts(session, params as any) - - expect(mockClient.db.findNotificationContexts).toHaveBeenCalledWith(params - ) - expect(result).toEqual(expectedResult) - }) - }) - - describe('findNotifications', () => { - it('should query database for notifications', async () => { - const params = { contextId: 'ctx-123' } - const expectedResult = [{ id: 'notif-1', contextId: 'ctx-123' }] - mockClient.db.findNotifications.mockResolvedValue(expectedResult as any) - - const result = await middleware.findNotifications(session, params as any) - - expect(mockClient.db.findNotifications).toHaveBeenCalledWith(params) - expect(result).toEqual(expectedResult) - }) - }) - - describe('findLabels', () => { - it('should query database for labels', async () => { - const params = { cardId: 'card-123' } - const expectedResult = [{ labelId: 'label-1', cardId: 'card-123' }] - mockClient.db.findLabels.mockResolvedValue(expectedResult as any) - - const result = await middleware.findLabels(session, params as any) - - expect(mockClient.db.findLabels).toHaveBeenCalledWith(params) - expect(result).toEqual(expectedResult) - }) - }) - - describe('findCollaborators', () => { - it('should query database for collaborators', async () => { - const params = { cardId: 'card-123' } - const expectedResult = [{ account: accountUuid, cardId: 'card-123' }] - mockClient.db.findCollaborators.mockResolvedValue(expectedResult as any) - - const result = await middleware.findCollaborators(session, params as any) - - expect(mockClient.db.findCollaborators).toHaveBeenCalledWith(params) - expect(result).toEqual(expectedResult) - }) - }) - - describe('findPeers', () => { - it('should query database for peers', async () => { - const params = { workspaceId: workspace, cardId: 'card-123' } - const expectedResult = [{ workspaceId: workspace, cardId: 'card-123' }] - mockClient.db.findPeers.mockResolvedValue(expectedResult as any) - - const result = await middleware.findPeers(session, params as any) - - expect(mockClient.db.findPeers).toHaveBeenCalledWith(params) - expect(result).toEqual(expectedResult) - }) - }) - - describe('event - CreateMessage', () => { - it('should create message in storage', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - messageId: 'msg-123', - socialId, - cardType: 'task' as CardType, - messageType: 'text', - content: 'Test message' as Markdown, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockClient.blob.getMessageGroupByDate).toHaveBeenCalledWith('card-123', event.date) - expect(mockClient.db.createMessageMeta).toHaveBeenCalledWith( - 'card-123', - 'msg-123', - socialId, - event.date, - 'blob-123' - ) - expect(mockClient.blob.insertMessage).toHaveBeenCalled() - expect(mockNext.event).toHaveBeenCalledWith(session, event, false) - }) - - it('should skip propagate if message already exists', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - messageId: 'msg-123', - socialId, - date: new Date(), - _eventExtra: {} - } - - mockClient.db.createMessageMeta.mockResolvedValue(false) - - await middleware.event(session, event, false) - - expect(event.skipPropagate).toBe(true) - expect(mockNext.event).not.toHaveBeenCalled() - }) - - it('should throw error if messageId is missing', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - socialId, - date: new Date(), - _eventExtra: {} - } - - await expect(middleware.event(session, event, false)).rejects.toThrow('Message id is required') - }) - - it('should throw error if message group not found', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - messageId: 'msg-123', - socialId, - date: new Date(), - _eventExtra: {} - } - - mockClient.blob.getMessageGroupByDate.mockResolvedValue(null) - - await expect(middleware.event(session, event, false)).rejects.toThrow('Cannot create message') - }) - }) - - describe('event - UpdatePatch', () => { - it('should update message in storage', async () => { - const event: Enriched = { - type: MessageEventType.UpdatePatch, - cardId: 'card-123', - messageId: 'msg-123', - content: 'Updated content' as Markdown, - socialId, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockClient.getMessageMeta).toHaveBeenCalledWith('card-123', 'msg-123') - expect(mockClient.blob.updateMessage).toHaveBeenCalledWith( - 'card-123', - 'blob-123', - 'msg-123', - { - content: event.content, - extra: event.extra, - language: event.language - }, - event.date - ) - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should skip propagate if message not found', async () => { - const event: Enriched = { - type: MessageEventType.UpdatePatch, - cardId: 'card-123', - messageId: 'msg-123', - content: 'Updated' as Markdown, - socialId, - date: new Date(), - _eventExtra: {} - } - - mockClient.getMessageMeta.mockResolvedValue(undefined) - - await middleware.event(session, event, false) - - expect(event.skipPropagate).toBe(true) - expect(mockNext.event).not.toHaveBeenCalled() - }) - }) - - describe('event - RemovePatch', () => { - it('should remove message from storage', async () => { - const event: Enriched = { - type: MessageEventType.RemovePatch, - cardId: 'card-123', - messageId: 'msg-123', - socialId, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockClient.getMessageMeta).toHaveBeenCalledWith('card-123', 'msg-123') - expect(mockClient.blob.removeMessage).toHaveBeenCalledWith('card-123', 'blob-123', 'msg-123') - expect(mockClient.removeMessageMeta).toHaveBeenCalledWith('card-123', 'msg-123') - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should skip propagate if message not found', async () => { - const event: Enriched = { - type: MessageEventType.RemovePatch, - cardId: 'card-123', - messageId: 'msg-123', - socialId, - date: new Date(), - _eventExtra: {} - } - - mockClient.getMessageMeta.mockResolvedValue(undefined) - - await middleware.event(session, event, false) - - expect(event.skipPropagate).toBe(true) - expect(mockNext.event).not.toHaveBeenCalled() - }) - }) - - describe('event - ReactionPatch', () => { - it('should add reaction to message', async () => { - const event: Enriched = { - type: MessageEventType.ReactionPatch, - cardId: 'card-123', - messageId: 'msg-123', - operation: { - opcode: 'add', - reaction: '👍' - }, - personUuid: 'person-123', - socialId, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockClient.blob.addReaction).toHaveBeenCalledWith( - 'card-123', - 'blob-123', - 'msg-123', - '👍', - 'person-123', - event.date - ) - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should remove reaction from message', async () => { - const event: Enriched = { - type: MessageEventType.ReactionPatch, - cardId: 'card-123', - messageId: 'msg-123', - operation: { - opcode: 'remove', - reaction: '👍' - }, - personUuid: 'person-123', - socialId, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockClient.blob.removeReaction).toHaveBeenCalledWith('card-123', 'blob-123', 'msg-123', '👍', 'person-123') - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should skip propagate if personUuid is missing', async () => { - const event: Enriched = { - type: MessageEventType.ReactionPatch, - cardId: 'card-123', - messageId: 'msg-123', - operation: { - opcode: 'add', - reaction: '👍' - }, - socialId, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(event.skipPropagate).toBe(true) - expect(mockNext.event).not.toHaveBeenCalled() - }) - }) - - describe('event - AttachmentPatch', () => { - it('should add attachments to message', async () => { - const event: Enriched = { - type: MessageEventType.AttachmentPatch, - cardId: 'card-123', - messageId: 'msg-123', - operations: [ - { - opcode: 'add', - attachments: [ - { - id: 'att-1', - mimeType: 'image/png', - params: { fileName: 'test.png' } - } - ] - } - ], - socialId, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockClient.blob.addAttachments).toHaveBeenCalled() - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should remove attachments from message', async () => { - const event: Enriched = { - type: MessageEventType.AttachmentPatch, - cardId: 'card-123', - messageId: 'msg-123', - operations: [ - { - opcode: 'remove', - ids: ['att-1', 'att-2'] - } - ], - socialId, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockClient.blob.removeAttachments).toHaveBeenCalledWith('card-123', 'blob-123', 'msg-123', ['att-1', 'att-2']) - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should set attachments on message', async () => { - const event: Enriched = { - type: MessageEventType.AttachmentPatch, - cardId: 'card-123', - messageId: 'msg-123', - operations: [ - { - opcode: 'set', - attachments: [ - { - id: 'att-1', - mimeType: 'image/png', - params: { fileName: 'test.png' } - } - ] - } - ], - socialId, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockClient.blob.setAttachments).toHaveBeenCalled() - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should update attachments on message', async () => { - const event: Enriched = { - type: MessageEventType.AttachmentPatch, - cardId: 'card-123', - messageId: 'msg-123', - operations: [ - { - opcode: 'update', - attachments: [ - { - id: 'att-1', - params: { fileName: 'updated.png' } - } - ] - } - ], - socialId, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockClient.blob.updateAttachments).toHaveBeenCalled() - expect(mockNext.event).toHaveBeenCalled() - }) - }) - - describe('event - BlobPatch (deprecated)', () => { - it('should convert blob attach to attachment add', async () => { - const event: Enriched = { - type: MessageEventType.BlobPatch, - cardId: 'card-123', - messageId: 'msg-123', - operations: [ - { - opcode: 'attach', - blobs: [ - { - blobId: 'blob-1', - mimeType: 'image/png', - fileName: 'test.png', - size: 1024 - } - ] - } - ], - socialId, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockClient.blob.addAttachments).toHaveBeenCalled() - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should convert blob detach to attachment remove', async () => { - const event: Enriched = { - type: MessageEventType.BlobPatch, - cardId: 'card-123', - messageId: 'msg-123', - operations: [ - { - opcode: 'detach', - blobIds: ['blob-1', 'blob-2'] - } - ], - socialId, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockClient.blob.removeAttachments).toHaveBeenCalled() - expect(mockNext.event).toHaveBeenCalled() - }) - }) - - describe('event - ThreadPatch', () => { - it('should attach thread to message', async () => { - const event: Enriched = { - type: MessageEventType.ThreadPatch, - cardId: 'card-123', - messageId: 'msg-123', - operation: { - opcode: 'attach', - threadId: 'thread-123', - threadType: 'task' as CardType - }, - socialId, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockClient.db.attachThreadMeta).toHaveBeenCalledWith( - 'card-123', - 'msg-123', - 'thread-123', - 'task', - socialId, - event.date - ) - expect(mockClient.blob.attachThread).toHaveBeenCalled() - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should update thread on message', async () => { - const event: Enriched = { - type: MessageEventType.ThreadPatch, - cardId: 'card-123', - messageId: 'msg-123', - operation: { - opcode: 'update', - threadId: 'thread-123', - update: { repliesCount: 5 } - }, - socialId, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockClient.blob.updateThread).toHaveBeenCalledWith( - 'card-123', - 'blob-123', - 'msg-123', - 'thread-123', - { repliesCount: 5 } - ) - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should add thread reply', async () => { - const event: Enriched = { - type: MessageEventType.ThreadPatch, - cardId: 'card-123', - messageId: 'msg-123', - operation: { - opcode: 'addReply', - threadId: 'thread-123' - }, - socialId, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockClient.findPersonUuid).toHaveBeenCalled() - expect(mockClient.blob.addThreadReply).toHaveBeenCalledWith( - 'card-123', - 'blob-123', - 'msg-123', - 'thread-123', - 'person-123', - event.date - ) - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should remove thread reply', async () => { - const event: Enriched = { - type: MessageEventType.ThreadPatch, - cardId: 'card-123', - messageId: 'msg-123', - operation: { - opcode: 'removeReply', - threadId: 'thread-123' - }, - socialId, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockClient.findPersonUuid).toHaveBeenCalled() - expect(mockClient.blob.removeThreadReply).toHaveBeenCalledWith( - 'card-123', - 'blob-123', - 'msg-123', - 'thread-123', - 'person-123' - ) - expect(mockNext.event).toHaveBeenCalled() - }) - }) - - describe('event - Notification events', () => { - it('should create notification', async () => { - const event: Enriched = { - type: NotificationEventType.CreateNotification, - contextId: 'ctx-123', - messageId: 'msg-123', - blobId: 'blob-123', - notificationType: 'message', - read: false, - content: 'Test notification', - creator: socialId, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockClient.db.createNotification).toHaveBeenCalledWith( - 'ctx-123', - 'msg-123', - 'blob-123', - 'message', - false, - 'Test notification', - socialId, - event.date - ) - expect(event.notificationId).toBe('notif-123') - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should update notification', async () => { - const event: Enriched = { - type: NotificationEventType.UpdateNotification, - contextId: 'ctx-123', - account: accountUuid, - query: { type: 'message' }, - updates: { read: true }, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockClient.db.updateNotification).toHaveBeenCalledWith( - { - contextId: 'ctx-123', - account: accountUuid, - type: 'message' - }, - { read: true } - ) - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should skip propagate if no notifications updated', async () => { - const event: Enriched = { - type: NotificationEventType.UpdateNotification, - contextId: 'ctx-123', - account: accountUuid, - query: {}, - updates: { read: true }, - date: new Date(), - _eventExtra: {} - } - - mockClient.db.updateNotification.mockResolvedValue(0) - - await middleware.event(session, event, false) - - expect(event.skipPropagate).toBe(true) - expect(mockNext.event).not.toHaveBeenCalled() - }) - - it('should remove notifications', async () => { - const event: Enriched = { - type: NotificationEventType.RemoveNotifications, - contextId: 'ctx-123', - account: accountUuid, - ids: ['notif-1', 'notif-2'], - date: new Date(), - _eventExtra: {} - } - - mockClient.db.removeNotifications.mockResolvedValue(['notif-1', 'notif-2']) - - await middleware.event(session, event, false) - - expect(mockClient.db.removeNotifications).toHaveBeenCalledWith({ - contextId: 'ctx-123', - account: accountUuid, - id: ['notif-1', 'notif-2'] - }) - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should skip propagate if no ids to remove', async () => { - const event: Enriched = { - type: NotificationEventType.RemoveNotifications, - contextId: 'ctx-123', - account: accountUuid, - ids: [], - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(event.skipPropagate).toBe(true) - expect(mockNext.event).not.toHaveBeenCalled() - }) - - it('should create notification context', async () => { - const event: Enriched = { - type: NotificationEventType.CreateNotificationContext, - account: accountUuid, - cardId: 'card-123', - lastUpdate: new Date(), - lastView: new Date(), - lastNotify: new Date(), - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockClient.db.createNotificationContext).toHaveBeenCalled() - expect(event.contextId).toBe('ctx-123') - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should remove notification context', async () => { - const event: Enriched = { - type: NotificationEventType.RemoveNotificationContext, - contextId: 'ctx-123', - account: accountUuid, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockClient.db.removeContext).toHaveBeenCalledWith({ - id: 'ctx-123', - account: accountUuid - }) - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should skip propagate if context not found', async () => { - const event: Enriched = { - type: NotificationEventType.RemoveNotificationContext, - contextId: 'ctx-123', - account: accountUuid, - date: new Date(), - _eventExtra: {} - } - - mockClient.db.removeContext.mockResolvedValue(undefined) - - await middleware.event(session, event, false) - - expect(event.skipPropagate).toBe(true) - expect(mockNext.event).not.toHaveBeenCalled() - }) - - it('should update notification context', async () => { - const event: Enriched = { - type: NotificationEventType.UpdateNotificationContext, - contextId: 'ctx-123', - account: accountUuid, - updates: { lastView: new Date() }, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockClient.db.updateContext).toHaveBeenCalled() - expect(mockNext.event).toHaveBeenCalled() - }) - }) - - describe('event - Collaborator events', () => { - it('should add collaborators', async () => { - const event: Enriched = { - type: NotificationEventType.AddCollaborators, - cardId: 'card-123', - cardType: 'task' as CardType, - collaborators: [accountUuid, 'account-456' as AccountUuid], - socialId, - date: new Date(), - _eventExtra: {} - } - - mockClient.db.addCollaborators.mockResolvedValue([accountUuid, 'account-456' as AccountUuid]) - - await middleware.event(session, event, false) - - expect(mockClient.db.addCollaborators).toHaveBeenCalledWith( - 'card-123', - 'task', - [accountUuid, 'account-456'], - event.date - ) - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should skip propagate if no collaborators added', async () => { - const event: Enriched = { - type: NotificationEventType.AddCollaborators, - cardId: 'card-123', - cardType: 'task' as CardType, - collaborators: [accountUuid], - socialId, - date: new Date(), - _eventExtra: {} - } - - mockClient.db.addCollaborators.mockResolvedValue([]) - - await middleware.event(session, event, false) - - expect(event.skipPropagate).toBe(true) - expect(mockNext.event).not.toHaveBeenCalled() - }) - - it('should remove collaborators', async () => { - const event: Enriched = { - type: NotificationEventType.RemoveCollaborators, - cardId: 'card-123', - cardType: 'task' as CardType, - collaborators: [accountUuid], - socialId, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockClient.db.removeCollaborators).toHaveBeenCalledWith({ - cardId: 'card-123', - account: [accountUuid] - }) - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should skip propagate if no collaborators to remove', async () => { - const event: Enriched = { - type: NotificationEventType.RemoveCollaborators, - cardId: 'card-123', - cardType: 'task' as CardType, - collaborators: [], - socialId, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(event.skipPropagate).toBe(true) - expect(mockNext.event).not.toHaveBeenCalled() - }) - }) - - describe('event - Label events', () => { - it('should create label', async () => { - const event: Enriched = { - type: LabelEventType.CreateLabel, - cardId: 'card-123', - cardType: 'task' as CardType, - labelId: 'label-123', - account: accountUuid, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockClient.db.createLabel).toHaveBeenCalledWith( - 'card-123', - 'task', - 'label-123', - accountUuid, - event.date - ) - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should remove label', async () => { - const event: Enriched = { - type: LabelEventType.RemoveLabel, - labelId: 'label-123', - cardId: 'card-123', - account: accountUuid, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockClient.db.removeLabels).toHaveBeenCalledWith({ - labelId: 'label-123', - cardId: 'card-123', - account: accountUuid - }) - expect(mockNext.event).toHaveBeenCalled() - }) - }) - - describe('event - Peer events', () => { - it('should create peer', async () => { - const event: Enriched = { - type: PeerEventType.CreatePeer, - workspaceId: workspace, - cardId: 'card-123', - kind: 'slack', - value: 'channel-123', - extra: { foo: 'bar' }, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockClient.db.createPeer).toHaveBeenCalledWith( - workspace, - 'card-123', - 'slack', - 'channel-123', - { foo: 'bar' }, - event.date - ) - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should remove peer', async () => { - const event: Enriched = { - type: PeerEventType.RemovePeer, - workspaceId: workspace, - cardId: 'card-123', - kind: 'slack', - value: 'channel-123', - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockClient.db.removePeer).toHaveBeenCalledWith( - workspace, - 'card-123', - 'slack', - 'channel-123' - ) - expect(mockNext.event).toHaveBeenCalled() - }) - }) - - describe('event - Card events', () => { - it('should handle UpdateCardType event', async () => { - const event: Enriched = { - type: CardEventType.UpdateCardType, - cardId: 'card-123', - cardType: 'issue' as CardType, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should handle RemoveCard event', async () => { - const event: Enriched = { - type: CardEventType.RemoveCard, - cardId: 'card-123', - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockNext.event).toHaveBeenCalled() - }) - }) - - describe('close', () => { - it('should close database connection', () => { - middleware.close() - - expect(mockClient.db.close).toHaveBeenCalled() - }) - }) -}) diff --git a/foundations/communication/packages/server/src/__tests__/middleware/triggers.test.ts b/foundations/communication/packages/server/src/__tests__/middleware/triggers.test.ts deleted file mode 100644 index a16efe8a72..0000000000 --- a/foundations/communication/packages/server/src/__tests__/middleware/triggers.test.ts +++ /dev/null @@ -1,891 +0,0 @@ -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. - -import { type MeasureContext, type WorkspaceUuid } from '@hcengineering/core' -import { - MessageEventType, - type SessionData, - NotificationEventType -} from '@hcengineering/communication-sdk-types' -import { type AccountUuid, type CardType, type Markdown, type SocialID } from '@hcengineering/communication-types' - -import { TriggersMiddleware } from '../../middleware/triggers' -import { type Enriched, type MiddlewareContext, type CommunicationCallbacks, Middleware } from '../../types' -import { type LowLevelClient } from '../../client' -import { notify } from '../../notification/notification' - -// Mock external dependencies -jest.mock('../../triggers/all', () => []) -jest.mock('../../notification/notification', () => ({ - notify: jest.fn().mockResolvedValue([]) -})) - -describe('TriggersMiddleware', () => { - // Test fixtures - let mockContext: MiddlewareContext - let mockClient: jest.Mocked - let mockMeasureCtx: jest.Mocked - let mockNext: any - let mockCallbacks: jest.Mocked - let session: SessionData - let middleware: TriggersMiddleware - - // Test constants - const workspace = 'test-workspace' as WorkspaceUuid - const accountUuid = 'account-123' as AccountUuid - const socialId = 'social-123' as SocialID - - beforeEach(() => { - jest.clearAllMocks() - - // Setup mock context - mockMeasureCtx = { - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - newChild: jest.fn().mockReturnThis(), - contextData: undefined - } as any as jest.Mocked - - // Setup mock client - mockClient = { - db: { - findNotificationContexts: jest.fn().mockResolvedValue([]), - findCollaborators: jest.fn().mockResolvedValue([]), - findThreadMeta: jest.fn().mockResolvedValue([]), - getAccountsByPersonIds: jest.fn().mockResolvedValue([]) - }, - blob: {}, - findPersonUuid: jest.fn().mockResolvedValue('person-123') - } as unknown as jest.Mocked - - // Setup mock middleware chain - mockNext = { - event: jest.fn().mockResolvedValue({}), - handleBroadcast: jest.fn(), - findNotificationContexts: jest.fn().mockResolvedValue([]), - findNotifications: jest.fn().mockResolvedValue([]), - findLabels: jest.fn().mockResolvedValue([]), - findCollaborators: jest.fn().mockResolvedValue([]), - findPeers: jest.fn().mockResolvedValue([]), - findMessagesMeta: jest.fn().mockResolvedValue([]), - findMessagesGroups: jest.fn().mockResolvedValue([]), - subscribeCard: jest.fn(), - unsubscribeCard: jest.fn(), - closeSession: jest.fn(), - close: jest.fn() - } as any as jest.Mocked - - // Setup mock callbacks - mockCallbacks = { - registerAsyncRequest: jest.fn((ctx, fn) => { - void fn(ctx).catch(() => {}) - return undefined - }), - broadcast: jest.fn(), - enqueue: jest.fn() - } as any as jest.Mocked - - // Setup middleware context - mockContext = { - ctx: mockMeasureCtx, - client: mockClient, - workspace, - metadata: { - accountsUrl: 'http://accounts', - hulylakeUrl: 'http://hulylake', - secret: 'secret', - messagesPerBlob: 100 - }, - cadsWithPeers: new Set(), - head: mockNext - } - - // Setup test session - session = { - account: { - uuid: accountUuid, - socialIds: [socialId] - }, - sessionId: 'session-123', - asyncData: [] - } as any as SessionData - - middleware = new TriggersMiddleware(mockCallbacks, mockContext, mockNext) - }) - - describe('event', () => { - it('should process event and call next middleware', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - messageId: 'msg-123', - cardType: 'task' as CardType, - messageType: 'text', - content: 'Test message' as Markdown, - socialId, - date: new Date(), - _eventExtra: {} - } - - const result = await middleware.event(session, event, false) - - expect(mockNext.event).toHaveBeenCalledWith(session, event, false) - expect(result).toBeDefined() - }) - - it('should skip propagation if event has skipPropagate flag', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - skipPropagate: true, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should handle derived events', async () => { - const event: Enriched = { - type: MessageEventType.UpdatePatch, - cardId: 'card-123', - messageId: 'msg-123', - content: 'Updated' as Markdown, - socialId, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, true) - - expect(mockNext.event).toHaveBeenCalledWith(session, event, true) - }) - - it('should register async request for non-derived events', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - messageId: 'msg-123', - cardType: 'task' as CardType, - messageType: 'text', - content: 'Test' as Markdown, - socialId, - date: new Date(), - _eventExtra: {} - } - - session.contextData = { foo: 'bar' } - - await middleware.event(session, event, false) - - expect(mockCallbacks.registerAsyncRequest).toHaveBeenCalled() - expect(session.isAsyncContext).toBe(true) - }) - - it('should not register duplicate async requests', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - messageId: 'msg-123', - cardType: 'task' as CardType, - messageType: 'text', - content: 'Test' as Markdown, - socialId, - date: new Date(), - _eventExtra: {} - } - - session.contextData = { foo: 'bar' } - session.isAsyncContext = true - - await middleware.event(session, event, false) - - expect(mockCallbacks.registerAsyncRequest).not.toHaveBeenCalled() - }) - - it('should handle events without context data', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - messageId: 'msg-123', - cardType: 'task' as CardType, - messageType: 'text', - content: 'Test' as Markdown, - socialId, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockCallbacks.registerAsyncRequest).not.toHaveBeenCalled() - }) - - it('should sort async data by date', async () => { - const date1 = new Date('2025-01-01T10:00:00Z') - const date2 = new Date('2025-01-01T09:00:00Z') - const date3 = new Date('2025-01-01T11:00:00Z') - - const event1: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - messageId: 'msg-1', - date: date1, - _eventExtra: {} - } - - const event2: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - messageId: 'msg-2', - date: date2, - _eventExtra: {} - } - - const event3: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - messageId: 'msg-3', - date: date3, - _eventExtra: {} - } - - session.asyncData = [event1, event2, event3] - - await middleware.event(session, event1, false) - - expect(session.asyncData).toBeDefined() - }) - }) - - describe('processDerived', () => { - it('should process derived events', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - messageId: 'msg-123', - cardType: 'task' as CardType, - messageType: 'text', - content: 'Test' as Markdown, - socialId, - date: new Date(), - _eventExtra: {} - } - - await middleware.processDerived(session, [event], true) - - expect(mockCallbacks.registerAsyncRequest).not.toHaveBeenCalled() - }) - - it('should register async request for non-derived events with context data', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - messageId: 'msg-123', - date: new Date(), - _eventExtra: {} - } - - session.contextData = { foo: 'bar' } - - await middleware.processDerived(session, [event], false) - - expect(mockCallbacks.registerAsyncRequest).toHaveBeenCalled() - expect(session.isAsyncContext).toBe(true) - }) - - it('should handle multiple events', async () => { - const event1: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - messageId: 'msg-1', - date: new Date(), - _eventExtra: {} - } - - const event2: Enriched = { - type: MessageEventType.UpdatePatch, - cardId: 'card-123', - messageId: 'msg-2', - content: 'Updated' as Markdown, - socialId, - date: new Date(), - _eventExtra: {} - } - - await middleware.processDerived(session, [event1, event2], false) - - expect(session.asyncData).toBeDefined() - }) - - it('should filter out events with skipPropagate in asyncData', async () => { - const event1: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123' as any, - messageId: 'msg-1', - date: new Date(), - skipPropagate: false, - _eventExtra: {} - } - - const event2: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-456' as any, - messageId: 'msg-2', - date: new Date(), - skipPropagate: true, - _eventExtra: {} - } - - await middleware.processDerived(session, [event1, event2], false) - - expect(session.asyncData).toBeDefined() - }) - }) - - describe('notification events', () => { - it('should handle notification context events', async () => { - const event: Enriched = { - type: NotificationEventType.CreateNotificationContext, - account: accountUuid, - cardId: 'card-123', - lastUpdate: new Date(), - lastView: new Date(), - lastNotify: new Date(), - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should handle update notification events', async () => { - const event: Enriched = { - type: NotificationEventType.UpdateNotification, - contextId: 'ctx-123', - account: accountUuid, - query: { type: 'message' }, - updates: { read: true }, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockNext.event).toHaveBeenCalled() - }) - }) - - describe('async context handling', () => { - it('should handle async context correctly', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - messageId: 'msg-123', - cardType: 'task' as CardType, - messageType: 'text', - content: 'Test' as Markdown, - socialId, - date: new Date(), - _eventExtra: {} - } - - session.isAsyncContext = true - - await middleware.event(session, event, false) - - expect(mockCallbacks.registerAsyncRequest).not.toHaveBeenCalled() - }) - - it('should clear asyncData after processing non-async events', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - messageId: 'msg-123', - date: new Date(), - _eventExtra: {} - } - - session.asyncData = [ - { - type: MessageEventType.CreateMessage, - cardId: 'card-456' as any, - messageId: 'msg-456' as any, - cardType: 'task' as CardType, - messageType: 'text' as any, - content: 'Test' as Markdown, - socialId, - date: new Date(), - _eventExtra: {} - } - ] - - await middleware.event(session, event, false) - - expect(session.asyncData).toEqual([]) - }) - - it('should preserve asyncData in async context', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - messageId: 'msg-123', - date: new Date(), - _eventExtra: {} - } - - session.isAsyncContext = true - session.asyncData = [ - { - type: MessageEventType.CreateMessage, - cardId: 'card-456' as any, - messageId: 'msg-456' as any, - cardType: 'task' as CardType, - messageType: 'text' as any, - content: 'Test' as Markdown, - socialId, - date: new Date(), - _eventExtra: {} - } - ] - - const initialAsyncDataLength = session.asyncData.length - - await middleware.event(session, event, false) - - expect(session.asyncData.length).toBeGreaterThanOrEqual(initialAsyncDataLength) - }) - }) - - describe('error handling', () => { - it('should handle errors in trigger execution', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - messageId: 'msg-123', - date: new Date(), - _eventExtra: {} - } - - mockNext.event.mockRejectedValue(new Error('Trigger error')) - - await expect(middleware.event(session, event, false)).rejects.toThrow('Trigger error') - }) - - it('should handle errors in async context', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - messageId: 'msg-123', - date: new Date(), - _eventExtra: {} - } - - session.contextData = { foo: 'bar' } - - let asyncCallbackPromise: Promise | undefined - mockCallbacks.registerAsyncRequest.mockImplementation((ctx, fn) => { - asyncCallbackPromise = fn(ctx) - return undefined - }) - - const notifyMock = notify as jest.MockedFunction - notifyMock.mockRejectedValueOnce(new Error('Async error')) - - await middleware.event(session, event, false) - - expect(mockCallbacks.registerAsyncRequest).toHaveBeenCalled() - - if (asyncCallbackPromise !== undefined) { - await expect(asyncCallbackPromise).rejects.toThrow('Async error') - } - }) - }) - - describe('edge cases', () => { - it('should handle empty asyncData array', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - messageId: 'msg-123', - date: new Date(), - _eventExtra: {} - } - - session.asyncData = [] - - await middleware.event(session, event, false) - - expect(session.asyncData).toEqual([]) - }) - - it('should handle undefined asyncData', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - messageId: 'msg-123', - date: new Date(), - _eventExtra: {} - } - - session.asyncData = undefined as any - - await middleware.event(session, event, false) - - expect(session.asyncData).toBeDefined() - }) - - it('should handle events without head middleware', async () => { - const contextWithoutHead = { ...mockContext, head: undefined } - const middlewareWithoutHead = new TriggersMiddleware(mockCallbacks, contextWithoutHead, mockNext) - - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - messageId: 'msg-123', - date: new Date(), - _eventExtra: {} - } - - await middlewareWithoutHead.event(session, event, false) - - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should handle events with null values', async () => { - const event: Enriched = { - type: MessageEventType.UpdatePatch, - cardId: 'card-123', - messageId: 'msg-123', - content: null as any, - socialId, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should process events with skipPropagate', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - messageId: 'msg-123', - skipPropagate: true, - date: new Date(), - _eventExtra: {} - } - - const result = await middleware.event(session, event, false) - - expect(result).toBeDefined() - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should handle very large asyncData arrays', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - messageId: 'msg-123', - date: new Date(), - _eventExtra: {} - } - - session.asyncData = Array.from({ length: 1000 }, (_, i) => ({ - type: MessageEventType.CreateMessage, - cardId: `card-${i}`, - messageId: `msg-${i}`, - cardType: 'task' as CardType, - messageType: 'text', - content: 'Test' as Markdown, - socialId, - date: new Date(), - _eventExtra: {} - })) as any - - await middleware.event(session, event, false) - - expect(session.asyncData).toBeDefined() - }) - - it('should handle events with different date formats', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - messageId: 'msg-123', - date: new Date('2025-01-01T00:00:00.000Z'), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should handle concurrent event processing', async () => { - const event1: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-1', - messageId: 'msg-1', - date: new Date(), - _eventExtra: {} - } - - const event2: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-2', - messageId: 'msg-2', - date: new Date(), - _eventExtra: {} - } - - await Promise.all([ - middleware.event(session, event1, false), - middleware.event(session, event2, false) - ]) - - expect(mockNext.event).toHaveBeenCalledTimes(2) - }) - - it('should preserve event order in asyncData', async () => { - const dates = [ - new Date('2025-01-01T10:00:00Z'), - new Date('2025-01-01T09:00:00Z'), - new Date('2025-01-01T11:00:00Z') - ] - - const events = dates.map((date, i) => ({ - type: MessageEventType.CreateMessage, - cardId: 'card-123', - messageId: `msg-${i}`, - date, - _eventExtra: {} - })) - - for (const event of events) { - await middleware.event(session, event as Enriched, false) - } - - expect(session.asyncData).toBeDefined() - }) - }) - - describe('trigger integration', () => { - it('should execute triggers for message events', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - messageId: 'msg-123', - cardType: 'task' as CardType, - messageType: 'text', - content: 'Test with @mention' as Markdown, - socialId, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should execute triggers for notification events', async () => { - const event: Enriched = { - type: NotificationEventType.CreateNotification, - contextId: 'ctx-123', - messageId: 'msg-123', - blobId: 'blob-123', - notificationType: 'message', - content: 'Test notification', - creator: socialId, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should handle multiple trigger results', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - messageId: 'msg-123', - cardType: 'task' as CardType, - messageType: 'text', - content: 'Test message' as Markdown, - socialId, - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(mockNext.event).toHaveBeenCalled() - }) - }) - - describe('async request management', () => { - it('should register only one async request per session', async () => { - const event1: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-1', - messageId: 'msg-1', - date: new Date(), - _eventExtra: {} - } - - const event2: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-2', - messageId: 'msg-2', - date: new Date(), - _eventExtra: {} - } - - session.contextData = { foo: 'bar' } - - await middleware.event(session, event1, false) - await middleware.event(session, event2, false) - - expect(mockCallbacks.registerAsyncRequest).toHaveBeenCalledTimes(1) - }) - - it('should handle async request with contextData', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - messageId: 'msg-123', - date: new Date(), - _eventExtra: {} - } - - session.contextData = { userId: 'user-123', requestId: 'req-456' } - - await middleware.event(session, event, false) - - expect(mockCallbacks.registerAsyncRequest).toHaveBeenCalled() - expect(session.isAsyncContext).toBe(true) - }) - - it('should execute async callback immediately in tests', async () => { - let callbackExecuted = false - mockCallbacks.registerAsyncRequest.mockImplementation((ctx, fn) => { - callbackExecuted = true - void fn(ctx) - return undefined - }) - - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - messageId: 'msg-123', - date: new Date(), - _eventExtra: {} - } - - session.contextData = { foo: 'bar' } - - await middleware.event(session, event, false) - - expect(callbackExecuted).toBe(true) - }) - }) - - describe('broadcast handling', () => { - it('should call handleBroadcast after event processing', async () => { - const handleBroadcastSpy = jest.spyOn(middleware, 'handleBroadcast') - - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - messageId: 'msg-123', - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(handleBroadcastSpy).toHaveBeenCalled() - }) - - it('should handle broadcast with multiple events', async () => { - const events: Enriched[] = [ - { - type: MessageEventType.CreateMessage, - cardId: 'card-1', - messageId: 'msg-1', - date: new Date(), - _eventExtra: {} - }, - { - type: MessageEventType.CreateMessage, - cardId: 'card-2', - messageId: 'msg-2', - date: new Date(), - _eventExtra: {} - } - ] - - middleware.handleBroadcast(session, events) - - expect(mockNext.handleBroadcast).toHaveBeenCalledWith(session, events) - }) - - it('should handle empty broadcast', async () => { - middleware.handleBroadcast(session, []) - - expect(mockNext.handleBroadcast).toHaveBeenCalledWith(session, []) - }) - }) - - describe('performance and memory', () => { - it('should handle rapid succession of events', async () => { - const events = Array.from({ length: 100 }, (_, i) => ({ - type: MessageEventType.CreateMessage, - cardId: 'card-123', - messageId: `msg-${i}`, - date: new Date(), - _eventExtra: {} - })) - - for (const event of events) { - await middleware.event(session, event as Enriched, false) - } - - expect(mockNext.event).toHaveBeenCalledTimes(100) - }) - - it('should clean up processed peers events', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - messageId: 'msg-123', - date: new Date(), - _eventExtra: {} - } - - await middleware.event(session, event, false) - - expect(session.asyncData).toEqual([]) - }) - }) -}) diff --git a/foundations/communication/packages/server/src/__tests__/middleware/validate.test.ts b/foundations/communication/packages/server/src/__tests__/middleware/validate.test.ts deleted file mode 100644 index f5b03fc829..0000000000 --- a/foundations/communication/packages/server/src/__tests__/middleware/validate.test.ts +++ /dev/null @@ -1,1028 +0,0 @@ -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. - -import { MeasureContext, WorkspaceUuid } from '@hcengineering/core' -import { MessageEventType, NotificationEventType, SessionData } from '@hcengineering/communication-sdk-types' -import { AccountUuid, CardID, CardType, Markdown, SocialID } from '@hcengineering/communication-types' - -import { ValidateMiddleware } from '../../middleware/validate' -import { Enriched, Middleware, MiddlewareContext } from '../../types' -import { LowLevelClient } from '../../client' - -describe('ValidateMiddleware', () => { - let mockContext: MiddlewareContext - let mockClient: jest.Mocked - let mockMeasureCtx: jest.Mocked - let mockNext: any - let session: SessionData - let middleware: ValidateMiddleware - - const workspace = 'test-workspace' as WorkspaceUuid - const accountUuid = 'account-123' as AccountUuid - const socialId = 'social-123' as SocialID - - beforeEach(() => { - jest.clearAllMocks() - - mockMeasureCtx = { - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - newChild: jest.fn().mockReturnThis() - } as any as jest.Mocked - - mockClient = { - db: {}, - blob: {} - } as unknown as jest.Mocked - - mockNext = { - event: jest.fn().mockResolvedValue({}), - findNotificationContexts: jest.fn().mockResolvedValue([]), - findNotifications: jest.fn().mockResolvedValue([]), - findLabels: jest.fn().mockResolvedValue([]), - findCollaborators: jest.fn().mockResolvedValue([]), - findMessagesGroups: jest.fn().mockResolvedValue([]), - findMessagesMeta: jest.fn().mockResolvedValue([]), - findPeers: jest.fn().mockResolvedValue([]), - subscribeCard: jest.fn(), - unsubscribeCard: jest.fn(), - handleBroadcast: jest.fn(), - closeSession: jest.fn(), - close: jest.fn() - } as any as jest.Mocked - - mockContext = { - ctx: mockMeasureCtx, - client: mockClient, - workspace, - metadata: { - accountsUrl: 'http://accounts', - hulylakeUrl: 'http://hulylake', - secret: 'secret', - messagesPerBlob: 100 - }, - cadsWithPeers: new Set() - } - - session = { - account: { - uuid: accountUuid, - socialIds: [socialId] - }, - sessionId: 'session-123' - } as any as SessionData - - middleware = new ValidateMiddleware(mockContext, mockNext) - }) - - describe('event - CreateMessage validation', () => { - it('should validate correct CreateMessage event', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123' as CardID, - cardType: 'task' as CardType, - messageType: 'text', - content: 'Test message' as Markdown, - socialId, - date: new Date() - } - - await middleware.event(session, event, false) - - expect(mockNext.event).toHaveBeenCalledWith(session, event, false) - }) - - it('should reject CreateMessage without cardId', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardType: 'task' as CardType, - messageType: 'text', - content: 'Test' as Markdown, - socialId, - date: new Date() - } - - await expect(middleware.event(session, event, false)).rejects.toThrow(Error) - }) - - it('should reject CreateMessage without content', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123' as CardID, - cardType: 'task' as CardType, - messageType: 'text', - socialId, - date: new Date() - } - - await expect(middleware.event(session, event, false)).rejects.toThrow(Error) - }) - - it('should reject CreateMessage without socialId', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123' as CardID, - cardType: 'task' as CardType, - messageType: 'text', - content: 'Test' as Markdown, - date: new Date() - } - - await expect(middleware.event(session, event, false)).rejects.toThrow(Error) - }) - }) - - describe('event - UpdatePatch validation', () => { - it('should validate correct UpdatePatch event', async () => { - const event: Enriched = { - type: MessageEventType.UpdatePatch, - cardId: 'card-123' as CardID, - messageId: 'msg-123', - content: 'Updated' as Markdown, - socialId, - date: new Date() - } - - await middleware.event(session, event, false) - - expect(mockNext.event).toHaveBeenCalledWith(session, event, false) - }) - - it('should validate UpdatePatch without messageId', async () => { - const event: Enriched = { - type: MessageEventType.UpdatePatch, - cardId: 'card-123' as CardID, - content: 'Updated' as Markdown, - socialId, - date: new Date() - } - - // UpdatePatch can have optional messageId according to schema - await middleware.event(session, event, false) - expect(mockNext.event).toHaveBeenCalled() - }) - }) - - describe('event - RemovePatch validation', () => { - it('should validate correct RemovePatch event', async () => { - const event: Enriched = { - type: MessageEventType.RemovePatch, - cardId: 'card-123' as CardID, - messageId: 'msg-123', - socialId, - date: new Date() - } - - await middleware.event(session, event, false) - - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should reject RemovePatch without cardId', async () => { - const event: Enriched = { - type: MessageEventType.RemovePatch, - messageId: 'msg-123', - socialId, - date: new Date() - } - - await expect(middleware.event(session, event, false)).rejects.toThrow(Error) - }) - }) - - describe('event - ReactionPatch validation', () => { - it('should validate correct ReactionPatch add event', async () => { - const event: Enriched = { - type: MessageEventType.ReactionPatch, - cardId: 'card-123' as CardID, - messageId: 'msg-123', - operation: { - opcode: 'add', - reaction: '👍' - }, - personUuid: 'person-123', - socialId, - date: new Date() - } - - await middleware.event(session, event, false) - - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should validate correct ReactionPatch remove event', async () => { - const event: Enriched = { - type: MessageEventType.ReactionPatch, - cardId: 'card-123' as CardID, - messageId: 'msg-123', - operation: { - opcode: 'remove', - reaction: '👍' - }, - personUuid: 'person-123', - socialId, - date: new Date() - } - - await middleware.event(session, event, false) - - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should reject ReactionPatch without operation', async () => { - const event: Enriched = { - type: MessageEventType.ReactionPatch, - cardId: 'card-123' as any, - messageId: 'msg-123', - personUuid: 'person-123', - socialId, - date: new Date() - } - - await expect(middleware.event(session, event, false)).rejects.toThrow(Error) - }) - - it('should reject ReactionPatch with invalid opcode', async () => { - const event: Enriched = { - type: MessageEventType.ReactionPatch, - cardId: 'card-123' as any, - messageId: 'msg-123', - operation: { - opcode: 'invalid', - reaction: '👍' - }, - personUuid: 'person-123', - socialId, - date: new Date() - } - - await expect(middleware.event(session, event, false)).rejects.toThrow(Error) - }) - }) - - describe('event - AttachmentPatch validation', () => { - it('should validate correct AttachmentPatch add event', async () => { - const event: Enriched = { - type: MessageEventType.AttachmentPatch, - cardId: 'card-123' as any, - messageId: 'msg-123', - operations: [ - { - opcode: 'add', - attachments: [ - { - id: '550e8400-e29b-41d4-a716-446655440000', - mimeType: 'application/blob', - params: { - blobId: '550e8400-e29b-41d4-a716-446655440001', - mimeType: 'image/png', - fileName: 'test.png', - size: 1024 - } - } - ] - } - ], - socialId, - date: new Date() - } - - await middleware.event(session, event, false) - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should validate AttachmentPatch remove event', async () => { - const event: Enriched = { - type: MessageEventType.AttachmentPatch, - cardId: 'card-123' as any, - messageId: 'msg-123', - operations: [ - { - opcode: 'remove', - ids: ['550e8400-e29b-41d4-a716-446655440000'] - } - ], - socialId, - date: new Date() - } - - await middleware.event(session, event, false) - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should validate AttachmentPatch set event', async () => { - const event: Enriched = { - type: MessageEventType.AttachmentPatch, - cardId: 'card-123' as any, - messageId: 'msg-123', - operations: [ - { - opcode: 'set', - attachments: [ - { - id: '550e8400-e29b-41d4-a716-446655440000', - mimeType: 'application/blob', - params: { - blobId: '550e8400-e29b-41d4-a716-446655440002', - mimeType: 'image/png', - fileName: 'file.png', - size: 2048 - } - } - ] - } - ], - socialId, - date: new Date() - } - - await middleware.event(session, event, false) - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should validate AttachmentPatch update event', async () => { - const event: Enriched = { - type: MessageEventType.AttachmentPatch, - cardId: 'card-123' as any, - messageId: 'msg-123', - operations: [ - { - opcode: 'update', - attachments: [ - { - id: '550e8400-e29b-41d4-a716-446655440000', - params: { newData: 'value' } - } - ] - } - ], - socialId, - date: new Date() - } - - await middleware.event(session, event, false) - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should reject AttachmentPatch with empty operations', async () => { - const event: Enriched = { - type: MessageEventType.AttachmentPatch, - cardId: 'card-123' as any, - messageId: 'msg-123', - operations: [], - socialId, - date: new Date() - } - - await expect(middleware.event(session, event, false)).rejects.toThrow(Error) - }) - - it('should reject AttachmentPatch without operations', async () => { - const event: Enriched = { - type: MessageEventType.AttachmentPatch, - cardId: 'card-123' as any, - messageId: 'msg-123', - socialId, - date: new Date() - } - - await expect(middleware.event(session, event, false)).rejects.toThrow(Error) - }) - - it('should validate AttachmentPatch with link preview', async () => { - const event: Enriched = { - type: MessageEventType.AttachmentPatch, - cardId: 'card-123' as any, - messageId: 'msg-123', - operations: [ - { - opcode: 'add', - attachments: [ - { - id: '550e8400-e29b-41d4-a716-446655440000', - mimeType: 'application/vnd.huly.link-preview', - params: { - url: 'https://example.com', - host: 'example.com' - } - } - ] - } - ], - socialId, - date: new Date() - } - - await middleware.event(session, event, false) - expect(mockNext.event).toHaveBeenCalled() - }) - }) - - describe('event - ThreadPatch validation', () => { - it('should validate correct ThreadPatch attach event', async () => { - const event: Enriched = { - type: MessageEventType.ThreadPatch, - cardId: 'card-123' as any, - messageId: 'msg-123', - operation: { - opcode: 'attach', - threadId: 'thread-123', - threadType: 'task' as CardType - }, - personUuid: 'person-123', - socialId, - date: new Date() - } - - await middleware.event(session, event, false) - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should reject ThreadPatch without operation', async () => { - const event: Enriched = { - type: MessageEventType.ThreadPatch, - cardId: 'card-123' as any, - messageId: 'msg-123', - personUuid: 'person-123', - socialId, - date: new Date() - } - - await expect(middleware.event(session, event, false)).rejects.toThrow(Error) - }) - - it('should reject ThreadPatch without personUuid', async () => { - const event: Enriched = { - type: MessageEventType.ThreadPatch, - cardId: 'card-123' as any, - messageId: 'msg-123', - operation: { - opcode: 'attach', - threadId: 'thread-123', - threadType: 'task' as CardType - }, - socialId, - date: new Date() - } - - await expect(middleware.event(session, event, false)).rejects.toThrow(Error) - }) - }) - - describe('event - BlobPatch validation (deprecated)', () => { - it('should validate correct BlobPatch attach event', async () => { - const event: Enriched = { - type: MessageEventType.BlobPatch, - cardId: 'card-123' as any, - messageId: 'msg-123', - operations: [ - { - opcode: 'attach', - blobs: [ - { - blobId: '550e8400-e29b-41d4-a716-446655440000', - mimeType: 'image/png', - fileName: 'test.png', - size: 1024 - } - ] - } - ], - socialId, - date: new Date() - } - - await middleware.event(session, event, false) - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should validate BlobPatch detach event', async () => { - const event: Enriched = { - type: MessageEventType.BlobPatch, - cardId: 'card-123' as any, - messageId: 'msg-123', - operations: [ - { - opcode: 'detach', - blobIds: ['550e8400-e29b-41d4-a716-446655440000'] - } - ], - socialId, - date: new Date() - } - - await middleware.event(session, event, false) - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should validate BlobPatch set event', async () => { - const event: Enriched = { - type: MessageEventType.BlobPatch, - cardId: 'card-123' as any, - messageId: 'msg-123', - operations: [ - { - opcode: 'set', - blobs: [ - { - blobId: '550e8400-e29b-41d4-a716-446655440000', - mimeType: 'application/pdf', - fileName: 'doc.pdf', - size: 2048 - } - ] - } - ], - socialId, - date: new Date() - } - - await middleware.event(session, event, false) - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should validate BlobPatch update event', async () => { - const event: Enriched = { - type: MessageEventType.BlobPatch, - cardId: 'card-123' as any, - messageId: 'msg-123', - operations: [ - { - opcode: 'update', - blobs: [ - { - blobId: '550e8400-e29b-41d4-a716-446655440000', - fileName: 'updated.png' - } - ] - } - ], - socialId, - date: new Date() - } - - await middleware.event(session, event, false) - expect(mockNext.event).toHaveBeenCalled() - }) - }) - - describe('event - Notification events validation', () => { - it('should validate UpdateNotification event', async () => { - const event: Enriched = { - type: NotificationEventType.UpdateNotification, - contextId: 'ctx-123' as any, - account: accountUuid, - query: { - type: 'message' - }, - updates: { - read: true - }, - date: new Date() - } - - await middleware.event(session, event, false) - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should validate RemoveNotificationContext event', async () => { - const event: Enriched = { - type: NotificationEventType.RemoveNotificationContext, - contextId: 'ctx-123' as any, - account: accountUuid, - date: new Date() - } - - await middleware.event(session, event, false) - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should validate UpdateNotificationContext event', async () => { - const event: Enriched = { - type: NotificationEventType.UpdateNotificationContext, - contextId: 'ctx-123' as any, - account: accountUuid, - updates: { - lastView: new Date() - }, - date: new Date() - } - - await middleware.event(session, event, false) - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should validate AddCollaborators event', async () => { - const event: Enriched = { - type: NotificationEventType.AddCollaborators, - cardId: 'card-123' as any, - cardType: 'task' as CardType, - collaborators: [accountUuid, 'account-456' as AccountUuid], - socialId, - date: new Date() - } - - await middleware.event(session, event, false) - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should reject AddCollaborators with empty collaborators array', async () => { - const event: Enriched = { - type: NotificationEventType.AddCollaborators, - cardId: 'card-123' as any, - cardType: 'task' as CardType, - collaborators: [], - socialId, - date: new Date() - } - - await expect(middleware.event(session, event, false)).rejects.toThrow(Error) - }) - - it('should validate RemoveCollaborators event', async () => { - const event: Enriched = { - type: NotificationEventType.RemoveCollaborators, - cardId: 'card-123' as any, - cardType: 'task' as CardType, - collaborators: [accountUuid], - socialId, - date: new Date() - } - - await middleware.event(session, event, false) - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should reject RemoveCollaborators with empty collaborators array', async () => { - const event: Enriched = { - type: NotificationEventType.RemoveCollaborators, - cardId: 'card-123' as any, - cardType: 'task' as CardType, - collaborators: [], - socialId, - date: new Date() - } - - await expect(middleware.event(session, event, false)).rejects.toThrow(Error) - }) - }) - - describe('event - CreateMessage with options', () => { - it('should validate CreateMessage with skipLinkPreviews option', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123' as any, - cardType: 'task' as CardType, - messageType: 'text', - content: 'Test' as Markdown, - socialId, - date: new Date(), - options: { - skipLinkPreviews: true - } - } - - await middleware.event(session, event, false) - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should validate CreateMessage with noNotify option', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123' as any, - cardType: 'task' as CardType, - messageType: 'text', - content: 'Test' as Markdown, - socialId, - date: new Date(), - options: { - noNotify: true - } - } - - await middleware.event(session, event, false) - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should validate CreateMessage with ignoreMentions option', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123' as any, - cardType: 'task' as CardType, - messageType: 'text', - content: 'Test @user' as Markdown, - socialId, - date: new Date(), - options: { - ignoreMentions: true - } - } - - await middleware.event(session, event, false) - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should validate CreateMessage with language', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123' as any, - cardType: 'task' as CardType, - messageType: 'text', - content: 'Test' as Markdown, - socialId, - date: new Date(), - language: 'en' - } - - await middleware.event(session, event, false) - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should validate CreateMessage with extra data', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId: 'card-123' as any, - cardType: 'task' as CardType, - messageType: 'text', - content: 'Test' as Markdown, - socialId, - date: new Date(), - extra: { - customField: 'value' - } - } - - await middleware.event(session, event, false) - expect(mockNext.event).toHaveBeenCalled() - }) - }) - - describe('event - UpdatePatch with options', () => { - it('should validate UpdatePatch with skipLinkPreviewsUpdate option', async () => { - const event: Enriched = { - type: MessageEventType.UpdatePatch, - cardId: 'card-123' as any, - messageId: 'msg-123', - content: 'Updated' as Markdown, - socialId, - date: new Date(), - options: { - skipLinkPreviewsUpdate: true - } - } - - await middleware.event(session, event, false) - expect(mockNext.event).toHaveBeenCalled() - }) - - it('should validate UpdatePatch with extra data', async () => { - const event: Enriched = { - type: MessageEventType.UpdatePatch, - cardId: 'card-123' as any, - messageId: 'msg-123', - content: 'Updated' as Markdown, - socialId, - date: new Date(), - extra: { - customField: 'newValue' - } - } - - await middleware.event(session, event, false) - expect(mockNext.event).toHaveBeenCalled() - }) - }) - - describe('event - derived events', () => { - it('should skip validation for derived events', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - // Missing required fields - date: new Date() - } - - await middleware.event(session, event, true) - - expect(mockNext.event).toHaveBeenCalledWith(session, event, true) - }) - }) - - describe('findNotificationContexts', () => { - it('should validate correct params', async () => { - const params = { cardId: 'card-123' as any } - - await middleware.findNotificationContexts(session, params) - - expect(mockNext.findNotificationContexts).toHaveBeenCalledWith(session, params, undefined) - }) - - it('should validate params with multiple fields', async () => { - const params = { - cardId: 'card-123' as any, - account: accountUuid, - limit: 10 - } - - await middleware.findNotificationContexts(session, params) - - expect(mockNext.findNotificationContexts).toHaveBeenCalled() - }) - - it('should pass subscription parameter', async () => { - const params = { cardId: 'card-123' as any } - const subscription = 'sub-123' - - await middleware.findNotificationContexts(session, params, subscription) - - expect(mockNext.findNotificationContexts).toHaveBeenCalledWith(session, params, subscription) - }) - - it('should validate params with array of cardIds', async () => { - const params = { - cardId: ['card-1', 'card-2', 'card-3'] as any[] - } - - await middleware.findNotificationContexts(session, params) - expect(mockNext.findNotificationContexts).toHaveBeenCalled() - }) - - it('should validate params with array of accounts', async () => { - const params = { - cardId: 'card-123' as any, - account: [accountUuid, 'account-456' as AccountUuid] - } - - await middleware.findNotificationContexts(session, params) - expect(mockNext.findNotificationContexts).toHaveBeenCalled() - }) - - it('should validate params with notifications nested params', async () => { - const params = { - cardId: 'card-123' as any, - notifications: { - limit: 10, - order: 1, - read: false, - total: true - } - } - - await middleware.findNotificationContexts(session, params) - expect(mockNext.findNotificationContexts).toHaveBeenCalled() - }) - - it('should validate params with order parameter', async () => { - const params = { - cardId: 'card-123' as any, - order: 1, - limit: 20 - } - - await middleware.findNotificationContexts(session, params) - expect(mockNext.findNotificationContexts).toHaveBeenCalled() - }) - }) - - describe('findNotifications', () => { - it('should validate correct params', async () => { - const params = { contextId: 'ctx-123' as any } - - await middleware.findNotifications(session, params) - - expect(mockNext.findNotifications).toHaveBeenCalledWith(session, params, undefined) - }) - - it('should validate params with query', async () => { - const params = { - contextId: 'ctx-123' as any, - read: false, - limit: 20 - } - - await middleware.findNotifications(session, params) - - expect(mockNext.findNotifications).toHaveBeenCalled() - }) - - it('should validate params with all optional fields', async () => { - const params = { - contextId: 'ctx-123' as any, - read: false, - cardId: 'card-123' as any, - total: true, - limit: 50, - order: 1 - } - - await middleware.findNotifications(session, params) - expect(mockNext.findNotifications).toHaveBeenCalled() - }) - - it('should validate params without contextId', async () => { - const params = { - account: accountUuid, - cardId: 'card-123' as any - } - - await middleware.findNotifications(session, params) - expect(mockNext.findNotifications).toHaveBeenCalled() - }) - }) - - describe('findLabels', () => { - it('should validate correct params', async () => { - const params = { cardId: 'card-123' as any } - - await middleware.findLabels(session, params) - - expect(mockNext.findLabels).toHaveBeenCalledWith(session, params, undefined) - }) - - it('should validate params with labelId', async () => { - const params = { - cardId: 'card-123' as any, - labelId: 'label-456' as any - } - - await middleware.findLabels(session, params) - - expect(mockNext.findLabels).toHaveBeenCalled() - }) - - it('should validate params with array of labelIds', async () => { - const params = { - cardId: 'card-123' as any, - labelId: ['label-1', 'label-2'] as any - } - - await middleware.findLabels(session, params) - expect(mockNext.findLabels).toHaveBeenCalled() - }) - - it('should validate params with cardType', async () => { - const params = { - cardId: 'card-123' as any, - cardType: 'task' as CardType - } - - await middleware.findLabels(session, params) - expect(mockNext.findLabels).toHaveBeenCalled() - }) - - it('should validate params with array of cardTypes', async () => { - const params = { - cardId: 'card-123' as any, - cardType: ['task', 'issue'] as CardType[] - } - - await middleware.findLabels(session, params) - expect(mockNext.findLabels).toHaveBeenCalled() - }) - }) - - describe('findCollaborators', () => { - it('should validate correct params', async () => { - const params = { cardId: 'card-123' as CardID } - - await middleware.findCollaborators(session, params) - - expect(mockNext.findCollaborators).toHaveBeenCalledWith(session, params) - }) - }) - - describe('findMessagesGroups', () => { - it('should validate correct params', async () => { - const paramsRaw = { cardId: 'card-123' as CardID, fromDate: { less: '2025-10-20T18:59:17.593Z' as any } } - const params = { cardId: 'card-123' as CardID, fromDate: { less: new Date('2025-10-20T18:59:17.593Z') } } - - await middleware.findMessagesGroups(session, paramsRaw) - - expect(mockNext.findMessagesGroups).toHaveBeenCalledWith(session, params) - }) - }) - - describe('validation error handling', () => { - it('should log validation errors', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - // Missing required fields - date: new Date() - } - - await expect(middleware.event(session, event, false)).rejects.toThrow(Error) - - expect(mockMeasureCtx.error).toHaveBeenCalled() - }) - }) -}) diff --git a/foundations/communication/packages/server/src/__tests__/middlewares.test.ts b/foundations/communication/packages/server/src/__tests__/middlewares.test.ts deleted file mode 100644 index eab78e1572..0000000000 --- a/foundations/communication/packages/server/src/__tests__/middlewares.test.ts +++ /dev/null @@ -1,644 +0,0 @@ -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. - -import { MeasureContext, WorkspaceUuid } from '@hcengineering/core' -import { MessageEventType, SessionData } from '@hcengineering/communication-sdk-types' -import { AccountUuid, CardType, Markdown, SocialID } from '@hcengineering/communication-types' -import { buildMiddlewares, Middlewares } from '../middlewares' -import { Metadata, MiddlewareContext, MiddlewareCreateFn, CommunicationCallbacks } from '../types' -import { LowLevelClient } from '../client' - -describe('Middlewares', () => { - let mockContext: MiddlewareContext - let mockClient: { - db: { - findPeers: jest.Mock - findMessagesMeta: jest.Mock - findNotificationContexts: jest.Mock - findNotifications: jest.Mock - findLabels: jest.Mock - findCollaborators: jest.Mock - } - blob: Record - } - let mockMeasureCtx: jest.Mocked - let mockCallbacks: jest.Mocked - let session: SessionData - let metadata: Metadata - - const workspace = 'test-workspace' as WorkspaceUuid - const accountUuid = 'account-123' as AccountUuid - const socialId = 'social-123' as SocialID - - beforeEach(() => { - jest.clearAllMocks() - - mockMeasureCtx = { - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - newChild: jest.fn().mockReturnThis(), - contextData: undefined - } as unknown as jest.Mocked - - mockClient = { - db: { - findPeers: jest.fn().mockResolvedValue([]), - findMessagesMeta: jest.fn().mockResolvedValue([]), - findNotificationContexts: jest.fn().mockResolvedValue([]), - findNotifications: jest.fn().mockResolvedValue([]), - findLabels: jest.fn().mockResolvedValue([]), - findCollaborators: jest.fn().mockResolvedValue([]) - }, - blob: {} - } - - mockCallbacks = { - registerAsyncRequest: jest.fn(), - broadcast: jest.fn(), - enqueue: jest.fn() - } as any as jest.Mocked - - metadata = { - accountsUrl: 'http://accounts.test', - hulylakeUrl: 'http://hulylake.test', - secret: 'test-secret', - messagesPerBlob: 100 - } - - mockContext = { - ctx: mockMeasureCtx, - client: mockClient as unknown as LowLevelClient, - workspace, - metadata, - cadsWithPeers: new Set() - } - - session = { - account: { - uuid: accountUuid, - socialIds: [socialId] - }, - sessionId: 'session-123', - asyncData: [] - } as unknown as SessionData - }) - - describe('buildMiddlewares', () => { - it('should build middleware chain with all middlewares', async () => { - const middlewares = await buildMiddlewares( - mockMeasureCtx, - workspace, - metadata, - mockClient as unknown as LowLevelClient, - mockCallbacks - ) - - expect(middlewares).toBeDefined() - expect(mockClient.db.findPeers).toHaveBeenCalledWith({ workspaceId: workspace }) - }) - - it('should initialize cadsWithPeers from database', async () => { - const peers = [ - { cardId: 'card-1' as any, workspaceId: workspace, kind: 'slack', value: 'channel-1' }, - { cardId: 'card-2' as any, workspaceId: workspace, kind: 'slack', value: 'channel-2' } - ] - mockClient.db.findPeers.mockResolvedValue(peers as any) - - await buildMiddlewares( - mockMeasureCtx, - workspace, - metadata, - mockClient as unknown as LowLevelClient, - mockCallbacks - ) - - expect(mockClient.db.findPeers).toHaveBeenCalledWith({ workspaceId: workspace }) - }) - - it('should handle errors during middleware initialization', async () => { - mockClient.db.findPeers.mockRejectedValue(new Error('Database error')) - - await expect( - buildMiddlewares(mockMeasureCtx, workspace, metadata, mockClient as unknown as LowLevelClient, mockCallbacks) - ).rejects.toThrow('Database error') - }) - }) - - describe('Middlewares.create', () => { - it('should create middleware chain from create functions', async () => { - const createFns: MiddlewareCreateFn[] = [ - async (context, next) => - ({ - ...next, - event: jest.fn().mockResolvedValue({}), - findMessagesMeta: jest.fn().mockResolvedValue([]), - findMessagesGroups: jest.fn().mockResolvedValue([]), - findNotificationContexts: jest.fn().mockResolvedValue([]), - findNotifications: jest.fn().mockResolvedValue([]), - findLabels: jest.fn().mockResolvedValue([]), - findCollaborators: jest.fn().mockResolvedValue([]), - findPeers: jest.fn().mockResolvedValue([]), - handleBroadcast: jest.fn(), - subscribeCard: jest.fn(), - unsubscribeCard: jest.fn(), - close: jest.fn(), - closeSession: jest.fn() - }) as any - ] - - const middlewares = await Middlewares.create(mockMeasureCtx, mockContext, createFns) - - expect(middlewares).toBeDefined() - }) - - it('should handle errors during chain building', async () => { - const createFns: MiddlewareCreateFn[] = [ - async () => { - throw new Error('Middleware initialization failed') - } - ] - - await expect(Middlewares.create(mockMeasureCtx, mockContext, createFns)).rejects.toThrow( - 'Middleware initialization failed' - ) - - expect(mockMeasureCtx.error).toHaveBeenCalledWith( - 'failed to initialize middlewares', - expect.objectContaining({ - workspace - }) - ) - }) - - it('should build chain in correct order', async () => { - const order: string[] = [] - const createFns: MiddlewareCreateFn[] = [ - async (context, next) => { - order.push('first') - return { - ...next, - event: jest.fn().mockResolvedValue({}), - findMessagesMeta: jest.fn().mockResolvedValue([]), - findMessagesGroups: jest.fn().mockResolvedValue([]), - findNotificationContexts: jest.fn().mockResolvedValue([]), - findNotifications: jest.fn().mockResolvedValue([]), - findLabels: jest.fn().mockResolvedValue([]), - findCollaborators: jest.fn().mockResolvedValue([]), - findPeers: jest.fn().mockResolvedValue([]), - handleBroadcast: jest.fn(), - subscribeCard: jest.fn(), - unsubscribeCard: jest.fn(), - close: jest.fn(), - closeSession: jest.fn() - } as any - }, - async (context, next) => { - order.push('second') - return { - ...next, - event: jest.fn().mockResolvedValue({}), - findMessagesMeta: jest.fn().mockResolvedValue([]), - findMessagesGroups: jest.fn().mockResolvedValue([]), - findNotificationContexts: jest.fn().mockResolvedValue([]), - findNotifications: jest.fn().mockResolvedValue([]), - findLabels: jest.fn().mockResolvedValue([]), - findCollaborators: jest.fn().mockResolvedValue([]), - findPeers: jest.fn().mockResolvedValue([]), - handleBroadcast: jest.fn(), - subscribeCard: jest.fn(), - unsubscribeCard: jest.fn(), - close: jest.fn(), - closeSession: jest.fn() - } as any - } - ] - - await Middlewares.create(mockMeasureCtx, mockContext, createFns) - - // Middlewares should be created in reverse order (last to first) - expect(order).toEqual(['second', 'first']) - }) - }) - - describe('Middlewares methods', () => { - let middlewares: Middlewares - let mockHead: any - - beforeEach(async () => { - mockHead = { - event: jest.fn().mockResolvedValue({}), - findMessagesMeta: jest.fn().mockResolvedValue([{ messageId: 'msg-1' }]), - findMessagesGroups: jest.fn().mockResolvedValue([{ blobId: 'blob-1' }]), - findNotificationContexts: jest.fn().mockResolvedValue([{ id: 'ctx-1' }]), - findNotifications: jest.fn().mockResolvedValue([{ id: 'notif-1' }]), - findLabels: jest.fn().mockResolvedValue([{ labelId: 'label-1' }]), - findCollaborators: jest.fn().mockResolvedValue([{ account: accountUuid }]), - findPeers: jest.fn().mockResolvedValue([{ cardId: 'card-1' }]), - handleBroadcast: jest.fn(), - subscribeCard: jest.fn(), - unsubscribeCard: jest.fn(), - close: jest.fn(), - closeSession: jest.fn() - } - - const createFns: MiddlewareCreateFn[] = [async () => mockHead] - - middlewares = await Middlewares.create(mockMeasureCtx, mockContext, createFns) - }) - - describe('findMessagesMeta', () => { - it('should delegate to head middleware', async () => { - const params = { cardId: 'card-123' as any } - - const result = await middlewares.findMessagesMeta(session, params) - - expect(mockHead.findMessagesMeta).toHaveBeenCalledWith(session, params) - expect(result).toEqual([{ messageId: 'msg-1' }]) - }) - - it('should return empty array if no head', async () => { - const middlewaresNoHead = await Middlewares.create(mockMeasureCtx, mockContext, []) - - const result = await middlewaresNoHead.findMessagesMeta(session, { cardId: 'card-123' as any }) - - expect(result).toEqual([]) - }) - }) - - describe('findMessagesGroups', () => { - it('should delegate to head middleware', async () => { - const params = { cardId: 'card-123' as any } - - const result = await middlewares.findMessagesGroups(session, params) - - expect(mockHead.findMessagesGroups).toHaveBeenCalledWith(session, params) - expect(result).toEqual([{ blobId: 'blob-1' }]) - }) - - it('should return empty array if no head', async () => { - const middlewaresNoHead = await Middlewares.create(mockMeasureCtx, mockContext, []) - - const result = await middlewaresNoHead.findMessagesGroups(session, { cardId: 'card-123' as any }) - - expect(result).toEqual([]) - }) - }) - - describe('findNotificationContexts', () => { - it('should delegate to head middleware', async () => { - const params = { cardId: 'card-123' as any } - const subscription = 'sub-123' - - const result = await middlewares.findNotificationContexts(session, params, subscription) - - expect(mockHead.findNotificationContexts).toHaveBeenCalledWith(session, params, subscription) - expect(result).toEqual([{ id: 'ctx-1' }]) - }) - - it('should return empty array if no head', async () => { - const middlewaresNoHead = await Middlewares.create(mockMeasureCtx, mockContext, []) - - const result = await middlewaresNoHead.findNotificationContexts(session, { cardId: 'card-123' as any }) - - expect(result).toEqual([]) - }) - }) - - describe('findNotifications', () => { - it('should delegate to head middleware', async () => { - const params = { contextId: 'ctx-123' as any } - const subscription = 'sub-123' - - const result = await middlewares.findNotifications(session, params, subscription) - - expect(mockHead.findNotifications).toHaveBeenCalledWith(session, params, subscription) - expect(result).toEqual([{ id: 'notif-1' }]) - }) - - it('should return empty array if no head', async () => { - const middlewaresNoHead = await Middlewares.create(mockMeasureCtx, mockContext, []) - - const result = await middlewaresNoHead.findNotifications(session, { contextId: 'ctx-123' as any }) - - expect(result).toEqual([]) - }) - }) - - describe('findLabels', () => { - it('should delegate to head middleware', async () => { - const params = { cardId: 'card-123' as any } - - const result = await middlewares.findLabels(session, params) - - expect(mockHead.findLabels).toHaveBeenCalledWith(session, params) - expect(result).toEqual([{ labelId: 'label-1' }]) - }) - - it('should return empty array if no head', async () => { - const middlewaresNoHead = await Middlewares.create(mockMeasureCtx, mockContext, []) - - const result = await middlewaresNoHead.findLabels(session, { cardId: 'card-123' as any }) - - expect(result).toEqual([]) - }) - }) - - describe('findCollaborators', () => { - it('should delegate to head middleware', async () => { - const params = { cardId: 'card-123' as any } - - const result = await middlewares.findCollaborators(session, params) - - expect(mockHead.findCollaborators).toHaveBeenCalledWith(session, params) - expect(result).toEqual([{ account: accountUuid }]) - }) - - it('should return empty array if no head', async () => { - const middlewaresNoHead = await Middlewares.create(mockMeasureCtx, mockContext, []) - - const result = await middlewaresNoHead.findCollaborators(session, { cardId: 'card-123' as any }) - - expect(result).toEqual([]) - }) - }) - - describe('findPeers', () => { - it('should delegate to head middleware', async () => { - const params = { workspaceId: workspace, cardId: 'card-123' as any } - - const result = await middlewares.findPeers(session, params) - - expect(mockHead.findPeers).toHaveBeenCalledWith(session, params) - expect(result).toEqual([{ cardId: 'card-1' }]) - }) - - it('should return empty array if no head', async () => { - const middlewaresNoHead = await Middlewares.create(mockMeasureCtx, mockContext, []) - - const result = await middlewaresNoHead.findPeers(session, { workspaceId: workspace, cardId: 'card-123' as any }) - - expect(result).toEqual([]) - }) - }) - - describe('event', () => { - it('should process event through middleware chain', async () => { - const event: any = { - type: MessageEventType.CreateMessage, - cardId: 'card-123', - messageId: 'msg-123', - cardType: 'task' as CardType, - messageType: 'text', - content: 'Test' as Markdown, - socialId, - date: new Date() - } - - const result = await middlewares.event(session, event) - - expect(mockHead.event).toHaveBeenCalledWith(session, event, false) - expect(mockHead.handleBroadcast).toHaveBeenCalledWith(session, [event]) - expect(result).toBeDefined() - }) - - it('should handle derived flag from session', async () => { - const event: any = { - type: MessageEventType.UpdatePatch, - cardId: 'card-123', - messageId: 'msg-123', - content: 'Updated' as Markdown, - socialId, - date: new Date() - } - - session.derived = true - - await middlewares.event(session, event) - - expect(mockHead.event).toHaveBeenCalledWith(session, event, true) - }) - - it('should return empty object if no head', async () => { - const middlewaresNoHead = await Middlewares.create(mockMeasureCtx, mockContext, []) - - const event: any = { - type: MessageEventType.CreateMessage, - date: new Date() - } - - const result = await middlewaresNoHead.event(session, event) - - expect(result).toEqual({}) - }) - }) - - describe('subscribeCard', () => { - it('should delegate to head middleware', () => { - const cardId = 'card-123' as any - const subscription = 'sub-123' - - middlewares.subscribeCard(session, cardId, subscription) - - expect(mockHead.subscribeCard).toHaveBeenCalledWith(session, cardId, subscription) - }) - - it('should do nothing if no head', () => { - const middlewaresNoHead = Middlewares.create(mockMeasureCtx, mockContext, []) - - expect(() => { - void middlewaresNoHead.then((m) => { - m.subscribeCard(session, 'card-123' as any, 'sub-123') - }) - }).not.toThrow() - }) - }) - - describe('unsubscribeCard', () => { - it('should delegate to head middleware', () => { - const cardId = 'card-123' as any - const subscription = 'sub-123' - - middlewares.unsubscribeCard(session, cardId, subscription) - - expect(mockHead.unsubscribeCard).toHaveBeenCalledWith(session, cardId, subscription) - }) - - it('should do nothing if no head', async () => { - const middlewaresNoHead = await Middlewares.create(mockMeasureCtx, mockContext, []) - - expect(() => { - middlewaresNoHead.unsubscribeCard(session, 'card-123' as any, 'sub-123') - }).not.toThrow() - }) - }) - - describe('closeSession', () => { - it('should delegate to head middleware', async () => { - await middlewares.closeSession('session-123') - - expect(mockHead.closeSession).toHaveBeenCalledWith('session-123') - }) - - it('should do nothing if no head', async () => { - const middlewaresNoHead = await Middlewares.create(mockMeasureCtx, mockContext, []) - - await expect(middlewaresNoHead.closeSession('session-123')).resolves.not.toThrow() - }) - }) - - describe('close', () => { - it('should close all middlewares', async () => { - await middlewares.close() - - expect(mockHead.close).toHaveBeenCalled() - }) - - it('should handle errors during close', async () => { - // Create a fresh context to avoid mock clearing issues - const freshMockMeasureCtx = { - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - newChild: jest.fn().mockReturnThis(), - contextData: undefined - } as unknown as jest.Mocked - - const freshContext = { - ...mockContext, - ctx: freshMockMeasureCtx - } - - // Create a new middlewares instance with a close function that throws - const mockMiddleware = { - event: jest.fn().mockResolvedValue({}), - findMessagesMeta: jest.fn().mockResolvedValue([]), - findMessagesGroups: jest.fn().mockResolvedValue([]), - findNotificationContexts: jest.fn().mockResolvedValue([]), - findNotifications: jest.fn().mockResolvedValue([]), - findLabels: jest.fn().mockResolvedValue([]), - findCollaborators: jest.fn().mockResolvedValue([]), - findPeers: jest.fn().mockResolvedValue([]), - handleBroadcast: jest.fn(), - subscribeCard: jest.fn(), - unsubscribeCard: jest.fn(), - close: jest.fn(() => { - throw new Error('Close error') - }), - closeSession: jest.fn() - } - - const createFns: MiddlewareCreateFn[] = [async () => mockMiddleware] - - const testMiddlewares = await Middlewares.create(freshMockMeasureCtx, freshContext, createFns) - - await testMiddlewares.close() - - expect(mockMiddleware.close).toHaveBeenCalled() - expect(freshMockMeasureCtx.error).toHaveBeenCalledWith( - 'Failed to close middleware', - expect.objectContaining({ - workspace - }) - ) - }) - - it('should do nothing if no head', async () => { - const middlewaresNoHead = await Middlewares.create(mockMeasureCtx, mockContext, []) - - await expect(middlewaresNoHead.close()).resolves.not.toThrow() - }) - }) - }) - - describe('Edge cases', () => { - it('should handle empty middleware chain', async () => { - const middlewares = await Middlewares.create(mockMeasureCtx, mockContext, []) - - const event: any = { - type: MessageEventType.CreateMessage, - date: new Date() - } - - const result = await middlewares.event(session, event) - expect(result).toEqual({}) - - const messages = await middlewares.findMessagesMeta(session, { cardId: 'card-123' as any }) - expect(messages).toEqual([]) - }) - - it('should handle multiple middlewares in chain', async () => { - const middleware1Called: string[] = [] - const middleware2Called: string[] = [] - - const createFns: MiddlewareCreateFn[] = [ - async (context, next) => - ({ - ...next, - event: jest.fn().mockImplementation(async (s, e, d) => { - middleware1Called.push('event') - return next?.event != null ? await next.event(s, e, d) : {} - }), - findMessagesMeta: jest.fn().mockImplementation(async (s, p) => { - middleware1Called.push('findMessagesMeta') - return next?.findMessagesMeta != null ? await next.findMessagesMeta(s, p) : [] - }), - findMessagesGroups: jest.fn().mockResolvedValue([]), - findNotificationContexts: jest.fn().mockResolvedValue([]), - findNotifications: jest.fn().mockResolvedValue([]), - findLabels: jest.fn().mockResolvedValue([]), - findCollaborators: jest.fn().mockResolvedValue([]), - findPeers: jest.fn().mockResolvedValue([]), - handleBroadcast: jest.fn(), - subscribeCard: jest.fn(), - unsubscribeCard: jest.fn(), - close: jest.fn(), - closeSession: jest.fn() - }) as any, - async (context, next) => - ({ - ...next, - event: jest.fn().mockImplementation(async (s, e, d) => { - middleware2Called.push('event') - return {} - }), - findMessagesMeta: jest.fn().mockImplementation(async () => { - middleware2Called.push('findMessagesMeta') - return [{ messageId: 'msg-1' }] - }), - findMessagesGroups: jest.fn().mockResolvedValue([]), - findNotificationContexts: jest.fn().mockResolvedValue([]), - findNotifications: jest.fn().mockResolvedValue([]), - findLabels: jest.fn().mockResolvedValue([]), - findCollaborators: jest.fn().mockResolvedValue([]), - findPeers: jest.fn().mockResolvedValue([]), - handleBroadcast: jest.fn(), - subscribeCard: jest.fn(), - unsubscribeCard: jest.fn(), - close: jest.fn(), - closeSession: jest.fn() - }) as any - ] - - const middlewares = await Middlewares.create(mockMeasureCtx, mockContext, createFns) - - await middlewares.event(session, { type: MessageEventType.CreateMessage, date: new Date() } as any) - await middlewares.findMessagesMeta(session, { cardId: 'card-123' as any }) - - expect(middleware1Called).toContain('event') - expect(middleware1Called).toContain('findMessagesMeta') - expect(middleware2Called).toContain('event') - expect(middleware2Called).toContain('findMessagesMeta') - }) - }) -}) diff --git a/foundations/communication/packages/server/src/__tests__/notification/notification.test.ts b/foundations/communication/packages/server/src/__tests__/notification/notification.test.ts deleted file mode 100644 index 9431cb808a..0000000000 --- a/foundations/communication/packages/server/src/__tests__/notification/notification.test.ts +++ /dev/null @@ -1,872 +0,0 @@ -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. - -import { type MeasureContext, type WorkspaceUuid, readOnlyGuestAccountUuid } from '@hcengineering/core' -import { - type Event, - MessageEventType, - NotificationEventType, - type CreateNotificationContextResult -} from '@hcengineering/communication-sdk-types' -import { - type AccountUuid, - type BlobID, - type CardID, - type CardType, - type ContextID, - type Markdown, - type MessageID, - type MessageMeta, - type NotificationContext, - NotificationType, - type ReactionNotificationContent, - type SocialID -} from '@hcengineering/communication-types' - -import { notify } from '../../notification/notification' -import { type TriggerCtx, type Enriched } from '../../types' -import { getNameBySocialID } from '../../triggers/utils' - -// Mock dependencies -jest.mock('@hcengineering/text-markdown', () => ({ - markdownToMarkup: jest.fn((md) => ({ type: 'doc', content: [{ type: 'text', text: md }] })) -})) - -jest.mock('@hcengineering/text-core', () => ({ - jsonToMarkup: jest.fn((json) => json), - markupToText: jest.fn((markup) => { - if (typeof markup === 'string') return markup - if (markup?.content?.[0]?.text !== undefined) return markup.content[0].text - return 'Test message text' - }) -})) - -jest.mock('../../triggers/utils', () => ({ - getNameBySocialID: jest.fn().mockResolvedValue('John Doe') -})) - -describe('notification', () => { - let mockCtx: TriggerCtx - let mockClient: any - let mockMeasureCtx: jest.Mocked - - const workspace = 'test-workspace' as WorkspaceUuid - const cardId = 'card-123' as CardID - const messageId = 'message-123' as MessageID - const blobId = 'blob-123' as BlobID - const socialId = 'social-123' as SocialID - const accountUuid = 'account-123' as AccountUuid - const contextId = 'context-123' as ContextID - const cardType = 'card' as CardType - - beforeEach(() => { - jest.clearAllMocks() - - // Reset the getNameBySocialID mock to its default behavior - const mockGetName = getNameBySocialID as jest.MockedFunction - mockGetName.mockResolvedValue('John Doe') - - mockMeasureCtx = { - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - newChild: jest.fn().mockReturnThis() - } as any - - mockClient = { - db: { - findNotifications: jest.fn().mockResolvedValue([]), - findNotificationContexts: jest.fn().mockResolvedValue([]), - getCardSpaceMembers: jest.fn().mockResolvedValue([accountUuid]), - getCollaboratorsCursor: jest.fn(), - getCardTitle: jest.fn().mockResolvedValue('Test Card'), - getNameByAccount: jest.fn().mockResolvedValue('John Doe') - }, - getMessageMeta: jest.fn(), - findPersonUuid: jest.fn() - } - - mockCtx = { - ctx: mockMeasureCtx, - client: mockClient, - workspace, - metadata: { - accountsUrl: 'http://accounts', - hulylakeUrl: 'http://hulylake', - secret: 'secret', - messagesPerBlob: 200 - }, - account: { - uuid: accountUuid, - socialIds: [socialId] - } as any, - execute: jest.fn() - } as any - }) - - describe('notify', () => { - describe('CreateMessage event', () => { - it('should return empty array when noNotify option is true', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId, - messageId, - content: 'Hello' as Markdown, - socialId, - date: new Date(), - cardType, - options: { noNotify: true } - } as any - - const result = await notify(mockCtx, event) - - expect(result).toEqual([]) - const getMessageMetaSpy = mockClient.getMessageMeta as jest.Mock - expect(getMessageMetaSpy).toHaveBeenCalledTimes(0) - }) - - it('should return empty array when messageId is null', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId, - messageId: null, - content: 'Hello' as Markdown, - socialId, - date: new Date(), - cardType - } as any - - const result = await notify(mockCtx, event) - - expect(result).toEqual([]) - const getMessageMetaSpy = mockClient.getMessageMeta as jest.Mock - expect(getMessageMetaSpy).toHaveBeenCalledTimes(0) - }) - - it('should return empty array when message meta is not found', async () => { - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId, - messageId, - content: 'Hello' as Markdown, - socialId, - date: new Date(), - cardType - } as any - - mockClient.getMessageMeta.mockResolvedValue(undefined) - - const result = await notify(mockCtx, event) - - expect(result).toEqual([]) - const getMessageMetaSpy = mockClient.getMessageMeta as jest.Mock - expect(getMessageMetaSpy).toHaveBeenCalledTimes(1) - expect(getMessageMetaSpy).toHaveBeenLastCalledWith(cardId, messageId) - }) - - it('should create notification for message', async () => { - const date = new Date() - const creatorSocialId = 'creator-social' as SocialID - const creatorAccount = 'creator-account' as AccountUuid - const collaboratorAccount = 'collaborator-account' as AccountUuid - - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId, - messageId, - content: 'Hello World' as Markdown, - socialId: creatorSocialId, - date, - cardType - } as any - - const meta: MessageMeta = { - cardId, - id: messageId, - blobId, - creator: creatorSocialId, - createdOn: date.getTime() - } as any - - mockClient.getMessageMeta.mockResolvedValue(meta) - - // First call for message creator, second for collaborator - mockClient.findPersonUuid - .mockResolvedValueOnce(creatorAccount) // creator account - .mockResolvedValueOnce(creatorAccount) // when checking if it's own message - - mockClient.db.getCardSpaceMembers.mockResolvedValue([collaboratorAccount, creatorAccount]) - - // Mock collaborators cursor - return a collaborator that's not the creator - const collaborators = [{ account: collaboratorAccount, personUuid: 'person-1' as any }] - mockClient.db.getCollaboratorsCursor.mockReturnValue({ - [Symbol.asyncIterator]: async function * () { - yield collaborators - } - }) - - // Mock context doesn't exist, will be created - mockClient.db.findNotificationContexts.mockResolvedValue([]) - - // Mock context creation - const contextResult: CreateNotificationContextResult = { id: contextId } - mockCtx.execute = jest.fn().mockResolvedValue(contextResult) - - const result = await notify(mockCtx, event) - - expect(result.length).toBeGreaterThan(0) - const getMessageMetaSpy = mockClient.getMessageMeta as jest.Mock - expect(getMessageMetaSpy).toHaveBeenCalledTimes(1) - expect(getMessageMetaSpy).toHaveBeenLastCalledWith(cardId, messageId) - }) - - it('should skip collaborators not in space members', async () => { - const date = new Date() - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId, - messageId, - content: 'Hello' as Markdown, - socialId, - date, - cardType - } as any - - const meta: MessageMeta = { - cardId, - id: messageId, - blobId, - creator: socialId, - createdOn: date.getTime() - } as any - - mockClient.getMessageMeta.mockResolvedValue(meta) - mockClient.findPersonUuid.mockResolvedValue('other-account' as AccountUuid) - mockClient.db.getCardSpaceMembers.mockResolvedValue([accountUuid]) - - const collaborators = [{ account: 'other-account' as AccountUuid, personUuid: 'person-2' as any }] - mockClient.db.getCollaboratorsCursor.mockReturnValue({ - [Symbol.asyncIterator]: async function * () { - yield collaborators - } - }) - - const result = await notify(mockCtx, event) - - expect(result).toEqual([]) - }) - - it('should handle errors during collaborator processing', async () => { - const date = new Date() - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId, - messageId, - content: 'Hello' as Markdown, - socialId, - date, - cardType - } as any - - const meta: MessageMeta = { - cardId, - id: messageId, - blobId, - creator: socialId, - createdOn: date.getTime() - } as any - - mockClient.getMessageMeta.mockResolvedValue(meta) - - // Make sure the creator account is different from collaborators - mockClient.findPersonUuid - .mockResolvedValueOnce('creator-account' as AccountUuid) // creator account (for socialId) - .mockResolvedValueOnce('creator-account' as AccountUuid) // used for isOwn check for first collaborator - .mockResolvedValueOnce('creator-account' as AccountUuid) // used for isOwn check for second collaborator - - // Two collaborators - processing will fail for the second one - const collaborators = [ - { account: accountUuid, personUuid: 'person-1' as any }, - { account: 'account-2' as AccountUuid, personUuid: 'person-2' as any } - ] - mockClient.db.getCollaboratorsCursor.mockReturnValue({ - [Symbol.asyncIterator]: async function * () { - yield collaborators - } - }) - - mockClient.db.getCardSpaceMembers.mockResolvedValue([accountUuid, 'account-2' as AccountUuid]) - - // Return contexts for BOTH collaborators so neither needs creation - const firstContext: NotificationContext = { - id: contextId, - cardId, - account: accountUuid, - lastUpdate: date, - lastView: date, - lastNotify: date - } as any - - const secondContext: NotificationContext = { - id: 'context-456' as ContextID, - cardId, - account: 'account-2' as AccountUuid, - lastUpdate: date, - lastView: date, - lastNotify: date - } as any - - mockClient.db.findNotificationContexts.mockResolvedValue([firstContext, secondContext]) - - // Make getNameBySocialID throw an error on the second call - const mockGetName = getNameBySocialID as jest.MockedFunction - mockGetName - .mockResolvedValueOnce('John Doe') // First collaborator succeeds - .mockRejectedValueOnce(new Error('Database error')) // Second collaborator fails - - const result = await notify(mockCtx, event) - - // Should have logged the error - expect(mockMeasureCtx.error).toHaveBeenCalledWith( - 'Error on create notification', - expect.objectContaining({ - collaborator: 'account-2', - error: expect.any(Error) - }) - ) - // Result should contain events from first collaborator even though second failed - expect(result).toBeDefined() - expect(Array.isArray(result)).toBe(true) - }) - - it('should continue processing other collaborators when one fails', async () => { - const date = new Date() - const event: Enriched = { - type: MessageEventType.CreateMessage, - cardId, - messageId, - content: 'Hello' as Markdown, - socialId, - date, - cardType - } as any - - const meta: MessageMeta = { - cardId, - id: messageId, - blobId, - creator: socialId, - createdOn: date.getTime() - } as any - - mockClient.getMessageMeta.mockResolvedValue(meta) - mockClient.findPersonUuid.mockResolvedValue(accountUuid) - - const collaborators = [ - { account: accountUuid, personUuid: 'person-1' as any } - ] - mockClient.db.getCollaboratorsCursor.mockReturnValue({ - [Symbol.asyncIterator]: async function * () { - yield collaborators - } - }) - - const context: NotificationContext = { - id: contextId, - cardId, - account: accountUuid, - lastUpdate: date, - lastView: date, - lastNotify: date - } as any - - mockClient.db.findNotificationContexts.mockResolvedValue([context]) - - const result = await notify(mockCtx, event) - - // Should return successfully even if there are potential errors - expect(result).toBeDefined() - expect(Array.isArray(result)).toBe(true) - }) - }) - - describe('ReactionPatch event - add', () => { - it('should create notification for reaction', async () => { - const date = new Date() - const reactionSocialId = 'reaction-social' as SocialID - const messageSocialId = 'message-social' as SocialID - const messageAccount = 'message-account' as AccountUuid - const reactionAccount = 'reaction-account' as AccountUuid - - const event: Enriched = { - type: MessageEventType.ReactionPatch, - cardId, - messageId, - operation: { - opcode: 'add', - reaction: '👍' - }, - socialId: reactionSocialId, - date - } as any - - const meta: MessageMeta = { - cardId, - id: messageId, - blobId, - creator: messageSocialId, - createdOn: date.getTime() - } as any - - mockClient.getMessageMeta.mockResolvedValue(meta) - mockClient.findPersonUuid - .mockResolvedValueOnce(messageAccount) // message creator - .mockResolvedValueOnce(reactionAccount) // reaction creator - - mockClient.db.getCardSpaceMembers.mockResolvedValue([messageAccount, reactionAccount]) - - const context: NotificationContext = { - id: contextId, - cardId, - account: messageAccount, - lastUpdate: new Date(date.getTime() - 1000), - lastView: new Date(date.getTime() - 2000), - lastNotify: new Date(date.getTime() - 1000) - } as any - - mockClient.db.findNotificationContexts.mockResolvedValue([context]) - - const result = await notify(mockCtx, event) - - expect(result).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - type: NotificationEventType.CreateNotification, - notificationType: NotificationType.Reaction, - messageId - }) - ]) - ) - }) - - it('should return empty array when message meta is not found', async () => { - const event: Enriched = { - type: MessageEventType.ReactionPatch, - cardId, - messageId, - operation: { opcode: 'add', reaction: '👍' }, - socialId, - date: new Date() - } as any - - mockClient.getMessageMeta.mockResolvedValue(undefined) - - const result = await notify(mockCtx, event) - - expect(result).toEqual([]) - }) - - it('should return empty array when message account is not found', async () => { - const event: Enriched = { - type: MessageEventType.ReactionPatch, - cardId, - messageId, - operation: { opcode: 'add', reaction: '👍' }, - socialId, - date: new Date() - } as any - - const meta: MessageMeta = { - cardId, - id: messageId, - blobId, - creator: socialId, - createdOn: Date.now() - } as any - - mockClient.getMessageMeta.mockResolvedValue(meta) - mockClient.findPersonUuid.mockResolvedValue(undefined) - - const result = await notify(mockCtx, event) - - expect(result).toEqual([]) - }) - - it('should return empty array when message account is not in space members', async () => { - const event: Enriched = { - type: MessageEventType.ReactionPatch, - cardId, - messageId, - operation: { opcode: 'add', reaction: '👍' }, - socialId, - date: new Date() - } as any - - const meta: MessageMeta = { - cardId, - id: messageId, - blobId, - creator: socialId, - createdOn: Date.now() - } as any - - mockClient.getMessageMeta.mockResolvedValue(meta) - mockClient.findPersonUuid.mockResolvedValue('other-account' as AccountUuid) - mockClient.db.getCardSpaceMembers.mockResolvedValue([accountUuid]) - - const result = await notify(mockCtx, event) - - expect(result).toEqual([]) - }) - - it('should not notify when reacting to own message', async () => { - const date = new Date() - const event: Enriched = { - type: MessageEventType.ReactionPatch, - cardId, - messageId, - operation: { opcode: 'add', reaction: '👍' }, - socialId, - date - } as any - - const meta: MessageMeta = { - cardId, - id: messageId, - blobId, - creator: socialId, - createdOn: date.getTime() - } as any - - mockClient.getMessageMeta.mockResolvedValue(meta) - mockClient.findPersonUuid.mockResolvedValue(accountUuid) - - const result = await notify(mockCtx, event) - - expect(result).toEqual([]) - }) - - it('should create context if it does not exist', async () => { - const date = new Date() - const otherSocialId = 'other-social' as SocialID - const event: Enriched = { - type: MessageEventType.ReactionPatch, - cardId, - messageId, - operation: { opcode: 'add', reaction: '👍' }, - socialId: otherSocialId, - date - } as any - - const meta: MessageMeta = { - cardId, - id: messageId, - blobId, - creator: socialId, - createdOn: date.getTime() - } as any - - mockClient.getMessageMeta.mockResolvedValue(meta) - mockClient.findPersonUuid - .mockResolvedValueOnce(accountUuid) // message creator - .mockResolvedValueOnce('other-account' as AccountUuid) // reaction creator - - mockClient.db.findNotificationContexts.mockResolvedValue([]) - - const createContextResult: CreateNotificationContextResult = { id: contextId } - mockCtx.execute = jest.fn().mockResolvedValue(createContextResult) - - const result = await notify(mockCtx, event) - - expect(mockCtx.execute).toHaveBeenCalledWith( - expect.objectContaining({ - type: NotificationEventType.CreateNotificationContext - }) - ) - expect(result.length).toBeGreaterThan(0) - }) - - it('should update context lastNotify if reaction is newer', async () => { - const date = new Date() - const oldDate = new Date(date.getTime() - 10000) - const otherSocialId = 'other-social' as SocialID - - const event: Enriched = { - type: MessageEventType.ReactionPatch, - cardId, - messageId, - operation: { opcode: 'add', reaction: '👍' }, - socialId: otherSocialId, - date - } as any - - const meta: MessageMeta = { - cardId, - id: messageId, - blobId, - creator: socialId, - createdOn: oldDate.getTime() - } as any - - const context: NotificationContext = { - id: contextId, - cardId, - account: accountUuid, - lastUpdate: oldDate, - lastView: oldDate, - lastNotify: oldDate - } as any - - mockClient.getMessageMeta.mockResolvedValue(meta) - mockClient.findPersonUuid - .mockResolvedValueOnce(accountUuid) - .mockResolvedValueOnce('other-account' as AccountUuid) - mockClient.db.findNotificationContexts.mockResolvedValue([context]) - - const result = await notify(mockCtx, event) - - expect(result).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - type: NotificationEventType.UpdateNotificationContext, - updates: expect.objectContaining({ - lastNotify: date - }) - }) - ]) - ) - }) - - it('should mark notification as read for readOnlyGuestAccount', async () => { - const date = new Date() - const otherSocialId = 'other-social' as SocialID - - const event: Enriched = { - type: MessageEventType.ReactionPatch, - cardId, - messageId, - operation: { opcode: 'add', reaction: '👍' }, - socialId: otherSocialId, - date - } as any - - const meta: MessageMeta = { - cardId, - id: messageId, - blobId, - creator: socialId, - createdOn: date.getTime() - } as any - - mockClient.getMessageMeta.mockResolvedValue(meta) - mockClient.findPersonUuid - .mockResolvedValueOnce(readOnlyGuestAccountUuid) // message creator - .mockResolvedValueOnce('other-account' as AccountUuid) // reaction creator - - // Make sure readOnlyGuestAccount is in space members - mockClient.db.getCardSpaceMembers.mockResolvedValue([readOnlyGuestAccountUuid, 'other-account' as AccountUuid]) - mockClient.db.findNotificationContexts.mockResolvedValue([]) - mockCtx.execute = jest.fn().mockResolvedValue({ id: contextId }) - - const result = await notify(mockCtx, event) - - const createNotification = result.find( - (e) => e.type === NotificationEventType.CreateNotification - ) - expect(createNotification).toBeDefined() - expect(createNotification).toMatchObject({ read: true }) - }) - }) - - describe('ReactionPatch event - remove', () => { - it('should remove reaction notification', async () => { - const date = new Date() - const event: Enriched = { - type: MessageEventType.ReactionPatch, - cardId, - messageId, - operation: { opcode: 'remove', reaction: '👍' }, - socialId, - date - } as any - - const meta: MessageMeta = { - cardId, - id: messageId, - blobId, - creator: socialId, - createdOn: date.getTime() - } as any - - const notificationContent: ReactionNotificationContent = { - emoji: '👍', - title: 'Reacted to your message', - shortText: '👍', - senderName: 'John Doe' - } - - const notification = { - id: 'notif-1', - contextId, - type: NotificationType.Reaction, - messageId, - account: accountUuid, - created: date, - content: notificationContent, - creator: socialId - } - - const context: NotificationContext = { - id: contextId, - cardId, - account: accountUuid, - lastUpdate: date, - lastView: date, - lastNotify: date - } as any - - mockClient.getMessageMeta.mockResolvedValue(meta) - mockClient.findPersonUuid.mockResolvedValue(accountUuid) - mockClient.db.findNotifications.mockResolvedValue([notification as any]) - mockClient.db.findNotificationContexts.mockResolvedValue([context]) - - const result = await notify(mockCtx, event) - - expect(result).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - type: NotificationEventType.RemoveNotifications, - ids: ['notif-1'] - }) - ]) - ) - }) - - it('should return empty array when notification not found', async () => { - const date = new Date() - const event: Enriched = { - type: MessageEventType.ReactionPatch, - cardId, - messageId, - operation: { opcode: 'remove', reaction: '👍' }, - socialId, - date - } as any - - const meta: MessageMeta = { - cardId, - id: messageId, - blobId, - creator: socialId, - createdOn: date.getTime() - } as any - - mockClient.getMessageMeta.mockResolvedValue(meta) - mockClient.findPersonUuid.mockResolvedValue(accountUuid) - mockClient.db.findNotifications.mockResolvedValue([]) - - const result = await notify(mockCtx, event) - - expect(result).toEqual([]) - }) - - it('should update context lastNotify when removed notification was the last one', async () => { - const date = new Date() - const olderDate = new Date(date.getTime() - 5000) - - const event: Enriched = { - type: MessageEventType.ReactionPatch, - cardId, - messageId, - operation: { opcode: 'remove', reaction: '👍' }, - socialId, - date - } as any - - const meta: MessageMeta = { - cardId, - id: messageId, - blobId, - creator: socialId, - createdOn: date.getTime() - } as any - - const notificationContent: ReactionNotificationContent = { - emoji: '👍', - title: 'Reacted to your message', - shortText: '👍', - senderName: 'John Doe' - } - - const notification = { - id: 'notif-1', - contextId, - type: NotificationType.Reaction, - messageId, - account: accountUuid, - created: date, - content: notificationContent, - creator: socialId - } - - const olderNotification = { - id: 'notif-2', - contextId, - created: olderDate - } - - const context: NotificationContext = { - id: contextId, - cardId, - account: accountUuid, - lastUpdate: date, - lastView: olderDate, - lastNotify: date - } as any - - mockClient.getMessageMeta.mockResolvedValue(meta) - mockClient.findPersonUuid.mockResolvedValue(accountUuid) - mockClient.db.findNotifications - .mockResolvedValueOnce([notification as any]) - .mockResolvedValueOnce([olderNotification as any]) - mockClient.db.findNotificationContexts.mockResolvedValue([context]) - - const result = await notify(mockCtx, event) - - expect(result).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - type: NotificationEventType.UpdateNotificationContext, - updates: expect.objectContaining({ - lastNotify: olderDate - }) - }) - ]) - ) - }) - }) - - describe('Unknown event type', () => { - it('should return empty array for unknown event type', async () => { - const event: Enriched = { - type: 'unknown.event' as any, - date: new Date() - } as any - - const result = await notify(mockCtx, event) - - expect(result).toEqual([]) - }) - }) - }) -}) diff --git a/foundations/communication/packages/server/src/blob.ts b/foundations/communication/packages/server/src/blob.ts deleted file mode 100644 index 181bf7ce85..0000000000 --- a/foundations/communication/packages/server/src/blob.ts +++ /dev/null @@ -1,594 +0,0 @@ -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. - -import { - MeasureContext, - PersonUuid, - SortingOrder, - systemAccountUuid, - WorkspaceUuid -} from '@hcengineering/core' -import { getWorkspaceClient, type HulylakeWorkspaceClient, type JsonPatch } from '@hcengineering/hulylake-client' -import { generateToken } from '@hcengineering/server-token' -import { - Attachment, - AttachmentID, - AttachmentUpdateData, - BlobID, - CardID, - CardType, - FindMessagesGroupParams, - Markdown, - Message, - MessageDoc, - MessageExtra, - MessageID, - MessagesDoc, - MessagesGroup, - MessagesGroupDoc, - MessagesGroupsDoc, - Thread - , ComparisonOperator -} from '@hcengineering/communication-types' -import { v4 as uuid } from 'uuid' - -import { Metadata } from './types' - -const LOG_CARD_ID = '67ecc702f182d88819f0a726' as CardID - -export class Blob { - private readonly client: HulylakeWorkspaceClient - // Groups sored by fromDate - private readonly messageGroupsByCardId = new Map() - private readonly messageGroupsPromises = new Map>() - - private readonly messageGroupCreationPromises = new Map>() - - private readonly retryOptions = { - maxRetries: 3, - isRetryable: () => true, - delayStrategy: { - getDelay: () => 1000 - } - } as const - - constructor (private readonly ctx: MeasureContext, private readonly workspace: WorkspaceUuid, private readonly metadata: Metadata) { - this.client = getWorkspaceClient(metadata.hulylakeUrl, workspace, generateToken(systemAccountUuid, workspace, undefined, metadata.secret)) - } - - public async findMessagesGroups (params: FindMessagesGroupParams): Promise { - const { cardId, fromDate, toDate, blobId, limit, order } = params - const groups = await this.getAllMessageGroups(cardId) - - if (order === SortingOrder.Ascending) { - groups.sort((a, b) => a.fromDate.getTime() - b.fromDate.getTime()) - } else if (order === SortingOrder.Descending) { - groups.sort((a, b) => b.fromDate.getTime() - a.fromDate.getTime()) - } - - if (fromDate == null && toDate == null && blobId == null && limit == null) { - return groups - } - - const result: MessagesGroup[] = [] - for (const group of groups) { - if (blobId != null && group.blobId !== blobId) continue - if (fromDate != null && !matchDate(group.fromDate, fromDate)) continue - if (toDate != null && !matchDate(group.toDate, toDate)) continue - - result.push(group) - - if (limit != null && result.length >= limit) break - } - - return result - } - - private async getAllMessageGroups (cardId: CardID): Promise { - const createPromise = this.messageGroupCreationPromises.get(cardId) - - if (createPromise != null) { - await createPromise - } - - if (this.messageGroupsByCardId.has(cardId)) { - return (this.messageGroupsByCardId.get(cardId) ?? []).sort((a, b) => a.fromDate.getTime() - b.fromDate.getTime()) - } - - const existingPromise = this.messageGroupsPromises.get(cardId) - if (existingPromise != null) return await existingPromise - - const promise = (async () => { - try { - const res = await this.client.getJson(`${cardId}/messages/groups`, this.retryOptions) - if (res.status === 404) { - await this.createMessagesGroupBlob(cardId) - this.messageGroupsByCardId.set(cardId, []) - return [] - } - - if (cardId === LOG_CARD_ID) { - this.ctx.info('Received groups', { groups: JSON.stringify(res.body ?? {}, undefined, 2) }) - } - - const groups = Object.values(res.body ?? {}).map(it => this.deserializeMessageGroup(it)).sort((a, b) => a.fromDate.getTime() - b.fromDate.getTime()) - this.messageGroupsByCardId.set(cardId, groups) - return groups - } finally { - this.messageGroupsPromises.delete(cardId) - } - })() - - this.messageGroupsPromises.set(cardId, promise) - return await promise - } - - public async getMessageGroupByDate (cardId: CardID, date: Date, create = true): Promise { - const all = await this.getAllMessageGroups(cardId) - if (cardId === LOG_CARD_ID) { - this.ctx.info('all groups sorted', { cardId, sortedGroups: JSON.stringify(all, undefined, 2) }) - } - const ts = date.getTime() - - const match = all.find(g => g.fromDate.getTime() <= ts && g.toDate.getTime() >= ts) - if (match != null) { - if (cardId === LOG_CARD_ID) { - this.ctx.info('math group', { date, match: JSON.stringify(match, undefined, 2) }) - } - return match - } - - const lastGroup = all[all.length - 1] - if (lastGroup != null && lastGroup.fromDate.getTime() <= ts && lastGroup.count < this.metadata.messagesPerBlob) { - if (cardId === LOG_CARD_ID) { - this.ctx.info('last group', { date, match: JSON.stringify(match, undefined, 2) }) - } - return lastGroup - } - - const firstGroup = all[0] - if (firstGroup != null && firstGroup.fromDate.getTime() >= ts && firstGroup.count < this.metadata.messagesPerBlob) { - if (cardId === LOG_CARD_ID) { - this.ctx.info('first group', { date, match: JSON.stringify(match, undefined, 2) }) - } - return firstGroup - } - - if (create) return await this.createMessageGroup(cardId, date) - - return undefined - } - - private async createMessagesGroupBlob (cardId: CardID): Promise { - await this.client.putJson(`${cardId}/messages/groups`, {}, undefined, this.retryOptions) - } - - private async incrementMessagesCount (cardId: CardID, blobId: BlobID, toDate?: Date, fromDate?: Date): Promise { - const groups = await this.getAllMessageGroups(cardId) - const group = groups.find((g) => g.blobId === blobId) - - if (group == null) return - - this.messageGroupsByCardId.set(cardId, groups.map((g) => g.blobId === blobId ? ({ ...g, count: g.count + 1, toDate: toDate ?? group.toDate, fromDate: fromDate ?? group.fromDate }) : g)) - - const patches: JsonPatch[] = [ - { - hop: 'inc', - path: `/${blobId}/count`, - value: 1 - }, - ...toDate != null - ? [{ - op: 'replace', - path: `/${blobId}/toDate`, - value: toDate - } as const] - : [], - ...fromDate != null - ? [{ - hop: 'add', - path: `/${blobId}/fromDate`, - value: fromDate - } as const] - : [] - ] - await this.client.patchJson(`${cardId}/messages/groups`, patches, undefined, this.retryOptions) - } - - private async decrementMessagesCount (cardId: CardID, blobId: BlobID): Promise { - const groups = await this.getAllMessageGroups(cardId) - const group = groups.find((g) => g.blobId === blobId) - - if (group == null) return - - const count = group.count - 1 - group.count = count - this.messageGroupsByCardId.set(cardId, groups.map((g) => g.blobId === blobId ? ({ ...g, count }) : g)) - - const patches: JsonPatch[] = [ - { - hop: 'inc', - path: `/${blobId}/count`, - value: -1 - } - ] - await this.client.patchJson(`${cardId}/messages/groups`, patches, undefined, this.retryOptions) - } - - private async createMessageGroup (cardId: CardID, date: Date): Promise { - const createPromise = this.messageGroupCreationPromises.get(cardId) - - if (createPromise != null) { - await createPromise - const group = await this.getMessageGroupByDate(cardId, date, false) - if (group != null) return group - } - - const promise = (async () => { - try { - const groupDoc: MessagesGroupDoc = { - cardId, - blobId: uuid() as BlobID, - fromDate: date.toISOString(), - toDate: date.toISOString(), - count: 0 - } - const patches: JsonPatch[] = [ - { - hop: 'add', - path: `/${groupDoc.blobId}`, - value: groupDoc, - safe: true - } - ] - - await this.client.patchJson(`${cardId}/messages/groups`, patches, undefined, this.retryOptions) - const group = this.deserializeMessageGroup(groupDoc) - if (this.messageGroupsByCardId.has(cardId)) { - this.messageGroupsByCardId.set(cardId, - [...this.messageGroupsByCardId.get(cardId) ?? [], group].sort((a, b) => a.fromDate.getTime() - b.fromDate.getTime()) - ) - } else { - this.messageGroupsByCardId.set(cardId, [group]) - } - await this.createMessagesBlob(cardId, groupDoc.blobId, date, date) - - return group - } finally { - this.messageGroupCreationPromises.delete(cardId) - } - })() - - this.messageGroupCreationPromises.set(cardId, promise) - return await promise - } - - private async createMessagesBlob (cardId: CardID, blobId: BlobID, from: Date, to: Date): Promise { - const initialJson: MessagesDoc = { - cardId, - fromDate: from.toISOString(), - toDate: to.toISOString(), - language: 'original', - messages: {} - } - - await this.client.putJson(`${cardId}/messages/${blobId}`, initialJson, undefined, this.retryOptions) - } - - async insertMessage (cardId: CardID, group: MessagesGroup, message: Message): Promise { - if (cardId === LOG_CARD_ID && group.blobId === 'ad77d5d3-a073-4a14-960b-2f46e844bb6d"') { - this.ctx.error('SELECT WRONG GROUP!', { cardId, group, message, groups: this.messageGroupsByCardId.get(cardId) }) - throw new Error('Select wrong group') - } - const updateToDate = message.created.getTime() > group.toDate.getTime() - const updateFromDate = message.created.getTime() < group.fromDate.getTime() - - const serializedMessage = this.serializeMessage(message) - const patches: JsonPatch[] = [ - { - hop: 'add', - path: `/messages/${message.id}`, - value: serializedMessage, - safe: true - }, - ...(updateToDate - ? [ - { - op: 'replace', - path: '/toDate', - value: message.created - } as const - ] - : []), - ...(updateFromDate - ? [ - { - hop: 'add', - path: '/fromDate', - value: message.created - } as const - ] - : []) - ] - await this.patchJson(cardId, group.blobId, patches) - void this.incrementMessagesCount(cardId, group.blobId, updateToDate ? message.created : undefined, updateFromDate ? message.created : undefined) - } - - async updateMessage (cardId: CardID, blobId: BlobID, messageId: MessageID, update: { - language?: string - content?: Markdown - extra?: MessageExtra - }, date: Date): Promise { - const patches: JsonPatch[] = [] - - if (update.content != null) { - patches.push({ - op: 'replace', - path: `/messages/${messageId}/content`, - value: update.content - }) - } - - if (update.extra != null) { - patches.push({ - op: 'replace', - path: `/messages/${messageId}/extra`, - value: update.extra - }) - } - - if (update.language != null) { - patches.push({ - op: 'replace', - path: `/messages/${messageId}/language`, - value: update.language - }) - } - - if (patches.length === 0) return - - if (update.content != null || update.extra != null) { - patches.push({ - op: 'replace', - path: `/messages/${messageId}/modified`, - value: date - }) - } - - await this.patchJson(cardId, blobId, patches) - } - - async removeMessage (cardId: CardID, blobId: BlobID, messageId: MessageID): Promise { - const patches: JsonPatch[] = [ - { - hop: 'remove', - path: `/messages/${messageId}`, - safe: true - } as const - ] - - await this.patchJson(cardId, blobId, patches) - void this.decrementMessagesCount(cardId, blobId) - } - - async addReaction (cardId: CardID, blobId: BlobID, messageId: MessageID, emoji: string, person: PersonUuid, date: Date): Promise { - const patches: JsonPatch[] = [ - { - hop: 'add', - path: `/messages/${messageId}/reactions/${emoji}`, - value: {}, - safe: true - }, - { - hop: 'add', - path: `/messages/${messageId}/reactions/${emoji}/${person}`, - value: { - count: 1, - date - }, - safe: true - } - ] - await this.patchJson(cardId, blobId, patches) - } - - async removeReaction (cardId: CardID, blobId: BlobID, messageId: MessageID, emoji: string, person: PersonUuid): Promise { - const patches: JsonPatch[] = [ - { - hop: 'remove', - path: `/messages/${messageId}/reactions/${emoji}/${person}`, - safe: true - } - ] - await this.patchJson(cardId, blobId, patches) - } - - async addAttachments (cardId: CardID, blobId: BlobID, messageId: MessageID, attachments: Attachment[]): Promise { - const patches: JsonPatch[] = [] - - for (const attachment of attachments) { - patches.push({ - op: 'add', - path: `/messages/${messageId}/attachments/${attachment.id}`, - value: attachment - }) - } - await this.patchJson(cardId, blobId, patches) - } - - async removeAttachments (cardId: CardID, blobId: BlobID, messageId: MessageID, attachmentIds: AttachmentID[]): Promise { - const patches: JsonPatch[] = [] - - for (const attachmentId of attachmentIds) { - patches.push({ - hop: 'remove', - path: `/messages/${messageId}/attachments/${attachmentId}`, - safe: true - }) - } - await this.patchJson(cardId, blobId, patches) - } - - async setAttachments (cardId: CardID, blobId: BlobID, messageId: MessageID, attachments: Attachment[]): Promise { - const patches: JsonPatch[] = [{ - op: 'replace', - path: `/messages/${messageId}/attachments`, - value: {} - }] - - for (const attachment of attachments) { - patches.push({ - op: 'add', - path: `/messages/${messageId}/attachments/${attachment.id}`, - value: attachment - }) - } - await this.patchJson(cardId, blobId, patches) - } - - async updateAttachments (cardId: CardID, blobId: BlobID, messageId: MessageID, updates: AttachmentUpdateData[], date: Date): Promise { - const patches: JsonPatch[] = [] - for (const update of updates) { - const keys = Object.keys(update.params) - if (keys.length === 0) continue - for (const key of keys) { - patches.push({ - op: 'add', - path: `/messages/${messageId}/attachments/${update.id}/params/${key}`, - value: update.params[key] - }) - } - patches.push({ - op: 'add', - path: `/messages/${messageId}/attachments/${update.id}/modified`, - value: date.toISOString() - }) - } - - await this.patchJson(cardId, blobId, patches) - } - - async attachThread (cardId: CardID, blobId: BlobID, messageId: MessageID, thread: Thread): Promise { - const patches: JsonPatch[] = [ - { - op: 'add', - path: `/messages/${messageId}/threads/${thread.threadId}`, - value: thread - } - ] - await this.patchJson(cardId, blobId, patches) - } - - async updateThread (cardId: CardID, blobId: BlobID, messageId: MessageID, threadId: CardID, update: { threadType: CardType }): Promise { - const patches: JsonPatch[] = [ - { - op: 'add', - path: `/messages/${messageId}/threads/${threadId}/threadType`, - value: update.threadType - } - ] - await this.patchJson(cardId, blobId, patches) - } - - async addThreadReply (cardId: CardID, blobId: BlobID, messageId: MessageID, threadId: CardID, person: PersonUuid, date: Date): Promise { - const patches: JsonPatch[] = - [ - { - hop: 'inc', - path: `/messages/${messageId}/threads/${threadId}/repliesCount`, - value: 1 - }, - { - op: 'add', - path: `/messages/${messageId}/threads/${threadId}/lastReply`, - value: date - }, - { - hop: 'inc', - path: `/messages/${messageId}/threads/${threadId}/repliedPersons/${person}`, - value: 1 - } - ] - - await this.patchJson(cardId, blobId, patches) - } - - async removeThreadReply (cardId: CardID, blobId: BlobID, messageId: MessageID, threadId: CardID, person: PersonUuid): Promise { - const patches: JsonPatch[] = - [ - { - hop: 'inc', - path: `/messages/${messageId}/threads/${threadId}/repliesCount`, - value: -1 - }, - { - hop: 'inc', - path: `/messages/${messageId}/threads/${threadId}/repliedPersons/${person}`, - value: -1 - } - ] - - await this.patchJson(cardId, blobId, patches) - } - - async removeThread (cardId: CardID, blobId: BlobID, messageId: MessageID, threadId: CardID): Promise { - const patches: JsonPatch[] = [ - { - hop: 'remove', - path: `/messages/${messageId}/threads/${threadId}`, - safe: true - } - ] - await this.patchJson(cardId, blobId, patches) - } - - private async patchJson (cardId: CardID, blobId: BlobID, patches: JsonPatch[]): Promise { - await this.client.patchJson(`${cardId}/messages/${blobId}`, patches, undefined, this.retryOptions) - } - - private deserializeMessageGroup (group: MessagesGroupDoc): MessagesGroup { - return { - cardId: group.cardId, - blobId: group.blobId, - fromDate: new Date(group.fromDate), - toDate: new Date(group.toDate), - count: group.count - } - } - - private serializeMessage (message: Message): MessageDoc { - return { - ...message, - language: message.language ?? null, - extra: message.extra ?? {}, - created: message.created.toISOString(), - modified: message.modified?.toISOString() ?? null, - reactions: {}, - attachments: {}, - threads: {} - } - } -} - -function matchDate (date: Date, filter: Partial> | Date): boolean { - const ts = date.getTime() - if (filter instanceof Date) return ts === filter.getTime() - - if (filter.greater != null && !(ts > filter.greater.getTime())) return false - if (filter.greaterOrEqual != null && !(ts >= filter.greaterOrEqual.getTime())) return false - if (filter.less != null && !(ts < filter.less.getTime())) return false - if (filter.lessOrEqual != null && !(ts <= filter.lessOrEqual.getTime())) return false - if (filter.notEqual != null && !(ts !== filter.notEqual.getTime())) return false - - return true -} diff --git a/foundations/communication/packages/server/src/client.ts b/foundations/communication/packages/server/src/client.ts deleted file mode 100644 index ce2ab8795b..0000000000 --- a/foundations/communication/packages/server/src/client.ts +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. - -import { DbAdapter } from '@hcengineering/communication-sdk-types' -import type { - CardID, FindMessagesOptions, - Message, - MessageID, - MessageMeta, - PersonUuid, - SocialID, - WorkspaceUuid -} from '@hcengineering/communication-types' -import { generateToken } from '@hcengineering/server-token' -import { Account, MeasureContext, systemAccountUuid } from '@hcengineering/core' -import { getClient as getAccountClient } from '@hcengineering/account-client' -import { loadMessages } from '@hcengineering/communication-shared' - -import { Blob } from './blob' -import type { Metadata } from './types' -import { getWorkspaceClient, HulylakeWorkspaceClient } from '@hcengineering/hulylake-client' - -export class LowLevelClient { - private readonly messageMetaCache = new Map() - private readonly personUuidBySocialIdCache = new Map() - private readonly lake: HulylakeWorkspaceClient - - constructor ( - readonly db: DbAdapter, - readonly blob: Blob, - private readonly metadata: Metadata, - private readonly workspace: WorkspaceUuid - ) { - this.lake = getWorkspaceClient(metadata.hulylakeUrl, workspace, generateToken(systemAccountUuid, workspace, undefined, metadata.secret)) - } - - async findMessage (cardId: CardID, messageId: MessageID, options?: FindMessagesOptions): Promise { - const meta = await this.getMessageMeta(cardId, messageId) - if (meta === undefined) { - return undefined - } - - return (await loadMessages(this.lake, meta.blobId, { - cardId, - id: messageId - }, options))[0] - } - - async findPersonUuid (ctx: { - ctx: MeasureContext - account: Account - }, socialId: SocialID, requireAccount: boolean = false): Promise { - if (ctx.account.socialIds.includes(socialId)) { - return ctx.account.uuid - } - const cached = this.personUuidBySocialIdCache.get(socialId) - if (cached !== undefined) { - return cached - } - - const url = this.metadata.accountsUrl ?? '' - if (url === '') return undefined - - const token = generateToken(systemAccountUuid, this.workspace, undefined, this.metadata.secret) - const accountClient = getAccountClient(this.metadata.accountsUrl, token) - - try { - const personUuid = await accountClient.findPersonBySocialId(socialId, requireAccount) - - if (personUuid !== undefined) { - this.personUuidBySocialIdCache.set(socialId, personUuid) - } - - return personUuid - } catch (err: any) { - ctx.ctx.warn('Cannot find person uuid', { socialString: socialId, err }) - } - } - - async getMessageMeta (cardId: CardID, messageId: MessageID): Promise { - const key = this.getMessageMetaKey(cardId, messageId) - if (this.messageMetaCache.has(key)) { - return this.messageMetaCache.get(key) - } - const meta = (await this.db.findMessagesMeta({ cardId, id: messageId }))[0] - if (meta === undefined) { - return undefined - } - this.messageMetaCache.set(key, meta) - return meta - } - - private getMessageMetaKey (cardId: CardID, messageId: MessageID): string { - return `${cardId}-${messageId}` - } - - async removeMessageMeta (cardId: CardID, messageId: MessageID): Promise { - await this.db.removeMessageMeta(cardId, messageId) - const key = this.getMessageMetaKey(cardId, messageId) - this.messageMetaCache.delete(key) - } -} diff --git a/foundations/communication/packages/server/src/error.ts b/foundations/communication/packages/server/src/error.ts deleted file mode 100644 index dea62e599b..0000000000 --- a/foundations/communication/packages/server/src/error.ts +++ /dev/null @@ -1,49 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export class ApiError extends Error { - public code: number - public message: string - - private constructor (code: number, message: string) { - super(message) - this.code = code - this.message = message - Object.setPrototypeOf(this, ApiError.prototype) - } - - static badRequest (message: string): ApiError { - return new ApiError(400, `Bad Request: ${message}`) - } - - static forbidden (message: string): ApiError { - return new ApiError(403, `Forbidden: ${message}`) - } - - static notFound (message: string): ApiError { - return new ApiError(404, `Not Found: ${message}`) - } - - toJSON (): object { - return { - code: this.code, - message: this.message - } - } - - toString (): string { - return JSON.stringify(this.toJSON()) - } -} diff --git a/foundations/communication/packages/server/src/index.ts b/foundations/communication/packages/server/src/index.ts deleted file mode 100644 index 6d0a45662d..0000000000 --- a/foundations/communication/packages/server/src/index.ts +++ /dev/null @@ -1,118 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { type MeasureContext } from '@hcengineering/core' -import type { - FindNotificationContextParams, - FindNotificationsParams, - NotificationContext, - WorkspaceUuid, - Notification, - FindLabelsParams, - Label, - FindCollaboratorsParams, - Collaborator, - FindPeersParams, - Peer, - CardID, FindMessagesMetaParams, MessageMeta, FindMessagesGroupParams, MessagesGroup -} from '@hcengineering/communication-types' -import { createDbAdapter } from '@hcengineering/communication-cockroach' -import type { EventResult, Event, ServerApi, SessionData } from '@hcengineering/communication-sdk-types' - -import { getMetadata } from './metadata' -import type { CommunicationCallbacks, Subscription } from './types' -import { buildMiddlewares, Middlewares } from './middlewares' -import { Blob } from './blob' -import { LowLevelClient } from './client' - -export class Api implements ServerApi { - private constructor ( - private readonly ctx: MeasureContext, - private readonly middlewares: Middlewares - ) {} - - static async create ( - ctx: MeasureContext, - workspace: WorkspaceUuid, - dbUrl: string, - callbacks: CommunicationCallbacks - ): Promise { - const metadata = getMetadata() - const db = await createDbAdapter(dbUrl, workspace, ctx, { - withLogs: process.env.COMMUNICATION_TIME_LOGGING_ENABLED === 'true' - }) - const blob = new Blob(ctx, workspace, metadata) - const client: LowLevelClient = new LowLevelClient(db, blob, metadata, workspace) - const middleware = await buildMiddlewares(ctx, workspace, metadata, client, callbacks) - - return new Api(ctx, middleware) - } - - async findMessagesMeta (session: SessionData, params: FindMessagesMetaParams): Promise { - return await this.middlewares.findMessagesMeta(session, params) - } - - async findMessagesGroups (session: SessionData, params: FindMessagesGroupParams): Promise { - return await this.middlewares.findMessagesGroups(session, params) - } - - async findNotificationContexts ( - session: SessionData, - params: FindNotificationContextParams, - subscription?: Subscription - ): Promise { - return await this.middlewares.findNotificationContexts(session, params, subscription) - } - - async findNotifications ( - session: SessionData, - params: FindNotificationsParams, - subscription?: Subscription - ): Promise { - return await this.middlewares.findNotifications(session, params, subscription) - } - - async findLabels (session: SessionData, params: FindLabelsParams): Promise { - return await this.middlewares.findLabels(session, params) - } - - async findCollaborators (session: SessionData, params: FindCollaboratorsParams): Promise { - return await this.middlewares.findCollaborators(session, params) - } - - async findPeers (session: SessionData, params: FindPeersParams): Promise { - return await this.middlewares.findPeers(session, params) - } - - subscribeCard (session: SessionData, cardId: CardID, subscription: Subscription): void { - this.middlewares.subscribeCard(session, cardId, subscription) - } - - unsubscribeCard (session: SessionData, cardId: CardID, subscription: Subscription): void { - this.middlewares.unsubscribeCard(session, cardId, subscription) - } - - async event (session: SessionData, event: Event): Promise { - return await this.middlewares.event(session, event) - } - - async closeSession (sessionId: string): Promise { - await this.middlewares.closeSession(sessionId) - } - - async close (): Promise { - await this.middlewares.close() - } -} diff --git a/foundations/communication/packages/server/src/messageId.ts b/foundations/communication/packages/server/src/messageId.ts deleted file mode 100644 index 8cc6fb7a6f..0000000000 --- a/foundations/communication/packages/server/src/messageId.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. - -import { MessageID } from '@hcengineering/communication-types' - -const EPOCH_OFFSET_US = BigInt(Date.UTC(2022, 0, 1)) * 1000n -const monoStartNs = process.hrtime.bigint() -const realStartUs = BigInt(Date.now()) * 1000n -let lastTick = 0n - -function getMonotonicTick10us (): bigint { - const nowNs = process.hrtime.bigint() - const deltaUs = (nowNs - monoStartNs) / 1000n - const absUs = realStartUs + deltaUs - const relUs = absUs > EPOCH_OFFSET_US ? absUs - EPOCH_OFFSET_US : 0n - const candidate = relUs / 10n - const tick = candidate <= lastTick ? lastTick + 1n : candidate - lastTick = tick - return tick & ((1n << 64n) - 1n) -} - -export function generateMessageId (): MessageID { - return getMonotonicTick10us().toString() as MessageID -} diff --git a/foundations/communication/packages/server/src/metadata.ts b/foundations/communication/packages/server/src/metadata.ts deleted file mode 100644 index 09f8c73ae5..0000000000 --- a/foundations/communication/packages/server/src/metadata.ts +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { Metadata } from './types' - -export function getMetadata (): Metadata { - return { - accountsUrl: process.env.ACCOUNTS_URL ?? '', - secret: process.env.SERVER_SECRET ?? 'secret', - hulylakeUrl: process.env.HULYLAKE_URL ?? 'http://huly.local:8096', - messagesPerBlob: Number(process.env.MESSAGES_PER_BLOB ?? '200') - } -} diff --git a/foundations/communication/packages/server/src/middleware/base.ts b/foundations/communication/packages/server/src/middleware/base.ts deleted file mode 100644 index 806a9b8ec5..0000000000 --- a/foundations/communication/packages/server/src/middleware/base.ts +++ /dev/null @@ -1,184 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { type Event, EventResult, type SessionData } from '@hcengineering/communication-sdk-types' -import type { - FindNotificationContextParams, - FindNotificationsParams, - NotificationContext, - Notification, - FindLabelsParams, - Label, - FindCollaboratorsParams, - Collaborator, - FindPeersParams, - Peer, - CardID, - FindMessagesMetaParams, - MessageMeta, - FindMessagesGroupParams, - MessagesGroup -} from '@hcengineering/communication-types' - -import type { Enriched, Middleware, MiddlewareContext, Subscription } from '../types' - -export class BaseMiddleware implements Middleware { - constructor ( - readonly context: MiddlewareContext, - protected readonly next?: Middleware - ) {} - - async findMessagesMeta (session: SessionData, params: FindMessagesMetaParams): Promise { - return await this.provideFindMessagesMeta(session, params) - } - - async findMessagesGroups (session: SessionData, params: FindMessagesGroupParams): Promise { - return await this.provideFindMessagesGroups(session, params) - } - - async findNotificationContexts ( - session: SessionData, - params: FindNotificationContextParams, - subscription?: Subscription - ): Promise { - return await this.provideFindNotificationContexts(session, params, subscription) - } - - async findNotifications ( - session: SessionData, - params: FindNotificationsParams, - subscription?: Subscription - ): Promise { - return await this.provideFindNotifications(session, params, subscription) - } - - async findLabels (session: SessionData, params: FindLabelsParams, subscription?: Subscription): Promise { - return await this.provideFindLabels(session, params, subscription) - } - - async findCollaborators (session: SessionData, params: FindCollaboratorsParams): Promise { - return await this.provideFindCollaborators(session, params) - } - - async findPeers (session: SessionData, params: FindPeersParams): Promise { - return await this.provideFindPeers(session, params) - } - - async event (session: SessionData, event: Enriched, derived: boolean): Promise { - return await this.provideEvent(session, event, derived) - } - - handleBroadcast (session: SessionData, events: Enriched[]): void { - this.provideHandleBroadcast(session, events) - } - - subscribeCard (session: SessionData, cardId: CardID, subscription: Subscription): void { - if (this.next !== undefined) { - this.next.subscribeCard(session, cardId, subscription) - } - } - - unsubscribeCard (session: SessionData, cardId: CardID, subscription: Subscription): void { - if (this.next !== undefined) { - this.next.unsubscribeCard(session, cardId, subscription) - } - } - - close (): void {} - closeSession (sessionId: string): void {} - - protected async provideEvent (session: SessionData, event: Enriched, derived: boolean): Promise { - if (this.next !== undefined) { - return await this.next.event(session, event, derived) - } - return {} - } - - protected async provideFindMessagesGroups ( - session: SessionData, - params: FindMessagesGroupParams - ): Promise { - if (this.next !== undefined) { - return await this.next.findMessagesGroups(session, params) - } - return [] - } - - protected async provideFindMessagesMeta ( - session: SessionData, - params: FindMessagesMetaParams - ): Promise { - if (this.next !== undefined) { - return await this.next.findMessagesMeta(session, params) - } - return [] - } - - protected async provideFindNotificationContexts ( - session: SessionData, - params: FindNotificationContextParams, - subscription?: Subscription - ): Promise { - if (this.next !== undefined) { - return await this.next.findNotificationContexts(session, params, subscription) - } - return [] - } - - protected async provideFindNotifications ( - session: SessionData, - params: FindNotificationsParams, - subscription?: Subscription - ): Promise { - if (this.next !== undefined) { - return await this.next.findNotifications(session, params, subscription) - } - return [] - } - - protected async provideFindLabels ( - session: SessionData, - params: FindLabelsParams, - subscription?: Subscription - ): Promise { - if (this.next !== undefined) { - return await this.next.findLabels(session, params, subscription) - } - return [] - } - - protected async provideFindCollaborators ( - session: SessionData, - params: FindCollaboratorsParams - ): Promise { - if (this.next !== undefined) { - return await this.next.findCollaborators(session, params) - } - return [] - } - - protected async provideFindPeers (session: SessionData, params: FindPeersParams): Promise { - if (this.next !== undefined) { - return await this.next.findPeers(session, params) - } - return [] - } - - protected provideHandleBroadcast (session: SessionData, events: Enriched[]): void { - if (this.next !== undefined) { - this.next.handleBroadcast(session, events) - } - } -} diff --git a/foundations/communication/packages/server/src/middleware/broadcast.ts b/foundations/communication/packages/server/src/middleware/broadcast.ts deleted file mode 100644 index c68c698604..0000000000 --- a/foundations/communication/packages/server/src/middleware/broadcast.ts +++ /dev/null @@ -1,199 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - CardEventType, - type Event, - EventResult, - LabelEventType, - MessageEventType, - NotificationEventType, - PeerEventType, - type SessionData -} from '@hcengineering/communication-sdk-types' -import type { - AccountUuid, - CardID, - FindLabelsParams, - FindNotificationContextParams, - FindNotificationsParams, - Label, - Notification, - NotificationContext -} from '@hcengineering/communication-types' - -import type { CommunicationCallbacks, Enriched, Middleware, MiddlewareContext, Subscription } from '../types' -import { BaseMiddleware } from './base' - -interface SessionInfo { - account: AccountUuid - subscriptions: Map> -} - -export class BroadcastMiddleware extends BaseMiddleware implements Middleware { - private readonly dataBySessionId = new Map() - - constructor ( - private readonly callbacks: CommunicationCallbacks, - readonly context: MiddlewareContext, - next?: Middleware - ) { - super(context, next) - } - - async findNotificationContexts ( - session: SessionData, - params: FindNotificationContextParams, - subscription?: Subscription - ): Promise { - this.createSession(session) - - const result = await this.provideFindNotificationContexts(session, params, subscription) - if (subscription != null && session.sessionId != null && session.sessionId !== '') { - this.subscribeContextsCard(session, subscription, result) - } - return result - } - - async findNotifications ( - session: SessionData, - params: FindNotificationsParams, - queryId?: Subscription - ): Promise { - this.createSession(session) - return await this.provideFindNotifications(session, params, queryId) - } - - async findLabels (session: SessionData, params: FindLabelsParams, queryId?: Subscription): Promise { - this.createSession(session) - return await this.provideFindLabels(session, params, queryId) - } - - async event (session: SessionData, event: Enriched, derived: boolean): Promise { - this.createSession(session) - return await this.provideEvent(session, event, derived) - } - - unsubscribeCard (session: SessionData, cardId: CardID, subscription: Subscription): void { - if (session.sessionId == null) return - const data = this.dataBySessionId.get(session.sessionId) - if (data == null) return - - const current = data.subscriptions.get(cardId) - if (current == null) return - current.delete(subscription) - - data.subscriptions.set(cardId, current) - } - - subscribeCard (session: SessionData, cardId: CardID, subscription: string | number): void { - if (session.sessionId == null) return - const data = this.dataBySessionId.get(session.sessionId) - if (data == null) return - - const current = data.subscriptions.get(cardId) ?? new Set() - current.add(subscription) - data.subscriptions.set(cardId, current) - } - - handleBroadcast (session: SessionData, events: Enriched[]): void { - if (events.length === 0) return - const sessionIds: Record[]> = {} - - for (const [sessionId, session] of this.dataBySessionId.entries()) { - sessionIds[sessionId] = events.filter((it) => this.match(it, session)) - } - - const ctx = this.context.ctx.newChild('enqueue', {}) - ctx.contextData = session.contextData - - if (Object.keys(sessionIds).length > 0) { - try { - this.callbacks.broadcast(ctx, sessionIds) - } catch (e) { - this.context.ctx.error('Failed to broadcast event', { error: e }) - } - } - - try { - this.callbacks.enqueue(ctx, events) - } catch (e) { - this.context.ctx.error('Failed to broadcast event', { error: e }) - } - } - - closeSession (sessionId: string): void { - this.dataBySessionId.delete(sessionId) - } - - close (): void { - this.dataBySessionId.clear() - } - - private subscribeContextsCard (session: SessionData, queryId: Subscription, result: NotificationContext[]): void { - const data = this.createSession(session) - if (data == null) return - - for (const context of result) { - this.subscribeCard(session, context.cardId, queryId) - } - } - - private createSession (session: SessionData): SessionInfo | undefined { - const id = session.sessionId - if (id == null) return - if (!this.dataBySessionId.has(id)) { - this.dataBySessionId.set(id, { - account: session.account.uuid, - subscriptions: new Map() - }) - } - - return this.dataBySessionId.get(id) - } - - private match (event: Enriched, info: SessionInfo): boolean { - switch (event.type) { - case MessageEventType.CreateMessage: - case MessageEventType.ThreadPatch: - case MessageEventType.ReactionPatch: - case MessageEventType.BlobPatch: - case MessageEventType.AttachmentPatch: - case MessageEventType.RemovePatch: - case MessageEventType.UpdatePatch: - case MessageEventType.TranslateMessage: - return info.subscriptions.has(event.cardId) - case NotificationEventType.RemoveNotifications: - case NotificationEventType.CreateNotification: - case NotificationEventType.UpdateNotification: - case NotificationEventType.RemoveNotificationContext: - case NotificationEventType.UpdateNotificationContext: - case NotificationEventType.CreateNotificationContext: - return info.account === event.account - case NotificationEventType.RemoveCollaborators: - case NotificationEventType.AddCollaborators: - return true - case LabelEventType.CreateLabel: - case LabelEventType.RemoveLabel: - return info.account === event.account - case CardEventType.UpdateCardType: - case CardEventType.RemoveCard: - return true - case PeerEventType.RemovePeer: - case PeerEventType.CreatePeer: - return false - } - } -} diff --git a/foundations/communication/packages/server/src/middleware/date.ts b/foundations/communication/packages/server/src/middleware/date.ts deleted file mode 100644 index ecf7f85c6d..0000000000 --- a/foundations/communication/packages/server/src/middleware/date.ts +++ /dev/null @@ -1,46 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { EventResult, type Event, type SessionData } from '@hcengineering/communication-sdk-types' -import { systemAccountUuid } from '@hcengineering/core' - -import type { Middleware, MiddlewareContext, Enriched } from '../types' -import { BaseMiddleware } from './base' - -export class DateMiddleware extends BaseMiddleware implements Middleware { - constructor ( - readonly context: MiddlewareContext, - next?: Middleware - ) { - super(context, next) - } - - async event (session: SessionData, event: Enriched, derived: boolean): Promise { - const canSetDate = derived || this.isSystem(session) - - if (!canSetDate || event.date == null) { - event.date = new Date() - } - - event._eventExtra = event._eventExtra ?? {} - - return await this.provideEvent(session, event, derived) - } - - private isSystem (session: SessionData): boolean { - const account = session.account - return systemAccountUuid === account.uuid - } -} diff --git a/foundations/communication/packages/server/src/middleware/id.ts b/foundations/communication/packages/server/src/middleware/id.ts deleted file mode 100644 index 137295f24a..0000000000 --- a/foundations/communication/packages/server/src/middleware/id.ts +++ /dev/null @@ -1,39 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { EventResult, MessageEventType, type Event, type SessionData } from '@hcengineering/communication-sdk-types' - -import { generateMessageId } from '../messageId' -import type { Middleware, MiddlewareContext, Enriched } from '../types' -import { BaseMiddleware } from './base' - -export class IdMiddleware extends BaseMiddleware implements Middleware { - constructor ( - readonly context: MiddlewareContext, - next?: Middleware - ) { - super(context, next) - } - - async event (session: SessionData, event: Enriched, derived: boolean): Promise { - if (event.type === MessageEventType.CreateMessage) { - if (event.messageId == null) { - event.messageId = generateMessageId() - } - } - - return await this.provideEvent(session, event, derived) - } -} diff --git a/foundations/communication/packages/server/src/middleware/indentity.ts b/foundations/communication/packages/server/src/middleware/indentity.ts deleted file mode 100644 index c8a0088fb6..0000000000 --- a/foundations/communication/packages/server/src/middleware/indentity.ts +++ /dev/null @@ -1,99 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { type Event, EventResult, MessageEventType, type SessionData } from '@hcengineering/communication-sdk-types' -import { systemAccountUuid } from '@hcengineering/core' -import type { - AccountUuid, - FindLabelsParams, - FindNotificationContextParams, - FindNotificationsParams, - Label, - Notification, - NotificationContext -} from '@hcengineering/communication-types' - -import type { Enriched, Middleware, MiddlewareContext, Subscription } from '../types' -import { BaseMiddleware } from './base' - -export class IdentityMiddleware extends BaseMiddleware implements Middleware { - constructor ( - readonly context: MiddlewareContext, - next?: Middleware - ) { - super(context, next) - } - - async event (session: SessionData, event: Enriched, derived: boolean): Promise { - switch (event.type) { - case MessageEventType.ThreadPatch: - case MessageEventType.ReactionPatch: { - const personUuid = await this.context.client.findPersonUuid( - { - ctx: this.context.ctx, - account: session.account - }, - event.socialId - ) - event.personUuid = personUuid - break - } - default: - break - } - - return await this.provideEvent(session, event, derived) - } - - async findNotificationContexts ( - session: SessionData, - params: FindNotificationContextParams, - subscription?: Subscription - ): Promise { - const paramsWithAccount = this.enrichParamsWithAccount(session, params) - return await this.provideFindNotificationContexts(session, paramsWithAccount, subscription) - } - - async findNotifications ( - session: SessionData, - params: FindNotificationsParams, - subscription?: Subscription - ): Promise { - const paramsWithAccount = this.enrichParamsWithAccount(session, params) - return await this.provideFindNotifications(session, paramsWithAccount, subscription) - } - - async findLabels (session: SessionData, params: FindLabelsParams, subscription?: Subscription): Promise { - const paramsWithAccount = this.enrichParamsWithAccount(session, params) - return await this.provideFindLabels(session, paramsWithAccount, subscription) - } - - private enrichParamsWithAccount( - session: SessionData, - params: T - ): T { - const account = session.account - const isSystem = account.uuid === systemAccountUuid - - if (isSystem) { - return params - } - - return { - ...params, - account: account.uuid - } - } -} diff --git a/foundations/communication/packages/server/src/middleware/peer.ts b/foundations/communication/packages/server/src/middleware/peer.ts deleted file mode 100644 index b9efe1777a..0000000000 --- a/foundations/communication/packages/server/src/middleware/peer.ts +++ /dev/null @@ -1,60 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - type Event, - EventResult, - MessageEventType, - PeerEventType, - type SessionData -} from '@hcengineering/communication-sdk-types' - -import type { Enriched, Middleware, MiddlewareContext } from '../types' -import { BaseMiddleware } from './base' - -export class PeerMiddleware extends BaseMiddleware implements Middleware { - constructor ( - readonly context: MiddlewareContext, - next?: Middleware - ) { - super(context, next) - } - - async event (session: SessionData, event: Enriched, derived: boolean): Promise { - switch (event.type) { - case PeerEventType.CreatePeer: - this.context.cadsWithPeers.add(event.cardId) - break - case MessageEventType.CreateMessage: - case MessageEventType.UpdatePatch: - case MessageEventType.RemovePatch: - case MessageEventType.AttachmentPatch: - case MessageEventType.ReactionPatch: - case MessageEventType.ThreadPatch: - case MessageEventType.BlobPatch: { - if (this.context.cadsWithPeers.has(event.cardId)) { - event._eventExtra.peers = - (await this.context.head?.findPeers(session, { - workspaceId: this.context.workspace, - cardId: event.cardId - })) ?? [] - } - break - } - } - - return await this.provideEvent(session, event, derived) - } -} diff --git a/foundations/communication/packages/server/src/middleware/permissions.ts b/foundations/communication/packages/server/src/middleware/permissions.ts deleted file mode 100644 index 0ccaa2dcba..0000000000 --- a/foundations/communication/packages/server/src/middleware/permissions.ts +++ /dev/null @@ -1,134 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - type Event, - EventResult, - MessageEventType, - NotificationEventType, - PeerEventType, - type SessionData -} from '@hcengineering/communication-sdk-types' -import { AccountRole, systemAccountUuid } from '@hcengineering/core' -import type { AccountUuid, CardID, MessageID, SocialID } from '@hcengineering/communication-types' - -import { ApiError } from '../error' -import type { Enriched, Middleware, MiddlewareContext } from '../types' -import { BaseMiddleware } from './base' - -export class PermissionsMiddleware extends BaseMiddleware implements Middleware { - constructor ( - readonly context: MiddlewareContext, - next?: Middleware - ) { - super(context, next) - } - - async event (session: SessionData, event: Enriched, derived: boolean): Promise { - if (derived) return await this.provideEvent(session, event, derived) - - this.notAnonymousAccount(session) - - if (this.isSystemAccount(session)) { - return await this.provideEvent(session, event, derived) - } - - switch (event.type) { - case MessageEventType.CreateMessage: - this.checkSocialId(session, event.socialId) - if (!this.isSystemAccount(session) && event?.options?.noNotify === true) { - event.options.noNotify = false - } - break - case MessageEventType.RemovePatch: - case MessageEventType.UpdatePatch: - case MessageEventType.BlobPatch: - case MessageEventType.AttachmentPatch: - this.checkSocialId(session, event.socialId) - await this.checkMessageAuthor(session, event.cardId, event.messageId) - break - case MessageEventType.ReactionPatch: - case MessageEventType.ThreadPatch: - case NotificationEventType.AddCollaborators: - case NotificationEventType.RemoveCollaborators: - this.checkSocialId(session, event.socialId) - break - case NotificationEventType.RemoveNotifications: - case NotificationEventType.UpdateNotificationContext: - case NotificationEventType.UpdateNotification: - case NotificationEventType.RemoveNotificationContext: { - this.checkAccount(session, event.account) - break - } - case MessageEventType.TranslateMessage: - case PeerEventType.CreatePeer: - case PeerEventType.RemovePeer: { - this.onlySystemAccount(session) - break - } - default: - break - } - - return await this.provideEvent(session, event, derived) - } - - private async checkMessageAuthor (session: SessionData, cardId: CardID, messageId: MessageID): Promise { - const meta = await this.context.client.getMessageMeta(cardId, messageId) - if (meta === undefined) { - throw ApiError.notFound(`message not found: cardId =${cardId}, messageId = ${messageId}`) - } - - if (!session.account.socialIds.includes(meta.creator)) { - throw ApiError.forbidden('message author is not allowed') - } - } - - private checkSocialId (session: SessionData, creator: SocialID): void { - const account = session.account - if (!account.socialIds.includes(creator) && systemAccountUuid !== account.uuid) { - throw ApiError.forbidden('social ID is not allowed') - } - } - - private checkAccount (session: SessionData, creator: AccountUuid): void { - const account = session.account - if (account.uuid !== creator && systemAccountUuid !== account.uuid) { - throw ApiError.forbidden('account is not allowed') - } - } - - private onlySystemAccount (session: SessionData): void { - if (!this.isSystemAccount(session)) { - throw ApiError.forbidden('only system account is allowed') - } - } - - private notAnonymousAccount (session: SessionData): void { - if (this.isAnonymousAccount(session)) { - throw ApiError.forbidden('anonymous account is not allowed') - } - } - - private isSystemAccount (session: SessionData): boolean { - const account = session.account - return systemAccountUuid === account.uuid - } - - private isAnonymousAccount (session: SessionData): boolean { - const account = session.account - return account.role === AccountRole.ReadOnlyGuest - } -} diff --git a/foundations/communication/packages/server/src/middleware/storage.ts b/foundations/communication/packages/server/src/middleware/storage.ts deleted file mode 100644 index 6a2c103db9..0000000000 --- a/foundations/communication/packages/server/src/middleware/storage.ts +++ /dev/null @@ -1,614 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - Attachment, - AttachmentID, - type Collaborator, - type FindCollaboratorsParams, - type FindLabelsParams, - type FindNotificationContextParams, - type FindNotificationsParams, - FindPeersParams, - FindThreadMetaParams, - type Label, - type Notification, - type NotificationContext, - Peer, - Thread, - ThreadMeta, - FindMessagesMetaParams, - MessageMeta, - FindMessagesGroupParams, - MessagesGroup -} from '@hcengineering/communication-types' -import { - type AddCollaboratorsEvent, - AttachmentPatchEvent, - BlobPatchEvent, - CardEventType, - type CreateLabelEvent, - type CreateMessageEvent, - CreateMessageResult, - type CreateNotificationContextEvent, - type CreateNotificationEvent, - CreatePeerEvent, - type DbAdapter, - type Event, - EventResult, - LabelEventType, - MessageEventType, - NotificationEventType, - PeerEventType, - ReactionPatchEvent, - type RemoveCollaboratorsEvent, - type RemoveLabelEvent, - type RemoveNotificationContextEvent, - type RemoveNotificationsEvent, - RemovePatchEvent, - RemovePeerEvent, - type SessionData, - ThreadPatchEvent, - type UpdateNotificationContextEvent, - type UpdateNotificationEvent, - UpdatePatchEvent -} from '@hcengineering/communication-sdk-types' -import { MessageProcessor } from '@hcengineering/communication-shared' -import { - AddAttachmentsOperation, - RemoveAttachmentsOperation, - SetAttachmentsOperation, - UpdateAttachmentsOperation -} from '@hcengineering/communication-sdk-types' - -import type { Enriched, Middleware, MiddlewareContext } from '../types' -import { BaseMiddleware } from './base' -import { Blob } from '../blob' - -interface Result { - skipPropagate?: boolean - result?: EventResult -} - -export class StorageMiddleware extends BaseMiddleware implements Middleware { - private readonly blob: Blob - private readonly db: DbAdapter - constructor ( - readonly context: MiddlewareContext, - next?: Middleware - ) { - super(context, next) - - this.blob = context.client.blob - this.db = context.client.db - } - - async findMessagesMeta (session: SessionData, params: FindMessagesMetaParams): Promise { - return await this.db.findMessagesMeta(params) - } - - async findMessagesGroups (session: SessionData, params: FindMessagesGroupParams): Promise { - if (params.id != null) { - const meta = await this.context.client.getMessageMeta(params.cardId, params.id) - if (meta == null) return [] - return await this.blob.findMessagesGroups({ - ...params, - blobId: params.blobId ?? meta.blobId - }) - } - return await this.blob.findMessagesGroups(params) - } - - async findNotificationContexts ( - _: SessionData, - params: FindNotificationContextParams - ): Promise { - return await this.db.findNotificationContexts(params) - } - - async findNotifications (_: SessionData, params: FindNotificationsParams): Promise { - return await this.db.findNotifications(params) - } - - async findLabels (_: SessionData, params: FindLabelsParams): Promise { - return await this.db.findLabels(params) - } - - async findCollaborators (_: SessionData, params: FindCollaboratorsParams): Promise { - return await this.db.findCollaborators(params) - } - - async findPeers (_: SessionData, params: FindPeersParams): Promise { - return await this.db.findPeers(params) - } - - async findThreadMeta (_: SessionData, params: FindThreadMetaParams): Promise { - return await this.db.findThreadMeta(params) - } - - async event (session: SessionData, event: Enriched, derived: boolean): Promise { - const result = await this.processEvent(session, event) - - if (result.skipPropagate === true) { - event.skipPropagate = true - } else { - await this.provideEvent(session, event, derived) - } - - return result.result ?? {} - } - - private async processEvent (session: SessionData, event: Enriched): Promise { - switch (event.type) { - // Messages - case MessageEventType.CreateMessage: - return await this.createMessage(event) - case MessageEventType.UpdatePatch: - return await this.updatePatch(event) - case MessageEventType.RemovePatch: - return await this.removePatch(event) - case MessageEventType.TranslateMessage: - return {} - - case MessageEventType.ReactionPatch: - return await this.reactionPatch(event, session) - case MessageEventType.BlobPatch: - return await this.blobPatch(event) - case MessageEventType.AttachmentPatch: - return await this.attachmentPatch(event) - case MessageEventType.ThreadPatch: - return await this.threadPatch(event, session) - - // Labels - case LabelEventType.CreateLabel: - return await this.createLabel(event) - case LabelEventType.RemoveLabel: - return await this.removeLabel(event) - - // Cards - case CardEventType.UpdateCardType: - case CardEventType.RemoveCard: - return {} - - // Peers - case PeerEventType.RemovePeer: - return await this.removePeer(event) - case PeerEventType.CreatePeer: - return await this.createPeer(event) - - // Collaborators - case NotificationEventType.AddCollaborators: - return await this.addCollaborators(event) - case NotificationEventType.RemoveCollaborators: - return await this.removeCollaborators(event) - - // Notifications - case NotificationEventType.CreateNotification: - return await this.createNotification(event) - case NotificationEventType.RemoveNotifications: - return await this.removeNotifications(event) - case NotificationEventType.UpdateNotification: - return await this.updateNotification(event) - - // Notification Contexts - case NotificationEventType.CreateNotificationContext: - return await this.createNotificationContext(event) - case NotificationEventType.RemoveNotificationContext: - return await this.removeNotificationContext(event) - case NotificationEventType.UpdateNotificationContext: - return await this.updateNotificationContext(event) - } - } - - private async addCollaborators (event: Enriched): Promise { - const added = await this.db.addCollaborators(event.cardId, event.cardType, event.collaborators, event.date) - - if (added.length === 0) return { skipPropagate: true } - event.collaborators = added - return {} - } - - private async removeCollaborators (event: Enriched): Promise { - if (event.collaborators.length === 0) return { skipPropagate: true } - await this.db.removeCollaborators({ cardId: event.cardId, account: event.collaborators }) - - return {} - } - - private async createMessage (event: Enriched): Promise { - if (event.messageId == null) { - throw new Error('Message id is required') - } - - const group = await this.blob.getMessageGroupByDate(event.cardId, event.date) - if (group == null) { - throw new Error( - `Cannot create message, group not found: cardId = ${event.cardId}, messageId = ${event.messageId}, created = ${event.date.toISOString()}` - ) - } - const result: CreateMessageResult = { - messageId: event.messageId, - created: event.date, - blobId: group.blobId - } - const created = await this.db.createMessageMeta( - event.cardId, - event.messageId, - event.socialId, - event.date, - group.blobId - ) - - if (!created) { - return { - skipPropagate: true, - result - } - } - await this.blob.insertMessage(event.cardId, group, MessageProcessor.create(event)) - - event._eventExtra.blobId = group.blobId - - return { - result - } - } - - private async updatePatch (event: Enriched): Promise { - const data = { - content: event.content, - extra: event.extra, - language: event.language - } - const meta = await this.context.client.getMessageMeta(event.cardId, event.messageId) - - if (meta === undefined) { - return { skipPropagate: true } - } - - await this.blob.updateMessage(event.cardId, meta.blobId, event.messageId, data, event.date) - event._eventExtra.blobId = meta.blobId - - return {} - } - - private async removePatch (event: Enriched): Promise { - const meta = await this.context.client.getMessageMeta(event.cardId, event.messageId) - - if (meta === undefined) { - return { skipPropagate: true } - } - await this.blob.removeMessage(event.cardId, meta.blobId, event.messageId) - await this.context.client.removeMessageMeta(event.cardId, event.messageId) - event._eventExtra.blobId = meta.blobId - return {} - } - - private async reactionPatch (event: Enriched, session: SessionData): Promise { - const meta = await this.context.client.getMessageMeta(event.cardId, event.messageId) - - if (meta === undefined) { - return { skipPropagate: true } - } - - const { operation, personUuid } = event - - if (personUuid === undefined) { - return { skipPropagate: true } - } - - if (operation.opcode === 'add') { - await this.blob.addReaction( - event.cardId, - meta.blobId, - event.messageId, - operation.reaction, - personUuid, - event.date - ) - } else if (operation.opcode === 'remove') { - await this.blob.removeReaction(event.cardId, meta.blobId, event.messageId, operation.reaction, personUuid) - } - - return {} - } - - private async blobPatch (event: Enriched): Promise { - const { operations } = event - - const attachmentOperations: ( - | AddAttachmentsOperation - | RemoveAttachmentsOperation - | SetAttachmentsOperation - | UpdateAttachmentsOperation - )[] = [] - - for (const operation of operations) { - if (operation.opcode === 'attach') { - attachmentOperations.push({ - opcode: 'add', - attachments: operation.blobs.map((b) => ({ - id: b.blobId as any as AttachmentID, - mimeType: b.mimeType, - params: b - })) - }) - } else if (operation.opcode === 'detach') { - attachmentOperations.push({ - opcode: 'remove', - ids: operation.blobIds as any as AttachmentID[] - }) - } else if (operation.opcode === 'set') { - attachmentOperations.push({ - opcode: 'set', - attachments: operation.blobs.map((b) => ({ - id: b.blobId as any as AttachmentID, - mimeType: b.mimeType, - params: b - })) - }) - } else if (operation.opcode === 'update') { - attachmentOperations.push({ - opcode: 'update', - attachments: operation.blobs.map((b) => ({ - id: b.blobId as any as AttachmentID, - params: { ...b } - })) - }) - } - } - - if (attachmentOperations.length === 0) { - return { skipPropagate: true } - } - - await this.attachmentPatch({ - _id: event._id, - type: MessageEventType.AttachmentPatch, - cardId: event.cardId, - messageId: event.messageId, - operations: attachmentOperations, - socialId: event.socialId, - date: event.date, - _eventExtra: event._eventExtra - }) - - return {} - } - - private async attachmentPatch (event: Enriched): Promise { - const meta = await this.context.client.getMessageMeta(event.cardId, event.messageId) - if (meta === undefined) { - return { skipPropagate: true } - } - - const { operations } = event - - for (const operation of operations) { - if (operation.opcode === 'add') { - const attachments: Attachment[] = operation.attachments.map( - (it) => - ({ - ...it, - created: event.date, - creator: event.socialId - }) as any - ) - await this.blob.addAttachments(event.cardId, meta.blobId, event.messageId, attachments) - } else if (operation.opcode === 'remove') { - await this.blob.removeAttachments(event.cardId, meta.blobId, event.messageId, operation.ids) - } else if (operation.opcode === 'set') { - const attachments: Attachment[] = operation.attachments.map( - (it) => - ({ - ...it, - created: event.date, - creator: event.socialId - }) as any - ) - await this.blob.setAttachments(event.cardId, meta.blobId, event.messageId, attachments) - } else if (operation.opcode === 'update') { - await this.blob.updateAttachments(event.cardId, meta.blobId, event.messageId, operation.attachments, event.date) - } - } - - return {} - } - - private async threadPatch (event: Enriched, session: SessionData): Promise { - const meta = await this.context.client.getMessageMeta(event.cardId, event.messageId) - if (meta === undefined) { - return { skipPropagate: true } - } - - if (event.operation.opcode === 'attach') { - const thread: Thread = { - cardId: event.operation.threadId, - messageId: event.messageId, - threadId: event.operation.threadId, - threadType: event.operation.threadType, - repliesCount: 0, - lastReplyDate: new Date(), - repliedPersons: {} - } - await this.db.attachThreadMeta( - event.cardId, - event.messageId, - thread.threadId, - thread.threadType, - event.socialId, - event.date - ) - await this.blob.attachThread(event.cardId, meta.blobId, event.messageId, thread) - } else if (event.operation.opcode === 'update') { - await this.blob.updateThread( - event.cardId, - meta.blobId, - event.messageId, - event.operation.threadId, - event.operation.update - ) - } else if (event.operation.opcode === 'addReply') { - const personUuid = await this.context.client.findPersonUuid( - { - ctx: this.context.ctx, - account: session.account - }, - event.socialId - ) - if (personUuid === undefined) return { skipPropagate: true } - await this.blob.addThreadReply( - event.cardId, - meta.blobId, - event.messageId, - event.operation.threadId, - personUuid, - event.date - ) - } else if (event.operation.opcode === 'removeReply') { - const personUuid = await this.context.client.findPersonUuid( - { - ctx: this.context.ctx, - account: session.account - }, - event.socialId - ) - - if (personUuid === undefined) return { skipPropagate: true } - await this.blob.removeThreadReply( - event.cardId, - meta.blobId, - event.messageId, - event.operation.threadId, - personUuid - ) - } - - return {} - } - - private async createNotification (event: Enriched): Promise { - const id = await this.db.createNotification( - event.contextId, - event.messageId, - event.blobId, - event.notificationType, - event.read ?? false, - event.content, - event.creator, - event.date - ) - - event.notificationId = id - - return {} - } - - private async updateNotification (event: Enriched): Promise { - const updated = await this.db.updateNotification( - { - contextId: event.contextId, - account: event.account, - ...event.query - }, - event.updates - ) - if (updated === 0) return { skipPropagate: true } - event.updated = updated - return {} - } - - private async removeNotifications (event: Enriched): Promise { - if (event.ids.length === 0) return { skipPropagate: true } - const ids = await this.db.removeNotifications({ - contextId: event.contextId, - account: event.account, - id: event.ids - }) - event.ids = ids - return { - result: { - ids - } - } - } - - private async createNotificationContext (event: Enriched): Promise { - const id = await this.db.createNotificationContext( - event.account, - event.cardId, - event.lastUpdate, - event.lastView, - event.lastNotify - ) - - event.contextId = id - return { - result: { id } - } - } - - private async removeNotificationContext (event: Enriched): Promise { - const id = await this.db.removeContext({ - id: event.contextId, - account: event.account - }) - - if (id == null) return { skipPropagate: true } - return {} - } - - async updateNotificationContext (event: Enriched): Promise { - await this.db.updateContext( - { - id: event.contextId, - account: event.account - }, - event.updates - ) - - return {} - } - - private async createLabel (event: Enriched): Promise { - await this.db.createLabel(event.cardId, event.cardType, event.labelId, event.account, event.date) - - return {} - } - - private async removeLabel (event: Enriched): Promise { - await this.db.removeLabels({ - labelId: event.labelId, - cardId: event.cardId, - account: event.account - }) - - return {} - } - - private async createPeer (event: Enriched): Promise { - await this.db.createPeer(event.workspaceId, event.cardId, event.kind, event.value, event.extra ?? {}, event.date) - return {} - } - - private async removePeer (event: Enriched): Promise { - await this.db.removePeer(event.workspaceId, event.cardId, event.kind, event.value) - return {} - } - - close (): void { - this.db.close() - } -} diff --git a/foundations/communication/packages/server/src/middleware/triggers.ts b/foundations/communication/packages/server/src/middleware/triggers.ts deleted file mode 100644 index d203be1c7e..0000000000 --- a/foundations/communication/packages/server/src/middleware/triggers.ts +++ /dev/null @@ -1,159 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { Event, EventResult, SessionData } from '@hcengineering/communication-sdk-types' -import type { MeasureContext } from '@hcengineering/core' - -import type { CommunicationCallbacks, Enriched, Middleware, MiddlewareContext, TriggerCtx } from '../types' -import { BaseMiddleware } from './base' -import triggers from '../triggers/all' -import { notify } from '../notification/notification' - -export class TriggersMiddleware extends BaseMiddleware implements Middleware { - private ctx: MeasureContext - private processedPeersEvents = new Set() - - constructor ( - private readonly callbacks: CommunicationCallbacks, - context: MiddlewareContext, - next?: Middleware - ) { - super(context, next) - this.ctx = context.ctx.newChild('triggers', {}) - } - - async event (session: SessionData, event: Enriched, derived: boolean): Promise { - const result = await this.provideEvent(session, event, derived) - if (event.skipPropagate === true) { - return result - } - - await this.processDerived(session, [event], derived) - - return result - } - - async processDerived (session: SessionData, events: Enriched[], derived: boolean): Promise { - // Ensure asyncData is initialized - if (session.asyncData === undefined) { - session.asyncData = [] - } - - const triggerCtx: Omit = { - metadata: this.context.metadata, - client: this.context.client, - workspace: this.context.workspace, - account: session.account, - processedPeersEvents: this.processedPeersEvents, - derived, - execute: async (event: Event) => { - // Will be enriched in head - return (await this.context.head?.event(session, event as Enriched, true)) ?? {} - } - } - - if (!derived && session.isAsyncContext !== true && session.contextData !== undefined) { - session.isAsyncContext = true - const ctx = this.context.ctx.newChild('async-triggers', {}) - ctx.contextData = session.contextData - - this.callbacks.registerAsyncRequest(ctx, async (_ctx) => { - this.ctx = _ctx - await this.callAsyncTriggers({ ...triggerCtx, ctx: this.ctx }, session, events) - this.handleBroadcast( - session, - (session.asyncData as Enriched[]).sort((a, b) => a.date.getTime() - b.date.getTime()) - ) - session.asyncData = [] - }) - } else { - await this.callAsyncTriggers({ ...triggerCtx, ctx: this.ctx }, session, events) - - if (session.isAsyncContext !== true) { - this.handleBroadcast( - session, - (session.asyncData as Enriched[]).sort((a, b) => a.date.getTime() - b.date.getTime()) - ) - session.asyncData = [] - this.processedPeersEvents = new Set() - } - } - } - - private async callAsyncTriggers (ctx: TriggerCtx, session: SessionData, events: Enriched[]): Promise { - const fromTriggers = await this.runTriggers({ ...ctx, ctx: this.ctx }, events) - - for (const event of fromTriggers) { - await this.context.head?.event(session, event as Enriched, true) - } - - const triggersDerived = (fromTriggers as Enriched[]).filter((it) => it.skipPropagate !== true) - session.asyncData = [...session.asyncData, ...triggersDerived] - - await this.callAsyncNotifications(ctx, session, events) - } - - private async callAsyncNotifications ( - ctx: TriggerCtx, - session: SessionData, - events: Enriched[] - ): Promise { - const notifications = ( - await Promise.all( - events.map(async (event) => { - return await notify( - { - ...ctx, - ctx: this.ctx.newChild('create-notifications', {}) - }, - event - ) - }) - ) - ).flat() - await Promise.all(notifications.map((d) => this.context.head?.event(session, d as Enriched, true))) - const notificationsDerived = (notifications as Enriched[]).filter((it) => it.skipPropagate !== true) - session.asyncData = [...session.asyncData, ...notificationsDerived] - } - - private async runTriggers (ctx: TriggerCtx, events: Enriched[]): Promise { - return ( - await Promise.all( - events.map(async (event) => { - return await this.applyTriggers(event, ctx) - }) - ) - ).flat() - } - - private async applyTriggers (event: Enriched, ctx: Omit): Promise { - const matchedTriggers = triggers.filter(([_, type]) => type === event.type) - if (matchedTriggers.length === 0) return [] - - return ( - await Promise.all( - matchedTriggers.map(([name, _, fn]) => - fn( - { - ...ctx, - ctx: this.ctx.newChild(name, {}) - }, - event - ) - ) - ) - ).flat() - } -} diff --git a/foundations/communication/packages/server/src/middleware/validate.ts b/foundations/communication/packages/server/src/middleware/validate.ts deleted file mode 100644 index 0107be0fd5..0000000000 --- a/foundations/communication/packages/server/src/middleware/validate.ts +++ /dev/null @@ -1,496 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - type EventResult, - MessageEventType, - NotificationEventType, - type Event, - type SessionData, - PeerEventType -} from '@hcengineering/communication-sdk-types' -import { - type AccountUuid, - type BlobID, - type CardID, - type CardType, - type Collaborator, - type ContextID, - type FindCollaboratorsParams, - type FindLabelsParams, - type FindMessagesGroupParams, - type FindNotificationContextParams, - type FindNotificationsParams, - type Label, - type LabelID, - type MessageID, - type MessagesGroup, - type Notification, - type NotificationContext, - NotificationType, - SortingOrder -} from '@hcengineering/communication-types' -import { z, ZodString, ZodType, ZodTypeDef } from 'zod' -import { isBlobAttachmentType, isLinkPreviewAttachmentType } from '@hcengineering/communication-shared' - -import type { Enriched, Middleware, Subscription } from '../types' -import { BaseMiddleware } from './base' -import { ApiError } from '../error' - -export class ValidateMiddleware extends BaseMiddleware implements Middleware { - private validate(data: unknown, schema: z.ZodType): T { - const validationResult = schema.safeParse(data) - if (!validationResult.success) { - const errors = validationResult.error.errors.map((err) => err.message) - this.context.ctx.error(validationResult.error.message, data as any) - throw ApiError.badRequest(errors.join(', ')) - } - return validationResult.data - } - - async findMessagesGroups (session: SessionData, params: unknown): Promise { - const validParams: FindMessagesGroupParams = this.validate(params, FindMessagesGroupsParamsSchema) - return await this.provideFindMessagesGroups(session, validParams) - } - - async findNotificationContexts ( - session: SessionData, - params: unknown, - queryId?: Subscription - ): Promise { - const validParams: FindNotificationContextParams = this.validate(params, FindNotificationContextParamsSchema) - return await this.provideFindNotificationContexts(session, validParams, queryId) - } - - async findNotifications (session: SessionData, params: unknown, queryId?: Subscription): Promise { - const validParams: FindNotificationsParams = this.validate(params, FindNotificationsParamsSchema) - return await this.provideFindNotifications(session, validParams, queryId) - } - - async findLabels (session: SessionData, params: unknown, queryId?: Subscription): Promise { - const validParams: FindLabelsParams = this.validate(params, FindLabelsParamsSchema) - return await this.provideFindLabels(session, validParams, queryId) - } - - async findCollaborators (session: SessionData, params: unknown): Promise { - const validParams: FindCollaboratorsParams = this.validate(params, FindCollaboratorsParamsSchema) - return await this.provideFindCollaborators(session, validParams) - } - - async event (session: SessionData, event: Enriched, derived: boolean): Promise { - if (derived) return await this.provideEvent(session, event, derived) - switch (event.type) { - case MessageEventType.CreateMessage: - this.validate(event, CreateMessageEventSchema) - break - case MessageEventType.UpdatePatch: - this.validate(event, UpdatePatchEventSchema) - break - case MessageEventType.RemovePatch: - this.validate(event, RemovePatchEventSchema) - break - case MessageEventType.ReactionPatch: - this.validate(event, ReactionPatchEventSchema) - break - case MessageEventType.BlobPatch: - this.validate(event, BlobPatchEventSchema) - break - case MessageEventType.AttachmentPatch: - this.validate(event, AttachmentPatchEventSchema) - event.operations.forEach((op) => { - if (op.opcode === 'add' || op.opcode === 'set') { - op.attachments.forEach((att) => { - if (isLinkPreviewAttachmentType(att.mimeType)) { - this.validate(att.params, LinkPreviewParamsSchema) - } else if (isBlobAttachmentType(att.mimeType)) { - this.validate(att.params, BlobParamsSchema) - } - }) - } - }) - break - case MessageEventType.ThreadPatch: - this.validate(event, ThreadPatchEventSchema) - break - case NotificationEventType.AddCollaborators: - this.validate(event, AddCollaboratorsEventSchema) - break - case NotificationEventType.RemoveCollaborators: - this.validate(event, RemoveCollaboratorsEventSchema) - break - case NotificationEventType.UpdateNotification: - this.validate(event, UpdateNotificationsEventSchema) - break - case NotificationEventType.RemoveNotificationContext: - this.validate(event, RemoveNotificationContextEventSchema) - break - case NotificationEventType.UpdateNotificationContext: - this.validate(event, UpdateNotificationContextEventSchema) - break - case PeerEventType.CreatePeer: - this.validate(event, CreatePeerEventSchema) - break - case PeerEventType.RemovePeer: - this.validate(event, RemovePeerEventSchema) - break - } - return await this.provideEvent(session, deserializeEvent(event), derived) - } -} - -function brandedString (base: ZodString): ZodType { - return base.transform((s): Type => s as Type) -} - -const WorkspaceUuidSchema = z.string().uuid() -const AccountUuidSchema = brandedString(z.string()) -const BlobIDSchema = brandedString(z.string().uuid()) -const AttachmentIDSchema = z.string().uuid() -const CardIDSchema = brandedString(z.string()) -const CardTypeSchema = brandedString(z.string()) -const ContextIDSchema = brandedString(z.string()) -const DateSchema = z.coerce.date() -const LabelIDSchema = brandedString(z.string()) -const MarkdownSchema = z.string() -const MessageExtraSchema = z.any() -const MessageIDSchema = brandedString(z.string()) -const MessageTypeSchema = z.string() -const SocialIDSchema = z.string() -const SortingOrderSchema = z.union([z.literal(SortingOrder.Ascending), z.literal(SortingOrder.Descending)]) -const NotificationTypeSchema = z.nativeEnum(NotificationType) - -const BlobParamsSchema = z.object({ - blobId: BlobIDSchema, - mimeType: z.string(), - fileName: z.string(), - size: z.number(), - metadata: z.record(z.string(), z.any()).optional() -}) - -const LinkPreviewParamsSchema = z - .object({ - url: z.string(), - host: z.string(), - title: z.string().optional(), - description: z.string().optional(), - siteName: z.string().optional(), - iconUrl: z.string().optional(), - previewImage: z - .object({ - url: z.string(), - width: z.number().optional(), - height: z.number().optional() - }) - .optional() - }) - .strict() - -const UpdateBlobDataSchema = z.object({ - blobId: BlobIDSchema, - mimeType: z.string().optional(), - fileName: z.string().optional(), - size: z.number().optional(), - metadata: z.record(z.string(), z.any()).optional() -}) - -const AttachmentDataSchema = z.object({ - id: AttachmentIDSchema, - mimeType: z.string(), - params: z.record(z.string(), z.any()) -}) - -const AttachmentUpdateDataSchema = z.object({ - id: AttachmentIDSchema, - params: z.record(z.string(), z.any()) -}) - -// Find params -const DateOrRecordSchema = z.union([DateSchema, z.record(DateSchema)]) - -const FindParamsSchema = z - .object({ - order: SortingOrderSchema.optional(), - limit: z.number().optional() - }) - .strict() - -const FindNotificationContextParamsSchema = FindParamsSchema.extend({ - id: ContextIDSchema.optional(), - cardId: z.union([CardIDSchema, z.array(CardIDSchema)]).optional(), - lastNotify: DateOrRecordSchema.optional(), - account: z.union([AccountUuidSchema, z.array(AccountUuidSchema)]).optional(), - notifications: z - .object({ - type: NotificationTypeSchema.optional(), - limit: z.number(), - order: SortingOrderSchema, - read: z.boolean().optional(), - total: z.boolean().optional() - }) - .optional() -}).strict() - -const FindMessagesGroupsParamsSchema = FindParamsSchema.extend({ - cardId: CardIDSchema, - id: MessageIDSchema.optional(), - blobId: BlobIDSchema.optional(), - fromDate: DateOrRecordSchema.optional(), - toDate: DateOrRecordSchema.optional() -}).strict() - -const FindNotificationsParamsSchema = FindParamsSchema.extend({ - contextId: ContextIDSchema.optional(), - type: NotificationTypeSchema.optional(), - read: z.boolean().optional(), - created: DateOrRecordSchema.optional(), - account: z.union([AccountUuidSchema, z.array(AccountUuidSchema)]).optional(), - cardId: CardIDSchema.optional(), - total: z.boolean().optional() -}).strict() - -const FindLabelsParamsSchema = FindParamsSchema.extend({ - labelId: z.union([LabelIDSchema, z.array(LabelIDSchema)]).optional(), - cardId: CardIDSchema.optional(), - cardType: z.union([CardTypeSchema, z.array(CardTypeSchema)]).optional(), - account: AccountUuidSchema.optional() -}).strict() - -const FindCollaboratorsParamsSchema = FindParamsSchema.extend({ - cardId: CardIDSchema, - account: z.union([AccountUuidSchema, z.array(AccountUuidSchema)]).optional() -}).strict() - -// Events - -const BaseEventSchema = z - .object({ - _id: z.string().optional(), - _eventExtra: z.record(z.any()).optional() - }) - .strict() - -// Message events -const CreateMessageEventSchema = BaseEventSchema.extend({ - type: z.literal(MessageEventType.CreateMessage), - - cardId: CardIDSchema, - cardType: CardTypeSchema, - - messageId: brandedString(z.string().max(22)).optional(), - messageType: MessageTypeSchema, - - content: MarkdownSchema, - extra: MessageExtraSchema.optional(), - - socialId: SocialIDSchema, - date: DateSchema, - language: z.string().optional(), - - options: z - .object({ - skipLinkPreviews: z.boolean().optional(), - noNotify: z.boolean().optional(), - ignoreMentions: z.boolean().optional() - }) - .optional() -}).strict() - -const UpdatePatchEventSchema = BaseEventSchema.extend({ - type: z.literal(MessageEventType.UpdatePatch), - cardId: CardIDSchema, - messageId: MessageIDSchema.optional(), - - content: MarkdownSchema.optional(), - extra: z.record(z.any()).optional(), - language: z.string().optional(), - - socialId: SocialIDSchema, - date: DateSchema, - - options: z - .object({ - skipLinkPreviewsUpdate: z.boolean().optional(), - ignoreMentions: z.boolean().optional() - }) - .optional() -}).strict() - -const RemovePatchEventSchema = BaseEventSchema.extend({ - type: z.literal(MessageEventType.RemovePatch), - cardId: CardIDSchema, - messageId: MessageIDSchema.optional(), - - socialId: SocialIDSchema, - date: DateSchema -}).strict() - -const ReactionOperationSchema = z.union([ - z.object({ opcode: z.literal('add'), reaction: z.string() }), - z.object({ opcode: z.literal('remove'), reaction: z.string() }) -]) - -const ReactionPatchEventSchema = BaseEventSchema.extend({ - type: z.literal(MessageEventType.ReactionPatch), - cardId: CardIDSchema, - messageId: MessageIDSchema, - operation: ReactionOperationSchema, - personUuid: z.string(), - socialId: SocialIDSchema, - date: DateSchema -}).strict() - -/** - * @deprecated - */ -const BlobOperationSchema = z.union([ - z.object({ opcode: z.literal('attach'), blobs: z.array(BlobParamsSchema).nonempty() }), - z.object({ opcode: z.literal('detach'), blobIds: z.array(BlobIDSchema).nonempty() }), - z.object({ opcode: z.literal('set'), blobs: z.array(BlobParamsSchema).nonempty() }), - z.object({ opcode: z.literal('update'), blobs: z.array(UpdateBlobDataSchema).nonempty() }) -]) - -/** - * @deprecated - */ -const BlobPatchEventSchema = BaseEventSchema.extend({ - type: z.literal(MessageEventType.BlobPatch), - cardId: CardIDSchema, - messageId: MessageIDSchema, - operations: z.array(BlobOperationSchema).nonempty(), - socialId: SocialIDSchema, - date: DateSchema -}).strict() - -const AttachmentOperationSchema = z.union([ - z.object({ opcode: z.literal('add'), attachments: z.array(AttachmentDataSchema).nonempty() }), - z.object({ opcode: z.literal('remove'), ids: z.array(AttachmentIDSchema).nonempty() }), - z.object({ opcode: z.literal('set'), attachments: z.array(AttachmentDataSchema).nonempty() }), - z.object({ opcode: z.literal('update'), attachments: z.array(AttachmentUpdateDataSchema).nonempty() }) -]) - -const AttachmentPatchEventSchema = BaseEventSchema.extend({ - type: z.literal(MessageEventType.AttachmentPatch), - cardId: CardIDSchema, - messageId: MessageIDSchema, - operations: z.array(AttachmentOperationSchema).nonempty(), - socialId: SocialIDSchema, - date: DateSchema -}).strict() - -const ThreadPatchEventSchema = BaseEventSchema.extend({ - type: z.literal(MessageEventType.ThreadPatch), - cardId: CardIDSchema, - messageId: MessageIDSchema, - operation: z.object({ opcode: z.literal('attach'), threadId: CardIDSchema, threadType: CardTypeSchema }), - socialId: SocialIDSchema, - personUuid: z.string(), - date: DateSchema -}).strict() - -// Notification events -const UpdateNotificationsEventSchema = BaseEventSchema.extend({ - type: z.literal(NotificationEventType.UpdateNotification), - contextId: ContextIDSchema, - account: AccountUuidSchema, - query: z.object({ - id: z.string().optional(), - type: z.string().optional(), - untilDate: DateSchema.optional() - }), - updates: z.object({ - read: z.boolean() - }), - date: DateSchema -}).strict() - -const RemoveNotificationContextEventSchema = BaseEventSchema.extend({ - type: z.literal(NotificationEventType.RemoveNotificationContext), - contextId: ContextIDSchema, - account: AccountUuidSchema, - date: DateSchema -}).strict() - -const UpdateNotificationContextEventSchema = BaseEventSchema.extend({ - type: z.literal(NotificationEventType.UpdateNotificationContext), - contextId: ContextIDSchema, - account: AccountUuidSchema, - updates: z.object({ - lastView: DateSchema.optional() - }), - date: DateSchema -}).strict() - -const AddCollaboratorsEventSchema = BaseEventSchema.extend({ - type: z.literal(NotificationEventType.AddCollaborators), - cardId: CardIDSchema, - cardType: CardTypeSchema, - collaborators: z.array(AccountUuidSchema).nonempty(), - socialId: SocialIDSchema, - date: DateSchema -}).strict() - -const RemoveCollaboratorsEventSchema = BaseEventSchema.extend({ - type: z.literal(NotificationEventType.RemoveCollaborators), - cardId: CardIDSchema, - cardType: CardTypeSchema, - collaborators: z.array(AccountUuidSchema).nonempty(), - socialId: SocialIDSchema, - date: DateSchema -}).strict() - -const CreatePeerEventSchema = BaseEventSchema.extend({ - type: z.literal(PeerEventType.CreatePeer), - workspaceId: WorkspaceUuidSchema, - cardId: CardIDSchema, - kind: z.string().nonempty(), - value: z.string().nonempty(), - extra: z.record(z.any()).optional(), - options: z - .object({ - newValue: z.boolean().optional() - }) - .optional(), - date: DateSchema -}).strict() - -const RemovePeerEventSchema = BaseEventSchema.extend({ - type: z.literal(PeerEventType.RemovePeer), - workspaceId: WorkspaceUuidSchema, - cardId: CardIDSchema, - kind: z.string().nonempty(), - value: z.string().nonempty(), - date: DateSchema -}).strict() - -function deserializeEvent (event: Enriched): Enriched { - switch (event.type) { - case NotificationEventType.UpdateNotificationContext: - event.updates.lastView = deserializeDate(event.updates.lastView) - break - case NotificationEventType.UpdateNotification: - event.query.untilDate = deserializeDate(event.query.untilDate) - break - } - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - event.date = deserializeDate(event.date)! - return event -} - -function deserializeDate (date?: Date | string | undefined | null): Date | undefined { - if (date == null) return undefined - if (date instanceof Date) return date - return new Date(date) -} diff --git a/foundations/communication/packages/server/src/middlewares.ts b/foundations/communication/packages/server/src/middlewares.ts deleted file mode 100644 index ec6a66e45f..0000000000 --- a/foundations/communication/packages/server/src/middlewares.ts +++ /dev/null @@ -1,212 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { MeasureContext } from '@hcengineering/core' -import type { EventResult, Event, SessionData } from '@hcengineering/communication-sdk-types' -import type { - CardID, - Collaborator, - FindCollaboratorsParams, - FindLabelsParams, FindMessagesGroupParams, FindMessagesMetaParams, - FindNotificationContextParams, - FindNotificationsParams, - FindPeersParams, - Label, MessageMeta, MessagesGroup, - Notification, - NotificationContext, Peer, - WorkspaceUuid -} from '@hcengineering/communication-types' - -import type { - CommunicationCallbacks, - Enriched, - Metadata, - Middleware, - MiddlewareContext, - MiddlewareCreateFn, - Subscription -} from './types' -import { PermissionsMiddleware } from './middleware/permissions' -import { StorageMiddleware } from './middleware/storage' -import { BroadcastMiddleware } from './middleware/broadcast' -import { TriggersMiddleware } from './middleware/triggers' -import { ValidateMiddleware } from './middleware/validate' -import { DateMiddleware } from './middleware/date' -import { IdentityMiddleware } from './middleware/indentity' -import { IdMiddleware } from './middleware/id' -import { PeerMiddleware } from './middleware/peer' -import { LowLevelClient } from './client' - -export async function buildMiddlewares ( - ctx: MeasureContext, - workspace: WorkspaceUuid, - metadata: Metadata, - client: LowLevelClient, - callbacks: CommunicationCallbacks -): Promise { - const peers = await client.db.findPeers({ workspaceId: workspace }) - - const createFns: MiddlewareCreateFn[] = [ - // Enrich events - async (context, next) => new DateMiddleware(context, next), - async (context, next) => new IdentityMiddleware(context, next), - async (context, next) => new IdMiddleware(context, next), - - // Validate events - async (context, next) => new ValidateMiddleware(context, next), - async (context, next) => new PermissionsMiddleware(context, next), - - // Process events - async (context, next) => new TriggersMiddleware(callbacks, context, next), - async (context, next) => new BroadcastMiddleware(callbacks, context, next), - async (context, next) => new StorageMiddleware(context, next), - async (context, next) => new PeerMiddleware(context, next) - ] - - const context: MiddlewareContext = { - ctx, - metadata, - workspace, - client, - cadsWithPeers: new Set(peers.map(it => it.cardId)) - } - - return await Middlewares.create(ctx, context, createFns) -} - -export class Middlewares { - private head: Middleware | undefined - - private readonly middlewares: Middleware[] = [] - - private constructor ( - private readonly ctx: MeasureContext, - private readonly context: MiddlewareContext - ) { - } - - static async create ( - ctx: MeasureContext, - context: MiddlewareContext, - createFns: MiddlewareCreateFn[] - ): Promise { - const middlewares = new Middlewares(ctx, context) - - const head = await middlewares.buildChain(ctx, createFns, middlewares.context) - middlewares.head = head - context.head = head - return middlewares - } - - private async buildChain ( - ctx: MeasureContext, - createFns: MiddlewareCreateFn[], - context: MiddlewareContext - ): Promise { - let current: Middleware | undefined - for (let index = createFns.length - 1; index >= 0; index--) { - const createFn = createFns[index] - try { - const nextCurrent = await createFn(context, current) - this.middlewares.push(nextCurrent) - current = nextCurrent - } catch (err: any) { - ctx.error('failed to initialize middlewares', { err, workspace: context.workspace }) - await this.close() - throw err - } - } - this.middlewares.reverse() - - return current - } - - async findMessagesGroups (session: SessionData, params: FindMessagesGroupParams): Promise { - if (this.head === undefined) return [] - return await this.head.findMessagesGroups(session, params) - } - - async findMessagesMeta (session: SessionData, params: FindMessagesMetaParams): Promise { - if (this.head === undefined) return [] - return await this.head.findMessagesMeta(session, params) - } - - async findNotificationContexts ( - session: SessionData, - params: FindNotificationContextParams, - queryId?: Subscription - ): Promise { - if (this.head === undefined) return [] - return await this.head.findNotificationContexts(session, params, queryId) - } - - async findNotifications ( - session: SessionData, - params: FindNotificationsParams, - queryId?: Subscription - ): Promise { - if (this.head === undefined) return [] - return await this.head.findNotifications(session, params, queryId) - } - - async findLabels (session: SessionData, params: FindLabelsParams): Promise { - if (this.head === undefined) return [] - return await this.head.findLabels(session, params) - } - - async findCollaborators (session: SessionData, params: FindCollaboratorsParams): Promise { - if (this.head === undefined) return [] - return await this.head.findCollaborators(session, params) - } - - async findPeers (session: SessionData, params: FindPeersParams): Promise { - if (this.head === undefined) return [] - return await this.head.findPeers(session, params) - } - - subscribeCard (session: SessionData, cardId: CardID, subscription: Subscription): void { - if (this.head === undefined) return - this.head?.subscribeCard(session, cardId, subscription) - } - - unsubscribeCard (session: SessionData, cardId: CardID, subscription: Subscription): void { - if (this.head === undefined) return - this.head?.unsubscribeCard(session, cardId, subscription) - } - - async event (session: SessionData, event: Event): Promise { - if (this.head === undefined) return {} - const result = (await this.head?.event(session, event as Enriched, session.derived ?? false)) ?? {} - - this.head?.handleBroadcast(session, [event] as Enriched[]) - - return result - } - - async closeSession (sessionId: string): Promise { - if (this.head === undefined) return - this.head.closeSession(sessionId) - } - - async close (): Promise { - for (const mw of this.middlewares) { - try { - mw.close() - } catch (err: any) { - this.ctx.error('Failed to close middleware', { err, workspace: this.context.workspace }) - } - } - } -} diff --git a/foundations/communication/packages/server/src/notification/notification.ts b/foundations/communication/packages/server/src/notification/notification.ts deleted file mode 100644 index bfa83d18d5..0000000000 --- a/foundations/communication/packages/server/src/notification/notification.ts +++ /dev/null @@ -1,405 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - type CreateNotificationContextResult, - NotificationEventType, - type Event, - MessageEventType -} from '@hcengineering/communication-sdk-types' -import { - type AccountUuid, - BlobID, - type CardID, - type CardType, - type ContextID, - Markdown, - type MessageID, - type NotificationContext, - NotificationType, - type ReactionNotificationContent, - type SocialID, - SortingOrder -} from '@hcengineering/communication-types' -import { markdownToMarkup } from '@hcengineering/text-markdown' -import { jsonToMarkup, markupToText } from '@hcengineering/text-core' -import { readOnlyGuestAccountUuid } from '@hcengineering/core' - -import type { Enriched, TriggerCtx } from '../types' -import { getNameBySocialID } from '../triggers/utils' - -const BATCH_SIZE = 500 -const maxDate = new Date('9999-12-31T23:59:59Z') - -export async function notify (ctx: TriggerCtx, event: Enriched): Promise { - switch (event.type) { - case MessageEventType.CreateMessage: { - if (event.options?.noNotify === true || event.messageId == null) return [] - const meta = await ctx.client.getMessageMeta(event.cardId, event.messageId) - if (meta == null) return [] - return await notifyMessage( - ctx, - event.cardId, - event.cardType, - event.messageId, - meta.blobId, - event.content, - event.socialId, - event.date - ) - } - case MessageEventType.ReactionPatch: { - if (event.operation.opcode === 'add') { - return await notifyReaction( - ctx, - event.cardId, - event.messageId, - event.operation.reaction, - event.socialId, - event.date - ) - } else if (event.operation.opcode === 'remove') { - return await removeReactionNotification( - ctx, - event.cardId, - event.messageId, - event.operation.reaction, - event.socialId - ) - } - } - } - - return [] -} - -async function removeReactionNotification ( - ctx: TriggerCtx, - cardId: CardID, - messageId: MessageID, - reaction: string, - socialId: SocialID -): Promise { - const result: Event[] = [] - const meta = await ctx.client.getMessageMeta(cardId, messageId) - if (meta == null) return result - - const messageAccount = (await ctx.client.findPersonUuid( - { - ctx: ctx.ctx, - account: ctx.account - }, - meta.creator - )) as AccountUuid | undefined - if (messageAccount == null) return result - - const notifications = await ctx.client.db.findNotifications({ - type: NotificationType.Reaction, - messageId, - account: messageAccount - }) - - const toDelete = notifications.find((n) => { - const content = n.content as ReactionNotificationContent - return content.emoji === reaction && n.creator === socialId - }) - - if (toDelete === undefined) return result - - const context = (await ctx.client.db.findNotificationContexts({ cardId, account: messageAccount, limit: 1 }))[0] - if (context == null) return result - if (context.lastNotify != null && context.lastNotify.getTime() === toDelete.created.getTime()) { - const lastNotification = ( - await ctx.client.db.findNotifications({ - account: messageAccount, - contextId: context.id, - created: { - less: context.lastNotify - }, - order: SortingOrder.Descending, - limit: 1 - }) - )[0] - if (lastNotification != null) { - result.push({ - type: NotificationEventType.UpdateNotificationContext, - contextId: context.id, - account: messageAccount, - updates: { - lastNotify: lastNotification.created - }, - date: new Date() - }) - } - } - - result.push({ - type: NotificationEventType.RemoveNotifications, - contextId: toDelete.contextId, - account: messageAccount, - ids: [toDelete.id] - }) - - return result -} -async function notifyReaction ( - ctx: TriggerCtx, - cardId: CardID, - messageId: MessageID, - reaction: string, - socialId: SocialID, - date: Date -): Promise { - const result: Event[] = [] - - const meta = await ctx.client.getMessageMeta(cardId, messageId) - if (meta == null) return result - - const messageAccount = (await ctx.client.findPersonUuid(ctx, meta.creator, true)) as AccountUuid | undefined - if (messageAccount == null) return result - - const spaceMembers = await ctx.client.db.getCardSpaceMembers(cardId) - if (!spaceMembers.includes(messageAccount)) return [] - - const reactionAccount = (await ctx.client.findPersonUuid(ctx, socialId, true)) as AccountUuid | undefined - if (reactionAccount === messageAccount) return result - - const context = (await ctx.client.db.findNotificationContexts({ cardId, account: messageAccount }))[0] - let contextId: ContextID | undefined = context?.id - - if (context == null) { - contextId = await createContext(ctx, messageAccount, cardId, date, undefined, date) - } - - if (contextId == null) return result - - const content: ReactionNotificationContent = { - emoji: reaction, - senderName: (await getNameBySocialID(ctx, socialId)) ?? 'System', - title: 'Reacted to your message', - shortText: reaction - } - result.push({ - type: NotificationEventType.CreateNotification, - notificationType: NotificationType.Reaction, - account: messageAccount, - cardId, - contextId, - messageId, - blobId: meta.blobId, - date, - content, - creator: socialId, - read: messageAccount === readOnlyGuestAccountUuid - }) - - if ((context?.lastNotify?.getTime() ?? date.getTime()) < date.getTime()) { - result.push({ - type: NotificationEventType.UpdateNotificationContext, - contextId, - account: messageAccount, - updates: { - lastNotify: date - }, - date - }) - } - return result -} - -async function notifyMessage ( - ctx: TriggerCtx, - cardId: CardID, - cardType: CardType, - messageId: MessageID, - blobId: BlobID, - markdown: Markdown, - socialId: SocialID, - date: Date -): Promise { - const { client } = ctx - const cursor = client.db.getCollaboratorsCursor(cardId, date, BATCH_SIZE) - const spaceMembers = await client.db.getCardSpaceMembers(cardId) - const creatorAccount = (await ctx.client.findPersonUuid(ctx, socialId, true)) as AccountUuid | undefined - const result: Event[] = [] - - const cardTitle = (await ctx.client.db.getCardTitle(cardId)) ?? 'New message' - - let isFirstBatch = true - - for await (const dbCollaborators of cursor) { - const collaborators: AccountUuid[] = dbCollaborators.map((it) => it.account) - const contexts: NotificationContext[] = await client.db.findNotificationContexts({ - cardId, - account: isFirstBatch && collaborators.length < BATCH_SIZE ? undefined : collaborators - }) - - for (const collaborator of collaborators) { - if (!spaceMembers.includes(collaborator)) continue - try { - const context = contexts.find((it) => it.account === collaborator) - const res = await processCollaborator( - ctx, - cardId, - cardType, - cardTitle, - messageId, - blobId, - markdown, - date, - collaborator, - socialId, - creatorAccount, - context - ) - result.push(...res) - } catch (e) { - ctx.ctx.error('Error on create notification', { collaborator, error: e }) - } - } - - isFirstBatch = false - } - - return result -} - -async function processCollaborator ( - ctx: TriggerCtx, - cardId: CardID, - cardType: CardType, - cardTitle: string, - messageId: MessageID, - blobId: BlobID, - markdown: Markdown, - date: Date, - collaborator: AccountUuid, - creatorSocialId: SocialID, - creatorAccount?: AccountUuid, - context?: NotificationContext -): Promise { - const result: Event[] = [] - const isOwn = creatorAccount === collaborator - const { contextId, events } = await createOrUpdateContext(ctx, cardId, date, collaborator, isOwn, context) - - result.push(...events) - - if (contextId == null || isOwn) return result - - const text = markupToText(jsonToMarkup(markdownToMarkup(markdown))) - const shortText = text.slice(0, 100) - const isRead = collaborator === readOnlyGuestAccountUuid - result.push({ - type: NotificationEventType.CreateNotification, - notificationType: NotificationType.Message, - account: collaborator, - contextId, - cardId, - messageId, - blobId, - date, - creator: creatorSocialId, - content: { - senderName: (await getNameBySocialID(ctx, creatorSocialId)) ?? 'System', - title: cardTitle, - shortText: shortText.length < text.length ? shortText + '...' : text - }, - read: isRead || date.getTime() < (context?.lastView?.getTime() ?? 0) - }) - return result -} - -async function createOrUpdateContext ( - ctx: TriggerCtx, - cardId: CardID, - date: Date, - collaborator: AccountUuid, - isOwn: boolean, - context?: NotificationContext -): Promise<{ - contextId: ContextID | undefined - events: Event[] - }> { - if (context == null) { - const lastView = collaborator === readOnlyGuestAccountUuid ? maxDate : isOwn ? date : undefined - const contextId = await createContext(ctx, collaborator, cardId, date, lastView, date) - - return { - contextId, - events: [] - } - } - - const lastUpdate = context.lastUpdate == null || date > context.lastUpdate ? date : context.lastUpdate - const lastView = - collaborator === readOnlyGuestAccountUuid ? maxDate : isOwn && isContextRead(context) ? date : undefined - - return { - contextId: context.id, - events: [ - { - type: NotificationEventType.UpdateNotificationContext, - contextId: context.id, - account: collaborator, - updates: { - lastView, - lastUpdate, - lastNotify: isOwn ? undefined : date - }, - date: new Date() - } - ] - } -} - -async function createContext ( - ctx: TriggerCtx, - account: AccountUuid, - cardId: CardID, - lastUpdate: Date, - lastView: Date | undefined, - lastNotify: Date -): Promise { - try { - const result = (await ctx.execute({ - type: NotificationEventType.CreateNotificationContext, - account, - cardId, - lastUpdate, - lastView: lastView ?? new Date(lastUpdate.getTime() - 1), - lastNotify, - date: new Date() - })) as CreateNotificationContextResult - - return result.id - } catch (e) { - return ( - await ctx.client.db.findNotificationContexts({ - account, - cardId - }) - )[0]?.id - } -} - -function isContextRead (context: NotificationContext): boolean { - const { lastView, lastUpdate } = context - - if (lastView == null) { - return false - } - - return lastView >= lastUpdate -} diff --git a/foundations/communication/packages/server/src/triggers/all.ts b/foundations/communication/packages/server/src/triggers/all.ts deleted file mode 100644 index 3ea8496dfa..0000000000 --- a/foundations/communication/packages/server/src/triggers/all.ts +++ /dev/null @@ -1,23 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { Triggers } from '../types' -import message from './message' -import notification from './notification' -import card from './card' - -const allTriggers: Triggers = [...message, ...notification, ...card] - -export default allTriggers diff --git a/foundations/communication/packages/server/src/triggers/card.ts b/foundations/communication/packages/server/src/triggers/card.ts deleted file mode 100644 index 7affc95382..0000000000 --- a/foundations/communication/packages/server/src/triggers/card.ts +++ /dev/null @@ -1,124 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - CardEventType, - MessageEventType, - NotificationEventType, - type Event, - UpdateCardTypeEvent, - RemoveCardEvent -} from '@hcengineering/communication-sdk-types' -import { type ActivityTypeUpdate, ActivityUpdateType, MessageType } from '@hcengineering/communication-types' - -import type { Enriched, TriggerCtx, TriggerFn, Triggers } from '../types' - -async function createActivityOnCardTypeUpdate (ctx: TriggerCtx, event: UpdateCardTypeEvent): Promise { - const updateDate: ActivityTypeUpdate = { - type: ActivityUpdateType.Type, - newType: event.cardType - } - - return [ - { - type: MessageEventType.CreateMessage, - messageType: MessageType.Activity, - cardId: event.cardId, - cardType: event.cardType, - content: 'Changed type', - socialId: event.socialId, - date: event.date, - extra: { - action: 'update', - update: updateDate - } - } - ] -} - -async function onCardTypeUpdates (ctx: TriggerCtx, event: Enriched): Promise { - await ctx.client.db.updateCollaborators({ cardId: event.cardId }, { cardType: event.cardType }) - await ctx.client.db.updateLabels({ cardId: event.cardId }, { cardType: event.cardType }) - - const thread = (await ctx.client.db.findThreadMeta({ threadId: event.cardId, limit: 1 }))[0] - if (thread === undefined) return [] - - return [ - { - type: MessageEventType.ThreadPatch, - cardId: thread.cardId, - messageId: thread.messageId, - operation: { - opcode: 'update', - threadId: thread.threadId, - update: { - threadType: event.cardType - } - }, - socialId: event.socialId, - date: event.date - } - ] -} - -async function removeCardCollaborators (ctx: TriggerCtx, event: UpdateCardTypeEvent): Promise { - await ctx.client.db.removeCollaborators({ cardId: event.cardId }) - return [] -} - -async function removeCardLabels (ctx: TriggerCtx, event: UpdateCardTypeEvent): Promise { - await ctx.client.db.removeLabels({ cardId: event.cardId }) - return [] -} - -async function removeCardThreads (ctx: TriggerCtx, event: RemoveCardEvent): Promise { - const toRemove = await ctx.client.db.findThreadMeta({ threadId: event.cardId }) - - await ctx.client.db.removeThreadMeta({ cardId: event.cardId }) - await ctx.client.db.removeThreadMeta({ threadId: event.cardId }) - - for (const thread of toRemove) { - const meta = await ctx.client.getMessageMeta(thread.cardId, thread.messageId) - if (meta === undefined) continue - await ctx.client.blob.removeThread(thread.cardId, meta.blobId, thread.messageId, thread.threadId) - } - - return [] -} - -async function removeNotificationContexts (ctx: TriggerCtx, event: RemoveCardEvent): Promise { - const result: Event[] = [] - const contexts = await ctx.client.db.findNotificationContexts({ cardId: event.cardId }) - for (const context of contexts) { - result.push({ - type: NotificationEventType.RemoveNotificationContext, - contextId: context.id, - account: context.account, - date: new Date() - }) - } - return result -} - -const triggers: Triggers = [ - ['on_card_type_updates', CardEventType.UpdateCardType, onCardTypeUpdates as TriggerFn], - ['create_activity_on_card_type_updates', CardEventType.UpdateCardType, createActivityOnCardTypeUpdate as TriggerFn], - ['remove_collaborators_on_card_removed', CardEventType.RemoveCard, removeCardCollaborators as TriggerFn], - ['remove_labels_on_card_removed', CardEventType.RemoveCard, removeCardLabels as TriggerFn], - ['remove_threads_on_card_removed', CardEventType.RemoveCard, removeCardThreads as TriggerFn], - ['remove_notification_contexts_on_card_removed', CardEventType.RemoveCard, removeNotificationContexts as TriggerFn] -] - -export default triggers diff --git a/foundations/communication/packages/server/src/triggers/message.ts b/foundations/communication/packages/server/src/triggers/message.ts deleted file mode 100644 index 2cdccaacde..0000000000 --- a/foundations/communication/packages/server/src/triggers/message.ts +++ /dev/null @@ -1,226 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - CreateMessageEvent, - type Event, - MessageEventType, - NotificationEventType, - PatchEvent, - RemovePatchEvent, - ThreadPatchEvent -} from '@hcengineering/communication-sdk-types' -import { CardPeer, MessageType, Peer } from '@hcengineering/communication-types' -import { type AccountUuid, generateId } from '@hcengineering/core' -import { extractReferences } from '@hcengineering/text-core' -import { markdownToMarkup } from '@hcengineering/text-markdown' - -import type { Enriched, TriggerCtx, TriggerFn, Triggers } from '../types' -import { generateMessageId } from '../messageId' - -async function addCollaborators (ctx: TriggerCtx, event: Enriched): Promise { - const { messageType, socialId, content, cardId, cardType, date } = event - if (messageType === MessageType.Activity) return [] - const account = (await ctx.client.findPersonUuid(ctx, socialId, true)) as AccountUuid | undefined - const collaborators = new Set() - - if (account !== undefined) { - collaborators.add(account) - } - - if (event.options?.ignoreMentions !== true) { - const markup = markdownToMarkup(content) - const references = extractReferences(markup) - const personIds = references - .filter((it) => ['contact:class:Person', 'contact:mixin:Employee'].includes(it.objectClass)) - .map((it) => it.objectId) - .filter((it) => it != null) as string[] - const accounts = await ctx.client.db.getAccountsByPersonIds(personIds) - - if (accounts.length > 0) { - const spaceMembers = await ctx.client.db.getCardSpaceMembers(cardId) - for (const account of accounts) { - if (spaceMembers.includes(account)) { - collaborators.add(account) - } - } - } - } - - if (collaborators.size === 0) { - return [] - } - - return [ - { - type: NotificationEventType.AddCollaborators, - cardId, - cardType, - collaborators: Array.from(collaborators), - socialId, - date: new Date(date.getTime() - 1) - } - ] -} - -async function addThreadReply (ctx: TriggerCtx, event: Enriched): Promise { - if (event.messageType !== MessageType.Text || event.extra?.threadRoot === true) { - return [] - } - const { cardId, socialId, date } = event - const thread = (await ctx.client.db.findThreadMeta({ threadId: cardId, limit: 1 }))[0] - - if (thread === undefined) return [] - - return [ - { - type: MessageEventType.ThreadPatch, - cardId: thread.cardId, - messageId: thread.messageId, - operation: { - opcode: 'addReply', - threadId: thread.threadId - }, - socialId, - date - } - ] -} - -async function removeThreadReply (ctx: TriggerCtx, event: Enriched): Promise { - const { cardId } = event - const thread = (await ctx.client.db.findThreadMeta({ threadId: cardId, limit: 1 }))[0] - if (thread === undefined) return [] - - return [ - { - type: MessageEventType.ThreadPatch, - cardId: thread.cardId, - messageId: thread.messageId, - operation: { - opcode: 'removeReply', - threadId: thread.threadId - }, - date: event.date, - socialId: event.socialId - } - ] -} - -async function onThreadAttached (ctx: TriggerCtx, event: Enriched): Promise { - if (event.operation.opcode !== 'attach') return [] - const message = await ctx.client.findMessage(event.cardId, event.messageId, { attachments: true }) - - if (message === undefined) return [] - - const result: Event[] = [] - - if (message.type === MessageType.Activity || message.extra?.threadRoot === true) { - return [] - } - - const messageId = generateMessageId() - - result.push({ - messageId, - type: MessageEventType.CreateMessage, - messageType: message.type, - cardId: event.operation.threadId, - cardType: event.operation.threadType, - content: message.content, - extra: { ...message.extra, threadRoot: true }, - socialId: message.creator, - date: message.created, - options: { - noNotify: true - } - }) - - result.push({ - type: MessageEventType.AttachmentPatch, - cardId: event.operation.threadId, - messageId, - operations: [ - { - opcode: 'add', - attachments: message.attachments.map((it) => ({ - id: it.id, - mimeType: it.mimeType, - params: it.params - })) - } - ], - socialId: message.creator, - date: message.created - }) - - return result -} - -async function checkPeers (ctx: TriggerCtx, event: Enriched): Promise { - if (ctx.processedPeersEvents.has(event._id)) return [] - if (event.type === MessageEventType.CreateMessage) { - if (event.messageType === MessageType.Activity) { - return [] - } - } - - if (event.type === MessageEventType.ThreadPatch) { - return [] - } - - const cardPeers = new Set( - (((event._eventExtra.peers ?? []) as Peer[]).filter((it) => it.kind === 'card') as CardPeer[]) - .flatMap((it) => it.members) - .filter((it) => it.workspaceId === ctx.workspace && it.cardId !== event.cardId) - .map((it) => it.cardId) - ) - - if (cardPeers.size === 0) return [] - const res: Event[] = [] - - for (const peer of cardPeers) { - const ev = { - ...event, - _id: generateId(), - cardId: peer - } - - ctx.processedPeersEvents.add(ev._id) - - res.push(ev) - } - - return res -} - -const triggers: Triggers = [ - ['add_collaborators_on_message_created', MessageEventType.CreateMessage, addCollaborators as TriggerFn], - - ['add_thread_reply_on_message_created', MessageEventType.CreateMessage, addThreadReply as TriggerFn], - ['remove_reply_on_messages_removed', MessageEventType.RemovePatch, removeThreadReply as TriggerFn], - - ['on_thread_created', MessageEventType.ThreadPatch, onThreadAttached as TriggerFn], - - ['check_peers_on_message_created', MessageEventType.CreateMessage, checkPeers as TriggerFn], - ['check_peers_on_update_patch', MessageEventType.UpdatePatch, checkPeers as TriggerFn], - ['check_peers_on_remove_patch', MessageEventType.RemovePatch, checkPeers as TriggerFn], - ['check_peers_on_reaction_patch', MessageEventType.ReactionPatch, checkPeers as TriggerFn], - ['check_peers_on_blob_patch', MessageEventType.BlobPatch, checkPeers as TriggerFn], - ['check_peers_on_attachment_patch', MessageEventType.AttachmentPatch, checkPeers as TriggerFn], - ['check_peers_on_thread_patch', MessageEventType.ThreadPatch, checkPeers as TriggerFn] -] - -export default triggers diff --git a/foundations/communication/packages/server/src/triggers/notification.ts b/foundations/communication/packages/server/src/triggers/notification.ts deleted file mode 100644 index 8159f7a746..0000000000 --- a/foundations/communication/packages/server/src/triggers/notification.ts +++ /dev/null @@ -1,189 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - AddCollaboratorsEvent, - LabelEventType, - MessageEventType, - NotificationEventType, - type Event, - UpdateNotificationContextEvent, - RemovePatchEvent, - RemoveCollaboratorsEvent -} from '@hcengineering/communication-sdk-types' -import { - type AccountUuid, - type ActivityCollaboratorsUpdate, - ActivityUpdateType, - MessageType, - NotificationType, - SubscriptionLabelID -} from '@hcengineering/communication-types' -import { groupByArray } from '@hcengineering/core' - -import type { TriggerCtx, TriggerFn, Triggers } from '../types' -import { getAddCollaboratorsMessageContent, getRemoveCollaboratorsMessageContent } from './utils' - -async function onAddedCollaborators (ctx: TriggerCtx, event: AddCollaboratorsEvent): Promise { - const { cardId, cardType, collaborators } = event - - if (collaborators.length === 0) return [] - const result: Event[] = [] - - for (const collaborator of collaborators) { - result.push({ - type: LabelEventType.CreateLabel, - cardId, - cardType, - account: collaborator, - labelId: SubscriptionLabelID, - date: event.date - }) - } - - const account = (await ctx.client.findPersonUuid(ctx, event.socialId, true)) as AccountUuid | undefined - - const updateDate: ActivityCollaboratorsUpdate = { - type: ActivityUpdateType.Collaborators, - added: collaborators, - removed: [] - } - result.push({ - type: MessageEventType.CreateMessage, - messageType: MessageType.Activity, - cardId, - cardType, - content: await getAddCollaboratorsMessageContent(ctx, account, collaborators), - socialId: event.socialId, - date: event.date, - extra: { - action: 'update', - update: updateDate - } - }) - return result -} - -async function onRemovedCollaborators (ctx: TriggerCtx, event: RemoveCollaboratorsEvent): Promise { - const { cardId, collaborators } = event - if (collaborators.length === 0) return [] - const result: Event[] = [] - const contexts = await ctx.client.db.findNotificationContexts({ cardId, account: event.collaborators }) - for (const collaborator of collaborators) { - const context = contexts.find((it) => it.account === collaborator) - result.push({ - type: LabelEventType.RemoveLabel, - cardId, - account: collaborator, - labelId: SubscriptionLabelID, - date: event.date - }) - - if (context !== undefined && context.lastUpdate.getTime() > context.lastView.getTime()) { - result.push({ - type: NotificationEventType.UpdateNotificationContext, - contextId: context.id, - account: collaborator, - updates: { - lastView: context.lastUpdate - }, - date: new Date() - }) - } - } - - const updateDate: ActivityCollaboratorsUpdate = { - type: ActivityUpdateType.Collaborators, - added: [], - removed: collaborators - } - const account = (await ctx.client.findPersonUuid(ctx, event.socialId, true)) as AccountUuid | undefined - result.push({ - type: MessageEventType.CreateMessage, - messageType: MessageType.Activity, - cardId, - cardType: event.cardType, - content: await getRemoveCollaboratorsMessageContent(ctx, account, collaborators), - socialId: event.socialId, - date: event.date, - extra: { - action: 'update', - update: updateDate - } - }) - - return result -} - -async function onNotificationContextUpdated (ctx: TriggerCtx, event: UpdateNotificationContextEvent): Promise { - const { contextId, updates } = event - const { lastView } = updates - if (lastView == null) return [] - - const context = (await ctx.client.db.findNotificationContexts({ id: contextId }))[0] - if (context == null) return [] - const result: Event[] = [] - - result.push({ - type: NotificationEventType.UpdateNotification, - account: context.account, - contextId: context.id, - query: { - type: NotificationType.Message, - untilDate: context.lastView - }, - updates: { - read: true - } - }) - - return result -} - -async function onMessagesRemoved (ctx: TriggerCtx, event: RemovePatchEvent): Promise { - const notifications = await ctx.client.db.findNotifications({ - cardId: event.cardId, - messageId: event.messageId - }) - - if (notifications.length === 0) return [] - - const result: Event[] = [] - - const byContextId = groupByArray(notifications, (it) => it.contextId) - for (const [context, ns] of byContextId.entries()) { - result.push({ - type: NotificationEventType.RemoveNotifications, - contextId: context, - account: ns[0].account, - ids: notifications.map((it) => it.id) - }) - } - - return result -} - -const triggers: Triggers = [ - [ - 'on_notification_context_updated', - NotificationEventType.UpdateNotificationContext, - onNotificationContextUpdated as TriggerFn - ], - ['on_added_collaborators', NotificationEventType.AddCollaborators, onAddedCollaborators as TriggerFn], - ['on_removed_collaborators', NotificationEventType.RemoveCollaborators, onRemovedCollaborators as TriggerFn], - ['remove_notifications_on_messages_removed', MessageEventType.RemovePatch, onMessagesRemoved as TriggerFn] -] - -export default triggers diff --git a/foundations/communication/packages/server/src/triggers/utils.ts b/foundations/communication/packages/server/src/triggers/utils.ts deleted file mode 100644 index e8f6c76df4..0000000000 --- a/foundations/communication/packages/server/src/triggers/utils.ts +++ /dev/null @@ -1,55 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { type AccountUuid, type Markdown, type SocialID } from '@hcengineering/communication-types' - -import { TriggerCtx } from '../types' - -export async function getNameBySocialID (ctx: TriggerCtx, id: SocialID): Promise { - const account = (await ctx.client.findPersonUuid(ctx, id, true)) as AccountUuid | undefined - return account != null ? ((await ctx.client.db.getNameByAccount(account)) ?? 'System') : 'System' -} - -export async function getAddCollaboratorsMessageContent ( - ctx: TriggerCtx, - sender: AccountUuid | undefined, - collaborators: AccountUuid[] -): Promise { - if (sender != null && collaborators.length === 1 && collaborators.includes(sender)) { - return 'Joined card' - } - - const collaboratorsNames = (await Promise.all(collaborators.map((it) => ctx.client.db.getNameByAccount(it)))).filter( - (it): it is string => it != null && it !== '' - ) - - return `Added ${collaboratorsNames.join(', ')}` -} - -export async function getRemoveCollaboratorsMessageContent ( - ctx: TriggerCtx, - sender: AccountUuid | undefined, - collaborators: AccountUuid[] -): Promise { - if (sender != null && collaborators.length === 1 && collaborators.includes(sender)) { - return 'Left card' - } - - const collaboratorsNames = (await Promise.all(collaborators.map((it) => ctx.client.db.getNameByAccount(it)))).filter( - (it): it is string => it != null && it !== '' - ) - - return `Removed ${collaboratorsNames.join(', ')}` -} diff --git a/foundations/communication/packages/server/src/types.ts b/foundations/communication/packages/server/src/types.ts deleted file mode 100644 index b01638392b..0000000000 --- a/foundations/communication/packages/server/src/types.ts +++ /dev/null @@ -1,117 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { Account, MeasureContext } from '@hcengineering/core' -import type { - EventResult, - Event, - SessionData, EventType -} from '@hcengineering/communication-sdk-types' -import type { - CardID, - Collaborator, - FindCollaboratorsParams, - FindLabelsParams, FindMessagesGroupParams, FindMessagesMetaParams, - FindNotificationContextParams, - FindNotificationsParams, - FindPeersParams, - Label, MessageMeta, MessagesGroup, - Notification, - NotificationContext, - Peer, - WorkspaceUuid -} from '@hcengineering/communication-types' - -import { LowLevelClient } from './client' - -export interface Metadata { - accountsUrl: string - hulylakeUrl: string - secret: string - messagesPerBlob: number -} - -export type Subscription = string | number - -export interface Middleware { - findMessagesMeta: (session: SessionData, params: FindMessagesMetaParams) => Promise - findMessagesGroups: (session: SessionData, params: FindMessagesGroupParams) => Promise - findNotificationContexts: ( - session: SessionData, - params: FindNotificationContextParams, - subscription?: Subscription - ) => Promise - findNotifications: ( - session: SessionData, - params: FindNotificationsParams, - subscription?: Subscription - ) => Promise - - findLabels: (session: SessionData, params: FindLabelsParams, subscription?: Subscription) => Promise - findCollaborators: (session: SessionData, params: FindCollaboratorsParams) => Promise - findPeers: (session: SessionData, params: FindPeersParams) => Promise - - event: (session: SessionData, event: Enriched, derived: boolean) => Promise - - subscribeCard: (session: SessionData, cardId: CardID, subscription: Subscription) => void - unsubscribeCard: (session: SessionData, cardId: CardID, subscription: Subscription) => void - - handleBroadcast: (session: SessionData, events: Enriched[]) => void - - closeSession: (sessionId: string) => void - close: () => void -} - -export interface MiddlewareContext { - ctx: MeasureContext - workspace: WorkspaceUuid - metadata: Metadata - client: LowLevelClient - - cadsWithPeers: Set - - derived?: Middleware - head?: Middleware -} - -export type MiddlewareCreateFn = (context: MiddlewareContext, next?: Middleware) => Promise - -export interface CommunicationCallbacks { - registerAsyncRequest: (ctx: MeasureContext, promise: (ctx: MeasureContext) => Promise) => void - broadcast: (ctx: MeasureContext, result: Record[]>) => void - enqueue: (ctx: MeasureContext, result: Enriched[]) => void -} - -export interface TriggerCtx { - ctx: MeasureContext - metadata: Metadata - client: LowLevelClient - workspace: WorkspaceUuid - account: Account - derived: boolean - processedPeersEvents: Set - execute: (event: Event) => Promise -} - -export type TriggerFn = (ctx: TriggerCtx, event: Enriched) => Promise -export type Triggers = [string, EventType, TriggerFn][] - -export type Enriched = T & { - _id: string - _eventExtra: Record - - skipPropagate?: boolean - date: Date -} diff --git a/foundations/communication/packages/server/tsconfig.json b/foundations/communication/packages/server/tsconfig.json deleted file mode 100644 index 2ad347b11d..0000000000 --- a/foundations/communication/packages/server/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo", - "types": ["node", "jest"] - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} diff --git a/foundations/communication/packages/shared/.eslintrc.cjs b/foundations/communication/packages/shared/.eslintrc.cjs deleted file mode 100644 index 72235dc283..0000000000 --- a/foundations/communication/packages/shared/.eslintrc.cjs +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/foundations/communication/packages/shared/CHANGELOG.json b/foundations/communication/packages/shared/CHANGELOG.json deleted file mode 100644 index 153aedde84..0000000000 --- a/foundations/communication/packages/shared/CHANGELOG.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "name": "@hcengineering/communication-shared", - "entries": [ - { - "version": "0.7.11", - "tag": "@hcengineering/communication-shared_v0.7.11", - "date": "Mon, 27 Oct 2025 16:28:25 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/communication-types\" from `^0.7.11` to `0.7.12`" - }, - { - "comment": "Updating dependency \"@hcengineering/communication-sdk-types\" from `^0.7.11` to `0.7.12`" - } - ] - } - }, - { - "version": "0.7.8", - "tag": "@hcengineering/communication-shared_v0.7.8", - "date": "Sat, 25 Oct 2025 22:20:35 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/communication-types\" from `^0.7.8` to `0.7.9`" - }, - { - "comment": "Updating dependency \"@hcengineering/communication-sdk-types\" from `^0.7.8` to `0.7.9`" - } - ] - } - }, - { - "version": "0.7.4", - "tag": "@hcengineering/communication-shared_v0.7.4", - "date": "Tue, 14 Oct 2025 10:12:38 GMT", - "comments": { - "patch": [ - { - "comment": "Fix update patch event apply" - }, - { - "comment": "update hulylake client" - }, - { - "comment": "update deps" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/communication-types\" from `^0.7.3` to `0.7.4`" - }, - { - "comment": "Updating dependency \"@hcengineering/communication-sdk-types\" from `^0.7.3` to `0.7.4`" - } - ] - } - } - ] -} diff --git a/foundations/communication/packages/shared/CHANGELOG.md b/foundations/communication/packages/shared/CHANGELOG.md deleted file mode 100644 index 4c189468e2..0000000000 --- a/foundations/communication/packages/shared/CHANGELOG.md +++ /dev/null @@ -1,23 +0,0 @@ -# Change Log - @hcengineering/communication-shared - -This log was last generated on Mon, 27 Oct 2025 16:28:25 GMT and should not be manually modified. - -## 0.7.11 -Mon, 27 Oct 2025 16:28:25 GMT - -_Version update only_ - -## 0.7.8 -Sat, 25 Oct 2025 22:20:35 GMT - -_Version update only_ - -## 0.7.4 -Tue, 14 Oct 2025 10:12:38 GMT - -### Patches - -- Fix update patch event apply -- update hulylake client -- update deps - diff --git a/foundations/communication/packages/shared/config/rig.json b/foundations/communication/packages/shared/config/rig.json deleted file mode 100644 index 0110930f55..0000000000 --- a/foundations/communication/packages/shared/config/rig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig" -} diff --git a/foundations/communication/packages/shared/package.json b/foundations/communication/packages/shared/package.json deleted file mode 100644 index f4f29c1fce..0000000000 --- a/foundations/communication/packages/shared/package.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "name": "@hcengineering/communication-shared", - "version": "0.7.11", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "files": [ - "lib/**/*", - "types/**/*", - "src/**/*", - "tsconfig.json" - ], - "scripts": { - "build": "../scripts/compile.js", - "_phase:build": "compile transpile src", - "_phase:validate": "compile validate", - "lint": "eslint \"src/**/*.ts\"", - "lint:fix": "eslint --fix \"src/**/*.ts\"", - "format": "prettier --write src/**/*.ts && eslint --fix \"src/**/*.ts\" && echo 'Formatted'", - "clean": "rm -rf lib && rm -rf types rm -rf node_modules" - }, - "devDependencies": { - "@hcengineering/platform-rig": "workspace:^0.7.19", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "@typescript-eslint/parser": "^6.21.0", - "esbuild": "^0.25.10", - "esbuild-plugin-copy": "^2.1.1", - "eslint": "^8.54.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-n": "^15.4.0", - "eslint-plugin-promise": "^6.1.1", - "jest": "^29.7.0", - "@types/jest": "^29.5.5", - "prettier": "^3.6.2", - "typescript": "^5.9.3" - }, - "dependencies": { - "@hcengineering/communication-types": "workspace:^0.7.12", - "@hcengineering/communication-sdk-types": "workspace:^0.7.12", - "@hcengineering/hulylake-client": "workspace:^0.7.17" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/hcengineering/communication.git" - }, - "publishConfig": { - "access": "public" - }, - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.js" - } - } -} diff --git a/foundations/communication/packages/shared/src/index.ts b/foundations/communication/packages/shared/src/index.ts deleted file mode 100644 index 5805bbae84..0000000000 --- a/foundations/communication/packages/shared/src/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export * from './retry' -export * from './utils' -export * from './processor' diff --git a/foundations/communication/packages/shared/src/processor.ts b/foundations/communication/packages/shared/src/processor.ts deleted file mode 100644 index d32a28e2ae..0000000000 --- a/foundations/communication/packages/shared/src/processor.ts +++ /dev/null @@ -1,437 +0,0 @@ -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. - -import { - AttachmentID, - ContextID, - Message, - MessageID, - Notification, - NotificationContext, - NotificationID, - PersonUuid, - Emoji, - AttachmentData, - SocialID, - Attachment, - AttachmentUpdateData, - CardID, - CardType -} from '@hcengineering/communication-types' -import { - AddAttachmentsOperation, - CreateMessageEvent, - CreateNotificationContextEvent, - CreateNotificationEvent, - MessageEventType, - PatchEvent, - RemoveAttachmentsOperation, - RemoveNotificationContextEvent, - SetAttachmentsOperation, - UpdateAttachmentsOperation, - UpdateNotificationContextEvent, - ReactionPatchEvent, - AttachmentPatchEvent, - BlobPatchEvent, - ThreadPatchEvent -} from '@hcengineering/communication-sdk-types' - -import { withTotal } from './utils' - -// eslint-disable-next-line @typescript-eslint/no-extraneous-class -export class MessageProcessor { - static create (event: CreateMessageEvent, id?: MessageID): Message { - const messageId = event.messageId ?? (id as MessageID) - if (messageId == null) throw new Error('Message id is required') - return { - id: messageId, - cardId: event.cardId, - type: event.messageType, - content: event.content, - extra: event.extra, - creator: event.socialId, - created: new Date(event.date ?? Date.now()), - language: event.language, - reactions: {}, - attachments: [], - threads: [] - } - } - - static applyPatch (message: Message, patchEvent: PatchEvent): Message | undefined { - return applyPatchEvent(message, patchEvent) - } -} - -// eslint-disable-next-line @typescript-eslint/no-extraneous-class -export class NotificationContextProcessor { - static create (event: CreateNotificationContextEvent, id?: ContextID): NotificationContext { - const contextId: ContextID | undefined = event.contextId ?? id - - if (contextId == null) { - throw new Error('Notification context id is required') - } - return { - id: contextId, - cardId: event.cardId, - account: event.account, - lastView: event.lastView, - lastUpdate: event.lastUpdate, - lastNotify: event.lastNotify, - notifications: withTotal([] as Notification[]) - } - } - - static update (context: NotificationContext, event: UpdateNotificationContextEvent): NotificationContext { - if (context.account !== event.account || context.id !== event.contextId) return context - return { - ...context, - lastView: event.updates.lastView ?? context.lastView, - lastUpdate: event.updates.lastUpdate ?? context.lastUpdate, - lastNotify: event.updates.lastNotify ?? context.lastNotify - } - } - - static remove (context: NotificationContext, event: RemoveNotificationContextEvent): NotificationContext | undefined { - if (context.account !== event.account || context.id !== event.contextId) return context - return undefined - } -} - -// eslint-disable-next-line @typescript-eslint/no-extraneous-class -export class NotificationProcessor { - static create (event: CreateNotificationEvent, id?: NotificationID): Notification { - const notificationId: NotificationID | undefined = event.notificationId ?? (id as NotificationID) - - if (notificationId == null) { - throw new Error('Notification id is required') - } - return { - id: notificationId, - cardId: event.cardId, - contextId: event.contextId, - account: event.account, - type: event.notificationType, - read: event.read, - content: event.content ?? {}, - created: event.date ?? new Date(), - messageId: event.messageId, - creator: event.creator, - blobId: event.blobId - } - } -} - -function applyPatchEvent (message: Message, event: PatchEvent): Message | undefined { - if (message.cardId !== event.cardId || message.id !== event.messageId) return message - const date = event.date ?? new Date() - - switch (event.type) { - case MessageEventType.UpdatePatch: { - if (date.getTime() < (message.modified?.getTime() ?? 0)) { - return message - } - const shouldUpdateDate = event.content != null || event.extra != null - return { - ...message, - modified: shouldUpdateDate ? date : message.modified, - content: event.content ?? message.content, - extra: event.extra ?? message.extra, - language: event.language ?? message.language - } - } - case MessageEventType.RemovePatch: { - return undefined - } - case MessageEventType.ReactionPatch: - return patchReactions(message, event, date) - case MessageEventType.AttachmentPatch: - return patchAttachments(message, event, date) - case MessageEventType.BlobPatch: - return patchBlobs(message, event, date) - case MessageEventType.ThreadPatch: - return patchThread(message, event, date) - } - return message -} - -function patchReactions (message: Message, event: ReactionPatchEvent, date: Date): Message { - if (event.personUuid == null) return message - if (event.operation.opcode === 'add') { - return addReaction(message, event.operation.reaction, event.personUuid, date) - } else if (event.operation.opcode === 'remove') { - return removeReaction(message, event.operation.reaction, event.personUuid) - } - return message -} - -function addReaction (message: Message, emoji: Emoji, creator: PersonUuid, created: Date): Message { - const emojiData = message.reactions[emoji] ?? [] - if (emojiData.some((it) => it.person === creator)) return message - - const data = { - count: 1, - person: creator, - date: created - } - - message.reactions = { - ...message.reactions, - [emoji]: [...emojiData, data] - } - return message -} - -function removeReaction (message: Message, emoji: Emoji, creator: PersonUuid): Message { - const emojiData = message.reactions[emoji] ?? [] - if (!emojiData.some((it) => it.person === creator)) return message - - return { - ...message, - reactions: { - ...message.reactions, - [emoji]: emojiData.filter((it) => it.person !== creator) - } - } -} - -function patchAttachments (message: Message, event: AttachmentPatchEvent, date: Date): Message { - for (const op of event.operations) { - if (op.opcode === 'add') { - return addAttachments(message, op.attachments, event.socialId, date) - } else if (op.opcode === 'remove') { - return removeAttachments(message, op.ids) - } else if (op.opcode === 'set') { - return setAttachments(message, op.attachments, event.socialId, date) - } else if (op.opcode === 'update') { - return updateAttachments(message, op.attachments, date) - } - } - return message -} - -function patchBlobs (message: Message, event: BlobPatchEvent, date: Date): Message { - const operations: ( - | AddAttachmentsOperation - | RemoveAttachmentsOperation - | SetAttachmentsOperation - | UpdateAttachmentsOperation - )[] = [] - for (const op of event.operations) { - if (op.opcode === 'attach') { - operations.push({ - opcode: 'add', - attachments: op.blobs.map((it) => ({ - id: it.blobId as any as AttachmentID, - mimeType: it.mimeType ?? it.mimeType, - params: it - })) - }) - } else if (op.opcode === 'detach') { - operations.push({ - opcode: 'remove', - ids: op.blobIds as any as AttachmentID[] - }) - } else if (op.opcode === 'set') { - operations.push({ - opcode: 'set', - attachments: op.blobs.map((it) => ({ - id: it.blobId as any as AttachmentID, - mimeType: it.mimeType ?? it.mimeType, - params: it - })) - }) - } else if (op.opcode === 'update') { - operations.push({ - opcode: 'update', - attachments: op.blobs.map((it) => ({ - id: it.blobId as any as AttachmentID, - params: it - })) - }) - } - } - - const ev: AttachmentPatchEvent = { - type: MessageEventType.AttachmentPatch, - cardId: message.cardId, - messageId: message.id, - operations, - socialId: event.socialId, - date: event.date - } - - return patchAttachments(message, ev, date) -} - -function addAttachments (message: Message, data: AttachmentData[], creator: SocialID, created: Date): Message { - const newAttachments: Attachment[] = [] - for (const attach of data) { - const isExists = message.attachments.some((it) => it.id === attach.id) - if (isExists === undefined) continue - const attachment: Attachment = { - ...attach, - created, - creator - } as any - newAttachments.push(attachment) - } - - if (newAttachments.length === 0) return message - return { - ...message, - attachments: [...message.attachments, ...newAttachments] - } -} - -function updateAttachments (message: Message, updates: AttachmentUpdateData[], date: Date): Message { - if (updates.length === 0) return message - const updatedAttachments: Attachment[] = [] - for (const attachment of message.attachments) { - const update = updates.find((it) => it.id === attachment.id) - if (update === undefined) { - updatedAttachments.push(attachment) - } else { - updatedAttachments.push({ - ...attachment, - params: { - ...attachment.params, - ...update.params - }, - modified: date.getTime() > (attachment.modified?.getTime() ?? 0) ? date : attachment.modified - } as any) - } - } - - return { - ...message, - attachments: updatedAttachments - } -} - -function removeAttachments (message: Message, ids: AttachmentID[]): Message { - const attachments = message.attachments.filter((it) => !ids.includes(it.id)) - if (attachments.length === message.attachments.length) return message - - return { - ...message, - attachments - } -} - -function setAttachments (message: Message, data: AttachmentData[], creator: SocialID, created: Date): Message { - if (data.length === 0) return message - return { - ...message, - attachments: data.map( - (it) => - ({ - ...it, - created, - creator - }) as any - ) - } -} - -function patchThread (message: Message, event: ThreadPatchEvent, date: Date): Message { - const { operation } = event - if (operation.opcode === 'attach') { - return attachThread(message, operation.threadId, operation.threadType) - } else if (operation.opcode === 'update') { - return updateThread(message, operation.threadId, operation.update.threadType) - } else if (operation.opcode === 'addReply') { - if (event.personUuid == null) return message - return addReply(message, operation.threadId, event.personUuid, date) - } else if (operation.opcode === 'removeReply') { - if (event.personUuid == null) return message - return removeReply(message, operation.threadId, event.personUuid) - } - return message -} - -function attachThread (message: Message, threadId: CardID, threadType: CardType): Message { - if (message.threads.some((thread) => thread.threadId === threadId)) return message - return { - ...message, - threads: [ - ...message.threads, - { - cardId: message.cardId, - messageId: message.id, - threadId, - threadType, - repliesCount: 0, - lastReplyDate: undefined, - repliedPersons: {} - } - ] - } -} - -function updateThread (message: Message, threadId: CardID, threadType: CardType): Message { - if (message.threads.length === 0) return message - const t = message.threads.find((it) => it.threadId === threadId) - if (t === undefined) return message - - return { - ...message, - threads: message.threads.map((it) => - it.threadId === threadId - ? { - ...it, - threadType - } - : it - ) - } -} - -function addReply (message: Message, threadId: CardID, person: PersonUuid, date: Date): Message { - if (!message.threads.some((thread) => thread.threadId === threadId)) return message - return { - ...message, - threads: message.threads.map((it) => - it.threadId === threadId - ? { - ...it, - repliesCount: it.repliesCount + 1, - lastReplyDate: date.getTime() > (it.lastReplyDate?.getTime() ?? 0) ? date : it.lastReplyDate, - repliedPersons: { - ...it.repliedPersons, - [person]: (it.repliedPersons[person] ?? 0) + 1 - } - } - : it - ) - } -} - -function removeReply (message: Message, threadId: CardID, person: PersonUuid): Message { - if (!message.threads.some((thread) => thread.threadId === threadId)) return message - return { - ...message, - threads: message.threads.map((it) => - it.threadId === threadId - ? { - ...it, - repliesCount: Math.max(0, it.repliesCount - 1), - repliedPersons: { - ...it.repliedPersons, - [person]: Math.max(0, (it.repliedPersons[person] ?? 0) - 1) - } - } - : it - ) - } -} diff --git a/foundations/communication/packages/shared/src/retry.ts b/foundations/communication/packages/shared/src/retry.ts deleted file mode 100644 index 455f4a5709..0000000000 --- a/foundations/communication/packages/shared/src/retry.ts +++ /dev/null @@ -1,35 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export interface RetryOptions { - retries: number - delay?: number -} - -export async function retry (op: () => Promise, { retries, delay }: RetryOptions): Promise { - let error: any - while (retries > 0) { - retries-- - try { - return await op() - } catch (err: any) { - error = err - if (retries !== 0 && delay !== undefined && delay > 0) { - await new Promise((resolve) => setTimeout(resolve, delay)) - } - } - } - throw error -} diff --git a/foundations/communication/packages/shared/src/utils.ts b/foundations/communication/packages/shared/src/utils.ts deleted file mode 100644 index b677e6aa10..0000000000 --- a/foundations/communication/packages/shared/src/utils.ts +++ /dev/null @@ -1,251 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - AppletAttachment, - Attachment, - BlobAttachment, - BlobID, - CardID, - type Emoji, - FindMessagesOptions, - FindMessagesParams, - LinkPreviewAttachment, - linkPreviewType, - Message, - MessageDoc, - MessageID, - MessagesDoc, - MessagesGroup, - MessagesGroupDoc, - MessagesGroupsDoc, - type PersonUuid, - SortingOrder, - TranslatedMessage, - TranslatedMessagesDoc, - WithTotal -} from '@hcengineering/communication-types' -import { type HulylakeWorkspaceClient } from '@hcengineering/hulylake-client' - -const COUNTER_BITS = 10n -const RANDOM_BITS = 10n -const MAX_SEQUENCE = (1n << COUNTER_BITS) - 1n - -let counter = 0n - -function makeBigIntId (): bigint { - const ts = BigInt(Date.now()) - counter = counter < MAX_SEQUENCE ? counter + 1n : 0n - const random = BigInt(Math.floor(Math.random() * Number((1n << RANDOM_BITS) - 1n))) - return (ts << (COUNTER_BITS + RANDOM_BITS)) | (counter << RANDOM_BITS) | random -} - -function toBase64Url (bytes: Uint8Array): string { - let s = '' - for (const b of bytes) s += String.fromCharCode(b) - // @ts-expect-error browser btoa / node Buffer - const base64 = typeof btoa === 'function' ? btoa(s) : Buffer.from(bytes).toString('base64') - return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') -} - -export function generateMessageId (): MessageID { - const idBig = makeBigIntId() - const buf = new Uint8Array(8) - new DataView(buf.buffer).setBigUint64(0, idBig, false) - return toBase64Url(buf) as MessageID -} - -export function isAppletAttachment (attachment: Attachment): attachment is AppletAttachment { - return attachment.mimeType.startsWith('application/vnd.huly.applet.') -} - -export function isLinkPreviewAttachmentType (mimeType: string): boolean { - return mimeType === linkPreviewType -} - -export function isAppletAttachmentType (mimeType: string): boolean { - return mimeType.startsWith('application/vnd.huly.applet.') -} - -export function isBlobAttachmentType (mimeType: string): boolean { - return !isLinkPreviewAttachmentType(mimeType) && !isAppletAttachmentType(mimeType) -} - -export function isLinkPreviewAttachment (attachment: Attachment): attachment is LinkPreviewAttachment { - return attachment.mimeType === linkPreviewType -} - -export function isBlobAttachment (attachment: Attachment): attachment is BlobAttachment { - return !isLinkPreviewAttachment(attachment) && !isAppletAttachment(attachment) && 'blobId' in attachment.params -} - -export function withTotal (objects: T[], total?: number): WithTotal { - const length = total ?? objects.length - - return Object.assign(objects, { total: length }) -} - -export async function loadMessagesGroups (client: HulylakeWorkspaceClient, cardId: CardID): Promise { - const res = await client.getJson(`${cardId}/messages/groups`, { - maxRetries: 3, - isRetryable: () => true, - delayStrategy: { - getDelay: () => 500 - } - }) - - if (res?.body === undefined) { - return [] - } - return Object.values(res.body) - .map((it) => deserializeMessageGroup(it)) - .sort((a, b) => a.fromDate.getTime() - b.fromDate.getTime()) -} - -function deserializeMessageGroup (group: MessagesGroupDoc): MessagesGroup { - return { - cardId: group.cardId, - blobId: group.blobId, - fromDate: new Date(group.fromDate), - toDate: new Date(group.toDate), - count: group.count - } -} - -export async function loadMessages ( - client: HulylakeWorkspaceClient, - blobId: BlobID, - params: FindMessagesParams, - options?: FindMessagesOptions -): Promise { - const { cardId } = params - const res = await client.getJson(`${cardId}/messages/${blobId}`, { - maxRetries: 3, - isRetryable: () => true, - delayStrategy: { - getDelay: () => 500 - } - }) - if (res?.body === undefined) { - return [] - } - return parseMessagesDoc(res.body, params, options) -} - -export function parseMessagesDoc ( - json: MessagesDoc, - params: FindMessagesParams, - options?: FindMessagesOptions -): Message[] { - let messages: Record = {} - if (params.id != null) { - const value = (json.messages as any)[params.id] - - if (value == null) { - return [] - } - - messages = { - [params.id]: value - } - } else { - messages = json.messages - } - - const result: Message[] = [] - for (const m of Object.values(messages)) { - if (params.limit != null && result.length >= params.limit) break - const message: Message = { - id: m.id, - cardId: m.cardId, - created: new Date(m.created), - creator: m.creator, - type: m.type, - content: m.content, - extra: m.extra, - language: m.language ?? undefined, - modified: m.modified != null ? new Date(m.modified) : undefined, - reactions: {}, - attachments: [], - threads: [] - } - - if (options?.reactions === true) { - for (const [emoji, users] of Object.entries(m.reactions)) { - for (const [user, data] of Object.entries(users)) { - const messageData = message.reactions[emoji as Emoji] ?? [] - messageData.push({ - count: Number(data.count), - person: user as PersonUuid, - date: new Date(data.date) - }) - message.reactions[emoji as Emoji] = messageData - } - } - } - - if (options?.attachments === true) { - for (const attachment of Object.values(m.attachments)) { - message.attachments.push({ - id: attachment.id, - mimeType: attachment.mimeType, - params: attachment.params as any, - creator: m.creator, - created: new Date(m.created) - }) - } - } - - if (options?.threads === true) { - for (const thread of Object.values(m.threads)) { - message.threads.push({ - cardId: m.cardId, - messageId: m.id, - threadId: thread.threadId, - threadType: thread.threadType, - repliesCount: Number(thread.repliesCount), - lastReplyDate: thread.lastReplyDate != null ? new Date(thread.lastReplyDate) : undefined, - repliedPersons: thread.repliedPersons - }) - } - } - - result.push(message) - } - - if (params.order === SortingOrder.Ascending) { - result.sort((a, b) => a.created.getTime() - b.created.getTime()) - } else if (params.order === SortingOrder.Descending) { - result.sort((a, b) => b.created.getTime() - a.created.getTime()) - } - return result -} - -export function parseTranslatedMessagesDoc (json: TranslatedMessagesDoc): TranslatedMessage[] { - const result: TranslatedMessage[] = [] - for (const m of Object.values(json.messages)) { - const message: TranslatedMessage = { - id: m.id, - created: new Date(m.created), - creator: m.creator, - content: m.content - } - - result.push(message) - } - - result.sort((a, b) => a.created.getTime() - b.created.getTime()) - return result -} diff --git a/foundations/communication/packages/shared/tsconfig.json b/foundations/communication/packages/shared/tsconfig.json deleted file mode 100644 index b5ae22f6e4..0000000000 --- a/foundations/communication/packages/shared/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/foundations/communication/packages/types/.eslintrc.cjs b/foundations/communication/packages/types/.eslintrc.cjs deleted file mode 100644 index 72235dc283..0000000000 --- a/foundations/communication/packages/types/.eslintrc.cjs +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/foundations/communication/packages/types/CHANGELOG.json b/foundations/communication/packages/types/CHANGELOG.json deleted file mode 100644 index 7f11edfb9e..0000000000 --- a/foundations/communication/packages/types/CHANGELOG.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "@hcengineering/communication-types", - "entries": [ - { - "version": "0.7.12", - "tag": "@hcengineering/communication-types_v0.7.12", - "date": "Mon, 27 Oct 2025 16:28:25 GMT", - "comments": { - "patch": [ - { - "comment": "update deps" - } - ] - } - }, - { - "version": "0.7.9", - "tag": "@hcengineering/communication-types_v0.7.9", - "date": "Sat, 25 Oct 2025 22:20:35 GMT", - "comments": { - "patch": [ - { - "comment": "Add collaborativeChange activity update" - }, - { - "comment": "Bump core" - } - ] - } - }, - { - "version": "0.7.4", - "tag": "@hcengineering/communication-types_v0.7.4", - "date": "Tue, 14 Oct 2025 10:12:38 GMT", - "comments": { - "patch": [ - { - "comment": "update deps" - } - ] - } - } - ] -} diff --git a/foundations/communication/packages/types/CHANGELOG.md b/foundations/communication/packages/types/CHANGELOG.md deleted file mode 100644 index 1f1d95096b..0000000000 --- a/foundations/communication/packages/types/CHANGELOG.md +++ /dev/null @@ -1,26 +0,0 @@ -# Change Log - @hcengineering/communication-types - -This log was last generated on Mon, 27 Oct 2025 16:28:25 GMT and should not be manually modified. - -## 0.7.12 -Mon, 27 Oct 2025 16:28:25 GMT - -### Patches - -- update deps - -## 0.7.9 -Sat, 25 Oct 2025 22:20:35 GMT - -### Patches - -- Add collaborativeChange activity update -- Bump core - -## 0.7.4 -Tue, 14 Oct 2025 10:12:38 GMT - -### Patches - -- update deps - diff --git a/foundations/communication/packages/types/config/rig.json b/foundations/communication/packages/types/config/rig.json deleted file mode 100644 index 0110930f55..0000000000 --- a/foundations/communication/packages/types/config/rig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig" -} diff --git a/foundations/communication/packages/types/package.json b/foundations/communication/packages/types/package.json deleted file mode 100644 index c8e4fa5878..0000000000 --- a/foundations/communication/packages/types/package.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "name": "@hcengineering/communication-types", - "version": "0.7.12", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "files": [ - "lib/**/*", - "types/**/*", - "src/**/*", - "tsconfig.json" - ], - "scripts": { - "build": "compile", - "_phase:build": "compile transpile src", - "_phase:validate": "compile validate", - "lint": "eslint \"src/**/*.ts\"", - "lint:fix": "eslint --fix \"src/**/*.ts\"", - "format": "prettier --write src/**/*.ts && eslint --fix \"src/**/*.ts\" && echo 'Formatted'", - "clean": "rm -rf lib && rm -rf types rm -rf node_modules" - }, - "devDependencies": { - "@hcengineering/platform-rig": "workspace:^0.7.19", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "@typescript-eslint/parser": "^6.21.0", - "esbuild": "^0.25.10", - "esbuild-plugin-copy": "^2.1.1", - "eslint": "^8.54.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-n": "^15.4.0", - "eslint-plugin-promise": "^6.1.1", - "jest": "^29.7.0", - "@types/jest": "^29.5.5", - "prettier": "^3.6.2", - "typescript": "^5.9.3" - }, - "dependencies": { - "@hcengineering/core": "workspace:^0.7.22" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/hcengineering/communication.git" - }, - "publishConfig": { - "access": "public" - }, - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.js" - } - } -} diff --git a/foundations/communication/packages/types/src/core.ts b/foundations/communication/packages/types/src/core.ts deleted file mode 100644 index 0f096e10ec..0000000000 --- a/foundations/communication/packages/types/src/core.ts +++ /dev/null @@ -1,26 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { Ref, Blob, AccountUuid, WorkspaceUuid, PersonId, BlobMetadata, PersonUuid } from '@hcengineering/core' - -export type { AccountUuid, PersonUuid, WorkspaceUuid, BlobMetadata } - -export type BlobID = Ref -export type CardID = Ref -export type CardType = Ref -export type SocialID = PersonId - -export type Markdown = string -export type ID = string diff --git a/foundations/communication/packages/types/src/file.ts b/foundations/communication/packages/types/src/file.ts deleted file mode 100644 index 1acbf257bd..0000000000 --- a/foundations/communication/packages/types/src/file.ts +++ /dev/null @@ -1,81 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { BlobID, CardID, CardType, Markdown, PersonUuid, SocialID } from './core' -import { MessageID, MessageType, MessageExtra, AttachmentID, AttachmentParams, Emoji } from './message' - -export interface MessagesDoc { - cardId: CardID - fromDate: string // ISO date - toDate: string // ISO date - messages: Record - language: string -} - -export interface MessageDoc { - id: MessageID - cardId: CardID - created: string // ISO date - creator: SocialID - type: MessageType - content: Markdown - extra: MessageExtra - language: string | null - modified: string | null // ISO date - - reactions: Record> - attachments: Record - threads: Record -} - -export interface AttachmentDoc { - id: AttachmentID - mimeType: string - params: AttachmentParams - creator: SocialID - created: string // ISO date - modified: string | null // ISO date -} - -export interface ThreadDoc { - threadId: CardID - threadType: CardType - repliesCount: number - lastReplyDate: string | null // ISO date - repliedPersons: Record -} - -export type MessagesGroupsDoc = Record - -export interface MessagesGroupDoc { - cardId: CardID - blobId: BlobID - fromDate: string // ISO date - toDate: string // ISO date - count: number -} - -export interface TranslatedMessagesDoc { - cardId: CardID - messages: Record - language: string -} - -export interface TranslatesMessageDoc { - id: MessageID - created: string // ISO date - creator: SocialID - content: Markdown -} diff --git a/foundations/communication/packages/types/src/index.ts b/foundations/communication/packages/types/src/index.ts deleted file mode 100644 index 7411108b88..0000000000 --- a/foundations/communication/packages/types/src/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export * from './core' -export * from './file' -export * from './message' -export * from './notification' -export * from './query' -export * from './label' -export * from './peer' diff --git a/foundations/communication/packages/types/src/label.ts b/foundations/communication/packages/types/src/label.ts deleted file mode 100644 index 19f96dfe5e..0000000000 --- a/foundations/communication/packages/types/src/label.ts +++ /dev/null @@ -1,28 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { AccountUuid, CardID, CardType } from './core' - -export type LabelID = string & { __label: true } - -export const SubscriptionLabelID = 'card:label:Subscribed' as LabelID - -export interface Label { - labelId: LabelID - cardId: CardID - cardType: CardType - account: AccountUuid - created: Date -} diff --git a/foundations/communication/packages/types/src/message.ts b/foundations/communication/packages/types/src/message.ts deleted file mode 100644 index 4b94d926eb..0000000000 --- a/foundations/communication/packages/types/src/message.ts +++ /dev/null @@ -1,215 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { Attribute, BlobMetadata, Class, Mixin, Ref } from '@hcengineering/core' - -import type { AccountUuid, BlobID, CardID, CardType, ID, Markdown, SocialID, PersonUuid } from './core' - -// Message -export type MessageID = ID & { message: true } -export type Emoji = string & { emoji: true } - -export enum MessageType { - Text = 'text', - Activity = 'activity' -} - -export type MessageExtra = Record - -export interface Message { - id: MessageID - cardId: CardID - - type: MessageType - content: Markdown - extra?: MessageExtra - language?: string - - creator: SocialID - created: Date - modified?: Date - - reactions: Record - attachments: Attachment[] - threads: Thread[] - translates?: Record -} - -export type TranslatedMessage = Pick - -export interface EmojiData { - count: number - person: PersonUuid - date: Date -} - -export type MessageMeta = Pick & { blobId: BlobID } - -export interface ActivityMessage extends Message { - type: MessageType.Activity - extra: ActivityMessageExtra -} - -export interface ActivityMessageExtra { - action: 'create' | 'remove' | 'update' - update?: ActivityUpdate -} - -export type ActivityUpdate = - | ActivityAttributeUpdate - | ActivityTagUpdate - | ActivityTypeUpdate - | ActivityCollaboratorsUpdate - | ActivityProcess - | ActivityCollaborativeChange - -export enum ActivityUpdateType { - Attribute = 'attribute', - Tag = 'tag', - Collaborators = 'collaborators', - Type = 'type', - Process = 'process', - CollaborativeChange = 'collaborativeChange' -} - -export interface ActivityProcess { - type: ActivityUpdateType.Process - process: Ref - action: 'started' | 'complete' | 'transition' - transitionTo?: Ref -} - -export interface ActivityTagUpdate { - type: ActivityUpdateType.Tag - tag: Ref - action: 'add' | 'remove' -} - -export interface ActivityCollaboratorsUpdate { - type: ActivityUpdateType.Collaborators - added: AccountUuid[] - removed: AccountUuid[] -} - -export interface ActivityTypeUpdate { - type: ActivityUpdateType.Type - newType: CardType -} - -export interface ActivityCollaborativeChange { - type: ActivityUpdateType.CollaborativeChange - attrKey: string - value: string - prevValue: string -} - -type AttributeValue = string | number | null - -export interface ActivityAttributeUpdate { - type: ActivityUpdateType.Attribute - attrKey: string - attrClass: Ref>> - mixin?: Ref> - set?: AttributeValue | AttributeValue[] - added?: AttributeValue[] - removed?: AttributeValue[] -} - -// LinkPreview -export const linkPreviewType = 'application/vnd.huly.link-preview' as const - -export interface LinkPreviewParams { - url: string - host: string - - title?: string - description?: string - siteName?: string - - iconUrl?: string - previewImage?: LinkPreviewImage -} - -export interface LinkPreviewImage { - url: string - width?: number - height?: number -} - -export interface BlobParams { - blobId: BlobID - fileName: string - size: number - metadata?: BlobMetadata -} - -// Attachment -export type AttachmentID = string & { __attachmentId: true } - -export type Attachment = BlobAttachment | LinkPreviewAttachment | AppletAttachment - -interface BaseAttachment extends AttachmentData { - creator: SocialID - created: Date - modified?: Date -} - -export interface LinkPreviewAttachment extends BaseAttachment { - mimeType: typeof linkPreviewType -} - -export interface BlobAttachment extends BaseAttachment {} - -export type AppletParams = Record -export type AppletType = `application/vnd.huly.applet.${string}` - -export interface AppletAttachment extends BaseAttachment { - mimeType: AppletType -} - -export interface AttachmentData

{ - id: AttachmentID - mimeType: string - params: P -} - -export type AttachmentParams = Record - -export interface AttachmentUpdateData

{ - id: AttachmentID - params: Partial

-} - -// Thread -export interface Thread { - cardId: CardID - messageId: MessageID - threadId: CardID - threadType: CardType - repliesCount: number - lastReplyDate: Date | undefined - repliedPersons: Record -} - -export type ThreadMeta = Pick - -// MessagesGroup -export interface MessagesGroup { - cardId: CardID - blobId: BlobID - fromDate: Date - toDate: Date - count: number -} diff --git a/foundations/communication/packages/types/src/notification.ts b/foundations/communication/packages/types/src/notification.ts deleted file mode 100644 index 6347c5df4f..0000000000 --- a/foundations/communication/packages/types/src/notification.ts +++ /dev/null @@ -1,69 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { AccountUuid, BlobID, CardID, CardType, ID, SocialID } from './core' -import type { Message, MessageID } from './message' - -export type ContextID = ID & { context: true } -export type NotificationID = ID & { notification: true } - -export interface Collaborator { - cardId: CardID - cardType: CardType - account: AccountUuid -} - -export interface Notification { - id: NotificationID - cardId: CardID - contextId: ContextID - account: AccountUuid - type: NotificationType - read: boolean - created: Date - content: NotificationContent - messageId: MessageID - creator: SocialID - blobId: BlobID - - message?: Message -} - -export enum NotificationType { - Message = 'message', - Reaction = 'reaction' -} - -export type NotificationContent = { - title: string - shortText: string - senderName: string -} & Record - -export type ReactionNotificationContent = NotificationContent & { - emoji: string -} - -export interface NotificationContext { - id: ContextID - cardId: CardID - account: AccountUuid - lastUpdate: Date - lastView: Date - lastNotify?: Date - - notifications?: Notification[] - totalNotifications?: number -} diff --git a/foundations/communication/packages/types/src/peer.ts b/foundations/communication/packages/types/src/peer.ts deleted file mode 100644 index 03739ff51e..0000000000 --- a/foundations/communication/packages/types/src/peer.ts +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. - -import { CardID, WorkspaceUuid } from './core' - -export type PeerKind = 'card' | string -export type PeerExtra = Record - -interface BasePeer { - workspaceId: WorkspaceUuid - cardId: CardID - kind: PeerKind - value: string - extra: PeerExtra - created: Date -} - -export interface CardPeer extends BasePeer { - kind: 'card' - members: CardPeerMember[] -} - -export interface ExternalPeer extends BasePeer { - kind: string -} - -export type Peer = CardPeer | ExternalPeer - -export interface CardPeerMember { - workspaceId: WorkspaceUuid - cardId: CardID - extra: PeerExtra -} diff --git a/foundations/communication/packages/types/src/query.ts b/foundations/communication/packages/types/src/query.ts deleted file mode 100644 index 51a4fd1982..0000000000 --- a/foundations/communication/packages/types/src/query.ts +++ /dev/null @@ -1,122 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { SortingOrder } from '@hcengineering/core' - -import type { MessageID } from './message' -import type { ContextID, NotificationID, NotificationType } from './notification' -import type { AccountUuid, BlobID, CardID, CardType, SocialID, WorkspaceUuid } from './core' -import type { LabelID } from './label' -import { PeerKind } from './peer' - -export { SortingOrder } - -export type ComparisonOperator = 'less' | 'lessOrEqual' | 'greater' | 'greaterOrEqual' | 'notEqual' - -export interface Window { - getResult: () => T[] - getTotal: () => number - - loadNextPage: () => Promise - - loadPrevPage: () => Promise - - hasNextPage: () => boolean - - hasPrevPage: () => boolean -} - -interface FindParams { - order?: SortingOrder - limit?: number -} - -export interface FindMessagesParams extends FindParams { - cardId: CardID - id?: MessageID -} - -export interface FindMessagesMetaParams extends FindParams { - cardId?: CardID - id?: MessageID - creator?: SocialID -} - -export interface FindMessagesGroupParams extends FindParams { - cardId: CardID - id?: MessageID - blobId?: BlobID - fromDate?: Partial> | Date - toDate?: Partial> | Date -} - -export interface FindMessagesOptions { - attachments?: boolean - reactions?: boolean - threads?: boolean -} - -export interface FindNotificationContextParams extends FindParams { - id?: ContextID - cardId?: CardID | CardID[] - lastNotify?: Partial> | Date - account?: AccountUuid | AccountUuid[] - notifications?: { - type?: NotificationType - limit: number - order: SortingOrder - read?: boolean - total?: boolean - } -} - -export interface FindNotificationsParams extends FindParams { - id?: NotificationID - messageId?: MessageID - type?: NotificationType - contextId?: ContextID - read?: boolean - created?: Partial> | Date - account?: AccountUuid | AccountUuid[] - cardId?: CardID - total?: boolean -} - -export interface FindCollaboratorsParams extends FindParams { - cardId: CardID - account?: AccountUuid | AccountUuid[] -} - -export interface FindLabelsParams extends FindParams { - labelId?: LabelID | LabelID[] - cardId?: CardID - cardType?: CardType | CardType[] - account?: AccountUuid -} - -export interface FindThreadMetaParams extends FindParams { - cardId?: CardID - messageId?: MessageID - threadId?: CardID -} - -export interface FindPeersParams extends FindParams { - workspaceId?: WorkspaceUuid - cardId?: CardID - kind?: PeerKind - value?: string -} - -export type WithTotal = T[] & { total: number } diff --git a/foundations/communication/packages/types/tsconfig.json b/foundations/communication/packages/types/tsconfig.json deleted file mode 100644 index b5ae22f6e4..0000000000 --- a/foundations/communication/packages/types/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/foundations/communication/rush.json b/foundations/communication/rush.json deleted file mode 100644 index 178197ed8d..0000000000 --- a/foundations/communication/rush.json +++ /dev/null @@ -1,73 +0,0 @@ -/** - * This is the main configuration file for Rush. - * For full documentation, please see https://rushjs.io - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush.schema.json", - "rushVersion": "5.153.2", - "pnpmVersion": "9.15.3", - "nodeSupportedVersionRange": ">=20.0.0 <25.0.0", - "projectFolderMaxDepth": 2, - "gitPolicy": {}, - "repository": { - "url": "https://github.com/hcengineering/communication", - "defaultBranch": "main", - "defaultRemote": "origin" - }, - "eventHooks": { - "preRushInstall": [], - "postRushInstall": [], - "preRushBuild": [], - "postRushBuild": [], - "preRushx": [], - "postRushx": [] - }, - "variants": [], - "projects": [ - { - "packageName": "@hcengineering/scripts", - "projectFolder": "common/scripts", - "shouldPublish": false - }, - { - "packageName": "@hcengineering/communication-types", - "projectFolder": "packages/types", - "shouldPublish": true - }, - { - "packageName": "@hcengineering/communication-sdk-types", - "projectFolder": "packages/sdk-types", - "shouldPublish": true - }, - { - "packageName": "@hcengineering/communication-shared", - "projectFolder": "packages/shared", - "shouldPublish": true - }, - { - "packageName": "@hcengineering/communication-rest-client", - "projectFolder": "packages/rest-client", - "shouldPublish": true - }, - { - "packageName": "@hcengineering/communication-cockroach", - "projectFolder": "packages/cockroach", - "shouldPublish": true - }, - { - "packageName": "@hcengineering/communication-server", - "projectFolder": "packages/server", - "shouldPublish": true - }, - { - "packageName": "@hcengineering/communication-query", - "projectFolder": "packages/query", - "shouldPublish": true - }, - { - "packageName": "@hcengineering/communication-client-query", - "projectFolder": "packages/client-query", - "shouldPublish": true - } - ] -} diff --git a/foundations/communication/translate.json b/foundations/communication/translate.json deleted file mode 100644 index 5b97bfd669..0000000000 --- a/foundations/communication/translate.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "cardId": "card-000", - "fromDate": "2025-09-02T10:00:00.000Z", - "toDate": "2025-09-02T12:30:00.000Z", - "language": "en", - - "messages": { - "msg-001": { - "id": "msg-001", - "cardId": "card-000", - "type": "text", - "content": "Привет! Это первое сообщение." - } - } -} diff --git a/foundations/core/.gitattributes b/foundations/core/.gitattributes deleted file mode 100644 index 79a85db5c2..0000000000 --- a/foundations/core/.gitattributes +++ /dev/null @@ -1,14 +0,0 @@ -# Don't allow people to merge changes to these generated files, because the result -# may be invalid. You need to run "rush update" again. -pnpm-lock.yaml merge=text -shrinkwrap.yaml merge=binary -npm-shrinkwrap.json merge=binary -yarn.lock merge=binary - -# Rush's JSON config files use JavaScript-style code comments. The rule below prevents pedantic -# syntax highlighters such as GitHub's from highlighting these comments as errors. Your text editor -# may also require a special configuration to allow comments in JSON. -# -# For more information, see this issue: https://github.com/microsoft/rushstack/issues/1088 -# -*.json linguist-language=JSON-with-Comments diff --git a/foundations/core/.github/copilot-instructions.md b/foundations/core/.github/copilot-instructions.md deleted file mode 100644 index bc7e1960df..0000000000 --- a/foundations/core/.github/copilot-instructions.md +++ /dev/null @@ -1,69 +0,0 @@ -# GitHub Copilot Instructions - Huly Core - -**Type**: Rush monorepo (TypeScript) -**License**: EPL-2.0 -**Build System**: Rush v5.158.1 + pnpm v10.15.1 - -## Critical Rules - -1. **Always use Rush commands**, never npm/pnpm directly: `rush install`, `rush build`, `rush test`, `rush update` -2. **Internal dependencies must use `workspace:^` protocol** in package.json -3. **All packages extend `@hcengineering/platform-rig`** for tsconfig/eslint - don't override configs -4. **Co-locate tests** in `src/__tests__/` directories (Jest + ts-jest) -5. **Named exports only** - avoid default exports - -## Quick Start - -```bash -rush install && rush build # Initial setup -rush build:watch # Development mode -rush rebuild # Force clean rebuild (clears cache) -rush test # Run all tests -``` - -## Package Structure (All packages follow this) - -``` -packages// -├── src/index.ts # Main entry (exports) -├── lib/ # Compiled JS (gitignored) -├── types/ # TS declarations (gitignored) -├── package.json # Scope: @hcengineering/, main: lib/index.js -├── tsconfig.json # Extends platform-rig -└── jest.config.js # preset: 'ts-jest', roots: ['./src'] -``` - -## Key Architecture Patterns - -**Plugin System**: `@hcengineering/platform` provides dependency injection. Packages register resources/services via plugin manifests. Example: `packages/core/src/plugin.ts` - -**Data Flow**: `core` → abstract models (Doc, Ref, Class) → `client` → concrete implementations → WebSocket/REST via `api-client` - -**Text Processing**: Modular `text-*` packages (core/html/markdown/ydoc) support extensible rich-text editing with Yjs collaboration - -**Storage Abstraction**: `storage` package defines interfaces; `storage-client` provides implementations; backends are pluggable - -## Common Tasks - -**Add a package**: Create under `packages/`, add standard files (see structure above), register in `rush.json` if needed, run `rush update` - -**Debug build failures**: Check `/rush-logs/` and `.build/build.tsbuildinfo`; use `rush rebuild` to clear incremental state - -**Run single package tests**: `cd packages/ && rushx test` - -**Update dependencies**: Edit package.json, run `rush update`, verify with `rush build` - -## Where to Look - -- **Rush config**: `common/config/rush/`, `rush.json` -- **Shared scripts**: `common/scripts/` (coverage merging, install helpers) -- **Core abstractions**: `packages/core/src/` (Doc, Hierarchy, TxOperations) -- **Platform runtime**: `packages/platform/src/` (plugin loader, resources) -- **Client layer**: `packages/client/src/` (LiveQuery, TxOperations client wrapper) - -## Watch Out For - -- Rush uses **incremental builds** - cached artifacts in `.build/` and `common/temp/` -- **TypeScript strict mode** enabled - type safety enforced -- **platform-rig** centralizes tooling configs (ESLint, Prettier, tsconfig) -- Check `common/temp/rush-recycler/` for moved files during dependency updates diff --git a/foundations/core/.github/workflows/ci.yml b/foundations/core/.github/workflows/ci.yml deleted file mode 100644 index 81ca764570..0000000000 --- a/foundations/core/.github/workflows/ci.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: CI - -on: - push: - branches: ['main'] - tags: - - 'v0.7.*' - - 's0.7.*' - pull_request: - branches: ['main'] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 2 - - uses: actions/setup-node@v3 - with: - node-version: 22 - - name: Verify Change Logs - run: node common/scripts/install-run-rush.js change --verify - - name: Rush Install - run: node common/scripts/install-run-rush.js install - - name: Rush check - run: node common/scripts/install-run-rush.js check --verbose - - name: Rush validate - run: node common/scripts/install-run-rush.js validate --verbose - - name: Rush test - run: node common/scripts/install-run-rush.js test --verbose - - name: Publish packages - - name: Formatting... - run: node common/scripts/install-run-rush.js format --force - - name: Check files formatting - run: | - echo '================================================================' - echo 'Checking for diff files' - echo '================================================================' - git diff '*.js' '*.ts' '*.svelte' '*.json' '*.yaml' | cat - [ -z "$(git diff --name-only '*.js' '*.ts' '*.svelte' '*.json' '*.yaml' | cat)" ] - echo '================================================================' - if: startsWith(github.ref, 'refs/tags/v0.7.') || startsWith(github.ref, 'refs/tags/s0.7.') - env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - run: node common/scripts/install-run-rush.js publish --include-all --publish diff --git a/foundations/core/.gitignore b/foundations/core/.gitignore deleted file mode 100644 index a175778200..0000000000 --- a/foundations/core/.gitignore +++ /dev/null @@ -1,115 +0,0 @@ -.heft/ -lib/ -_api-extractor-temp/ -temp/ -.idea -pods/workspace/init/ -pods/workspace/init-scripts/ - -# Logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -*./rush-logs -*tests/sanity/screenshots - -# Runtime data -*.pid -*.seed -*.pid.lock - -# VS Code settings -.vscode/settings.json - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -# .env - -# next.js build output -.next - -# OS X temporary files -.DS_Store - -# Rush temporary files -common/deploy/ -common/temp/ -common/autoinstallers/*/.npmrc -**/.rush/temp/ -bundle.js -bundle/*.js -dist -.build -typings -types -.validate -tsconfig.tsbuildinfo -ingest-attachment-*.zip -tsdoc-metadata.json -pods/front/dist -*.cpuprofile -*.pyc -metrics.txt -dev/tool/report*.csv -tests/db_dump -.build -.format -tools/apm/apm.js -deploy -metrics.txt -services/github/pod-github/src/github.graphql -.build -.format -dev/tool/report.csv -bundle/* -bundle.js.map -tests/profiles -**/bundle/model.json -.wrangler -dump -**/logs/** -dev/tool/history.json -.aider* -/combined_dependencies -.tmp -ws-tests/docker-compose.override.yml diff --git a/foundations/core/.nvmrc b/foundations/core/.nvmrc deleted file mode 100644 index 53d1c14db3..0000000000 --- a/foundations/core/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -v22 diff --git a/foundations/core/.prettierrc b/foundations/core/.prettierrc deleted file mode 100644 index 00d5897747..0000000000 --- a/foundations/core/.prettierrc +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "trailingComma": "none", - "tabWidth": 2, - "semi": false, - "singleQuote": true, - "printWidth": 120, - "useTabs": false, - "bracketSpacing": true, - "proseWrap": "preserve", - "plugins": [], - "overrides": [ - { - "files": "*.svelte", - "options": { - "parser": "svelte" - } - } - ] -} diff --git a/foundations/core/.vscode/extensions.json b/foundations/core/.vscode/extensions.json deleted file mode 100644 index b8552f9c56..0000000000 --- a/foundations/core/.vscode/extensions.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "recommendations": [ - "dbaeumer.vscode-eslint", - "svelte.svelte-vscode", - "esbenp.prettier-vscode", - "firsttris.vscode-jest-runner" - ] -} diff --git a/foundations/core/LICENSE b/foundations/core/LICENSE deleted file mode 100644 index e48e096345..0000000000 --- a/foundations/core/LICENSE +++ /dev/null @@ -1,277 +0,0 @@ -Eclipse Public License - v 2.0 - - THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE - PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION - OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. - -1. DEFINITIONS - -"Contribution" means: - - a) in the case of the initial Contributor, the initial content - Distributed under this Agreement, and - - b) in the case of each subsequent Contributor: - i) changes to the Program, and - ii) additions to the Program; - where such changes and/or additions to the Program originate from - and are Distributed by that particular Contributor. A Contribution - "originates" from a Contributor if it was added to the Program by - such Contributor itself or anyone acting on such Contributor's behalf. - Contributions do not include changes or additions to the Program that - are not Modified Works. - -"Contributor" means any person or entity that Distributes the Program. - -"Licensed Patents" mean patent claims licensable by a Contributor which -are necessarily infringed by the use or sale of its Contribution alone -or when combined with the Program. - -"Program" means the Contributions Distributed in accordance with this -Agreement. - -"Recipient" means anyone who receives the Program under this Agreement -or any Secondary License (as applicable), including Contributors. - -"Derivative Works" shall mean any work, whether in Source Code or other -form, that is based on (or derived from) the Program and for which the -editorial revisions, annotations, elaborations, or other modifications -represent, as a whole, an original work of authorship. - -"Modified Works" shall mean any work in Source Code or other form that -results from an addition to, deletion from, or modification of the -contents of the Program, including, for purposes of clarity any new file -in Source Code form that contains any contents of the Program. Modified -Works shall not include works that contain only declarations, -interfaces, types, classes, structures, or files of the Program solely -in each case in order to link to, bind by name, or subclass the Program -or Modified Works thereof. - -"Distribute" means the acts of a) distributing or b) making available -in any manner that enables the transfer of a copy. - -"Source Code" means the form of a Program preferred for making -modifications, including but not limited to software source code, -documentation source, and configuration files. - -"Secondary License" means either the GNU General Public License, -Version 2.0, or any later versions of that license, including any -exceptions or additional permissions as identified by the initial -Contributor. - -2. GRANT OF RIGHTS - - a) Subject to the terms of this Agreement, each Contributor hereby - grants Recipient a non-exclusive, worldwide, royalty-free copyright - license to reproduce, prepare Derivative Works of, publicly display, - publicly perform, Distribute and sublicense the Contribution of such - Contributor, if any, and such Derivative Works. - - b) Subject to the terms of this Agreement, each Contributor hereby - grants Recipient a non-exclusive, worldwide, royalty-free patent - license under Licensed Patents to make, use, sell, offer to sell, - import and otherwise transfer the Contribution of such Contributor, - if any, in Source Code or other form. This patent license shall - apply to the combination of the Contribution and the Program if, at - the time the Contribution is added by the Contributor, such addition - of the Contribution causes such combination to be covered by the - Licensed Patents. The patent license shall not apply to any other - combinations which include the Contribution. No hardware per se is - licensed hereunder. - - c) Recipient understands that although each Contributor grants the - licenses to its Contributions set forth herein, no assurances are - provided by any Contributor that the Program does not infringe the - patent or other intellectual property rights of any other entity. - Each Contributor disclaims any liability to Recipient for claims - brought by any other entity based on infringement of intellectual - property rights or otherwise. As a condition to exercising the - rights and licenses granted hereunder, each Recipient hereby - assumes sole responsibility to secure any other intellectual - property rights needed, if any. For example, if a third party - patent license is required to allow Recipient to Distribute the - Program, it is Recipient's responsibility to acquire that license - before distributing the Program. - - d) Each Contributor represents that to its knowledge it has - sufficient copyright rights in its Contribution, if any, to grant - the copyright license set forth in this Agreement. - - e) Notwithstanding the terms of any Secondary License, no - Contributor makes additional grants to any Recipient (other than - those set forth in this Agreement) as a result of such Recipient's - receipt of the Program under the terms of a Secondary License - (if permitted under the terms of Section 3). - -3. REQUIREMENTS - -3.1 If a Contributor Distributes the Program in any form, then: - - a) the Program must also be made available as Source Code, in - accordance with section 3.2, and the Contributor must accompany - the Program with a statement that the Source Code for the Program - is available under this Agreement, and informs Recipients how to - obtain it in a reasonable manner on or through a medium customarily - used for software exchange; and - - b) the Contributor may Distribute the Program under a license - different than this Agreement, provided that such license: - i) effectively disclaims on behalf of all other Contributors all - warranties and conditions, express and implied, including - warranties or conditions of title and non-infringement, and - implied warranties or conditions of merchantability and fitness - for a particular purpose; - - ii) effectively excludes on behalf of all other Contributors all - liability for damages, including direct, indirect, special, - incidental and consequential damages, such as lost profits; - - iii) does not attempt to limit or alter the recipients' rights - in the Source Code under section 3.2; and - - iv) requires any subsequent distribution of the Program by any - party to be under a license that satisfies the requirements - of this section 3. - -3.2 When the Program is Distributed as Source Code: - - a) it must be made available under this Agreement, or if the - Program (i) is combined with other material in a separate file or - files made available under a Secondary License, and (ii) the initial - Contributor attached to the Source Code the notice described in - Exhibit A of this Agreement, then the Program may be made available - under the terms of such Secondary Licenses, and - - b) a copy of this Agreement must be included with each copy of - the Program. - -3.3 Contributors may not remove or alter any copyright, patent, -trademark, attribution notices, disclaimers of warranty, or limitations -of liability ("notices") contained within the Program from any copy of -the Program which they Distribute, provided that Contributors may add -their own appropriate notices. - -4. COMMERCIAL DISTRIBUTION - -Commercial distributors of software may accept certain responsibilities -with respect to end users, business partners and the like. While this -license is intended to facilitate the commercial use of the Program, -the Contributor who includes the Program in a commercial product -offering should do so in a manner which does not create potential -liability for other Contributors. Therefore, if a Contributor includes -the Program in a commercial product offering, such Contributor -("Commercial Contributor") hereby agrees to defend and indemnify every -other Contributor ("Indemnified Contributor") against any losses, -damages and costs (collectively "Losses") arising from claims, lawsuits -and other legal actions brought by a third party against the Indemnified -Contributor to the extent caused by the acts or omissions of such -Commercial Contributor in connection with its distribution of the Program -in a commercial product offering. The obligations in this section do not -apply to any claims or Losses relating to any actual or alleged -intellectual property infringement. In order to qualify, an Indemnified -Contributor must: a) promptly notify the Commercial Contributor in -writing of such claim, and b) allow the Commercial Contributor to control, -and cooperate with the Commercial Contributor in, the defense and any -related settlement negotiations. The Indemnified Contributor may -participate in any such claim at its own expense. - -For example, a Contributor might include the Program in a commercial -product offering, Product X. That Contributor is then a Commercial -Contributor. If that Commercial Contributor then makes performance -claims, or offers warranties related to Product X, those performance -claims and warranties are such Commercial Contributor's responsibility -alone. Under this section, the Commercial Contributor would have to -defend claims against the other Contributors related to those performance -claims and warranties, and if a court requires any other Contributor to -pay any damages as a result, the Commercial Contributor must pay -those damages. - -5. NO WARRANTY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT -PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" -BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR -IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF -TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR -PURPOSE. Each Recipient is solely responsible for determining the -appropriateness of using and distributing the Program and assumes all -risks associated with its exercise of rights under this Agreement, -including but not limited to the risks and costs of program errors, -compliance with applicable laws, damage to or loss of data, programs -or equipment, and unavailability or interruption of operations. - -6. DISCLAIMER OF LIABILITY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT -PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS -SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST -PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE -EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - -7. GENERAL - -If any provision of this Agreement is invalid or unenforceable under -applicable law, it shall not affect the validity or enforceability of -the remainder of the terms of this Agreement, and without further -action by the parties hereto, such provision shall be reformed to the -minimum extent necessary to make such provision valid and enforceable. - -If Recipient institutes patent litigation against any entity -(including a cross-claim or counterclaim in a lawsuit) alleging that the -Program itself (excluding combinations of the Program with other software -or hardware) infringes such Recipient's patent(s), then such Recipient's -rights granted under Section 2(b) shall terminate as of the date such -litigation is filed. - -All Recipient's rights under this Agreement shall terminate if it -fails to comply with any of the material terms or conditions of this -Agreement and does not cure such failure in a reasonable period of -time after becoming aware of such noncompliance. If all Recipient's -rights under this Agreement terminate, Recipient agrees to cease use -and distribution of the Program as soon as reasonably practicable. -However, Recipient's obligations under this Agreement and any licenses -granted by Recipient relating to the Program shall continue and survive. - -Everyone is permitted to copy and distribute copies of this Agreement, -but in order to avoid inconsistency the Agreement is copyrighted and -may only be modified in the following manner. The Agreement Steward -reserves the right to publish new versions (including revisions) of -this Agreement from time to time. No one other than the Agreement -Steward has the right to modify this Agreement. The Eclipse Foundation -is the initial Agreement Steward. The Eclipse Foundation may assign the -responsibility to serve as the Agreement Steward to a suitable separate -entity. Each new version of the Agreement will be given a distinguishing -version number. The Program (including Contributions) may always be -Distributed subject to the version of the Agreement under which it was -received. In addition, after a new version of the Agreement is published, -Contributor may elect to Distribute the Program (including its -Contributions) under the new version. - -Except as expressly stated in Sections 2(a) and 2(b) above, Recipient -receives no rights or licenses to the intellectual property of any -Contributor under this Agreement, whether expressly, by implication, -estoppel or otherwise. All rights in the Program not expressly granted -under this Agreement are reserved. Nothing in this Agreement is intended -to be enforceable by any entity that is not a Contributor or Recipient. -No third-party beneficiary rights are created under this Agreement. - -Exhibit A - Form of Secondary Licenses Notice - -"This Source Code may also be made available under the following -Secondary Licenses when the conditions for such availability set forth -in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), -version(s), and exceptions or additional permissions here}." - - Simply including a copy of this Agreement, including this Exhibit A - is not sufficient to license the Source Code under Secondary Licenses. - - If it is not possible or desirable to put the notice in a particular - file, then You may include the notice in a location (such as a LICENSE - file in a relevant directory) where a recipient would be likely to - look for such a notice. - - You may add additional accurate notices of copyright ownership. diff --git a/foundations/core/README.md b/foundations/core/README.md deleted file mode 100644 index 0221a7792b..0000000000 --- a/foundations/core/README.md +++ /dev/null @@ -1,167 +0,0 @@ -# Huly Core - -[![GitHub License](https://img.shields.io/github/license/hcengineering/huly.core?style=for-the-badge)](LICENSE) - -⭐️ Your star shines on us. Star us on GitHub! - -## About - -Huly Core is a collection of core packages extracted from the [Huly Platform](https://github.com/hcengineering/platform). This repository contains fundamental building blocks and libraries that power the Huly ecosystem, including core data models, client libraries, text processing engines, and platform utilities. - -These packages are designed to be reusable, modular, and framework-agnostic, making them suitable for building custom applications on top of the Huly Platform or integrating Huly functionality into existing projects. - -## Packages - -This repository includes the following core packages: - -### Core Packages - -- **[@hcengineering/core](packages/core)** - Core data models, types, and fundamental platform abstractions -- **[@hcengineering/platform](packages/platform)** - Platform runtime, plugin system, and dependency injection -- **[@hcengineering/model](packages/model)** - Data model definitions and schema management - -### Client Libraries - -- **[@hcengineering/client](packages/client)** - Client-side data access and synchronization layer -- **[@hcengineering/client-resources](packages/client-resources)** - Shared client resources and utilities -- **[@hcengineering/api-client](packages/api-client)** - API client for programmatic access to Huly Platform (WebSocket and REST) -- **[@hcengineering/account-client](packages/account-client)** - Account management client -- **[@hcengineering/collaborator-client](packages/collaborator-client)** - Real-time collaboration client -- **[@hcengineering/hulylake-client](packages/hulylake-client)** - HulyLake data warehouse client -- **[@hcengineering/analytics](packages/analytics)** - Analytics and tracking -- **[@hcengineering/analytics-service](packages/analytics-service)** - Analytics service implementation - -### Text Processing - -- **[@hcengineering/text](packages/text)** - High-level text processing utilities -- **[@hcengineering/text-core](packages/text-core)** - Core text processing engine -- **[@hcengineering/text-html](packages/text-html)** - HTML text rendering and parsing -- **[@hcengineering/text-markdown](packages/text-markdown)** - Markdown support -- **[@hcengineering/text-ydoc](packages/text-ydoc)** - Yjs document integration for collaborative editing - -### Utilities - -- **[@hcengineering/query](packages/query)** - Query language and execution engine -- **[@hcengineering/storage](packages/storage)** - Storage abstractions and implementations -- **[@hcengineering/rank](packages/rank)** - Ranking and ordering utilities -- **[@hcengineering/retry](packages/retry)** - Retry logic and resilience patterns -- **[@hcengineering/rpc](packages/rpc)** - RPC communication layer -- **[@hcengineering/token](packages/token)** - Token management and authentication utilities - -## Pre-requisites - -Before proceeding, ensure that your system meets the following requirements: - -- [Node.js](https://nodejs.org/en/download/) (v20.11.0 or higher is required) -- [Rush](https://rushjs.io/) - Microsoft's scalable monorepo manager - -## Installation - -You need Microsoft's [rush](https://rushjs.io/) to install the application. - -1. Install Rush globally using the command: - -```bash -npm install -g @microsoft/rush -``` - -1. Navigate to the repository root and run the following commands: - -```bash -rush install -rush build -``` - -## Build - -To build all packages: - -```bash -rush build -``` - -To rebuild (ignoring cache): - -```bash -rush rebuild -``` - -## Build & Watch - -For development purposes, `rush build:watch` action could be used: - -```bash -rush build:watch -``` - -It includes build and validate phases in watch mode. - -## Update project structure - -If the project's structure is updated, it may be necessary to relink and rebuild the projects: - -```bash -rush update -rush build -``` - -## Troubleshooting - -If a build fails, but the code is correct, try to delete the [build cache](https://rushjs.io/pages/maintainer/build_cache/) and retry: - -```bash -rm -rf common/temp/build-cache -rush rebuild -``` - -## Tests - -To execute all tests: - -```bash -rush test -``` - -For individual test execution inside a package directory: - -```bash -rushx test -``` - -## Package Publishing - -To bump a package version: - -```bash -node ./common/scripts/bump.js -p projectName -``` - -## API Client Usage - -If you want to interact with Huly programmatically, check out the [API Client](packages/api-client/README.md) documentation. The API client provides a typed interface for all Huly operations and can be used to build integrations and custom applications. - -You can find API usage examples in the [Huly examples](https://github.com/hcengineering/huly-examples) repository. - -## Related Projects - -- **[Huly Platform](https://github.com/hcengineering/platform)** - The main Huly Platform repository -- **[Huly Self-Host](https://github.com/hcengineering/huly-selfhost)** - Self-hosting solution for Huly -- **[Huly Examples](https://github.com/hcengineering/huly-examples)** - API usage examples - -## Contributing - -Contributions are welcome! Please feel free to submit a Pull Request. - -## License - -Licensed under the [EPL-2.0](LICENSE) license. - -## Additional Links - -- [Huly Website](https://huly.io/) -- [Documentation](https://docs.huly.io/) -- [Community](https://github.com/hcengineering/platform/discussions) - ---- - -© 2025 [Hardcore Engineering Inc](https://hardcoreeng.com/). diff --git a/foundations/core/common/changes/@hcengineering/client-resources/enable-formatting-check_2025-11-20-14-13.json b/foundations/core/common/changes/@hcengineering/client-resources/enable-formatting-check_2025-11-20-14-13.json deleted file mode 100644 index 77ca2b0fb1..0000000000 --- a/foundations/core/common/changes/@hcengineering/client-resources/enable-formatting-check_2025-11-20-14-13.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@hcengineering/client-resources", - "comment": "Add support for custom exclude filters", - "type": "patch" - } - ], - "packageName": "@hcengineering/client-resources" -} \ No newline at end of file diff --git a/foundations/core/common/changes/@hcengineering/client-resources/main_2025-10-29-07-46.json b/foundations/core/common/changes/@hcengineering/client-resources/main_2025-10-29-07-46.json deleted file mode 100644 index 2c0564f3c0..0000000000 --- a/foundations/core/common/changes/@hcengineering/client-resources/main_2025-10-29-07-46.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@hcengineering/client-resources", - "comment": "formatting", - "type": "none" - } - ], - "packageName": "@hcengineering/client-resources" -} \ No newline at end of file diff --git a/foundations/core/common/changes/@hcengineering/client/enable-formatting-check_2025-11-20-14-13.json b/foundations/core/common/changes/@hcengineering/client/enable-formatting-check_2025-11-20-14-13.json deleted file mode 100644 index 508a372f52..0000000000 --- a/foundations/core/common/changes/@hcengineering/client/enable-formatting-check_2025-11-20-14-13.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@hcengineering/client", - "comment": "Add support for custom exclude filters", - "type": "patch" - } - ], - "packageName": "@hcengineering/client" -} \ No newline at end of file diff --git a/foundations/core/common/config/rush/.npmrc b/foundations/core/common/config/rush/.npmrc deleted file mode 100644 index 4bb3c57a4e..0000000000 --- a/foundations/core/common/config/rush/.npmrc +++ /dev/null @@ -1,33 +0,0 @@ -# Rush uses this file to configure the NPM package registry during installation. It is applicable -# to PNPM, NPM, and Yarn package managers. It is used by operations such as "rush install", -# "rush update", and the "install-run.js" scripts. -# -# NOTE: The "rush publish" command uses .npmrc-publish instead. -# -# Before invoking the package manager, Rush will generate an .npmrc in the folder where installation -# is performed. This generated file will omit any config lines that reference environment variables -# that are undefined in that session; this avoids problems that would otherwise result due to -# a missing variable being replaced by an empty string. -# -# If "subspacesEnabled" is true in subspaces.json, the generated file will merge settings from -# "common/config/rush/.npmrc" and "common/config/subspaces//.npmrc", with the latter taking -# precedence. -# -# * * * SECURITY WARNING * * * -# -# It is NOT recommended to store authentication tokens in a text file on a lab machine, because -# other unrelated processes may be able to read that file. Also, the file may persist indefinitely, -# for example if the machine loses power. A safer practice is to pass the token via an -# environment variable, which can be referenced from .npmrc using ${} expansion. For example: -# -# //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN} -# - -# Explicitly specify the NPM registry that "rush install" and "rush update" will use by default: -registry=https://registry.npmjs.org/ - -# Optionally provide an authentication token for the above registry URL (if it is a private registry): -//registry.npmjs.org/:_authToken=${NPM_TOKEN} - -# Change this to "true" if your registry requires authentication for read-only operations: -always-auth=false diff --git a/foundations/core/common/config/rush/.npmrc-publish b/foundations/core/common/config/rush/.npmrc-publish deleted file mode 100644 index 951a4918cf..0000000000 --- a/foundations/core/common/config/rush/.npmrc-publish +++ /dev/null @@ -1,29 +0,0 @@ -# This config file is very similar to common/config/rush/.npmrc, except that .npmrc-publish -# is used by the "rush publish" command, as publishing often involves different credentials -# and registries than other operations. -# -# Before invoking the package manager, Rush will copy this file to "common/temp/publish-home/.npmrc" -# and then temporarily map that folder as the "home directory" for the current user account. -# This enables the same settings to apply for each project folder that gets published. The copied file -# will omit any config lines that reference environment variables that are undefined in that session; -# this avoids problems that would otherwise result due to a missing variable being replaced by -# an empty string. -# -# * * * SECURITY WARNING * * * -# -# It is NOT recommended to store authentication tokens in a text file on a lab machine, because -# other unrelated processes may be able to read the file. Also, the file may persist indefinitely, -# for example if the machine loses power. A safer practice is to pass the token via an -# environment variable, which can be referenced from .npmrc using ${} expansion. For example: -# -# //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN} -# - -# Explicitly specify the NPM registry that "rush publish" will use by default: -registry=https://registry.npmjs.org/ - -# Optionally provide an authentication token for the above registry URL (if it is a private registry): -//registry.npmjs.org/:_authToken=${NPM_TOKEN} - -# Change this to "true" if your registry requires authentication for read-only operations: -always-auth=false diff --git a/foundations/core/common/config/rush/.pnpmfile.cjs b/foundations/core/common/config/rush/.pnpmfile.cjs deleted file mode 100644 index 98cf3279ec..0000000000 --- a/foundations/core/common/config/rush/.pnpmfile.cjs +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -/** - * When using the PNPM package manager, you can use pnpmfile.js to workaround - * dependencies that have mistakes in their package.json file. (This feature is - * functionally similar to Yarn's "resolutions".) - * - * For details, see the PNPM documentation: - * https://pnpm.io/pnpmfile#hooks - * - * IMPORTANT: SINCE THIS FILE CONTAINS EXECUTABLE CODE, MODIFYING IT IS LIKELY TO INVALIDATE - * ANY CACHED DEPENDENCY ANALYSIS. After any modification to pnpmfile.js, it's recommended to run - * "rush update --full" so that PNPM will recalculate all version selections. - */ -module.exports = { - hooks: { - readPackage - } -}; - -/** - * This hook is invoked during installation before a package's dependencies - * are selected. - * The `packageJson` parameter is the deserialized package.json - * contents for the package that is about to be installed. - * The `context` parameter provides a log() function. - * The return value is the updated object. - */ -function readPackage(packageJson, context) { - - // // The karma types have a missing dependency on typings from the log4js package. - // if (packageJson.name === '@types/karma') { - // context.log('Fixed up dependencies for @types/karma'); - // packageJson.dependencies['log4js'] = '0.6.38'; - // } - - return packageJson; -} diff --git a/foundations/core/common/config/rush/artifactory.json b/foundations/core/common/config/rush/artifactory.json deleted file mode 100644 index 268065478a..0000000000 --- a/foundations/core/common/config/rush/artifactory.json +++ /dev/null @@ -1,109 +0,0 @@ -/** - * This configuration file manages Rush integration with JFrog Artifactory services. - * More documentation is available on the Rush website: https://rushjs.io - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/artifactory.schema.json", - - "packageRegistry": { - /** - * (Required) Set this to "true" to enable Rush to manage tokens for an Artifactory NPM registry. - * When enabled, "rush install" will automatically detect when the user's ~/.npmrc - * authentication token is missing or expired. And "rush setup" will prompt the user to - * renew their token. - * - * The default value is false. - */ - "enabled": false, - - /** - * (Required) Specify the URL of your NPM registry. This is the same URL that appears in - * your .npmrc file. It should look something like this example: - * - * https://your-company.jfrog.io/your-project/api/npm/npm-private/ - */ - "registryUrl": "", - - /** - * A list of custom strings that "rush setup" should add to the user's ~/.npmrc file at the time - * when the token is updated. This could be used for example to configure the company registry - * to be used whenever NPM is invoked as a standalone command (but it's not needed for Rush - * operations like "rush add" and "rush install", which get their mappings from the monorepo's - * common/config/rush/.npmrc file). - * - * NOTE: The ~/.npmrc settings are global for the user account on a given machine, so be careful - * about adding settings that may interfere with other work outside the monorepo. - */ - "userNpmrcLinesToAdd": [ - // "@example:registry=https://your-company.jfrog.io/your-project/api/npm/npm-private/" - ], - - /** - * (Required) Specifies the URL of the Artifactory control panel where the user can generate - * an API key. This URL is printed after the "visitWebsite" message. - * It should look something like this example: https://your-company.jfrog.io/ - * Specify an empty string to suppress this line entirely. - */ - "artifactoryWebsiteUrl": "", - - /** - * Uncomment this line to specify the type of credential to save in the user's ~/.npmrc file. - * The default is "password", which means the user's API token will be traded in for an - * npm password specific to that registry. Optionally you can specify "authToken", which - * will save the user's API token as credentials instead. - */ - // "credentialType": "password", - - /** - * These settings allow the "rush setup" interactive prompts to be customized, for - * example with messages specific to your team or configuration. Specify an empty string - * to suppress that message entirely. - */ - "messageOverrides": { - /** - * Overrides the message that normally says: - * "This monorepo consumes packages from an Artifactory private NPM registry." - */ - // "introduction": "", - - /** - * Overrides the message that normally says: - * "Please contact the repository maintainers for help with setting up an Artifactory user account." - */ - // "obtainAnAccount": "", - - /** - * Overrides the message that normally says: - * "Please open this URL in your web browser:" - * - * The "artifactoryWebsiteUrl" string is printed after this message. - */ - // "visitWebsite": "", - - /** - * Overrides the message that normally says: - * "Your user name appears in the upper-right corner of the JFrog website." - */ - // "locateUserName": "", - - /** - * Overrides the message that normally says: - * "Click 'Edit Profile' on the JFrog website. Click the 'Generate API Key' - * button if you haven't already done so previously." - */ - // "locateApiKey": "" - - /** - * Overrides the message that normally prompts: - * "What is your Artifactory user name?" - */ - // "userNamePrompt": "" - - /** - * Overrides the message that normally prompts: - * "What is your Artifactory API key?" - */ - // "apiKeyPrompt": "" - } - } -} diff --git a/foundations/core/common/config/rush/build-cache.json b/foundations/core/common/config/rush/build-cache.json deleted file mode 100644 index 072e9f7d49..0000000000 --- a/foundations/core/common/config/rush/build-cache.json +++ /dev/null @@ -1,160 +0,0 @@ -/** - * This configuration file manages Rush's build cache feature. - * More documentation is available on the Rush website: https://rushjs.io - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/build-cache.schema.json", - - /** - * (Required) EXPERIMENTAL - Set this to true to enable the build cache feature. - * - * See https://rushjs.io/pages/maintainer/build_cache/ for details about this experimental feature. - */ - "buildCacheEnabled": false, - - /** - * (Required) Choose where project build outputs will be cached. - * - * Possible values: "local-only", "azure-blob-storage", "amazon-s3" - */ - "cacheProvider": "local-only", - - /** - * Setting this property overrides the cache entry ID. If this property is set, it must contain - * a [hash] token. - * - * Other available tokens: - * - [projectName] Example: "@my-scope/my-project" - * - [projectName:normalize] Example: "my-scope+my-project" - * - [phaseName] Example: "_phase:test/api" - * - [phaseName:normalize] Example: "_phase:test+api" - * - [phaseName:trimPrefix] Example: "test/api" - * - [os] Example: "win32" - * - [arch] Example: "x64" - */ - // "cacheEntryNamePattern": "[projectName:normalize]-[phaseName:normalize]-[hash]" - - /** - * (Optional) Salt to inject during calculation of the cache key. This can be used to invalidate the cache for all projects when the salt changes. - */ - // "cacheHashSalt": "1", - - /** - * Use this configuration with "cacheProvider"="azure-blob-storage" - */ - "azureBlobStorageConfiguration": { - /** - * (Required) The name of the the Azure storage account to use for build cache. - */ - // "storageAccountName": "example", - - /** - * (Required) The name of the container in the Azure storage account to use for build cache. - */ - // "storageContainerName": "my-container", - - /** - * The Azure environment the storage account exists in. Defaults to AzurePublicCloud. - * - * Possible values: "AzurePublicCloud", "AzureChina", "AzureGermany", "AzureGovernment" - */ - // "azureEnvironment": "AzurePublicCloud", - - /** - * An optional prefix for cache item blob names. - */ - // "blobPrefix": "my-prefix", - - /** - * If set to true, allow writing to the cache. Defaults to false. - */ - // "isCacheWriteAllowed": true, - - /** - * The Entra ID login flow to use. Defaults to 'AdoCodespacesAuth' on GitHub Codespaces, 'InteractiveBrowser' otherwise. - */ - // "loginFlow": "InteractiveBrowser", - - /** - * If set to true, reading the cache requires authentication. Defaults to false. - */ - // "readRequiresAuthentication": true - }, - - /** - * Use this configuration with "cacheProvider"="amazon-s3" - */ - "amazonS3Configuration": { - /** - * (Required unless s3Endpoint is specified) The name of the bucket to use for build cache. - * Example: "my-bucket" - */ - // "s3Bucket": "my-bucket", - - /** - * (Required unless s3Bucket is specified) The Amazon S3 endpoint of the bucket to use for build cache. - * This should not include any path; use the s3Prefix to set the path. - * Examples: "my-bucket.s3.us-east-2.amazonaws.com" or "http://localhost:9000" - */ - // "s3Endpoint": "https://my-bucket.s3.us-east-2.amazonaws.com", - - /** - * (Required) The Amazon S3 region of the bucket to use for build cache. - * Example: "us-east-1" - */ - // "s3Region": "us-east-1", - - /** - * An optional prefix ("folder") for cache items. It should not start with "/". - */ - // "s3Prefix": "my-prefix", - - /** - * If set to true, allow writing to the cache. Defaults to false. - */ - // "isCacheWriteAllowed": true - }, - - /** - * Use this configuration with "cacheProvider"="http" - */ - "httpConfiguration": { - /** - * (Required) The URL of the server that stores the caches. - * Example: "https://build-cacches.example.com/" - */ - // "url": "https://build-cacches.example.com/", - - /** - * (Optional) The HTTP method to use when writing to the cache (defaults to PUT). - * Should be one of PUT, POST, or PATCH. - * Example: "PUT" - */ - // "uploadMethod": "PUT", - - /** - * (Optional) HTTP headers to pass to the cache server. - * Example: { "X-HTTP-Company-Id": "109283" } - */ - // "headers": {}, - - /** - * (Optional) Shell command that prints the authorization token needed to communicate with the - * cache server, and exits with exit code 0. This command will be executed from the root of - * the monorepo. - * Example: { "exec": "node", "args": ["common/scripts/auth.js"] } - */ - // "tokenHandler": { "exec": "node", "args": ["common/scripts/auth.js"] }, - - /** - * (Optional) Prefix for cache keys. - * Example: "my-company-" - */ - // "cacheKeyPrefix": "", - - /** - * (Optional) If set to true, allow writing to the cache. Defaults to false. - */ - // "isCacheWriteAllowed": true - } -} diff --git a/foundations/core/common/config/rush/cobuild.json b/foundations/core/common/config/rush/cobuild.json deleted file mode 100644 index a47fad18d5..0000000000 --- a/foundations/core/common/config/rush/cobuild.json +++ /dev/null @@ -1,22 +0,0 @@ -/** - * This configuration file manages Rush's cobuild feature. - * More documentation is available on the Rush website: https://rushjs.io - */ - { - "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/cobuild.schema.json", - - /** - * (Required) EXPERIMENTAL - Set this to true to enable the cobuild feature. - * RUSH_COBUILD_CONTEXT_ID should always be specified as an environment variable with an non-empty string, - * otherwise the cobuild feature will be disabled. - */ - "cobuildFeatureEnabled": false, - - /** - * (Required) Choose where cobuild lock will be acquired. - * - * The lock provider is registered by the rush plugins. - * For example, @rushstack/rush-redis-cobuild-plugin registers the "redis" lock provider. - */ - "cobuildLockProvider": "redis" -} diff --git a/foundations/core/common/config/rush/command-line.json b/foundations/core/common/config/rush/command-line.json deleted file mode 100644 index edf0062005..0000000000 --- a/foundations/core/common/config/rush/command-line.json +++ /dev/null @@ -1,517 +0,0 @@ -/** - * This configuration file defines custom commands for the "rush" command-line. - * More documentation is available on the Rush website: https://rushjs.io - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/command-line.schema.json", - - "phases": [ - { - "name": "_phase:build", - "dependencies": { - "upstream": ["_phase:build"] - }, - "ignoreMissingScript": true, - "allowWarningsOnSuccess": false - }, - { - "name": "_phase:validate", - "dependencies": { - "self": ["_phase:build"], - "upstream": ["_phase:validate", "_phase:build"] - }, - "ignoreMissingScript": true, - "allowWarningsOnSuccess": false - }, - { - "name": "_phase:test", - "dependencies": { - "self": ["_phase:build"], - "upstream": ["_phase:validate"] - }, - "ignoreMissingScript": true, - "allowWarningsOnSuccess": true - }, - { - "name": "_phase:lint", - "dependencies": { - "self": ["_phase:build"] - }, - "ignoreMissingScript": true, - "allowWarningsOnSuccess": false - }, - { - "name": "_phase:bundle", - "dependencies": { - "self": ["_phase:build"] - }, - "ignoreMissingScript": true, - "allowWarningsOnSuccess": false - }, - { - "name": "_phase:format", - "dependencies": { - "self": ["_phase:build"] - }, - "ignoreMissingScript": true, - "allowWarningsOnSuccess": false - }, - { - "name": "_phase:svelte-check", - "dependencies": { - "self": ["_phase:build"] - }, - "ignoreMissingScript": true, - "allowWarningsOnSuccess": true - }, - { - "name": "_phase:package", - "dependencies": { - "self": ["_phase:build"], - "upstream": ["_phase:package"] - }, - "ignoreMissingScript": true, - "allowWarningsOnSuccess": false - }, - { - "name": "_phase:docker-build", - "dependencies": { - "self": ["_phase:build", "_phase:package", "_phase:bundle"], - "upstream": ["_phase:build", "_phase:bundle", "_phase:package"] - }, - "ignoreMissingScript": true, - "allowWarningsOnSuccess": true - }, - { - "name": "_phase:docker-staging", - "dependencies": { - "self": ["_phase:build", "_phase:package", "_phase:bundle"] - }, - "ignoreMissingScript": true, - "allowWarningsOnSuccess": true - } - ], - "commands": [ - { - "commandKind": "global", - "name": "coverage", - "summary": "Run tests, merge LCOV and generate HTML coverage", - "description": "Run 'rush test', then merge per-package LCOV files and generate HTML coverage in coverage/html", - "safeForSimultaneousRushProcesses": true, - "shellCommand": "rush test && node scripts/merge-coverage.js && node scripts/generate-coverage-html.js coverage/lcov.info coverage/html" - }, - { - "commandKind": "bulk", - "name": "format", - "summary": "Format", - "description": "Perform a formatting", - "enableParallelism": true, - "incremental": false, - "ignoreMissingScript": true, - "safeForSimultaneousRushProcesses": true, - "disableBuildCache": true - }, - { - "commandKind": "global", - "name": "doformat", - "summary": "Do a format and show errors after it", - "description": "Do a format and show errors after it", - "safeForSimultaneousRushProcesses": true, - "shellCommand": "(rush format && true) && ./common/scripts/format-show.sh" - }, - { - "commandKind": "phased", - "name": "build:watch", - "summary": "Build and watch", - "phases": ["_phase:build", "_phase:validate"], - "description": "Perform build with tsc and watch for changes with rush", - "enableParallelism": true, - "incremental": true, - "safeForSimultaneousRushProcesses": true, - "watchOptions": { - "alwaysWatch": true, - "watchPhases": ["_phase:build", "_phase:validate"] - } - }, - { - "summary": "svelte-check", - "commandKind": "phased", - "name": "svelte-check", - "phases": ["_phase:build", "_phase:svelte-check"], - "enableParallelism": true, - "incremental": true - }, - { - "commandKind": "phased", - "name": "build", - "summary": "build", - "phases": ["_phase:build"], - "enableParallelism": true, - "incremental": true, - "watchOptions": { - "alwaysWatch": false, - "watchPhases": ["_phase:build"] - } - }, - { - "commandKind": "phased", - "name": "validate", - "phases": ["_phase:validate"], - "summary": "validate", - "enableParallelism": true, - "incremental": true - }, - { - "commandKind": "phased", - "name": "rebuild", - "summary": "ReBuild and test all projects.", - "phases": ["_phase:build"], - "enableParallelism": true, - "incremental": false - }, - { - "commandKind": "phased", - "name": "dorevalidate", - "phases": ["_phase:validate"], - "summary": "dorevalidate", - "enableParallelism": true, - "incremental": false - }, - { - "commandKind": "phased", - "summary": "Do testing", - "name": "test", - "phases": ["_phase:build", "_phase:test"], - "enableParallelism": true, - "incremental": true - }, - { - "commandKind": "phased", - "name": "retest", - "summary": "Build and test all projects.", - "phases": ["_phase:build", "_phase:test"], - "enableParallelism": true, - "incremental": false - }, - - { - "commandKind": "phased", - "summary": "Do bundle", - "name": "bundle", - "phases": ["_phase:build", "_phase:bundle"], - "enableParallelism": true, - "incremental": true - }, - { - "commandKind": "phased", - "summary": "Do packaging", - "name": "package", - "phases": ["_phase:build", "_phase:package"], - "enableParallelism": true, - "incremental": true - }, - - { - "summary": "docker:build", - "commandKind": "phased", - "name": "docker:build", - "phases": ["_phase:build", "_phase:bundle", "_phase:package", "_phase:docker-build"], - "enableParallelism": true, - "incremental": true - }, - { - "summary": "docker:rebuild", - "commandKind": "phased", - "name": "docker:rebuild", - "phases": ["_phase:build", "_phase:bundle", "_phase:package", "_phase:docker-build"], - "enableParallelism": true, - "incremental": false - }, - { - "summary": "docker:staging", - "commandKind": "phased", - "name": "docker:staging", - "phases": ["_phase:build", "_phase:bundle", "_phase:package", "_phase:docker-staging"], - "enableParallelism": true, - "incremental": true - }, - { - "commandKind": "bulk", - "name": "docker:push", - "summary": "docker:push", - "description": "Push docker release images", - "enableParallelism": true, - "incremental": false, - "ignoreDependencyOrder": false, - "ignoreMissingScript": true, - "disableBuildCache": true, - "allowWarningsInSuccessfulBuild": true - }, - { - "commandKind": "global", - "name": "docker", - "summary": "Build docker with platform", - "description": "use to build all docker containers required for platform", - "safeForSimultaneousRushProcesses": true, - "shellCommand": "./common/scripts/docker.sh" - }, - { - "commandKind": "global", - "name": "docker:up", - "summary": "Up development build", - "description": "Up development build", - "safeForSimultaneousRushProcesses": true, - "shellCommand": "cd ./dev && docker compose up -d --force-recreate" - }, - { - "commandKind": "global", - "name": "docker:local", - "summary": "Up development build", - "description": "Up development build", - "safeForSimultaneousRushProcesses": true, - "shellCommand": "cd ./dev/local-mongo && docker compose -p dev up -d --force-recreate" - }, - { - "commandKind": "global", - "name": "tool:upgrade", - "summary": "Upgrade all local models", - "description": "Upgrade all local models", - "safeForSimultaneousRushProcesses": true, - "shellCommand": "cd ./dev/tool && rushx run-local upgrade -f" - }, - { - "commandKind": "global", - "name": "apply-templates", - "summary": "Update all package.json according to templates matched from templates folder", - "description": "Use to update all projects to templates", - "safeForSimultaneousRushProcesses": true, - "shellCommand": "node templates/apply.js" - }, - { - "commandKind": "global", - "name": "ts-clean", - "summary": "Clean tsconfig.tsbuildinfo", - "description": "Clean typescript incremental cache", - "safeForSimultaneousRushProcesses": true, - "shellCommand": "find .|grep tsconfig.tsbuildinfo | xargs rm | pwd" - }, - { - "commandKind": "global", - "name": "revalidate", - "summary": "Clean Validate cache and to validate again", - "description": "Clean typescript incremental cache", - "safeForSimultaneousRushProcesses": true, - "shellCommand": "find .|grep tsBuildInfoFile.info | xargs rm | pwd && rush dorevalidate" - }, - { - "commandKind": "bulk", - "name": "remove-ts-types", - "summary": "Clean validate types", - "description": "Clean validate types", - "shellCommand": "rm -rf ./types", - "enableParallelism": true, - "incremental": false, - "ignoreDependencyOrder": true, - "ignoreMissingScript": true, - "disableBuildCache": true, - "allowWarningsInSuccessfulBuild": true - }, - { - "commandKind": "global", - "name": "model-version", - "summary": "show model version", - "description": "show model version", - "safeForSimultaneousRushProcesses": true, - "shellCommand": "npx node ./common/scripts/show_version.js" - }, - { - "commandKind": "global", - "name": "show-model", - "summary": "show model", - "description": "show model", - "safeForSimultaneousRushProcesses": true, - "shellCommand": "cd ./models/all && rushx show-model" - }, - { - "commandKind": "global", - "name": "deps-clean", - "summary": "Clean package-deps-*.json files", - "description": "Clean package-deps-*.json files", - "safeForSimultaneousRushProcesses": true, - "shellCommand": "find .|grep .rush/temp/package-deps_ | xargs rm" - }, - { - "commandKind": "global", - "name": "fast-format", - "summary": "Format changed projects", - "description": "Format and autofix linting issues in changed projects", - "safeForSimultaneousRushProcesses": true, - "shellCommand": "./common/scripts/fast-format.sh" - }, - { - "commandKind": "global", - "name": "bump-changes", - "summary": "Bump changes from tag", - "description": "Bump changes from previous tag", - "safeForSimultaneousRushProcesses": true, - "shellCommand": "./common/scripts/node_modules/.bin/bump-changes-from-tag" - } - ], - - /** - * Custom "parameters" introduce new parameters for specified Rush command-line commands. - * For example, you might define a "--production" parameter for the "rush build" command. - */ - "parameters": [ - { - "parameterKind": "flag", - "longName": "--lite", - "shortName": "-l", - "description": "Enable Heft lite building option, will skip some phases.", - "associatedCommands": ["build", "rebuild"] - }, - { - "parameterKind": "flag", - "longName": "--clean", - "description": "Enable Heft clean building option", - "associatedCommands": ["build", "rebuild"] - }, - { - "parameterKind": "flag", - "longName": "--force", - "shortName": "-f", - "description": "Force formatting", - "associatedCommands": ["format"] - }, - { - "parameterKind": "string", - "argumentName": "BRANCH", - "required": false, - "associatedPhases": [], - "shortName": "-b", - "longName": "--branch", - "description": "Force formatting of branch", - "associatedCommands": ["fast-format"] - } - // { - // /** - // * (Required) Determines the type of custom parameter. - // * A "flag" is a custom command-line parameter whose presence acts as an on/off switch. - // */ - // "parameterKind": "flag", - // - // /** - // * (Required) The long name of the parameter. It must be lower-case and use dash delimiters. - // */ - // "longName": "--my-flag", - // - // /** - // * An optional alternative short name for the parameter. It must be a dash followed by a single - // * lower-case or upper-case letter, which is case-sensitive. - // * - // * NOTE: The Rush developers recommend that automation scripts should always use the long name - // * to improve readability. The short name is only intended as a convenience for humans. - // * The alphabet letters run out quickly, and are difficult to memorize, so *only* use - // * a short name if you expect the parameter to be needed very often in everyday operations. - // */ - // "shortName": "-m", - // - // /** - // * (Required) A long description to be shown in the command-line help. - // * - // * Whenever you introduce commands/parameters, taking a little time to write meaningful - // * documentation can make a big difference for the developer experience in your repo. - // */ - // "description": "A custom flag parameter that is passed to the scripts that are invoked when building projects", - // - // /** - // * (Required) A list of custom commands and/or built-in Rush commands that this parameter may - // * be used with. The parameter will be appended to the shell command that Rush invokes. - // */ - // "associatedCommands": ["build", "rebuild"] - // }, - // - // { - // /** - // * (Required) Determines the type of custom parameter. - // * A "string" is a custom command-line parameter whose value is a simple text string. - // */ - // "parameterKind": "string", - // "longName": "--my-string", - // "description": "A custom string parameter for the \"my-global-command\" custom command", - // - // "associatedCommands": ["my-global-command"], - // - // /** - // * The name of the argument, which will be shown in the command-line help. - // * - // * For example, if the parameter name is '--count" and the argument name is "NUMBER", - // * then the command-line help would display "--count NUMBER". The argument name must - // * be comprised of upper-case letters, numbers, and underscores. It should be kept short. - // */ - // "argumentName": "SOME_TEXT", - // - // /** - // * If true, this parameter must be included with the command. The default is false. - // */ - // "required": false - // }, - // - // { - // /** - // * (Required) Determines the type of custom parameter. - // * A "choice" is a custom command-line parameter whose argument must be chosen from a list of - // * allowable alternatives. - // */ - // "parameterKind": "choice", - // "longName": "--my-choice", - // "description": "A custom choice parameter for the \"my-global-command\" custom command", - // - // "associatedCommands": ["my-global-command"], - // - // /** - // * If true, this parameter must be included with the command. The default is false. - // */ - // "required": false, - // - // /** - // * Normally if a parameter is omitted from the command line, it will not be passed - // * to the shell command. this value will be inserted by default. Whereas if a "defaultValue" - // * is defined, the parameter will always be passed to the shell command, and will use the - // * default value if unspecified. The value must be one of the defined alternatives. - // */ - // "defaultValue": "vanilla", - // - // /** - // * (Required) A list of alternative argument values that can be chosen for this parameter. - // */ - // "alternatives": [ - // { - // /** - // * A token that is one of the alternatives that can be used with the choice parameter, - // * e.g. "vanilla" in "--flavor vanilla". - // */ - // "name": "vanilla", - // - // /** - // * A detailed description for the alternative that can be shown in the command-line help. - // * - // * Whenever you introduce commands/parameters, taking a little time to write meaningful - // * documentation can make a big difference for the developer experience in your repo. - // */ - // "description": "Use the vanilla flavor (the default)" - // }, - // - // { - // "name": "chocolate", - // "description": "Use the chocolate flavor" - // }, - // - // { - // "name": "strawberry", - // "description": "Use the strawberry flavor" - // } - // ] - // } - ] -} diff --git a/foundations/core/common/config/rush/common-versions.json b/foundations/core/common/config/rush/common-versions.json deleted file mode 100644 index 4a3ecbd1ee..0000000000 --- a/foundations/core/common/config/rush/common-versions.json +++ /dev/null @@ -1,77 +0,0 @@ -/** - * This configuration file specifies NPM dependency version selections that affect all projects - * in a Rush repo. More documentation is available on the Rush website: https://rushjs.io - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/common-versions.schema.json", - - /** - * A table that specifies a "preferred version" for a given NPM package. This feature is typically used - * to hold back an indirect dependency to a specific older version, or to reduce duplication of indirect dependencies. - * - * The "preferredVersions" value can be any SemVer range specifier (e.g. "~1.2.3"). Rush injects these values into - * the "dependencies" field of the top-level common/temp/package.json, which influences how the package manager - * will calculate versions. The specific effect depends on your package manager. Generally it will have no - * effect on an incompatible or already constrained SemVer range. If you are using PNPM, similar effects can be - * achieved using the pnpmfile.js hook. See the Rush documentation for more details. - * - * After modifying this field, it's recommended to run "rush update --full" so that the package manager - * will recalculate all version selections. - */ - "preferredVersions": { - /** - * When someone asks for "^1.0.0" make sure they get "1.2.3" when working in this repo, - * instead of the latest version. - */ - // "some-library": "1.2.3" - }, - - /** - * When set to true, for all projects in the repo, all dependencies will be automatically added as preferredVersions, - * except in cases where different projects specify different version ranges for a given dependency. For older - * package managers, this tended to reduce duplication of indirect dependencies. However, it can sometimes cause - * trouble for indirect dependencies with incompatible peerDependencies ranges. - * - * The default value is true. If you're encountering installation errors related to peer dependencies, - * it's recommended to set this to false. - * - * After modifying this field, it's recommended to run "rush update --full" so that the package manager - * will recalculate all version selections. - */ - // "implicitlyPreferredVersions": false, - - /** - * If you would like the version specifiers for your dependencies to be consistent, then - * uncomment this line. This is effectively similar to running "rush check" before any - * of the following commands: - * - * rush install, rush update, rush link, rush version, rush publish - * - * In some cases you may want this turned on, but need to allow certain packages to use a different - * version. In those cases, you will need to add an entry to the "allowedAlternativeVersions" - * section of the common-versions.json. - * - * In the case that subspaces is enabled, this setting will take effect at a subspace level. - */ - // "ensureConsistentVersions": true, - - /** - * The "rush check" command can be used to enforce that every project in the repo must specify - * the same SemVer range for a given dependency. However, sometimes exceptions are needed. - * The allowedAlternativeVersions table allows you to list other SemVer ranges that will be - * accepted by "rush check" for a given dependency. - * - * IMPORTANT: THIS TABLE IS FOR *ADDITIONAL* VERSION RANGES THAT ARE ALTERNATIVES TO THE - * USUAL VERSION (WHICH IS INFERRED BY LOOKING AT ALL PROJECTS IN THE REPO). - * This design avoids unnecessary churn in this file. - */ - "allowedAlternativeVersions": { - /** - * For example, allow some projects to use an older TypeScript compiler - * (in addition to whatever "usual" version is being used by other projects in the repo): - */ - // "typescript": [ - // "~2.4.0" - // ] - } -} \ No newline at end of file diff --git a/foundations/core/common/config/rush/custom-tips.json b/foundations/core/common/config/rush/custom-tips.json deleted file mode 100644 index 31e540d6ad..0000000000 --- a/foundations/core/common/config/rush/custom-tips.json +++ /dev/null @@ -1,29 +0,0 @@ -/** - * This configuration file allows repo maintainers to configure extra details to be - * printed alongside certain Rush messages. More documentation is available on the - * Rush website: https://rushjs.io - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/custom-tips.schema.json", - - /** - * Custom tips allow you to annotate Rush's console messages with advice tailored for - * your specific monorepo. - */ - "customTips": [ - // { - // /** - // * (REQUIRED) An identifier indicating a message that may be printed by Rush. - // * If that message is printed, then this custom tip will be shown. - // * The list of available tip identifiers can be found on this page: - // * https://rushjs.io/pages/maintainer/custom_tips/ - // */ - // "tipId": "TIP_RUSH_INCONSISTENT_VERSIONS", - // - // /** - // * (REQUIRED) The message text to be displayed for this tip. - // */ - // "message": "For additional troubleshooting information, refer this wiki article:\n\nhttps://intranet.contoso.com/docs/pnpm-mismatch" - // } - ] -} diff --git a/foundations/core/common/config/rush/experiments.json b/foundations/core/common/config/rush/experiments.json deleted file mode 100644 index 01f8f8f902..0000000000 --- a/foundations/core/common/config/rush/experiments.json +++ /dev/null @@ -1,121 +0,0 @@ -/** - * This configuration file allows repo maintainers to enable and disable experimental - * Rush features. More documentation is available on the Rush website: https://rushjs.io - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/experiments.schema.json", - - /** - * By default, 'rush install' passes --no-prefer-frozen-lockfile to 'pnpm install'. - * Set this option to true to pass '--frozen-lockfile' instead for faster installs. - */ - // "usePnpmFrozenLockfileForRushInstall": true, - - /** - * By default, 'rush update' passes --no-prefer-frozen-lockfile to 'pnpm install'. - * Set this option to true to pass '--prefer-frozen-lockfile' instead to minimize shrinkwrap changes. - */ - // "usePnpmPreferFrozenLockfileForRushUpdate": true, - - /** - * By default, 'rush update' runs as a single operation. - * Set this option to true to instead update the lockfile with `--lockfile-only`, then perform a `--frozen-lockfile` install. - * Necessary when using the `afterAllResolved` hook in .pnpmfile.cjs. - */ - // "usePnpmLockfileOnlyThenFrozenLockfileForRushUpdate": true, - - /** - * If using the 'preventManualShrinkwrapChanges' option, restricts the hash to only include the layout of external dependencies. - * Used to allow links between workspace projects or the addition/removal of references to existing dependency versions to not - * cause hash changes. - */ - // "omitImportersFromPreventManualShrinkwrapChanges": true, - - /** - * If true, the chmod field in temporary project tar headers will not be normalized. - * This normalization can help ensure consistent tarball integrity across platforms. - */ - // "noChmodFieldInTarHeaderNormalization": true, - - /** - * If true, build caching will respect the allowWarningsInSuccessfulBuild flag and cache builds with warnings. - * This will not replay warnings from the cached build. - */ - // "buildCacheWithAllowWarningsInSuccessfulBuild": true, - - /** - * If true, build skipping will respect the allowWarningsInSuccessfulBuild flag and skip builds with warnings. - * This will not replay warnings from the skipped build. - */ - // "buildSkipWithAllowWarningsInSuccessfulBuild": true, - - /** - * If true, perform a clean install after when running `rush install` or `rush update` if the - * `.npmrc` file has changed since the last install. - */ - // "cleanInstallAfterNpmrcChanges": true, - - /** - * If true, print the outputs of shell commands defined in event hooks to the console. - */ - // "printEventHooksOutputToConsole": true, - - /** - * If true, Rush will not allow node_modules in the repo folder or in parent folders. - */ - // "forbidPhantomResolvableNodeModulesFolders": true, - - /** - * (UNDER DEVELOPMENT) For certain installation problems involving peer dependencies, PNPM cannot - * correctly satisfy versioning requirements without installing duplicate copies of a package inside the - * node_modules folder. This poses a problem for "workspace:*" dependencies, as they are normally - * installed by making a symlink to the local project source folder. PNPM's "injected dependencies" - * feature provides a model for copying the local project folder into node_modules, however copying - * must occur AFTER the dependency project is built and BEFORE the consuming project starts to build. - * The "pnpm-sync" tool manages this operation; see its documentation for details. - * Enable this experiment if you want "rush" and "rushx" commands to resync injected dependencies - * by invoking "pnpm-sync" during the build. - */ - // "usePnpmSyncForInjectedDependencies": true, - - /** - * If set to true, Rush will generate a `project-impact-graph.yaml` file in the repository root during `rush update`. - */ - // "generateProjectImpactGraphDuringRushUpdate": true, - - /** - * If true, when running in watch mode, Rush will check for phase scripts named `_phase::ipc` and run them instead - * of `_phase:` if they exist. The created child process will be provided with an IPC channel and expected to persist - * across invocations. - */ - // "useIPCScriptsInWatchMode": true, - - /** - * (UNDER DEVELOPMENT) The Rush alerts feature provides a way to send announcements to engineers - * working in the monorepo, by printing directly in the user's shell window when they invoke Rush commands. - * This ensures that important notices will be seen by anyone doing active development, since people often - * ignore normal discussion group messages or don't know to subscribe. - */ - // "rushAlerts": true, - - - /** - * When using cobuilds, this experiment allows uncacheable operations to benefit from cobuild orchestration without using the build cache. - */ - // "allowCobuildWithoutCache": true, - - /** - * By default, rush perform a full scan of the entire repository. For example, Rush runs `git status` to check for local file changes. - * When this toggle is enabled, Rush will only scan specific paths, significantly speeding up Git operations. - */ - // "enableSubpathScan": true, - - /** - * Rush has a policy that normally requires Rush projects to specify `workspace:*` in package.json when depending - * on other projects in the workspace, unless they are explicitly declared as `decoupledLocalDependencies` - * in rush.json. Enabling this experiment will remove that requirement for dependencies belonging to a different - * subspace. This is useful for large product groups who work in separate subspaces and generally prefer to consume - * each other's packages via the NPM registry. - */ - // "exemptDecoupledDependenciesBetweenSubspaces": false -} diff --git a/foundations/core/common/config/rush/pnpm-config.json b/foundations/core/common/config/rush/pnpm-config.json deleted file mode 100644 index c6b1d837b5..0000000000 --- a/foundations/core/common/config/rush/pnpm-config.json +++ /dev/null @@ -1,329 +0,0 @@ -/** - * This configuration file provides settings specific to the PNPM package manager. - * More documentation is available on the Rush website: https://rushjs.io - * - * Rush normally looks for this file in `common/config/rush/pnpm-config.json`. However, - * if `subspacesEnabled` is true in subspaces.json, then Rush will instead first look - * for `common/config/subspaces//pnpm-config.json`. (If the file exists in both places, - * then the file under `common/config/rush` is ignored.) - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/pnpm-config.schema.json", - - /** - * If true, then `rush install` and `rush update` will use the PNPM workspaces feature - * to perform the install, instead of the old model where Rush generated the symlinks - * for each projects's node_modules folder. - * - * When using workspaces, Rush will generate a `common/temp/pnpm-workspace.yaml` file referencing - * all local projects to install. Rush will also generate a `.pnpmfile.cjs` shim which implements - * Rush-specific features such as preferred versions. The user's `common/config/rush/.pnpmfile.cjs` - * is invoked by the shim. - * - * This option is strongly recommended. The default value is false. - */ - "useWorkspaces": true, - - /** - * This setting determines how PNPM chooses version numbers during `rush update`. - * For example, suppose `lib-x@3.0.0` depends on `"lib-y": "^1.2.3"` whose latest major - * releases are `1.8.9` and `2.3.4`. The resolution mode `lowest-direct` might choose - * `lib-y@1.2.3`, wheres `highest` will choose 1.8.9, and `time-based` will pick the - * highest compatible version at the time when `lib-x@3.0.0` itself was published (ensuring - * that the version could have been tested by the maintainer of "lib-x"). For local workspace - * projects, `time-based` instead works like `lowest-direct`, avoiding upgrades unless - * they are explicitly requested. Although `time-based` is the most robust option, it may be - * slightly slower with registries such as npmjs.com that have not implemented an optimization. - * - * IMPORTANT: Be aware that PNPM 8.0.0 initially defaulted to `lowest-direct` instead of - * `highest`, but PNPM reverted this decision in 8.6.12 because it caused confusion for users. - * Rush version 5.106.0 and newer avoids this confusion by consistently defaulting to - * `highest` when `resolutionMode` is not explicitly set in pnpm-config.json or .npmrc, - * regardless of your PNPM version. - * - * PNPM documentation: https://pnpm.io/npmrc#resolution-mode - * - * Possible values are: `highest`, `time-based`, and `lowest-direct`. - * The default is `highest`. - */ - // "resolutionMode": "time-based", - - /** - * This setting determines whether PNPM will automatically install (non-optional) - * missing peer dependencies instead of reporting an error. Doing so conveniently - * avoids the need to specify peer versions in package.json, but in a large monorepo - * this often creates worse problems. The reason is that peer dependency behavior - * is inherently complicated, and it is easier to troubleshoot consequences of an explicit - * version than an invisible heuristic. The original NPM RFC discussion pointed out - * some other problems with this feature: https://github.com/npm/rfcs/pull/43 - - * IMPORTANT: Without Rush, the setting defaults to true for PNPM 8 and newer; however, - * as of Rush version 5.109.0 the default is always false unless `autoInstallPeers` - * is specified in pnpm-config.json or .npmrc, regardless of your PNPM version. - - * PNPM documentation: https://pnpm.io/npmrc#auto-install-peers - - * The default value is false. - */ - // "autoInstallPeers": false, - - /** - * If true, then Rush will add the `--strict-peer-dependencies` command-line parameter when - * invoking PNPM. This causes `rush update` to fail if there are unsatisfied peer dependencies, - * which is an invalid state that can cause build failures or incompatible dependency versions. - * (For historical reasons, JavaScript package managers generally do not treat this invalid - * state as an error.) - * - * PNPM documentation: https://pnpm.io/npmrc#strict-peer-dependencies - * - * The default value is false to avoid legacy compatibility issues. - * It is strongly recommended to set `strictPeerDependencies=true`. - */ - "strictPeerDependencies": true, - - /** - * Environment variables that will be provided to PNPM. - */ - // "environmentVariables": { - // "NODE_OPTIONS": { - // "value": "--max-old-space-size=4096", - // "override": false - // } - // }, - - /** - * Specifies the location of the PNPM store. There are two possible values: - * - * - `local` - use the `pnpm-store` folder in the current configured temp folder: - * `common/temp/pnpm-store` by default. - * - `global` - use PNPM's global store, which has the benefit of being shared - * across multiple repo folders, but the disadvantage of less isolation for builds - * (for example, bugs or incompatibilities when two repos use different releases of PNPM) - * - * In both cases, the store path can be overridden by the environment variable `RUSH_PNPM_STORE_PATH`. - * - * The default value is `local`. - */ - // "pnpmStore": "global", - - /** - * If true, then `rush install` will report an error if manual modifications - * were made to the PNPM shrinkwrap file without running `rush update` afterwards. - * - * This feature protects against accidental inconsistencies that may be introduced - * if the PNPM shrinkwrap file (`pnpm-lock.yaml`) is manually edited. When this - * feature is enabled, `rush update` will append a hash to the file as a YAML comment, - * and then `rush update` and `rush install` will validate the hash. Note that this - * does not prohibit manual modifications, but merely requires `rush update` be run - * afterwards, ensuring that PNPM can report or repair any potential inconsistencies. - * - * To temporarily disable this validation when invoking `rush install`, use the - * `--bypass-policy` command-line parameter. - * - * The default value is false. - */ - // "preventManualShrinkwrapChanges": true, - - /** - * When a project uses `workspace:` to depend on another Rush project, PNPM normally installs - * it by creating a symlink under `node_modules`. This generally works well, but in certain - * cases such as differing `peerDependencies` versions, symlinking may cause trouble - * such as incorrectly satisfied versions. For such cases, the dependency can be declared - * as "injected", causing PNPM to copy its built output into `node_modules` like a real - * install from a registry. Details here: https://rushjs.io/pages/advanced/injected_deps/ - * - * When using Rush subspaces, these sorts of versioning problems are much more likely if - * `workspace:` refers to a project from a different subspace. This is because the symlink - * would point to a separate `node_modules` tree installed by a different PNPM lockfile. - * A comprehensive solution is to enable `alwaysInjectDependenciesFromOtherSubspaces`, - * which automatically treats all projects from other subspaces as injected dependencies - * without having to manually configure them. - * - * NOTE: Use carefully -- excessive file copying can slow down the `rush install` and - * `pnpm-sync` operations if too many dependencies become injected. - * - * The default value is false. - */ - // "alwaysInjectDependenciesFromOtherSubspaces": false, - - /** - * Defines the policies to be checked for the `pnpm-lock.yaml` file. - */ - "pnpmLockfilePolicies": { - /** - * This policy will cause "rush update" to report an error if `pnpm-lock.yaml` contains - * any SHA1 integrity hashes. - * - * For each NPM dependency, `pnpm-lock.yaml` normally stores an `integrity` hash. Although - * its main purpose is to detect corrupted or truncated network requests, this hash can also - * serve as a security fingerprint to protect against attacks that would substitute a - * malicious tarball, for example if a misconfigured .npmrc caused a machine to accidentally - * download a matching package name+version from npmjs.com instead of the private NPM registry. - * NPM originally used a SHA1 hash; this was insecure because an attacker can too easily craft - * a tarball with a matching fingerprint. For this reason, NPM later deprecated SHA1 and - * instead adopted a cryptographically strong SHA512 hash. Nonetheless, SHA1 hashes can - * occasionally reappear during "rush update", for example due to missing metadata fallbacks - * (https://github.com/orgs/pnpm/discussions/6194) or an incompletely migrated private registry. - * The `disallowInsecureSha1` policy prevents this, avoiding potential security/compliance alerts. - */ - // "disallowInsecureSha1": { - // /** - // * Enables the "disallowInsecureSha1" policy. The default value is false. - // */ - // "enabled": true, - // - // /** - // * In rare cases, a private NPM registry may continue to serve SHA1 hashes for very old - // * package versions, perhaps due to a caching issue or database migration glitch. To avoid - // * having to disable the "disallowInsecureSha1" policy for the entire monorepo, the problematic - // * package versions can be individually ignored. The "exemptPackageVersions" key is the - // * package name, and the array value lists exact version numbers to be ignored. - // */ - // "exemptPackageVersions": { - // "example1": ["1.0.0"], - // "example2": ["2.0.0", "2.0.1"] - // } - // } - }, - - /** - * The "globalOverrides" setting provides a simple mechanism for overriding version selections - * for all dependencies of all projects in the monorepo workspace. The settings are copied - * into the `pnpm.overrides` field of the `common/temp/package.json` file that is generated - * by Rush during installation. - * - * Order of precedence: `.pnpmfile.cjs` has the highest precedence, followed by - * `unsupportedPackageJsonSettings`, `globalPeerDependencyRules`, `globalPackageExtensions`, - * and `globalOverrides` has lowest precedence. - * - * PNPM documentation: https://pnpm.io/package_json#pnpmoverrides - */ - "globalOverrides": { - // "example1": "^1.0.0", - // "example2": "npm:@company/example2@^1.0.0" - }, - - /** - * The `globalPeerDependencyRules` setting provides various settings for suppressing validation errors - * that are reported during installation with `strictPeerDependencies=true`. The settings are copied - * into the `pnpm.peerDependencyRules` field of the `common/temp/package.json` file that is generated - * by Rush during installation. - * - * Order of precedence: `.pnpmfile.cjs` has the highest precedence, followed by - * `unsupportedPackageJsonSettings`, `globalPeerDependencyRules`, `globalPackageExtensions`, - * and `globalOverrides` has lowest precedence. - * - * https://pnpm.io/package_json#pnpmpeerdependencyrules - */ - "globalPeerDependencyRules": { - // "ignoreMissing": ["@eslint/*"], - // "allowedVersions": { "react": "17" }, - // "allowAny": ["@babel/*"] - }, - - /** - * The `globalPackageExtension` setting provides a way to patch arbitrary package.json fields - * for any PNPM dependency of the monorepo. The settings are copied into the `pnpm.packageExtensions` - * field of the `common/temp/package.json` file that is generated by Rush during installation. - * The `globalPackageExtension` setting has similar capabilities as `.pnpmfile.cjs` but without - * the downsides of an executable script (nondeterminism, unreliable caching, performance concerns). - * - * Order of precedence: `.pnpmfile.cjs` has the highest precedence, followed by - * `unsupportedPackageJsonSettings`, `globalPeerDependencyRules`, `globalPackageExtensions`, - * and `globalOverrides` has lowest precedence. - * - * PNPM documentation: https://pnpm.io/package_json#pnpmpackageextensions - */ - "globalPackageExtensions": { - // "fork-ts-checker-webpack-plugin": { - // "dependencies": { - // "@babel/core": "1" - // }, - // "peerDependencies": { - // "eslint": ">= 6" - // }, - // "peerDependenciesMeta": { - // "eslint": { - // "optional": true - // } - // } - // } - }, - - /** - * The `globalNeverBuiltDependencies` setting suppresses the `preinstall`, `install`, and `postinstall` - * lifecycle events for the specified NPM dependencies. This is useful for scripts with poor practices - * such as downloading large binaries without retries or attempting to invoke OS tools such as - * a C++ compiler. (PNPM's terminology refers to these lifecycle events as "building" a package; - * it has nothing to do with build system operations such as `rush build` or `rushx build`.) - * The settings are copied into the `pnpm.neverBuiltDependencies` field of the `common/temp/package.json` - * file that is generated by Rush during installation. - * - * PNPM documentation: https://pnpm.io/package_json#pnpmneverbuiltdependencies - */ - "globalNeverBuiltDependencies": [ - // "fsevents" - ], - - /** - * The `globalIgnoredOptionalDependencies` setting suppresses the installation of optional NPM - * dependencies specified in the list. This is useful when certain optional dependencies are - * not needed in your environment, such as platform-specific packages or dependencies that - * fail during installation but are not critical to your project. - * These settings are copied into the `pnpm.overrides` field of the `common/temp/package.json` - * file that is generated by Rush during installation, instructing PNPM to ignore the specified - * optional dependencies. - * - * PNPM documentation: https://pnpm.io/package_json#pnpmignoredoptionaldependencies - */ - "globalIgnoredOptionalDependencies": [ - // "fsevents" - ], - - /** - * The `globalAllowedDeprecatedVersions` setting suppresses installation warnings for package - * versions that the NPM registry reports as being deprecated. This is useful if the - * deprecated package is an indirect dependency of an external package that has not released a fix. - * The settings are copied into the `pnpm.allowedDeprecatedVersions` field of the `common/temp/package.json` - * file that is generated by Rush during installation. - * - * PNPM documentation: https://pnpm.io/package_json#pnpmalloweddeprecatedversions - * - * If you are working to eliminate a deprecated version, it's better to specify `allowedDeprecatedVersions` - * in the package.json file for individual Rush projects. - */ - "globalAllowedDeprecatedVersions": { - // "request": "*" - }, - - /** - * (THIS FIELD IS MACHINE GENERATED) The "globalPatchedDependencies" field is updated automatically - * by the `rush-pnpm patch-commit` command. It is a dictionary, where the key is an NPM package name - * and exact version, and the value is a relative path to the associated patch file. - * - * PNPM documentation: https://pnpm.io/package_json#pnpmpatcheddependencies - */ - "globalPatchedDependencies": {}, - - /** - * (USE AT YOUR OWN RISK) This is a free-form property bag that will be copied into - * the `common/temp/package.json` file that is generated by Rush during installation. - * This provides a way to experiment with new PNPM features. These settings will override - * any other Rush configuration associated with a given JSON field except for `.pnpmfile.cjs`. - * - * USAGE OF THIS SETTING IS NOT SUPPORTED BY THE RUSH MAINTAINERS AND MAY CAUSE RUSH - * TO MALFUNCTION. If you encounter a missing PNPM setting that you believe should - * be supported, please create a GitHub issue or PR. Note that Rush does not aim to - * support every possible PNPM setting, but rather to promote a battle-tested installation - * strategy that is known to provide a good experience for large teams with lots of projects. - */ - "unsupportedPackageJsonSettings": { - // "dependencies": { - // "not-a-good-practice": "*" - // }, - // "scripts": { - // "do-something": "echo Also not a good practice" - // }, - // "pnpm": { "futurePnpmFeature": true } - } -} diff --git a/foundations/core/common/config/rush/pnpm-lock.yaml b/foundations/core/common/config/rush/pnpm-lock.yaml deleted file mode 100644 index 16d79d3fc1..0000000000 --- a/foundations/core/common/config/rush/pnpm-lock.yaml +++ /dev/null @@ -1,10389 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: false - excludeLinksFromLockfile: false - -pnpmfileChecksum: sha256-La8sfCMI7irxJPTW6u4mJb4vNiyLrXeEbKaXv5G3dL0= - -importers: - - .: {} - - ../../packages/account-client: - dependencies: - '@hcengineering/core': - specifier: workspace:^0.7.22 - version: link:../core - '@hcengineering/platform': - specifier: workspace:^0.7.18 - version: link:../platform - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@types/node': - specifier: ^22.15.29 - version: 22.18.10 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - cross-env: - specifier: ~7.0.3 - version: 7.0.3 - esbuild: - specifier: ^0.25.9 - version: 0.25.11 - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - eslint-plugin-svelte: - specifier: ^2.35.1 - version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - prettier: - specifier: ^3.1.0 - version: 3.6.2 - ts-jest: - specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - ../../packages/analytics: - dependencies: - '@hcengineering/platform': - specifier: workspace:^0.7.18 - version: link:../platform - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - eslint-plugin-svelte: - specifier: ^2.35.1 - version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - prettier: - specifier: ^3.1.0 - version: 3.6.2 - ts-jest: - specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - ../../packages/analytics-service: - dependencies: - '@hcengineering/analytics': - specifier: workspace:^0.7.17 - version: link:../analytics - '@hcengineering/core': - specifier: workspace:^0.7.22 - version: link:../core - '@hcengineering/measurements-otlp': - specifier: workspace:^0.7.17 - version: link:../measurements-otlp - '@hcengineering/platform': - specifier: workspace:^0.7.18 - version: link:../platform - winston: - specifier: ^3.11.0 - version: 3.18.3 - winston-daily-rotate-file: - specifier: ^5.0.0 - version: 5.0.0(winston@3.18.3) - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - eslint-plugin-svelte: - specifier: ^2.35.1 - version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - prettier: - specifier: ^3.1.0 - version: 3.6.2 - ts-jest: - specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - ../../packages/api-client: - dependencies: - '@hcengineering/account-client': - specifier: workspace:^0.7.19 - version: link:../account-client - '@hcengineering/client': - specifier: workspace:^0.7.17 - version: link:../client - '@hcengineering/client-resources': - specifier: workspace:^0.7.17 - version: link:../client-resources - '@hcengineering/collaborator-client': - specifier: workspace:^0.7.17 - version: link:../collaborator-client - '@hcengineering/core': - specifier: workspace:^0.7.22 - version: link:../core - '@hcengineering/platform': - specifier: workspace:^0.7.18 - version: link:../platform - '@hcengineering/text': - specifier: workspace:^0.7.18 - version: link:../text - '@hcengineering/text-markdown': - specifier: workspace:^0.7.19 - version: link:../text-markdown - snappyjs: - specifier: ^0.7.0 - version: 0.7.0 - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@types/node': - specifier: ^22.15.29 - version: 22.18.10 - '@types/snappyjs': - specifier: ^0.7.1 - version: 0.7.1 - '@types/ws': - specifier: ^8.5.12 - version: 8.18.1 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - eslint-plugin-svelte: - specifier: ^2.35.1 - version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - prettier: - specifier: ^3.1.0 - version: 3.6.2 - ts-jest: - specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) - ts-node: - specifier: ^10.8.0 - version: 10.9.2(@types/node@22.18.10)(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - optionalDependencies: - ws: - specifier: ^8.18.2 - version: 8.18.3 - - ../../packages/client: - dependencies: - '@hcengineering/core': - specifier: workspace:^0.7.22 - version: link:../core - '@hcengineering/platform': - specifier: workspace:^0.7.18 - version: link:../platform - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@types/node': - specifier: ^22.15.29 - version: 22.18.10 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - eslint-plugin-svelte: - specifier: ^2.35.1 - version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - prettier: - specifier: ^3.1.0 - version: 3.6.2 - ts-jest: - specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - ../../packages/client-resources: - dependencies: - '@hcengineering/analytics': - specifier: workspace:^0.7.17 - version: link:../analytics - '@hcengineering/client': - specifier: workspace:^0.7.17 - version: link:../client - '@hcengineering/core': - specifier: workspace:^0.7.22 - version: link:../core - '@hcengineering/platform': - specifier: workspace:^0.7.18 - version: link:../platform - '@hcengineering/rpc': - specifier: workspace:^0.7.17 - version: link:../rpc - snappyjs: - specifier: ^0.7.0 - version: 0.7.0 - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@types/node': - specifier: ^22.15.29 - version: 22.18.10 - '@types/snappyjs': - specifier: ^0.7.1 - version: 0.7.1 - '@types/ws': - specifier: ^8.5.12 - version: 8.18.1 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - eslint-plugin-svelte: - specifier: ^2.35.1 - version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - prettier: - specifier: ^3.1.0 - version: 3.6.2 - ts-jest: - specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - ws: - specifier: ^8.18.2 - version: 8.18.3 - - ../../packages/collaborator-client: - dependencies: - '@hcengineering/core': - specifier: workspace:^0.7.22 - version: link:../core - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@types/node': - specifier: ^22.15.29 - version: 22.18.10 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - cross-env: - specifier: ~7.0.3 - version: 7.0.3 - esbuild: - specifier: ^0.25.9 - version: 0.25.11 - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - eslint-plugin-svelte: - specifier: ^2.35.1 - version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - prettier: - specifier: ^3.1.0 - version: 3.6.2 - ts-jest: - specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - ../../packages/core: - dependencies: - '@hcengineering/analytics': - specifier: workspace:^0.7.17 - version: link:../analytics - '@hcengineering/measurements': - specifier: workspace:^0.7.18 - version: link:../measurements - '@hcengineering/platform': - specifier: workspace:^0.7.18 - version: link:../platform - fast-equals: - specifier: ^5.2.2 - version: 5.3.2 - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@types/node': - specifier: ^22.15.29 - version: 22.18.10 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - eslint-plugin-svelte: - specifier: ^2.35.1 - version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - prettier: - specifier: ^3.1.0 - version: 3.6.2 - ts-jest: - specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - ../../packages/hulylake-client: - dependencies: - '@hcengineering/core': - specifier: workspace:^0.7.22 - version: link:../core - '@hcengineering/retry': - specifier: workspace:^0.7.17 - version: link:../retry - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@types/node': - specifier: ^22.15.29 - version: 22.18.10 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - cross-env: - specifier: ~7.0.3 - version: 7.0.3 - esbuild: - specifier: ^0.25.9 - version: 0.25.11 - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - eslint-plugin-svelte: - specifier: ^2.35.1 - version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - prettier: - specifier: ^3.1.0 - version: 3.6.2 - ts-jest: - specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - ../../packages/measurements: - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-node: - specifier: ^11.1.0 - version: 11.1.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - ts-jest: - specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) - - ../../packages/measurements-otlp: - dependencies: - '@hcengineering/measurements': - specifier: workspace:^0.7.18 - version: link:../measurements - '@opentelemetry/api': - specifier: ^1.9.0 - version: 1.9.0 - '@opentelemetry/api-logs': - specifier: ^0.203.0 - version: 0.203.0 - '@opentelemetry/auto-instrumentations-node': - specifier: ^0.62.0 - version: 0.62.2(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.1.0(@opentelemetry/api@1.9.0)) - '@opentelemetry/core': - specifier: ^2.0.1 - version: 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-http': - specifier: ^0.203.0 - version: 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': - specifier: ^0.203.0 - version: 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-http': - specifier: ^0.203.0 - version: 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/id-generator-aws-xray': - specifier: ^2.0.0 - version: 2.0.3(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': - specifier: ^0.203.0 - version: 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': - specifier: ^2.0.1 - version: 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': - specifier: ^0.203.0 - version: 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': - specifier: ^2.0.1 - version: 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-node': - specifier: ^0.203.0 - version: 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-node': - specifier: ^2.0.1 - version: 2.1.0(@opentelemetry/api@1.9.0) - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-node: - specifier: ^11.1.0 - version: 11.1.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - ts-jest: - specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) - - ../../packages/model: - dependencies: - '@hcengineering/account-client': - specifier: workspace:^0.7.19 - version: link:../account-client - '@hcengineering/analytics': - specifier: workspace:^0.7.17 - version: link:../analytics - '@hcengineering/core': - specifier: workspace:^0.7.22 - version: link:../core - '@hcengineering/platform': - specifier: workspace:^0.7.18 - version: link:../platform - '@hcengineering/rank': - specifier: workspace:^0.7.17 - version: link:../rank - '@hcengineering/storage': - specifier: workspace:^0.7.17 - version: link:../storage - fast-equals: - specifier: ^5.2.2 - version: 5.3.2 - toposort: - specifier: ^2.0.2 - version: 2.0.2 - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@types/node': - specifier: ^22.15.29 - version: 22.18.10 - '@types/toposort': - specifier: ^2.0.3 - version: 2.0.7 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - eslint-plugin-svelte: - specifier: ^2.35.1 - version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - prettier: - specifier: ^3.1.0 - version: 3.6.2 - ts-jest: - specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - ../../packages/platform: - dependencies: - intl-messageformat: - specifier: ^10.7.14 - version: 10.7.18 - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - eslint-plugin-svelte: - specifier: ^2.35.1 - version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - prettier: - specifier: ^3.1.0 - version: 3.6.2 - ts-jest: - specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - ../../packages/postgres-base: - dependencies: - postgres: - specifier: ^3.4.7 - version: 3.4.7 - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@types/node': - specifier: ^22.15.29 - version: 22.18.10 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-node: - specifier: ^11.1.0 - version: 11.1.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - ts-jest: - specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) - - ../../packages/query: - dependencies: - '@hcengineering/analytics': - specifier: workspace:^0.7.17 - version: link:../analytics - '@hcengineering/core': - specifier: workspace:^0.7.22 - version: link:../core - '@hcengineering/platform': - specifier: workspace:^0.7.18 - version: link:../platform - fast-equals: - specifier: ^5.2.2 - version: 5.3.2 - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - eslint-plugin-svelte: - specifier: ^2.35.1 - version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - prettier: - specifier: ^3.1.0 - version: 3.6.2 - ts-jest: - specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - ../../packages/rank: - dependencies: - '@hcengineering/core': - specifier: workspace:^0.7.22 - version: link:../core - lexorank: - specifier: ~1.0.4 - version: 1.0.5 - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - eslint-plugin-svelte: - specifier: ^2.35.1 - version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - prettier: - specifier: ^3.1.0 - version: 3.6.2 - ts-jest: - specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - ../../packages/retry: - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@types/node': - specifier: ^22.15.29 - version: 22.18.10 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - eslint-plugin-svelte: - specifier: ^2.35.1 - version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - prettier: - specifier: ^3.1.0 - version: 3.6.2 - ts-jest: - specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - ../../packages/rpc: - dependencies: - '@hcengineering/core': - specifier: workspace:^0.7.22 - version: link:../core - '@hcengineering/platform': - specifier: workspace:^0.7.18 - version: link:../platform - msgpackr: - specifier: ^1.11.2 - version: 1.11.5 - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@types/node': - specifier: ^22.15.29 - version: 22.18.10 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - eslint-plugin-svelte: - specifier: ^2.35.1 - version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - prettier: - specifier: ^3.1.0 - version: 3.6.2 - ts-jest: - specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - ../../packages/storage: - dependencies: - '@hcengineering/core': - specifier: workspace:^0.7.22 - version: link:../core - '@hcengineering/platform': - specifier: workspace:^0.7.18 - version: link:../platform - fast-equals: - specifier: ^5.2.2 - version: 5.3.2 - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@types/node': - specifier: ^22.15.29 - version: 22.18.10 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - eslint-plugin-svelte: - specifier: ^2.35.1 - version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - prettier: - specifier: ^3.1.0 - version: 3.6.2 - ts-jest: - specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - ../../packages/storage-client: - dependencies: - '@hcengineering/core': - specifier: workspace:^0.7.22 - version: link:../core - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@types/node': - specifier: ^22.15.29 - version: 22.18.10 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - cross-env: - specifier: ~7.0.3 - version: 7.0.3 - esbuild: - specifier: ^0.25.9 - version: 0.25.11 - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - prettier: - specifier: ^3.1.0 - version: 3.6.2 - ts-jest: - specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - ../../packages/text: - dependencies: - '@hcengineering/core': - specifier: workspace:^0.7.22 - version: link:../core - '@hcengineering/text-core': - specifier: workspace:^0.7.18 - version: link:../text-core - '@tiptap/core': - specifier: ^2.11.7 - version: 2.26.3(@tiptap/pm@2.26.3) - '@tiptap/extension-blockquote': - specifier: ^2.11.7 - version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-bold': - specifier: ^2.11.7 - version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-bullet-list': - specifier: ^2.11.7 - version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-code': - specifier: ^2.11.7 - version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-code-block': - specifier: ^2.11.7 - version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) - '@tiptap/extension-document': - specifier: ^2.11.7 - version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-dropcursor': - specifier: ^2.11.7 - version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) - '@tiptap/extension-gapcursor': - specifier: ^2.11.7 - version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) - '@tiptap/extension-hard-break': - specifier: ^2.11.7 - version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-heading': - specifier: ^2.11.7 - version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-highlight': - specifier: ^2.11.7 - version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-history': - specifier: ^2.11.7 - version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) - '@tiptap/extension-horizontal-rule': - specifier: ^2.11.7 - version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) - '@tiptap/extension-italic': - specifier: ^2.11.7 - version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-link': - specifier: ^2.11.7 - version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) - '@tiptap/extension-list-item': - specifier: ^2.11.7 - version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-mention': - specifier: ^2.11.7 - version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)(@tiptap/suggestion@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)) - '@tiptap/extension-ordered-list': - specifier: ^2.11.7 - version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-paragraph': - specifier: ^2.11.7 - version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-strike': - specifier: ^2.11.7 - version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-table': - specifier: ^2.11.7 - version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) - '@tiptap/extension-table-cell': - specifier: ^2.11.7 - version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-table-header': - specifier: ^2.11.7 - version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-table-row': - specifier: ^2.11.7 - version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-task-item': - specifier: ^2.11.7 - version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) - '@tiptap/extension-task-list': - specifier: ^2.11.7 - version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-text': - specifier: ^2.11.7 - version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-text-align': - specifier: ~2.11.0 - version: 2.11.9(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-text-style': - specifier: ~2.11.0 - version: 2.11.9(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-typography': - specifier: ^2.11.7 - version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-underline': - specifier: ^2.11.7 - version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/html': - specifier: ^2.11.7 - version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) - '@tiptap/pm': - specifier: ^2.11.7 - version: 2.26.3 - '@tiptap/starter-kit': - specifier: ^2.11.7 - version: 2.26.3 - '@tiptap/suggestion': - specifier: ^2.11.7 - version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) - fast-equals: - specifier: ^5.2.2 - version: 5.3.2 - prosemirror-codemark: - specifier: ^0.4.2 - version: 0.4.2(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3) - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@types/markdown-it': - specifier: ~13.0.0 - version: 13.0.9 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - eslint-plugin-svelte: - specifier: ^2.35.1 - version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - prettier: - specifier: ^3.1.0 - version: 3.6.2 - ts-jest: - specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - ../../packages/text-core: - dependencies: - '@hcengineering/core': - specifier: workspace:^0.7.22 - version: link:../core - fast-equals: - specifier: ^5.2.2 - version: 5.3.2 - hash-it: - specifier: ^6.0.0 - version: 6.0.0 - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@types/markdown-it': - specifier: ~13.0.0 - version: 13.0.9 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - eslint-plugin-svelte: - specifier: ^2.35.1 - version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - jest-environment-jsdom: - specifier: ^29.7.0 - version: 29.7.0 - prettier: - specifier: ^3.1.0 - version: 3.6.2 - ts-jest: - specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - ../../packages/text-html: - dependencies: - '@hcengineering/text-core': - specifier: workspace:^0.7.18 - version: link:../text-core - htmlparser2: - specifier: ^9.0.0 - version: 9.1.0 - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - eslint-plugin-svelte: - specifier: ^2.35.1 - version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - prettier: - specifier: ^3.1.0 - version: 3.6.2 - ts-jest: - specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - ../../packages/text-markdown: - dependencies: - '@hcengineering/text-core': - specifier: workspace:^0.7.18 - version: link:../text-core - '@hcengineering/text-html': - specifier: workspace:^0.7.18 - version: link:../text-html - fast-equals: - specifier: ^5.2.2 - version: 5.3.2 - markdown-it: - specifier: ^14.0.0 - version: 14.1.0 - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@types/markdown-it': - specifier: ~13.0.0 - version: 13.0.9 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - eslint-plugin-svelte: - specifier: ^2.35.1 - version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - prettier: - specifier: ^3.1.0 - version: 3.6.2 - ts-jest: - specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - ../../packages/text-ydoc: - dependencies: - '@hcengineering/core': - specifier: workspace:^0.7.22 - version: link:../core - '@hcengineering/text': - specifier: workspace:^0.7.18 - version: link:../text - '@hcengineering/text-core': - specifier: workspace:^0.7.18 - version: link:../text-core - y-protocols: - specifier: ^1.0.6 - version: 1.0.6(yjs@13.6.27) - yjs: - specifier: ^13.6.27 - version: 13.6.27 - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - eslint-plugin-svelte: - specifier: ^2.35.1 - version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - fast-equals: - specifier: ^5.2.2 - version: 5.3.2 - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - jest-environment-jsdom: - specifier: ^29.7.0 - version: 29.7.0 - prettier: - specifier: ^3.1.0 - version: 3.6.2 - ts-jest: - specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - y-prosemirror: - specifier: ^1.3.7 - version: 1.3.7(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) - - ../../packages/token: - dependencies: - '@hcengineering/core': - specifier: workspace:^0.7.22 - version: link:../core - '@hcengineering/platform': - specifier: workspace:^0.7.18 - version: link:../platform - jwt-simple: - specifier: ^0.5.6 - version: 0.5.6 - uuid: - specifier: ^8.3.2 - version: 8.3.2 - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@types/node': - specifier: ^22.15.29 - version: 22.18.10 - '@types/uuid': - specifier: ^8.3.1 - version: 8.3.4 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - eslint-plugin-svelte: - specifier: ^2.35.1 - version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - prettier: - specifier: ^3.1.0 - version: 3.6.2 - ts-jest: - specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - - ../scripts: - devDependencies: - '@hcengineering/platform-rig': - specifier: ^0.7.19 - version: 0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - eslint-plugin-svelte: - specifier: ^2.35.1 - version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - -packages: - - '@ampproject/remapping@2.3.0': - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} - - '@babel/code-frame@7.27.1': - resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} - engines: {node: '>=6.9.0'} - - '@babel/compat-data@7.28.4': - resolution: {integrity: sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.28.4': - resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.28.3': - resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-compilation-targets@7.27.2': - resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-globals@7.28.0': - resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-imports@7.27.1': - resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-transforms@7.28.3': - resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-plugin-utils@7.27.1': - resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.27.1': - resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-option@7.27.1': - resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} - engines: {node: '>=6.9.0'} - - '@babel/helpers@7.28.4': - resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.28.4': - resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/plugin-syntax-async-generators@7.8.4': - resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-bigint@7.8.3': - resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-class-properties@7.12.13': - resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-class-static-block@7.14.5': - resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-import-attributes@7.27.1': - resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-import-meta@7.10.4': - resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-json-strings@7.8.3': - resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-jsx@7.27.1': - resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-logical-assignment-operators@7.10.4': - resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': - resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-numeric-separator@7.10.4': - resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-object-rest-spread@7.8.3': - resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-optional-catch-binding@7.8.3': - resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-optional-chaining@7.8.3': - resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-private-property-in-object@7.14.5': - resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-top-level-await@7.14.5': - resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-typescript@7.27.1': - resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/template@7.27.2': - resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.28.4': - resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.28.4': - resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} - engines: {node: '>=6.9.0'} - - '@bcoe/v8-coverage@0.2.3': - resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - - '@colors/colors@1.6.0': - resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} - engines: {node: '>=0.1.90'} - - '@cspotcode/source-map-support@0.8.1': - resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} - engines: {node: '>=12'} - - '@dabh/diagnostics@2.0.8': - resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==} - - '@esbuild/aix-ppc64@0.25.11': - resolution: {integrity: sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.25.11': - resolution: {integrity: sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.25.11': - resolution: {integrity: sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.25.11': - resolution: {integrity: sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.25.11': - resolution: {integrity: sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.25.11': - resolution: {integrity: sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.25.11': - resolution: {integrity: sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.25.11': - resolution: {integrity: sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.25.11': - resolution: {integrity: sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.25.11': - resolution: {integrity: sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.25.11': - resolution: {integrity: sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.25.11': - resolution: {integrity: sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.25.11': - resolution: {integrity: sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.25.11': - resolution: {integrity: sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.25.11': - resolution: {integrity: sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.25.11': - resolution: {integrity: sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.25.11': - resolution: {integrity: sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.25.11': - resolution: {integrity: sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.25.11': - resolution: {integrity: sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.25.11': - resolution: {integrity: sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.25.11': - resolution: {integrity: sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openharmony-arm64@0.25.11': - resolution: {integrity: sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/sunos-x64@0.25.11': - resolution: {integrity: sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.25.11': - resolution: {integrity: sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.25.11': - resolution: {integrity: sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.25.11': - resolution: {integrity: sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@eslint-community/eslint-utils@4.9.0': - resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - '@eslint-community/regexpp@4.12.1': - resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - '@eslint/eslintrc@2.1.4': - resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - '@eslint/js@8.57.1': - resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - '@formatjs/ecma402-abstract@2.3.6': - resolution: {integrity: sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==} - - '@formatjs/fast-memoize@2.2.7': - resolution: {integrity: sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==} - - '@formatjs/icu-messageformat-parser@2.11.4': - resolution: {integrity: sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==} - - '@formatjs/icu-skeleton-parser@1.8.16': - resolution: {integrity: sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==} - - '@formatjs/intl-localematcher@0.6.2': - resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==} - - '@grpc/grpc-js@1.14.0': - resolution: {integrity: sha512-N8Jx6PaYzcTRNzirReJCtADVoq4z7+1KQ4E70jTg/koQiMoUSN1kbNjPOqpPbhMFhfU1/l7ixspPl8dNY+FoUg==} - engines: {node: '>=12.10.0'} - - '@grpc/proto-loader@0.8.0': - resolution: {integrity: sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==} - engines: {node: '>=6'} - hasBin: true - - '@hcengineering/platform-rig@0.7.19': - resolution: {integrity: sha512-3Fi5nU+nEPjzzcm3FIfEZU+ZgdkiSfL46ojbj42jdXnWMkxew2aZZVtfmIpPnMUP9AMZlj2/EA+yOcMzfY/VmA==} - hasBin: true - - '@humanwhocodes/config-array@0.13.0': - resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} - engines: {node: '>=10.10.0'} - deprecated: Use @eslint/config-array instead - - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - - '@humanwhocodes/object-schema@2.0.3': - resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} - deprecated: Use @eslint/object-schema instead - - '@istanbuljs/load-nyc-config@1.1.0': - resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} - engines: {node: '>=8'} - - '@istanbuljs/schema@0.1.3': - resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} - engines: {node: '>=8'} - - '@jest/console@29.7.0': - resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/core@29.7.0': - resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - - '@jest/environment@29.7.0': - resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/expect-utils@29.7.0': - resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/expect@29.7.0': - resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/fake-timers@29.7.0': - resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/globals@29.7.0': - resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/reporters@29.7.0': - resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - - '@jest/schemas@29.6.3': - resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/source-map@29.6.3': - resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/test-result@29.7.0': - resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/test-sequencer@29.7.0': - resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/transform@29.7.0': - resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/types@29.6.3': - resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jridgewell/gen-mapping@0.3.13': - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - - '@jridgewell/trace-mapping@0.3.9': - resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - - '@js-sdsl/ordered-map@4.4.2': - resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} - - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': - resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} - cpu: [arm64] - os: [darwin] - - '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': - resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==} - cpu: [x64] - os: [darwin] - - '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': - resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==} - cpu: [arm64] - os: [linux] - - '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': - resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==} - cpu: [arm] - os: [linux] - - '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': - resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==} - cpu: [x64] - os: [linux] - - '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': - resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==} - cpu: [x64] - os: [win32] - - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - - '@opentelemetry/api-logs@0.203.0': - resolution: {integrity: sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==} - engines: {node: '>=8.0.0'} - - '@opentelemetry/api@1.9.0': - resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} - engines: {node: '>=8.0.0'} - - '@opentelemetry/auto-instrumentations-node@0.62.2': - resolution: {integrity: sha512-Ipe6X7ddrCiRsuewyTU83IvKiSFT4piqmv9z8Ovg1E7v98pdTj1pUE6sDrHV50zl7/ypd+cONBgt+EYSZu4u9Q==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.4.1 - '@opentelemetry/core': ^2.0.0 - - '@opentelemetry/context-async-hooks@2.0.1': - resolution: {integrity: sha512-XuY23lSI3d4PEqKA+7SLtAgwqIfc6E/E9eAQWLN1vlpC53ybO3o6jW4BsXo1xvz9lYyyWItfQDDLzezER01mCw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - - '@opentelemetry/context-async-hooks@2.1.0': - resolution: {integrity: sha512-zOyetmZppnwTyPrt4S7jMfXiSX9yyfF0hxlA8B5oo2TtKl+/RGCy7fi4DrBfIf3lCPrkKsRBWZZD7RFojK7FDg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - - '@opentelemetry/core@2.0.1': - resolution: {integrity: sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - - '@opentelemetry/core@2.1.0': - resolution: {integrity: sha512-RMEtHsxJs/GiHHxYT58IY57UXAQTuUnZVco6ymDEqTNlJKTimM4qPUPVe8InNFyBjhHBEAx4k3Q8LtNayBsbUQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - - '@opentelemetry/exporter-logs-otlp-grpc@0.203.0': - resolution: {integrity: sha512-g/2Y2noc/l96zmM+g0LdeuyYKINyBwN6FJySoU15LHPLcMN/1a0wNk2SegwKcxrRdE7Xsm7fkIR5n6XFe3QpPw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/exporter-logs-otlp-http@0.203.0': - resolution: {integrity: sha512-s0hys1ljqlMTbXx2XiplmMJg9wG570Z5lH7wMvrZX6lcODI56sG4HL03jklF63tBeyNwK2RV1/ntXGo3HgG4Qw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/exporter-logs-otlp-proto@0.203.0': - resolution: {integrity: sha512-nl/7S91MXn5R1aIzoWtMKGvqxgJgepB/sH9qW0rZvZtabnsjbf8OQ1uSx3yogtvLr0GzwD596nQKz2fV7q2RBw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/exporter-metrics-otlp-grpc@0.203.0': - resolution: {integrity: sha512-FCCj9nVZpumPQSEI57jRAA89hQQgONuoC35Lt+rayWY/mzCAc6BQT7RFyFaZKJ2B7IQ8kYjOCPsF/HGFWjdQkQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/exporter-metrics-otlp-http@0.203.0': - resolution: {integrity: sha512-HFSW10y8lY6BTZecGNpV3GpoSy7eaO0Z6GATwZasnT4bEsILp8UJXNG5OmEsz4SdwCSYvyCbTJdNbZP3/8LGCQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/exporter-metrics-otlp-proto@0.203.0': - resolution: {integrity: sha512-OZnhyd9npU7QbyuHXFEPVm3LnjZYifuKpT3kTnF84mXeEQ84pJJZgyLBpU4FSkSwUkt/zbMyNAI7y5+jYTWGIg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/exporter-prometheus@0.203.0': - resolution: {integrity: sha512-2jLuNuw5m4sUj/SncDf/mFPabUxMZmmYetx5RKIMIQyPnl6G6ooFzfeE8aXNRf8YD1ZXNlCnRPcISxjveGJHNg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/exporter-trace-otlp-grpc@0.203.0': - resolution: {integrity: sha512-322coOTf81bm6cAA8+ML6A+m4r2xTCdmAZzGNTboPXRzhwPt4JEmovsFAs+grpdarObd68msOJ9FfH3jxM6wqA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/exporter-trace-otlp-http@0.203.0': - resolution: {integrity: sha512-ZDiaswNYo0yq/cy1bBLJFe691izEJ6IgNmkjm4C6kE9ub/OMQqDXORx2D2j8fzTBTxONyzusbaZlqtfmyqURPw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/exporter-trace-otlp-proto@0.203.0': - resolution: {integrity: sha512-1xwNTJ86L0aJmWRwENCJlH4LULMG2sOXWIVw+Szta4fkqKVY50Eo4HoVKKq6U9QEytrWCr8+zjw0q/ZOeXpcAQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/exporter-zipkin@2.0.1': - resolution: {integrity: sha512-a9eeyHIipfdxzCfc2XPrE+/TI3wmrZUDFtG2RRXHSbZZULAny7SyybSvaDvS77a7iib5MPiAvluwVvbGTsHxsw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.0.0 - - '@opentelemetry/id-generator-aws-xray@2.0.3': - resolution: {integrity: sha512-IoJiHlNAYn/XetYUwWR2HvAQxzoh7Ef/V2toKjnRz26RckfywhJAGlzKMWc0D7XyKMbBxGGg8K9IcDVu1JuJXQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.0.0 - - '@opentelemetry/instrumentation-amqplib@0.50.0': - resolution: {integrity: sha512-kwNs/itehHG/qaQBcVrLNcvXVPW0I4FCOVtw3LHMLdYIqD7GJ6Yv2nX+a4YHjzbzIeRYj8iyMp0Bl7tlkidq5w==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-aws-lambda@0.54.1': - resolution: {integrity: sha512-qm8pGSAM1mXk7unbrGktWWGJc6IFI58ZsaHJ+i420Fp5VO3Vf7GglIgaXTS8CKBrVB4LHFj3NvzJg31PtsAQcA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-aws-sdk@0.58.0': - resolution: {integrity: sha512-9vFH7gU686dsAeLMCkqUj9y0MQZ1xrTtStSpNV2UaGWtDnRjJrAdJLu9Y545oKEaDTeVaob4UflyZvvpZnw3Xw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-bunyan@0.49.0': - resolution: {integrity: sha512-ky5Am1y6s3Ex/3RygHxB/ZXNG07zPfg9Z6Ora+vfeKcr/+I6CJbWXWhSBJor3gFgKN3RvC11UWVURnmDpBS6Pg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-cassandra-driver@0.49.0': - resolution: {integrity: sha512-BNIvqldmLkeikfI5w5Rlm9vG5NnQexfPoxOgEMzfDVOEF+vS6351I6DzWLLgWWR9CNF/jQJJi/lr6am2DLp0Rw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-connect@0.47.0': - resolution: {integrity: sha512-pjenvjR6+PMRb6/4X85L4OtkQCootgb/Jzh/l/Utu3SJHBid1F+gk9sTGU2FWuhhEfV6P7MZ7BmCdHXQjgJ42g==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-cucumber@0.19.0': - resolution: {integrity: sha512-99ms8kQWRuPt5lkDqbJJzD+7Tq5TMUlBZki4SA2h6CgK4ncX+tyep9XFY1e+XTBLJIWmuFMGbWqBLJ4fSKIQNQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.0.0 - - '@opentelemetry/instrumentation-dataloader@0.21.1': - resolution: {integrity: sha512-hNAm/bwGawLM8VDjKR0ZUDJ/D/qKR3s6lA5NV+btNaPVm2acqhPcT47l2uCVi+70lng2mywfQncor9v8/ykuyw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-dns@0.47.0': - resolution: {integrity: sha512-775fOnewWkTF4iXMGKgwvOGqEmPrU1PZpXjjqvTrEErYBJe7Fz1WlEeUStHepyKOdld7Ghv7TOF/kE3QDctvrg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-express@0.52.0': - resolution: {integrity: sha512-W7pizN0Wh1/cbNhhTf7C62NpyYw7VfCFTYg0DYieSTrtPBT1vmoSZei19wfKLnrMsz3sHayCg0HxCVL2c+cz5w==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-fastify@0.48.0': - resolution: {integrity: sha512-3zQlE/DoVfVH6/ycuTv7vtR/xib6WOa0aLFfslYcvE62z0htRu/ot8PV/zmMZfnzpTQj8S/4ULv36R6UIbpJIg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-fs@0.23.0': - resolution: {integrity: sha512-Puan+QopWHA/KNYvDfOZN6M/JtF6buXEyD934vrb8WhsX1/FuM7OtoMlQyIqAadnE8FqqDL4KDPiEfCQH6pQcQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-generic-pool@0.47.0': - resolution: {integrity: sha512-UfHqf3zYK+CwDwEtTjaD12uUqGGTswZ7ofLBEdQ4sEJp9GHSSJMQ2hT3pgBxyKADzUdoxQAv/7NqvL42ZI+Qbw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-graphql@0.51.0': - resolution: {integrity: sha512-LchkOu9X5DrXAnPI1+Z06h/EH/zC7D6sA86hhPrk3evLlsJTz0grPrkL/yUJM9Ty0CL/y2HSvmWQCjbJEz/ADg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-grpc@0.203.0': - resolution: {integrity: sha512-Qmjx2iwccHYRLoE4RFS46CvQE9JG9Pfeae4EPaNZjvIuJxb/pZa2R9VWzRlTehqQWpAvto/dGhtkw8Tv+o0LTg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-hapi@0.50.0': - resolution: {integrity: sha512-5xGusXOFQXKacrZmDbpHQzqYD1gIkrMWuwvlrEPkYOsjUqGUjl1HbxCsn5Y9bUXOCgP1Lj6A4PcKt1UiJ2MujA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-http@0.203.0': - resolution: {integrity: sha512-y3uQAcCOAwnO6vEuNVocmpVzG3PER6/YZqbPbbffDdJ9te5NkHEkfSMNzlC3+v7KlE+WinPGc3N7MR30G1HY2g==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-ioredis@0.51.0': - resolution: {integrity: sha512-9IUws0XWCb80NovS+17eONXsw1ZJbHwYYMXiwsfR9TSurkLV5UNbRSKb9URHO+K+pIJILy9wCxvyiOneMr91Ig==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-kafkajs@0.13.0': - resolution: {integrity: sha512-FPQyJsREOaGH64hcxlzTsIEQC4DYANgTwHjiB7z9lldmvua1LRMVn3/FfBlzXoqF179B0VGYviz6rn75E9wsDw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-knex@0.48.0': - resolution: {integrity: sha512-V5wuaBPv/lwGxuHjC6Na2JFRjtPgstw19jTFl1B1b6zvaX8zVDYUDaR5hL7glnQtUSCMktPttQsgK4dhXpddcA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-koa@0.51.0': - resolution: {integrity: sha512-XNLWeMTMG1/EkQBbgPYzCeBD0cwOrfnn8ao4hWgLv0fNCFQu1kCsJYygz2cvKuCs340RlnG4i321hX7R8gj3Rg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-lru-memoizer@0.48.0': - resolution: {integrity: sha512-KUW29wfMlTPX1wFz+NNrmE7IzN7NWZDrmFWHM/VJcmFEuQGnnBuTIdsP55CnBDxKgQ/qqYFp4udQFNtjeFosPw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-memcached@0.47.0': - resolution: {integrity: sha512-vXDs/l4hlWy1IepPG1S6aYiIZn+tZDI24kAzwKKJmR2QEJRL84PojmALAEJGazIOLl/VdcCPZdMb0U2K0VzojA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-mongodb@0.56.0': - resolution: {integrity: sha512-YG5IXUUmxX3Md2buVMvxm9NWlKADrnavI36hbJsihqqvBGsWnIfguf0rUP5Srr0pfPqhQjUP+agLMsvu0GmUpA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-mongoose@0.50.0': - resolution: {integrity: sha512-Am8pk1Ct951r4qCiqkBcGmPIgGhoDiFcRtqPSLbJrUZqEPUsigjtMjoWDRLG1Ki1NHgOF7D0H7d+suWz1AAizw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-mysql2@0.50.0': - resolution: {integrity: sha512-PoOMpmq73rOIE3nlTNLf3B1SyNYGsp7QXHYKmeTZZnJ2Ou7/fdURuOhWOI0e6QZ5gSem18IR1sJi6GOULBQJ9g==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-mysql@0.49.0': - resolution: {integrity: sha512-QU9IUNqNsrlfE3dJkZnFHqLjlndiU39ll/YAAEvWE40sGOCi9AtOF6rmEGzJ1IswoZ3oyePV7q2MP8SrhJfVAA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-nestjs-core@0.49.0': - resolution: {integrity: sha512-1R/JFwdmZIk3T/cPOCkVvFQeKYzbbUvDxVH3ShXamUwBlGkdEu5QJitlRMyVNZaHkKZKWgYrBarGQsqcboYgaw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-net@0.47.0': - resolution: {integrity: sha512-csoJ++Njpf7C09JH+0HNGenuNbDZBqO1rFhMRo6s0rAmJwNh9zY3M/urzptmKlqbKnf4eH0s+CKHy/+M8fbFsQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-oracledb@0.29.0': - resolution: {integrity: sha512-2aHLiJdkyiUbooIUm7FaZf+O4jyqEl+RfFpgud1dxT87QeeYM216wi+xaMNzsb5yKtRBqbA3qeHBCyenYrOZwA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-pg@0.56.1': - resolution: {integrity: sha512-0/PiHDPVaLdcXNw6Gqb3JBdMxComMEwh444X8glwiynJKJHRTR49+l2cqJfoOVzB8Sl1XRl3Yaqw6aDi3s8e9w==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-pino@0.50.1': - resolution: {integrity: sha512-pBbvuWiHA9iAumAuQ0SKYOXK7NRlbnVTf/qBV0nMdRnxBPrc/GZTbh0f7Y59gZfYsbCLhXLL1oRTEnS+PwS3CA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-redis@0.52.0': - resolution: {integrity: sha512-R8Y7cCZlJ2Vl31S2i7bl5SqyC/aul54ski4wCFip/Tp9WGtLK1xVATi2rwy2wkc8ZCtjdEe9eEVR+QFG6gGZxg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-restify@0.49.0': - resolution: {integrity: sha512-tsGZZhS4mVZH7omYxw5jpsrD3LhWizqWc0PYtAnzpFUvL5ZINHE+cm57bssTQ2AK/GtZMxu9LktwCvIIf3dSmw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-router@0.48.0': - resolution: {integrity: sha512-Wixrc8CchuJojXpaS/dCQjFOMc+3OEil1H21G+WLYQb8PcKt5kzW9zDBT19nyjjQOx/D/uHPfgbrT+Dc7cfJ9w==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-runtime-node@0.17.1': - resolution: {integrity: sha512-c1FlAk+bB2uF9a8YneGmNPTl7c/xVaan4mmWvbkWcOmH/ipKqR1LaKUlz/BMzLrJLjho1EJlG2NrS2w2Arg+nw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-socket.io@0.50.0': - resolution: {integrity: sha512-6JN6lnKN9ZuZtZdMQIR+no1qHzQvXSZUsNe3sSWMgqmNRyEXuDUWBIyKKeG0oHRHtR4xE4QhJyD4D5kKRPWZFA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-tedious@0.22.0': - resolution: {integrity: sha512-XrrNSUCyEjH1ax9t+Uo6lv0S2FCCykcF7hSxBMxKf7Xn0bPRxD3KyFUZy25aQXzbbbUHhtdxj3r2h88SfEM3aA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-undici@0.14.0': - resolution: {integrity: sha512-2HN+7ztxAReXuxzrtA3WboAKlfP5OsPA57KQn2AdYZbJ3zeRPcLXyW4uO/jpLE6PLm0QRtmeGCmfYpqRlwgSwg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.7.0 - - '@opentelemetry/instrumentation-winston@0.48.1': - resolution: {integrity: sha512-XyOuVwdziirHHYlsw+BWrvdI/ymjwnexupKA787zQQ+D5upaE/tseZxjfQa7+t4+FdVLxHICaMTmkSD4yZHpzQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation@0.203.0': - resolution: {integrity: sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/otlp-exporter-base@0.203.0': - resolution: {integrity: sha512-Wbxf7k+87KyvxFr5D7uOiSq/vHXWommvdnNE7vECO3tAhsA2GfOlpWINCMWUEPdHZ7tCXxw6Epp3vgx3jU7llQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/otlp-grpc-exporter-base@0.203.0': - resolution: {integrity: sha512-te0Ze1ueJF+N/UOFl5jElJW4U0pZXQ8QklgSfJ2linHN0JJsuaHG8IabEUi2iqxY8ZBDlSiz1Trfv5JcjWWWwQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/otlp-transformer@0.203.0': - resolution: {integrity: sha512-Y8I6GgoCna0qDQ2W6GCRtaF24SnvqvA8OfeTi7fqigD23u8Jpb4R5KFv/pRvrlGagcCLICMIyh9wiejp4TXu/A==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/propagator-b3@2.0.1': - resolution: {integrity: sha512-Hc09CaQ8Tf5AGLmf449H726uRoBNGPBL4bjr7AnnUpzWMvhdn61F78z9qb6IqB737TffBsokGAK1XykFEZ1igw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - - '@opentelemetry/propagator-jaeger@2.0.1': - resolution: {integrity: sha512-7PMdPBmGVH2eQNb/AtSJizQNgeNTfh6jQFqys6lfhd6P4r+m/nTh3gKPPpaCXVdRQ+z93vfKk+4UGty390283w==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - - '@opentelemetry/redis-common@0.38.2': - resolution: {integrity: sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA==} - engines: {node: ^18.19.0 || >=20.6.0} - - '@opentelemetry/resource-detector-alibaba-cloud@0.31.9': - resolution: {integrity: sha512-V+HbpICyzmJoQHYpiN0xRlj7QqeR9pPo+JZiZztV77L2MdlUCa/Cq7h0gdFNIKc0P9u9rYYYW21oaqdhhC5LZg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.0.0 - - '@opentelemetry/resource-detector-aws@2.6.0': - resolution: {integrity: sha512-atZ9/HNXh9ZJuMZUH2TPl89imFZBaoiU0Mksa70ysVhYRzhk3hfJyiu+eETjZ7NhGjBPrd3sfVYEq/St/7+o3g==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.0.0 - - '@opentelemetry/resource-detector-azure@0.10.0': - resolution: {integrity: sha512-5cNAiyPBg53Uxe/CW7hsCq8HiKNAUGH+gi65TtgpzSR9bhJG4AEbuZhbJDFwe97tn2ifAD1JTkbc/OFuaaFWbA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.0.0 - - '@opentelemetry/resource-detector-container@0.7.9': - resolution: {integrity: sha512-BiS14kCylLzh/mayN/sjnOdhnpfgiekaEsIzaL29MErfQR0mFCZjAE2uu8jMjShva9bSDFs65ouuAFft+vBthg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.0.0 - - '@opentelemetry/resource-detector-gcp@0.37.0': - resolution: {integrity: sha512-LGpJBECIMsVKhiulb4nxUw++m1oF4EiDDPmFGW2aqYaAF0oUvJNv8Z/55CAzcZ7SxvlTgUwzewXDBsuCup7iqw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.0.0 - - '@opentelemetry/resources@2.0.1': - resolution: {integrity: sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.3.0 <1.10.0' - - '@opentelemetry/resources@2.1.0': - resolution: {integrity: sha512-1CJjf3LCvoefUOgegxi8h6r4B/wLSzInyhGP2UmIBYNlo4Qk5CZ73e1eEyWmfXvFtm1ybkmfb2DqWvspsYLrWw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.3.0 <1.10.0' - - '@opentelemetry/sdk-logs@0.203.0': - resolution: {integrity: sha512-vM2+rPq0Vi3nYA5akQD2f3QwossDnTDLvKbea6u/A2NZ3XDkPxMfo/PNrDoXhDUD/0pPo2CdH5ce/thn9K0kLw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.4.0 <1.10.0' - - '@opentelemetry/sdk-metrics@2.0.1': - resolution: {integrity: sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.9.0 <1.10.0' - - '@opentelemetry/sdk-metrics@2.1.0': - resolution: {integrity: sha512-J9QX459mzqHLL9Y6FZ4wQPRZG4TOpMCyPOh6mkr/humxE1W2S3Bvf4i75yiMW9uyed2Kf5rxmLhTm/UK8vNkAw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.9.0 <1.10.0' - - '@opentelemetry/sdk-node@0.203.0': - resolution: {integrity: sha512-zRMvrZGhGVMvAbbjiNQW3eKzW/073dlrSiAKPVWmkoQzah9wfynpVPeL55f9fVIm0GaBxTLcPeukWGy0/Wj7KQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.3.0 <1.10.0' - - '@opentelemetry/sdk-trace-base@2.0.1': - resolution: {integrity: sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.3.0 <1.10.0' - - '@opentelemetry/sdk-trace-base@2.1.0': - resolution: {integrity: sha512-uTX9FBlVQm4S2gVQO1sb5qyBLq/FPjbp+tmGoxu4tIgtYGmBYB44+KX/725RFDe30yBSaA9Ml9fqphe1hbUyLQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.3.0 <1.10.0' - - '@opentelemetry/sdk-trace-node@2.0.1': - resolution: {integrity: sha512-UhdbPF19pMpBtCWYP5lHbTogLWx9N0EBxtdagvkn5YtsAnCBZzL7SjktG+ZmupRgifsHMjwUaCCaVmqGfSADmA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - - '@opentelemetry/sdk-trace-node@2.1.0': - resolution: {integrity: sha512-SvVlBFc/jI96u/mmlKm86n9BbTCbQ35nsPoOohqJX6DXH92K0kTe73zGY5r8xoI1QkjR9PizszVJLzMC966y9Q==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - - '@opentelemetry/semantic-conventions@1.37.0': - resolution: {integrity: sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA==} - engines: {node: '>=14'} - - '@opentelemetry/sql-common@0.41.2': - resolution: {integrity: sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.1.0 - - '@protobufjs/aspromise@1.1.2': - resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} - - '@protobufjs/base64@1.1.2': - resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} - - '@protobufjs/codegen@2.0.4': - resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} - - '@protobufjs/eventemitter@1.1.0': - resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} - - '@protobufjs/fetch@1.1.0': - resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} - - '@protobufjs/float@1.0.2': - resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} - - '@protobufjs/inquire@1.1.0': - resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} - - '@protobufjs/path@1.1.2': - resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} - - '@protobufjs/pool@1.1.0': - resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} - - '@protobufjs/utf8@1.1.0': - resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} - - '@remirror/core-constants@3.0.0': - resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} - - '@rtsao/scc@1.1.0': - resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - - '@sinclair/typebox@0.27.8': - resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} - - '@sinonjs/commons@3.0.1': - resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} - - '@sinonjs/fake-timers@10.3.0': - resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} - - '@so-ric/colorspace@1.1.6': - resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==} - - '@tiptap/core@2.26.3': - resolution: {integrity: sha512-TaOJzu2v5ufsOx+yu94NqXE504zmupVdFCxH1g3hk5fzZ3gT57Lh9R/27OjwM4e6o+Z3DXDl8yfFMHIcR3zUkg==} - peerDependencies: - '@tiptap/pm': ^2.7.0 - - '@tiptap/extension-blockquote@2.26.3': - resolution: {integrity: sha512-brz8+wh03TuMevNUztTSC9BzZEsLCNakPJCCicD8FRpBJoLj4benT6T3GYVdMhkk4BmhpruSFZB0FPY+rxCVlA==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-bold@2.26.3': - resolution: {integrity: sha512-ssXKQxSwQ+Webv65emK/A1d13iTvnfbw8I2wlzuxsrMChyb4wH2HyqI5N4g0FpLqCpkXFumforoY+0XKktve+w==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-bullet-list@2.26.3': - resolution: {integrity: sha512-pfBMOup1JbXgf2aVTtG1A5t7qFZJrpD+wNPuypjF2YWmCl/pAlwbPFz9hNuWyZq14+QoQg5tML1/G1M7cgrrtw==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-code-block@2.26.3': - resolution: {integrity: sha512-3DbzKRfMqw9EGS7mGkpyopbRWTO+qpV52Mby4Ll2+OfhvGnHzSN4Q7xOsp+VeZr14GMEmua5Oq2e/gRypqXatQ==} - peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 - - '@tiptap/extension-code@2.26.3': - resolution: {integrity: sha512-bAkUNzV+tA1J1RYbtbAGTFqkRw9+yRpAd+d3S9jy/dAD+uOe1ZD1EIngyEf2GTonnoy4bpDYtytbCjUt9PozoA==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-document@2.26.3': - resolution: {integrity: sha512-gcJg4Otchilr4eSUwhPNwbhPUkEYvXhkUZ/1MAhVGD40Ovq2P8ZWkJipA3tKOCJinL5MJK59ccZBstnKSTw+JA==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-dropcursor@2.26.3': - resolution: {integrity: sha512-54rgDTmRStVmXZR7KdCvSOCAbumh5luXgticUkRM8OM8PBe1c0T9X8jfV7+XEFGugRVl8mtCZZpgUt5vhuxHog==} - peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 - - '@tiptap/extension-gapcursor@2.26.3': - resolution: {integrity: sha512-ZDNSkpz7ik2PJOjrys27rwko5Ufe6GtLjaAxjvkWmyzcgAOTadDeth9NaRdBVMDGgSLBKbXihYZZXLkiAP9RLA==} - peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 - - '@tiptap/extension-hard-break@2.26.3': - resolution: {integrity: sha512-KJWUi+2KOZejVRb2KI0NM3LgCpNimxcunbOCKsZKygV/UByzhUl7UaCAIa+ySMM+kbu/Ec3hkTzafGfaU9ZkLg==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-heading@2.26.3': - resolution: {integrity: sha512-bp7YildFOustuGJGl8TInG26h7xbcpBKskm49TjwyBjUqRHPGH4V11554afStAr+bsTlPN4TDXt7extvq3UYLA==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-highlight@2.26.3': - resolution: {integrity: sha512-cW5V+9es7UPLUQgU4I9gqj9w4G4PgWwJMxB107ChCAsFEb2IvC2fDcwRCHY+xiLJGPq0xZag/kvtx0uZkovITw==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-history@2.26.3': - resolution: {integrity: sha512-Qg4+WWf/hDgiBspxLbrhrIFUy7lzi2eBKPSoF/haEYFw/t/FeN60NXYYYtpLimUNpUzyJSOSIwsngFcVJO5X+g==} - peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 - - '@tiptap/extension-horizontal-rule@2.26.3': - resolution: {integrity: sha512-NhlJEDj0b/P1Rj4UOMgt4CjS4IXEhXQFsdiXmsYZxchfr4J72HrsOfZs4vAqIQbkrLgUlYEr/DGMNWzME78FrA==} - peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 - - '@tiptap/extension-italic@2.26.3': - resolution: {integrity: sha512-DJX31JQsyerqoNM+hAtbjHoJ42W/EpnMMCtQr/gRS8ssEdrVtcDDhSO2tkaP6dNjhG8zH2hKYsXpLCCFdDgvwg==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-link@2.26.3': - resolution: {integrity: sha512-cNYqAeiaG/65ctVEUOHt1MQnTF1JcdZqBkN9pLf3grzcmkmdr3w1/JbKOphZc84vOB2rxuhGZx9NFV2lrC5Qwg==} - peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 - - '@tiptap/extension-list-item@2.26.3': - resolution: {integrity: sha512-9qU0SoC+tDSKYhfdWFS3dkioEk3ml1ycBeRmOxh7h+w0ezmTomiT5yvc9t3KM30ps8n1p78sIPo19GF65u1dFQ==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-mention@2.26.3': - resolution: {integrity: sha512-VZi4Qrg8Q8Lg2v/YfuYuTjmyEtpO3cuy2racIIyXw11LhQSQ5rflHK+0mWKo00bhx2mPxxod4DYIR31DqSfvPA==} - peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 - '@tiptap/suggestion': ^2.7.0 - - '@tiptap/extension-ordered-list@2.26.3': - resolution: {integrity: sha512-x6G0qA7dAvSq+kphA7P64m+ScoVEAW8s9pl7o3jIJzcIW/LrbL1xkyOjbgCvGEvwyQVsgyqtLQDQ2oeloosDBw==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-paragraph@2.26.3': - resolution: {integrity: sha512-eBC5UsaTJRUMhePtK1dcCAfes0CpqqFiewpIM0lWk4XMtpG2aoczVVVkImybbFKfqsvEEo3vgHJ2YiE5YZFCSg==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-strike@2.26.3': - resolution: {integrity: sha512-Po3al5hP0IwvHHIHYy3DbUvCD/kbYTsi3sWTjPAB9QgqaoJGl+jyhIyha8FsR+U3MCIIJIekMktI5o1+ySMGpg==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-table-cell@2.26.3': - resolution: {integrity: sha512-m/uZSeXuRAJaLedq0MOu9ZGibh4kkovXX0i5Oj6K9lT+TtBLQBNCSABQeOCe2FFPXhpWRpnhZrqhJgdo/n9BSg==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-table-header@2.26.3': - resolution: {integrity: sha512-8+P3j5kNE04zbqGqwEFKJ82ECBoRfx6PaPkoNlftBkRnAQIWajdPcLxLpBPak1tHw9sEtnZg38aBFz7kQLcklg==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-table-row@2.26.3': - resolution: {integrity: sha512-eJC9/iWAEi/7nxL7I/3d+pUOarp8ns8cQoN0xbZnQ40irSzNXY6vpCskVm+1IulhgCbGyOtjiS8z06wBck1X9Q==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-table@2.26.3': - resolution: {integrity: sha512-Ycvem797qPazgY+9OUXL8EXS2XCnb45y1IPi6gOhP1DStaGSVMhNBWvuLipSbu8UBpB+yIJ4/sqIvlLqsZQceQ==} - peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 - - '@tiptap/extension-task-item@2.26.3': - resolution: {integrity: sha512-Pe2B/57qNPW1XlH44qdwYeKyBGYKw6TiPXg1pvdhu9zyUglu4J4gbvvn224Z5D9IHlSbPSxr4ss6DNm5S64I/w==} - peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 - - '@tiptap/extension-task-list@2.26.3': - resolution: {integrity: sha512-VSQlU9m3uQ9ReJ4CYM6+IOepLdvNHBA6P/ti/bArb0vUseDfkx0KsQINTmxnXLPF+NLQnk1uXVk5/TJg1+OdMg==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-text-align@2.11.9': - resolution: {integrity: sha512-MaCL83TPiDX20vdKSe+QwghBcJ5HDtrK6btGqgW4pugae0Pi71oe2lqkeM+eJBsiXN4WG+x9XMwBRd9vUnLF0g==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-text-style@2.11.9': - resolution: {integrity: sha512-lB3uJBRiRYTCxtNeEF70IiwzGFfAU69xvsHpQowJU357lZsTj/QPRQt8Blu48qIoUAJGU00vGRTLae6X9myMMw==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-text-style@2.26.3': - resolution: {integrity: sha512-B+t6k41xtmlIxyi0r+g8MAShGMCK6kmz8EdxoLAUVrlCxYWVk6qvzoojZbjQKlb2sE+idIo4X5yCcKpdkxFe0w==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-text@2.26.3': - resolution: {integrity: sha512-sGRbX96ss4jQeKw9d0iphuAWja8Dv4w4ryTDKfxD7Lizx3UaIxQB/y+Wna89tM3kfbi/qJcrD3AF7NJgfc/tEA==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-typography@2.26.3': - resolution: {integrity: sha512-GgWICb2FFCFzd2JJ+ECrFubcEwEtTsFcKR71jvnvVw22pgKHIp0C7PTmpzJTBVynoha+MgIweWMzC2ZtvUNNqQ==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-underline@2.26.3': - resolution: {integrity: sha512-FXQUiHjvKDIpU9Bg+fV7UqQnEjhGvVQUnrf9VOI1/9hm+GWWxAfV2asRiZgV6jeBvNJWYzUGzvQUN6vFzmVbdw==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/html@2.26.3': - resolution: {integrity: sha512-4xC91c2/qwWoQULh/JFFoRdI0wJyA/8AhYpgI549u+F4mzZ9HvEI/ztx0dMFvft7vUC/1iszVkLajOOBA33xPg==} - peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 - - '@tiptap/pm@2.26.3': - resolution: {integrity: sha512-8gUmdxWlUevmgq2mNvGxvf2CpDW097tVKECMWKEn8sf846kXv3CoqaGRhI3db4kfR+09uWZeRM7rtrjRBmUThg==} - - '@tiptap/starter-kit@2.26.3': - resolution: {integrity: sha512-hznj/j+mFIuKfNB0ToaZVcVjdtpSOHoBoX3ocSz9BaYCtK+nX1c0gTlfbJ1BcpYUZNtqG+tpUeIfvXifRkq/OQ==} - - '@tiptap/suggestion@2.26.3': - resolution: {integrity: sha512-kcyiyKEEDnqFImGQQEEuRa6N/N+/vU/OrI99wRfJnDnN8c3dP6UHJ4wr2qX6bUpx3Z0QTu6GGCpMpaqwtHTtJg==} - peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 - - '@tootallnate/once@2.0.0': - resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} - engines: {node: '>= 10'} - - '@tsconfig/node10@1.0.11': - resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} - - '@tsconfig/node12@1.0.11': - resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} - - '@tsconfig/node14@1.0.3': - resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} - - '@tsconfig/node16@1.0.4': - resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} - - '@types/aws-lambda@8.10.152': - resolution: {integrity: sha512-soT/c2gYBnT5ygwiHPmd9a1bftj462NWVk2tKCc1PYHSIacB2UwbTS2zYG4jzag1mRDuzg/OjtxQjQ2NKRB6Rw==} - - '@types/babel__core@7.20.5': - resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} - - '@types/babel__generator@7.27.0': - resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} - - '@types/babel__template@7.4.4': - resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} - - '@types/babel__traverse@7.28.0': - resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} - - '@types/bunyan@1.8.11': - resolution: {integrity: sha512-758fRH7umIMk5qt5ELmRMff4mLDlN+xyYzC+dkPTdKwbSkJFvz6xwyScrytPU0QIBbRRwbiE8/BIg8bpajerNQ==} - - '@types/connect@3.4.38': - resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - - '@types/graceful-fs@4.1.9': - resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} - - '@types/istanbul-lib-coverage@2.0.6': - resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} - - '@types/istanbul-lib-report@3.0.3': - resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} - - '@types/istanbul-reports@3.0.4': - resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} - - '@types/jest@29.5.14': - resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} - - '@types/jsdom@20.0.1': - resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} - - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - - '@types/json5@0.0.29': - resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - - '@types/linkify-it@3.0.5': - resolution: {integrity: sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==} - - '@types/linkify-it@5.0.0': - resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} - - '@types/markdown-it@13.0.9': - resolution: {integrity: sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw==} - - '@types/markdown-it@14.1.2': - resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} - - '@types/mdurl@1.0.5': - resolution: {integrity: sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==} - - '@types/mdurl@2.0.0': - resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} - - '@types/memcached@2.2.10': - resolution: {integrity: sha512-AM9smvZN55Gzs2wRrqeMHVP7KE8KWgCJO/XL5yCly2xF6EKa4YlbpK+cLSAH4NG/Ah64HrlegmGqW8kYws7Vxg==} - - '@types/mysql@2.15.27': - resolution: {integrity: sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==} - - '@types/node@22.18.10': - resolution: {integrity: sha512-anNG/V/Efn/YZY4pRzbACnKxNKoBng2VTFydVu8RRs5hQjikP8CQfaeAV59VFSCzKNp90mXiVXW2QzV56rwMrg==} - - '@types/oracledb@6.5.2': - resolution: {integrity: sha512-kK1eBS/Adeyis+3OlBDMeQQuasIDLUYXsi2T15ccNJ0iyUpQ4xDF7svFu3+bGVrI0CMBUclPciz+lsQR3JX3TQ==} - - '@types/pg-pool@2.0.6': - resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==} - - '@types/pg@8.15.5': - resolution: {integrity: sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==} - - '@types/pug@2.0.10': - resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==} - - '@types/semver@7.7.1': - resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} - - '@types/snappyjs@0.7.1': - resolution: {integrity: sha512-OxjzJ6cQZstysMh6PEwZWmK9qlKZyezHJKOkcUkZDooSFuog2votUEKkxMaTq51UQF3cJkXKQ+XGlj4FSl8JQQ==} - - '@types/stack-utils@2.0.3': - resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} - - '@types/tedious@4.0.14': - resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} - - '@types/toposort@2.0.7': - resolution: {integrity: sha512-sQNk65vbC36+UixCkcky+dCr7MlflHcVILg1FVGqlUntsLFv9xd9ToWIVko/gTuin+cVe16t+2YubEFkhnSuPQ==} - - '@types/tough-cookie@4.0.5': - resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} - - '@types/triple-beam@1.3.5': - resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} - - '@types/uuid@8.3.4': - resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==} - - '@types/ws@8.18.1': - resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - - '@types/yargs-parser@21.0.3': - resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} - - '@types/yargs@17.0.33': - resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} - - '@typescript-eslint/eslint-plugin@6.21.0': - resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/parser@6.21.0': - resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/scope-manager@6.21.0': - resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} - engines: {node: ^16.0.0 || >=18.0.0} - - '@typescript-eslint/type-utils@6.21.0': - resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/types@6.21.0': - resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} - engines: {node: ^16.0.0 || >=18.0.0} - - '@typescript-eslint/typescript-estree@6.21.0': - resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/utils@6.21.0': - resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - - '@typescript-eslint/visitor-keys@6.21.0': - resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} - engines: {node: ^16.0.0 || >=18.0.0} - - '@ungap/structured-clone@1.3.0': - resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - - abab@2.0.6: - resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} - deprecated: Use your platform's native atob() and btoa() methods instead - - acorn-globals@7.0.1: - resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} - - acorn-import-attributes@1.9.5: - resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} - peerDependencies: - acorn: ^8 - - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - - acorn-walk@8.3.4: - resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} - engines: {node: '>=0.4.0'} - - acorn@8.15.0: - resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} - engines: {node: '>=0.4.0'} - hasBin: true - - agent-base@6.0.2: - resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} - engines: {node: '>= 6.0.0'} - - agent-base@7.1.4: - resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} - engines: {node: '>= 14'} - - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - - ansi-escapes@4.3.2: - resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} - engines: {node: '>=8'} - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - - arg@4.1.3: - resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} - - argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - - aria-query@5.3.2: - resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} - engines: {node: '>= 0.4'} - - array-buffer-byte-length@1.0.2: - resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} - engines: {node: '>= 0.4'} - - array-includes@3.1.9: - resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} - engines: {node: '>= 0.4'} - - array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} - - array.prototype.findlastindex@1.2.6: - resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} - engines: {node: '>= 0.4'} - - array.prototype.flat@1.3.3: - resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} - engines: {node: '>= 0.4'} - - array.prototype.flatmap@1.3.3: - resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} - engines: {node: '>= 0.4'} - - arraybuffer.prototype.slice@1.0.4: - resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} - engines: {node: '>= 0.4'} - - async-function@1.0.0: - resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} - engines: {node: '>= 0.4'} - - async@3.2.6: - resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} - - asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - - available-typed-arrays@1.0.7: - resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} - engines: {node: '>= 0.4'} - - axobject-query@4.1.0: - resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} - engines: {node: '>= 0.4'} - - babel-jest@29.7.0: - resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@babel/core': ^7.8.0 - - babel-plugin-istanbul@6.1.1: - resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} - engines: {node: '>=8'} - - babel-plugin-jest-hoist@29.6.3: - resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - babel-preset-current-node-syntax@1.2.0: - resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} - peerDependencies: - '@babel/core': ^7.0.0 || ^8.0.0-0 - - babel-preset-jest@29.6.3: - resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@babel/core': ^7.0.0 - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - baseline-browser-mapping@2.8.16: - resolution: {integrity: sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==} - hasBin: true - - bignumber.js@9.3.1: - resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} - - binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} - - brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - - brace-expansion@2.0.2: - resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - - browserslist@4.26.3: - resolution: {integrity: sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - bs-logger@0.2.6: - resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} - engines: {node: '>= 6'} - - bser@2.1.1: - resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} - - buffer-crc32@1.0.0: - resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} - engines: {node: '>=8.0.0'} - - buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - - builtins@5.1.0: - resolution: {integrity: sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==} - - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - - call-bind@1.0.8: - resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} - engines: {node: '>= 0.4'} - - call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} - - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - camelcase@5.3.1: - resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} - engines: {node: '>=6'} - - camelcase@6.3.0: - resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} - engines: {node: '>=10'} - - caniuse-lite@1.0.30001750: - resolution: {integrity: sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - char-regex@1.0.2: - resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} - engines: {node: '>=10'} - - chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} - - ci-info@3.9.0: - resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} - engines: {node: '>=8'} - - cjs-module-lexer@1.4.3: - resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} - - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - - co@4.6.0: - resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} - engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} - - code-red@1.0.4: - resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==} - - collect-v8-coverage@1.0.2: - resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-convert@3.1.2: - resolution: {integrity: sha512-UNqkvCDXstVck3kdowtOTWROIJQwafjOfXSmddoDrXo4cewMKmusCeF22Q24zvjR8nwWib/3S/dfyzPItPEiJg==} - engines: {node: '>=14.6'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - color-name@2.0.2: - resolution: {integrity: sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A==} - engines: {node: '>=12.20'} - - color-string@2.1.2: - resolution: {integrity: sha512-RxmjYxbWemV9gKu4zPgiZagUxbH3RQpEIO77XoSSX0ivgABDZ+h8Zuash/EMFLTI4N9QgFPOJ6JQpPZKFxa+dA==} - engines: {node: '>=18'} - - color@5.0.2: - resolution: {integrity: sha512-e2hz5BzbUPcYlIRHo8ieAhYgoajrJr+hWoceg6E345TPsATMUKqDgzt8fSXZJJbxfpiPzkWyphz8yn8At7q3fA==} - engines: {node: '>=18'} - - combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - - create-jest@29.7.0: - resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - - create-require@1.1.1: - resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - - crelt@1.0.6: - resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} - - cross-env@7.0.3: - resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} - engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} - hasBin: true - - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - - css-tree@2.3.1: - resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} - engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} - - css-what@6.2.2: - resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} - engines: {node: '>= 6'} - - cssesc@3.0.0: - resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} - engines: {node: '>=4'} - hasBin: true - - cssom@0.3.8: - resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} - - cssom@0.5.0: - resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} - - cssstyle@2.3.0: - resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} - engines: {node: '>=8'} - - data-urls@3.0.2: - resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} - engines: {node: '>=12'} - - data-view-buffer@1.0.2: - resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} - engines: {node: '>= 0.4'} - - data-view-byte-length@1.0.2: - resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} - engines: {node: '>= 0.4'} - - data-view-byte-offset@1.0.1: - resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} - engines: {node: '>= 0.4'} - - debug@3.2.7: - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - decimal.js@10.6.0: - resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} - - dedent-js@1.0.1: - resolution: {integrity: sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==} - - dedent@1.7.0: - resolution: {integrity: sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==} - peerDependencies: - babel-plugin-macros: ^3.1.0 - peerDependenciesMeta: - babel-plugin-macros: - optional: true - - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - - deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} - - define-data-property@1.1.4: - resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} - engines: {node: '>= 0.4'} - - define-properties@1.2.1: - resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} - engines: {node: '>= 0.4'} - - delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - - detect-indent@6.1.0: - resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} - engines: {node: '>=8'} - - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - - detect-newline@3.1.0: - resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} - engines: {node: '>=8'} - - diff-sequences@29.6.3: - resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - diff@4.0.2: - resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} - engines: {node: '>=0.3.1'} - - dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} - - doctrine@2.1.0: - resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} - engines: {node: '>=0.10.0'} - - doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} - - dom-serializer@2.0.0: - resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} - - domelementtype@2.3.0: - resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - - domexception@4.0.0: - resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} - engines: {node: '>=12'} - deprecated: Use your platform's native DOMException instead - - domhandler@5.0.3: - resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} - engines: {node: '>= 4'} - - domutils@3.2.2: - resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} - - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - - electron-to-chromium@1.5.237: - resolution: {integrity: sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==} - - emittery@0.13.1: - resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} - engines: {node: '>=12'} - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - enabled@2.0.0: - resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} - - entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} - - entities@5.0.0: - resolution: {integrity: sha512-BeJFvFRJddxobhvEdm5GqHzRV/X+ACeuw0/BuuxsCh1EUZcAIz8+kYmBp/LrQuloy6K1f3a0M7+IhmZ7QnkISA==} - engines: {node: '>=0.12'} - - entities@6.0.1: - resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} - engines: {node: '>=0.12'} - - error-ex@1.3.4: - resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} - - es-abstract@1.24.0: - resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} - engines: {node: '>= 0.4'} - - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - - es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} - engines: {node: '>= 0.4'} - - es-set-tostringtag@2.1.0: - resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} - engines: {node: '>= 0.4'} - - es-shim-unscopables@1.1.0: - resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} - engines: {node: '>= 0.4'} - - es-to-primitive@1.3.0: - resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} - engines: {node: '>= 0.4'} - - es6-promise@3.3.1: - resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} - - esbuild-plugin-copy@2.1.1: - resolution: {integrity: sha512-Bk66jpevTcV8KMFzZI1P7MZKZ+uDcrZm2G2egZ2jNIvVnivDpodZI+/KnpL3Jnap0PBdIHU7HwFGB8r+vV5CVw==} - peerDependencies: - esbuild: '>= 0.14.0' - - esbuild-svelte@0.9.3: - resolution: {integrity: sha512-CgEcGY1r/d16+aggec3czoFBEBaYIrFOnMxpsO6fWNaNEqHregPN5DLAPZDqrL7rXDNplW+WMu8s3GMq9FqgJA==} - engines: {node: '>=18'} - peerDependencies: - esbuild: '>=0.17.0' - svelte: '>=4.2.1 <6' - - esbuild@0.25.11: - resolution: {integrity: sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==} - engines: {node: '>=18'} - hasBin: true - - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - - escape-string-regexp@2.0.0: - resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} - engines: {node: '>=8'} - - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - - escodegen@2.1.0: - resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} - engines: {node: '>=6.0'} - hasBin: true - - eslint-compat-utils@0.5.1: - resolution: {integrity: sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==} - engines: {node: '>=12'} - peerDependencies: - eslint: '>=6.0.0' - - eslint-config-standard-with-typescript@40.0.0: - resolution: {integrity: sha512-GXUJcwIXiTQaS3H4etv8a1lejVVdZYaxZNz3g7vt6GoJosQqMTurbmSC4FVGyHiGT/d1TjFr3+47A3xsHhsG+Q==} - deprecated: Please use eslint-config-love, instead. - peerDependencies: - '@typescript-eslint/eslint-plugin': ^6.4.0 - eslint: ^8.0.1 - eslint-plugin-import: ^2.25.2 - eslint-plugin-n: '^15.0.0 || ^16.0.0 ' - eslint-plugin-promise: ^6.0.0 - typescript: '*' - - eslint-config-standard@17.1.0: - resolution: {integrity: sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==} - engines: {node: '>=12.0.0'} - peerDependencies: - eslint: ^8.0.1 - eslint-plugin-import: ^2.25.2 - eslint-plugin-n: '^15.0.0 || ^16.0.0 ' - eslint-plugin-promise: ^6.0.0 - - eslint-import-resolver-node@0.3.9: - resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} - - eslint-module-utils@2.12.1: - resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} - engines: {node: '>=4'} - peerDependencies: - eslint: '*' - peerDependenciesMeta: - eslint: - optional: true - - eslint-plugin-es@3.0.1: - resolution: {integrity: sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==} - engines: {node: '>=8.10.0'} - peerDependencies: - eslint: '>=4.19.1' - - eslint-plugin-es@4.1.0: - resolution: {integrity: sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==} - engines: {node: '>=8.10.0'} - peerDependencies: - eslint: '>=4.19.1' - - eslint-plugin-import@2.32.0: - resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} - engines: {node: '>=4'} - peerDependencies: - eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 - - eslint-plugin-n@15.7.0: - resolution: {integrity: sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==} - engines: {node: '>=12.22.0'} - peerDependencies: - eslint: '>=7.0.0' - - eslint-plugin-node@11.1.0: - resolution: {integrity: sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==} - engines: {node: '>=8.10.0'} - peerDependencies: - eslint: '>=5.16.0' - - eslint-plugin-promise@6.6.0: - resolution: {integrity: sha512-57Zzfw8G6+Gq7axm2Pdo3gW/Rx3h9Yywgn61uE/3elTCOePEHVrn2i5CdfBwA1BLK0Q0WqctICIUSqXZW/VprQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 - - eslint-plugin-svelte@2.46.1: - resolution: {integrity: sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw==} - engines: {node: ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0-0 || ^9.0.0-0 - svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 - peerDependenciesMeta: - svelte: - optional: true - - eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-utils@2.1.0: - resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==} - engines: {node: '>=6'} - - eslint-utils@3.0.0: - resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} - engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} - peerDependencies: - eslint: '>=5' - - eslint-visitor-keys@1.3.0: - resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} - engines: {node: '>=4'} - - eslint-visitor-keys@2.1.0: - resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} - engines: {node: '>=10'} - - eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint@8.57.1: - resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. - hasBin: true - - espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - - esquery@1.6.0: - resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} - engines: {node: '>=0.10'} - - esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - - execa@5.1.1: - resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} - engines: {node: '>=10'} - - exit@0.1.2: - resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} - engines: {node: '>= 0.8.0'} - - expect@29.7.0: - resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - extend@3.0.2: - resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-equals@5.3.2: - resolution: {integrity: sha512-6rxyATwPCkaFIL3JLqw8qXqMpIZ942pTX/tbQFkRsDGblS8tNGtlUauA/+mt6RUfqn/4MoEr+WDkYoIQbibWuQ==} - engines: {node: '>=6.0.0'} - - fast-glob@3.3.3: - resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} - engines: {node: '>=8.6.0'} - - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - - fastq@1.19.1: - resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} - - fb-watchman@2.0.2: - resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} - - fecha@4.2.3: - resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} - - file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} - - file-stream-rotator@0.6.1: - resolution: {integrity: sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==} - - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - - find-up@4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} - engines: {node: '>=8'} - - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - - flat-cache@3.2.0: - resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} - engines: {node: ^10.12.0 || >=12.0.0} - - flatted@3.3.3: - resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - - fn.name@1.1.0: - resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} - - for-each@0.3.5: - resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} - engines: {node: '>= 0.4'} - - form-data@4.0.4: - resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} - engines: {node: '>= 6'} - - forwarded-parse@2.1.2: - resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} - - fs-extra@10.1.0: - resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} - engines: {node: '>=12'} - - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - function.prototype.name@1.1.8: - resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} - engines: {node: '>= 0.4'} - - functions-have-names@1.2.3: - resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - - gaxios@6.7.1: - resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} - engines: {node: '>=14'} - - gcp-metadata@6.1.1: - resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} - engines: {node: '>=14'} - - generator-function@2.0.1: - resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} - engines: {node: '>= 0.4'} - - gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} - - get-package-type@0.1.0: - resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} - engines: {node: '>=8.0.0'} - - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} - - get-stream@6.0.1: - resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} - engines: {node: '>=10'} - - get-symbol-description@1.1.0: - resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} - engines: {node: '>= 0.4'} - - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - - glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported - - globals@13.24.0: - resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} - engines: {node: '>=8'} - - globalthis@1.0.4: - resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} - engines: {node: '>= 0.4'} - - globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} - - google-logging-utils@0.0.2: - resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} - engines: {node: '>=14'} - - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - - graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - - handlebars@4.7.8: - resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} - engines: {node: '>=0.4.7'} - hasBin: true - - has-bigints@1.1.0: - resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} - engines: {node: '>= 0.4'} - - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - - has-property-descriptors@1.0.2: - resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} - - has-proto@1.2.0: - resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} - engines: {node: '>= 0.4'} - - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - - has-tostringtag@1.0.2: - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} - engines: {node: '>= 0.4'} - - hash-it@6.0.0: - resolution: {integrity: sha512-KHzmSFx1KwyMPw0kXeeUD752q/Kfbzhy6dAZrjXV9kAIXGqzGvv8vhkUqj+2MGZldTo0IBpw6v7iWE7uxsvH0w==} - - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - - html-encoding-sniffer@3.0.0: - resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} - engines: {node: '>=12'} - - html-escaper@2.0.2: - resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - - htmlparser2@9.1.0: - resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} - - http-proxy-agent@5.0.0: - resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} - engines: {node: '>= 6'} - - https-proxy-agent@5.0.1: - resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} - engines: {node: '>= 6'} - - https-proxy-agent@7.0.6: - resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} - engines: {node: '>= 14'} - - human-signals@2.1.0: - resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} - engines: {node: '>=10.17.0'} - - iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - - ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} - engines: {node: '>= 4'} - - import-fresh@3.3.1: - resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} - engines: {node: '>=6'} - - import-in-the-middle@1.15.0: - resolution: {integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==} - - import-local@3.2.0: - resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} - engines: {node: '>=8'} - hasBin: true - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - internal-slot@1.1.0: - resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} - engines: {node: '>= 0.4'} - - intl-messageformat@10.7.18: - resolution: {integrity: sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==} - - is-array-buffer@3.0.5: - resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} - engines: {node: '>= 0.4'} - - is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - - is-async-function@2.1.1: - resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} - engines: {node: '>= 0.4'} - - is-bigint@1.1.0: - resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} - engines: {node: '>= 0.4'} - - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - - is-boolean-object@1.2.2: - resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} - engines: {node: '>= 0.4'} - - is-callable@1.2.7: - resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} - engines: {node: '>= 0.4'} - - is-core-module@2.16.1: - resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} - engines: {node: '>= 0.4'} - - is-data-view@1.0.2: - resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} - engines: {node: '>= 0.4'} - - is-date-object@1.1.0: - resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} - engines: {node: '>= 0.4'} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-finalizationregistry@1.1.1: - resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} - engines: {node: '>= 0.4'} - - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - - is-generator-fn@2.1.0: - resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} - engines: {node: '>=6'} - - is-generator-function@1.1.2: - resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} - engines: {node: '>= 0.4'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - is-map@2.0.3: - resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} - engines: {node: '>= 0.4'} - - is-negative-zero@2.0.3: - resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} - engines: {node: '>= 0.4'} - - is-number-object@1.1.1: - resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} - engines: {node: '>= 0.4'} - - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - - is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - - is-potential-custom-element-name@1.0.1: - resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} - - is-reference@3.0.3: - resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} - - is-regex@1.2.1: - resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} - engines: {node: '>= 0.4'} - - is-set@2.0.3: - resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} - engines: {node: '>= 0.4'} - - is-shared-array-buffer@1.0.4: - resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} - engines: {node: '>= 0.4'} - - is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - - is-string@1.1.1: - resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} - engines: {node: '>= 0.4'} - - is-symbol@1.1.1: - resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} - engines: {node: '>= 0.4'} - - is-typed-array@1.1.15: - resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} - engines: {node: '>= 0.4'} - - is-weakmap@2.0.2: - resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} - engines: {node: '>= 0.4'} - - is-weakref@1.1.1: - resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} - engines: {node: '>= 0.4'} - - is-weakset@2.0.4: - resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} - engines: {node: '>= 0.4'} - - isarray@2.0.5: - resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - isomorphic.js@0.2.5: - resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} - - istanbul-lib-coverage@3.2.2: - resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} - engines: {node: '>=8'} - - istanbul-lib-instrument@5.2.1: - resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} - engines: {node: '>=8'} - - istanbul-lib-instrument@6.0.3: - resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} - engines: {node: '>=10'} - - istanbul-lib-report@3.0.1: - resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} - engines: {node: '>=10'} - - istanbul-lib-source-maps@4.0.1: - resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} - engines: {node: '>=10'} - - istanbul-reports@3.2.0: - resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} - engines: {node: '>=8'} - - jest-changed-files@29.7.0: - resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-circus@29.7.0: - resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-cli@29.7.0: - resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - - jest-config@29.7.0: - resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@types/node': '*' - ts-node: '>=9.0.0' - peerDependenciesMeta: - '@types/node': - optional: true - ts-node: - optional: true - - jest-diff@29.7.0: - resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-docblock@29.7.0: - resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-each@29.7.0: - resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-environment-jsdom@29.7.0: - resolution: {integrity: sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - canvas: ^2.5.0 - peerDependenciesMeta: - canvas: - optional: true - - jest-environment-node@29.7.0: - resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-get-type@29.6.3: - resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-haste-map@29.7.0: - resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-leak-detector@29.7.0: - resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-matcher-utils@29.7.0: - resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-message-util@29.7.0: - resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-mock@29.7.0: - resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-pnp-resolver@1.2.3: - resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} - engines: {node: '>=6'} - peerDependencies: - jest-resolve: '*' - peerDependenciesMeta: - jest-resolve: - optional: true - - jest-regex-util@29.6.3: - resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-resolve-dependencies@29.7.0: - resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-resolve@29.7.0: - resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-runner@29.7.0: - resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-runtime@29.7.0: - resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-snapshot@29.7.0: - resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-util@29.7.0: - resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-validate@29.7.0: - resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-watcher@29.7.0: - resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-worker@29.7.0: - resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest@29.7.0: - resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - js-yaml@3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} - hasBin: true - - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - - jsdom@20.0.3: - resolution: {integrity: sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==} - engines: {node: '>=14'} - peerDependencies: - canvas: ^2.5.0 - peerDependenciesMeta: - canvas: - optional: true - - jsesc@3.1.0: - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} - engines: {node: '>=6'} - hasBin: true - - json-bigint@1.0.0: - resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} - - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - - json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - - json5@1.0.2: - resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} - hasBin: true - - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - - jsonfile@6.2.0: - resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} - - jwt-simple@0.5.6: - resolution: {integrity: sha512-40aUybvhH9t2h71ncA1/1SbtTNCVZHgsTsTgqPUxGWDmUDrXyDf2wMNQKEbdBjbf4AI+fQhbECNTV6lWxQKUzg==} - engines: {node: '>= 0.4.0'} - - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - - kleur@3.0.3: - resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} - engines: {node: '>=6'} - - known-css-properties@0.35.0: - resolution: {integrity: sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==} - - kuler@2.0.0: - resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} - - leven@3.1.0: - resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} - engines: {node: '>=6'} - - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - - lexorank@1.0.5: - resolution: {integrity: sha512-K1B/Yr/gIU0wm68hk/yB0p/mv6xM3ShD5aci42vOwcjof8slG8Kpo3Q7+1WTv7DaRHKWRgLPqrFDt+4GtuFAtA==} - - lib0@0.2.114: - resolution: {integrity: sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==} - engines: {node: '>=16'} - hasBin: true - - lilconfig@2.1.0: - resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} - engines: {node: '>=10'} - - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - - linkify-it@5.0.0: - resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} - - linkifyjs@4.3.2: - resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==} - - locate-character@3.0.0: - resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} - - locate-path@5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} - engines: {node: '>=8'} - - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - - lodash.camelcase@4.3.0: - resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} - - lodash.memoize@4.1.2: - resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} - - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - - logform@2.7.0: - resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} - engines: {node: '>= 12.0.0'} - - long@5.3.2: - resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} - - lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - - magic-string@0.30.19: - resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} - - make-dir@4.0.0: - resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} - engines: {node: '>=10'} - - make-error@1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - - makeerror@1.0.12: - resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} - - markdown-it@14.1.0: - resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} - hasBin: true - - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - - mdn-data@2.0.30: - resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} - - mdurl@2.0.0: - resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} - - merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} - - mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - - mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - - mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} - - min-indent@1.0.1: - resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} - engines: {node: '>=4'} - - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - - minimatch@9.0.3: - resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} - engines: {node: '>=16 || 14 >=14.17'} - - minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - - mkdirp@0.5.6: - resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} - hasBin: true - - module-details-from-path@1.0.4: - resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} - - moment@2.30.1: - resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - msgpackr-extract@3.0.3: - resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} - hasBin: true - - msgpackr@1.11.5: - resolution: {integrity: sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==} - - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - - neo-async@2.6.2: - resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - - node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - - node-gyp-build-optional-packages@5.2.2: - resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} - hasBin: true - - node-int64@0.4.0: - resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - - node-releases@2.0.23: - resolution: {integrity: sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==} - - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - - npm-run-path@4.0.1: - resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} - engines: {node: '>=8'} - - nwsapi@2.2.22: - resolution: {integrity: sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==} - - object-hash@3.0.0: - resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} - engines: {node: '>= 6'} - - object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} - - object-keys@1.1.1: - resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} - engines: {node: '>= 0.4'} - - object.assign@4.1.7: - resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} - engines: {node: '>= 0.4'} - - object.fromentries@2.0.8: - resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} - engines: {node: '>= 0.4'} - - object.groupby@1.0.3: - resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} - engines: {node: '>= 0.4'} - - object.values@1.2.1: - resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} - engines: {node: '>= 0.4'} - - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - - one-time@1.0.0: - resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} - - onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} - - optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} - - orderedmap@2.1.1: - resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} - - own-keys@1.0.1: - resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} - engines: {node: '>= 0.4'} - - p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} - - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - - p-locate@4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} - engines: {node: '>=8'} - - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - - p-try@2.2.0: - resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} - engines: {node: '>=6'} - - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - - parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} - - parse5@7.3.0: - resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} - - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - - path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - - path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} - - periscopic@3.1.0: - resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} - - pg-int8@1.0.1: - resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} - engines: {node: '>=4.0.0'} - - pg-protocol@1.10.3: - resolution: {integrity: sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==} - - pg-types@2.2.0: - resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} - engines: {node: '>=4'} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - - pirates@4.0.7: - resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} - engines: {node: '>= 6'} - - pkg-dir@4.2.0: - resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} - engines: {node: '>=8'} - - possible-typed-array-names@1.1.0: - resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} - engines: {node: '>= 0.4'} - - postcss-load-config@3.1.4: - resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} - engines: {node: '>= 10'} - peerDependencies: - postcss: '>=8.0.9' - ts-node: '>=9.0.0' - peerDependenciesMeta: - postcss: - optional: true - ts-node: - optional: true - - postcss-safe-parser@6.0.0: - resolution: {integrity: sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==} - engines: {node: '>=12.0'} - peerDependencies: - postcss: ^8.3.3 - - postcss-scss@4.0.9: - resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} - engines: {node: '>=12.0'} - peerDependencies: - postcss: ^8.4.29 - - postcss-selector-parser@6.1.2: - resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} - engines: {node: '>=4'} - - postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} - engines: {node: ^10 || ^12 || >=14} - - postgres-array@2.0.0: - resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} - engines: {node: '>=4'} - - postgres-bytea@1.0.0: - resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} - engines: {node: '>=0.10.0'} - - postgres-date@1.0.7: - resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} - engines: {node: '>=0.10.0'} - - postgres-interval@1.2.0: - resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} - engines: {node: '>=0.10.0'} - - postgres@3.4.7: - resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} - engines: {node: '>=12'} - - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - - prettier-plugin-svelte@3.4.0: - resolution: {integrity: sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==} - peerDependencies: - prettier: ^3.0.0 - svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 - - prettier@3.6.2: - resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} - engines: {node: '>=14'} - hasBin: true - - pretty-format@29.7.0: - resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - prompts@2.4.2: - resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} - engines: {node: '>= 6'} - - prosemirror-changeset@2.3.1: - resolution: {integrity: sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==} - - prosemirror-codemark@0.4.2: - resolution: {integrity: sha512-4n+PnGQToa/vTjn0OiivUvE8/moLtguUAfry8UA4Q8p47MhqT2Qpf2zBLustX5Upi4mSp3z1ZYBqLLovZC6abA==} - peerDependencies: - prosemirror-inputrules: ^1.2.0 - prosemirror-model: ^1.18.1 - prosemirror-state: ^1.4.1 - prosemirror-view: ^1.26.2 - - prosemirror-collab@1.3.1: - resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==} - - prosemirror-commands@1.7.1: - resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==} - - prosemirror-dropcursor@1.8.2: - resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==} - - prosemirror-gapcursor@1.4.0: - resolution: {integrity: sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==} - - prosemirror-history@1.4.1: - resolution: {integrity: sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==} - - prosemirror-inputrules@1.5.1: - resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==} - - prosemirror-keymap@1.2.3: - resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} - - prosemirror-markdown@1.13.2: - resolution: {integrity: sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==} - - prosemirror-menu@1.2.5: - resolution: {integrity: sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==} - - prosemirror-model@1.25.3: - resolution: {integrity: sha512-dY2HdaNXlARknJbrManZ1WyUtos+AP97AmvqdOQtWtrrC5g4mohVX5DTi9rXNFSk09eczLq9GuNTtq3EfMeMGA==} - - prosemirror-schema-basic@1.2.4: - resolution: {integrity: sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==} - - prosemirror-schema-list@1.5.1: - resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} - - prosemirror-state@1.4.3: - resolution: {integrity: sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==} - - prosemirror-tables@1.8.1: - resolution: {integrity: sha512-DAgDoUYHCcc6tOGpLVPSU1k84kCUWTWnfWX3UDy2Delv4ryH0KqTD6RBI6k4yi9j9I8gl3j8MkPpRD/vWPZbug==} - - prosemirror-trailing-node@3.0.0: - resolution: {integrity: sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==} - peerDependencies: - prosemirror-model: ^1.22.1 - prosemirror-state: ^1.4.2 - prosemirror-view: ^1.33.8 - - prosemirror-transform@1.10.4: - resolution: {integrity: sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw==} - - prosemirror-view@1.41.3: - resolution: {integrity: sha512-SqMiYMUQNNBP9kfPhLO8WXEk/fon47vc52FQsUiJzTBuyjKgEcoAwMyF04eQ4WZ2ArMn7+ReypYL60aKngbACQ==} - - protobufjs@7.5.4: - resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} - engines: {node: '>=12.0.0'} - - psl@1.15.0: - resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} - - punycode.js@2.3.1: - resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} - engines: {node: '>=6'} - - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - - pure-rand@6.1.0: - resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} - - querystringify@2.2.0: - resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} - - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - - react-is@18.3.1: - resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - - readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} - - readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - - reflect.getprototypeof@1.0.10: - resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} - engines: {node: '>= 0.4'} - - regexp.prototype.flags@1.5.4: - resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} - engines: {node: '>= 0.4'} - - regexpp@3.2.0: - resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} - engines: {node: '>=8'} - - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - - require-in-the-middle@7.5.2: - resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} - engines: {node: '>=8.6.0'} - - requires-port@1.0.0: - resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} - - resolve-cwd@3.0.0: - resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} - engines: {node: '>=8'} - - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - - resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} - - resolve.exports@2.0.3: - resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} - engines: {node: '>=10'} - - resolve@1.22.10: - resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} - engines: {node: '>= 0.4'} - hasBin: true - - reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - - rimraf@2.7.1: - resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - - rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - - rope-sequence@1.3.4: - resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} - - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - - safe-array-concat@1.1.3: - resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} - engines: {node: '>=0.4'} - - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - - safe-push-apply@1.0.0: - resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} - engines: {node: '>= 0.4'} - - safe-regex-test@1.1.0: - resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} - engines: {node: '>= 0.4'} - - safe-stable-stringify@2.5.0: - resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} - engines: {node: '>=10'} - - safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - - sander@0.5.1: - resolution: {integrity: sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==} - - saxes@6.0.0: - resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} - engines: {node: '>=v12.22.7'} - - scule@1.3.0: - resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} - - semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - - semver@7.7.3: - resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} - engines: {node: '>=10'} - hasBin: true - - set-function-length@1.2.2: - resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} - engines: {node: '>= 0.4'} - - set-function-name@2.0.2: - resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} - engines: {node: '>= 0.4'} - - set-proto@1.0.0: - resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} - engines: {node: '>= 0.4'} - - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - side-channel-list@1.0.0: - resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} - engines: {node: '>= 0.4'} - - side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} - engines: {node: '>= 0.4'} - - side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} - engines: {node: '>= 0.4'} - - side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} - engines: {node: '>= 0.4'} - - signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - - sisteransi@1.0.5: - resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - - slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} - - snappyjs@0.7.0: - resolution: {integrity: sha512-u5iEEXkMe2EInQio6Wv9LWHOQYRDbD2O9hzS27GpT/lwfIQhTCnHCTqedqHIHe9ZcvQo+9au6vngQayipz1NYw==} - - sorcery@0.11.1: - resolution: {integrity: sha512-o7npfeJE6wi6J9l0/5LKshFzZ2rMatRiCDwYeDQaOzqdzRJwALhX7mk/A/ecg6wjMu7wdZbmXfD2S/vpOg0bdQ==} - hasBin: true - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - source-map-support@0.5.13: - resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} - - source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - - sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - - stack-trace@0.0.10: - resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} - - stack-utils@2.0.6: - resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} - engines: {node: '>=10'} - - stop-iteration-iterator@1.1.0: - resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} - engines: {node: '>= 0.4'} - - string-length@4.0.2: - resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} - engines: {node: '>=10'} - - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - string.prototype.trim@1.2.10: - resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} - engines: {node: '>= 0.4'} - - string.prototype.trimend@1.0.9: - resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} - engines: {node: '>= 0.4'} - - string.prototype.trimstart@1.0.8: - resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} - engines: {node: '>= 0.4'} - - string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - strip-bom@3.0.0: - resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} - engines: {node: '>=4'} - - strip-bom@4.0.0: - resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} - engines: {node: '>=8'} - - strip-final-newline@2.0.0: - resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} - engines: {node: '>=6'} - - strip-indent@3.0.0: - resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} - engines: {node: '>=8'} - - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} - - supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - - svelte-eslint-parser@0.33.1: - resolution: {integrity: sha512-vo7xPGTlKBGdLH8T5L64FipvTrqv3OQRx9d2z5X05KKZDlF4rQk8KViZO4flKERY+5BiVdOh7zZ7JGJWo5P0uA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - svelte: ^3.37.0 || ^4.0.0 - peerDependenciesMeta: - svelte: - optional: true - - svelte-eslint-parser@0.43.0: - resolution: {integrity: sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 - peerDependenciesMeta: - svelte: - optional: true - - svelte-preprocess@5.1.4: - resolution: {integrity: sha512-IvnbQ6D6Ao3Gg6ftiM5tdbR6aAETwjhHV+UKGf5bHGYR69RQvF1ho0JKPcbUON4vy4R7zom13jPjgdOWCQ5hDA==} - engines: {node: '>= 16.0.0'} - peerDependencies: - '@babel/core': ^7.10.2 - coffeescript: ^2.5.1 - less: ^3.11.3 || ^4.0.0 - postcss: ^7 || ^8 - postcss-load-config: ^2.1.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 - pug: ^3.0.0 - sass: ^1.26.8 - stylus: ^0.55.0 - sugarss: ^2.0.0 || ^3.0.0 || ^4.0.0 - svelte: ^3.23.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0 - typescript: '>=3.9.5 || ^4.0.0 || ^5.0.0' - peerDependenciesMeta: - '@babel/core': - optional: true - coffeescript: - optional: true - less: - optional: true - postcss: - optional: true - postcss-load-config: - optional: true - pug: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - typescript: - optional: true - - svelte2tsx@0.7.45: - resolution: {integrity: sha512-cSci+mYGygYBHIZLHlm/jYlEc1acjAHqaQaDFHdEBpUueM9kSTnPpvPtSl5VkJOU1qSJ7h1K+6F/LIUYiqC8VA==} - peerDependencies: - svelte: ^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0 - typescript: ^4.9.4 || ^5.0.0 - - svelte@4.2.20: - resolution: {integrity: sha512-eeEgGc2DtiUil5ANdtd8vPwt9AgaMdnuUFnPft9F5oMvU/FHu5IHFic+p1dR/UOB7XU2mX2yHW+NcTch4DCh5Q==} - engines: {node: '>=16'} - - symbol-tree@3.2.4: - resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - - test-exclude@6.0.0: - resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} - engines: {node: '>=8'} - - text-hex@1.0.0: - resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} - - text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - - tmpl@1.0.5: - resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} - - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - - toposort@2.0.2: - resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} - - tough-cookie@4.1.4: - resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} - engines: {node: '>=6'} - - tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - - tr46@3.0.0: - resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} - engines: {node: '>=12'} - - triple-beam@1.4.1: - resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} - engines: {node: '>= 14.0.0'} - - ts-api-utils@1.4.3: - resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} - engines: {node: '>=16'} - peerDependencies: - typescript: '>=4.2.0' - - ts-jest@29.4.5: - resolution: {integrity: sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q==} - engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@babel/core': '>=7.0.0-beta.0 <8' - '@jest/transform': ^29.0.0 || ^30.0.0 - '@jest/types': ^29.0.0 || ^30.0.0 - babel-jest: ^29.0.0 || ^30.0.0 - esbuild: '*' - jest: ^29.0.0 || ^30.0.0 - jest-util: ^29.0.0 || ^30.0.0 - typescript: '>=4.3 <6' - peerDependenciesMeta: - '@babel/core': - optional: true - '@jest/transform': - optional: true - '@jest/types': - optional: true - babel-jest: - optional: true - esbuild: - optional: true - jest-util: - optional: true - - ts-node@10.9.2: - resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} - hasBin: true - peerDependencies: - '@swc/core': '>=1.2.50' - '@swc/wasm': '>=1.2.50' - '@types/node': '*' - typescript: '>=2.7' - peerDependenciesMeta: - '@swc/core': - optional: true - '@swc/wasm': - optional: true - - tsconfig-paths@3.15.0: - resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} - - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - - type-detect@4.0.8: - resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} - engines: {node: '>=4'} - - type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - - type-fest@0.21.3: - resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} - engines: {node: '>=10'} - - type-fest@4.41.0: - resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} - engines: {node: '>=16'} - - typed-array-buffer@1.0.3: - resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} - engines: {node: '>= 0.4'} - - typed-array-byte-length@1.0.3: - resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} - engines: {node: '>= 0.4'} - - typed-array-byte-offset@1.0.4: - resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} - engines: {node: '>= 0.4'} - - typed-array-length@1.0.7: - resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} - engines: {node: '>= 0.4'} - - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - - uc.micro@2.1.0: - resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} - - uglify-js@3.19.3: - resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} - engines: {node: '>=0.8.0'} - hasBin: true - - unbox-primitive@1.1.0: - resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} - engines: {node: '>= 0.4'} - - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - - universalify@0.2.0: - resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} - engines: {node: '>= 4.0.0'} - - universalify@2.0.1: - resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} - engines: {node: '>= 10.0.0'} - - update-browserslist-db@1.1.3: - resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - - url-parse@1.5.10: - resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} - - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - - uuid@8.3.2: - resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} - hasBin: true - - uuid@9.0.1: - resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} - hasBin: true - - v8-compile-cache-lib@3.0.1: - resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} - - v8-to-istanbul@9.3.0: - resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} - engines: {node: '>=10.12.0'} - - w3c-keyname@2.2.8: - resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} - - w3c-xmlserializer@4.0.0: - resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} - engines: {node: '>=14'} - - walker@1.0.8: - resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} - - webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - - webidl-conversions@7.0.0: - resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} - engines: {node: '>=12'} - - whatwg-encoding@2.0.0: - resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} - engines: {node: '>=12'} - - whatwg-mimetype@3.0.0: - resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} - engines: {node: '>=12'} - - whatwg-url@11.0.0: - resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} - engines: {node: '>=12'} - - whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - - which-boxed-primitive@1.1.1: - resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} - engines: {node: '>= 0.4'} - - which-builtin-type@1.2.1: - resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} - engines: {node: '>= 0.4'} - - which-collection@1.0.2: - resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} - engines: {node: '>= 0.4'} - - which-typed-array@1.1.19: - resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} - engines: {node: '>= 0.4'} - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - winston-daily-rotate-file@5.0.0: - resolution: {integrity: sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==} - engines: {node: '>=8'} - peerDependencies: - winston: ^3 - - winston-transport@4.9.0: - resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} - engines: {node: '>= 12.0.0'} - - winston@3.18.3: - resolution: {integrity: sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==} - engines: {node: '>= 12.0.0'} - - word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} - - wordwrap@1.0.0: - resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} - - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - - write-file-atomic@4.0.2: - resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - - ws@8.18.3: - resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - xml-name-validator@4.0.0: - resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} - engines: {node: '>=12'} - - xmlchars@2.2.0: - resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} - - xtend@4.0.2: - resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} - engines: {node: '>=0.4'} - - y-prosemirror@1.3.7: - resolution: {integrity: sha512-NpM99WSdD4Fx4if5xOMDpPtU3oAmTSjlzh5U4353ABbRHl1HtAFUx6HlebLZfyFxXN9jzKMDkVbcRjqOZVkYQg==} - engines: {node: '>=16.0.0', npm: '>=8.0.0'} - peerDependencies: - prosemirror-model: ^1.7.1 - prosemirror-state: ^1.2.3 - prosemirror-view: ^1.9.10 - y-protocols: ^1.0.1 - yjs: ^13.5.38 - - y-protocols@1.0.6: - resolution: {integrity: sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==} - engines: {node: '>=16.0.0', npm: '>=8.0.0'} - peerDependencies: - yjs: ^13.0.0 - - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - - yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - - yaml@1.10.2: - resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} - engines: {node: '>= 6'} - - yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} - - yjs@13.6.27: - resolution: {integrity: sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==} - engines: {node: '>=16.0.0', npm: '>=8.0.0'} - - yn@3.1.1: - resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} - engines: {node: '>=6'} - - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - - zeed-dom@0.15.1: - resolution: {integrity: sha512-dtZ0aQSFyZmoJS0m06/xBN1SazUBPL5HpzlAcs/KcRW0rzadYw12deQBjeMhGKMMeGEp7bA9vmikMLaO4exBcg==} - engines: {node: '>=14.13.1'} - -snapshots: - - '@ampproject/remapping@2.3.0': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - - '@babel/code-frame@7.27.1': - dependencies: - '@babel/helper-validator-identifier': 7.27.1 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/compat-data@7.28.4': {} - - '@babel/core@7.28.4': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.3 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) - '@babel/helpers': 7.28.4 - '@babel/parser': 7.28.4 - '@babel/template': 7.27.2 - '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 - '@jridgewell/remapping': 2.3.5 - convert-source-map: 2.0.0 - debug: 4.4.3 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/generator@7.28.3': - dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 - - '@babel/helper-compilation-targets@7.27.2': - dependencies: - '@babel/compat-data': 7.28.4 - '@babel/helper-validator-option': 7.27.1 - browserslist: 4.26.3 - lru-cache: 5.1.1 - semver: 6.3.1 - - '@babel/helper-globals@7.28.0': {} - - '@babel/helper-module-imports@7.27.1': - dependencies: - '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 - transitivePeerDependencies: - - supports-color - - '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.28.4 - transitivePeerDependencies: - - supports-color - - '@babel/helper-plugin-utils@7.27.1': {} - - '@babel/helper-string-parser@7.27.1': {} - - '@babel/helper-validator-identifier@7.27.1': {} - - '@babel/helper-validator-option@7.27.1': {} - - '@babel/helpers@7.28.4': - dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.28.4 - - '@babel/parser@7.28.4': - dependencies: - '@babel/types': 7.28.4 - - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/template@7.27.2': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 - - '@babel/traverse@7.28.4': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.3 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.4 - '@babel/template': 7.27.2 - '@babel/types': 7.28.4 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - '@babel/types@7.28.4': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - - '@bcoe/v8-coverage@0.2.3': {} - - '@colors/colors@1.6.0': {} - - '@cspotcode/source-map-support@0.8.1': - dependencies: - '@jridgewell/trace-mapping': 0.3.9 - - '@dabh/diagnostics@2.0.8': - dependencies: - '@so-ric/colorspace': 1.1.6 - enabled: 2.0.0 - kuler: 2.0.0 - - '@esbuild/aix-ppc64@0.25.11': - optional: true - - '@esbuild/android-arm64@0.25.11': - optional: true - - '@esbuild/android-arm@0.25.11': - optional: true - - '@esbuild/android-x64@0.25.11': - optional: true - - '@esbuild/darwin-arm64@0.25.11': - optional: true - - '@esbuild/darwin-x64@0.25.11': - optional: true - - '@esbuild/freebsd-arm64@0.25.11': - optional: true - - '@esbuild/freebsd-x64@0.25.11': - optional: true - - '@esbuild/linux-arm64@0.25.11': - optional: true - - '@esbuild/linux-arm@0.25.11': - optional: true - - '@esbuild/linux-ia32@0.25.11': - optional: true - - '@esbuild/linux-loong64@0.25.11': - optional: true - - '@esbuild/linux-mips64el@0.25.11': - optional: true - - '@esbuild/linux-ppc64@0.25.11': - optional: true - - '@esbuild/linux-riscv64@0.25.11': - optional: true - - '@esbuild/linux-s390x@0.25.11': - optional: true - - '@esbuild/linux-x64@0.25.11': - optional: true - - '@esbuild/netbsd-arm64@0.25.11': - optional: true - - '@esbuild/netbsd-x64@0.25.11': - optional: true - - '@esbuild/openbsd-arm64@0.25.11': - optional: true - - '@esbuild/openbsd-x64@0.25.11': - optional: true - - '@esbuild/openharmony-arm64@0.25.11': - optional: true - - '@esbuild/sunos-x64@0.25.11': - optional: true - - '@esbuild/win32-arm64@0.25.11': - optional: true - - '@esbuild/win32-ia32@0.25.11': - optional: true - - '@esbuild/win32-x64@0.25.11': - optional: true - - '@eslint-community/eslint-utils@4.9.0(eslint@8.57.1)': - dependencies: - eslint: 8.57.1 - eslint-visitor-keys: 3.4.3 - - '@eslint-community/regexpp@4.12.1': {} - - '@eslint/eslintrc@2.1.4': - dependencies: - ajv: 6.12.6 - debug: 4.4.3 - espree: 9.6.1 - globals: 13.24.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - - '@eslint/js@8.57.1': {} - - '@formatjs/ecma402-abstract@2.3.6': - dependencies: - '@formatjs/fast-memoize': 2.2.7 - '@formatjs/intl-localematcher': 0.6.2 - decimal.js: 10.6.0 - tslib: 2.8.1 - - '@formatjs/fast-memoize@2.2.7': - dependencies: - tslib: 2.8.1 - - '@formatjs/icu-messageformat-parser@2.11.4': - dependencies: - '@formatjs/ecma402-abstract': 2.3.6 - '@formatjs/icu-skeleton-parser': 1.8.16 - tslib: 2.8.1 - - '@formatjs/icu-skeleton-parser@1.8.16': - dependencies: - '@formatjs/ecma402-abstract': 2.3.6 - tslib: 2.8.1 - - '@formatjs/intl-localematcher@0.6.2': - dependencies: - tslib: 2.8.1 - - '@grpc/grpc-js@1.14.0': - dependencies: - '@grpc/proto-loader': 0.8.0 - '@js-sdsl/ordered-map': 4.4.2 - - '@grpc/proto-loader@0.8.0': - dependencies: - lodash.camelcase: 4.3.0 - long: 5.3.2 - protobufjs: 7.5.4 - yargs: 17.7.2 - - '@hcengineering/platform-rig@0.7.19(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3))': - dependencies: - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) - esbuild: 0.25.11 - esbuild-plugin-copy: 2.1.1(esbuild@0.25.11) - esbuild-svelte: 0.9.3(esbuild@0.25.11)(svelte@4.2.20) - eslint: 8.57.1 - eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: 2.32.0(eslint@8.57.1) - eslint-plugin-n: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: 6.6.0(eslint@8.57.1) - eslint-plugin-svelte: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - prettier: 3.6.2 - prettier-plugin-svelte: 3.4.0(prettier@3.6.2)(svelte@4.2.20) - svelte: 4.2.20 - svelte-eslint-parser: 0.33.1(svelte@4.2.20) - svelte-preprocess: 5.1.4(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(svelte@4.2.20)(typescript@5.9.3) - svelte2tsx: 0.7.45(svelte@4.2.20)(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - '@babel/core' - - coffeescript - - less - - postcss - - postcss-load-config - - pug - - sass - - stylus - - sugarss - - supports-color - - ts-node - - '@humanwhocodes/config-array@0.13.0': - dependencies: - '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.3 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - - '@humanwhocodes/module-importer@1.0.1': {} - - '@humanwhocodes/object-schema@2.0.3': {} - - '@istanbuljs/load-nyc-config@1.1.0': - dependencies: - camelcase: 5.3.1 - find-up: 4.1.0 - get-package-type: 0.1.0 - js-yaml: 3.14.1 - resolve-from: 5.0.0 - - '@istanbuljs/schema@0.1.3': {} - - '@jest/console@29.7.0': - dependencies: - '@jest/types': 29.6.3 - '@types/node': 22.18.10 - chalk: 4.1.2 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - slash: 3.0.0 - - '@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3))': - dependencies: - '@jest/console': 29.7.0 - '@jest/reporters': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.18.10 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 3.9.0 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - jest-haste-map: 29.7.0 - jest-message-util: 29.7.0 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-resolve-dependencies: 29.7.0 - jest-runner: 29.7.0 - jest-runtime: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - jest-watcher: 29.7.0 - micromatch: 4.0.8 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-ansi: 6.0.1 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - ts-node - - '@jest/environment@29.7.0': - dependencies: - '@jest/fake-timers': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.18.10 - jest-mock: 29.7.0 - - '@jest/expect-utils@29.7.0': - dependencies: - jest-get-type: 29.6.3 - - '@jest/expect@29.7.0': - dependencies: - expect: 29.7.0 - jest-snapshot: 29.7.0 - transitivePeerDependencies: - - supports-color - - '@jest/fake-timers@29.7.0': - dependencies: - '@jest/types': 29.6.3 - '@sinonjs/fake-timers': 10.3.0 - '@types/node': 22.18.10 - jest-message-util: 29.7.0 - jest-mock: 29.7.0 - jest-util: 29.7.0 - - '@jest/globals@29.7.0': - dependencies: - '@jest/environment': 29.7.0 - '@jest/expect': 29.7.0 - '@jest/types': 29.6.3 - jest-mock: 29.7.0 - transitivePeerDependencies: - - supports-color - - '@jest/reporters@29.7.0': - dependencies: - '@bcoe/v8-coverage': 0.2.3 - '@jest/console': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.31 - '@types/node': 22.18.10 - chalk: 4.1.2 - collect-v8-coverage: 1.0.2 - exit: 0.1.2 - glob: 7.2.3 - graceful-fs: 4.2.11 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-instrument: 6.0.3 - istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 4.0.1 - istanbul-reports: 3.2.0 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - jest-worker: 29.7.0 - slash: 3.0.0 - string-length: 4.0.2 - strip-ansi: 6.0.1 - v8-to-istanbul: 9.3.0 - transitivePeerDependencies: - - supports-color - - '@jest/schemas@29.6.3': - dependencies: - '@sinclair/typebox': 0.27.8 - - '@jest/source-map@29.6.3': - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - callsites: 3.1.0 - graceful-fs: 4.2.11 - - '@jest/test-result@29.7.0': - dependencies: - '@jest/console': 29.7.0 - '@jest/types': 29.6.3 - '@types/istanbul-lib-coverage': 2.0.6 - collect-v8-coverage: 1.0.2 - - '@jest/test-sequencer@29.7.0': - dependencies: - '@jest/test-result': 29.7.0 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - slash: 3.0.0 - - '@jest/transform@29.7.0': - dependencies: - '@babel/core': 7.28.4 - '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.31 - babel-plugin-istanbul: 6.1.1 - chalk: 4.1.2 - convert-source-map: 2.0.0 - fast-json-stable-stringify: 2.1.0 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - jest-regex-util: 29.6.3 - jest-util: 29.7.0 - micromatch: 4.0.8 - pirates: 4.0.7 - slash: 3.0.0 - write-file-atomic: 4.0.2 - transitivePeerDependencies: - - supports-color - - '@jest/types@29.6.3': - dependencies: - '@jest/schemas': 29.6.3 - '@types/istanbul-lib-coverage': 2.0.6 - '@types/istanbul-reports': 3.0.4 - '@types/node': 22.18.10 - '@types/yargs': 17.0.33 - chalk: 4.1.2 - - '@jridgewell/gen-mapping@0.3.13': - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/remapping@2.3.5': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/sourcemap-codec@1.5.5': {} - - '@jridgewell/trace-mapping@0.3.31': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - - '@jridgewell/trace-mapping@0.3.9': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - - '@js-sdsl/ordered-map@4.4.2': {} - - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': - optional: true - - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.19.1 - - '@opentelemetry/api-logs@0.203.0': - dependencies: - '@opentelemetry/api': 1.9.0 - - '@opentelemetry/api@1.9.0': {} - - '@opentelemetry/auto-instrumentations-node@0.62.2(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.1.0(@opentelemetry/api@1.9.0))': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-amqplib': 0.50.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-aws-lambda': 0.54.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-aws-sdk': 0.58.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-bunyan': 0.49.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-cassandra-driver': 0.49.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-connect': 0.47.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-cucumber': 0.19.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-dataloader': 0.21.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-dns': 0.47.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-express': 0.52.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-fastify': 0.48.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-fs': 0.23.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-generic-pool': 0.47.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-graphql': 0.51.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-grpc': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-hapi': 0.50.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-http': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-ioredis': 0.51.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-kafkajs': 0.13.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-knex': 0.48.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-koa': 0.51.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-lru-memoizer': 0.48.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-memcached': 0.47.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mongodb': 0.56.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mongoose': 0.50.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mysql': 0.49.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mysql2': 0.50.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-nestjs-core': 0.49.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-net': 0.47.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-oracledb': 0.29.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-pg': 0.56.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-pino': 0.50.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-redis': 0.52.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-restify': 0.49.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-router': 0.48.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-runtime-node': 0.17.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-socket.io': 0.50.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-tedious': 0.22.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-undici': 0.14.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-winston': 0.48.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resource-detector-alibaba-cloud': 0.31.9(@opentelemetry/api@1.9.0) - '@opentelemetry/resource-detector-aws': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resource-detector-azure': 0.10.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resource-detector-container': 0.7.9(@opentelemetry/api@1.9.0) - '@opentelemetry/resource-detector-gcp': 0.37.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-node': 0.203.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - encoding - - supports-color - - '@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - - '@opentelemetry/context-async-hooks@2.1.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - - '@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/semantic-conventions': 1.37.0 - - '@opentelemetry/core@2.1.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/semantic-conventions': 1.37.0 - - '@opentelemetry/exporter-logs-otlp-grpc@0.203.0(@opentelemetry/api@1.9.0)': - dependencies: - '@grpc/grpc-js': 1.14.0 - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.0) - - '@opentelemetry/exporter-logs-otlp-http@0.203.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.203.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.0) - - '@opentelemetry/exporter-logs-otlp-proto@0.203.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.203.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) - - '@opentelemetry/exporter-metrics-otlp-grpc@0.203.0(@opentelemetry/api@1.9.0)': - dependencies: - '@grpc/grpc-js': 1.14.0 - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) - - '@opentelemetry/exporter-metrics-otlp-http@0.203.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) - - '@opentelemetry/exporter-metrics-otlp-proto@0.203.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) - - '@opentelemetry/exporter-prometheus@0.203.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) - - '@opentelemetry/exporter-trace-otlp-grpc@0.203.0(@opentelemetry/api@1.9.0)': - dependencies: - '@grpc/grpc-js': 1.14.0 - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) - - '@opentelemetry/exporter-trace-otlp-http@0.203.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) - - '@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) - - '@opentelemetry/exporter-zipkin@2.0.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - - '@opentelemetry/id-generator-aws-xray@2.0.3(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/sdk-trace-base': 2.1.0(@opentelemetry/api@1.9.0) - - '@opentelemetry/instrumentation-amqplib@0.50.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-aws-lambda@0.54.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - '@types/aws-lambda': 8.10.152 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-aws-sdk@0.58.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-bunyan@0.49.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.203.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@types/bunyan': 1.8.11 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-cassandra-driver@0.49.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-connect@0.47.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - '@types/connect': 3.4.38 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-cucumber@0.19.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-dataloader@0.21.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-dns@0.47.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-express@0.52.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-fastify@0.48.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-fs@0.23.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-generic-pool@0.47.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-graphql@0.51.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-grpc@0.203.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-hapi@0.50.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-http@0.203.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - forwarded-parse: 2.1.2 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-ioredis@0.51.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/redis-common': 0.38.2 - '@opentelemetry/semantic-conventions': 1.37.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-kafkajs@0.13.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-knex@0.48.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-koa@0.51.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-lru-memoizer@0.48.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-memcached@0.47.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - '@types/memcached': 2.2.10 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-mongodb@0.56.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-mongoose@0.50.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-mysql2@0.50.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-mysql@0.49.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - '@types/mysql': 2.15.27 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-nestjs-core@0.49.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-net@0.47.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-oracledb@0.29.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - '@types/oracledb': 6.5.2 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-pg@0.56.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.0) - '@types/pg': 8.15.5 - '@types/pg-pool': 2.0.6 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-pino@0.50.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.203.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-redis@0.52.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/redis-common': 0.38.2 - '@opentelemetry/semantic-conventions': 1.37.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-restify@0.49.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-router@0.48.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-runtime-node@0.17.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-socket.io@0.50.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-tedious@0.22.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - '@types/tedious': 4.0.14 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-undici@0.14.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-winston@0.48.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.203.0 - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation@0.203.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.203.0 - import-in-the-middle: 1.15.0 - require-in-the-middle: 7.5.2 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/otlp-exporter-base@0.203.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) - - '@opentelemetry/otlp-grpc-exporter-base@0.203.0(@opentelemetry/api@1.9.0)': - dependencies: - '@grpc/grpc-js': 1.14.0 - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) - - '@opentelemetry/otlp-transformer@0.203.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.203.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) - protobufjs: 7.5.4 - - '@opentelemetry/propagator-b3@2.0.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - - '@opentelemetry/propagator-jaeger@2.0.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - - '@opentelemetry/redis-common@0.38.2': {} - - '@opentelemetry/resource-detector-alibaba-cloud@0.31.9(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) - - '@opentelemetry/resource-detector-aws@2.6.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - - '@opentelemetry/resource-detector-azure@0.10.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - - '@opentelemetry/resource-detector-container@0.7.9(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) - - '@opentelemetry/resource-detector-gcp@0.37.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - gcp-metadata: 6.1.1 - transitivePeerDependencies: - - encoding - - supports-color - - '@opentelemetry/resources@2.0.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - - '@opentelemetry/resources@2.1.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - - '@opentelemetry/sdk-logs@0.203.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.203.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - - '@opentelemetry/sdk-metrics@2.0.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - - '@opentelemetry/sdk-metrics@2.1.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) - - '@opentelemetry/sdk-node@0.203.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.203.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-grpc': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-proto': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-grpc': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-proto': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-prometheus': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-grpc': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-proto': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-zipkin': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-b3': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-jaeger': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-node': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - - '@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 - - '@opentelemetry/sdk-trace-node@2.0.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/context-async-hooks': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) - - '@opentelemetry/sdk-trace-node@2.1.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/context-async-hooks': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.1.0(@opentelemetry/api@1.9.0) - - '@opentelemetry/semantic-conventions@1.37.0': {} - - '@opentelemetry/sql-common@0.41.2(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - - '@protobufjs/aspromise@1.1.2': {} - - '@protobufjs/base64@1.1.2': {} - - '@protobufjs/codegen@2.0.4': {} - - '@protobufjs/eventemitter@1.1.0': {} - - '@protobufjs/fetch@1.1.0': - dependencies: - '@protobufjs/aspromise': 1.1.2 - '@protobufjs/inquire': 1.1.0 - - '@protobufjs/float@1.0.2': {} - - '@protobufjs/inquire@1.1.0': {} - - '@protobufjs/path@1.1.2': {} - - '@protobufjs/pool@1.1.0': {} - - '@protobufjs/utf8@1.1.0': {} - - '@remirror/core-constants@3.0.0': {} - - '@rtsao/scc@1.1.0': {} - - '@sinclair/typebox@0.27.8': {} - - '@sinonjs/commons@3.0.1': - dependencies: - type-detect: 4.0.8 - - '@sinonjs/fake-timers@10.3.0': - dependencies: - '@sinonjs/commons': 3.0.1 - - '@so-ric/colorspace@1.1.6': - dependencies: - color: 5.0.2 - text-hex: 1.0.0 - - '@tiptap/core@2.26.3(@tiptap/pm@2.26.3)': - dependencies: - '@tiptap/pm': 2.26.3 - - '@tiptap/extension-blockquote@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - - '@tiptap/extension-bold@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - - '@tiptap/extension-bullet-list@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - - '@tiptap/extension-code-block@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - '@tiptap/pm': 2.26.3 - - '@tiptap/extension-code@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - - '@tiptap/extension-document@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - - '@tiptap/extension-dropcursor@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - '@tiptap/pm': 2.26.3 - - '@tiptap/extension-gapcursor@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - '@tiptap/pm': 2.26.3 - - '@tiptap/extension-hard-break@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - - '@tiptap/extension-heading@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - - '@tiptap/extension-highlight@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - - '@tiptap/extension-history@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - '@tiptap/pm': 2.26.3 - - '@tiptap/extension-horizontal-rule@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - '@tiptap/pm': 2.26.3 - - '@tiptap/extension-italic@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - - '@tiptap/extension-link@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - '@tiptap/pm': 2.26.3 - linkifyjs: 4.3.2 - - '@tiptap/extension-list-item@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - - '@tiptap/extension-mention@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)(@tiptap/suggestion@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3))': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - '@tiptap/pm': 2.26.3 - '@tiptap/suggestion': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) - - '@tiptap/extension-ordered-list@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - - '@tiptap/extension-paragraph@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - - '@tiptap/extension-strike@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - - '@tiptap/extension-table-cell@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - - '@tiptap/extension-table-header@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - - '@tiptap/extension-table-row@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - - '@tiptap/extension-table@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - '@tiptap/pm': 2.26.3 - - '@tiptap/extension-task-item@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - '@tiptap/pm': 2.26.3 - - '@tiptap/extension-task-list@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - - '@tiptap/extension-text-align@2.11.9(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - - '@tiptap/extension-text-style@2.11.9(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - - '@tiptap/extension-text-style@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - - '@tiptap/extension-text@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - - '@tiptap/extension-typography@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - - '@tiptap/extension-underline@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - - '@tiptap/html@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - '@tiptap/pm': 2.26.3 - zeed-dom: 0.15.1 - - '@tiptap/pm@2.26.3': - dependencies: - prosemirror-changeset: 2.3.1 - prosemirror-collab: 1.3.1 - prosemirror-commands: 1.7.1 - prosemirror-dropcursor: 1.8.2 - prosemirror-gapcursor: 1.4.0 - prosemirror-history: 1.4.1 - prosemirror-inputrules: 1.5.1 - prosemirror-keymap: 1.2.3 - prosemirror-markdown: 1.13.2 - prosemirror-menu: 1.2.5 - prosemirror-model: 1.25.3 - prosemirror-schema-basic: 1.2.4 - prosemirror-schema-list: 1.5.1 - prosemirror-state: 1.4.3 - prosemirror-tables: 1.8.1 - prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3) - prosemirror-transform: 1.10.4 - prosemirror-view: 1.41.3 - - '@tiptap/starter-kit@2.26.3': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - '@tiptap/extension-blockquote': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-bold': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-bullet-list': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-code': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-code-block': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) - '@tiptap/extension-document': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-dropcursor': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) - '@tiptap/extension-gapcursor': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) - '@tiptap/extension-hard-break': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-heading': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-history': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) - '@tiptap/extension-horizontal-rule': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) - '@tiptap/extension-italic': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-list-item': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-ordered-list': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-paragraph': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-strike': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-text': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/extension-text-style': 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) - '@tiptap/pm': 2.26.3 - - '@tiptap/suggestion@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)': - dependencies: - '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) - '@tiptap/pm': 2.26.3 - - '@tootallnate/once@2.0.0': {} - - '@tsconfig/node10@1.0.11': {} - - '@tsconfig/node12@1.0.11': {} - - '@tsconfig/node14@1.0.3': {} - - '@tsconfig/node16@1.0.4': {} - - '@types/aws-lambda@8.10.152': {} - - '@types/babel__core@7.20.5': - dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 - '@types/babel__generator': 7.27.0 - '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.28.0 - - '@types/babel__generator@7.27.0': - dependencies: - '@babel/types': 7.28.4 - - '@types/babel__template@7.4.4': - dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 - - '@types/babel__traverse@7.28.0': - dependencies: - '@babel/types': 7.28.4 - - '@types/bunyan@1.8.11': - dependencies: - '@types/node': 22.18.10 - - '@types/connect@3.4.38': - dependencies: - '@types/node': 22.18.10 - - '@types/estree@1.0.8': {} - - '@types/graceful-fs@4.1.9': - dependencies: - '@types/node': 22.18.10 - - '@types/istanbul-lib-coverage@2.0.6': {} - - '@types/istanbul-lib-report@3.0.3': - dependencies: - '@types/istanbul-lib-coverage': 2.0.6 - - '@types/istanbul-reports@3.0.4': - dependencies: - '@types/istanbul-lib-report': 3.0.3 - - '@types/jest@29.5.14': - dependencies: - expect: 29.7.0 - pretty-format: 29.7.0 - - '@types/jsdom@20.0.1': - dependencies: - '@types/node': 22.18.10 - '@types/tough-cookie': 4.0.5 - parse5: 7.3.0 - - '@types/json-schema@7.0.15': {} - - '@types/json5@0.0.29': {} - - '@types/linkify-it@3.0.5': {} - - '@types/linkify-it@5.0.0': {} - - '@types/markdown-it@13.0.9': - dependencies: - '@types/linkify-it': 3.0.5 - '@types/mdurl': 1.0.5 - - '@types/markdown-it@14.1.2': - dependencies: - '@types/linkify-it': 5.0.0 - '@types/mdurl': 2.0.0 - - '@types/mdurl@1.0.5': {} - - '@types/mdurl@2.0.0': {} - - '@types/memcached@2.2.10': - dependencies: - '@types/node': 22.18.10 - - '@types/mysql@2.15.27': - dependencies: - '@types/node': 22.18.10 - - '@types/node@22.18.10': - dependencies: - undici-types: 6.21.0 - - '@types/oracledb@6.5.2': - dependencies: - '@types/node': 22.18.10 - - '@types/pg-pool@2.0.6': - dependencies: - '@types/pg': 8.15.5 - - '@types/pg@8.15.5': - dependencies: - '@types/node': 22.18.10 - pg-protocol: 1.10.3 - pg-types: 2.2.0 - - '@types/pug@2.0.10': {} - - '@types/semver@7.7.1': {} - - '@types/snappyjs@0.7.1': {} - - '@types/stack-utils@2.0.3': {} - - '@types/tedious@4.0.14': - dependencies: - '@types/node': 22.18.10 - - '@types/toposort@2.0.7': {} - - '@types/tough-cookie@4.0.5': {} - - '@types/triple-beam@1.3.5': {} - - '@types/uuid@8.3.4': {} - - '@types/ws@8.18.1': - dependencies: - '@types/node': 22.18.10 - - '@types/yargs-parser@21.0.3': {} - - '@types/yargs@17.0.33': - dependencies: - '@types/yargs-parser': 21.0.3 - - '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': - dependencies: - '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.3 - eslint: 8.57.1 - graphemer: 1.4.0 - ignore: 5.3.2 - natural-compare: 1.4.0 - semver: 7.7.3 - ts-api-utils: 1.4.3(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3)': - dependencies: - '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.3 - eslint: 8.57.1 - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/scope-manager@6.21.0': - dependencies: - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/visitor-keys': 6.21.0 - - '@typescript-eslint/type-utils@6.21.0(eslint@8.57.1)(typescript@5.9.3)': - dependencies: - '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.9.3) - '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.9.3) - debug: 4.4.3 - eslint: 8.57.1 - ts-api-utils: 1.4.3(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/types@6.21.0': {} - - '@typescript-eslint/typescript-estree@6.21.0(typescript@5.9.3)': - dependencies: - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.3 - globby: 11.1.0 - is-glob: 4.0.3 - minimatch: 9.0.3 - semver: 7.7.3 - ts-api-utils: 1.4.3(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@6.21.0(eslint@8.57.1)(typescript@5.9.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1) - '@types/json-schema': 7.0.15 - '@types/semver': 7.7.1 - '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.9.3) - eslint: 8.57.1 - semver: 7.7.3 - transitivePeerDependencies: - - supports-color - - typescript - - '@typescript-eslint/visitor-keys@6.21.0': - dependencies: - '@typescript-eslint/types': 6.21.0 - eslint-visitor-keys: 3.4.3 - - '@ungap/structured-clone@1.3.0': {} - - abab@2.0.6: {} - - acorn-globals@7.0.1: - dependencies: - acorn: 8.15.0 - acorn-walk: 8.3.4 - - acorn-import-attributes@1.9.5(acorn@8.15.0): - dependencies: - acorn: 8.15.0 - - acorn-jsx@5.3.2(acorn@8.15.0): - dependencies: - acorn: 8.15.0 - - acorn-walk@8.3.4: - dependencies: - acorn: 8.15.0 - - acorn@8.15.0: {} - - agent-base@6.0.2: - dependencies: - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - agent-base@7.1.4: {} - - ajv@6.12.6: - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - - ansi-escapes@4.3.2: - dependencies: - type-fest: 0.21.3 - - ansi-regex@5.0.1: {} - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - ansi-styles@5.2.0: {} - - anymatch@3.1.3: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - - arg@4.1.3: {} - - argparse@1.0.10: - dependencies: - sprintf-js: 1.0.3 - - argparse@2.0.1: {} - - aria-query@5.3.2: {} - - array-buffer-byte-length@1.0.2: - dependencies: - call-bound: 1.0.4 - is-array-buffer: 3.0.5 - - array-includes@3.1.9: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - is-string: 1.1.1 - math-intrinsics: 1.1.0 - - array-union@2.1.0: {} - - array.prototype.findlastindex@1.2.6: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-shim-unscopables: 1.1.0 - - array.prototype.flat@1.3.3: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-shim-unscopables: 1.1.0 - - array.prototype.flatmap@1.3.3: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-shim-unscopables: 1.1.0 - - arraybuffer.prototype.slice@1.0.4: - dependencies: - array-buffer-byte-length: 1.0.2 - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - is-array-buffer: 3.0.5 - - async-function@1.0.0: {} - - async@3.2.6: {} - - asynckit@0.4.0: {} - - available-typed-arrays@1.0.7: - dependencies: - possible-typed-array-names: 1.1.0 - - axobject-query@4.1.0: {} - - babel-jest@29.7.0(@babel/core@7.28.4): - dependencies: - '@babel/core': 7.28.4 - '@jest/transform': 29.7.0 - '@types/babel__core': 7.20.5 - babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.6.3(@babel/core@7.28.4) - chalk: 4.1.2 - graceful-fs: 4.2.11 - slash: 3.0.0 - transitivePeerDependencies: - - supports-color - - babel-plugin-istanbul@6.1.1: - dependencies: - '@babel/helper-plugin-utils': 7.27.1 - '@istanbuljs/load-nyc-config': 1.1.0 - '@istanbuljs/schema': 0.1.3 - istanbul-lib-instrument: 5.2.1 - test-exclude: 6.0.0 - transitivePeerDependencies: - - supports-color - - babel-plugin-jest-hoist@29.6.3: - dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.28.4 - '@types/babel__core': 7.20.5 - '@types/babel__traverse': 7.28.0 - - babel-preset-current-node-syntax@1.2.0(@babel/core@7.28.4): - dependencies: - '@babel/core': 7.28.4 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.28.4) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.28.4) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.28.4) - '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.4) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.28.4) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.28.4) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.4) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.4) - - babel-preset-jest@29.6.3(@babel/core@7.28.4): - dependencies: - '@babel/core': 7.28.4 - babel-plugin-jest-hoist: 29.6.3 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.4) - - balanced-match@1.0.2: {} - - baseline-browser-mapping@2.8.16: {} - - bignumber.js@9.3.1: {} - - binary-extensions@2.3.0: {} - - brace-expansion@1.1.12: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - brace-expansion@2.0.2: - dependencies: - balanced-match: 1.0.2 - - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - - browserslist@4.26.3: - dependencies: - baseline-browser-mapping: 2.8.16 - caniuse-lite: 1.0.30001750 - electron-to-chromium: 1.5.237 - node-releases: 2.0.23 - update-browserslist-db: 1.1.3(browserslist@4.26.3) - - bs-logger@0.2.6: - dependencies: - fast-json-stable-stringify: 2.1.0 - - bser@2.1.1: - dependencies: - node-int64: 0.4.0 - - buffer-crc32@1.0.0: {} - - buffer-from@1.1.2: {} - - builtins@5.1.0: - dependencies: - semver: 7.7.3 - - call-bind-apply-helpers@1.0.2: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - - call-bind@1.0.8: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - get-intrinsic: 1.3.0 - set-function-length: 1.2.2 - - call-bound@1.0.4: - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 - - callsites@3.1.0: {} - - camelcase@5.3.1: {} - - camelcase@6.3.0: {} - - caniuse-lite@1.0.30001750: {} - - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - char-regex@1.0.2: {} - - chokidar@3.6.0: - dependencies: - anymatch: 3.1.3 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 - - ci-info@3.9.0: {} - - cjs-module-lexer@1.4.3: {} - - cliui@8.0.1: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - - co@4.6.0: {} - - code-red@1.0.4: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@types/estree': 1.0.8 - acorn: 8.15.0 - estree-walker: 3.0.3 - periscopic: 3.1.0 - - collect-v8-coverage@1.0.2: {} - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-convert@3.1.2: - dependencies: - color-name: 2.0.2 - - color-name@1.1.4: {} - - color-name@2.0.2: {} - - color-string@2.1.2: - dependencies: - color-name: 2.0.2 - - color@5.0.2: - dependencies: - color-convert: 3.1.2 - color-string: 2.1.2 - - combined-stream@1.0.8: - dependencies: - delayed-stream: 1.0.0 - - concat-map@0.0.1: {} - - convert-source-map@2.0.0: {} - - create-jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)): - dependencies: - '@jest/types': 29.6.3 - chalk: 4.1.2 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - jest-util: 29.7.0 - prompts: 2.4.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - - create-require@1.1.1: {} - - crelt@1.0.6: {} - - cross-env@7.0.3: - dependencies: - cross-spawn: 7.0.6 - - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - css-tree@2.3.1: - dependencies: - mdn-data: 2.0.30 - source-map-js: 1.2.1 - - css-what@6.2.2: {} - - cssesc@3.0.0: {} - - cssom@0.3.8: {} - - cssom@0.5.0: {} - - cssstyle@2.3.0: - dependencies: - cssom: 0.3.8 - - data-urls@3.0.2: - dependencies: - abab: 2.0.6 - whatwg-mimetype: 3.0.0 - whatwg-url: 11.0.0 - - data-view-buffer@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - data-view-byte-length@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - data-view-byte-offset@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - debug@3.2.7: - dependencies: - ms: 2.1.3 - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - decimal.js@10.6.0: {} - - dedent-js@1.0.1: {} - - dedent@1.7.0: {} - - deep-is@0.1.4: {} - - deepmerge@4.3.1: {} - - define-data-property@1.1.4: - dependencies: - es-define-property: 1.0.1 - es-errors: 1.3.0 - gopd: 1.2.0 - - define-properties@1.2.1: - dependencies: - define-data-property: 1.1.4 - has-property-descriptors: 1.0.2 - object-keys: 1.1.1 - - delayed-stream@1.0.0: {} - - detect-indent@6.1.0: {} - - detect-libc@2.1.2: - optional: true - - detect-newline@3.1.0: {} - - diff-sequences@29.6.3: {} - - diff@4.0.2: {} - - dir-glob@3.0.1: - dependencies: - path-type: 4.0.0 - - doctrine@2.1.0: - dependencies: - esutils: 2.0.3 - - doctrine@3.0.0: - dependencies: - esutils: 2.0.3 - - dom-serializer@2.0.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - entities: 4.5.0 - - domelementtype@2.3.0: {} - - domexception@4.0.0: - dependencies: - webidl-conversions: 7.0.0 - - domhandler@5.0.3: - dependencies: - domelementtype: 2.3.0 - - domutils@3.2.2: - dependencies: - dom-serializer: 2.0.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - - electron-to-chromium@1.5.237: {} - - emittery@0.13.1: {} - - emoji-regex@8.0.0: {} - - enabled@2.0.0: {} - - entities@4.5.0: {} - - entities@5.0.0: {} - - entities@6.0.1: {} - - error-ex@1.3.4: - dependencies: - is-arrayish: 0.2.1 - - es-abstract@1.24.0: - dependencies: - array-buffer-byte-length: 1.0.2 - arraybuffer.prototype.slice: 1.0.4 - available-typed-arrays: 1.0.7 - call-bind: 1.0.8 - call-bound: 1.0.4 - data-view-buffer: 1.0.2 - data-view-byte-length: 1.0.2 - data-view-byte-offset: 1.0.1 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-set-tostringtag: 2.1.0 - es-to-primitive: 1.3.0 - function.prototype.name: 1.1.8 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - get-symbol-description: 1.1.0 - globalthis: 1.0.4 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - has-proto: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - internal-slot: 1.1.0 - is-array-buffer: 3.0.5 - is-callable: 1.2.7 - is-data-view: 1.0.2 - is-negative-zero: 2.0.3 - is-regex: 1.2.1 - is-set: 2.0.3 - is-shared-array-buffer: 1.0.4 - is-string: 1.1.1 - is-typed-array: 1.1.15 - is-weakref: 1.1.1 - math-intrinsics: 1.1.0 - object-inspect: 1.13.4 - object-keys: 1.1.1 - object.assign: 4.1.7 - own-keys: 1.0.1 - regexp.prototype.flags: 1.5.4 - safe-array-concat: 1.1.3 - safe-push-apply: 1.0.0 - safe-regex-test: 1.1.0 - set-proto: 1.0.0 - stop-iteration-iterator: 1.1.0 - string.prototype.trim: 1.2.10 - string.prototype.trimend: 1.0.9 - string.prototype.trimstart: 1.0.8 - typed-array-buffer: 1.0.3 - typed-array-byte-length: 1.0.3 - typed-array-byte-offset: 1.0.4 - typed-array-length: 1.0.7 - unbox-primitive: 1.1.0 - which-typed-array: 1.1.19 - - es-define-property@1.0.1: {} - - es-errors@1.3.0: {} - - es-object-atoms@1.1.1: - dependencies: - es-errors: 1.3.0 - - es-set-tostringtag@2.1.0: - dependencies: - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - - es-shim-unscopables@1.1.0: - dependencies: - hasown: 2.0.2 - - es-to-primitive@1.3.0: - dependencies: - is-callable: 1.2.7 - is-date-object: 1.1.0 - is-symbol: 1.1.1 - - es6-promise@3.3.1: {} - - esbuild-plugin-copy@2.1.1(esbuild@0.25.11): - dependencies: - chalk: 4.1.2 - chokidar: 3.6.0 - esbuild: 0.25.11 - fs-extra: 10.1.0 - globby: 11.1.0 - - esbuild-svelte@0.9.3(esbuild@0.25.11)(svelte@4.2.20): - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - esbuild: 0.25.11 - svelte: 4.2.20 - - esbuild@0.25.11: - optionalDependencies: - '@esbuild/aix-ppc64': 0.25.11 - '@esbuild/android-arm': 0.25.11 - '@esbuild/android-arm64': 0.25.11 - '@esbuild/android-x64': 0.25.11 - '@esbuild/darwin-arm64': 0.25.11 - '@esbuild/darwin-x64': 0.25.11 - '@esbuild/freebsd-arm64': 0.25.11 - '@esbuild/freebsd-x64': 0.25.11 - '@esbuild/linux-arm': 0.25.11 - '@esbuild/linux-arm64': 0.25.11 - '@esbuild/linux-ia32': 0.25.11 - '@esbuild/linux-loong64': 0.25.11 - '@esbuild/linux-mips64el': 0.25.11 - '@esbuild/linux-ppc64': 0.25.11 - '@esbuild/linux-riscv64': 0.25.11 - '@esbuild/linux-s390x': 0.25.11 - '@esbuild/linux-x64': 0.25.11 - '@esbuild/netbsd-arm64': 0.25.11 - '@esbuild/netbsd-x64': 0.25.11 - '@esbuild/openbsd-arm64': 0.25.11 - '@esbuild/openbsd-x64': 0.25.11 - '@esbuild/openharmony-arm64': 0.25.11 - '@esbuild/sunos-x64': 0.25.11 - '@esbuild/win32-arm64': 0.25.11 - '@esbuild/win32-ia32': 0.25.11 - '@esbuild/win32-x64': 0.25.11 - - escalade@3.2.0: {} - - escape-string-regexp@2.0.0: {} - - escape-string-regexp@4.0.0: {} - - escodegen@2.1.0: - dependencies: - esprima: 4.0.1 - estraverse: 5.3.0 - esutils: 2.0.3 - optionalDependencies: - source-map: 0.6.1 - - eslint-compat-utils@0.5.1(eslint@8.57.1): - dependencies: - eslint: 8.57.1 - semver: 7.7.3 - - eslint-config-standard-with-typescript@40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3): - dependencies: - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) - eslint: 8.57.1 - eslint-config-standard: 17.1.0(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.32.0(eslint@8.57.1) - eslint-plugin-n: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: 6.6.0(eslint@8.57.1) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - eslint-config-standard@17.1.0(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1): - dependencies: - eslint: 8.57.1 - eslint-plugin-import: 2.32.0(eslint@8.57.1) - eslint-plugin-n: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: 6.6.0(eslint@8.57.1) - - eslint-import-resolver-node@0.3.9: - dependencies: - debug: 3.2.7 - is-core-module: 2.16.1 - resolve: 1.22.10 - - eslint-module-utils@2.12.1(eslint@8.57.1): - dependencies: - debug: 3.2.7 - optionalDependencies: - eslint: 8.57.1 - - eslint-plugin-es@3.0.1(eslint@8.57.1): - dependencies: - eslint: 8.57.1 - eslint-utils: 2.1.0 - regexpp: 3.2.0 - - eslint-plugin-es@4.1.0(eslint@8.57.1): - dependencies: - eslint: 8.57.1 - eslint-utils: 2.1.0 - regexpp: 3.2.0 - - eslint-plugin-import@2.32.0(eslint@8.57.1): - dependencies: - '@rtsao/scc': 1.1.0 - array-includes: 3.1.9 - array.prototype.findlastindex: 1.2.6 - array.prototype.flat: 1.3.3 - array.prototype.flatmap: 1.3.3 - debug: 3.2.7 - doctrine: 2.1.0 - eslint: 8.57.1 - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(eslint@8.57.1) - hasown: 2.0.2 - is-core-module: 2.16.1 - is-glob: 4.0.3 - minimatch: 3.1.2 - object.fromentries: 2.0.8 - object.groupby: 1.0.3 - object.values: 1.2.1 - semver: 6.3.1 - string.prototype.trimend: 1.0.9 - tsconfig-paths: 3.15.0 - - eslint-plugin-n@15.7.0(eslint@8.57.1): - dependencies: - builtins: 5.1.0 - eslint: 8.57.1 - eslint-plugin-es: 4.1.0(eslint@8.57.1) - eslint-utils: 3.0.0(eslint@8.57.1) - ignore: 5.3.2 - is-core-module: 2.16.1 - minimatch: 3.1.2 - resolve: 1.22.10 - semver: 7.7.3 - - eslint-plugin-node@11.1.0(eslint@8.57.1): - dependencies: - eslint: 8.57.1 - eslint-plugin-es: 3.0.1(eslint@8.57.1) - eslint-utils: 2.1.0 - ignore: 5.3.2 - minimatch: 3.1.2 - resolve: 1.22.10 - semver: 6.3.1 - - eslint-plugin-promise@6.6.0(eslint@8.57.1): - dependencies: - eslint: 8.57.1 - - eslint-plugin-svelte@2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)): - dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1) - '@jridgewell/sourcemap-codec': 1.5.5 - eslint: 8.57.1 - eslint-compat-utils: 0.5.1(eslint@8.57.1) - esutils: 2.0.3 - known-css-properties: 0.35.0 - postcss: 8.5.6 - postcss-load-config: 3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - postcss-safe-parser: 6.0.0(postcss@8.5.6) - postcss-selector-parser: 6.1.2 - semver: 7.7.3 - svelte-eslint-parser: 0.43.0(svelte@4.2.20) - optionalDependencies: - svelte: 4.2.20 - transitivePeerDependencies: - - ts-node - - eslint-scope@7.2.2: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-utils@2.1.0: - dependencies: - eslint-visitor-keys: 1.3.0 - - eslint-utils@3.0.0(eslint@8.57.1): - dependencies: - eslint: 8.57.1 - eslint-visitor-keys: 2.1.0 - - eslint-visitor-keys@1.3.0: {} - - eslint-visitor-keys@2.1.0: {} - - eslint-visitor-keys@3.4.3: {} - - eslint@8.57.1: - dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1) - '@eslint-community/regexpp': 4.12.1 - '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.57.1 - '@humanwhocodes/config-array': 0.13.0 - '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - '@ungap/structured-clone': 1.3.0 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.4.3 - doctrine: 3.0.0 - escape-string-regexp: 4.0.0 - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 - esquery: 1.6.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 - find-up: 5.0.0 - glob-parent: 6.0.2 - globals: 13.24.0 - graphemer: 1.4.0 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - is-path-inside: 3.0.3 - js-yaml: 4.1.0 - json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.4 - strip-ansi: 6.0.1 - text-table: 0.2.0 - transitivePeerDependencies: - - supports-color - - espree@9.6.1: - dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) - eslint-visitor-keys: 3.4.3 - - esprima@4.0.1: {} - - esquery@1.6.0: - dependencies: - estraverse: 5.3.0 - - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@5.3.0: {} - - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.8 - - esutils@2.0.3: {} - - execa@5.1.1: - dependencies: - cross-spawn: 7.0.6 - get-stream: 6.0.1 - human-signals: 2.1.0 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 - - exit@0.1.2: {} - - expect@29.7.0: - dependencies: - '@jest/expect-utils': 29.7.0 - jest-get-type: 29.6.3 - jest-matcher-utils: 29.7.0 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - - extend@3.0.2: {} - - fast-deep-equal@3.1.3: {} - - fast-equals@5.3.2: {} - - fast-glob@3.3.3: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - - fast-json-stable-stringify@2.1.0: {} - - fast-levenshtein@2.0.6: {} - - fastq@1.19.1: - dependencies: - reusify: 1.1.0 - - fb-watchman@2.0.2: - dependencies: - bser: 2.1.1 - - fecha@4.2.3: {} - - file-entry-cache@6.0.1: - dependencies: - flat-cache: 3.2.0 - - file-stream-rotator@0.6.1: - dependencies: - moment: 2.30.1 - - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - - find-up@4.1.0: - dependencies: - locate-path: 5.0.0 - path-exists: 4.0.0 - - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - - flat-cache@3.2.0: - dependencies: - flatted: 3.3.3 - keyv: 4.5.4 - rimraf: 3.0.2 - - flatted@3.3.3: {} - - fn.name@1.1.0: {} - - for-each@0.3.5: - dependencies: - is-callable: 1.2.7 - - form-data@4.0.4: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - es-set-tostringtag: 2.1.0 - hasown: 2.0.2 - mime-types: 2.1.35 - - forwarded-parse@2.1.2: {} - - fs-extra@10.1.0: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.2.0 - universalify: 2.0.1 - - fs.realpath@1.0.0: {} - - fsevents@2.3.3: - optional: true - - function-bind@1.1.2: {} - - function.prototype.name@1.1.8: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - functions-have-names: 1.2.3 - hasown: 2.0.2 - is-callable: 1.2.7 - - functions-have-names@1.2.3: {} - - gaxios@6.7.1: - dependencies: - extend: 3.0.2 - https-proxy-agent: 7.0.6 - is-stream: 2.0.1 - node-fetch: 2.7.0 - uuid: 9.0.1 - transitivePeerDependencies: - - encoding - - supports-color - - gcp-metadata@6.1.1: - dependencies: - gaxios: 6.7.1 - google-logging-utils: 0.0.2 - json-bigint: 1.0.0 - transitivePeerDependencies: - - encoding - - supports-color - - generator-function@2.0.1: {} - - gensync@1.0.0-beta.2: {} - - get-caller-file@2.0.5: {} - - get-intrinsic@1.3.0: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - math-intrinsics: 1.1.0 - - get-package-type@0.1.0: {} - - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 - - get-stream@6.0.1: {} - - get-symbol-description@1.1.0: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - - glob@7.2.3: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 - - globals@13.24.0: - dependencies: - type-fest: 0.20.2 - - globalthis@1.0.4: - dependencies: - define-properties: 1.2.1 - gopd: 1.2.0 - - globby@11.1.0: - dependencies: - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.3.3 - ignore: 5.3.2 - merge2: 1.4.1 - slash: 3.0.0 - - google-logging-utils@0.0.2: {} - - gopd@1.2.0: {} - - graceful-fs@4.2.11: {} - - graphemer@1.4.0: {} - - handlebars@4.7.8: - dependencies: - minimist: 1.2.8 - neo-async: 2.6.2 - source-map: 0.6.1 - wordwrap: 1.0.0 - optionalDependencies: - uglify-js: 3.19.3 - - has-bigints@1.1.0: {} - - has-flag@4.0.0: {} - - has-property-descriptors@1.0.2: - dependencies: - es-define-property: 1.0.1 - - has-proto@1.2.0: - dependencies: - dunder-proto: 1.0.1 - - has-symbols@1.1.0: {} - - has-tostringtag@1.0.2: - dependencies: - has-symbols: 1.1.0 - - hash-it@6.0.0: {} - - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - - html-encoding-sniffer@3.0.0: - dependencies: - whatwg-encoding: 2.0.0 - - html-escaper@2.0.2: {} - - htmlparser2@9.1.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.2.2 - entities: 4.5.0 - - http-proxy-agent@5.0.0: - dependencies: - '@tootallnate/once': 2.0.0 - agent-base: 6.0.2 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - https-proxy-agent@5.0.1: - dependencies: - agent-base: 6.0.2 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - https-proxy-agent@7.0.6: - dependencies: - agent-base: 7.1.4 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - human-signals@2.1.0: {} - - iconv-lite@0.6.3: - dependencies: - safer-buffer: 2.1.2 - - ignore@5.3.2: {} - - import-fresh@3.3.1: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - - import-in-the-middle@1.15.0: - dependencies: - acorn: 8.15.0 - acorn-import-attributes: 1.9.5(acorn@8.15.0) - cjs-module-lexer: 1.4.3 - module-details-from-path: 1.0.4 - - import-local@3.2.0: - dependencies: - pkg-dir: 4.2.0 - resolve-cwd: 3.0.0 - - imurmurhash@0.1.4: {} - - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - - inherits@2.0.4: {} - - internal-slot@1.1.0: - dependencies: - es-errors: 1.3.0 - hasown: 2.0.2 - side-channel: 1.1.0 - - intl-messageformat@10.7.18: - dependencies: - '@formatjs/ecma402-abstract': 2.3.6 - '@formatjs/fast-memoize': 2.2.7 - '@formatjs/icu-messageformat-parser': 2.11.4 - tslib: 2.8.1 - - is-array-buffer@3.0.5: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - - is-arrayish@0.2.1: {} - - is-async-function@2.1.1: - dependencies: - async-function: 1.0.0 - call-bound: 1.0.4 - get-proto: 1.0.1 - has-tostringtag: 1.0.2 - safe-regex-test: 1.1.0 - - is-bigint@1.1.0: - dependencies: - has-bigints: 1.1.0 - - is-binary-path@2.1.0: - dependencies: - binary-extensions: 2.3.0 - - is-boolean-object@1.2.2: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-callable@1.2.7: {} - - is-core-module@2.16.1: - dependencies: - hasown: 2.0.2 - - is-data-view@1.0.2: - dependencies: - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - is-typed-array: 1.1.15 - - is-date-object@1.1.0: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-extglob@2.1.1: {} - - is-finalizationregistry@1.1.1: - dependencies: - call-bound: 1.0.4 - - is-fullwidth-code-point@3.0.0: {} - - is-generator-fn@2.1.0: {} - - is-generator-function@1.1.2: - dependencies: - call-bound: 1.0.4 - generator-function: 2.0.1 - get-proto: 1.0.1 - has-tostringtag: 1.0.2 - safe-regex-test: 1.1.0 - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - is-map@2.0.3: {} - - is-negative-zero@2.0.3: {} - - is-number-object@1.1.1: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-number@7.0.0: {} - - is-path-inside@3.0.3: {} - - is-potential-custom-element-name@1.0.1: {} - - is-reference@3.0.3: - dependencies: - '@types/estree': 1.0.8 - - is-regex@1.2.1: - dependencies: - call-bound: 1.0.4 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - - is-set@2.0.3: {} - - is-shared-array-buffer@1.0.4: - dependencies: - call-bound: 1.0.4 - - is-stream@2.0.1: {} - - is-string@1.1.1: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-symbol@1.1.1: - dependencies: - call-bound: 1.0.4 - has-symbols: 1.1.0 - safe-regex-test: 1.1.0 - - is-typed-array@1.1.15: - dependencies: - which-typed-array: 1.1.19 - - is-weakmap@2.0.2: {} - - is-weakref@1.1.1: - dependencies: - call-bound: 1.0.4 - - is-weakset@2.0.4: - dependencies: - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - - isarray@2.0.5: {} - - isexe@2.0.0: {} - - isomorphic.js@0.2.5: {} - - istanbul-lib-coverage@3.2.2: {} - - istanbul-lib-instrument@5.2.1: - dependencies: - '@babel/core': 7.28.4 - '@babel/parser': 7.28.4 - '@istanbuljs/schema': 0.1.3 - istanbul-lib-coverage: 3.2.2 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - istanbul-lib-instrument@6.0.3: - dependencies: - '@babel/core': 7.28.4 - '@babel/parser': 7.28.4 - '@istanbuljs/schema': 0.1.3 - istanbul-lib-coverage: 3.2.2 - semver: 7.7.3 - transitivePeerDependencies: - - supports-color - - istanbul-lib-report@3.0.1: - dependencies: - istanbul-lib-coverage: 3.2.2 - make-dir: 4.0.0 - supports-color: 7.2.0 - - istanbul-lib-source-maps@4.0.1: - dependencies: - debug: 4.4.3 - istanbul-lib-coverage: 3.2.2 - source-map: 0.6.1 - transitivePeerDependencies: - - supports-color - - istanbul-reports@3.2.0: - dependencies: - html-escaper: 2.0.2 - istanbul-lib-report: 3.0.1 - - jest-changed-files@29.7.0: - dependencies: - execa: 5.1.1 - jest-util: 29.7.0 - p-limit: 3.1.0 - - jest-circus@29.7.0: - dependencies: - '@jest/environment': 29.7.0 - '@jest/expect': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.18.10 - chalk: 4.1.2 - co: 4.6.0 - dedent: 1.7.0 - is-generator-fn: 2.1.0 - jest-each: 29.7.0 - jest-matcher-utils: 29.7.0 - jest-message-util: 29.7.0 - jest-runtime: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - p-limit: 3.1.0 - pretty-format: 29.7.0 - pure-rand: 6.1.0 - slash: 3.0.0 - stack-utils: 2.0.6 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-cli@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)): - dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - chalk: 4.1.2 - create-jest: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - exit: 0.1.2 - import-local: 3.2.0 - jest-config: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - jest-util: 29.7.0 - jest-validate: 29.7.0 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - - jest-config@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)): - dependencies: - '@babel/core': 7.28.4 - '@jest/test-sequencer': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.28.4) - chalk: 4.1.2 - ci-info: 3.9.0 - deepmerge: 4.3.1 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-circus: 29.7.0 - jest-environment-node: 29.7.0 - jest-get-type: 29.6.3 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-runner: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - micromatch: 4.0.8 - parse-json: 5.2.0 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 22.18.10 - ts-node: 10.9.2(@types/node@22.18.10)(typescript@5.9.3) - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-diff@29.7.0: - dependencies: - chalk: 4.1.2 - diff-sequences: 29.6.3 - jest-get-type: 29.6.3 - pretty-format: 29.7.0 - - jest-docblock@29.7.0: - dependencies: - detect-newline: 3.1.0 - - jest-each@29.7.0: - dependencies: - '@jest/types': 29.6.3 - chalk: 4.1.2 - jest-get-type: 29.6.3 - jest-util: 29.7.0 - pretty-format: 29.7.0 - - jest-environment-jsdom@29.7.0: - dependencies: - '@jest/environment': 29.7.0 - '@jest/fake-timers': 29.7.0 - '@jest/types': 29.6.3 - '@types/jsdom': 20.0.1 - '@types/node': 22.18.10 - jest-mock: 29.7.0 - jest-util: 29.7.0 - jsdom: 20.0.3 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - jest-environment-node@29.7.0: - dependencies: - '@jest/environment': 29.7.0 - '@jest/fake-timers': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.18.10 - jest-mock: 29.7.0 - jest-util: 29.7.0 - - jest-get-type@29.6.3: {} - - jest-haste-map@29.7.0: - dependencies: - '@jest/types': 29.6.3 - '@types/graceful-fs': 4.1.9 - '@types/node': 22.18.10 - anymatch: 3.1.3 - fb-watchman: 2.0.2 - graceful-fs: 4.2.11 - jest-regex-util: 29.6.3 - jest-util: 29.7.0 - jest-worker: 29.7.0 - micromatch: 4.0.8 - walker: 1.0.8 - optionalDependencies: - fsevents: 2.3.3 - - jest-leak-detector@29.7.0: - dependencies: - jest-get-type: 29.6.3 - pretty-format: 29.7.0 - - jest-matcher-utils@29.7.0: - dependencies: - chalk: 4.1.2 - jest-diff: 29.7.0 - jest-get-type: 29.6.3 - pretty-format: 29.7.0 - - jest-message-util@29.7.0: - dependencies: - '@babel/code-frame': 7.27.1 - '@jest/types': 29.6.3 - '@types/stack-utils': 2.0.3 - chalk: 4.1.2 - graceful-fs: 4.2.11 - micromatch: 4.0.8 - pretty-format: 29.7.0 - slash: 3.0.0 - stack-utils: 2.0.6 - - jest-mock@29.7.0: - dependencies: - '@jest/types': 29.6.3 - '@types/node': 22.18.10 - jest-util: 29.7.0 - - jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): - optionalDependencies: - jest-resolve: 29.7.0 - - jest-regex-util@29.6.3: {} - - jest-resolve-dependencies@29.7.0: - dependencies: - jest-regex-util: 29.6.3 - jest-snapshot: 29.7.0 - transitivePeerDependencies: - - supports-color - - jest-resolve@29.7.0: - dependencies: - chalk: 4.1.2 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) - jest-util: 29.7.0 - jest-validate: 29.7.0 - resolve: 1.22.10 - resolve.exports: 2.0.3 - slash: 3.0.0 - - jest-runner@29.7.0: - dependencies: - '@jest/console': 29.7.0 - '@jest/environment': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.18.10 - chalk: 4.1.2 - emittery: 0.13.1 - graceful-fs: 4.2.11 - jest-docblock: 29.7.0 - jest-environment-node: 29.7.0 - jest-haste-map: 29.7.0 - jest-leak-detector: 29.7.0 - jest-message-util: 29.7.0 - jest-resolve: 29.7.0 - jest-runtime: 29.7.0 - jest-util: 29.7.0 - jest-watcher: 29.7.0 - jest-worker: 29.7.0 - p-limit: 3.1.0 - source-map-support: 0.5.13 - transitivePeerDependencies: - - supports-color - - jest-runtime@29.7.0: - dependencies: - '@jest/environment': 29.7.0 - '@jest/fake-timers': 29.7.0 - '@jest/globals': 29.7.0 - '@jest/source-map': 29.6.3 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.18.10 - chalk: 4.1.2 - cjs-module-lexer: 1.4.3 - collect-v8-coverage: 1.0.2 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - jest-message-util: 29.7.0 - jest-mock: 29.7.0 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - slash: 3.0.0 - strip-bom: 4.0.0 - transitivePeerDependencies: - - supports-color - - jest-snapshot@29.7.0: - dependencies: - '@babel/core': 7.28.4 - '@babel/generator': 7.28.3 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.4) - '@babel/types': 7.28.4 - '@jest/expect-utils': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.4) - chalk: 4.1.2 - expect: 29.7.0 - graceful-fs: 4.2.11 - jest-diff: 29.7.0 - jest-get-type: 29.6.3 - jest-matcher-utils: 29.7.0 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - natural-compare: 1.4.0 - pretty-format: 29.7.0 - semver: 7.7.3 - transitivePeerDependencies: - - supports-color - - jest-util@29.7.0: - dependencies: - '@jest/types': 29.6.3 - '@types/node': 22.18.10 - chalk: 4.1.2 - ci-info: 3.9.0 - graceful-fs: 4.2.11 - picomatch: 2.3.1 - - jest-validate@29.7.0: - dependencies: - '@jest/types': 29.6.3 - camelcase: 6.3.0 - chalk: 4.1.2 - jest-get-type: 29.6.3 - leven: 3.1.0 - pretty-format: 29.7.0 - - jest-watcher@29.7.0: - dependencies: - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.18.10 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - emittery: 0.13.1 - jest-util: 29.7.0 - string-length: 4.0.2 - - jest-worker@29.7.0: - dependencies: - '@types/node': 22.18.10 - jest-util: 29.7.0 - merge-stream: 2.0.0 - supports-color: 8.1.1 - - jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)): - dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - '@jest/types': 29.6.3 - import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - - js-tokens@4.0.0: {} - - js-yaml@3.14.1: - dependencies: - argparse: 1.0.10 - esprima: 4.0.1 - - js-yaml@4.1.0: - dependencies: - argparse: 2.0.1 - - jsdom@20.0.3: - dependencies: - abab: 2.0.6 - acorn: 8.15.0 - acorn-globals: 7.0.1 - cssom: 0.5.0 - cssstyle: 2.3.0 - data-urls: 3.0.2 - decimal.js: 10.6.0 - domexception: 4.0.0 - escodegen: 2.1.0 - form-data: 4.0.4 - html-encoding-sniffer: 3.0.0 - http-proxy-agent: 5.0.0 - https-proxy-agent: 5.0.1 - is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.22 - parse5: 7.3.0 - saxes: 6.0.0 - symbol-tree: 3.2.4 - tough-cookie: 4.1.4 - w3c-xmlserializer: 4.0.0 - webidl-conversions: 7.0.0 - whatwg-encoding: 2.0.0 - whatwg-mimetype: 3.0.0 - whatwg-url: 11.0.0 - ws: 8.18.3 - xml-name-validator: 4.0.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - jsesc@3.1.0: {} - - json-bigint@1.0.0: - dependencies: - bignumber.js: 9.3.1 - - json-buffer@3.0.1: {} - - json-parse-even-better-errors@2.3.1: {} - - json-schema-traverse@0.4.1: {} - - json-stable-stringify-without-jsonify@1.0.1: {} - - json5@1.0.2: - dependencies: - minimist: 1.2.8 - - json5@2.2.3: {} - - jsonfile@6.2.0: - dependencies: - universalify: 2.0.1 - optionalDependencies: - graceful-fs: 4.2.11 - - jwt-simple@0.5.6: {} - - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 - - kleur@3.0.3: {} - - known-css-properties@0.35.0: {} - - kuler@2.0.0: {} - - leven@3.1.0: {} - - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - - lexorank@1.0.5: {} - - lib0@0.2.114: - dependencies: - isomorphic.js: 0.2.5 - - lilconfig@2.1.0: {} - - lines-and-columns@1.2.4: {} - - linkify-it@5.0.0: - dependencies: - uc.micro: 2.1.0 - - linkifyjs@4.3.2: {} - - locate-character@3.0.0: {} - - locate-path@5.0.0: - dependencies: - p-locate: 4.1.0 - - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - - lodash.camelcase@4.3.0: {} - - lodash.memoize@4.1.2: {} - - lodash.merge@4.6.2: {} - - logform@2.7.0: - dependencies: - '@colors/colors': 1.6.0 - '@types/triple-beam': 1.3.5 - fecha: 4.2.3 - ms: 2.1.3 - safe-stable-stringify: 2.5.0 - triple-beam: 1.4.1 - - long@5.3.2: {} - - lru-cache@5.1.1: - dependencies: - yallist: 3.1.1 - - magic-string@0.30.19: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - - make-dir@4.0.0: - dependencies: - semver: 7.7.3 - - make-error@1.3.6: {} - - makeerror@1.0.12: - dependencies: - tmpl: 1.0.5 - - markdown-it@14.1.0: - dependencies: - argparse: 2.0.1 - entities: 4.5.0 - linkify-it: 5.0.0 - mdurl: 2.0.0 - punycode.js: 2.3.1 - uc.micro: 2.1.0 - - math-intrinsics@1.1.0: {} - - mdn-data@2.0.30: {} - - mdurl@2.0.0: {} - - merge-stream@2.0.0: {} - - merge2@1.4.1: {} - - micromatch@4.0.8: - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 - - mime-db@1.52.0: {} - - mime-types@2.1.35: - dependencies: - mime-db: 1.52.0 - - mimic-fn@2.1.0: {} - - min-indent@1.0.1: {} - - minimatch@3.1.2: - dependencies: - brace-expansion: 1.1.12 - - minimatch@9.0.3: - dependencies: - brace-expansion: 2.0.2 - - minimist@1.2.8: {} - - mkdirp@0.5.6: - dependencies: - minimist: 1.2.8 - - module-details-from-path@1.0.4: {} - - moment@2.30.1: {} - - ms@2.1.3: {} - - msgpackr-extract@3.0.3: - dependencies: - node-gyp-build-optional-packages: 5.2.2 - optionalDependencies: - '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3 - '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 - optional: true - - msgpackr@1.11.5: - optionalDependencies: - msgpackr-extract: 3.0.3 - - nanoid@3.3.11: {} - - natural-compare@1.4.0: {} - - neo-async@2.6.2: {} - - node-fetch@2.7.0: - dependencies: - whatwg-url: 5.0.0 - - node-gyp-build-optional-packages@5.2.2: - dependencies: - detect-libc: 2.1.2 - optional: true - - node-int64@0.4.0: {} - - node-releases@2.0.23: {} - - normalize-path@3.0.0: {} - - npm-run-path@4.0.1: - dependencies: - path-key: 3.1.1 - - nwsapi@2.2.22: {} - - object-hash@3.0.0: {} - - object-inspect@1.13.4: {} - - object-keys@1.1.1: {} - - object.assign@4.1.7: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - has-symbols: 1.1.0 - object-keys: 1.1.1 - - object.fromentries@2.0.8: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-object-atoms: 1.1.1 - - object.groupby@1.0.3: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - - object.values@1.2.1: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - - once@1.4.0: - dependencies: - wrappy: 1.0.2 - - one-time@1.0.0: - dependencies: - fn.name: 1.1.0 - - onetime@5.1.2: - dependencies: - mimic-fn: 2.1.0 - - optionator@0.9.4: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - - orderedmap@2.1.1: {} - - own-keys@1.0.1: - dependencies: - get-intrinsic: 1.3.0 - object-keys: 1.1.1 - safe-push-apply: 1.0.0 - - p-limit@2.3.0: - dependencies: - p-try: 2.2.0 - - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - - p-locate@4.1.0: - dependencies: - p-limit: 2.3.0 - - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - - p-try@2.2.0: {} - - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - - parse-json@5.2.0: - dependencies: - '@babel/code-frame': 7.27.1 - error-ex: 1.3.4 - json-parse-even-better-errors: 2.3.1 - lines-and-columns: 1.2.4 - - parse5@7.3.0: - dependencies: - entities: 6.0.1 - - path-exists@4.0.0: {} - - path-is-absolute@1.0.1: {} - - path-key@3.1.1: {} - - path-parse@1.0.7: {} - - path-type@4.0.0: {} - - periscopic@3.1.0: - dependencies: - '@types/estree': 1.0.8 - estree-walker: 3.0.3 - is-reference: 3.0.3 - - pg-int8@1.0.1: {} - - pg-protocol@1.10.3: {} - - pg-types@2.2.0: - dependencies: - pg-int8: 1.0.1 - postgres-array: 2.0.0 - postgres-bytea: 1.0.0 - postgres-date: 1.0.7 - postgres-interval: 1.2.0 - - picocolors@1.1.1: {} - - picomatch@2.3.1: {} - - pirates@4.0.7: {} - - pkg-dir@4.2.0: - dependencies: - find-up: 4.1.0 - - possible-typed-array-names@1.1.0: {} - - postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)): - dependencies: - lilconfig: 2.1.0 - yaml: 1.10.2 - optionalDependencies: - postcss: 8.5.6 - ts-node: 10.9.2(@types/node@22.18.10)(typescript@5.9.3) - - postcss-safe-parser@6.0.0(postcss@8.5.6): - dependencies: - postcss: 8.5.6 - - postcss-scss@4.0.9(postcss@8.5.6): - dependencies: - postcss: 8.5.6 - - postcss-selector-parser@6.1.2: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - - postcss@8.5.6: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - postgres-array@2.0.0: {} - - postgres-bytea@1.0.0: {} - - postgres-date@1.0.7: {} - - postgres-interval@1.2.0: - dependencies: - xtend: 4.0.2 - - postgres@3.4.7: {} - - prelude-ls@1.2.1: {} - - prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@4.2.20): - dependencies: - prettier: 3.6.2 - svelte: 4.2.20 - - prettier@3.6.2: {} - - pretty-format@29.7.0: - dependencies: - '@jest/schemas': 29.6.3 - ansi-styles: 5.2.0 - react-is: 18.3.1 - - prompts@2.4.2: - dependencies: - kleur: 3.0.3 - sisteransi: 1.0.5 - - prosemirror-changeset@2.3.1: - dependencies: - prosemirror-transform: 1.10.4 - - prosemirror-codemark@0.4.2(prosemirror-inputrules@1.5.1)(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3): - dependencies: - prosemirror-inputrules: 1.5.1 - prosemirror-model: 1.25.3 - prosemirror-state: 1.4.3 - prosemirror-view: 1.41.3 - - prosemirror-collab@1.3.1: - dependencies: - prosemirror-state: 1.4.3 - - prosemirror-commands@1.7.1: - dependencies: - prosemirror-model: 1.25.3 - prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.4 - - prosemirror-dropcursor@1.8.2: - dependencies: - prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.4 - prosemirror-view: 1.41.3 - - prosemirror-gapcursor@1.4.0: - dependencies: - prosemirror-keymap: 1.2.3 - prosemirror-model: 1.25.3 - prosemirror-state: 1.4.3 - prosemirror-view: 1.41.3 - - prosemirror-history@1.4.1: - dependencies: - prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.4 - prosemirror-view: 1.41.3 - rope-sequence: 1.3.4 - - prosemirror-inputrules@1.5.1: - dependencies: - prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.4 - - prosemirror-keymap@1.2.3: - dependencies: - prosemirror-state: 1.4.3 - w3c-keyname: 2.2.8 - - prosemirror-markdown@1.13.2: - dependencies: - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - prosemirror-model: 1.25.3 - - prosemirror-menu@1.2.5: - dependencies: - crelt: 1.0.6 - prosemirror-commands: 1.7.1 - prosemirror-history: 1.4.1 - prosemirror-state: 1.4.3 - - prosemirror-model@1.25.3: - dependencies: - orderedmap: 2.1.1 - - prosemirror-schema-basic@1.2.4: - dependencies: - prosemirror-model: 1.25.3 - - prosemirror-schema-list@1.5.1: - dependencies: - prosemirror-model: 1.25.3 - prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.4 - - prosemirror-state@1.4.3: - dependencies: - prosemirror-model: 1.25.3 - prosemirror-transform: 1.10.4 - prosemirror-view: 1.41.3 - - prosemirror-tables@1.8.1: - dependencies: - prosemirror-keymap: 1.2.3 - prosemirror-model: 1.25.3 - prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.4 - prosemirror-view: 1.41.3 - - prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3): - dependencies: - '@remirror/core-constants': 3.0.0 - escape-string-regexp: 4.0.0 - prosemirror-model: 1.25.3 - prosemirror-state: 1.4.3 - prosemirror-view: 1.41.3 - - prosemirror-transform@1.10.4: - dependencies: - prosemirror-model: 1.25.3 - - prosemirror-view@1.41.3: - dependencies: - prosemirror-model: 1.25.3 - prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.4 - - protobufjs@7.5.4: - dependencies: - '@protobufjs/aspromise': 1.1.2 - '@protobufjs/base64': 1.1.2 - '@protobufjs/codegen': 2.0.4 - '@protobufjs/eventemitter': 1.1.0 - '@protobufjs/fetch': 1.1.0 - '@protobufjs/float': 1.0.2 - '@protobufjs/inquire': 1.1.0 - '@protobufjs/path': 1.1.2 - '@protobufjs/pool': 1.1.0 - '@protobufjs/utf8': 1.1.0 - '@types/node': 22.18.10 - long: 5.3.2 - - psl@1.15.0: - dependencies: - punycode: 2.3.1 - - punycode.js@2.3.1: {} - - punycode@2.3.1: {} - - pure-rand@6.1.0: {} - - querystringify@2.2.0: {} - - queue-microtask@1.2.3: {} - - react-is@18.3.1: {} - - readable-stream@3.6.2: - dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - - readdirp@3.6.0: - dependencies: - picomatch: 2.3.1 - - reflect.getprototypeof@1.0.10: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - which-builtin-type: 1.2.1 - - regexp.prototype.flags@1.5.4: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-errors: 1.3.0 - get-proto: 1.0.1 - gopd: 1.2.0 - set-function-name: 2.0.2 - - regexpp@3.2.0: {} - - require-directory@2.1.1: {} - - require-in-the-middle@7.5.2: - dependencies: - debug: 4.4.3 - module-details-from-path: 1.0.4 - resolve: 1.22.10 - transitivePeerDependencies: - - supports-color - - requires-port@1.0.0: {} - - resolve-cwd@3.0.0: - dependencies: - resolve-from: 5.0.0 - - resolve-from@4.0.0: {} - - resolve-from@5.0.0: {} - - resolve.exports@2.0.3: {} - - resolve@1.22.10: - dependencies: - is-core-module: 2.16.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - reusify@1.1.0: {} - - rimraf@2.7.1: - dependencies: - glob: 7.2.3 - - rimraf@3.0.2: - dependencies: - glob: 7.2.3 - - rope-sequence@1.3.4: {} - - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - - safe-array-concat@1.1.3: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - has-symbols: 1.1.0 - isarray: 2.0.5 - - safe-buffer@5.2.1: {} - - safe-push-apply@1.0.0: - dependencies: - es-errors: 1.3.0 - isarray: 2.0.5 - - safe-regex-test@1.1.0: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-regex: 1.2.1 - - safe-stable-stringify@2.5.0: {} - - safer-buffer@2.1.2: {} - - sander@0.5.1: - dependencies: - es6-promise: 3.3.1 - graceful-fs: 4.2.11 - mkdirp: 0.5.6 - rimraf: 2.7.1 - - saxes@6.0.0: - dependencies: - xmlchars: 2.2.0 - - scule@1.3.0: {} - - semver@6.3.1: {} - - semver@7.7.3: {} - - set-function-length@1.2.2: - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - function-bind: 1.1.2 - get-intrinsic: 1.3.0 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - - set-function-name@2.0.2: - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - functions-have-names: 1.2.3 - has-property-descriptors: 1.0.2 - - set-proto@1.0.0: - dependencies: - dunder-proto: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - side-channel-list@1.0.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - - side-channel-map@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - - side-channel-weakmap@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - side-channel-map: 1.0.1 - - side-channel@1.1.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - side-channel-list: 1.0.0 - side-channel-map: 1.0.1 - side-channel-weakmap: 1.0.2 - - signal-exit@3.0.7: {} - - sisteransi@1.0.5: {} - - slash@3.0.0: {} - - snappyjs@0.7.0: {} - - sorcery@0.11.1: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - buffer-crc32: 1.0.0 - minimist: 1.2.8 - sander: 0.5.1 - - source-map-js@1.2.1: {} - - source-map-support@0.5.13: - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - - source-map@0.6.1: {} - - sprintf-js@1.0.3: {} - - stack-trace@0.0.10: {} - - stack-utils@2.0.6: - dependencies: - escape-string-regexp: 2.0.0 - - stop-iteration-iterator@1.1.0: - dependencies: - es-errors: 1.3.0 - internal-slot: 1.1.0 - - string-length@4.0.2: - dependencies: - char-regex: 1.0.2 - strip-ansi: 6.0.1 - - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - string.prototype.trim@1.2.10: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-data-property: 1.1.4 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-object-atoms: 1.1.1 - has-property-descriptors: 1.0.2 - - string.prototype.trimend@1.0.9: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - - string.prototype.trimstart@1.0.8: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - - string_decoder@1.3.0: - dependencies: - safe-buffer: 5.2.1 - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - strip-bom@3.0.0: {} - - strip-bom@4.0.0: {} - - strip-final-newline@2.0.0: {} - - strip-indent@3.0.0: - dependencies: - min-indent: 1.0.1 - - strip-json-comments@3.1.1: {} - - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - supports-color@8.1.1: - dependencies: - has-flag: 4.0.0 - - supports-preserve-symlinks-flag@1.0.0: {} - - svelte-eslint-parser@0.33.1(svelte@4.2.20): - dependencies: - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 - postcss: 8.5.6 - postcss-scss: 4.0.9(postcss@8.5.6) - optionalDependencies: - svelte: 4.2.20 - - svelte-eslint-parser@0.43.0(svelte@4.2.20): - dependencies: - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 - postcss: 8.5.6 - postcss-scss: 4.0.9(postcss@8.5.6) - optionalDependencies: - svelte: 4.2.20 - - svelte-preprocess@5.1.4(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(postcss@8.5.6)(svelte@4.2.20)(typescript@5.9.3): - dependencies: - '@types/pug': 2.0.10 - detect-indent: 6.1.0 - magic-string: 0.30.19 - sorcery: 0.11.1 - strip-indent: 3.0.0 - svelte: 4.2.20 - optionalDependencies: - '@babel/core': 7.28.4 - postcss: 8.5.6 - postcss-load-config: 3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - typescript: 5.9.3 - - svelte2tsx@0.7.45(svelte@4.2.20)(typescript@5.9.3): - dependencies: - dedent-js: 1.0.1 - scule: 1.3.0 - svelte: 4.2.20 - typescript: 5.9.3 - - svelte@4.2.20: - dependencies: - '@ampproject/remapping': 2.3.0 - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 - '@types/estree': 1.0.8 - acorn: 8.15.0 - aria-query: 5.3.2 - axobject-query: 4.1.0 - code-red: 1.0.4 - css-tree: 2.3.1 - estree-walker: 3.0.3 - is-reference: 3.0.3 - locate-character: 3.0.0 - magic-string: 0.30.19 - periscopic: 3.1.0 - - symbol-tree@3.2.4: {} - - test-exclude@6.0.0: - dependencies: - '@istanbuljs/schema': 0.1.3 - glob: 7.2.3 - minimatch: 3.1.2 - - text-hex@1.0.0: {} - - text-table@0.2.0: {} - - tmpl@1.0.5: {} - - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - - toposort@2.0.2: {} - - tough-cookie@4.1.4: - dependencies: - psl: 1.15.0 - punycode: 2.3.1 - universalify: 0.2.0 - url-parse: 1.5.10 - - tr46@0.0.3: {} - - tr46@3.0.0: - dependencies: - punycode: 2.3.1 - - triple-beam@1.4.1: {} - - ts-api-utils@1.4.3(typescript@5.9.3): - dependencies: - typescript: 5.9.3 - - ts-jest@29.4.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.11)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3): - dependencies: - bs-logger: 0.2.6 - fast-json-stable-stringify: 2.1.0 - handlebars: 4.7.8 - jest: 29.7.0(@types/node@22.18.10)(ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3)) - json5: 2.2.3 - lodash.memoize: 4.1.2 - make-error: 1.3.6 - semver: 7.7.3 - type-fest: 4.41.0 - typescript: 5.9.3 - yargs-parser: 21.1.1 - optionalDependencies: - '@babel/core': 7.28.4 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.28.4) - esbuild: 0.25.11 - jest-util: 29.7.0 - - ts-node@10.9.2(@types/node@22.18.10)(typescript@5.9.3): - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.11 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.4 - '@types/node': 22.18.10 - acorn: 8.15.0 - acorn-walk: 8.3.4 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.2 - make-error: 1.3.6 - typescript: 5.9.3 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - - tsconfig-paths@3.15.0: - dependencies: - '@types/json5': 0.0.29 - json5: 1.0.2 - minimist: 1.2.8 - strip-bom: 3.0.0 - - tslib@2.8.1: {} - - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - - type-detect@4.0.8: {} - - type-fest@0.20.2: {} - - type-fest@0.21.3: {} - - type-fest@4.41.0: {} - - typed-array-buffer@1.0.3: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-typed-array: 1.1.15 - - typed-array-byte-length@1.0.3: - dependencies: - call-bind: 1.0.8 - for-each: 0.3.5 - gopd: 1.2.0 - has-proto: 1.2.0 - is-typed-array: 1.1.15 - - typed-array-byte-offset@1.0.4: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.8 - for-each: 0.3.5 - gopd: 1.2.0 - has-proto: 1.2.0 - is-typed-array: 1.1.15 - reflect.getprototypeof: 1.0.10 - - typed-array-length@1.0.7: - dependencies: - call-bind: 1.0.8 - for-each: 0.3.5 - gopd: 1.2.0 - is-typed-array: 1.1.15 - possible-typed-array-names: 1.1.0 - reflect.getprototypeof: 1.0.10 - - typescript@5.9.3: {} - - uc.micro@2.1.0: {} - - uglify-js@3.19.3: - optional: true - - unbox-primitive@1.1.0: - dependencies: - call-bound: 1.0.4 - has-bigints: 1.1.0 - has-symbols: 1.1.0 - which-boxed-primitive: 1.1.1 - - undici-types@6.21.0: {} - - universalify@0.2.0: {} - - universalify@2.0.1: {} - - update-browserslist-db@1.1.3(browserslist@4.26.3): - dependencies: - browserslist: 4.26.3 - escalade: 3.2.0 - picocolors: 1.1.1 - - uri-js@4.4.1: - dependencies: - punycode: 2.3.1 - - url-parse@1.5.10: - dependencies: - querystringify: 2.2.0 - requires-port: 1.0.0 - - util-deprecate@1.0.2: {} - - uuid@8.3.2: {} - - uuid@9.0.1: {} - - v8-compile-cache-lib@3.0.1: {} - - v8-to-istanbul@9.3.0: - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - '@types/istanbul-lib-coverage': 2.0.6 - convert-source-map: 2.0.0 - - w3c-keyname@2.2.8: {} - - w3c-xmlserializer@4.0.0: - dependencies: - xml-name-validator: 4.0.0 - - walker@1.0.8: - dependencies: - makeerror: 1.0.12 - - webidl-conversions@3.0.1: {} - - webidl-conversions@7.0.0: {} - - whatwg-encoding@2.0.0: - dependencies: - iconv-lite: 0.6.3 - - whatwg-mimetype@3.0.0: {} - - whatwg-url@11.0.0: - dependencies: - tr46: 3.0.0 - webidl-conversions: 7.0.0 - - whatwg-url@5.0.0: - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - - which-boxed-primitive@1.1.1: - dependencies: - is-bigint: 1.1.0 - is-boolean-object: 1.2.2 - is-number-object: 1.1.1 - is-string: 1.1.1 - is-symbol: 1.1.1 - - which-builtin-type@1.2.1: - dependencies: - call-bound: 1.0.4 - function.prototype.name: 1.1.8 - has-tostringtag: 1.0.2 - is-async-function: 2.1.1 - is-date-object: 1.1.0 - is-finalizationregistry: 1.1.1 - is-generator-function: 1.1.2 - is-regex: 1.2.1 - is-weakref: 1.1.1 - isarray: 2.0.5 - which-boxed-primitive: 1.1.1 - which-collection: 1.0.2 - which-typed-array: 1.1.19 - - which-collection@1.0.2: - dependencies: - is-map: 2.0.3 - is-set: 2.0.3 - is-weakmap: 2.0.2 - is-weakset: 2.0.4 - - which-typed-array@1.1.19: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.8 - call-bound: 1.0.4 - for-each: 0.3.5 - get-proto: 1.0.1 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - winston-daily-rotate-file@5.0.0(winston@3.18.3): - dependencies: - file-stream-rotator: 0.6.1 - object-hash: 3.0.0 - triple-beam: 1.4.1 - winston: 3.18.3 - winston-transport: 4.9.0 - - winston-transport@4.9.0: - dependencies: - logform: 2.7.0 - readable-stream: 3.6.2 - triple-beam: 1.4.1 - - winston@3.18.3: - dependencies: - '@colors/colors': 1.6.0 - '@dabh/diagnostics': 2.0.8 - async: 3.2.6 - is-stream: 2.0.1 - logform: 2.7.0 - one-time: 1.0.0 - readable-stream: 3.6.2 - safe-stable-stringify: 2.5.0 - stack-trace: 0.0.10 - triple-beam: 1.4.1 - winston-transport: 4.9.0 - - word-wrap@1.2.5: {} - - wordwrap@1.0.0: {} - - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - wrappy@1.0.2: {} - - write-file-atomic@4.0.2: - dependencies: - imurmurhash: 0.1.4 - signal-exit: 3.0.7 - - ws@8.18.3: {} - - xml-name-validator@4.0.0: {} - - xmlchars@2.2.0: {} - - xtend@4.0.2: {} - - y-prosemirror@1.3.7(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27): - dependencies: - lib0: 0.2.114 - prosemirror-model: 1.25.3 - prosemirror-state: 1.4.3 - prosemirror-view: 1.41.3 - y-protocols: 1.0.6(yjs@13.6.27) - yjs: 13.6.27 - - y-protocols@1.0.6(yjs@13.6.27): - dependencies: - lib0: 0.2.114 - yjs: 13.6.27 - - y18n@5.0.8: {} - - yallist@3.1.1: {} - - yaml@1.10.2: {} - - yargs-parser@21.1.1: {} - - yargs@17.7.2: - dependencies: - cliui: 8.0.1 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 - - yjs@13.6.27: - dependencies: - lib0: 0.2.114 - - yn@3.1.1: {} - - yocto-queue@0.1.0: {} - - zeed-dom@0.15.1: - dependencies: - css-what: 6.2.2 - entities: 5.0.0 diff --git a/foundations/core/common/config/rush/repo-state.json b/foundations/core/common/config/rush/repo-state.json deleted file mode 100644 index 0e7b144099..0000000000 --- a/foundations/core/common/config/rush/repo-state.json +++ /dev/null @@ -1,4 +0,0 @@ -// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. -{ - "preferredVersionsHash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f" -} diff --git a/foundations/core/common/config/rush/rush-plugins.json b/foundations/core/common/config/rush/rush-plugins.json deleted file mode 100644 index 752e373213..0000000000 --- a/foundations/core/common/config/rush/rush-plugins.json +++ /dev/null @@ -1,29 +0,0 @@ -/** - * This configuration file manages Rush's plugin feature. - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush-plugins.schema.json", - "plugins": [ - /** - * Each item configures a plugin to be loaded by Rush. - */ - // { - // /** - // * The name of the NPM package that provides the plugin. - // */ - // "packageName": "@scope/my-rush-plugin", - // /** - // * The name of the plugin. This can be found in the "pluginName" - // * field of the "rush-plugin-manifest.json" file in the NPM package folder. - // */ - // "pluginName": "my-plugin-name", - // /** - // * The name of a Rush autoinstaller that will be used for installation, which - // * can be created using "rush init-autoinstaller". Add the plugin's NPM package - // * to the package.json "dependencies" of your autoinstaller, then run - // * "rush update-autoinstaller". - // */ - // "autoinstallerName": "rush-plugins" - // } - ] -} \ No newline at end of file diff --git a/foundations/core/common/config/rush/subspaces.json b/foundations/core/common/config/rush/subspaces.json deleted file mode 100644 index d3c3ae8c51..0000000000 --- a/foundations/core/common/config/rush/subspaces.json +++ /dev/null @@ -1,35 +0,0 @@ -/** - * This configuration file manages the experimental "subspaces" feature for Rush, - * which allows multiple PNPM lockfiles to be used in a single Rush workspace. - * For full documentation, please see https://rushjs.io - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/subspaces.schema.json", - - /** - * Set this flag to "true" to enable usage of subspaces. - */ - "subspacesEnabled": false, - - /** - * (DEPRECATED) This is a temporary workaround for migrating from an earlier prototype - * of this feature: https://github.com/microsoft/rushstack/pull/3481 - * It allows subspaces with only one project to store their config files in the project folder. - */ - "splitWorkspaceCompatibility": false, - - /** - * When a command such as "rush update" is invoked without the "--subspace" or "--to" - * parameters, Rush will install all subspaces. In a huge monorepo with numerous subspaces, - * this would be extremely slow. Set "preventSelectingAllSubspaces" to true to avoid this - * mistake by always requiring selection parameters for commands such as "rush update". - */ - "preventSelectingAllSubspaces": false, - - /** - * The list of subspace names, which should be lowercase alphanumeric words separated by - * hyphens, for example "my-subspace". The corresponding config files will have paths - * such as "common/config/subspaces/my-subspace/package-lock.yaml". - */ - "subspaceNames": [] -} diff --git a/foundations/core/common/config/rush/version-policies.json b/foundations/core/common/config/rush/version-policies.json deleted file mode 100644 index 00501e9e33..0000000000 --- a/foundations/core/common/config/rush/version-policies.json +++ /dev/null @@ -1,102 +0,0 @@ -/** - * This is configuration file is used for advanced publishing configurations with Rush. - * More documentation is available on the Rush website: https://rushjs.io - */ - -/** - * A list of version policy definitions. A "version policy" is a custom package versioning - * strategy that affects "rush change", "rush version", and "rush publish". The strategy applies - * to a set of projects that are specified using the "versionPolicyName" field in rush.json. - */ -[ - // { - // /** - // * (Required) Indicates the kind of version policy being defined ("lockStepVersion" or "individualVersion"). - // * - // * The "lockStepVersion" mode specifies that the projects will use "lock-step versioning". This - // * strategy is appropriate for a set of packages that act as selectable components of a - // * unified product. The entire set of packages are always published together, and always share - // * the same NPM version number. When the packages depend on other packages in the set, the - // * SemVer range is usually restricted to a single version. - // */ - // "definitionName": "lockStepVersion", - // - // /** - // * (Required) The name that will be used for the "versionPolicyName" field in rush.json. - // * This name is also used command-line parameters such as "--version-policy" - // * and "--to-version-policy". - // */ - // "policyName": "MyBigFramework", - // - // /** - // * (Required) The current version. All packages belonging to the set should have this version - // * in the current branch. When bumping versions, Rush uses this to determine the next version. - // * (The "version" field in package.json is NOT considered.) - // */ - // "version": "1.0.0", - // - // /** - // * (Required) The type of bump that will be performed when publishing the next release. - // * When creating a release branch in Git, this field should be updated according to the - // * type of release. - // * - // * Valid values are: "prerelease", "preminor", "minor", "patch", "major" - // */ - // "nextBump": "prerelease", - // - // /** - // * (Optional) If specified, all packages in the set share a common CHANGELOG.md file. - // * This file is stored with the specified "main" project, which must be a member of the set. - // * - // * If this field is omitted, then a separate CHANGELOG.md file will be maintained for each - // * package in the set. - // */ - // "mainProject": "my-app", - // - // /** - // * (Optional) If enabled, the "rush change" command will prompt the user for their email address - // * and include it in the JSON change files. If an organization maintains multiple repos, tracking - // * this contact information may be useful for a service that automatically upgrades packages and - // * needs to notify engineers whose change may be responsible for a downstream build break. It might - // * also be useful for crediting contributors. Rush itself does not do anything with the collected - // * email addresses. The default value is "false". - // */ - // // "includeEmailInChangeFile": true - // }, - // - { - /** - * (Required) Indicates the kind of version policy being defined ("lockStepVersion" or "individualVersion"). - * - * The "individualVersion" mode specifies that the projects will use "individual versioning". - * This is the typical NPM model where each package has an independent version number - * and CHANGELOG.md file. Although a single CI definition is responsible for publishing the - * packages, they otherwise don't have any special relationship. The version bumping will - * depend on how developers answer the "rush change" questions for each package that - * is changed. - */ - "definitionName": "individualVersion", - - "policyName": "Huly.core", - - /** - * (Optional) This can be used to enforce that all packages in the set must share a common - * major version number, e.g. because they are from the same major release branch. - * It can also be used to discourage people from accidentally making "MAJOR" SemVer changes - * inappropriately. The minor/patch version parts will be bumped independently according - * to the types of changes made to each project, according to the "rush change" command. - */ - "lockedMajor": 0.7, - - /** - * (Optional) When publishing is managed by Rush, by default the "rush change" command will - * request changes for any projects that are modified by a pull request. These change entries - * will produce a CHANGELOG.md file. If you author your CHANGELOG.md manually or announce updates - * in some other way, set "exemptFromRushChange" to true to tell "rush change" to ignore the projects - * belonging to this version policy. - */ - "exemptFromRushChange": false - - // "includeEmailInChangeFile": true - } -] diff --git a/foundations/core/common/git-hooks/commit-msg.sample b/foundations/core/common/git-hooks/commit-msg.sample deleted file mode 100644 index 59cacb80ca..0000000000 --- a/foundations/core/common/git-hooks/commit-msg.sample +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/sh -# -# This is an example Git hook for use with Rush. To enable this hook, rename this file -# to "commit-msg" and then run "rush install", which will copy it from common/git-hooks -# to the .git/hooks folder. -# -# TO LEARN MORE ABOUT GIT HOOKS -# -# The Git documentation is here: https://git-scm.com/docs/githooks -# Some helpful resources: https://githooks.com -# -# ABOUT THIS EXAMPLE -# -# The commit-msg hook is called by "git commit" with one argument, the name of the file -# that has the commit message. The hook should exit with non-zero status after issuing -# an appropriate message if it wants to stop the commit. The hook is allowed to edit -# the commit message file. - -# This example enforces that commit message should contain a minimum amount of -# description text. -if [ `cat $1 | wc -w` -lt 3 ]; then - echo "" - echo "Invalid commit message: The message must contain at least 3 words." - exit 1 -fi diff --git a/foundations/core/common/scripts/generate-coverage-html.js b/foundations/core/common/scripts/generate-coverage-html.js deleted file mode 100644 index 6bc54b62cb..0000000000 --- a/foundations/core/common/scripts/generate-coverage-html.js +++ /dev/null @@ -1,153 +0,0 @@ -#!/usr/bin/env node -const fs = require('fs') -const path = require('path') - -const [, , inFile = 'coverage/lcov.info', outDir = 'coverage/html'] = process.argv - -if (!fs.existsSync(inFile)) { - console.error('Input lcov not found:', inFile) - process.exit(1) -} - -const lcovParse = require('lcov-parse') -const libCoverage = require('istanbul-lib-coverage') -const reports = require('istanbul-reports') -const libReport = require('istanbul-lib-report') - -const data = fs.readFileSync(inFile, 'utf8') - -// build repo file index to resolve source files -const root = process.cwd() -const ignoreDirs = new Set(['node_modules', '.git', 'coverage', 'lib', 'dist', 'types', '.rush', 'temp', 'pnpm-store']) -const repoFiles = [] -function walk(dir) { - const items = fs.readdirSync(dir, { withFileTypes: true }) - for (const it of items) { - if (it.isDirectory()) { - if (ignoreDirs.has(it.name)) continue - if (it.name.startsWith('.')) continue - try { - walk(path.join(dir, it.name)) - } catch (e) {} - } else if (it.isFile()) { - repoFiles.push(path.join(dir, it.name)) - } - } -} -try { - walk(root) -} catch (e) {} - -lcovParse(data, (err, parsed) => { - if (err) { - console.error('lcov-parse error:', err) - process.exit(1) - } - - const map = libCoverage.createCoverageMap({}) - for (const file of parsed) { - // parsed entries include 'file', 'lines', 'functions', 'branches' - const coverage = { - path: file.file, - statementMap: {}, - fnMap: {}, - branchMap: {}, - s: {}, - f: {}, - b: {} - } - - // The lcov parser gives line coverage data; create synthetic statement entries per line - if (file.lines && file.lines.details) { - let idx = 0 - for (const d of file.lines.details) { - idx++ - const key = String(idx) - coverage.statementMap[key] = { start: { line: d.line, column: 0 }, end: { line: d.line, column: 0 } } - coverage.s[key] = d.hit - } - } - - // functions and branches are ignored for more accurate tools; keep minimal - map.addFileCoverage(coverage) - } - - // custom source finder: try absolute, repo-relative, and suffix matches - const sourceFinder = (filePath) => { - try { - if (!global.__seenPaths) global.__seenPaths = [] - if (global.__seenPaths.length < 500) global.__seenPaths.push(filePath) - if (global.__seenPaths.length === 500 && !global.__seenLogged) { - console.error('sourceFinder seen paths (sample):\n', global.__seenPaths.join('\n')) - global.__seenLogged = true - } - if (global.__seenPaths.length <= 200) console.error('sourceFinder request:', filePath) - } catch (e) {} - try { - if (path.isAbsolute(filePath) && fs.existsSync(filePath)) return fs.readFileSync(filePath, 'utf8') - const abs1 = path.resolve(root, filePath) - if (fs.existsSync(abs1)) return fs.readFileSync(abs1, 'utf8') - // try suffix match - const found = repoFiles.find((p) => p.endsWith(path.sep + filePath) || p.endsWith(filePath)) - if (found) return fs.readFileSync(found, 'utf8') - // debug unresolved - if (!found) { - try { - if (!global.__unresolved) global.__unresolved = new Set() - if (global.__unresolved.size < 200) global.__unresolved.add(filePath) - } catch (e) {} - } - } catch (e) { - // ignore and return null below - } - return null - } - - const context = libReport.createContext({ dir: outDir, coverageMap: map, sourceFinder }) - const report = reports.create('html', {}) - report.execute(context) - if (global.__unresolved && global.__unresolved.size) { - console.error('Unresolved filePath samples:\n', Array.from(global.__unresolved).slice(0, 50).join('\n')) - } - console.log('HTML report generated in', outDir) - // Post-process HTML files: if any report page contains the 'Unable to lookup source' placeholder, - // replace it with the actual source file contents when we can resolve it. - try { - for (const file of parsed) { - const srcAbs = file.file - // normalize key as used by report (from last '/src/' onward) if present - let key - const idx = srcAbs.lastIndexOf(path.sep + 'src' + path.sep) - if (idx !== -1) key = srcAbs.slice(idx + 1) - else key = path.basename(srcAbs) - - const htmlPath = path.join(outDir, key + '.html') - if (!fs.existsSync(htmlPath)) continue - let html = fs.readFileSync(htmlPath, 'utf8') - if (!html.includes('Unable to lookup source')) continue - // read source - let src - try { - src = fs.readFileSync(srcAbs, 'utf8') - } catch (e) { - src = null - } - if (!src) { - // try suffix match in repoFiles - const found = repoFiles.find((p) => p.endsWith(path.sep + key) || p.endsWith(key)) - if (found) src = fs.readFileSync(found, 'utf8') - } - if (!src) continue - // escape HTML - const esc = src.replace(/&/g, '&').replace(//g, '>') - // replace the first prettyprint

...
block that contains 'Unable to lookup source' - html = html.replace( - /
${esc}
` - ) - fs.writeFileSync(htmlPath, html, 'utf8') - } - } catch (e) { - console.error('post-process html error', e) - } -}) diff --git a/foundations/core/common/scripts/install-run-rush-pnpm.js b/foundations/core/common/scripts/install-run-rush-pnpm.js deleted file mode 100644 index 2356649f4e..0000000000 --- a/foundations/core/common/scripts/install-run-rush-pnpm.js +++ /dev/null @@ -1,31 +0,0 @@ -// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED. -// -// This script is intended for usage in an automated build environment where the Rush command may not have -// been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush -// specified in the rush.json configuration file (if not already installed), and then pass a command-line to the -// rush-pnpm command. -// -// An example usage would be: -// -// node common/scripts/install-run-rush-pnpm.js pnpm-command -// -// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/ -// -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See the @microsoft/rush package's LICENSE file for details. - -/******/ (() => { // webpackBootstrap -/******/ "use strict"; -var __webpack_exports__ = {}; -/*!*****************************************************!*\ - !*** ./lib-esnext/scripts/install-run-rush-pnpm.js ***! - \*****************************************************/ - -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. -require('./install-run-rush'); -//# sourceMappingURL=install-run-rush-pnpm.js.map -module.exports = __webpack_exports__; -/******/ })() -; -//# sourceMappingURL=install-run-rush-pnpm.js.map \ No newline at end of file diff --git a/foundations/core/common/scripts/install-run-rush.js b/foundations/core/common/scripts/install-run-rush.js deleted file mode 100644 index ef1d697f9c..0000000000 --- a/foundations/core/common/scripts/install-run-rush.js +++ /dev/null @@ -1,218 +0,0 @@ -// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED. -// -// This script is intended for usage in an automated build environment where the Rush command may not have -// been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush -// specified in the rush.json configuration file (if not already installed), and then pass a command-line to it. -// An example usage would be: -// -// node common/scripts/install-run-rush.js install -// -// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/ -// -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See the @microsoft/rush package's LICENSE file for details. - -/******/ (() => { // webpackBootstrap -/******/ "use strict"; -/******/ var __webpack_modules__ = ({ - -/***/ 16928: -/*!***********************!*\ - !*** external "path" ***! - \***********************/ -/***/ ((module) => { - -module.exports = require("path"); - -/***/ }), - -/***/ 179896: -/*!*********************!*\ - !*** external "fs" ***! - \*********************/ -/***/ ((module) => { - -module.exports = require("fs"); - -/***/ }) - -/******/ }); -/************************************************************************/ -/******/ // The module cache -/******/ var __webpack_module_cache__ = {}; -/******/ -/******/ // The require function -/******/ function __webpack_require__(moduleId) { -/******/ // Check if module is in cache -/******/ var cachedModule = __webpack_module_cache__[moduleId]; -/******/ if (cachedModule !== undefined) { -/******/ return cachedModule.exports; -/******/ } -/******/ // Create a new module (and put it into the cache) -/******/ var module = __webpack_module_cache__[moduleId] = { -/******/ // no module.id needed -/******/ // no module.loaded needed -/******/ exports: {} -/******/ }; -/******/ -/******/ // Execute the module function -/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); -/******/ -/******/ // Return the exports of the module -/******/ return module.exports; -/******/ } -/******/ -/************************************************************************/ -/******/ /* webpack/runtime/compat get default export */ -/******/ (() => { -/******/ // getDefaultExport function for compatibility with non-harmony modules -/******/ __webpack_require__.n = (module) => { -/******/ var getter = module && module.__esModule ? -/******/ () => (module['default']) : -/******/ () => (module); -/******/ __webpack_require__.d(getter, { a: getter }); -/******/ return getter; -/******/ }; -/******/ })(); -/******/ -/******/ /* webpack/runtime/define property getters */ -/******/ (() => { -/******/ // define getter functions for harmony exports -/******/ __webpack_require__.d = (exports, definition) => { -/******/ for(var key in definition) { -/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { -/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); -/******/ } -/******/ } -/******/ }; -/******/ })(); -/******/ -/******/ /* webpack/runtime/hasOwnProperty shorthand */ -/******/ (() => { -/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) -/******/ })(); -/******/ -/******/ /* webpack/runtime/make namespace object */ -/******/ (() => { -/******/ // define __esModule on exports -/******/ __webpack_require__.r = (exports) => { -/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { -/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); -/******/ } -/******/ Object.defineProperty(exports, '__esModule', { value: true }); -/******/ }; -/******/ })(); -/******/ -/************************************************************************/ -var __webpack_exports__ = {}; -// This entry needs to be wrapped in an IIFE because it needs to be isolated against other modules in the chunk. -(() => { -/*!************************************************!*\ - !*** ./lib-esnext/scripts/install-run-rush.js ***! - \************************************************/ -__webpack_require__.r(__webpack_exports__); -/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! path */ 16928); -/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! fs */ 179896); -/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_1__); -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. -/* eslint-disable no-console */ - - -const { installAndRun, findRushJsonFolder, RUSH_JSON_FILENAME, runWithErrorAndStatusCode } = require('./install-run'); -const PACKAGE_NAME = '@microsoft/rush'; -const RUSH_PREVIEW_VERSION = 'RUSH_PREVIEW_VERSION'; -const INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE = 'INSTALL_RUN_RUSH_LOCKFILE_PATH'; -function _getRushVersion(logger) { - const rushPreviewVersion = process.env[RUSH_PREVIEW_VERSION]; - if (rushPreviewVersion !== undefined) { - logger.info(`Using Rush version from environment variable ${RUSH_PREVIEW_VERSION}=${rushPreviewVersion}`); - return rushPreviewVersion; - } - const rushJsonFolder = findRushJsonFolder(); - const rushJsonPath = path__WEBPACK_IMPORTED_MODULE_0__.join(rushJsonFolder, RUSH_JSON_FILENAME); - try { - const rushJsonContents = fs__WEBPACK_IMPORTED_MODULE_1__.readFileSync(rushJsonPath, 'utf-8'); - // Use a regular expression to parse out the rushVersion value because rush.json supports comments, - // but JSON.parse does not and we don't want to pull in more dependencies than we need to in this script. - const rushJsonMatches = rushJsonContents.match(/\"rushVersion\"\s*\:\s*\"([0-9a-zA-Z.+\-]+)\"/); - return rushJsonMatches[1]; - } - catch (e) { - throw new Error(`Unable to determine the required version of Rush from ${RUSH_JSON_FILENAME} (${rushJsonFolder}). ` + - `The 'rushVersion' field is either not assigned in ${RUSH_JSON_FILENAME} or was specified ` + - 'using an unexpected syntax.'); - } -} -function _getBin(scriptName) { - switch (scriptName.toLowerCase()) { - case 'install-run-rush-pnpm.js': - return 'rush-pnpm'; - case 'install-run-rushx.js': - return 'rushx'; - default: - return 'rush'; - } -} -function _run() { - const [nodePath /* Ex: /bin/node */, scriptPath /* /repo/common/scripts/install-run-rush.js */, ...packageBinArgs /* [build, --to, myproject] */] = process.argv; - // Detect if this script was directly invoked, or if the install-run-rushx script was invokved to select the - // appropriate binary inside the rush package to run - const scriptName = path__WEBPACK_IMPORTED_MODULE_0__.basename(scriptPath); - const bin = _getBin(scriptName); - if (!nodePath || !scriptPath) { - throw new Error('Unexpected exception: could not detect node path or script path'); - } - let commandFound = false; - let logger = { info: console.log, error: console.error }; - for (const arg of packageBinArgs) { - if (arg === '-q' || arg === '--quiet') { - // The -q/--quiet flag is supported by both `rush` and `rushx`, and will suppress - // any normal informational/diagnostic information printed during startup. - // - // To maintain the same user experience, the install-run* scripts pass along this - // flag but also use it to suppress any diagnostic information normally printed - // to stdout. - logger = { - info: () => { }, - error: console.error - }; - } - else if (!arg.startsWith('-') || arg === '-h' || arg === '--help') { - // We either found something that looks like a command (i.e. - doesn't start with a "-"), - // or we found the -h/--help flag, which can be run without a command - commandFound = true; - } - } - if (!commandFound) { - console.log(`Usage: ${scriptName} [args...]`); - if (scriptName === 'install-run-rush-pnpm.js') { - console.log(`Example: ${scriptName} pnpm-command`); - } - else if (scriptName === 'install-run-rush.js') { - console.log(`Example: ${scriptName} build --to myproject`); - } - else { - console.log(`Example: ${scriptName} custom-command`); - } - process.exit(1); - } - runWithErrorAndStatusCode(logger, () => { - const version = _getRushVersion(logger); - logger.info(`The ${RUSH_JSON_FILENAME} configuration requests Rush version ${version}`); - const lockFilePath = process.env[INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE]; - if (lockFilePath) { - logger.info(`Found ${INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE}="${lockFilePath}", installing with lockfile.`); - } - return installAndRun(logger, PACKAGE_NAME, version, bin, packageBinArgs, lockFilePath); - }); -} -_run(); -//# sourceMappingURL=install-run-rush.js.map -})(); - -module.exports = __webpack_exports__; -/******/ })() -; -//# sourceMappingURL=install-run-rush.js.map \ No newline at end of file diff --git a/foundations/core/common/scripts/install-run-rushx.js b/foundations/core/common/scripts/install-run-rushx.js deleted file mode 100644 index 6581521f3c..0000000000 --- a/foundations/core/common/scripts/install-run-rushx.js +++ /dev/null @@ -1,31 +0,0 @@ -// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED. -// -// This script is intended for usage in an automated build environment where the Rush command may not have -// been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush -// specified in the rush.json configuration file (if not already installed), and then pass a command-line to the -// rushx command. -// -// An example usage would be: -// -// node common/scripts/install-run-rushx.js custom-command -// -// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/ -// -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See the @microsoft/rush package's LICENSE file for details. - -/******/ (() => { // webpackBootstrap -/******/ "use strict"; -var __webpack_exports__ = {}; -/*!*************************************************!*\ - !*** ./lib-esnext/scripts/install-run-rushx.js ***! - \*************************************************/ - -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. -require('./install-run-rush'); -//# sourceMappingURL=install-run-rushx.js.map -module.exports = __webpack_exports__; -/******/ })() -; -//# sourceMappingURL=install-run-rushx.js.map \ No newline at end of file diff --git a/foundations/core/common/scripts/install-run.js b/foundations/core/common/scripts/install-run.js deleted file mode 100644 index a35726bf16..0000000000 --- a/foundations/core/common/scripts/install-run.js +++ /dev/null @@ -1,778 +0,0 @@ -// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED. -// -// This script is intended for usage in an automated build environment where a Node tool may not have -// been preinstalled, or may have an unpredictable version. This script will automatically install the specified -// version of the specified tool (if not already installed), and then pass a command-line to it. -// An example usage would be: -// -// node common/scripts/install-run.js qrcode@1.2.2 qrcode https://rushjs.io -// -// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/ -// -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See the @microsoft/rush package's LICENSE file for details. - -/******/ (() => { // webpackBootstrap -/******/ "use strict"; -/******/ var __webpack_modules__ = ({ - -/***/ 16928: -/*!***********************!*\ - !*** external "path" ***! - \***********************/ -/***/ ((module) => { - -module.exports = require("path"); - -/***/ }), - -/***/ 179896: -/*!*********************!*\ - !*** external "fs" ***! - \*********************/ -/***/ ((module) => { - -module.exports = require("fs"); - -/***/ }), - -/***/ 370857: -/*!*********************!*\ - !*** external "os" ***! - \*********************/ -/***/ ((module) => { - -module.exports = require("os"); - -/***/ }), - -/***/ 535317: -/*!********************************!*\ - !*** external "child_process" ***! - \********************************/ -/***/ ((module) => { - -module.exports = require("child_process"); - -/***/ }), - -/***/ 832286: -/*!************************************************!*\ - !*** ./lib-esnext/utilities/npmrcUtilities.js ***! - \************************************************/ -/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { - -__webpack_require__.r(__webpack_exports__); -/* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ isVariableSetInNpmrcFile: () => (/* binding */ isVariableSetInNpmrcFile), -/* harmony export */ syncNpmrc: () => (/* binding */ syncNpmrc), -/* harmony export */ trimNpmrcFileLines: () => (/* binding */ trimNpmrcFileLines) -/* harmony export */ }); -/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! fs */ 179896); -/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! path */ 16928); -/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_1__); -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. -// IMPORTANT - do not use any non-built-in libraries in this file - - -/** - * This function reads the content for given .npmrc file path, and also trims - * unusable lines from the .npmrc file. - * - * @returns - * The text of the the .npmrc. - */ -// create a global _combinedNpmrc for cache purpose -const _combinedNpmrcMap = new Map(); -function _trimNpmrcFile(options) { - const { sourceNpmrcPath, linesToPrepend, linesToAppend, supportEnvVarFallbackSyntax } = options; - const combinedNpmrcFromCache = _combinedNpmrcMap.get(sourceNpmrcPath); - if (combinedNpmrcFromCache !== undefined) { - return combinedNpmrcFromCache; - } - let npmrcFileLines = []; - if (linesToPrepend) { - npmrcFileLines.push(...linesToPrepend); - } - if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(sourceNpmrcPath)) { - npmrcFileLines.push(...fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(sourceNpmrcPath).toString().split('\n')); - } - if (linesToAppend) { - npmrcFileLines.push(...linesToAppend); - } - npmrcFileLines = npmrcFileLines.map((line) => (line || '').trim()); - const resultLines = trimNpmrcFileLines(npmrcFileLines, process.env, supportEnvVarFallbackSyntax); - const combinedNpmrc = resultLines.join('\n'); - //save the cache - _combinedNpmrcMap.set(sourceNpmrcPath, combinedNpmrc); - return combinedNpmrc; -} -/** - * - * @param npmrcFileLines The npmrc file's lines - * @param env The environment variables object - * @param supportEnvVarFallbackSyntax Whether to support fallback values in the form of `${VAR_NAME:-fallback}` - * @returns - */ -function trimNpmrcFileLines(npmrcFileLines, env, supportEnvVarFallbackSyntax) { - var _a; - const resultLines = []; - // This finds environment variable tokens that look like "${VAR_NAME}" - const expansionRegExp = /\$\{([^\}]+)\}/g; - // Comment lines start with "#" or ";" - const commentRegExp = /^\s*[#;]/; - // Trim out lines that reference environment variables that aren't defined - for (let line of npmrcFileLines) { - let lineShouldBeTrimmed = false; - //remove spaces before or after key and value - line = line - .split('=') - .map((lineToTrim) => lineToTrim.trim()) - .join('='); - // Ignore comment lines - if (!commentRegExp.test(line)) { - const environmentVariables = line.match(expansionRegExp); - if (environmentVariables) { - for (const token of environmentVariables) { - /** - * Remove the leading "${" and the trailing "}" from the token - * - * ${nameString} -> nameString - * ${nameString-fallbackString} -> name-fallbackString - * ${nameString:-fallbackString} -> name:-fallbackString - */ - const nameWithFallback = token.substring(2, token.length - 1); - let environmentVariableName; - let fallback; - if (supportEnvVarFallbackSyntax) { - /** - * Get the environment variable name and fallback value. - * - * name fallback - * nameString -> nameString undefined - * nameString-fallbackString -> nameString fallbackString - * nameString:-fallbackString -> nameString fallbackString - */ - const matched = nameWithFallback.match(/^([^:-]+)(?:\:?-(.+))?$/); - // matched: [originStr, variableName, fallback] - environmentVariableName = (_a = matched === null || matched === void 0 ? void 0 : matched[1]) !== null && _a !== void 0 ? _a : nameWithFallback; - fallback = matched === null || matched === void 0 ? void 0 : matched[2]; - } - else { - environmentVariableName = nameWithFallback; - } - // Is the environment variable and fallback value defined. - if (!env[environmentVariableName] && !fallback) { - // No, so trim this line - lineShouldBeTrimmed = true; - break; - } - } - } - } - if (lineShouldBeTrimmed) { - // Example output: - // "; MISSING ENVIRONMENT VARIABLE: //my-registry.com/npm/:_authToken=${MY_AUTH_TOKEN}" - resultLines.push('; MISSING ENVIRONMENT VARIABLE: ' + line); - } - else { - resultLines.push(line); - } - } - return resultLines; -} -function _copyAndTrimNpmrcFile(options) { - const { logger, sourceNpmrcPath, targetNpmrcPath } = options; - logger.info(`Transforming ${sourceNpmrcPath}`); // Verbose - logger.info(` --> "${targetNpmrcPath}"`); - const combinedNpmrc = _trimNpmrcFile(options); - fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(targetNpmrcPath, combinedNpmrc); - return combinedNpmrc; -} -function syncNpmrc(options) { - const { sourceNpmrcFolder, targetNpmrcFolder, useNpmrcPublish, logger = { - // eslint-disable-next-line no-console - info: console.log, - // eslint-disable-next-line no-console - error: console.error - }, createIfMissing = false } = options; - const sourceNpmrcPath = path__WEBPACK_IMPORTED_MODULE_1__.join(sourceNpmrcFolder, !useNpmrcPublish ? '.npmrc' : '.npmrc-publish'); - const targetNpmrcPath = path__WEBPACK_IMPORTED_MODULE_1__.join(targetNpmrcFolder, '.npmrc'); - try { - if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(sourceNpmrcPath) || createIfMissing) { - // Ensure the target folder exists - if (!fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(targetNpmrcFolder)) { - fs__WEBPACK_IMPORTED_MODULE_0__.mkdirSync(targetNpmrcFolder, { recursive: true }); - } - return _copyAndTrimNpmrcFile({ - sourceNpmrcPath, - targetNpmrcPath, - logger, - ...options - }); - } - else if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(targetNpmrcPath)) { - // If the source .npmrc doesn't exist and there is one in the target, delete the one in the target - logger.info(`Deleting ${targetNpmrcPath}`); // Verbose - fs__WEBPACK_IMPORTED_MODULE_0__.unlinkSync(targetNpmrcPath); - } - } - catch (e) { - throw new Error(`Error syncing .npmrc file: ${e}`); - } -} -function isVariableSetInNpmrcFile(sourceNpmrcFolder, variableKey, supportEnvVarFallbackSyntax) { - const sourceNpmrcPath = `${sourceNpmrcFolder}/.npmrc`; - //if .npmrc file does not exist, return false directly - if (!fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(sourceNpmrcPath)) { - return false; - } - const trimmedNpmrcFile = _trimNpmrcFile({ sourceNpmrcPath, supportEnvVarFallbackSyntax }); - const variableKeyRegExp = new RegExp(`^${variableKey}=`, 'm'); - return trimmedNpmrcFile.match(variableKeyRegExp) !== null; -} -//# sourceMappingURL=npmrcUtilities.js.map - -/***/ }) - -/******/ }); -/************************************************************************/ -/******/ // The module cache -/******/ var __webpack_module_cache__ = {}; -/******/ -/******/ // The require function -/******/ function __webpack_require__(moduleId) { -/******/ // Check if module is in cache -/******/ var cachedModule = __webpack_module_cache__[moduleId]; -/******/ if (cachedModule !== undefined) { -/******/ return cachedModule.exports; -/******/ } -/******/ // Create a new module (and put it into the cache) -/******/ var module = __webpack_module_cache__[moduleId] = { -/******/ // no module.id needed -/******/ // no module.loaded needed -/******/ exports: {} -/******/ }; -/******/ -/******/ // Execute the module function -/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); -/******/ -/******/ // Return the exports of the module -/******/ return module.exports; -/******/ } -/******/ -/************************************************************************/ -/******/ /* webpack/runtime/compat get default export */ -/******/ (() => { -/******/ // getDefaultExport function for compatibility with non-harmony modules -/******/ __webpack_require__.n = (module) => { -/******/ var getter = module && module.__esModule ? -/******/ () => (module['default']) : -/******/ () => (module); -/******/ __webpack_require__.d(getter, { a: getter }); -/******/ return getter; -/******/ }; -/******/ })(); -/******/ -/******/ /* webpack/runtime/define property getters */ -/******/ (() => { -/******/ // define getter functions for harmony exports -/******/ __webpack_require__.d = (exports, definition) => { -/******/ for(var key in definition) { -/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { -/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); -/******/ } -/******/ } -/******/ }; -/******/ })(); -/******/ -/******/ /* webpack/runtime/hasOwnProperty shorthand */ -/******/ (() => { -/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) -/******/ })(); -/******/ -/******/ /* webpack/runtime/make namespace object */ -/******/ (() => { -/******/ // define __esModule on exports -/******/ __webpack_require__.r = (exports) => { -/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { -/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); -/******/ } -/******/ Object.defineProperty(exports, '__esModule', { value: true }); -/******/ }; -/******/ })(); -/******/ -/************************************************************************/ -var __webpack_exports__ = {}; -// This entry needs to be wrapped in an IIFE because it needs to be isolated against other modules in the chunk. -(() => { -/*!*******************************************!*\ - !*** ./lib-esnext/scripts/install-run.js ***! - \*******************************************/ -__webpack_require__.r(__webpack_exports__); -/* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ RUSH_JSON_FILENAME: () => (/* binding */ RUSH_JSON_FILENAME), -/* harmony export */ findRushJsonFolder: () => (/* binding */ findRushJsonFolder), -/* harmony export */ getNpmPath: () => (/* binding */ getNpmPath), -/* harmony export */ installAndRun: () => (/* binding */ installAndRun), -/* harmony export */ runWithErrorAndStatusCode: () => (/* binding */ runWithErrorAndStatusCode) -/* harmony export */ }); -/* harmony import */ var child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! child_process */ 535317); -/* harmony import */ var child_process__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(child_process__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! fs */ 179896); -/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var os__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! os */ 370857); -/* harmony import */ var os__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(os__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! path */ 16928); -/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_3__); -/* harmony import */ var _utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../utilities/npmrcUtilities */ 832286); -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. -/* eslint-disable no-console */ - - - - - -const RUSH_JSON_FILENAME = 'rush.json'; -const RUSH_TEMP_FOLDER_ENV_VARIABLE_NAME = 'RUSH_TEMP_FOLDER'; -const INSTALL_RUN_LOCKFILE_PATH_VARIABLE = 'INSTALL_RUN_LOCKFILE_PATH'; -const INSTALLED_FLAG_FILENAME = 'installed.flag'; -const NODE_MODULES_FOLDER_NAME = 'node_modules'; -const PACKAGE_JSON_FILENAME = 'package.json'; -/** - * Parse a package specifier (in the form of name\@version) into name and version parts. - */ -function _parsePackageSpecifier(rawPackageSpecifier) { - rawPackageSpecifier = (rawPackageSpecifier || '').trim(); - const separatorIndex = rawPackageSpecifier.lastIndexOf('@'); - let name; - let version = undefined; - if (separatorIndex === 0) { - // The specifier starts with a scope and doesn't have a version specified - name = rawPackageSpecifier; - } - else if (separatorIndex === -1) { - // The specifier doesn't have a version - name = rawPackageSpecifier; - } - else { - name = rawPackageSpecifier.substring(0, separatorIndex); - version = rawPackageSpecifier.substring(separatorIndex + 1); - } - if (!name) { - throw new Error(`Invalid package specifier: ${rawPackageSpecifier}`); - } - return { name, version }; -} -let _npmPath = undefined; -/** - * Get the absolute path to the npm executable - */ -function getNpmPath() { - if (!_npmPath) { - try { - if (_isWindows()) { - // We're on Windows - const whereOutput = child_process__WEBPACK_IMPORTED_MODULE_0__.execSync('where npm', { stdio: [] }).toString(); - const lines = whereOutput.split(os__WEBPACK_IMPORTED_MODULE_2__.EOL).filter((line) => !!line); - // take the last result, we are looking for a .cmd command - // see https://github.com/microsoft/rushstack/issues/759 - _npmPath = lines[lines.length - 1]; - } - else { - // We aren't on Windows - assume we're on *NIX or Darwin - _npmPath = child_process__WEBPACK_IMPORTED_MODULE_0__.execSync('command -v npm', { stdio: [] }).toString(); - } - } - catch (e) { - throw new Error(`Unable to determine the path to the NPM tool: ${e}`); - } - _npmPath = _npmPath.trim(); - if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(_npmPath)) { - throw new Error('The NPM executable does not exist'); - } - } - return _npmPath; -} -function _ensureFolder(folderPath) { - if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(folderPath)) { - const parentDir = path__WEBPACK_IMPORTED_MODULE_3__.dirname(folderPath); - _ensureFolder(parentDir); - fs__WEBPACK_IMPORTED_MODULE_1__.mkdirSync(folderPath); - } -} -/** - * Create missing directories under the specified base directory, and return the resolved directory. - * - * Does not support "." or ".." path segments. - * Assumes the baseFolder exists. - */ -function _ensureAndJoinPath(baseFolder, ...pathSegments) { - let joinedPath = baseFolder; - try { - for (let pathSegment of pathSegments) { - pathSegment = pathSegment.replace(/[\\\/]/g, '+'); - joinedPath = path__WEBPACK_IMPORTED_MODULE_3__.join(joinedPath, pathSegment); - if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(joinedPath)) { - fs__WEBPACK_IMPORTED_MODULE_1__.mkdirSync(joinedPath); - } - } - } - catch (e) { - throw new Error(`Error building local installation folder (${path__WEBPACK_IMPORTED_MODULE_3__.join(baseFolder, ...pathSegments)}): ${e}`); - } - return joinedPath; -} -function _getRushTempFolder(rushCommonFolder) { - const rushTempFolder = process.env[RUSH_TEMP_FOLDER_ENV_VARIABLE_NAME]; - if (rushTempFolder !== undefined) { - _ensureFolder(rushTempFolder); - return rushTempFolder; - } - else { - return _ensureAndJoinPath(rushCommonFolder, 'temp'); - } -} -/** - * Compare version strings according to semantic versioning. - * Returns a positive integer if "a" is a later version than "b", - * a negative integer if "b" is later than "a", - * and 0 otherwise. - */ -function _compareVersionStrings(a, b) { - const aParts = a.split(/[.-]/); - const bParts = b.split(/[.-]/); - const numberOfParts = Math.max(aParts.length, bParts.length); - for (let i = 0; i < numberOfParts; i++) { - if (aParts[i] !== bParts[i]) { - return (Number(aParts[i]) || 0) - (Number(bParts[i]) || 0); - } - } - return 0; -} -/** - * Resolve a package specifier to a static version - */ -function _resolvePackageVersion(logger, rushCommonFolder, { name, version }) { - if (!version) { - version = '*'; // If no version is specified, use the latest version - } - if (version.match(/^[a-zA-Z0-9\-\+\.]+$/)) { - // If the version contains only characters that we recognize to be used in static version specifiers, - // pass the version through - return version; - } - else { - // version resolves to - try { - const rushTempFolder = _getRushTempFolder(rushCommonFolder); - const sourceNpmrcFolder = path__WEBPACK_IMPORTED_MODULE_3__.join(rushCommonFolder, 'config', 'rush'); - (0,_utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__.syncNpmrc)({ - sourceNpmrcFolder, - targetNpmrcFolder: rushTempFolder, - logger, - supportEnvVarFallbackSyntax: false - }); - const npmPath = getNpmPath(); - // This returns something that looks like: - // ``` - // [ - // "3.0.0", - // "3.0.1", - // ... - // "3.0.20" - // ] - // ``` - // - // if multiple versions match the selector, or - // - // ``` - // "3.0.0" - // ``` - // - // if only a single version matches. - const spawnSyncOptions = { - cwd: rushTempFolder, - stdio: [], - shell: _isWindows() - }; - const platformNpmPath = _getPlatformPath(npmPath); - const npmVersionSpawnResult = child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync(platformNpmPath, ['view', `${name}@${version}`, 'version', '--no-update-notifier', '--json'], spawnSyncOptions); - if (npmVersionSpawnResult.status !== 0) { - throw new Error(`"npm view" returned error code ${npmVersionSpawnResult.status}`); - } - const npmViewVersionOutput = npmVersionSpawnResult.stdout.toString(); - const parsedVersionOutput = JSON.parse(npmViewVersionOutput); - const versions = Array.isArray(parsedVersionOutput) - ? parsedVersionOutput - : [parsedVersionOutput]; - let latestVersion = versions[0]; - for (let i = 1; i < versions.length; i++) { - const latestVersionCandidate = versions[i]; - if (_compareVersionStrings(latestVersionCandidate, latestVersion) > 0) { - latestVersion = latestVersionCandidate; - } - } - if (!latestVersion) { - throw new Error('No versions found for the specified version range.'); - } - return latestVersion; - } - catch (e) { - throw new Error(`Unable to resolve version ${version} of package ${name}: ${e}`); - } - } -} -let _rushJsonFolder; -/** - * Find the absolute path to the folder containing rush.json - */ -function findRushJsonFolder() { - if (!_rushJsonFolder) { - let basePath = __dirname; - let tempPath = __dirname; - do { - const testRushJsonPath = path__WEBPACK_IMPORTED_MODULE_3__.join(basePath, RUSH_JSON_FILENAME); - if (fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(testRushJsonPath)) { - _rushJsonFolder = basePath; - break; - } - else { - basePath = tempPath; - } - } while (basePath !== (tempPath = path__WEBPACK_IMPORTED_MODULE_3__.dirname(basePath))); // Exit the loop when we hit the disk root - if (!_rushJsonFolder) { - throw new Error(`Unable to find ${RUSH_JSON_FILENAME}.`); - } - } - return _rushJsonFolder; -} -/** - * Detects if the package in the specified directory is installed - */ -function _isPackageAlreadyInstalled(packageInstallFolder) { - try { - const flagFilePath = path__WEBPACK_IMPORTED_MODULE_3__.join(packageInstallFolder, INSTALLED_FLAG_FILENAME); - if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(flagFilePath)) { - return false; - } - const fileContents = fs__WEBPACK_IMPORTED_MODULE_1__.readFileSync(flagFilePath).toString(); - return fileContents.trim() === process.version; - } - catch (e) { - return false; - } -} -/** - * Delete a file. Fail silently if it does not exist. - */ -function _deleteFile(file) { - try { - fs__WEBPACK_IMPORTED_MODULE_1__.unlinkSync(file); - } - catch (err) { - if (err.code !== 'ENOENT' && err.code !== 'ENOTDIR') { - throw err; - } - } -} -/** - * Removes the following files and directories under the specified folder path: - * - installed.flag - * - - * - node_modules - */ -function _cleanInstallFolder(rushTempFolder, packageInstallFolder, lockFilePath) { - try { - const flagFile = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, INSTALLED_FLAG_FILENAME); - _deleteFile(flagFile); - const packageLockFile = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, 'package-lock.json'); - if (lockFilePath) { - fs__WEBPACK_IMPORTED_MODULE_1__.copyFileSync(lockFilePath, packageLockFile); - } - else { - // Not running `npm ci`, so need to cleanup - _deleteFile(packageLockFile); - const nodeModulesFolder = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME); - if (fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(nodeModulesFolder)) { - const rushRecyclerFolder = _ensureAndJoinPath(rushTempFolder, 'rush-recycler'); - fs__WEBPACK_IMPORTED_MODULE_1__.renameSync(nodeModulesFolder, path__WEBPACK_IMPORTED_MODULE_3__.join(rushRecyclerFolder, `install-run-${Date.now().toString()}`)); - } - } - } - catch (e) { - throw new Error(`Error cleaning the package install folder (${packageInstallFolder}): ${e}`); - } -} -function _createPackageJson(packageInstallFolder, name, version) { - try { - const packageJsonContents = { - name: 'ci-rush', - version: '0.0.0', - dependencies: { - [name]: version - }, - description: "DON'T WARN", - repository: "DON'T WARN", - license: 'MIT' - }; - const packageJsonPath = path__WEBPACK_IMPORTED_MODULE_3__.join(packageInstallFolder, PACKAGE_JSON_FILENAME); - fs__WEBPACK_IMPORTED_MODULE_1__.writeFileSync(packageJsonPath, JSON.stringify(packageJsonContents, undefined, 2)); - } - catch (e) { - throw new Error(`Unable to create package.json: ${e}`); - } -} -/** - * Run "npm install" in the package install folder. - */ -function _installPackage(logger, packageInstallFolder, name, version, command) { - try { - logger.info(`Installing ${name}...`); - const npmPath = getNpmPath(); - const platformNpmPath = _getPlatformPath(npmPath); - const result = child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync(platformNpmPath, [command], { - stdio: 'inherit', - cwd: packageInstallFolder, - env: process.env, - shell: _isWindows() - }); - if (result.status !== 0) { - throw new Error(`"npm ${command}" encountered an error`); - } - logger.info(`Successfully installed ${name}@${version}`); - } - catch (e) { - throw new Error(`Unable to install package: ${e}`); - } -} -/** - * Get the ".bin" path for the package. - */ -function _getBinPath(packageInstallFolder, binName) { - const binFolderPath = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME, '.bin'); - const resolvedBinName = _isWindows() ? `${binName}.cmd` : binName; - return path__WEBPACK_IMPORTED_MODULE_3__.resolve(binFolderPath, resolvedBinName); -} -/** - * Returns a cross-platform path - windows must enclose any path containing spaces within double quotes. - */ -function _getPlatformPath(platformPath) { - return _isWindows() && platformPath.includes(' ') ? `"${platformPath}"` : platformPath; -} -function _isWindows() { - return os__WEBPACK_IMPORTED_MODULE_2__.platform() === 'win32'; -} -/** - * Write a flag file to the package's install directory, signifying that the install was successful. - */ -function _writeFlagFile(packageInstallFolder) { - try { - const flagFilePath = path__WEBPACK_IMPORTED_MODULE_3__.join(packageInstallFolder, INSTALLED_FLAG_FILENAME); - fs__WEBPACK_IMPORTED_MODULE_1__.writeFileSync(flagFilePath, process.version); - } - catch (e) { - throw new Error(`Unable to create installed.flag file in ${packageInstallFolder}`); - } -} -function installAndRun(logger, packageName, packageVersion, packageBinName, packageBinArgs, lockFilePath = process.env[INSTALL_RUN_LOCKFILE_PATH_VARIABLE]) { - const rushJsonFolder = findRushJsonFolder(); - const rushCommonFolder = path__WEBPACK_IMPORTED_MODULE_3__.join(rushJsonFolder, 'common'); - const rushTempFolder = _getRushTempFolder(rushCommonFolder); - const packageInstallFolder = _ensureAndJoinPath(rushTempFolder, 'install-run', `${packageName}@${packageVersion}`); - if (!_isPackageAlreadyInstalled(packageInstallFolder)) { - // The package isn't already installed - _cleanInstallFolder(rushTempFolder, packageInstallFolder, lockFilePath); - const sourceNpmrcFolder = path__WEBPACK_IMPORTED_MODULE_3__.join(rushCommonFolder, 'config', 'rush'); - (0,_utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__.syncNpmrc)({ - sourceNpmrcFolder, - targetNpmrcFolder: packageInstallFolder, - logger, - supportEnvVarFallbackSyntax: false - }); - _createPackageJson(packageInstallFolder, packageName, packageVersion); - const command = lockFilePath ? 'ci' : 'install'; - _installPackage(logger, packageInstallFolder, packageName, packageVersion, command); - _writeFlagFile(packageInstallFolder); - } - const statusMessage = `Invoking "${packageBinName} ${packageBinArgs.join(' ')}"`; - const statusMessageLine = new Array(statusMessage.length + 1).join('-'); - logger.info('\n' + statusMessage + '\n' + statusMessageLine + '\n'); - const binPath = _getBinPath(packageInstallFolder, packageBinName); - const binFolderPath = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME, '.bin'); - // Windows environment variables are case-insensitive. Instead of using SpawnSyncOptions.env, we need to - // assign via the process.env proxy to ensure that we append to the right PATH key. - const originalEnvPath = process.env.PATH || ''; - let result; - try { - // `npm` bin stubs on Windows are `.cmd` files - // Node.js will not directly invoke a `.cmd` file unless `shell` is set to `true` - const platformBinPath = _getPlatformPath(binPath); - process.env.PATH = [binFolderPath, originalEnvPath].join(path__WEBPACK_IMPORTED_MODULE_3__.delimiter); - result = child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync(platformBinPath, packageBinArgs, { - stdio: 'inherit', - windowsVerbatimArguments: false, - shell: _isWindows(), - cwd: process.cwd(), - env: process.env - }); - } - finally { - process.env.PATH = originalEnvPath; - } - if (result.status !== null) { - return result.status; - } - else { - throw result.error || new Error('An unknown error occurred.'); - } -} -function runWithErrorAndStatusCode(logger, fn) { - process.exitCode = 1; - try { - const exitCode = fn(); - process.exitCode = exitCode; - } - catch (e) { - logger.error('\n\n' + e.toString() + '\n\n'); - } -} -function _run() { - const [nodePath /* Ex: /bin/node */, scriptPath /* /repo/common/scripts/install-run-rush.js */, rawPackageSpecifier /* qrcode@^1.2.0 */, packageBinName /* qrcode */, ...packageBinArgs /* [-f, myproject/lib] */] = process.argv; - if (!nodePath) { - throw new Error('Unexpected exception: could not detect node path'); - } - if (path__WEBPACK_IMPORTED_MODULE_3__.basename(scriptPath).toLowerCase() !== 'install-run.js') { - // If install-run.js wasn't directly invoked, don't execute the rest of this function. Return control - // to the script that (presumably) imported this file - return; - } - if (process.argv.length < 4) { - console.log('Usage: install-run.js @ [args...]'); - console.log('Example: install-run.js qrcode@1.2.2 qrcode https://rushjs.io'); - process.exit(1); - } - const logger = { info: console.log, error: console.error }; - runWithErrorAndStatusCode(logger, () => { - const rushJsonFolder = findRushJsonFolder(); - const rushCommonFolder = _ensureAndJoinPath(rushJsonFolder, 'common'); - const packageSpecifier = _parsePackageSpecifier(rawPackageSpecifier); - const name = packageSpecifier.name; - const version = _resolvePackageVersion(logger, rushCommonFolder, packageSpecifier); - if (packageSpecifier.version !== version) { - console.log(`Resolved to ${name}@${version}`); - } - return installAndRun(logger, name, version, packageBinName, packageBinArgs); - }); -} -_run(); -//# sourceMappingURL=install-run.js.map -})(); - -module.exports = __webpack_exports__; -/******/ })() -; -//# sourceMappingURL=install-run.js.map \ No newline at end of file diff --git a/foundations/core/common/scripts/merge-coverage.js b/foundations/core/common/scripts/merge-coverage.js deleted file mode 100644 index 7d55b07ce9..0000000000 --- a/foundations/core/common/scripts/merge-coverage.js +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env node -const fs = require('fs') -const path = require('path') - -const root = process.cwd() -const patterns = ['packages', 'pods', 'tests'] -let files = [] -for (const p of patterns) { - const dir = path.join(root, p) - if (!fs.existsSync(dir)) continue - const items = fs.readdirSync(dir) - for (const it of items) { - const lcov = path.join(dir, it, 'coverage', 'lcov.info') - if (fs.existsSync(lcov)) files.push(lcov) - } -} - -if (files.length === 0) { - console.error('No lcov files found in packages/pods/tests/*/coverage/lcov.info') - process.exit(1) -} - -const outDir = path.join(root, 'coverage') -if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true }) -const outFile = path.join(outDir, 'lcov.info') - -let outData = '' -let seenTN = false -// Build a repo file index to help resolve SF entries that are ambiguous -const ignoreDirs = new Set(['node_modules', '.git', 'coverage', 'lib', 'dist', 'types', '.rush', 'temp', 'pnpm-store']) -const repoFiles = [] -function walk(dir) { - const items = fs.readdirSync(dir, { withFileTypes: true }) - for (const it of items) { - if (it.isDirectory()) { - if (ignoreDirs.has(it.name)) continue - // skip hidden folders except top-level .config maybe - if (it.name.startsWith('.')) continue - try { - walk(path.join(dir, it.name)) - } catch (e) { - // ignore permission errors - } - } else if (it.isFile()) { - repoFiles.push(path.join(dir, it.name)) - } - } -} -try { - walk(root) -} catch (e) { - /* ignore */ -} - -for (const f of files) { - const data = fs.readFileSync(f, 'utf8') - const pkgDir = path.dirname(path.dirname(f)) - const lines = data.split(/\r?\n/) - const outLines = [] - for (const line of lines) { - if (!line) continue - // skip duplicate TN: headers (test name) - if (line.startsWith('TN:')) { - if (seenTN) continue - seenTN = true - outLines.push(line) - continue - } - - if (line.startsWith('SF:')) { - const orig = line.slice(3) - // if path is absolute and exists, keep it; otherwise resolve from package dir - if (path.isAbsolute(orig)) { - outLines.push('SF:' + orig) - continue - } - - const abs = path.resolve(pkgDir, orig) - if (fs.existsSync(abs)) { - outLines.push('SF:' + abs) - } else { - // try package/src/orig if orig is not already prefixed with src - const alt = path.resolve(pkgDir, orig) - if (fs.existsSync(alt)) { - outLines.push('SF:' + path.relative(root, alt)) - } else { - // try to find any file in repo that ends with the orig path - const found = repoFiles.find((p) => p.endsWith(path.sep + orig) || p.endsWith(orig)) - if (found) { - outLines.push('SF:' + found) - } else { - // keep original if we can't resolve - outLines.push('SF:' + orig) - } - } - } - continue - } - - outLines.push(line) - } - - outData += outLines.join('\n') + '\n' -} -fs.writeFileSync(outFile, outData, 'utf8') -console.log('Merged', files.length, 'lcov files into', outFile) diff --git a/foundations/core/common/scripts/package.json b/foundations/core/common/scripts/package.json deleted file mode 100644 index f4a987ee54..0000000000 --- a/foundations/core/common/scripts/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "@hcengineering/scripts", - "version": "0.7.17", - "scripts": { - "format": "echo \"No format specified\"" - }, - "devDependencies": { - "@hcengineering/platform-rig": "workspace:^0.7.19", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "@typescript-eslint/parser": "^6.21.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "eslint-plugin-svelte": "^2.35.1" - }, - "private": true -} diff --git a/foundations/core/common/scripts/show-coverage.sh b/foundations/core/common/scripts/show-coverage.sh deleted file mode 100755 index cc99b27499..0000000000 --- a/foundations/core/common/scripts/show-coverage.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -echo "=== FINAL COVERAGE REPORT ===" -echo "" - -# Iterate through each package directory -for pkg in packages/*/; do - pkgname=$(basename "$pkg") - - echo "📦 Package: $pkgname" - echo "---" - - # Change to package directory - cd "$pkg" || continue - - # Run tests with coverage and extract summary - npm test -- --coverage --silent 2>&1 | \ - grep -A 4 "Coverage summary" | \ - grep -E "Statements|Branches|Functions|Lines" - - # Return to root directory - cd ../.. || exit - - echo "" -done - -echo "=== END OF COVERAGE REPORT ===" \ No newline at end of file diff --git a/foundations/core/packages/account-client/.eslintrc.js b/foundations/core/packages/account-client/.eslintrc.js deleted file mode 100644 index 72235dc283..0000000000 --- a/foundations/core/packages/account-client/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/foundations/core/packages/account-client/.npmignore b/foundations/core/packages/account-client/.npmignore deleted file mode 100644 index e3ec093c38..0000000000 --- a/foundations/core/packages/account-client/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!/lib/** -!CHANGELOG.md -/lib/**/__tests__/ diff --git a/foundations/core/packages/account-client/CHANGELOG.json b/foundations/core/packages/account-client/CHANGELOG.json deleted file mode 100644 index cef733b400..0000000000 --- a/foundations/core/packages/account-client/CHANGELOG.json +++ /dev/null @@ -1,132 +0,0 @@ -{ - "name": "@hcengineering/account-client", - "entries": [ - { - "version": "0.7.19", - "tag": "@hcengineering/account-client_v0.7.19", - "date": "Thu, 30 Oct 2025 08:41:42 GMT", - "comments": { - "none": [ - { - "comment": "formatting" - } - ], - "patch": [ - { - "comment": "add workspace usage info" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.19` to `0.7.20`" - } - ] - } - }, - { - "version": "0.7.18", - "tag": "@hcengineering/account-client_v0.7.18", - "date": "Tue, 28 Oct 2025 21:50:57 GMT", - "comments": { - "patch": [ - { - "comment": "new sub methods" - } - ] - } - }, - { - "version": "0.7.17", - "tag": "@hcengineering/account-client_v0.7.17", - "date": "Mon, 27 Oct 2025 13:27:12 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" - } - ] - } - }, - { - "version": "0.7.7", - "tag": "@hcengineering/account-client_v0.7.7", - "date": "Wed, 22 Oct 2025 12:46:09 GMT", - "comments": { - "patch": [ - { - "comment": "add subs methods" - } - ] - } - }, - { - "version": "0.7.6", - "tag": "@hcengineering/account-client_v0.7.6", - "date": "Wed, 15 Oct 2025 18:01:30 GMT", - "comments": { - "patch": [ - { - "comment": "added methods to work with profile" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.7` to `0.7.8`" - } - ] - } - }, - { - "version": "0.7.5", - "tag": "@hcengineering/account-client_v0.7.5", - "date": "Tue, 14 Oct 2025 04:58:17 GMT", - "comments": { - "patch": [ - { - "comment": "update deps" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`" - }, - { - "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.4` to `0.7.5`" - } - ] - } - }, - { - "version": "0.7.4", - "tag": "@hcengineering/account-client_v0.7.4", - "date": "Sat, 11 Oct 2025 18:20:33 GMT", - "comments": { - "patch": [ - { - "comment": "Update to latest platform rig" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`" - }, - { - "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.3` to `0.7.4`" - } - ] - } - }, - { - "version": "0.7.3", - "tag": "@hcengineering/account-client_v0.7.3", - "date": "Wed, 08 Oct 2025 03:40:53 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`" - } - ] - } - } - ] -} diff --git a/foundations/core/packages/account-client/CHANGELOG.md b/foundations/core/packages/account-client/CHANGELOG.md deleted file mode 100644 index 8640bd6fe0..0000000000 --- a/foundations/core/packages/account-client/CHANGELOG.md +++ /dev/null @@ -1,56 +0,0 @@ -# Change Log - @hcengineering/account-client - -This log was last generated on Thu, 30 Oct 2025 08:41:42 GMT and should not be manually modified. - -## 0.7.19 -Thu, 30 Oct 2025 08:41:42 GMT - -### Patches - -- add workspace usage info - -## 0.7.18 -Tue, 28 Oct 2025 21:50:57 GMT - -### Patches - -- new sub methods - -## 0.7.17 -Mon, 27 Oct 2025 13:27:12 GMT - -_Version update only_ - -## 0.7.7 -Wed, 22 Oct 2025 12:46:09 GMT - -### Patches - -- add subs methods - -## 0.7.6 -Wed, 15 Oct 2025 18:01:30 GMT - -### Patches - -- added methods to work with profile - -## 0.7.5 -Tue, 14 Oct 2025 04:58:17 GMT - -### Patches - -- update deps - -## 0.7.4 -Sat, 11 Oct 2025 18:20:33 GMT - -### Patches - -- Update to latest platform rig - -## 0.7.3 -Wed, 08 Oct 2025 03:40:53 GMT - -_Initial release_ - diff --git a/foundations/core/packages/account-client/config/rig.json b/foundations/core/packages/account-client/config/rig.json deleted file mode 100644 index 0110930f55..0000000000 --- a/foundations/core/packages/account-client/config/rig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig" -} diff --git a/foundations/core/packages/account-client/jest.config.js b/foundations/core/packages/account-client/jest.config.js deleted file mode 100644 index 069ea8aa0d..0000000000 --- a/foundations/core/packages/account-client/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ['./src'], - coverageReporters: ['text-summary', 'html', 'lcov'] -} diff --git a/foundations/core/packages/account-client/package.json b/foundations/core/packages/account-client/package.json deleted file mode 100644 index f2d75506b5..0000000000 --- a/foundations/core/packages/account-client/package.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "name": "@hcengineering/account-client", - "version": "0.7.19", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "files": [ - "lib/**/*", - "types/**/*", - "src/**/*", - "!src/**/__test__/**", - "tsconfig.json" - ], - "author": "Hardcore Engineering Inc.", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "build:watch": "compile", - "format": "format src", - "test": "jest --passWithNoTests --silent --coverage", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --coverage", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "cross-env": "~7.0.3", - "@hcengineering/platform-rig": "workspace:^0.7.19", - "@types/node": "^22.18.1", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "esbuild": "^0.25.10", - "@typescript-eslint/parser": "^6.21.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.6.2", - "typescript": "^5.9.3", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5", - "eslint-plugin-svelte": "^2.35.1" - }, - "dependencies": { - "@hcengineering/core": "workspace:^0.7.22", - "@hcengineering/platform": "workspace:^0.7.18" - }, - "repository": "https://github.com/hcengineering/huly.core", - "publishConfig": { - "access": "public" - }, - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.js" - } - } -} diff --git a/foundations/core/packages/account-client/src/client.ts b/foundations/core/packages/account-client/src/client.ts deleted file mode 100644 index 92b7c5f7a5..0000000000 --- a/foundations/core/packages/account-client/src/client.ts +++ /dev/null @@ -1,1277 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// -import { - type AccountInfo, - type AccountRole, - type AccountUuid, - type BackupStatus, - concatLink, - Data, - type Person, - type PersonId, - type PersonInfo, - type PersonUuid, - type SocialIdType, - Version, - type UsageStatus, - type WorkspaceInfoWithStatus, - type WorkspaceMemberInfo, - WorkspaceMode, - type WorkspaceUserOperation, - type WorkspaceUuid -} from '@hcengineering/core' -import platform, { PlatformError, Severity, Status } from '@hcengineering/platform' -import type { - AccountAggregatedInfo, - Integration, - IntegrationKey, - IntegrationSecret, - IntegrationSecretKey, - LoginInfo, - LoginInfoByToken, - LoginInfoRequestData, - LoginInfoWithWorkspaces, - MailboxInfo, - MailboxOptions, - MailboxSecret, - OtpInfo, - PersonWithProfile, - ProviderInfo, - RegionInfo, - SocialId, - Subscription, - SubscriptionData, - UserProfile, - WorkspaceLoginInfo, - WorkspaceOperation -} from './types' -import { getClientTimezone } from './utils' - -/** @public */ -export interface AccountClient { - // Static methods - getProviders: () => Promise - - // RPC - getUserWorkspaces: () => Promise - selectWorkspace: ( - workspaceUrl: string, - kind?: 'external' | 'internal' | 'byregion', - externalRegions?: string[] - ) => Promise - validateOtp: (email: string, code: string, password?: string, action?: 'verify') => Promise - loginOtp: (email: string) => Promise - getLoginInfoByToken: (data?: LoginInfoRequestData) => Promise - getLoginWithWorkspaceInfo: () => Promise - restorePassword: (password: string) => Promise - confirm: () => Promise - requestPasswordReset: (email: string) => Promise - sendInvite: (email: string, role: AccountRole) => Promise - resendInvite: (email: string, role: AccountRole) => Promise - createInviteLink: ( - email: string, - role: AccountRole, - autoJoin: boolean, - firstName: string, - lastName: string, - navigateUrl?: string, - expHours?: number - ) => Promise - leaveWorkspace: (account: AccountUuid) => Promise - changeUsername: (first: string, last: string) => Promise - changePassword: (oldPassword: string, newPassword: string) => Promise - signUpJoin: ( - email: string, - password: string, - first: string, - last: string, - inviteId: string, - workspaceUrl: string - ) => Promise - join: (email: string, password: string, inviteId: string, workspaceUrl: string) => Promise - createInvite: (exp: number, emailMask: string, limit: number, role: AccountRole) => Promise - /** - * @param options.personalized - * If true, will generate a link with a personalized token for one person access - * If false, will generate a link with an open-ended account in the token. Every token use will generate a new account. - * When false, notBefore and expiration parameters are mandatory. - * @param options.notBefore - not valid before; timestamp in seconds - * @param options.expiration - expires after; timestamp in seconds - */ - createAccessLink: ( - role: AccountRole, - options?: { - firstName?: string - lastName?: string - navigateUrl?: string - extra?: Record - spaces?: string[] - notBefore?: number - expiration?: number - personalized?: boolean - } - ) => Promise - checkJoin: (inviteId: string) => Promise - checkAutoJoin: (inviteId: string, firstName?: string, lastName?: string) => Promise - getWorkspaceInfo: (updateLastVisit?: boolean) => Promise - getWorkspacesInfo: (workspaces: WorkspaceUuid[]) => Promise - updateLastVisit: (workspaces: WorkspaceUuid[]) => Promise - getRegionInfo: () => Promise - createWorkspace: (name: string, region?: string) => Promise - signUpOtp: (email: string, first: string, last: string) => Promise - /** - * Deprecated. Only to be used for dev setups without mail service. - */ - signUp: (email: string, password: string, first: string, last: string) => Promise - login: (email: string, password: string) => Promise - loginAsGuest: () => Promise - isReadOnlyGuest: () => Promise - getPerson: () => Promise - getPersonInfo: (account: PersonUuid) => Promise - getSocialIds: (includeDeleted?: boolean) => Promise - getWorkspaceMembers: () => Promise - updateWorkspaceRole: (account: string, role: AccountRole) => Promise - updateAllowReadOnlyGuests: ( - readOnlyGuestsAllowed: boolean - ) => Promise<{ guestPerson: Person, guestSocialIds: SocialId[] } | undefined> - updateAllowGuestSignUp: (guestSignUpAllowed: boolean) => Promise - updateWorkspaceName: (name: string) => Promise - deleteWorkspace: () => Promise - findPersonBySocialKey: (socialKey: string, requireAccount?: boolean) => Promise - findPersonBySocialId: (socialId: PersonId, requireAccount?: boolean) => Promise - findSocialIdBySocialKey: (socialKey: string) => Promise - findFullSocialIdBySocialKey: (socialKey: string) => Promise - findFullSocialIds: (socialIds: PersonId[]) => Promise - getMailboxOptions: () => Promise - getMailboxSecret: (mailbox: string) => Promise - createMailbox: (name: string, domain: string) => Promise<{ mailbox: string, socialId: PersonId }> - getMailboxes: () => Promise - deleteMailbox: (mailbox: string) => Promise - listAccounts: (search?: string, skip?: number, limit?: number) => Promise - deleteAccount: (uuid: AccountUuid) => Promise - - workerHandshake: (region: string, version: Data, operation: WorkspaceOperation) => Promise - getPendingWorkspace: ( - region: string, - version: Data, - operation: WorkspaceOperation - ) => Promise - updateWorkspaceInfo: ( - wsUuid: string, - event: string, - version: Data, - progress: number, - message?: string - ) => Promise - listWorkspaces: (region?: string | null, mode?: WorkspaceMode | null) => Promise - performWorkspaceOperation: ( - workspaceId: string | string[], - event: WorkspaceUserOperation, - ...params: any - ) => Promise - assignWorkspace: (email: string, workspaceUuid: string, role: AccountRole) => Promise - updateBackupInfo: (info: BackupStatus) => Promise - updateUsageInfo: (info: UsageStatus) => Promise - updateWorkspaceRoleBySocialKey: (socialKey: string, targetRole: AccountRole) => Promise - ensurePerson: ( - socialType: SocialIdType, - socialValue: string, - firstName: string, - lastName: string - ) => Promise<{ uuid: PersonUuid, socialId: PersonId }> - addSocialIdToPerson: ( - person: PersonUuid, - type: SocialIdType, - value: string, - confirmed: boolean, - displayValue?: string - ) => Promise - updateSocialId: (personId: PersonId, displayValue: string) => Promise - exchangeGuestToken: (token: string) => Promise - /** - * Releases the target social id for the target account. - * If called with user's token it releases the social id for the user's account. - * @param personUuid Required for services - * @param type Social id type - * @param value Social id value - * @param deleteIntegrations Deletes associated integrations if true. Otherwise, throws an error if any. - * @returns Deleted social id with updated isDeleted flag and key/value - */ - releaseSocialId: ( - personUuid: PersonUuid | undefined, - type: SocialIdType, - value: string, - deleteIntegrations?: boolean - ) => Promise - createIntegration: (integration: Integration) => Promise - updateIntegration: (integration: Integration) => Promise - deleteIntegration: (integrationKey: IntegrationKey) => Promise - getIntegration: (integrationKey: IntegrationKey) => Promise - listIntegrations: (filter: Partial) => Promise - addIntegrationSecret: (integrationSecret: IntegrationSecret) => Promise - updateIntegrationSecret: (integrationSecret: IntegrationSecret) => Promise - deleteIntegrationSecret: (integrationSecretKey: IntegrationSecretKey) => Promise - getIntegrationSecret: (integrationSecretKey: IntegrationSecretKey) => Promise - listIntegrationsSecrets: (filter: Partial) => Promise - getAccountInfo: (uuid: AccountUuid) => Promise - canMergeSpecifiedPersons: (primaryPerson: PersonUuid, secondaryPerson: PersonUuid) => Promise - mergeSpecifiedPersons: (primaryPerson: PersonUuid, secondaryPerson: PersonUuid) => Promise - mergeSpecifiedAccounts: (primaryAccount: AccountUuid, secondaryAccount: AccountUuid) => Promise - addEmailSocialId: (email: string) => Promise - addHulyAssistantSocialId: () => Promise - refreshHulyAssistantToken: () => Promise - - setMyProfile: (profile: Partial>) => Promise - getUserProfile: (personUuid?: PersonUuid) => Promise - - getSubscriptions: (workspaceUuid?: WorkspaceUuid | undefined, activeOnly?: boolean) => Promise - getSubscriptionByProviderId: (provider: string, providerSubscriptionId: string) => Promise - getSubscriptionById: (subscriptionId: string) => Promise - upsertSubscription: (subscription: SubscriptionData) => Promise - - setCookie: () => Promise - deleteCookie: () => Promise -} - -/** @public */ -export function getClient (accountsUrl?: string, token?: string, retryTimeoutMs?: number): AccountClient { - if (accountsUrl === undefined) { - throw new Error('Accounts url not specified') - } - - return new AccountClientImpl(accountsUrl, token, retryTimeoutMs) -} - -interface Request { - method: string - params: Record -} - -class AccountClientImpl implements AccountClient { - private readonly request: RequestInit - private readonly rpc: typeof this._rpc - - constructor ( - private readonly url: string, - private readonly token?: string, - retryTimeoutMs?: number - ) { - if (url === '') { - throw new Error('Accounts url not specified') - } - - const isBrowser = typeof window !== 'undefined' - - this.request = { - keepalive: true, - headers: { - ...(this.token === undefined - ? {} - : { - Authorization: 'Bearer ' + this.token - }) - }, - ...(isBrowser ? { credentials: 'include' } : {}) - } - this.rpc = withRetryUntilTimeout(this._rpc.bind(this), retryTimeoutMs ?? 5000) - } - - async getProviders (): Promise { - return await withRetryUntilMaxAttempts(async () => { - const response = await fetch(concatLink(this.url, '/providers')) - - return await response.json() - })() - } - - private async _rpc(request: Request): Promise { - const timezone = getClientTimezone() - const meta: Record = timezone !== undefined ? { 'x-timezone': timezone } : {} - const response = await fetch(this.url, { - ...this.request, - headers: { - ...this.request.headers, - 'Content-Type': 'application/json', - Connection: 'keep-alive', - ...meta - }, - method: 'POST', - body: JSON.stringify(request) - }) - - const result = await response.json() - if (result.error != null) { - throw new PlatformError(result.error) - } - - return result.result - } - - private flattenStatus (ws: any): WorkspaceInfoWithStatus { - if (ws === undefined) { - throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, {})) - } - - const status = ws.status - if (status === undefined) { - return ws - } - - const result = { ...ws, ...status, processingAttemps: status.processingAttempts ?? 0 } - delete result.status - - return result - } - - async getUserWorkspaces (): Promise { - const request = { - method: 'getUserWorkspaces' as const, - params: {} - } - - return (await this.rpc(request)).map((ws) => this.flattenStatus(ws)) - } - - async selectWorkspace ( - workspaceUrl: string, - kind: 'external' | 'internal' | 'byregion' = 'external', - externalRegions: string[] = [] - ): Promise { - const request = { - method: 'selectWorkspace' as const, - params: { workspaceUrl, kind, externalRegions } - } - - return await this.rpc(request) - } - - async validateOtp (email: string, code: string, password?: string, action?: 'verify'): Promise { - const request = { - method: 'validateOtp' as const, - params: { email, code, password, action } - } - - return await this.rpc(request) - } - - async loginOtp (email: string): Promise { - const request = { - method: 'loginOtp' as const, - params: { email } - } - - return await this.rpc(request) - } - - async getLoginInfoByToken (data?: LoginInfoRequestData): Promise { - const request = { - method: 'getLoginInfoByToken' as const, - params: data ?? {} - } - - return await this.rpc(request) - } - - async getLoginWithWorkspaceInfo (): Promise { - const request = { - method: 'getLoginWithWorkspaceInfo' as const, - params: {} - } - - return await this.rpc(request) - } - - async restorePassword (password: string): Promise { - const request = { - method: 'restorePassword' as const, - params: { password } - } - - return await this.rpc(request) - } - - async confirm (): Promise { - const request = { - method: 'confirm' as const, - params: {} - } - - return await this.rpc(request) - } - - async requestPasswordReset (email: string): Promise { - const request = { - method: 'requestPasswordReset' as const, - params: { email } - } - - await this.rpc(request) - } - - async sendInvite (email: string, role: AccountRole): Promise { - const request = { - method: 'sendInvite' as const, - params: { email, role } - } - - await this.rpc(request) - } - - async resendInvite (email: string, role: AccountRole): Promise { - const request = { - method: 'resendInvite' as const, - params: { email, role } - } - - await this.rpc(request) - } - - async createInviteLink ( - email: string, - role: AccountRole, - autoJoin: boolean, - firstName: string, - lastName: string, - navigateUrl?: string, - expHours?: number - ): Promise { - const request = { - method: 'createInviteLink' as const, - params: { email, role, autoJoin, firstName, lastName, navigateUrl, expHours } - } - - return await this.rpc(request) - } - - async createAccessLink ( - role: AccountRole, - options?: { - firstName?: string - lastName?: string - navigateUrl?: string - extra?: Record - spaces?: string[] - notBefore?: number - expiration?: number - personalized?: boolean - } - ): Promise { - const params: any = { ...(options ?? {}), role } - if (params.extra != null) { - params.extra = JSON.stringify(params.extra) - } - - const request = { - method: 'createAccessLink' as const, - params - } - - return await this.rpc(request) - } - - async leaveWorkspace (account: AccountUuid): Promise { - const request = { - method: 'leaveWorkspace' as const, - params: { account } - } - - return await this.rpc(request) - } - - async changeUsername (first: string, last: string): Promise { - const request = { - method: 'changeUsername' as const, - params: { first, last } - } - - await this.rpc(request) - } - - async changePassword (oldPassword: string, newPassword: string): Promise { - const request = { - method: 'changePassword' as const, - params: { oldPassword, newPassword } - } - - await this.rpc(request) - } - - async signUpJoin ( - email: string, - password: string, - first: string, - last: string, - inviteId: string, - workspaceUrl: string - ): Promise { - const request = { - method: 'signUpJoin' as const, - params: { email, password, first, last, inviteId, workspaceUrl } - } - - return await this.rpc(request) - } - - async join (email: string, password: string, inviteId: string, workspaceUrl: string): Promise { - const request = { - method: 'join' as const, - params: { email, password, inviteId, workspaceUrl } - } - - return await this.rpc(request) - } - - async createInvite (exp: number, emailMask: string, limit: number, role: AccountRole): Promise { - const request = { - method: 'createInvite' as const, - params: { exp, emailMask, limit, role } - } - - return await this.rpc(request) - } - - async checkJoin (inviteId: string): Promise { - const request = { - method: 'checkJoin' as const, - params: { inviteId } - } - - return await this.rpc(request) - } - - async checkAutoJoin (inviteId: string, firstName?: string, lastName?: string): Promise { - const request = { - method: 'checkAutoJoin' as const, - params: { inviteId, firstName, lastName } - } - - return await this.rpc(request) - } - - async getWorkspacesInfo (ids: WorkspaceUuid[]): Promise { - const request = { - method: 'getWorkspacesInfo' as const, - params: { ids } - } - const infos: any[] = await this.rpc(request) - return Array.from(infos).map((it) => this.flattenStatus(it)) - } - - async updateLastVisit (ids: WorkspaceUuid[]): Promise { - const request = { - method: 'updateLastVisit' as const, - params: { ids } - } - await this.rpc(request) - } - - async getWorkspaceInfo (updateLastVisit: boolean = false): Promise { - const request = { - method: 'getWorkspaceInfo' as const, - params: updateLastVisit ? { updateLastVisit: true } : {} - } - - return this.flattenStatus(await this.rpc(request)) - } - - async getRegionInfo (): Promise { - const request = { - method: 'getRegionInfo' as const, - params: {} - } - - return await this.rpc(request) - } - - async createWorkspace (workspaceName: string, region?: string): Promise { - const request = { - method: 'createWorkspace' as const, - params: { workspaceName, region } - } - - return await this.rpc(request) - } - - async signUpOtp (email: string, firstName: string, lastName: string): Promise { - const request = { - method: 'signUpOtp' as const, - params: { email, firstName, lastName } - } - - return await this.rpc(request) - } - - async signUp (email: string, password: string, firstName: string, lastName: string): Promise { - const request = { - method: 'signUp' as const, - params: { email, password, firstName, lastName } - } - - return await this.rpc(request) - } - - async login (email: string, password: string): Promise { - const request = { - method: 'login' as const, - params: { email, password } - } - - return await this.rpc(request) - } - - async loginAsGuest (): Promise { - const request = { - method: 'loginAsGuest' as const, - params: {} - } - - return await this.rpc(request) - } - - async isReadOnlyGuest (): Promise { - const request = { - method: 'isReadOnlyGuest' as const, - params: {} - } - - return await this.rpc(request) - } - - async getPerson (): Promise { - const request = { - method: 'getPerson' as const, - params: {} - } - - return await this.rpc(request) - } - - async getPersonInfo (account: PersonUuid): Promise { - const request = { - method: 'getPersonInfo' as const, - params: { account } - } - - return await this.rpc(request) - } - - async getSocialIds (includeDeleted?: boolean): Promise { - const request = { - method: 'getSocialIds' as const, - params: { includeDeleted } - } - - return await this.rpc(request) - } - - async workerHandshake (region: string, version: Data, operation: WorkspaceOperation): Promise { - const request = { - method: 'workerHandshake' as const, - params: { region, version, operation } - } - - await this.rpc(request) - } - - async getPendingWorkspace ( - region: string, - version: Data, - operation: WorkspaceOperation - ): Promise { - const request = { - method: 'getPendingWorkspace' as const, - params: { region, version, operation } - } - - const result = await this.rpc(request) - if (result == null) { - return null - } - - return this.flattenStatus(result) - } - - async updateWorkspaceInfo ( - workspaceUuid: string, - event: string, - version: Data, - progress: number, - message?: string - ): Promise { - const request = { - method: 'updateWorkspaceInfo' as const, - params: { workspaceUuid, event, version, progress, message } - } - - await this.rpc(request) - } - - async updateAllowReadOnlyGuests ( - readOnlyGuestsAllowed: boolean - ): Promise<{ guestPerson: Person, guestSocialIds: SocialId[] } | undefined> { - const request = { - method: 'updateAllowReadOnlyGuests' as const, - params: { readOnlyGuestsAllowed } - } - - return await this.rpc(request) - } - - async updateAllowGuestSignUp (guestSignUpAllowed: boolean): Promise { - const request = { - method: 'updateAllowGuestSignUp' as const, - params: { guestSignUpAllowed } - } - - await this.rpc(request) - } - - async getWorkspaceMembers (): Promise { - const request = { - method: 'getWorkspaceMembers' as const, - params: {} - } - - return await this.rpc(request) - } - - async updateWorkspaceRole (targetAccount: string, targetRole: AccountRole): Promise { - const request = { - method: 'updateWorkspaceRole' as const, - params: { targetAccount, targetRole } - } - - await this.rpc(request) - } - - async updateWorkspaceName (name: string): Promise { - const request = { - method: 'updateWorkspaceName' as const, - params: { name } - } - - await this.rpc(request) - } - - async deleteWorkspace (): Promise { - const request = { - method: 'deleteWorkspace' as const, - params: {} - } - - await this.rpc(request) - } - - async findPersonBySocialKey (socialString: string, requireAccount?: boolean): Promise { - const request = { - method: 'findPersonBySocialKey' as const, - params: { socialString, requireAccount } - } - - return await this.rpc(request) - } - - async findPersonBySocialId (socialId: PersonId, requireAccount?: boolean): Promise { - const request = { - method: 'findPersonBySocialId' as const, - params: { socialId, requireAccount } - } - - return await this.rpc(request) - } - - async findSocialIdBySocialKey (socialKey: string): Promise { - const request = { - method: 'findSocialIdBySocialKey' as const, - params: { socialKey } - } - - return await this.rpc(request) - } - - async findFullSocialIdBySocialKey (socialKey: string): Promise { - const request = { - method: 'findFullSocialIdBySocialKey' as const, - params: { socialKey } - } - return await this.rpc(request) - } - - async findFullSocialIds (socialIds: PersonId[]): Promise { - const request = { - method: 'findFullSocialIds' as const, - params: { socialIds } - } - return await this.rpc(request) - } - - async listWorkspaces (region?: string | null, mode: WorkspaceMode | null = null): Promise { - const request = { - method: 'listWorkspaces' as const, - params: { region, mode } - } - - return ((await this.rpc(request)) ?? []).map((ws) => this.flattenStatus(ws)) - } - - async performWorkspaceOperation ( - workspaceId: string | string[], - event: WorkspaceUserOperation, - ...params: any - ): Promise { - const request = { - method: 'performWorkspaceOperation' as const, - params: { workspaceId, event, params } - } - - return await this.rpc(request) - } - - async updateBackupInfo (backupInfo: BackupStatus): Promise { - const request = { - method: 'updateBackupInfo' as const, - params: { backupInfo } - } - - await this.rpc(request) - } - - async updateUsageInfo (usageInfo: UsageStatus): Promise { - const request = { - method: 'updateUsageInfo' as const, - params: { usageInfo } - } - - await this.rpc(request) - } - - async assignWorkspace (email: string, workspaceUuid: string, role: AccountRole): Promise { - const request = { - method: 'assignWorkspace' as const, - params: { email, workspaceUuid, role } - } - - await this.rpc(request) - } - - async updateWorkspaceRoleBySocialKey (socialKey: string, targetRole: AccountRole): Promise { - const request = { - method: 'updateWorkspaceRoleBySocialKey' as const, - params: { socialKey, targetRole } - } - - await this.rpc(request) - } - - async ensurePerson ( - socialType: SocialIdType, - socialValue: string, - firstName: string, - lastName: string - ): Promise<{ uuid: PersonUuid, socialId: PersonId }> { - const request = { - method: 'ensurePerson' as const, - params: { socialType, socialValue, firstName, lastName } - } - - return await this.rpc(request) - } - - async exchangeGuestToken (token: string): Promise { - const request = { - method: 'exchangeGuestToken' as const, - params: { token } - } - - return await this.rpc(request) - } - - async addSocialIdToPerson ( - person: PersonUuid, - type: SocialIdType, - value: string, - confirmed: boolean, - displayValue?: string - ): Promise { - const request = { - method: 'addSocialIdToPerson' as const, - params: { person, type, value, confirmed, displayValue } - } - - return await this.rpc(request) - } - - async updateSocialId (personId: PersonId, displayValue: string): Promise { - const request = { - method: 'updateSocialId' as const, - params: { personId, displayValue } - } - return await this.rpc(request) - } - - async getMailboxOptions (): Promise { - const request = { - method: 'getMailboxOptions' as const, - params: {} - } - - return await this.rpc(request) - } - - async getMailboxSecret (mailbox: string): Promise { - const request = { - method: 'getMailboxSecret' as const, - params: { mailbox } - } - - return await this.rpc(request) - } - - async createMailbox (name: string, domain: string): Promise<{ mailbox: string, socialId: PersonId }> { - const request = { - method: 'createMailbox' as const, - params: { name, domain } - } - - return await this.rpc(request) - } - - async getMailboxes (): Promise { - const request = { - method: 'getMailboxes' as const, - params: {} - } - - return await this.rpc(request) - } - - async deleteMailbox (mailbox: string): Promise { - const request = { - method: 'deleteMailbox' as const, - params: { mailbox } - } - - await this.rpc(request) - } - - async listAccounts (search?: string, skip?: number, limit?: number): Promise { - const request = { - method: 'listAccounts' as const, - params: { search, skip, limit } - } - - return await this.rpc(request) - } - - async deleteAccount (uuid: AccountUuid): Promise { - const request = { - method: 'deleteAccount' as const, - params: { uuid } - } - - await this.rpc(request) - } - - async releaseSocialId ( - personUuid: PersonUuid | undefined, - type: SocialIdType, - value: string, - deleteIntegrations = false - ): Promise { - const request = { - method: 'releaseSocialId' as const, - params: { personUuid, type, value, deleteIntegrations } - } - - return await this.rpc(request) - } - - async createIntegration (integration: Integration): Promise { - const request = { - method: 'createIntegration' as const, - params: integration - } - - await this.rpc(request) - } - - async updateIntegration (integration: Integration): Promise { - const request = { - method: 'updateIntegration' as const, - params: integration - } - - await this.rpc(request) - } - - async deleteIntegration (integrationKey: IntegrationKey): Promise { - const request = { - method: 'deleteIntegration' as const, - params: integrationKey - } - - await this.rpc(request) - } - - async getIntegration (integrationKey: IntegrationKey): Promise { - const request = { - method: 'getIntegration' as const, - params: integrationKey - } - - return await this.rpc(request) - } - - async listIntegrations (filter: Partial): Promise { - const request = { - method: 'listIntegrations' as const, - params: filter - } - - return await this.rpc(request) - } - - async addIntegrationSecret (integrationSecret: IntegrationSecret): Promise { - const request = { - method: 'addIntegrationSecret' as const, - params: integrationSecret - } - - await this.rpc(request) - } - - async updateIntegrationSecret (integrationSecret: IntegrationSecret): Promise { - const request = { - method: 'updateIntegrationSecret' as const, - params: integrationSecret - } - - await this.rpc(request) - } - - async deleteIntegrationSecret (integrationSecretKey: IntegrationSecretKey): Promise { - const request = { - method: 'deleteIntegrationSecret' as const, - params: integrationSecretKey - } - - await this.rpc(request) - } - - async getIntegrationSecret (integrationSecretKey: IntegrationSecretKey): Promise { - const request = { - method: 'getIntegrationSecret' as const, - params: integrationSecretKey - } - - return await this.rpc(request) - } - - async listIntegrationsSecrets (filter: Partial): Promise { - const request = { - method: 'listIntegrationsSecrets' as const, - params: filter - } - - return await this.rpc(request) - } - - async getAccountInfo (uuid: AccountUuid): Promise { - const request = { - method: 'getAccountInfo' as const, - params: { accountId: uuid } - } - - return await this.rpc(request) - } - - async canMergeSpecifiedPersons (primaryPerson: PersonUuid, secondaryPerson: PersonUuid): Promise { - const request = { - method: 'canMergeSpecifiedPersons' as const, - params: { primaryPerson, secondaryPerson } - } - - return await this.rpc(request) - } - - async mergeSpecifiedPersons (primaryPerson: PersonUuid, secondaryPerson: PersonUuid): Promise { - const request = { - method: 'mergeSpecifiedPersons' as const, - params: { primaryPerson, secondaryPerson } - } - - await this.rpc(request) - } - - async mergeSpecifiedAccounts (primaryAccount: AccountUuid, secondaryAccount: AccountUuid): Promise { - const request = { - method: 'mergeSpecifiedAccounts' as const, - params: { primaryAccount, secondaryAccount } - } - - await this.rpc(request) - } - - async addEmailSocialId (email: string): Promise { - const request = { - method: 'addEmailSocialId' as const, - params: { email } - } - - return await this.rpc(request) - } - - async addHulyAssistantSocialId (): Promise { - const request = { - method: 'addHulyAssistantSocialId' as const, - params: {} - } - - return await this.rpc(request) - } - - async refreshHulyAssistantToken (): Promise { - const request = { - method: 'refreshHulyAssistantToken' as const, - params: {} - } - - await this.rpc(request) - } - - async setCookie (): Promise { - const url = concatLink(this.url, '/cookie') - const response = await fetch(url, { ...this.request, method: 'PUT' }) - - if (!response.ok) { - const result = await response.json() - if (result.error != null) { - throw new PlatformError(result.error) - } - } - } - - async deleteCookie (): Promise { - const url = concatLink(this.url, '/cookie') - const response = await fetch(url, { ...this.request, method: 'DELETE' }) - - if (!response.ok) { - const result = await response.json() - if (result.error != null) { - throw new PlatformError(result.error) - } - } - } - - async setMyProfile (profile: Partial>): Promise { - const request = { - method: 'setMyProfile', - params: { - profile - } - } - - await this._rpc(request) - } - - async getUserProfile (personUuid?: PersonUuid): Promise { - return await this._rpc({ - method: 'getUserProfile', - params: { - personUuid - } - }) - } - - async getSubscriptions ( - workspaceUuid: WorkspaceUuid | undefined = undefined, - activeOnly: boolean = true - ): Promise { - return await this._rpc({ - method: 'getSubscriptions', - params: { - workspaceUuid, - activeOnly - } - }) - } - - async getSubscriptionByProviderId (provider: string, providerSubscriptionId: string): Promise { - return await this._rpc({ - method: 'getSubscriptionByProviderId', - params: { - provider, - providerSubscriptionId - } - }) - } - - async getSubscriptionById (subscriptionId: string): Promise { - return await this._rpc({ - method: 'getSubscriptionById', - params: { - subscriptionId - } - }) - } - - async upsertSubscription (subscription: SubscriptionData): Promise { - await this._rpc({ - method: 'upsertSubscription', - params: subscription - }) - } -} - -function withRetry Promise> ( - f: F, - shouldFail: (err: any, attempt: number) => boolean, - intervalMs: number = 25 -): F { - return async function (...params: any[]): Promise { - let attempt = 0 - while (true) { - try { - return await f(...params) - } catch (err: any) { - if (shouldFail(err, attempt)) { - throw err - } - - attempt++ - await new Promise((resolve) => setTimeout(resolve, intervalMs)) - if (intervalMs < 1000) { - intervalMs += 100 - } - } - } - } as F -} - -const connectionErrorCodes = ['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND'] - -function withRetryUntilTimeout Promise> (f: F, timeoutMs: number = 5000): F { - const timeout = Date.now() + timeoutMs - const shouldFail = (err: any): boolean => !connectionErrorCodes.includes(err?.cause?.code) || timeout < Date.now() - - return withRetry(f, shouldFail) -} - -function withRetryUntilMaxAttempts Promise> (f: F, maxAttempts: number = 5): F { - const shouldFail = (err: any, attempt: number): boolean => - !connectionErrorCodes.includes(err?.cause?.code) || attempt === maxAttempts - - return withRetry(f, shouldFail) -} diff --git a/foundations/core/packages/account-client/src/index.ts b/foundations/core/packages/account-client/src/index.ts deleted file mode 100644 index b487cf8270..0000000000 --- a/foundations/core/packages/account-client/src/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export * from './client' -export * from './types' -export * from './utils' diff --git a/foundations/core/packages/account-client/src/types.ts b/foundations/core/packages/account-client/src/types.ts deleted file mode 100644 index 748d35196d..0000000000 --- a/foundations/core/packages/account-client/src/types.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { - type AccountUuid, - PersonId, - WorkspaceDataId, - WorkspaceUuid, - type AccountRole, - type Timestamp, - type SocialId as SocialIdBase, - PersonUuid, - type WorkspaceMode, - Person, - WorkspaceInfo, - AccountInfo, - IntegrationKind -} from '@hcengineering/core' - -export interface LoginInfo { - account: AccountUuid - name?: string - socialId?: PersonId - token?: string -} - -export interface EndpointInfo { - internalUrl: string - externalUrl: string - region: string -} -export interface WorkspaceVersion { - versionMajor: number - versionMinor: number - versionPatch: number -} - -export interface LoginInfoWorkspace { - url: string - dataId?: WorkspaceDataId - mode: WorkspaceMode - version: WorkspaceVersion - endpoint: EndpointInfo - role: AccountRole | null - progress?: number - branding?: string -} - -export interface LoginInfoWithWorkspaces extends LoginInfo { - // Information necessary to handle user <--> transactor connectivity. - workspaces: Record - socialIds: SocialId[] -} - -export type LoginInfoByToken = LoginInfo | WorkspaceLoginInfo | LoginInfoRequest | null - -/** - * @public - */ -export interface WorkspaceLoginInfo extends LoginInfo { - workspace: WorkspaceUuid // worspace uuid - workspaceDataId?: WorkspaceDataId - workspaceUrl: string - endpoint: string - token: string - role: AccountRole - allowGuestSignUp?: boolean -} - -export interface LoginInfoRequestData { - firstName?: string - lastName?: string -} - -export type LoginInfoRequest = { - request: true -} & LoginInfoRequestData - -export interface WorkspaceInviteInfo { - workspace: WorkspaceUuid - email?: string - name?: string -} - -export interface OtpInfo { - sent: boolean - retryOn: Timestamp -} - -export interface RegionInfo { - region: string - name: string -} - -export type WorkspaceOperation = 'create' | 'upgrade' | 'all' | 'all+backup' - -export interface MailboxOptions { - availableDomains: string[] - minNameLength: number - maxNameLength: number - maxMailboxCount: number -} - -export interface MailboxInfo { - mailbox: string - aliases: string[] - appPasswords: string[] -} - -export interface MailboxSecret { - mailbox: string - app?: string - secret: string -} - -export interface Integration { - socialId: PersonId - kind: IntegrationKind // Integration kind. E.g. 'github', 'mail', 'telegram-bot', 'telegram' etc. - workspaceUuid: WorkspaceUuid | null - data?: Record - disabled?: boolean -} - -export interface SocialId extends SocialIdBase { - personUuid: PersonUuid - isDeleted?: boolean -} - -export type IntegrationKey = Omit - -export interface IntegrationSecret { - socialId: PersonId - kind: IntegrationKind // Integration kind. E.g. 'github', 'mail', 'telegram-bot', 'telegram' etc. - workspaceUuid: WorkspaceUuid | null - key: string // Key for the secret in the integration. Different secrets for the same integration must have different keys. Can be any string. E.g. '', 'user_app_1' etc. - secret: string -} - -export type IntegrationSecretKey = Omit - -export interface ProviderInfo { - name: string - displayName?: string -} - -export interface AccountAggregatedInfo extends AccountInfo, Person { - uuid: AccountUuid - integrations: Omit[] - socialIds: SocialId[] - workspaces: Omit[] -} - -/** - * User profile with additional information for public sharing - * Stored in accounts database (global, not workspace-specific) - */ -export interface UserProfile { - personUuid: PersonUuid - bio?: string // LinkedIn-style bio (up to ~2000 chars) - city?: string - country?: string - website?: string // Personal website URL - socialLinks?: Record // Flexible storage for social links - isPublic: boolean // Public visibility toggle (default: false) -} - -export type PersonWithProfile = Person & Omit - -/** - * Subscription status enum - * Reflects the subscription lifecycle from active to canceled/expired - */ -export enum SubscriptionStatus { - Active = 'active', // Subscription is active and paid - Trialing = 'trialing', // In trial period (free usage) - PastDue = 'past_due', // Payment failed but subscription not yet canceled - Canceled = 'canceled', // Subscription was canceled by user or admin - Paused = 'paused', // Subscription is temporarily paused (some providers support this) - Expired = 'expired' // Subscription or trial has expired -} - -/** - * Subscription type/purpose - * Allows multiple active subscriptions per workspace for different purposes - */ -export enum SubscriptionType { - Tier = 'tier', // Main workspace tier (free, starter, pro, enterprise) - Support = 'support' // Voluntary support/donation subscription -} - -/** - * Workspace subscription information - * Provider-agnostic subscription data managed by billing service - * Multiple subscriptions can be active per workspace (tier + addons + support) - * Historical subscriptions are preserved with status: canceled/expired - */ -export interface Subscription { - id: string // Our internal unique subscription ID (UUID) - workspaceUuid: WorkspaceUuid - accountUuid: AccountUuid // Account that paid for the subscription - - // Provider details - provider: string // Payment provider identifier (e.g. 'polar', 'stripe', 'manual') - providerSubscriptionId: string // External subscription ID from the provider - providerCheckoutId?: string // External checkout/session ID that created this subscription - - // Subscription classification - type: SubscriptionType // What this subscription is for (tier, addon, support) - status: SubscriptionStatus // Current status - plan: string // Plan/product identifier (e.g. 'free', 'pro', 'storage-100gb', 'supporter') - - // Amount paid (in cents, e.g. 9999 = $99.99) - // Used primarily for pay-what-you-want/donation subscriptions to track actual payment - amount?: number - - // Billing period (optional - not set for free/manual plans) - periodStart?: Timestamp - periodEnd?: Timestamp - - // Trial information (optional) - trialEnd?: Timestamp - - // Cancellation tracking (optional) - canceledAt?: Timestamp - - // Provider-specific data stored as JSONB (optional) - providerData?: Record - - // Timestamps (managed by database) - createdOn: Timestamp - updatedOn: Timestamp -} - -/** - * Subscription data for creating/updating subscriptions (without timestamps) - * Used by billing service to upsert subscription data - */ -export type SubscriptionData = Omit diff --git a/foundations/core/packages/account-client/src/utils.ts b/foundations/core/packages/account-client/src/utils.ts deleted file mode 100644 index a8afa6a817..0000000000 --- a/foundations/core/packages/account-client/src/utils.ts +++ /dev/null @@ -1,33 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { LoginInfoByToken, LoginInfoRequest, WorkspaceLoginInfo } from './types' - -export function isWorkspaceLoginInfo (loginInfo: LoginInfoByToken): loginInfo is WorkspaceLoginInfo { - return !isLoginInfoRequest(loginInfo) && (loginInfo as WorkspaceLoginInfo)?.workspace != null -} - -export function isLoginInfoRequest (info: LoginInfoByToken): info is LoginInfoRequest { - return (info as LoginInfoRequest)?.request -} - -export function getClientTimezone (): string | undefined { - try { - return Intl.DateTimeFormat().resolvedOptions().timeZone - } catch (err: any) { - console.error('Failed to get client timezone', err) - return undefined - } -} diff --git a/foundations/core/packages/account-client/tsconfig.json b/foundations/core/packages/account-client/tsconfig.json deleted file mode 100644 index b5ae22f6e4..0000000000 --- a/foundations/core/packages/account-client/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/foundations/core/packages/analytics-service/.eslintrc.js b/foundations/core/packages/analytics-service/.eslintrc.js deleted file mode 100644 index 72235dc283..0000000000 --- a/foundations/core/packages/analytics-service/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/foundations/core/packages/analytics-service/.npmignore b/foundations/core/packages/analytics-service/.npmignore deleted file mode 100644 index e3ec093c38..0000000000 --- a/foundations/core/packages/analytics-service/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!/lib/** -!CHANGELOG.md -/lib/**/__tests__/ diff --git a/foundations/core/packages/analytics-service/CHANGELOG.json b/foundations/core/packages/analytics-service/CHANGELOG.json deleted file mode 100644 index 347b4621cf..0000000000 --- a/foundations/core/packages/analytics-service/CHANGELOG.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "name": "@hcengineering/analytics-service", - "entries": [ - { - "version": "0.7.17", - "tag": "@hcengineering/analytics-service_v0.7.17", - "date": "Mon, 27 Oct 2025 13:27:12 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" - } - ] - } - }, - { - "version": "0.7.5", - "tag": "@hcengineering/analytics-service_v0.7.5", - "date": "Tue, 14 Oct 2025 04:58:17 GMT", - "comments": { - "patch": [ - { - "comment": "update deps" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.4` to `0.7.5`" - }, - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`" - }, - { - "comment": "Updating dependency \"@hcengineering/analytics\" from `^0.7.4` to `0.7.5`" - } - ] - } - }, - { - "version": "0.7.4", - "tag": "@hcengineering/analytics-service_v0.7.4", - "date": "Sat, 11 Oct 2025 18:20:33 GMT", - "comments": { - "patch": [ - { - "comment": "Update to latest platform rig" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.3` to `0.7.4`" - }, - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`" - }, - { - "comment": "Updating dependency \"@hcengineering/analytics\" from `^0.7.3` to `0.7.4`" - } - ] - } - }, - { - "version": "0.7.3", - "tag": "@hcengineering/analytics-service_v0.7.3", - "date": "Wed, 08 Oct 2025 03:40:53 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`" - } - ] - } - } - ] -} diff --git a/foundations/core/packages/analytics-service/CHANGELOG.md b/foundations/core/packages/analytics-service/CHANGELOG.md deleted file mode 100644 index 044186394a..0000000000 --- a/foundations/core/packages/analytics-service/CHANGELOG.md +++ /dev/null @@ -1,28 +0,0 @@ -# Change Log - @hcengineering/analytics-service - -This log was last generated on Mon, 27 Oct 2025 13:27:12 GMT and should not be manually modified. - -## 0.7.17 -Mon, 27 Oct 2025 13:27:12 GMT - -_Version update only_ - -## 0.7.5 -Tue, 14 Oct 2025 04:58:17 GMT - -### Patches - -- update deps - -## 0.7.4 -Sat, 11 Oct 2025 18:20:33 GMT - -### Patches - -- Update to latest platform rig - -## 0.7.3 -Wed, 08 Oct 2025 03:40:53 GMT - -_Initial release_ - diff --git a/foundations/core/packages/analytics-service/config/rig.json b/foundations/core/packages/analytics-service/config/rig.json deleted file mode 100644 index 0110930f55..0000000000 --- a/foundations/core/packages/analytics-service/config/rig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig" -} diff --git a/foundations/core/packages/analytics-service/jest.config.js b/foundations/core/packages/analytics-service/jest.config.js deleted file mode 100644 index 069ea8aa0d..0000000000 --- a/foundations/core/packages/analytics-service/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ['./src'], - coverageReporters: ['text-summary', 'html', 'lcov'] -} diff --git a/foundations/core/packages/analytics-service/package.json b/foundations/core/packages/analytics-service/package.json deleted file mode 100644 index 4070788497..0000000000 --- a/foundations/core/packages/analytics-service/package.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "name": "@hcengineering/analytics-service", - "version": "0.7.17", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "files": [ - "lib/**/*", - "types/**/*", - "src/**/*", - "!src/**/__test__/**", - "tsconfig.json" - ], - "author": "Anticrm Platform Contributors", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "build:watch": "compile", - "test": "jest --passWithNoTests --silent --coverage", - "format": "format src", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --coverage", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "workspace:^0.7.19", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "@typescript-eslint/parser": "^6.21.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.6.2", - "typescript": "^5.9.3", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5", - "eslint-plugin-svelte": "^2.35.1" - }, - "dependencies": { - "@hcengineering/platform": "workspace:^0.7.18", - "@hcengineering/core": "workspace:^0.7.22", - "@hcengineering/analytics": "workspace:^0.7.17", - "@hcengineering/measurements-otlp": "workspace:^0.7.17", - "winston": "^3.11.0", - "winston-daily-rotate-file": "^5.0.0" - }, - "repository": "https://github.com/hcengineering/huly.core", - "publishConfig": { - "access": "public" - }, - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.js" - } - } -} diff --git a/foundations/core/packages/analytics-service/src/index.ts b/foundations/core/packages/analytics-service/src/index.ts deleted file mode 100644 index 3f7c9a2ff6..0000000000 --- a/foundations/core/packages/analytics-service/src/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// - -import { AnalyticProvider, Analytics } from '@hcengineering/analytics' -import { initOpenTelemetrySDK, reportOTELError } from '@hcengineering/measurements-otlp' - -export * from '@hcengineering/measurements-otlp' -export * from './logging' - -class OTELAnalyticsProvider implements AnalyticProvider { - init (config: Record): boolean { - return true - } - - setUser: (email: string, data: any) => void = (email, data) => {} - - setAlias: (distinctId: string, alias: string) => void = (distinctId, alias) => {} - - setTag: (key: string, value: string) => void = (key, value) => {} - - setWorkspace: (ws: string, guest: boolean) => void = (ws, guest) => {} - - handleEvent: (event: string, params: Record) => void = (event, params) => {} - - handleError (error: Error): void { - reportOTELError(error) - } - - navigate (path: string): void {} - - logout (): void {} -} - -export function configureAnalytics (serviceName: string, serviceVersion: string, config?: Record): void { - const providers: AnalyticProvider[] = [new OTELAnalyticsProvider()] - - initOpenTelemetrySDK(serviceName, serviceVersion) - for (const provider of providers) { - Analytics.init(provider, config ?? {}) - } -} diff --git a/foundations/core/packages/analytics-service/src/logging.ts b/foundations/core/packages/analytics-service/src/logging.ts deleted file mode 100644 index 2ccf0db338..0000000000 --- a/foundations/core/packages/analytics-service/src/logging.ts +++ /dev/null @@ -1,121 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -import { MeasureLogger, ParamsType } from '@hcengineering/core' -import { basename, dirname, join } from 'path' -import winston from 'winston' -import DailyRotateFile from 'winston-daily-rotate-file' - -export class SplitLogger implements MeasureLogger { - logger: winston.Logger - - constructor ( - readonly name: string, - readonly opts: { root?: string, parent?: winston.Logger, pretty?: boolean, enableConsole?: boolean } - ) { - const rootDir = this.opts.root ?? 'logs' - - this.logger = winston.createLogger({ - level: 'info', - exitOnError: false - }) - const errorPrinter = ({ message, stack, ...rest }: Error): object => ({ - message, - stack, - ...rest - }) - const jsonOptions: winston.Logform.JsonOptions = { - replacer: (key, value) => { - return value instanceof Error ? errorPrinter(value) : value - } - } - this.logger.add( - new DailyRotateFile({ - format: winston.format.combine( - winston.format.timestamp(), - opts.pretty === true ? winston.format.prettyPrint() : winston.format.json(jsonOptions) - ), - filename: `${name}-combined-%DATE%.log`, - auditFile: join(rootDir, `${basename(name)}-audit.log`), - dirname: rootDir, - datePattern: 'YYYY-MM-DD', - zippedArchive: true, - maxSize: '20m', - maxFiles: '14d' - }) - ) - this.logger.add( - new DailyRotateFile({ - format: winston.format.combine(winston.format.timestamp(), winston.format.prettyPrint()), - filename: `${name}-error-%DATE%.log`, - auditFile: join(rootDir, `${basename(name)}-audit.log`), - level: 'error', - dirname: rootDir, - datePattern: 'YYYY-MM-DD', - zippedArchive: true, - maxSize: '20m', - maxFiles: '14d' - }) - ) - if (opts.parent === undefined && opts.enableConsole === true) { - console.log('Logging also into console', process.env.NODE_ENV, opts.enableConsole) - this.logger.add( - new winston.transports.Console({ - format: winston.format.combine( - winston.format.timestamp(), - winston.format.json(jsonOptions), - winston.format.colorize({ all: true }) - ) - }) - ) - } - this.logger.info( - '####################################################################################################################' - ) - this.logger.info( - `########################SplitLogger ${this.name} initialized: ${new Date().toISOString()}###########################` - ) - } - - error (message: string, obj?: Record): void { - if (this.opts.parent !== undefined) { - this.opts.parent.error({ message, ...obj }) - } - this.logger.error({ message, ...obj }) - } - - info (message: string, obj?: Record): void { - if (this.opts.parent !== undefined && this.opts.enableConsole === true) { - // Only propogate if enable console is true - this.opts.parent.info({ message, ...obj }) - } - this.logger.info({ message, ...obj }) - } - - warn (message: string, obj?: Record): void { - if (this.opts.parent !== undefined) { - this.opts.parent.warn({ message, ...obj }) - } - this.logger.warn({ message, ...obj }) - } - - logOperation (operation: string, time: number, params: ParamsType): void { - this.logger.info(operation, { time, ...params }) - } - - childLogger (name: string, params: Record): MeasureLogger { - const dirName = dirname(name) - const { enableConsole, ...otherParams } = params - const child = this.logger.child({ name, ...otherParams }) - return new SplitLogger(name, { - ...this.opts, - parent: child, - root: join(this.opts.root ?? 'logs', dirName), - enableConsole: enableConsole === 'true' - }) - } - - async close (): Promise { - this.logger.close() - } -} diff --git a/foundations/core/packages/analytics-service/tsconfig.json b/foundations/core/packages/analytics-service/tsconfig.json deleted file mode 100644 index b5ae22f6e4..0000000000 --- a/foundations/core/packages/analytics-service/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/foundations/core/packages/analytics/.eslintrc.js b/foundations/core/packages/analytics/.eslintrc.js deleted file mode 100644 index 72235dc283..0000000000 --- a/foundations/core/packages/analytics/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/foundations/core/packages/analytics/.npmignore b/foundations/core/packages/analytics/.npmignore deleted file mode 100644 index e3ec093c38..0000000000 --- a/foundations/core/packages/analytics/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!/lib/** -!CHANGELOG.md -/lib/**/__tests__/ diff --git a/foundations/core/packages/analytics/CHANGELOG.json b/foundations/core/packages/analytics/CHANGELOG.json deleted file mode 100644 index 5714d257f5..0000000000 --- a/foundations/core/packages/analytics/CHANGELOG.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "name": "@hcengineering/analytics", - "entries": [ - { - "version": "0.7.17", - "tag": "@hcengineering/analytics_v0.7.17", - "date": "Fri, 31 Oct 2025 20:11:13 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.17` to `0.7.18`" - } - ] - } - }, - { - "version": "0.7.5", - "tag": "@hcengineering/analytics_v0.7.5", - "date": "Tue, 14 Oct 2025 04:58:17 GMT", - "comments": { - "patch": [ - { - "comment": "update deps" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.4` to `0.7.5`" - } - ] - } - }, - { - "version": "0.7.4", - "tag": "@hcengineering/analytics_v0.7.4", - "date": "Sat, 11 Oct 2025 18:20:33 GMT", - "comments": { - "patch": [ - { - "comment": "Update to latest platform rig" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.3` to `0.7.4`" - } - ] - } - } - ] -} diff --git a/foundations/core/packages/analytics/CHANGELOG.md b/foundations/core/packages/analytics/CHANGELOG.md deleted file mode 100644 index 12e6001636..0000000000 --- a/foundations/core/packages/analytics/CHANGELOG.md +++ /dev/null @@ -1,23 +0,0 @@ -# Change Log - @hcengineering/analytics - -This log was last generated on Fri, 31 Oct 2025 20:11:13 GMT and should not be manually modified. - -## 0.7.17 -Fri, 31 Oct 2025 20:11:13 GMT - -_Version update only_ - -## 0.7.5 -Tue, 14 Oct 2025 04:58:17 GMT - -### Patches - -- update deps - -## 0.7.4 -Sat, 11 Oct 2025 18:20:33 GMT - -### Patches - -- Update to latest platform rig - diff --git a/foundations/core/packages/analytics/config/rig.json b/foundations/core/packages/analytics/config/rig.json deleted file mode 100644 index 0110930f55..0000000000 --- a/foundations/core/packages/analytics/config/rig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig" -} diff --git a/foundations/core/packages/analytics/jest.config.js b/foundations/core/packages/analytics/jest.config.js deleted file mode 100644 index 069ea8aa0d..0000000000 --- a/foundations/core/packages/analytics/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ['./src'], - coverageReporters: ['text-summary', 'html', 'lcov'] -} diff --git a/foundations/core/packages/analytics/package.json b/foundations/core/packages/analytics/package.json deleted file mode 100644 index bb78bafcb7..0000000000 --- a/foundations/core/packages/analytics/package.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "name": "@hcengineering/analytics", - "version": "0.7.17", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "files": [ - "lib/**/*", - "types/**/*", - "src/**/*", - "!src/**/__test__/**", - "tsconfig.json" - ], - "author": "Anticrm Platform Contributors", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "build:watch": "compile", - "test": "jest --passWithNoTests --silent --coverage", - "format": "format src", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --coverage", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "workspace:^0.7.19", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "@typescript-eslint/parser": "^6.21.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.6.2", - "typescript": "^5.9.3", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5", - "eslint-plugin-svelte": "^2.35.1" - }, - "dependencies": { - "@hcengineering/platform": "workspace:^0.7.18" - }, - "repository": "https://github.com/hcengineering/huly.core", - "publishConfig": { - "access": "public" - }, - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.js" - } - } -} diff --git a/foundations/core/packages/analytics/src/index.ts b/foundations/core/packages/analytics/src/index.ts deleted file mode 100644 index 31f28b4a02..0000000000 --- a/foundations/core/packages/analytics/src/index.ts +++ /dev/null @@ -1,84 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc -// - -import { addEventListener, PlatformEvent, Severity, Status, translate } from '@hcengineering/platform' - -export const providers: AnalyticProvider[] = [] -export interface AnalyticProvider { - init: (config: Record) => boolean - setUser: (email: string, data: any) => void - setAlias: (distinctId: string, alias: string) => void - setTag: (key: string, value: string) => void - setWorkspace: (ws: string, guest: boolean) => void - handleEvent: (event: string, params: Record) => void - handleError: (error: Error) => void - navigate: (path: string) => void - logout: () => void -} - -export const Analytics = { - data: {}, - - init (provider: AnalyticProvider, config: Record): void { - const res = provider.init(config) - if (res) { - providers.push(provider) - } - }, - - setUser (email: string, data: any): void { - providers.forEach((provider) => { - provider.setUser(email, data) - }) - }, - - setAlias (distinctId: string, alias: string): void { - providers.forEach((provider) => { - provider.setAlias(distinctId, alias) - }) - }, - - setTag (key: string, value: string): void { - providers.forEach((provider) => { - provider.setTag(key, value) - }) - }, - - setWorkspace (ws: string, guest: boolean): void { - providers.forEach((provider) => { - provider.setWorkspace(ws, guest) - }) - }, - - handleEvent (event: string, params: Record = {}): void { - providers.forEach((provider) => { - provider.handleEvent(event, { ...this.data, ...params }) - }) - }, - - handleError (error: Error): void { - providers.forEach((provider) => { - provider.handleError(error) - }) - }, - - navigate (path: string): void { - providers.forEach((provider) => { - provider.navigate(path) - }) - }, - - logout (): void { - providers.forEach((provider) => { - provider.logout() - }) - } -} - -addEventListener(PlatformEvent, async (_event, _status: Status) => { - if (_status.severity === Severity.ERROR) { - const label = await translate(_status.code, _status.params, 'en') - Analytics.handleError(new Error(label)) - } -}) diff --git a/foundations/core/packages/analytics/tsconfig.json b/foundations/core/packages/analytics/tsconfig.json deleted file mode 100644 index b5ae22f6e4..0000000000 --- a/foundations/core/packages/analytics/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/foundations/core/packages/api-client/.eslintrc.js b/foundations/core/packages/api-client/.eslintrc.js deleted file mode 100644 index 72235dc283..0000000000 --- a/foundations/core/packages/api-client/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/foundations/core/packages/api-client/.npmignore b/foundations/core/packages/api-client/.npmignore deleted file mode 100644 index e3ec093c38..0000000000 --- a/foundations/core/packages/api-client/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!/lib/** -!CHANGELOG.md -/lib/**/__tests__/ diff --git a/foundations/core/packages/api-client/CHANGELOG.json b/foundations/core/packages/api-client/CHANGELOG.json deleted file mode 100644 index 1f381bf589..0000000000 --- a/foundations/core/packages/api-client/CHANGELOG.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "name": "@hcengineering/api-client", - "entries": [ - { - "version": "0.7.18", - "tag": "@hcengineering/api-client_v0.7.18", - "date": "Mon, 27 Oct 2025 17:09:21 GMT", - "comments": { - "patch": [ - { - "comment": "add support for textColor and textStyle marks" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/text\" from `^0.7.17` to `0.7.18`" - } - ] - } - }, - { - "version": "0.7.17", - "tag": "@hcengineering/api-client_v0.7.17", - "date": "Mon, 27 Oct 2025 13:27:12 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" - } - ] - } - }, - { - "version": "0.7.5", - "tag": "@hcengineering/api-client_v0.7.5", - "date": "Tue, 14 Oct 2025 04:58:17 GMT", - "comments": { - "patch": [ - { - "comment": "update deps" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/account-client\" from `^0.7.4` to `0.7.5`" - }, - { - "comment": "Updating dependency \"@hcengineering/client\" from `^0.7.5` to `0.7.6`" - }, - { - "comment": "Updating dependency \"@hcengineering/client-resources\" from `^0.7.5` to `0.7.6`" - }, - { - "comment": "Updating dependency \"@hcengineering/collaborator-client\" from `^0.7.4` to `0.7.5`" - }, - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`" - }, - { - "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.4` to `0.7.5`" - }, - { - "comment": "Updating dependency \"@hcengineering/text\" from `^0.7.4` to `0.7.5`" - }, - { - "comment": "Updating dependency \"@hcengineering/text-markdown\" from `^0.7.4` to `0.7.5`" - } - ] - } - }, - { - "version": "0.7.4", - "tag": "@hcengineering/api-client_v0.7.4", - "date": "Sat, 11 Oct 2025 18:20:33 GMT", - "comments": { - "patch": [ - { - "comment": "Update to latest platform rig" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/account-client\" from `^0.7.3` to `0.7.4`" - }, - { - "comment": "Updating dependency \"@hcengineering/client\" from `^0.7.4` to `0.7.5`" - }, - { - "comment": "Updating dependency \"@hcengineering/client-resources\" from `^0.7.4` to `0.7.5`" - }, - { - "comment": "Updating dependency \"@hcengineering/collaborator-client\" from `^0.7.3` to `0.7.4`" - }, - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`" - }, - { - "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.3` to `0.7.4`" - }, - { - "comment": "Updating dependency \"@hcengineering/text\" from `^0.7.3` to `0.7.4`" - }, - { - "comment": "Updating dependency \"@hcengineering/text-markdown\" from `^0.7.3` to `0.7.4`" - } - ] - } - }, - { - "version": "0.7.3", - "tag": "@hcengineering/api-client_v0.7.3", - "date": "Wed, 08 Oct 2025 03:40:53 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`" - } - ] - } - } - ] -} diff --git a/foundations/core/packages/api-client/CHANGELOG.md b/foundations/core/packages/api-client/CHANGELOG.md deleted file mode 100644 index 5698adeb18..0000000000 --- a/foundations/core/packages/api-client/CHANGELOG.md +++ /dev/null @@ -1,35 +0,0 @@ -# Change Log - @hcengineering/api-client - -This log was last generated on Mon, 27 Oct 2025 17:09:21 GMT and should not be manually modified. - -## 0.7.18 -Mon, 27 Oct 2025 17:09:21 GMT - -### Patches - -- add support for textColor and textStyle marks - -## 0.7.17 -Mon, 27 Oct 2025 13:27:12 GMT - -_Version update only_ - -## 0.7.5 -Tue, 14 Oct 2025 04:58:17 GMT - -### Patches - -- update deps - -## 0.7.4 -Sat, 11 Oct 2025 18:20:33 GMT - -### Patches - -- Update to latest platform rig - -## 0.7.3 -Wed, 08 Oct 2025 03:40:53 GMT - -_Initial release_ - diff --git a/foundations/core/packages/api-client/README.md b/foundations/core/packages/api-client/README.md deleted file mode 100644 index eea04475dd..0000000000 --- a/foundations/core/packages/api-client/README.md +++ /dev/null @@ -1,446 +0,0 @@ -# Huly Platform API Client - -A TypeScript client library for interacting with the Huly Platform API. - -## Installation - -In order to be able to install required packages, you will need to obtain GitHub access token. You can create a token by following the instructions [here](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry#authenticating-with-a-personal-access-token). - -```bash -npm install @hcengineering/api-client -``` - -## WebSocket Client vs REST Client - -The api client package provides two main client variants: a WebSocket client and a REST client. The WebSocket client holds persistent connection to the Huly Platform API. The REST client uses standard HTTP requests to perform operations. - -### WebSocket Client - -```ts -import { connect } from '@hcengineering/api-client' - -// Connect to Huly -const client = await connect('https://huly.app', { - email: 'johndoe@example.com', - password: 'password', - workspace: 'my-workspace', -}) - -// Use the client to perform operations -... - -// Close the client when done -await client.close() -``` - -### REST Client - -```ts -import { connectRest } from '@hcengineering/api-client' - -// Connect to Huly -const client = await connectRest('https://huly.app', { - email: 'johndoe@example.com', - password: 'password', - workspace: 'my-workspace' -}) - -// Use the client to perform operations -... - -``` - -## Authentication - -The client supports two authentication methods: using email and password, or using a token. -When authenticated, the client will have access to the same resources as the user. - -> Note: The examples below use the WebSocket client (`connect`). To use the REST client instead, import and call `connectRest` with the same options. - -Parameters: - -- `url`: URL of the Huly instance, for Huly Cloud use `https://huly.app` -- `options`: Connection options - - `workspace`: Name of the workspace to connect to, the workspace name can be found in the URL of the workspace: `https://huly.app/workbench/` - - `token`: Optional authentication token - - `email`: Optional user email - - `password`: Optional user password - -### Using Email and Password - -```ts -import { connect } from '@hcengineering/api-client' - -const client = await connect('https://huly.app', { - email: 'johndoe@example.com', - password: 'password', - workspace: 'my-workspace' -}) - -... - -await client.close() -``` - -### Using Token - -```ts -import { connect } from '@hcengineering/api-client' - -const client = await connect('https://huly.app', { - token: '...', - workspace: 'my-workspace' -}) - -... - -await client.close() -``` - -## Client API - -The client provides a set of methods for interacting with the Huly Platform API. This section describes the main methods available in the client. - -### Fetch API - -The client provides two main methods for retrieving documents: `findOne` and `findAll`. - -#### findOne - -Retrieves a single document matching the query criteria. - -Parameters: - -- `_class`: Class of the object to find, results will include all subclasses of the target class -- `query`: Query criteria -- `options`: Find options - - `limit`: Limit the number of results returned - - `sort`: Sorting criteria - - `lookup`: Lookup criteria - - `projection`: Projection criteria - - `total`: If specified total will be returned - -Example: - -```ts -import contact from '@hcengineering/contact' - -... - -const person = await client.findOne( - contact.class.Person, - { - _id: 'person-id' - } -) -``` - -#### findAll - -Retrieves multiple document matching the query criteria. - -Parameters: - -- `_class`: Class of the object to find, results will include all subclasses of the target class -- `query`: Query criteria -- `options`: Find options - - `limit`: Limit the number of results returned - - `sort`: Sorting criteria - - `lookup`: Lookup criteria - - `projection`: Projection criteria - - `total`: If specified total will be returned - -Example: - -```ts -import { SortingOrder } from '@hcengineering/core' -import contact from '@hcengineering/contact' - -.. - -const persons = await client.findAll( - contact.class.Person, - { - city: 'New York' - }, - { - limit: 10, - sort: { - name: SortingOrder.Ascending - } - } -) -``` - -### Documents API - -The client provides three main methods for managing documents: `createDoc`, `updateDoc`, and `removeDoc`. These methods allow you to perform CRUD operations on documents. - -#### createDoc - -Creates a new document in the specified space. - -Parameters: - -- `_class`: Class of the object -- `space`: Space of the object -- `attributes`: Attributes of the object -- `id`: Optional id of the object, if not provided, a new id will be generated - -Example: - -```ts -import contact, { AvatarType } from '@hcengineering/contact' - -.. - -const personId = await client.createDoc( - contact.class.Person, - contact.space.Contacts, - { - name: 'Doe,John', - city: 'New York', - avatarType: AvatarType.COLOR - } -) -``` - -#### updateDoc - -Updates existing document. - -Parameters: - -- `_class`: Class of the object -- `space`: Space of the object -- `objectId`: Id of the object -- `operations`: Attributes of the object to update - -Example: - -```ts -import contact from '@hcengineering/contact' - -.. - -await client.updateDoc( - contact.class.Person, - contact.space.Contacts, - personId, - { - city: 'New York', - } -) -``` - -#### removeDoc - -Removes existing document. - -Parameters: - -- `_class`: Class of the object -- `space`: Space of the object -- `objectId`: Id of the object - -Example: - -```ts -import contact from '@hcengineering/contact' - -.. - -await client.removeDoc( - contact.class.Person, - contact.space.Contacts, - personId -) -``` - -### Collections API - -#### addCollection - -Creates a new attached document in the specified collection. - -Parameters: - -- `_class`: Class of the object to create -- `space`: Space of the object to create -- `attachedTo`: Id of the object to attach to -- `attachedToClass`: Class of the object to attach to -- `collection`: Name of the collection containing attached documents -- `attributes`: Attributes of the object -- `id`: Optional id of the object, if not provided, a new id will be generated - -Example: - -```ts -import contact, { AvatarType } from '@hcengineering/contact' - -.. - -const personId = await client.createDoc( - contact.class.Person, - contact.space.Contacts, - { - name: 'Doe,John', - city: 'New York', - avatarType: AvatarType.COLOR - } -) - -await client.addCollection( - contact.class.Channel, - contact.space.Contacts, - personId, - contact.class.Person, - 'channels', - { - provider: contact.channelProvider.Email, - value: 'john.doe@example.com' - } -) -``` - -#### updateCollection - -Updates existing attached document in collection. - -Parameters: - -- `_class`: Class of the object to update -- `space`: Space of the object to update -- `objectId`: Space of the object to update -- `attachedTo`: Id of the parent object -- `attachedToClass`: Class of the parent object -- `collection`: Name of the collection containing attached documents -- `attributes`: Attributes of the object to update - -Example: - -```ts -import contact from '@hcengineering/contact' - -.. - -await client.updateCollection( - contact.class.Channel, - contact.space.Contacts, - channelId, - personId, - contact.class.Person, - 'channels', - { - city: 'New York', - } -) -``` - -#### removeCollection - -Removes existing attached document from collection. - -Parameters: - -- `_class`: Class of the object to remove -- `space`: Space of the object to remove -- `objectId`: Space of the object to remove -- `attachedTo`: Id of the parent object -- `attachedToClass`: Class of the parent object -- `collection`: Name of the collection containing attached documents - -Example: - -```ts -import contact from '@hcengineering/contact' - -.. - -await client.removeCollection( - contact.class.Channel, - contact.space.Contacts, - channelId, - personId, - contact.class.Person, - 'channels' -) -``` - -### Mixins API - -The client provides two methods for managing mixins: `createMixin` and `updateMixin`. - -#### createMixin - -Creates a new mixin for a specified document. - -Parameters: - -- `objectId`: Id of the object the mixin is attached to -- `objectClass`: Class of the object the mixin is attached to -- `objectSpace`: Space of the object the mixin is attached to -- `mixin`: Id of the mixin type to update -- `attributes`: Attributes of the mixin - -```ts -import contact, { AvatarType } from '@hcengineering/contact' - -.. - -const personId = await client.createDoc( - contact.class.Person, - contact.space.Contacts, - { - name: 'Doe,John', - city: 'New York', - avatarType: AvatarType.COLOR - } -) - -await client.createMixin( - personId, - contact.class.Person, - contact.space.Contacts, - contact.mixin.Employee, - { - active: true, - position: 'CEO' - } -) -``` - -#### updateMixin - -Updates an existing mixin. - -Parameters: - -- `objectId`: Id of the object the mixin is attached to -- `objectClass`: Class of the object the mixin is attached to -- `objectSpace`: Space of the object the mixin is attached to -- `mixin`: Id of the mixin type to update -- `attributes`: Attributes of the mixin to update - -```ts -import contact, { AvatarType } from '@hcengineering/contact' - -.. - -const person = await client.findOne( - contact.class.Person, - { - _id: 'person-id' - } -) - -await client.updateMixin( - personId, - contact.class.Person, - contact.space.Contacts, - contact.mixin.Employee, - { - active: false - } -) -``` diff --git a/foundations/core/packages/api-client/config/rig.json b/foundations/core/packages/api-client/config/rig.json deleted file mode 100644 index 0110930f55..0000000000 --- a/foundations/core/packages/api-client/config/rig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig" -} diff --git a/foundations/core/packages/api-client/jest.config.js b/foundations/core/packages/api-client/jest.config.js deleted file mode 100644 index 069ea8aa0d..0000000000 --- a/foundations/core/packages/api-client/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ['./src'], - coverageReporters: ['text-summary', 'html', 'lcov'] -} diff --git a/foundations/core/packages/api-client/package.json b/foundations/core/packages/api-client/package.json deleted file mode 100644 index 15d69d48f1..0000000000 --- a/foundations/core/packages/api-client/package.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "name": "@hcengineering/api-client", - "version": "0.7.18", - "main": "lib/index.js", - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.js" - } - }, - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "files": [ - "lib/**/*", - "types/**/*", - "src/**/*", - "!src/**/__test__/**", - "tsconfig.json" - ], - "author": "Anticrm Platform Contributors", - "template": "@hcengineering/api-package", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "build:watch": "compile", - "test": "jest --passWithNoTests --silent --coverage", - "format": "format src", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --coverage", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "workspace:^0.7.19", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "@typescript-eslint/parser": "^6.21.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.6.2", - "typescript": "^5.9.3", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "ts-node": "^10.9.2", - "@types/node": "^22.18.1", - "@types/jest": "^29.5.5", - "@types/ws": "^8.5.12", - "@types/snappyjs": "^0.7.1", - "eslint-plugin-svelte": "^2.35.1" - }, - "dependencies": { - "@hcengineering/account-client": "workspace:^0.7.19", - "@hcengineering/client": "workspace:^0.7.17", - "@hcengineering/client-resources": "workspace:^0.7.17", - "@hcengineering/collaborator-client": "workspace:^0.7.17", - "@hcengineering/core": "workspace:^0.7.22", - "@hcengineering/platform": "workspace:^0.7.18", - "@hcengineering/text": "workspace:^0.7.18", - "@hcengineering/text-markdown": "workspace:^0.7.20", - "snappyjs": "^0.7.0" - }, - "repository": "https://github.com/hcengineering/huly.core", - "publishConfig": { - "access": "public" - }, - "optionalDependencies": { - "ws": "^8.18.2" - } -} diff --git a/foundations/core/packages/api-client/src/__tests__/config.test.ts b/foundations/core/packages/api-client/src/__tests__/config.test.ts deleted file mode 100644 index 7a44d62887..0000000000 --- a/foundations/core/packages/api-client/src/__tests__/config.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// - -import { loadServerConfig } from '../config' - -describe('loadServerConfig', () => { - const mockFetch = jest.fn() - global.fetch = mockFetch as any - - beforeEach(() => { - mockFetch.mockClear() - }) - - it('should load server config successfully', async () => { - const mockConfig = { - ACCOUNTS_URL: 'https://accounts.example.com', - COLLABORATOR_URL: 'https://collaborator.example.com', - FILES_URL: 'https://files.example.com', - UPLOAD_URL: 'https://upload.example.com' - } - - mockFetch.mockResolvedValue({ - ok: true, - json: async () => mockConfig - }) - - const config = await loadServerConfig('https://api.example.com') - - expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/config.json', { keepalive: true }) - expect(config).toEqual(mockConfig) - }) - - it('should throw error when fetch fails', async () => { - mockFetch.mockResolvedValue({ - ok: false, - status: 404 - }) - - await expect(loadServerConfig('https://api.example.com')).rejects.toThrow('Failed to fetch config') - }) - - it('should handle network errors', async () => { - mockFetch.mockRejectedValue(new Error('Network error')) - - await expect(loadServerConfig('https://api.example.com')).rejects.toThrow('Network error') - }) - - it('should construct correct config URL', async () => { - mockFetch.mockResolvedValue({ - ok: true, - json: async () => ({ - ACCOUNTS_URL: '', - COLLABORATOR_URL: '', - FILES_URL: '', - UPLOAD_URL: '' - }) - }) - - await loadServerConfig('https://api.example.com/') - expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/config.json', { keepalive: true }) - }) - - it('should handle URL without trailing slash', async () => { - mockFetch.mockResolvedValue({ - ok: true, - json: async () => ({ - ACCOUNTS_URL: '', - COLLABORATOR_URL: '', - FILES_URL: '', - UPLOAD_URL: '' - }) - }) - - await loadServerConfig('https://api.example.com') - expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/config.json', { keepalive: true }) - }) -}) diff --git a/foundations/core/packages/api-client/src/__tests__/markup-client.test.ts b/foundations/core/packages/api-client/src/__tests__/markup-client.test.ts deleted file mode 100644 index ece7fffa76..0000000000 --- a/foundations/core/packages/api-client/src/__tests__/markup-client.test.ts +++ /dev/null @@ -1,183 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// - -import { createMarkupOperations } from '../markup/client' -import { getClient } from '@hcengineering/collaborator-client' -import { makeCollabId } from '@hcengineering/core' - -// Mock dependencies -jest.mock('@hcengineering/collaborator-client') -jest.mock('@hcengineering/text', () => ({ - htmlToJSON: jest.fn((html) => ({ type: 'doc', content: [{ type: 'text', text: html }] })), - jsonToHTML: jest.fn((json) => json.content?.[0]?.text ?? ''), - jsonToMarkup: jest.fn((json) => json.content?.[0]?.text ?? ''), - markupToJSON: jest.fn((markup) => ({ type: 'doc', content: [{ type: 'text', text: markup }] })) -})) -jest.mock('@hcengineering/text-markdown', () => ({ - markdownToMarkup: jest.fn((md) => md), - markupToMarkdown: jest.fn((json) => json.content?.[0]?.text ?? '') -})) - -describe('MarkupOperations', () => { - const mockConfig = { - ACCOUNTS_URL: 'https://accounts.example.com', - COLLABORATOR_URL: 'https://collaborator.example.com', - FILES_URL: 'https://files.example.com', - UPLOAD_URL: 'https://upload.example.com' - } - - const workspace = 'test-workspace' as any - const token = 'test-token' - const url = 'https://api.example.com' - - let mockCollaborator: any - let operations: any - - beforeEach(() => { - jest.clearAllMocks() - - mockCollaborator = { - getMarkup: jest.fn(), - createMarkup: jest.fn() - } - ;(getClient as jest.Mock).mockReturnValue(mockCollaborator) - - operations = createMarkupOperations(url, workspace, token, mockConfig) - }) - - describe('fetchMarkup', () => { - const objectClass = 'class:test.Doc' as any - const objectId = 'doc-id-123' as any - const objectAttr = 'content' - const markupRef = 'markup-ref-456' as any - - it('should fetch markup in markup format', async () => { - const mockMarkup = 'Test markup content' - mockCollaborator.getMarkup.mockResolvedValue(mockMarkup) - - const result = await operations.fetchMarkup(objectClass, objectId, objectAttr, markupRef, 'markup') - - const collabId = makeCollabId(objectClass, objectId, objectAttr) - expect(mockCollaborator.getMarkup).toHaveBeenCalledWith(collabId, markupRef) - expect(result).toBe(mockMarkup) - }) - - it('should fetch markup in HTML format', async () => { - const mockMarkup = '

Test content

' - mockCollaborator.getMarkup.mockResolvedValue(mockMarkup) - - const result = await operations.fetchMarkup(objectClass, objectId, objectAttr, markupRef, 'html') - - expect(mockCollaborator.getMarkup).toHaveBeenCalled() - expect(result).toBeDefined() - }) - - it('should fetch markup in markdown format', async () => { - const mockMarkup = '# Test heading' - mockCollaborator.getMarkup.mockResolvedValue(mockMarkup) - - const result = await operations.fetchMarkup(objectClass, objectId, objectAttr, markupRef, 'markdown') - - expect(mockCollaborator.getMarkup).toHaveBeenCalled() - expect(result).toBeDefined() - }) - - it('should throw error for unknown format', async () => { - mockCollaborator.getMarkup.mockResolvedValue('content') - - await expect( - operations.fetchMarkup(objectClass, objectId, objectAttr, markupRef, 'unknown-format' as any) - ).rejects.toThrow('Unknown content format') - }) - - it('should handle collaborator errors', async () => { - mockCollaborator.getMarkup.mockRejectedValue(new Error('Collaborator error')) - - await expect(operations.fetchMarkup(objectClass, objectId, objectAttr, markupRef, 'markup')).rejects.toThrow( - 'Collaborator error' - ) - }) - }) - - describe('uploadMarkup', () => { - const objectClass = 'class:test.Doc' as any - const objectId = 'doc-id-123' as any - const objectAttr = 'content' - const mockMarkupRef = 'new-markup-ref-789' as any - - beforeEach(() => { - mockCollaborator.createMarkup.mockResolvedValue(mockMarkupRef) - }) - - it('should upload markup in markup format', async () => { - const content = 'Test markup content' - - const result = await operations.uploadMarkup(objectClass, objectId, objectAttr, content, 'markup') - - const collabId = makeCollabId(objectClass, objectId, objectAttr) - expect(mockCollaborator.createMarkup).toHaveBeenCalledWith(collabId, content) - expect(result).toBe(mockMarkupRef) - }) - - it('should upload markup in HTML format', async () => { - const content = '

Test HTML content

' - - const result = await operations.uploadMarkup(objectClass, objectId, objectAttr, content, 'html') - - expect(mockCollaborator.createMarkup).toHaveBeenCalled() - expect(result).toBe(mockMarkupRef) - }) - - it('should upload markup in markdown format', async () => { - const content = '# Test markdown' - - const result = await operations.uploadMarkup(objectClass, objectId, objectAttr, content, 'markdown') - - expect(mockCollaborator.createMarkup).toHaveBeenCalled() - expect(result).toBe(mockMarkupRef) - }) - - it('should throw error for unknown format', async () => { - await expect( - operations.uploadMarkup(objectClass, objectId, objectAttr, 'content', 'unknown-format' as any) - ).rejects.toThrow('Unknown content format') - }) - - it('should handle empty content', async () => { - const result = await operations.uploadMarkup(objectClass, objectId, objectAttr, '', 'markup') - - const collabId = makeCollabId(objectClass, objectId, objectAttr) - expect(mockCollaborator.createMarkup).toHaveBeenCalledWith(collabId, '') - expect(result).toBe(mockMarkupRef) - }) - - it('should handle collaborator errors', async () => { - mockCollaborator.createMarkup.mockRejectedValue(new Error('Upload failed')) - - await expect(operations.uploadMarkup(objectClass, objectId, objectAttr, 'content', 'markup')).rejects.toThrow( - 'Upload failed' - ) - }) - }) - - describe('initialization', () => { - it('should initialize collaborator client with correct parameters', () => { - expect(getClient).toHaveBeenCalledWith(workspace, token, mockConfig.COLLABORATOR_URL) - }) - - it('should handle different workspace IDs', () => { - const differentWorkspace = 'different-workspace' as any - createMarkupOperations(url, differentWorkspace, token, mockConfig) - - expect(getClient).toHaveBeenCalledWith(differentWorkspace, token, mockConfig.COLLABORATOR_URL) - }) - - it('should handle different tokens', () => { - const differentToken = 'different-token' - createMarkupOperations(url, workspace, differentToken, mockConfig) - - expect(getClient).toHaveBeenCalledWith(workspace, differentToken, mockConfig.COLLABORATOR_URL) - }) - }) -}) diff --git a/foundations/core/packages/api-client/src/__tests__/markup-types.test.ts b/foundations/core/packages/api-client/src/__tests__/markup-types.test.ts deleted file mode 100644 index 780b0c2ef5..0000000000 --- a/foundations/core/packages/api-client/src/__tests__/markup-types.test.ts +++ /dev/null @@ -1,124 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// - -import { html, markdown, MarkupContent } from '../markup/types' - -describe('MarkupContent', () => { - describe('constructor', () => { - it('should create MarkupContent with content and kind', () => { - const content = '

Hello World

' - const markup = new MarkupContent(content, 'html') - - expect(markup.content).toBe(content) - expect(markup.kind).toBe('html') - }) - - it('should create MarkupContent with markdown kind', () => { - const content = '# Hello World' - const markup = new MarkupContent(content, 'markdown') - - expect(markup.content).toBe(content) - expect(markup.kind).toBe('markdown') - }) - - it('should create MarkupContent with markup kind', () => { - const content = 'plain markup content' - const markup = new MarkupContent(content, 'markup') - - expect(markup.content).toBe(content) - expect(markup.kind).toBe('markup') - }) - }) - - describe('html helper', () => { - it('should create HTML MarkupContent', () => { - const content = '

Title

Content

' - const markup = html(content) - - expect(markup).toBeInstanceOf(MarkupContent) - expect(markup.content).toBe(content) - expect(markup.kind).toBe('html') - }) - - it('should handle empty HTML', () => { - const markup = html('') - - expect(markup.content).toBe('') - expect(markup.kind).toBe('html') - }) - - it('should handle complex HTML with attributes', () => { - const content = '' - const markup = html(content) - - expect(markup.content).toBe(content) - expect(markup.kind).toBe('html') - }) - }) - - describe('markdown helper', () => { - it('should create Markdown MarkupContent', () => { - const content = '# Heading\n\n* List item 1\n* List item 2' - const markup = markdown(content) - - expect(markup).toBeInstanceOf(MarkupContent) - expect(markup.content).toBe(content) - expect(markup.kind).toBe('markdown') - }) - - it('should handle empty markdown', () => { - const markup = markdown('') - - expect(markup.content).toBe('') - expect(markup.kind).toBe('markdown') - }) - - it('should handle markdown with code blocks', () => { - const content = '```javascript\nconst x = 42;\n```' - const markup = markdown(content) - - expect(markup.content).toBe(content) - expect(markup.kind).toBe('markdown') - }) - - it('should handle markdown with links', () => { - const content = '[Link text](https://example.com)' - const markup = markdown(content) - - expect(markup.content).toBe(content) - expect(markup.kind).toBe('markdown') - }) - }) - - describe('edge cases', () => { - it('should handle special characters in content', () => { - const content = '

Special chars: & < > " \'

' - const markup = html(content) - - expect(markup.content).toBe(content) - }) - - it('should handle Unicode characters', () => { - const content = '# 你好世界 🌍' - const markup = markdown(content) - - expect(markup.content).toBe(content) - }) - - it('should handle very long content', () => { - const content = 'a'.repeat(10000) - const markup = html(content) - - expect(markup.content).toBe(content) - expect(markup.content.length).toBe(10000) - }) - - it('should handle multiline content', () => { - const content = 'Line 1\nLine 2\nLine 3' - const markup = markdown(content) - - expect(markup.content).toBe(content) - }) - }) -}) diff --git a/foundations/core/packages/api-client/src/__tests__/rest-utils.test.ts b/foundations/core/packages/api-client/src/__tests__/rest-utils.test.ts deleted file mode 100644 index 620872493e..0000000000 --- a/foundations/core/packages/api-client/src/__tests__/rest-utils.test.ts +++ /dev/null @@ -1,209 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// - -import { withRetry, extractJson } from '../rest/utils' - -describe('withRetry', () => { - it('should return result on first success', async () => { - const fn = jest.fn().mockResolvedValue('success') - - const result = await withRetry(fn) - - expect(result).toBe('success') - expect(fn).toHaveBeenCalledTimes(1) - }) - - it('should retry on failure and eventually succeed', async () => { - const fn = jest - .fn() - .mockRejectedValueOnce(new Error('fail 1')) - .mockRejectedValueOnce(new Error('fail 2')) - .mockResolvedValue('success') - - const result = await withRetry(fn) - - expect(result).toBe('success') - expect(fn).toHaveBeenCalledTimes(3) - }) - - it('should throw error after max retries', async () => { - const error = new Error('persistent failure') - const fn = jest.fn().mockRejectedValue(error) - - await expect(withRetry(fn)).rejects.toThrow('persistent failure') - expect(fn).toHaveBeenCalledTimes(3) - }) - - it('should use exponential backoff', async () => { - const delays: number[] = [] - const startTimes: number[] = [] - let lastTime = Date.now() - - const fn = jest.fn(async () => { - const now = Date.now() - if (startTimes.length > 0) { - delays.push(now - lastTime) - } - startTimes.push(now) - lastTime = now - throw new Error('fail') - }) - - await expect(withRetry(fn)).rejects.toThrow('fail') - - expect(fn).toHaveBeenCalledTimes(3) - // Delays should be approximately 100ms and 200ms (with some tolerance) - expect(delays.length).toBe(2) - expect(delays[0]).toBeGreaterThanOrEqual(80) - expect(delays[0]).toBeLessThan(150) - expect(delays[1]).toBeGreaterThanOrEqual(180) - expect(delays[1]).toBeLessThan(250) - }) - - it('should not decrement attempt when ignoreAttemptCheck returns true', async () => { - let callCount = 0 - const fn = jest.fn(async () => { - callCount++ - if (callCount <= 5) { - throw new Error('ignore') - } - throw new Error('real error') - }) - - const ignoreCheck = jest.fn((err: any) => err.message === 'ignore') - - await expect(withRetry(fn, ignoreCheck)).rejects.toThrow('real error') - - // Should have tried more than 3 times because ignored errors don't count - expect(fn.mock.calls.length).toBeGreaterThan(3) - expect(ignoreCheck).toHaveBeenCalled() - }) - - it('should handle promise rejection', async () => { - const fn = jest.fn(async () => { - throw new Error('async error') - }) - - await expect(withRetry(fn)).rejects.toThrow('async error') - expect(fn).toHaveBeenCalledTimes(3) - }) - - it('should handle errors that bypass the check', async () => { - const fn = jest.fn(async () => { - // Always throw error - throw new Error('persistent error') - }) - - const ignoreCheck = jest.fn(() => false) // Never ignore - - await expect(withRetry(fn, ignoreCheck)).rejects.toThrow('persistent error') - expect(fn).toHaveBeenCalledTimes(3) - expect(ignoreCheck).toHaveBeenCalledTimes(3) - }) -}) - -describe('extractJson', () => { - it('should extract plain JSON', async () => { - const mockResponse = { - headers: { - get: jest.fn().mockReturnValue(null) - }, - text: jest.fn().mockResolvedValue('{"key":"value"}') - } as any - - const result = await extractJson(mockResponse) - - expect(result).toEqual({ key: 'value' }) - expect(mockResponse.headers.get).toHaveBeenCalledWith('content-encoding') - }) - - it('should handle TotalArray dataType', async () => { - const jsonString = JSON.stringify({ - dataType: 'TotalArray', - value: [{ id: 1 }, { id: 2 }], - total: 10, - lookupMap: { key: 'value' } - }) - - const mockResponse = { - headers: { - get: jest.fn().mockReturnValue(null) - }, - text: jest.fn().mockResolvedValue(jsonString) - } as any - - const result = await extractJson(mockResponse) - - expect(Array.isArray(result)).toBe(true) - expect(result.total).toBe(10) - expect(result.lookupMap).toEqual({ key: 'value' }) - expect(result[0]).toEqual({ id: 1 }) - }) - - it('should handle snappy encoding header', async () => { - // For this test, we'll just verify the snappy path is attempted - // Actual snappy compression/decompression would require valid compressed data - const mockResponse = { - headers: { - get: jest.fn().mockReturnValue('snappy') - }, - arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(0)) - } as any - - // This will fail to decompress, but we're testing that the snappy path is taken - await expect(extractJson(mockResponse)).rejects.toThrow() - - expect(mockResponse.headers.get).toHaveBeenCalledWith('content-encoding') - expect(mockResponse.arrayBuffer).toHaveBeenCalled() - }) - - it('should handle empty JSON object', async () => { - const mockResponse = { - headers: { - get: jest.fn().mockReturnValue(null) - }, - text: jest.fn().mockResolvedValue('{}') - } as any - - const result = await extractJson(mockResponse) - - expect(result).toEqual({}) - }) - - it('should handle nested objects', async () => { - const nestedData = { - level1: { - level2: { - dataType: 'TotalArray', - value: [1, 2, 3], - total: 3, - lookupMap: {} - } - } - } - - const mockResponse = { - headers: { - get: jest.fn().mockReturnValue(null) - }, - text: jest.fn().mockResolvedValue(JSON.stringify(nestedData)) - } as any - - const result = await extractJson(mockResponse) - - expect(result.level1.level2.total).toBe(3) - expect(Array.isArray(result.level1.level2)).toBe(true) - }) - - it('should throw error on invalid JSON', async () => { - const mockResponse = { - headers: { - get: jest.fn().mockReturnValue(null) - }, - text: jest.fn().mockResolvedValue('invalid json{') - } as any - - await expect(extractJson(mockResponse)).rejects.toThrow() - }) -}) diff --git a/foundations/core/packages/api-client/src/__tests__/utils.test.ts b/foundations/core/packages/api-client/src/__tests__/utils.test.ts deleted file mode 100644 index 68d6d676ba..0000000000 --- a/foundations/core/packages/api-client/src/__tests__/utils.test.ts +++ /dev/null @@ -1,202 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// - -import { getWorkspaceToken } from '../utils' -import { loadServerConfig } from '../config' -import { getClient as getAccountClient } from '@hcengineering/account-client' - -// Mock dependencies -jest.mock('../config') -jest.mock('@hcengineering/account-client') - -describe('getWorkspaceToken', () => { - const mockConfig = { - ACCOUNTS_URL: 'https://accounts.example.com', - COLLABORATOR_URL: 'https://collaborator.example.com', - FILES_URL: 'https://files.example.com', - UPLOAD_URL: 'https://upload.example.com' - } - - const mockWorkspaceInfo = { - endpoint: 'wss://workspace.example.com', - token: 'workspace-token-123', - workspace: 'workspace-id-123' as any, - email: 'user@example.com', - workspaceId: 'workspace-id-123', - workspaceName: 'Test Workspace', - workspaceUrl: 'https://workspace.example.com', - createdOn: Date.now(), - lastVisit: Date.now(), - role: 0, - account: 'account-id-123' as any - } - - const mockLoginInfo = { - token: 'login-token-456', - endpoint: 'wss://endpoint.example.com' - } - - let mockAccountClient: any - - beforeEach(() => { - jest.clearAllMocks() - - mockAccountClient = { - login: jest.fn().mockResolvedValue(mockLoginInfo), - selectWorkspace: jest.fn().mockResolvedValue(mockWorkspaceInfo) - } - ;(getAccountClient as jest.Mock).mockReturnValue(mockAccountClient) - ;(loadServerConfig as jest.Mock).mockResolvedValue(mockConfig) - }) - - describe('with email/password authentication', () => { - it('should successfully get workspace token with credentials', async () => { - const result = await getWorkspaceToken('https://api.example.com', { - email: 'user@example.com', - password: 'password123', - workspace: 'test-workspace' - }) - - expect(loadServerConfig).toHaveBeenCalledWith('https://api.example.com') - expect(mockAccountClient.login).toHaveBeenCalledWith('user@example.com', 'password123') - expect(mockAccountClient.selectWorkspace).toHaveBeenCalledWith('test-workspace') - - expect(result).toEqual({ - endpoint: mockWorkspaceInfo.endpoint, - token: mockWorkspaceInfo.token, - workspaceId: mockWorkspaceInfo.workspace, - info: mockWorkspaceInfo - }) - }) - - it('should use provided config if available', async () => { - await getWorkspaceToken( - 'https://api.example.com', - { - email: 'user@example.com', - password: 'password123', - workspace: 'test-workspace' - }, - mockConfig - ) - - expect(loadServerConfig).not.toHaveBeenCalled() - expect(getAccountClient).toHaveBeenCalledWith(mockConfig.ACCOUNTS_URL) - }) - - it('should throw error when login fails', async () => { - mockAccountClient.login.mockResolvedValue({ token: undefined }) - - await expect( - getWorkspaceToken('https://api.example.com', { - email: 'user@example.com', - password: 'wrong-password', - workspace: 'test-workspace' - }) - ).rejects.toThrow('Login failed') - }) - - it('should throw error when workspace not found', async () => { - mockAccountClient.selectWorkspace.mockResolvedValue(undefined) - - await expect( - getWorkspaceToken('https://api.example.com', { - email: 'user@example.com', - password: 'password123', - workspace: 'non-existent-workspace' - }) - ).rejects.toThrow('Workspace not found') - }) - }) - - describe('with token authentication', () => { - it('should successfully get workspace token with existing token', async () => { - const result = await getWorkspaceToken('https://api.example.com', { - token: 'existing-token-789', - workspace: 'test-workspace' - }) - - expect(mockAccountClient.login).not.toHaveBeenCalled() - expect(getAccountClient).toHaveBeenCalledWith(mockConfig.ACCOUNTS_URL, 'existing-token-789') - expect(mockAccountClient.selectWorkspace).toHaveBeenCalledWith('test-workspace') - - expect(result).toEqual({ - endpoint: mockWorkspaceInfo.endpoint, - token: mockWorkspaceInfo.token, - workspaceId: mockWorkspaceInfo.workspace, - info: mockWorkspaceInfo - }) - }) - - it('should throw error when workspace not found with token', async () => { - mockAccountClient.selectWorkspace.mockResolvedValue(undefined) - - await expect( - getWorkspaceToken('https://api.example.com', { - token: 'existing-token-789', - workspace: 'non-existent-workspace' - }) - ).rejects.toThrow('Workspace not found') - }) - }) - - describe('error handling', () => { - it('should propagate config loading errors', async () => { - ;(loadServerConfig as jest.Mock).mockRejectedValue(new Error('Config load failed')) - - await expect( - getWorkspaceToken('https://api.example.com', { - email: 'user@example.com', - password: 'password123', - workspace: 'test-workspace' - }) - ).rejects.toThrow('Config load failed') - }) - - it('should propagate login errors', async () => { - mockAccountClient.login.mockRejectedValue(new Error('Invalid credentials')) - - await expect( - getWorkspaceToken('https://api.example.com', { - email: 'user@example.com', - password: 'wrong-password', - workspace: 'test-workspace' - }) - ).rejects.toThrow('Invalid credentials') - }) - - it('should propagate workspace selection errors', async () => { - mockAccountClient.selectWorkspace.mockRejectedValue(new Error('Access denied')) - - await expect( - getWorkspaceToken('https://api.example.com', { - email: 'user@example.com', - password: 'password123', - workspace: 'test-workspace' - }) - ).rejects.toThrow('Access denied') - }) - }) - - describe('edge cases', () => { - it('should handle empty workspace name', async () => { - await getWorkspaceToken('https://api.example.com', { - token: 'token', - workspace: '' - }) - - expect(mockAccountClient.selectWorkspace).toHaveBeenCalledWith('') - }) - - it('should handle special characters in credentials', async () => { - await getWorkspaceToken('https://api.example.com', { - email: 'user+test@example.com', - password: 'p@ssw0rd!#$%', - workspace: 'test-workspace' - }) - - expect(mockAccountClient.login).toHaveBeenCalledWith('user+test@example.com', 'p@ssw0rd!#$%') - }) - }) -}) diff --git a/foundations/core/packages/api-client/src/client.ts b/foundations/core/packages/api-client/src/client.ts deleted file mode 100644 index 118fde1760..0000000000 --- a/foundations/core/packages/api-client/src/client.ts +++ /dev/null @@ -1,297 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// -import { getClient as getAccountClient } from '@hcengineering/account-client' -import client, { clientId } from '@hcengineering/client' -import { - type Account, - type Class, - type Client, - type Data, - type Doc, - type DocumentQuery, - type FindOptions, - type FindResult, - type Hierarchy, - type ModelDb, - type Ref, - type Space, - type TxResult, - type WithLookup, - AttachedData, - AttachedDoc, - DocumentUpdate, - Mixin, - MixinData, - MixinUpdate, - TxOperations, - WorkspaceUuid, - generateId, - pickPrimarySocialId -} from '@hcengineering/core' -import { addLocation, getResource } from '@hcengineering/platform' - -import { type ServerConfig, loadServerConfig } from './config' -import { - type MarkupFormat, - type MarkupOperations, - type MarkupRef, - MarkupContent, - createMarkupOperations -} from './markup' -import { type ConnectOptions, type PlatformClient, WithMarkup } from './types' -import { getWorkspaceToken } from './utils' - -/** - * Create platform client - * @public */ -export async function connect (url: string, options: ConnectOptions): Promise { - const config = await loadServerConfig(url) - - const { endpoint, token } = await getWorkspaceToken(url, options, config) - const accountClient = getAccountClient(config.ACCOUNTS_URL, token) - const socialIds = await accountClient.getSocialIds(true) - const wsLoginInfo = await accountClient.selectWorkspace(options.workspace) - - if (wsLoginInfo === undefined) { - throw new Error(`Workspace ${options.workspace} not found`) - } - - const account: Account = { - uuid: wsLoginInfo.account, - role: wsLoginInfo.role, - primarySocialId: pickPrimarySocialId(socialIds)._id, - socialIds: socialIds.map((si) => si._id), - fullSocialIds: socialIds - } - - return await createClient(url, endpoint, token, wsLoginInfo.workspace, account, config, options) -} - -async function createClient ( - url: string, - endpoint: string, - token: string, - workspaceUuid: WorkspaceUuid, - account: Account, - config: ServerConfig, - options: ConnectOptions -): Promise { - addLocation(clientId, () => import(/* webpackChunkName: "client" */ '@hcengineering/client-resources')) - - const { socketFactory, connectionTimeout } = options - - const clientFactory = await getResource(client.function.GetClient) - const connection = await clientFactory(token, endpoint, { - socketFactory, - connectionTimeout - }) - - return new PlatformClientImpl(url, workspaceUuid, token, config, connection, account) -} - -class PlatformClientImpl implements PlatformClient { - private readonly client: TxOperations - private readonly markup: MarkupOperations - - constructor ( - private readonly url: string, - private readonly workspace: WorkspaceUuid, - private readonly token: string, - private readonly config: ServerConfig, - private readonly connection: Client, - private readonly account: Account - ) { - this.client = new TxOperations(connection, account.primarySocialId) - this.markup = createMarkupOperations(url, workspace, token, config) - } - - // Client - - getHierarchy (): Hierarchy { - return this.client.getHierarchy() - } - - getModel (): ModelDb { - return this.client.getModel() - } - - async getAccount (): Promise { - return this.account - } - - async findOne( - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): Promise | undefined> { - return await this.client.findOne(_class, query, options) - } - - async findAll( - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): Promise> { - return await this.client.findAll(_class, query, options) - } - - async close (): Promise { - await this.connection.close() - } - - private async processMarkup(_class: Ref>, id: Ref, data: WithMarkup): Promise { - const result: any = {} - - for (const [key, value] of Object.entries(data)) { - if (value instanceof MarkupContent) { - result[key] = this.markup.uploadMarkup(_class, id, key, value.content, value.kind) - } else { - result[key] = value - } - } - - return result as T - } - - // DocOperations - - async createDoc( - _class: Ref>, - space: Ref, - attributes: WithMarkup>, - id?: Ref - ): Promise> { - id ??= generateId() - const data = await this.processMarkup>(_class, id, attributes) - return await this.client.createDoc(_class, space, data, id) - } - - async updateDoc( - _class: Ref>, - space: Ref, - objectId: Ref, - operations: WithMarkup>, - retrieve?: boolean - ): Promise { - const update = await this.processMarkup>(_class, objectId, operations) - return await this.client.updateDoc(_class, space, objectId, update, retrieve) - } - - async removeDoc(_class: Ref>, space: Ref, objectId: Ref): Promise { - return await this.client.removeDoc(_class, space, objectId) - } - - // CollectionOperations - - async addCollection( - _class: Ref>, - space: Ref, - attachedTo: Ref, - attachedToClass: Ref>, - collection: Extract | string, - attributes: WithMarkup>, - id?: Ref

- ): Promise> { - id ??= generateId() - const data = await this.processMarkup>(_class, id, attributes) - return await this.client.addCollection(_class, space, attachedTo, attachedToClass, collection, data, id) - } - - async updateCollection( - _class: Ref>, - space: Ref, - objectId: Ref

, - attachedTo: Ref, - attachedToClass: Ref>, - collection: Extract | string, - operations: WithMarkup>, - retrieve?: boolean - ): Promise> { - const update = await this.processMarkup>(_class, objectId, operations) - return await this.client.updateCollection( - _class, - space, - objectId, - attachedTo, - attachedToClass, - collection, - update, - retrieve - ) - } - - async removeCollection( - _class: Ref>, - space: Ref, - objectId: Ref

, - attachedTo: Ref, - attachedToClass: Ref>, - collection: Extract | string - ): Promise> { - return await this.client.removeCollection(_class, space, objectId, attachedTo, attachedToClass, collection) - } - - // MixinOperations - - async createMixin( - objectId: Ref, - objectClass: Ref>, - objectSpace: Ref, - mixin: Ref>, - attributes: WithMarkup> - ): Promise { - const data = await this.processMarkup>(objectClass, objectId, attributes) - return await this.client.createMixin(objectId, objectClass, objectSpace, mixin, data) - } - - async updateMixin( - objectId: Ref, - objectClass: Ref>, - objectSpace: Ref, - mixin: Ref>, - attributes: WithMarkup> - ): Promise { - const update = await this.processMarkup>(objectClass, objectId, attributes) - return await this.client.updateMixin(objectId, objectClass, objectSpace, mixin, update) - } - - // Markup - - async fetchMarkup ( - objectClass: Ref>, - objectId: Ref, - objectAttr: string, - markup: MarkupRef, - format: MarkupFormat - ): Promise { - return await this.markup.fetchMarkup(objectClass, objectId, objectAttr, markup, format) - } - - async uploadMarkup ( - objectClass: Ref>, - objectId: Ref, - objectAttr: string, - markup: string, - format: MarkupFormat - ): Promise { - return await this.markup.uploadMarkup(objectClass, objectId, objectAttr, markup, format) - } - - // AsyncDisposable - - async [Symbol.asyncDispose] (): Promise { - await this.close() - } -} diff --git a/foundations/core/packages/api-client/src/config.ts b/foundations/core/packages/api-client/src/config.ts deleted file mode 100644 index 9386748a38..0000000000 --- a/foundations/core/packages/api-client/src/config.ts +++ /dev/null @@ -1,32 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { concatLink } from '@hcengineering/core' - -export interface ServerConfig { - ACCOUNTS_URL: string - COLLABORATOR_URL: string - FILES_URL: string - UPLOAD_URL: string -} - -export async function loadServerConfig (url: string): Promise { - const configUrl = concatLink(url, '/config.json') - const res = await fetch(configUrl, { keepalive: true }) - if (res.ok) { - return (await res.json()) as ServerConfig - } - throw new Error('Failed to fetch config') -} diff --git a/foundations/core/packages/api-client/src/index.ts b/foundations/core/packages/api-client/src/index.ts deleted file mode 100644 index 2379b838de..0000000000 --- a/foundations/core/packages/api-client/src/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export * from './client' -export * from './markup/types' -export * from './socket' -export * from './types' -export * from './rest' -export * from './config' -export * from './utils' -export * from './storage' diff --git a/foundations/core/packages/api-client/src/markup/client.ts b/foundations/core/packages/api-client/src/markup/client.ts deleted file mode 100644 index 5a783a2080..0000000000 --- a/foundations/core/packages/api-client/src/markup/client.ts +++ /dev/null @@ -1,106 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - type Class, - type Doc, - type Markup, - type Ref, - WorkspaceUuid, - concatLink, - makeCollabId -} from '@hcengineering/core' -import { type CollaboratorClient, getClient } from '@hcengineering/collaborator-client' -import { htmlToJSON, jsonToHTML, jsonToMarkup, markupToJSON } from '@hcengineering/text' -import { markdownToMarkup, markupToMarkdown } from '@hcengineering/text-markdown' - -import { type ServerConfig } from '../config' -import { type MarkupOperations, type MarkupFormat, type MarkupRef } from './types' - -export function createMarkupOperations ( - url: string, - workspace: WorkspaceUuid, - token: string, - config: ServerConfig -): MarkupOperations { - return new MarkupOperationsImpl(url, workspace, token, config) -} - -class MarkupOperationsImpl implements MarkupOperations { - private readonly collaborator: CollaboratorClient - private readonly imageUrl: string - private readonly refUrl: string - - constructor ( - private readonly url: string, - private readonly workspace: WorkspaceUuid, - private readonly token: string, - private readonly config: ServerConfig - ) { - this.refUrl = concatLink(this.url, `/browse?workspace=${workspace}`) - this.imageUrl = concatLink(this.url, `/files?workspace=${workspace}&file=`) - this.collaborator = getClient(workspace, token, config.COLLABORATOR_URL) - } - - async fetchMarkup ( - objectClass: Ref>, - objectId: Ref, - objectAttr: string, - doc: MarkupRef, - format: MarkupFormat - ): Promise { - const collabId = makeCollabId(objectClass, objectId, objectAttr) - const markup = await this.collaborator.getMarkup(collabId, doc) - const json = markupToJSON(markup) - - switch (format) { - case 'markup': - return markup - case 'html': - return jsonToHTML(json) - case 'markdown': - return markupToMarkdown(json, { refUrl: this.refUrl, imageUrl: this.imageUrl }) - default: - throw new Error('Unknown content format') - } - } - - async uploadMarkup ( - objectClass: Ref>, - objectId: Ref, - objectAttr: string, - value: string, - format: MarkupFormat - ): Promise { - let markup: Markup = '' - - switch (format) { - case 'markup': - markup = value - break - case 'html': - markup = jsonToMarkup(htmlToJSON(value)) - break - case 'markdown': - markup = jsonToMarkup(markdownToMarkup(value, { refUrl: this.refUrl, imageUrl: this.imageUrl })) - break - default: - throw new Error('Unknown content format') - } - - const collabId = makeCollabId(objectClass, objectId, objectAttr) - return await this.collaborator.createMarkup(collabId, markup) - } -} diff --git a/foundations/core/packages/api-client/src/markup/index.ts b/foundations/core/packages/api-client/src/markup/index.ts deleted file mode 100644 index 3c18f70583..0000000000 --- a/foundations/core/packages/api-client/src/markup/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export * from './client' -export * from './types' diff --git a/foundations/core/packages/api-client/src/markup/types.ts b/foundations/core/packages/api-client/src/markup/types.ts deleted file mode 100644 index a4f6765689..0000000000 --- a/foundations/core/packages/api-client/src/markup/types.ts +++ /dev/null @@ -1,79 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { Class, type Blob, type Doc, type Ref } from '@hcengineering/core' - -/** @public */ -export type MarkupRef = Ref - -/** @public */ -export type MarkupFormat = 'markup' | 'html' | 'markdown' - -/** @public */ -export class MarkupContent { - constructor ( - readonly content: string, - readonly kind: MarkupFormat - ) {} -} - -/** @public */ -export function html (content: string): MarkupContent { - return new MarkupContent(content, 'html') -} - -/** @public */ -export function markdown (content: string): MarkupContent { - return new MarkupContent(content, 'markdown') -} - -/** - * Provides operations for managing markup (rich-text) content. - * @public */ -export interface MarkupOperations { - /** - * Retrieves markup content for a specified document object - * * @param objectClass - Reference to the class of the document containing the markup - * @param objectId - Reference to the document containing the markup - * @param objectAttr - The attribute/field name where the markup is stored - * @param id - Unique reference identifying the specific markup content - * @param format - The format of the markup (e.g., HTML, Markdown, etc.) - * @returns Promise containing the markup content as a string - */ - fetchMarkup: ( - objectClass: Ref>, - objectId: Ref, - objectAttr: string, - id: MarkupRef, - format: MarkupFormat - ) => Promise - - /** - * Saves markup content for a document object - * @param objectClass - Reference to the class of the document where markup should be stored - * @param objectId - Reference to the document where markup should be stored - * @param objectAttr - The attribute/field name where markup should be saved - * @param markup - The actual markup content to be uploaded - * @param format - The format of the provided markup (e.g., HTML, Markdown, etc.) - * @returns Promise containing a reference to the newly saved markup - */ - uploadMarkup: ( - objectClass: Ref>, - objectId: Ref, - objectAttr: string, - markup: string, - format: MarkupFormat - ) => Promise -} diff --git a/foundations/core/packages/api-client/src/rest/index.ts b/foundations/core/packages/api-client/src/rest/index.ts deleted file mode 100644 index 371187c27a..0000000000 --- a/foundations/core/packages/api-client/src/rest/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export { createRestClient, connectRest } from './rest' -export { createRestTxOperations } from './tx' -export * from './types' diff --git a/foundations/core/packages/api-client/src/rest/rest.ts b/foundations/core/packages/api-client/src/rest/rest.ts deleted file mode 100644 index 827e2dbbf3..0000000000 --- a/foundations/core/packages/api-client/src/rest/rest.ts +++ /dev/null @@ -1,368 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - type Account, - buildModel, - type Class, - concatLink, - type Doc, - type DocumentQuery, - type DomainParams, - type DomainRequestOptions, - type DomainResult, - type FindOptions, - type FindResult, - Hierarchy, - MeasureMetricsContext, - ModelDb, - OperationDomain, - PersonId, - PersonUuid, - type Ref, - type SearchOptions, - type SearchQuery, - type SearchResult, - SocialIdType, - type Tx, - type TxResult, - type WithLookup -} from '@hcengineering/core' -import { PlatformError, type Status, unknownError } from '@hcengineering/platform' - -import { AuthOptions } from '../types' -import { getWorkspaceToken } from '../utils' -import type { RestClient } from './types' -import { extractJson, withRetry } from './utils' - -export function createRestClient (endpoint: string, workspaceId: string, token: string): RestClient { - return new RestClientImpl(endpoint, workspaceId, token) -} - -export async function connectRest (url: string, options: AuthOptions): Promise { - const { endpoint, token, workspaceId } = await getWorkspaceToken(url, options) - return createRestClient(endpoint, workspaceId, token) -} - -const rateLimitError = 'rate-limit' - -function isRLE (err: any): boolean { - return err.message === rateLimitError -} - -export class RestClientImpl implements RestClient { - endpoint: string - - slowDownTimer = 0 - currentRateLimit: { remaining: number, limit: number } = { remaining: 1000, limit: 1000 } - - remaining: number = 1000 - limit: number = 1000 - constructor ( - endpoint: string, - readonly workspace: string, - readonly token: string - ) { - this.endpoint = endpoint.replace('ws', 'http') - } - - jsonHeaders (): Record { - return { - 'Content-Type': 'application/json', - Authorization: 'Bearer ' + this.token, - 'accept-encoding': 'snappy, gzip' - } - } - - requestInit (): RequestInit { - return { - method: 'GET', - keepalive: true, - headers: this.jsonHeaders() - } - } - - async findAll( - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): Promise> { - const params = new URLSearchParams() - params.append('class', _class) - if (query !== undefined && Object.keys(query).length > 0) { - params.append('query', JSON.stringify(query)) - } - if (options !== undefined && Object.keys(options).length > 0) { - params.append('options', JSON.stringify(options)) - } - const requestUrl = concatLink(this.endpoint, `/api/v1/find-all/${this.workspace}?${params.toString()}`) - const result = await withRetry & { error?: Status }>(async () => { - const response = await fetch(requestUrl, this.requestInit()) - if (!response.ok) { - await this.checkRateLimits(response) - throw new PlatformError(unknownError(response.statusText)) - } - this.updateRateLimit(response) - return await extractJson>(response) - }, isRLE) - - if (result.error !== undefined) { - throw new PlatformError(result.error) - } - - if (result.lookupMap !== undefined) { - // We need to extract lookup map to document lookups - for (const d of result) { - if (d.$lookup !== undefined) { - for (const [k, v] of Object.entries(d.$lookup)) { - if (!Array.isArray(v)) { - ;(d as any).$lookup[k] = result.lookupMap[v] - } else { - ;(d as any).$lookup[k] = v.map((it) => result.lookupMap?.[it]) - } - } - } - } - delete result.lookupMap - } - - // We need to revert deleted query simple values. - // We need to get rid of simple query parameters matched in documents - for (const doc of result) { - if (doc._class == null) { - doc._class = _class - } - for (const [k, v] of Object.entries(query)) { - if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') { - if ((doc as any)[k] == null) { - ;(doc as any)[k] = v - } - } - } - } - - return result - } - - private async checkRate (): Promise { - if (this.currentRateLimit.remaining < this.currentRateLimit.limit / 3) { - if (this.slowDownTimer < 50) { - this.slowDownTimer += 50 - } - this.slowDownTimer++ - } else if (this.slowDownTimer > 0) { - this.slowDownTimer-- - } - if (this.slowDownTimer > 0) { - // We need to wait a bit to avoid ban. - await new Promise((resolve) => setTimeout(resolve, this.slowDownTimer)) - } - } - - private updateRateLimit (response: Response): void { - const rateLimitLimit: number = parseInt(response.headers.get('X-RateLimit-Limit') ?? '100') - const remaining: number = parseInt(response.headers.get('X-RateLimit-Remaining') ?? '100') - this.currentRateLimit = { remaining, limit: rateLimitLimit } - } - - private async checkRateLimits (response: Response): Promise { - if (response.status === 429) { - // Extract rate limit information from headers - const retryAfter = response.headers.get('Retry-After') - const retryAfterMS = response.headers.get('Retry-After-ms') - const rateLimitReset = response.headers.get('X-RateLimit-Reset') - - this.updateRateLimit(response) - const waitTime = - (retryAfterMS != null ? parseInt(retryAfterMS) : undefined) ?? - (retryAfter != null - ? parseInt(retryAfter) * 1000 - : rateLimitReset != null - ? new Date(parseInt(rateLimitReset)).getTime() - Date.now() - : 1000) // Default to 1 seconds if no headers are provided - await new Promise((resolve) => setTimeout(resolve, waitTime)) - throw new Error(rateLimitError) - } - } - - async getAccount (): Promise { - const requestUrl = concatLink(this.endpoint, `/api/v1/account/${this.workspace}`) - await this.checkRate() - const result = await withRetry(async () => { - const response = await fetch(requestUrl, this.requestInit()) - if (!response.ok) { - await this.checkRateLimits(response) - throw new PlatformError(unknownError(response.statusText)) - } - this.updateRateLimit(response) - return await extractJson(response) - }) - if (result.error !== undefined) { - throw new PlatformError(result.error) - } - return result - } - - async getModel (full: boolean = false): Promise<{ hierarchy: Hierarchy, model: ModelDb }> { - const requestUrl = new URL(concatLink(this.endpoint, `/api/v1/load-model/${this.workspace}`)) - if (full) { - requestUrl.searchParams.append('full', 'true') - } - await this.checkRate() - const result = await withRetry<{ hierarchy: Hierarchy, model: ModelDb, error?: Status }>(async () => { - const response = await fetch(requestUrl, this.requestInit()) - if (!response.ok) { - await this.checkRateLimits(response) - throw new PlatformError(unknownError(response.statusText)) - } - this.updateRateLimit(response) - - const modelResponse: Tx[] = await extractJson(response) - - const hierarchy = new Hierarchy() - const model = new ModelDb(hierarchy) - - const ctx = new MeasureMetricsContext('loadModel', {}) - buildModel(ctx, modelResponse, undefined, hierarchy, model) - - return { hierarchy, model } - }, isRLE) - if (result.error !== undefined) { - throw new PlatformError(result.error) - } - return result - } - - async findOne( - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): Promise | undefined> { - return (await this.findAll(_class, query, { ...options, limit: 1 })).shift() - } - - async tx (tx: Tx): Promise { - const requestUrl = concatLink(this.endpoint, `/api/v1/tx/${this.workspace}`) - await this.checkRate() - const result = await withRetry(async () => { - const response = await fetch(requestUrl, { - method: 'POST', - headers: this.jsonHeaders(), - keepalive: true, - body: JSON.stringify(tx) - }) - if (!response.ok) { - await this.checkRateLimits(response) - throw new PlatformError(unknownError(response.statusText)) - } - this.updateRateLimit(response) - return await extractJson(response) - }, isRLE) - if (result.error !== undefined) { - throw new PlatformError(result.error) - } - return result - } - - async searchFulltext (query: SearchQuery, options: SearchOptions): Promise { - const result = await withRetry(async () => { - const params = new URLSearchParams() - params.append('query', query.query) - if (query.classes != null && Object.keys(query.classes).length > 0) { - params.append('classes', JSON.stringify(query.classes)) - } - if (query.spaces != null && Object.keys(query.spaces).length > 0) { - params.append('spaces', JSON.stringify(query.spaces)) - } - if (options.limit != null) { - params.append('limit', `${options.limit}`) - } - const requestUrl = concatLink(this.endpoint, `/api/v1/search-fulltext/${this.workspace}?${params.toString()}`) - const response = await fetch(requestUrl, { - method: 'GET', - headers: this.jsonHeaders(), - keepalive: true - }) - if (!response.ok) { - await this.checkRateLimits(response) - throw new PlatformError(unknownError(response.statusText)) - } - this.updateRateLimit(response) - return await extractJson(response) - }) - if (result.error !== undefined) { - throw new PlatformError(result.error) - } - return result - } - - async domainRequest( - domain: OperationDomain, - params: DomainParams, - options?: DomainRequestOptions - ): Promise> { - const requestUrl = concatLink(this.endpoint, `/api/v1/request/${domain}/${this.workspace}`) - - await this.checkRate() - return await withRetry(async () => { - const response = await fetch(requestUrl, { - method: 'POST', - headers: this.jsonHeaders(), - keepalive: true, - body: JSON.stringify(params) - }) - if (!response.ok) { - await this.checkRateLimits(response) - throw new PlatformError(unknownError(response.statusText)) - } - this.updateRateLimit(response) - const value = await extractJson(response) - return { domain, value } - }, isRLE) - } - - async ensurePerson ( - socialType: SocialIdType, - socialValue: string, - firstName: string, - lastName: string - ): Promise<{ uuid: PersonUuid, socialId: PersonId, localPerson: string }> { - const requestUrl = concatLink(this.endpoint, `/api/v1/ensure-person/${this.workspace}`) - await this.checkRate() - const result = await withRetry(async () => { - const response = await fetch(requestUrl, { - method: 'POST', - headers: this.jsonHeaders(), - keepalive: true, - body: JSON.stringify({ - socialType, - socialValue, - firstName, - lastName - }) - }) - if (!response.ok) { - await this.checkRateLimits(response) - throw new PlatformError(unknownError(response.statusText)) - } - this.updateRateLimit(response) - return await extractJson<{ uuid: PersonUuid, socialId: PersonId, localPerson: string }>(response) - }, isRLE) - if (result.error !== undefined) { - throw new PlatformError(result.error) - } - return result - } -} diff --git a/foundations/core/packages/api-client/src/rest/tx.ts b/foundations/core/packages/api-client/src/rest/tx.ts deleted file mode 100644 index d13a68ecea..0000000000 --- a/foundations/core/packages/api-client/src/rest/tx.ts +++ /dev/null @@ -1,114 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - type Account, - type Class, - type Client, - type Doc, - type DocumentQuery, - type DomainParams, - type DomainRequestOptions, - type DomainResult, - type FindOptions, - type FindResult, - Hierarchy, - ModelDb, - type OperationDomain, - type Ref, - type SearchOptions, - type SearchQuery, - type SearchResult, - toFindResult, - type Tx, - TxOperations, - type TxResult, - type WithLookup -} from '@hcengineering/core' -import { RestClientImpl } from './rest' - -export async function createRestTxOperations ( - endpoint: string, - workspaceId: string, - token: string, - fullModel: boolean = false -): Promise { - const restClient = new RestClientImpl(endpoint, workspaceId, token) - - const account = await restClient.getAccount() - const { hierarchy, model } = await restClient.getModel(fullModel) - - return new TxOperations(new RestTxClient(restClient, hierarchy, model, account), account.socialIds[0]) -} - -class RestTxClient implements Client { - constructor ( - readonly client: RestClientImpl, - readonly hierarchy: Hierarchy, - readonly model: ModelDb, - readonly account: Account - ) {} - - close (): Promise { - return Promise.resolve() - } - - async findAll( - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): Promise> { - const data = await this.client.findAll(_class, query, options) - const result = data.map((v) => { - return this.hierarchy.updateLookupMixin(_class, v, options) - }) - return toFindResult(result, data.total) - } - - async domainRequest( - domain: OperationDomain, - params: DomainParams, - options?: DomainRequestOptions - ): Promise> { - return await this.client.domainRequest(domain, params, options) - } - - async findOne( - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): Promise | undefined> { - const v = await this.client.findOne(_class, query, options) - if (v === undefined) { - return - } - return this.hierarchy.updateLookupMixin(_class, v, options) - } - - getHierarchy: () => Hierarchy = () => this.hierarchy - getModel: () => ModelDb = () => this.model - - async getAccount (): Promise { - return this.account - } - - async tx (tx: Tx): Promise { - return await this.client.tx(tx) - } - - async searchFulltext (query: SearchQuery, options: SearchOptions): Promise { - return await this.client.searchFulltext(query, options) - } -} diff --git a/foundations/core/packages/api-client/src/rest/types.ts b/foundations/core/packages/api-client/src/rest/types.ts deleted file mode 100644 index d878fcccce..0000000000 --- a/foundations/core/packages/api-client/src/rest/types.ts +++ /dev/null @@ -1,60 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - type Account, - type Class, - type Doc, - type DocumentQuery, - type DomainParams, - type DomainRequestOptions, - type DomainResult, - type FindOptions, - type FulltextStorage, - type Hierarchy, - type ModelDb, - type OperationDomain, - type PersonId, - type PersonUuid, - type Ref, - type SocialIdType, - type Storage, - type WithLookup -} from '@hcengineering/core' - -export interface RestClient extends Storage, FulltextStorage { - getAccount: () => Promise - - findOne: ( - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ) => Promise | undefined> - - getModel: () => Promise<{ hierarchy: Hierarchy, model: ModelDb }> - - domainRequest: ( - domain: OperationDomain, - params: DomainParams, - options?: DomainRequestOptions - ) => Promise> - - ensurePerson: ( - socialType: SocialIdType, - socialValue: string, - firstName: string, - lastName: string - ) => Promise<{ uuid: PersonUuid, socialId: PersonId, localPerson: string }> -} diff --git a/foundations/core/packages/api-client/src/rest/utils.ts b/foundations/core/packages/api-client/src/rest/utils.ts deleted file mode 100644 index 8b098ac7e5..0000000000 --- a/foundations/core/packages/api-client/src/rest/utils.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { uncompress } from 'snappyjs' - -export async function withRetry (fn: () => Promise, ignoreAttemptCheck?: (err: any) => boolean): Promise { - const maxRetries = 3 - let lastError: any - - for (let attempt = 0; attempt < maxRetries; attempt++) { - try { - return await fn() - } catch (err: any) { - if (ignoreAttemptCheck !== undefined && ignoreAttemptCheck(err)) { - // Do not decrement attempt - attempt-- - } else { - lastError = err - } - if (attempt === maxRetries - 1) { - throw lastError - } - await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 100)) - } - } - throw lastError -} - -function rpcJSONReceiver (key: string, value: any): any { - if (typeof value === 'object' && value !== null) { - if (value.dataType === 'TotalArray') { - return Object.assign(value.value, { total: value.total, lookupMap: value.lookupMap }) - } - } - return value -} - -export async function extractJson (response: Response): Promise { - const encoding = response.headers.get('content-encoding') - if (encoding === 'snappy') { - const buffer = await response.arrayBuffer() - const decompressed = uncompress(buffer) - const decoder = new TextDecoder() - const jsonString = decoder.decode(decompressed) - return JSON.parse(jsonString, rpcJSONReceiver) as T - } - const jsonString = await response.text() - return JSON.parse(jsonString, rpcJSONReceiver) as T -} diff --git a/foundations/core/packages/api-client/src/socket/browser.ts b/foundations/core/packages/api-client/src/socket/browser.ts deleted file mode 100644 index a9dcaca512..0000000000 --- a/foundations/core/packages/api-client/src/socket/browser.ts +++ /dev/null @@ -1,22 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { type ClientSocket, type ClientSocketFactory } from '@hcengineering/client' - -/** @public */ -export const BrowserWebSocketFactory: ClientSocketFactory = (url: string): ClientSocket => { - const ws = new WebSocket(url) - return ws as ClientSocket -} diff --git a/foundations/core/packages/api-client/src/socket/index.ts b/foundations/core/packages/api-client/src/socket/index.ts deleted file mode 100644 index bebba34b90..0000000000 --- a/foundations/core/packages/api-client/src/socket/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export * from './browser' -export * from './node' diff --git a/foundations/core/packages/api-client/src/socket/node.ts b/foundations/core/packages/api-client/src/socket/node.ts deleted file mode 100644 index 5aac1f53d7..0000000000 --- a/foundations/core/packages/api-client/src/socket/node.ts +++ /dev/null @@ -1,102 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { type ClientSocket, type ClientSocketFactory } from '@hcengineering/client' - -/** @public */ -export const NodeWebSocketFactory: ClientSocketFactory = (url: string): ClientSocket => { - // We need to override default factory with 'ws' one. - // eslint-disable-next-line - let WebSocket - try { - WebSocket = require('ws') - } catch (error) { - throw new Error('The "ws" package is required for NodeWebSocketFactory. ') - } - type WebSocketData = Parameters[1] - - const ws = new WebSocket(url) - - const client: ClientSocket = { - get readyState (): number { - return ws.readyState - }, - - send: (data: string | ArrayBufferLike | Blob | ArrayBufferView): void => { - if (data instanceof Blob) { - void data.arrayBuffer().then((buffer) => { - ws.send(buffer) - }) - } else { - ws.send(data) - } - }, - - close: (code?: number): void => { - ws.close(code) - } - } - - ws.on('message', (data: WebSocketData) => { - if (client.onmessage != null) { - const event = { - data, - type: 'message', - target: this - } as unknown as MessageEvent - - client.onmessage(event) - } - }) - - ws.on('close', (code: number, reason: string) => { - if (client.onclose != null) { - const closeEvent = { - code, - reason, - wasClean: code === 1000, - type: 'close', - target: this - } as unknown as CloseEvent - - client.onclose(closeEvent) - } - }) - - ws.on('open', () => { - if (client.onopen != null) { - const event = { - type: 'open', - target: this - } as unknown as Event - - client.onopen(event) - } - }) - - ws.on('error', (error: Error) => { - if (client.onerror != null) { - const event = { - type: 'error', - target: this, - error - } as unknown as Event - - client.onerror(event) - } - }) - - return client -} diff --git a/foundations/core/packages/api-client/src/storage/client.ts b/foundations/core/packages/api-client/src/storage/client.ts deleted file mode 100644 index 025d7d8ac5..0000000000 --- a/foundations/core/packages/api-client/src/storage/client.ts +++ /dev/null @@ -1,220 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import core, { concatLink, WorkspaceUuid, Blob, Ref } from '@hcengineering/core' -import { Readable } from 'stream' -import { StorageClient } from './types' -import { loadServerConfig, ServerConfig } from '../config' -import { NetworkError, NotFoundError, StorageError } from './error' -import { AuthOptions } from '../types' -import { getWorkspaceToken } from '../utils' - -interface ObjectMetadata { - name: string - etag: string - size: number - contentType: string - lastModified: number - cacheControl?: string -} - -interface BlobUploadSuccess { - key: string - id: string - metadata: ObjectMetadata -} - -interface BlobUploadError { - key: string - error: string -} - -type BlobUploadResult = BlobUploadSuccess | BlobUploadError - -export class StorageClientImpl implements StorageClient { - private readonly headers: Record - constructor ( - readonly filesUrl: string, - readonly uploadUrl: string, - token: string, - readonly workspace: WorkspaceUuid - ) { - this.headers = { - Authorization: 'Bearer ' + token - } - } - - getObjectUrl (objectName: string): string { - return this.filesUrl.replace(':filename', objectName).replace(':blobId', objectName) - } - - async stat (objectName: string): Promise { - const url = this.getObjectUrl(objectName) - let response - try { - response = await wrappedFetch(url, { method: 'HEAD', headers: { ...this.headers } }) - } catch (error: any) { - if (error instanceof NotFoundError) { - return - } - throw error - } - const headers = response.headers - const lastModified = Date.parse(headers.get('Last-Modified') ?? '') - const size = parseInt(headers.get('Content-Length') ?? '0', 10) - return { - provider: '', - _class: core.class.Blob, - _id: objectName as Ref, - contentType: headers.get('Content-Type') ?? '', - size: isNaN(size) ? 0 : (size ?? 0), - etag: headers.get('ETag') ?? '', - space: core.space.Configuration, - modifiedBy: core.account.System, - modifiedOn: isNaN(lastModified) ? 0 : lastModified, - version: null - } - } - - async get (objectName: string): Promise { - const url = this.getObjectUrl(objectName) - - const response = await wrappedFetch(url, { headers: { ...this.headers } }) - - if (response.body == null) { - throw new StorageError('Missing response body') - } - return Readable.from(response.body) - } - - async put (objectName: string, stream: Readable | Buffer | string, contentType: string, size?: number): Promise { - const buffer = await toBuffer(stream) - const file = new File([new Uint8Array(buffer)], objectName, { type: contentType }) - const formData = new FormData() - formData.append('file', file) - let response - try { - response = await fetch(this.uploadUrl, { - method: 'POST', - body: formData, - headers: { ...this.headers } - }) - } catch (error: any) { - throw new NetworkError(`Network error ${error}`) - } - if (!response.ok) { - throw new StorageError(await response.text()) - } - const result = (await response.json()) as BlobUploadResult[] - if (Object.hasOwn(result[0], 'id')) { - const fileResult = result[0] as BlobUploadSuccess - return { - _class: core.class.Blob, - _id: fileResult.id as Ref, - space: core.space.Configuration, - modifiedOn: fileResult.metadata.lastModified, - modifiedBy: core.account.System, - provider: '', - contentType: fileResult.metadata.contentType, - etag: fileResult.metadata.etag, - version: null, - size: fileResult.metadata.size - } - } else { - const error = (result[0] as BlobUploadError) ?? 'Unknown error' - throw new StorageError(`Storage error ${error.error}`) - } - } - - async partial (objectName: string, offset: number, length?: number): Promise { - const url = this.getObjectUrl(objectName) - - const response = await wrappedFetch(url, { - headers: { - ...this.headers, - Range: length !== undefined ? `bytes=${offset}-${offset + length - 1}` : `bytes=${offset}` - } - }) - - if (response.body == null) { - throw new StorageError('Missing response body') - } - return Readable.from(response.body) - } - - async remove (objectName: string): Promise { - const url = this.getObjectUrl(objectName) - await wrappedFetch(url, { - method: 'DELETE', - headers: { ...this.headers } - }) - } -} - -async function toBuffer (data: Buffer | string | Readable): Promise { - if (Buffer.isBuffer(data)) { - return data - } else if (typeof data === 'string') { - return Buffer.from(data) - } else if (data instanceof Readable) { - const chunks: Buffer[] = [] - for await (const chunk of data) { - chunks.push(chunk) - } - return Buffer.concat(chunks as any) - } else { - throw new TypeError('Unsupported data type') - } -} - -async function wrappedFetch (url: string | URL, init?: RequestInit): Promise { - let response: Response - try { - response = await fetch(url, init) - } catch (error: any) { - throw new NetworkError(`Network error ${error}`) - } - if (!response.ok) { - const text = await response.text() - if (response.status === 404) { - throw new NotFoundError(text) - } else { - throw new StorageError(text) - } - } - return response -} - -export function createStorageClient ( - filesUrl: string, - uploadUrl: string, - token: string, - workspace: WorkspaceUuid -): StorageClient { - return new StorageClientImpl(filesUrl, uploadUrl, token, workspace) -} - -export async function connectStorage (url: string, options: AuthOptions, config?: ServerConfig): Promise { - config ??= await loadServerConfig(url) - const token = await getWorkspaceToken(url, options, config) - const filesUrl = (config.FILES_URL.startsWith('/') ? concatLink(url, config.FILES_URL) : config.FILES_URL).replace( - ':workspace', - token.workspaceId - ) - const uploadUrl = ( - config.UPLOAD_URL.startsWith('/') ? concatLink(url, config.UPLOAD_URL) : config.UPLOAD_URL - ).replace(':workspace', token.workspaceId) - return new StorageClientImpl(filesUrl, uploadUrl, token.token, token.workspaceId) -} diff --git a/foundations/core/packages/api-client/src/storage/error.ts b/foundations/core/packages/api-client/src/storage/error.ts deleted file mode 100644 index 2e5d98590d..0000000000 --- a/foundations/core/packages/api-client/src/storage/error.ts +++ /dev/null @@ -1,35 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export class NetworkError extends Error { - constructor (message: string) { - super(message) - this.name = 'NetworkError' - } -} - -export class StorageError extends Error { - constructor (message: string) { - super(message) - this.name = 'StorageError' - } -} - -export class NotFoundError extends StorageError { - constructor (message = 'Not Found') { - super(message) - this.name = 'NotFoundError' - } -} diff --git a/foundations/core/packages/api-client/src/storage/index.ts b/foundations/core/packages/api-client/src/storage/index.ts deleted file mode 100644 index 8be0b038d4..0000000000 --- a/foundations/core/packages/api-client/src/storage/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export { createStorageClient, connectStorage } from './client' -export * from './error' -export * from './types' diff --git a/foundations/core/packages/api-client/src/storage/types.ts b/foundations/core/packages/api-client/src/storage/types.ts deleted file mode 100644 index a3aed0c22d..0000000000 --- a/foundations/core/packages/api-client/src/storage/types.ts +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { Blob } from '@hcengineering/core' -import { Readable } from 'stream' - -export interface StorageClient { - stat: (objectName: string) => Promise - get: (objectName: string) => Promise - put: (objectName: string, stream: Readable | Buffer | string, contentType: string, size?: number) => Promise - partial: (objectName: string, offset: number, length?: number) => Promise - remove: (objectName: string) => Promise -} diff --git a/foundations/core/packages/api-client/src/types.ts b/foundations/core/packages/api-client/src/types.ts deleted file mode 100644 index 0b509ceee1..0000000000 --- a/foundations/core/packages/api-client/src/types.ts +++ /dev/null @@ -1,224 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { type ClientSocketFactory } from '@hcengineering/client' -import { - CollaborativeDoc, - type Account, - type AttachedData, - type AttachedDoc, - type Class, - type Data, - type Doc, - type DocumentQuery, - type DocumentUpdate, - type FindOptions, - type FindResult, - type Hierarchy, - type Mixin, - type MixinData, - type MixinUpdate, - type ModelDb, - type Ref, - type Space, - type TxResult, - type WithLookup -} from '@hcengineering/core' -import { type MarkupContent, type MarkupOperations } from './markup' - -type WithPropertyType = { - [P in keyof T]: T[P] extends X ? Y : T[P] -} - -/** @public */ -export type WithMarkup = WithPropertyType< -WithPropertyType, -CollaborativeDoc, -MarkupContent -> - -/** - * Platform API client - * @public - * */ -export type PlatformClient = { - getHierarchy: () => Hierarchy - - getModel: () => ModelDb - - getAccount: () => Promise - - close: () => Promise -} & FindOperations & -DocOperations & -CollectionOperations & -MixinOperations & -MarkupOperations & -AsyncDisposable - -/** - * @public - */ -export interface FindOperations { - findAll: ( - _class: Ref>, - query: DocumentQuery, - options?: FindOptions | undefined - ) => Promise> - - findOne: ( - _class: Ref>, - query: DocumentQuery, - options?: FindOptions | undefined - ) => Promise | undefined> -} - -/** - * @public - */ -export interface DocOperations { - createDoc: ( - _class: Ref>, - space: Ref, - attributes: WithMarkup>, - id?: Ref - ) => Promise> - - updateDoc: ( - _class: Ref>, - space: Ref, - objectId: Ref, - operations: WithMarkup>, - retrieve?: boolean - ) => Promise - - removeDoc: (_class: Ref>, space: Ref, objectId: Ref) => Promise -} - -/** - * @public - */ -export interface CollectionOperations { - addCollection: ( - _class: Ref>, - space: Ref, - attachedTo: Ref, - attachedToClass: Ref>, - collection: Extract | string, - attributes: WithMarkup>, - id?: Ref

- ) => Promise> - - updateCollection: ( - _class: Ref>, - space: Ref, - objectId: Ref

, - attachedTo: Ref, - attachedToClass: Ref>, - collection: Extract | string, - operations: WithMarkup>, - retrieve?: boolean - ) => Promise> - - removeCollection: ( - _class: Ref>, - space: Ref, - objectId: Ref

, - attachedTo: Ref, - attachedToClass: Ref>, - collection: Extract | string - ) => Promise> -} - -/** - * @public - */ -export interface MixinOperations { - createMixin: ( - objectId: Ref, - objectClass: Ref>, - objectSpace: Ref, - mixin: Ref>, - attributes: WithMarkup> - ) => Promise - - updateMixin: ( - objectId: Ref, - objectClass: Ref>, - objectSpace: Ref, - mixin: Ref>, - attributes: WithMarkup> - ) => Promise -} - -/** - * Configuration options for password-based authentication - * @public - */ -export interface PasswordAuthOptions { - /** User's email address */ - email: string - - /** User's password */ - password: string - - /** Workspace URL name */ - workspace: string -} - -/** - * Configuration options for token-based authentication - * @public - */ -export interface TokenAuthOptions { - /** Authentication token */ - token: string - - /** Workspace URL name */ - workspace: string -} - -/** - * Union type representing all authentication options - * Can be either password-based or token-based authentication - * @public - */ -export type AuthOptions = PasswordAuthOptions | TokenAuthOptions - -/** - * Configuration options for socket connection - * @public - */ -export interface ConnectSocketOptions { - /** - * Optional factory for creating custom WebSocket implementations - * Particularly useful in Node.js environments where you might need - * to provide a specific WebSocket client implementation - * If not provided, a default WebSocket implementation will be used - */ - socketFactory?: ClientSocketFactory - - /** - * Optional timeout duration for the connection attempt in milliseconds - * Specifies how long to wait for a connection before timing out - */ - connectionTimeout?: number -} - -/** - * API connect options - * @public - */ -export type ConnectOptions = ConnectSocketOptions & AuthOptions diff --git a/foundations/core/packages/api-client/src/utils.ts b/foundations/core/packages/api-client/src/utils.ts deleted file mode 100644 index 0d6efe5f3c..0000000000 --- a/foundations/core/packages/api-client/src/utils.ts +++ /dev/null @@ -1,55 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { type WorkspaceLoginInfo, getClient as getAccountClient } from '@hcengineering/account-client' -import { WorkspaceUuid } from '@hcengineering/core' -import { AuthOptions } from './types' -import { loadServerConfig, ServerConfig } from './config' - -export interface WorkspaceToken { - endpoint: string - token: string - workspaceId: WorkspaceUuid - info: WorkspaceLoginInfo -} - -export async function getWorkspaceToken ( - url: string, - options: AuthOptions, - config?: ServerConfig -): Promise { - config ??= await loadServerConfig(url) - - let token: string | undefined - - if ('token' in options) { - token = options.token - } else { - const { email, password } = options - const loginInfo = await getAccountClient(config.ACCOUNTS_URL).login(email, password) - token = loginInfo.token - } - - if (token === undefined) { - throw new Error('Login failed') - } - - const ws = await getAccountClient(config.ACCOUNTS_URL, token).selectWorkspace(options.workspace) - if (ws === undefined) { - throw new Error('Workspace not found') - } - - return { endpoint: ws.endpoint, token: ws.token, workspaceId: ws.workspace, info: ws } -} diff --git a/foundations/core/packages/api-client/tsconfig.json b/foundations/core/packages/api-client/tsconfig.json deleted file mode 100644 index 28ecf4dfd3..0000000000 --- a/foundations/core/packages/api-client/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declaration": true, - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo", - "types": ["node", "jest"] - } -} diff --git a/foundations/core/packages/client-resources/.eslintrc.js b/foundations/core/packages/client-resources/.eslintrc.js deleted file mode 100644 index 72235dc283..0000000000 --- a/foundations/core/packages/client-resources/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/foundations/core/packages/client-resources/.npmignore b/foundations/core/packages/client-resources/.npmignore deleted file mode 100644 index e3ec093c38..0000000000 --- a/foundations/core/packages/client-resources/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!/lib/** -!CHANGELOG.md -/lib/**/__tests__/ diff --git a/foundations/core/packages/client-resources/CHANGELOG.json b/foundations/core/packages/client-resources/CHANGELOG.json deleted file mode 100644 index c9f112b314..0000000000 --- a/foundations/core/packages/client-resources/CHANGELOG.json +++ /dev/null @@ -1,175 +0,0 @@ -{ - "name": "@hcengineering/client-resources", - "entries": [ - { - "version": "0.7.17", - "tag": "@hcengineering/client-resources_v0.7.17", - "date": "Mon, 27 Oct 2025 13:27:12 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" - } - ] - } - }, - { - "version": "0.7.6", - "tag": "@hcengineering/client-resources_v0.7.6", - "date": "Tue, 14 Oct 2025 04:58:17 GMT", - "comments": { - "patch": [ - { - "comment": "update deps" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/analytics\" from `^0.7.4` to `0.7.5`" - }, - { - "comment": "Updating dependency \"@hcengineering/client\" from `^0.7.5` to `0.7.6`" - }, - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`" - }, - { - "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.4` to `0.7.5`" - }, - { - "comment": "Updating dependency \"@hcengineering/rpc\" from `^0.7.4` to `0.7.5`" - } - ] - } - }, - { - "version": "0.7.5", - "tag": "@hcengineering/client-resources_v0.7.5", - "date": "Sat, 11 Oct 2025 18:20:33 GMT", - "comments": { - "patch": [ - { - "comment": "Update to latest platform rig" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/analytics\" from `^0.7.3` to `0.7.4`" - }, - { - "comment": "Updating dependency \"@hcengineering/client\" from `^0.7.4` to `0.7.5`" - }, - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`" - }, - { - "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.3` to `0.7.4`" - }, - { - "comment": "Updating dependency \"@hcengineering/rpc\" from `^0.7.3` to `0.7.4`" - } - ] - } - }, - { - "version": "0.7.4", - "tag": "@hcengineering/client-resources_v0.7.4", - "date": "Thu, 09 Oct 2025 16:57:55 GMT", - "comments": { - "patch": [ - { - "comment": "fix formatting" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/client\" from `^0.7.3` to `0.7.4`" - }, - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.4` to `0.7.5`" - } - ] - } - }, - { - "version": "0.7.3", - "tag": "@hcengineering/client-resources_v0.7.3", - "date": "Wed, 08 Oct 2025 03:40:53 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`" - } - ] - } - }, - { - "version": "0.6.4", - "tag": "@hcengineering/client-resources_v0.6.4", - "date": "Tue, 09 Nov 2021 11:00:07 GMT", - "comments": { - "patch": [ - { - "comment": "Expose `connect`." - } - ] - } - }, - { - "version": "0.6.3", - "tag": "@hcengineering/client-resources_v0.6.3", - "date": "Sat, 14 Aug 2021 09:12:06 GMT", - "comments": { - "patch": [ - { - "comment": "ping" - } - ] - } - }, - { - "version": "0.6.2", - "tag": "@hcengineering/client-resources_v0.6.2", - "date": "Wed, 11 Aug 2021 10:08:19 GMT", - "comments": { - "patch": [ - { - "comment": "server ping" - } - ] - } - }, - { - "version": "0.6.1", - "tag": "@hcengineering/client-resources_v0.6.1", - "date": "Sun, 08 Aug 2021 21:05:26 GMT", - "comments": { - "patch": [ - { - "comment": "Fix server connection" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `~0.6.0` to `~0.6.8`" - }, - { - "comment": "Updating dependency \"@hcengineering/client\" from `~0.6.0` to `~0.6.1`" - } - ] - } - }, - { - "version": "0.6.0", - "tag": "@hcengineering/client-resources_v0.6.0", - "date": "Sun, 08 Aug 2021 10:14:57 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/platform\" from `~0.6.3` to `~0.6.4`" - } - ] - } - } - ] -} diff --git a/foundations/core/packages/client-resources/CHANGELOG.md b/foundations/core/packages/client-resources/CHANGELOG.md deleted file mode 100644 index a15491ef4e..0000000000 --- a/foundations/core/packages/client-resources/CHANGELOG.md +++ /dev/null @@ -1,68 +0,0 @@ -# Change Log - @hcengineering/client-resources - -This log was last generated on Mon, 27 Oct 2025 13:27:12 GMT and should not be manually modified. - -## 0.7.17 -Mon, 27 Oct 2025 13:27:12 GMT - -_Version update only_ - -## 0.7.6 -Tue, 14 Oct 2025 04:58:17 GMT - -### Patches - -- update deps - -## 0.7.5 -Sat, 11 Oct 2025 18:20:33 GMT - -### Patches - -- Update to latest platform rig - -## 0.7.4 -Thu, 09 Oct 2025 16:57:55 GMT - -### Patches - -- fix formatting - -## 0.7.3 -Wed, 08 Oct 2025 03:40:53 GMT - -_Version update only_ - -## 0.6.4 -Tue, 09 Nov 2021 11:00:07 GMT - -### Patches - -- Expose `connect`. - -## 0.6.3 -Sat, 14 Aug 2021 09:12:06 GMT - -### Patches - -- ping - -## 0.6.2 -Wed, 11 Aug 2021 10:08:19 GMT - -### Patches - -- server ping - -## 0.6.1 -Sun, 08 Aug 2021 21:05:26 GMT - -### Patches - -- Fix server connection - -## 0.6.0 -Sun, 08 Aug 2021 10:14:57 GMT - -_Initial release_ - diff --git a/foundations/core/packages/client-resources/config/rig.json b/foundations/core/packages/client-resources/config/rig.json deleted file mode 100644 index 0110930f55..0000000000 --- a/foundations/core/packages/client-resources/config/rig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig" -} diff --git a/foundations/core/packages/client-resources/jest.config.js b/foundations/core/packages/client-resources/jest.config.js deleted file mode 100644 index fc291ae4b4..0000000000 --- a/foundations/core/packages/client-resources/jest.config.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ['./src'], - coverageReporters: ['text-summary', 'html', 'lcov'], - testTimeout: 10000, // 10 seconds timeout for all tests - forceExit: true // Force exit after tests complete to handle lingering timers -} diff --git a/foundations/core/packages/client-resources/package.json b/foundations/core/packages/client-resources/package.json deleted file mode 100644 index 6272bb9af9..0000000000 --- a/foundations/core/packages/client-resources/package.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "name": "@hcengineering/client-resources", - "version": "0.7.17", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "files": [ - "lib/**/*", - "types/**/*", - "src/**/*", - "!src/**/__test__/**", - "tsconfig.json" - ], - "author": "Anticrm Platform Contributors", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "build:watch": "compile", - "format": "format src", - "test": "jest --passWithNoTests --silent --coverage", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --coverage", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "workspace:^0.7.19", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "@typescript-eslint/parser": "^6.21.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.6.2", - "typescript": "^5.9.3", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5", - "@types/snappyjs": "^0.7.1", - "@types/node": "^22.18.1", - "ws": "^8.18.2", - "@types/ws": "^8.5.12", - "eslint-plugin-svelte": "^2.35.1" - }, - "dependencies": { - "@hcengineering/analytics": "workspace:^0.7.17", - "@hcengineering/client": "workspace:^0.7.17", - "@hcengineering/core": "workspace:^0.7.22", - "@hcengineering/platform": "workspace:^0.7.18", - "@hcengineering/rpc": "workspace:^0.7.17", - "snappyjs": "^0.7.0" - }, - "repository": "https://github.com/hcengineering/huly.core", - "publishConfig": { - "access": "public" - }, - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.js" - } - } -} diff --git a/foundations/core/packages/client-resources/readme.md b/foundations/core/packages/client-resources/readme.md deleted file mode 100644 index 64370999be..0000000000 --- a/foundations/core/packages/client-resources/readme.md +++ /dev/null @@ -1,33 +0,0 @@ -# Overview - -Package allow to create a client to interact with running platform. - -## Usage - -```ts - import clientResources from '@hcengineering/client-resources' - import core, { Client } from '@hcengineering/core' - - // ... - - const token = ... // Token obtained somehow. - - const connection: Client = await (await clientResources()).function.GetClient(token, transactorUrl) - - // Now client is usable - - // Use close, to shutdown connection. - await connection.close() -``` - -## Node JS - -For NodeJS environment it is required to configure ClientSocketFactory using 'ws' package. - -```ts -// We need to override default WebSocket factory with 'ws' one. -setMetadata(client.metadata.ClientSocketFactory, (url) => new WebSocket(url)) - -const connection: Client = await (await clientResources()).function.GetClient(token, transactorUrl) -... -``` diff --git a/foundations/core/packages/client-resources/src/__tests__/connection.test.ts b/foundations/core/packages/client-resources/src/__tests__/connection.test.ts deleted file mode 100644 index 7fb4f195be..0000000000 --- a/foundations/core/packages/client-resources/src/__tests__/connection.test.ts +++ /dev/null @@ -1,374 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { ClientSocket, ClientSocketReadyState, pongConst, pingConst } from '@hcengineering/client' -import core, { - type Tx, - generateId, - type WorkspaceUuid, - type PersonUuid, - TxCreateDoc, - type Doc -} from '@hcengineering/core' -import { connect } from '../connection' - -// Mock CloseEvent for Node.js environment (used in MockWebSocket) -// eslint-disable-next-line @typescript-eslint/no-extraneous-class -class MockCloseEvent { - readonly type: string - readonly code: number - - constructor (type: string, init?: { code?: number }) { - this.type = type - this.code = init?.code ?? 1000 - } -} - -// Mock MessageEvent for Node.js environment (used in MockWebSocket) -// eslint-disable-next-line @typescript-eslint/no-extraneous-class -class MockMessageEvent { - readonly type: string - readonly data: any - - constructor (type: string, init: { data: any }) { - this.type = type - this.data = init.data - } -} - -// Mock Event for Node.js environment (used in MockWebSocket) -// eslint-disable-next-line @typescript-eslint/no-extraneous-class -class MockEvent { - readonly type: string - - constructor (type: string) { - this.type = type - } -} - -// Mock WebSocket implementation for testing -class MockWebSocket implements ClientSocket { - readyState: ClientSocketReadyState = ClientSocketReadyState.CONNECTING - onmessage?: ((this: ClientSocket, ev: MessageEvent) => any) | null = null - onclose?: ((this: ClientSocket, ev: CloseEvent) => any) | null = null - onopen?: ((this: ClientSocket, ev: Event) => any) | null = null - onerror?: ((this: ClientSocket, ev: Event) => any) | null = null - bufferedAmount?: number = 0 - - private readonly messageQueue: any[] = [] - private closeCode?: number - private timers: any[] = [] - - constructor (public url: string) { - // Simulate async connection - const timer = setTimeout(() => { - if (this.readyState === ClientSocketReadyState.CLOSED) { - return - } - this.readyState = ClientSocketReadyState.OPEN - if (this.onopen !== null && this.onopen !== undefined) { - this.onopen(new MockEvent('open') as any) - } - // Process queued messages - this.processQueue() - }, 10) - this.timers.push(timer) - } - - clearAllTimers (): void { - for (const timer of this.timers) { - clearTimeout(timer) - } - this.timers = [] - } - - send (data: string | ArrayBufferLike | Blob | ArrayBufferView): void { - if (this.readyState !== ClientSocketReadyState.OPEN) { - throw new Error('WebSocket is not open') - } - - // Parse and respond to messages - try { - const message = typeof data === 'string' ? data : new TextDecoder().decode(data as ArrayBuffer) - - if (message === pingConst) { - this.simulateMessage(pongConst) - return - } - - // Parse RPC message - const rpcMessage = JSON.parse(message) - - // Handle hello request - if (rpcMessage.method === 'hello') { - const response = { - id: rpcMessage.id, - result: { - binary: false, - compression: false, - reconnect: false - } - } - this.simulateMessage(JSON.stringify(response)) - return - } - - // Handle loadModel request - if (rpcMessage.method === 'loadModel') { - const response = { - id: rpcMessage.id, - result: { - transactions: [], - hash: 'test-hash', - full: false - } - } - this.simulateMessage(JSON.stringify(response)) - return - } - - // Handle findAll request - if (rpcMessage.method === 'findAll') { - const response = { - id: rpcMessage.id, - result: [] - } - this.simulateMessage(JSON.stringify(response)) - return - } - - // Handle tx request - if (rpcMessage.method === 'tx') { - const response = { - id: rpcMessage.id, - result: {} - } - this.simulateMessage(JSON.stringify(response)) - return - } - - // Default response - const response = { - id: rpcMessage.id, - result: {} - } - this.simulateMessage(JSON.stringify(response)) - } catch (err) { - console.error('Error processing message:', err) - } - } - - close (code?: number): void { - this.closeCode = code - this.readyState = ClientSocketReadyState.CLOSING - this.clearAllTimers() - const timer = setTimeout(() => { - this.readyState = ClientSocketReadyState.CLOSED - if (this.onclose !== null && this.onclose !== undefined) { - this.onclose(new MockCloseEvent('close', { code: code ?? 1000 }) as any) - } - }, 10) - this.timers.push(timer) - } - - // Helper method to simulate receiving messages - simulateMessage (data: string | ArrayBuffer): void { - if (this.readyState === ClientSocketReadyState.OPEN) { - if (this.onmessage !== null && this.onmessage !== undefined) { - const event = new MockMessageEvent('message', { data }) - this.onmessage(event as any) - } - } else { - this.messageQueue.push(data) - } - } - - private processQueue (): void { - while (this.messageQueue.length > 0) { - const data = this.messageQueue.shift() - this.simulateMessage(data) - } - } - - // Helper to simulate server-initiated transactions - simulateTransaction (tx: Tx): void { - const message = { - result: { - _class: 'core:class:TxNotification', - tx: [tx] - } - } - this.simulateMessage(JSON.stringify(message)) - } -} - -describe('MockWebSocket', () => { - it('should connect and send messages', async () => { - const ws = new MockWebSocket('ws://localhost:3333') - - await new Promise((resolve) => { - ws.onopen = () => { - expect(ws.readyState).toBe(ClientSocketReadyState.OPEN) - resolve() - } - }) - - // Test message sending - let receivedMessage: string | null = null - ws.onmessage = (ev: MessageEvent) => { - receivedMessage = ev.data as string - } - - ws.simulateMessage('test message') - - expect(receivedMessage).toBe('test message') - - // Test close - await new Promise((resolve) => { - ws.onclose = () => { - expect(ws.readyState).toBe(ClientSocketReadyState.CLOSED) - resolve() - } - ws.close() - }) - }) - - it('should handle ping/pong', async () => { - const ws = new MockWebSocket('ws://localhost:3333') - - await new Promise((resolve) => { - ws.onopen = () => { - resolve() - } - }) - - let receivedMessage: string | null = null - ws.onmessage = (ev: MessageEvent) => { - receivedMessage = ev.data as string - } - - ws.send(pingConst) - - await new Promise((resolve) => setTimeout(resolve, 50)) - - expect(receivedMessage).toBe(pongConst) - - ws.close() - }) -}) - -describe('connect function', () => { - let connections: Array<{ close: () => Promise }> = [] - let mockWebSockets: MockWebSocket[] = [] - - afterEach(async () => { - // Clean up all connections - for (const conn of connections) { - await conn.close() - } - connections = [] - - // Clean up all mock websockets - for (const ws of mockWebSockets) { - ws.clearAllTimers() - if (ws.readyState !== ClientSocketReadyState.CLOSED) { - ws.close() - } - } - mockWebSockets = [] - - // Give time for all timers to clear - await new Promise((resolve) => setTimeout(resolve, 100)) - }) - - it('should establish connection', async () => { - const workspaceId = 'test-workspace' as WorkspaceUuid - const userId = 'test-user' as PersonUuid - - const handler = jest.fn() - - const mockWs = new MockWebSocket('ws://localhost:3333') - mockWebSockets.push(mockWs) - - const client = connect('ws://localhost:3333', handler, workspaceId, userId, { - socketFactory: (url: string) => mockWs as any - }) - - connections.push(client) - - expect(client).toBeDefined() - - // Wait for connection to establish - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Close connection immediately to prevent timers from continuing - await client.close() - - // Wait for close to complete - await new Promise((resolve) => setTimeout(resolve, 50)) - }) - - it('should handle transactions', async () => { - const workspaceId = 'test-workspace' as WorkspaceUuid - const userId = 'test-user' as PersonUuid - - let txReceived: Tx | null = null - const handler = (...tx: Tx[]): void => { - if (tx.length > 0) { - txReceived = tx[0] - } - } - - const mockWs = new MockWebSocket('ws://localhost:3333') - const client = connect('ws://localhost:3333', handler, workspaceId, userId, { - socketFactory: (url: string) => mockWs as any - }) - - connections.push(client) - - // Wait for connection to establish - await new Promise((resolve) => setTimeout(resolve, 150)) - - // Simulate a transaction from server - const testTx: TxCreateDoc = { - _id: generateId(), - _class: core.class.TxCreateDoc, - space: core.space.Model, - objectId: generateId(), - objectClass: core.class.Space, - objectSpace: core.space.Model, - modifiedBy: core.account.System, - modifiedOn: Date.now(), - createdOn: Date.now(), - attributes: { - name: 'Test Space', - description: '', - private: false, - archived: false, - members: [] - } - } - - mockWs.simulateTransaction(testTx) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(txReceived).toBeDefined() - - // Close immediately after test - await client.close() - await new Promise((resolve) => setTimeout(resolve, 50)) - }) -}) diff --git a/foundations/core/packages/client-resources/src/__tests__/integration.test.ts b/foundations/core/packages/client-resources/src/__tests__/integration.test.ts deleted file mode 100644 index c0f6b2642e..0000000000 --- a/foundations/core/packages/client-resources/src/__tests__/integration.test.ts +++ /dev/null @@ -1,537 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import core, { - type Client, - type Tx, - generateId, - TxCreateDoc, - type Doc, - createClient, - type ClientConnection, - type TxHandler, - ClientConnectEvent, - type LoadModelResponse, - Hierarchy, - ModelDb, - TxDb, - type Class, - type Ref, - type DocumentQuery, - type FindOptions, - type FindResult, - type SearchQuery, - type SearchOptions, - type SearchResult, - type TxResult, - type Timestamp, - type Domain, - type DocChunk, - type OperationDomain, - type DomainParams, - type DomainRequestOptions, - type DomainResult, - DOMAIN_MODEL, - DOMAIN_TX, - ClassifierKind, - TxFactory, - type Data, - type Obj -} from '@hcengineering/core' -import type { IntlString } from '@hcengineering/platform' - -const txFactory = new TxFactory(core.account.System) - -function createClass (_class: Ref>, attributes: Data>): TxCreateDoc { - return txFactory.createTxCreateDoc(core.class.Class, core.space.Model, attributes, _class) -} - -// Generate minimal model for testing -function generateMinimalModel (): Tx[] { - const txes: Tx[] = [] - - txes.push(createClass(core.class.Obj, { label: 'Obj' as IntlString, kind: ClassifierKind.CLASS })) - txes.push( - createClass(core.class.Doc, { label: 'Doc' as IntlString, extends: core.class.Obj, kind: ClassifierKind.CLASS }) - ) - txes.push( - createClass(core.class.Class, { - label: 'Class' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.CLASS, - domain: DOMAIN_MODEL - }) - ) - txes.push( - createClass(core.class.Space, { - label: 'Space' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.CLASS, - domain: DOMAIN_MODEL - }) - ) - txes.push( - createClass(core.class.Tx, { - label: 'Tx' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.CLASS, - domain: DOMAIN_TX - }) - ) - txes.push( - createClass(core.class.TxCUD, { - label: 'TxCUD' as IntlString, - extends: core.class.Tx, - kind: ClassifierKind.CLASS, - domain: DOMAIN_TX - }) - ) - txes.push( - createClass(core.class.TxCreateDoc, { - label: 'TxCreateDoc' as IntlString, - extends: core.class.TxCUD, - kind: ClassifierKind.CLASS - }) - ) - txes.push( - createClass(core.class.TxUpdateDoc, { - label: 'TxUpdateDoc' as IntlString, - extends: core.class.TxCUD, - kind: ClassifierKind.CLASS - }) - ) - txes.push( - createClass(core.class.TxRemoveDoc, { - label: 'TxRemoveDoc' as IntlString, - extends: core.class.TxCUD, - kind: ClassifierKind.CLASS - }) - ) - - return txes -} - -// Test utilities for creating mock servers and clients - -/** - * Mock ClientConnection for integration testing - */ -export class MockClientConnection implements ClientConnection { - private readonly handlers: TxHandler[] = [] - private _connected: boolean = true - private readonly hierarchy: Hierarchy - private readonly model: ModelDb - private readonly transactions: TxDb - - onConnect?: (event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise - getLastHash?: () => Promise - - constructor (txes: Tx[] = []) { - const minimalModel = txes.length === 0 ? generateMinimalModel() : txes - - this.hierarchy = new Hierarchy() - for (const tx of minimalModel) { - this.hierarchy.tx(tx) - } - - this.model = new ModelDb(this.hierarchy) - this.transactions = new TxDb(this.hierarchy) - - for (const tx of minimalModel) { - void this.model.tx(tx) - void this.transactions.tx(tx) - } - } - - isConnected (): boolean { - return this._connected - } - - setConnected (value: boolean): void { - this._connected = value - } - - pushHandler (handler: TxHandler): void { - this.handlers.push(handler) - } - - async findAll( - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): Promise> { - return await this.model.findAll(_class, query, options) - } - - async searchFulltext (query: SearchQuery, options: SearchOptions): Promise { - return { docs: [] } - } - - async tx (tx: Tx): Promise { - if (tx.objectSpace === core.space.Model) { - this.hierarchy.tx(tx) - await this.model.tx(tx) - } - await this.transactions.tx(tx) - - this.handlers.forEach((h) => { - h(tx) - }) - - return {} - } - - async loadModel (last: Timestamp, hash?: string): Promise { - const txes = await this.transactions.findAll(core.class.Tx, { - objectSpace: core.space.Model, - modifiedOn: { $gt: last } - }) - - if (hash !== undefined) { - return { - transactions: txes, - hash: 'test-hash', - full: false - } - } - return txes - } - - async close (): Promise { - this._connected = false - } - - async loadChunk (domain: Domain, idx?: number): Promise { - return { - idx: idx ?? 0, - docs: [], - finished: true - } - } - - async getDomainHash (domain: Domain): Promise { - return 'test-hash' - } - - async closeChunk (idx: number): Promise {} - - async loadDocs (domain: Domain, docs: Ref[]): Promise { - return [] - } - - async upload (domain: Domain, docs: Doc[]): Promise {} - - async clean (domain: Domain, docs: Ref[]): Promise {} - - async sendForceClose (): Promise {} - - async domainRequest ( - ctx: OperationDomain, - params: DomainParams, - options?: DomainRequestOptions - ): Promise { - return { domain: ctx, value: null } - } - - simulateTransaction (tx: Tx): void { - this.handlers.forEach((h) => { - h(tx) - }) - } - - async simulateConnect (event: ClientConnectEvent = ClientConnectEvent.Connected): Promise { - if (this.onConnect !== undefined) { - await this.onConnect(event, undefined, {}) - } - } -} - -/** - * Create a test client with mock connection - */ -export async function createTestClient (initialTxes: Tx[] = []): Promise<{ - client: Client - connection: MockClientConnection - close: () => Promise -}> { - const connection = new MockClientConnection(initialTxes) - - const client = await createClient(async (handler: TxHandler) => { - connection.pushHandler(handler) - return connection - }) - - return { - client, - connection, - close: async () => { - await client.close() - } - } -} - -/** - * Helper to create a test transaction - */ -export function createTestTx (objectClass: Ref> = core.class.Space, attributes: any = {}): TxCreateDoc { - return { - _id: generateId(), - _class: core.class.TxCreateDoc, - space: core.space.Tx, - objectId: generateId(), - objectClass, - objectSpace: core.space.Model, - modifiedBy: core.account.System, - modifiedOn: Date.now(), - createdOn: Date.now(), - attributes: { - name: 'TestDoc', - description: '', - ...attributes - } - } -} - -describe('Client-Resources Integration Tests', () => { - it('should create a full client with connection', async () => { - const { client, connection, close } = await createTestClient() - - expect(client).toBeDefined() - expect(client.getHierarchy()).toBeDefined() - expect(client.getModel()).toBeDefined() - expect(connection.isConnected()).toBe(true) - - await close() - }) - - it('should handle transactions end-to-end', async () => { - const { client, close } = await createTestClient() - - const notifySpy = jest.fn() - client.notify = notifySpy - - const tx = createTestTx(core.class.Space, { - name: 'TestSpace', - private: false, - archived: false, - members: [] - }) - - // When we tx directly, it goes through the connection which simulates the server - // The transaction is processed locally first, then notify is called when - // transaction comes back from "server" (our mock connection) - await client.tx(tx) - - // The mock connection immediately notifies handlers when we call tx - // So notifySpy should have been called - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(notifySpy).toHaveBeenCalled() - - await close() - }) - - it('should handle reconnection events', async () => { - const { connection, close } = await createTestClient() - - let connectEventReceived: ClientConnectEvent | undefined - - const originalOnConnect = connection.onConnect - connection.onConnect = async (event, lastTx, data) => { - connectEventReceived = event - await originalOnConnect?.(event, lastTx, data) - } - - await connection.simulateConnect(ClientConnectEvent.Reconnected) - - expect(connectEventReceived).toBe(ClientConnectEvent.Reconnected) - - await close() - }) - - it('should handle model updates', async () => { - const { client, close } = await createTestClient() - - const tx = createTestTx(core.class.Space, { - name: 'ModelSpace', - private: false, - archived: false, - members: [] - }) - - tx.objectSpace = core.space.Model - - await client.tx(tx) - - const spaces = await client.findAll(core.class.Space, { name: 'ModelSpace' }) - expect(spaces).toBeDefined() - - await close() - }) - - it('should handle findAll with options', async () => { - const { client, close } = await createTestClient() - - const result = await client.findAll(core.class.Space, {}, { limit: 10, sort: { name: 1 } }) - - expect(result).toBeDefined() - expect(Array.isArray(result)).toBe(true) - expect(result.total).toBeDefined() - - await close() - }) - - it('should handle findOne', async () => { - const { client, close } = await createTestClient() - - const result = await client.findOne(core.class.Space, {}) - - // Result may be undefined if no documents exist - expect(result === undefined || result !== null).toBe(true) - - await close() - }) - - it('should handle connection loss and reconnection', async () => { - const { connection, close } = await createTestClient() - - expect(connection.isConnected()).toBe(true) - - connection.setConnected(false) - expect(connection.isConnected()).toBe(false) - - connection.setConnected(true) - expect(connection.isConnected()).toBe(true) - - await close() - }) - - it('should handle multiple handlers', async () => { - const { connection, close } = await createTestClient() - - const handler1 = jest.fn() - const handler2 = jest.fn() - - connection.pushHandler(handler1) - connection.pushHandler(handler2) - - const tx = createTestTx() - - connection.simulateTransaction(tx) - - expect(handler1).toHaveBeenCalledWith(tx) - expect(handler2).toHaveBeenCalledWith(tx) - - await close() - }) - - it('should handle searchFulltext', async () => { - const { client, close } = await createTestClient() - - const query: SearchQuery = { query: 'test' } - const options: SearchOptions = {} - - const result = await client.searchFulltext(query, options) - - expect(result).toBeDefined() - expect(result.docs).toBeDefined() - expect(Array.isArray(result.docs)).toBe(true) - - await close() - }) - - it('should handle domainRequest', async () => { - const { client, close } = await createTestClient() - - const result = await client.domainRequest('test-domain' as OperationDomain, {}) - - expect(result).toBeDefined() - - await close() - }) - - it('should properly clean up on close', async () => { - const { connection, close } = await createTestClient() - - expect(connection.isConnected()).toBe(true) - - await close() - - expect(connection.isConnected()).toBe(false) - }) - - it('should handle upgrade events', async () => { - const { connection, close } = await createTestClient() - - let upgradeReceived = false - - const originalOnConnect = connection.onConnect - connection.onConnect = async (event, lastTx, data) => { - if (event === ClientConnectEvent.Upgraded) { - upgradeReceived = true - } - await originalOnConnect?.(event, lastTx, data) - } - - await connection.simulateConnect(ClientConnectEvent.Upgraded) - - expect(upgradeReceived).toBe(true) - - await close() - }) - - it('should handle maintenance events', async () => { - const { connection, close } = await createTestClient() - - let maintenanceReceived = false - - const originalOnConnect = connection.onConnect - connection.onConnect = async (event, lastTx, data) => { - if (event === ClientConnectEvent.Maintenance) { - maintenanceReceived = true - } - await originalOnConnect?.(event, lastTx, data) - } - - await connection.simulateConnect(ClientConnectEvent.Maintenance) - - expect(maintenanceReceived).toBe(true) - - await close() - }) - - it('should handle concurrent transactions', async () => { - const { client, connection, close } = await createTestClient() - - const notifySpy = jest.fn() - client.notify = notifySpy - - const tx1 = createTestTx(core.class.Space, { name: 'Space1' }) - const tx2 = createTestTx(core.class.Space, { name: 'Space2' }) - const tx3 = createTestTx(core.class.Space, { name: 'Space3' }) - - await Promise.all([client.tx(tx1), client.tx(tx2), client.tx(tx3)]) - - connection.simulateTransaction(tx1) - connection.simulateTransaction(tx2) - connection.simulateTransaction(tx3) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(notifySpy.mock.calls.length).toBeGreaterThanOrEqual(3) - - await close() - }) -}) diff --git a/foundations/core/packages/client-resources/src/connection.ts b/foundations/core/packages/client-resources/src/connection.ts deleted file mode 100644 index b692c2ccc7..0000000000 --- a/foundations/core/packages/client-resources/src/connection.ts +++ /dev/null @@ -1,911 +0,0 @@ -// -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// Copyright © 2021 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { Analytics } from '@hcengineering/analytics' -import client, { - type ClientFactoryOptions, - ClientSocket, - ClientSocketReadyState, - pingConst, - pongConst -} from '@hcengineering/client' -import core, { - Account, - Class, - ClientConnectEvent, - ClientConnection, - clone, - Doc, - DocChunk, - DocumentQuery, - Domain, - type DomainParams, - type DomainRequestOptions, - type DomainResult, - FindOptions, - FindResult, - generateId, - LoadModelResponse, - type MeasureContext, - MeasureMetricsContext, - type OperationDomain, - type PersonUuid, - Ref, - SearchOptions, - SearchQuery, - SearchResult, - Timestamp, - toFindResult, - Tx, - TxApplyIf, - TxHandler, - TxResult, - type WorkspaceUuid -} from '@hcengineering/core' -import platform, { getMetadata, PlatformError, Severity, Status, UNAUTHORIZED } from '@hcengineering/platform' -import { HelloRequest, HelloResponse, type RateLimitInfo, ReqId, type Response, RPCHandler } from '@hcengineering/rpc' -import { uncompress } from 'snappyjs' - -const SECOND = 1000 -const pingTimeout = 10 * SECOND -const hangTimeout = 5 * 60 * SECOND -const dialTimeout = 30 * SECOND - -class RequestPromise { - startTime: number = Date.now() - handleTime?: (diff: number, result: any, serverTime: number, queue: number, toRecieve: number) => void - readonly promise: Promise - resolve!: (value?: any) => void - reject!: (reason?: any) => void - reconnect?: () => void - - // Required to properly handle rate limits - sendData: () => void = () => {} - - constructor ( - readonly method: string, - readonly params: any[], - - readonly handleResult?: (result: any) => Promise - ) { - this.promise = new Promise((resolve, reject) => { - this.resolve = resolve - this.reject = reject - }) - } - - chunks?: { index: number, data: FindResult }[] -} - -const globalRPCHandler: RPCHandler = new RPCHandler() - -interface OnConnectHandler { - resolve: () => void - reject: (err: Error) => void -} - -class Connection implements ClientConnection { - private websocket: ClientSocket | null = null - binaryMode = false - compressionMode = false - private readonly requests = new Map() - private lastId = 0 - private interval: number | undefined - private dialTimer: number | undefined - - private sockets = 0 - private openAction: any - - private readonly sessionId: string | undefined - private closed = false - - private upgrading: boolean = false - - private pingResponse: number = Date.now() - - private helloReceived: boolean = false - - private account: Account | undefined - - onConnect?: (event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise - - rpcHandler: RPCHandler - - lastHash?: string - - handlers: TxHandler[] = [] - - constructor ( - private readonly ctx: MeasureContext, - private readonly url: string, - handler: TxHandler, - readonly workspace: WorkspaceUuid, - readonly user: PersonUuid, - readonly opt?: ClientFactoryOptions - ) { - if (typeof sessionStorage !== 'undefined') { - // Find local session id in session storage only if user refresh a page. - const sKey = 'session.id.' + this.url - let sessionId = sessionStorage.getItem(sKey) ?? undefined - if (sessionId === undefined) { - sessionId = generateId() - console.log('Generate new SessionId', sessionId) - this.sessionId = sessionId - } else { - this.sessionId = sessionId - sessionStorage.removeItem(sKey) - } - window.addEventListener('beforeunload', () => { - sessionStorage.setItem(sKey, sessionId) - }) - } else { - this.sessionId = generateId() - } - this.rpcHandler = opt?.useGlobalRPCHandler === true ? globalRPCHandler : new RPCHandler() - this.pushHandler(handler) - this.onConnect = opt?.onConnect - - this.scheduleOpen(this.ctx, false) - } - - pushHandler (handler: TxHandler): void { - this.handlers.push(handler) - } - - async getLastHash (ctx: MeasureContext): Promise { - await this.waitOpenConnection(ctx) - return this.lastHash - } - - private schedulePing (socketId: number): void { - this.pingResponse = Date.now() - const wsocket = this.websocket - - clearInterval(this.interval) - this.interval = setInterval(() => { - if (wsocket !== this.websocket) { - clearInterval(this.interval) - return - } - if (!this.upgrading && this.pingResponse !== 0 && Date.now() - this.pingResponse > hangTimeout) { - // No ping response from server. - - if (this.websocket !== null) { - console.log('no ping response from server. Closing socket.', socketId, this.workspace, this.user) - clearInterval(this.interval) - this.websocket.close(1000) - return - } - } - - if (!this.closed) { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - void this.sendRequest({ - method: pingConst, - params: [], - once: true, - handleResult: async (result) => { - if (this.websocket === wsocket) { - this.pingResponse = Date.now() - } - } - }).catch((err) => { - this.ctx.error('failed to send msg', { err }) - }) - } else { - clearInterval(this.interval) - } - }, pingTimeout) - } - - async close (): Promise { - this.closed = true - clearTimeout(this.openAction) - clearTimeout(this.dialTimer) - clearInterval(this.interval) - for (const handler of this.onConnectHandlers) { - handler.reject(new Error('Connection closed')) - } - for (const req of this.requests.values()) { - req.reject(new Error('Connection closed')) - } - if (this.websocket !== null) { - this.websocket.close(1000) - this.websocket = null - } - } - - isConnected (): boolean { - return this.websocket != null && this.websocket.readyState === ClientSocketReadyState.OPEN && this.helloReceived - } - - delay = 0 - onConnectHandlers: OnConnectHandler[] = [] - - private waitOpenConnection (ctx: MeasureContext): Promise | undefined { - if (this.isConnected()) { - return undefined - } - - return ctx.with( - 'wait-connection', - {}, - (ctx) => - new Promise((resolve, reject) => { - this.onConnectHandlers.push({ - resolve, - reject - }) - // Websocket is null for first time - this.scheduleOpen(ctx, false) - }) - ) - } - - scheduleOpen (ctx: MeasureContext, force: boolean): void { - if (force) { - ctx.withSync('close-ws', {}, () => { - if (this.websocket !== null) { - this.websocket.close() - this.websocket = null - } - }) - clearTimeout(this.openAction) - this.openAction = undefined - } - clearInterval(this.interval) - if (!this.closed && this.openAction === undefined) { - if (this.websocket === null) { - const socketId = ++this.sockets - // Re create socket in case of error, if not closed - if (this.delay === 0) { - this.openConnection(ctx, socketId) - } else { - this.openAction = setTimeout(() => { - this.openAction = undefined - this.openConnection(ctx, socketId) - }, this.delay * 1000) - } - } - } - } - - currentRateLimit: RateLimitInfo | undefined - slowDownTimer = 0 - - handleMsg (socketId: number, resp: Response): void { - if (this.closed) { - return - } - - if (resp.rateLimit !== undefined && resp.rateLimit.remaining < 50) { - this.currentRateLimit = resp.rateLimit - if (this.currentRateLimit.remaining < this.currentRateLimit.limit / 3) { - if (this.slowDownTimer < 50) { - this.slowDownTimer += 50 - } - this.slowDownTimer++ - } else if (this.slowDownTimer > 0) { - this.slowDownTimer-- - } - } - - if (resp.error !== undefined) { - if (resp.error?.code === UNAUTHORIZED.code || resp.terminate === true) { - if ( - resp.error.code !== platform.status.WorkspaceArchived && - resp.error.code !== platform.status.WorkspaceNotFound - ) { - Analytics.handleError(new PlatformError(resp.error)) - } - this.closed = true - this.websocket?.close() - if (resp.error?.code === UNAUTHORIZED.code) { - this.opt?.onUnauthorized?.() - } - if (resp.error?.code === platform.status.WorkspaceArchived) { - this.opt?.onArchived?.() - } - if (resp.error?.code === platform.status.WorkspaceMigration) { - this.opt?.onMigration?.() - } - } - - if (resp.id !== undefined) { - const promise = this.requests.get(resp.id) - - // Support rate limits - if (resp.rateLimit !== undefined) { - const { remaining, retryAfter } = resp.rateLimit - if (remaining === 0) { - void new Promise((resolve) => setTimeout(resolve, retryAfter ?? 1)).then(() => { - // Retry after a while, so rate limits allow to call more. - promise?.sendData() - }) - return - } - } - - if (promise !== undefined) { - promise.reject(new PlatformError(resp.error)) - } - } - - return - } - - if (resp.id === -1) { - this.delay = 0 - if (resp.result?.state === 'upgrading') { - void this.onConnect?.(ClientConnectEvent.Maintenance, undefined, resp.result.stats) - this.upgrading = true - this.delay = 3 - return - } - if (resp.result === 'hello') { - const helloResp = resp as HelloResponse - this.binaryMode = helloResp.binary - this.compressionMode = helloResp.useCompression ?? false - - // We need to clear dial timer, since we recieve hello response. - clearTimeout(this.dialTimer) - this.dialTimer = undefined - this.lastHash = (resp as HelloResponse).lastHash - - const serverVersion = helloResp.serverVersion - if (typeof window !== 'undefined') { - console.log('Connected to server:', serverVersion) - } - - if (this.opt?.onHello !== undefined && !this.opt.onHello(serverVersion)) { - this.closed = true - this.websocket?.close() - return - } - this.account = helloResp.account - this.helloReceived = true - if (this.upgrading) { - // We need to call upgrade since connection is upgraded - this.opt?.onUpgrade?.() - } - - this.upgrading = false - // Notify all waiting connection listeners - const handlers = this.onConnectHandlers.splice(0, this.onConnectHandlers.length) - for (const h of handlers) { - h.resolve() - } - - for (const [, v] of this.requests.entries()) { - v.reconnect?.() - } - - void this.onConnect?.( - helloResp.reconnect === true ? ClientConnectEvent.Reconnected : ClientConnectEvent.Connected, - helloResp.lastTx, - this.sessionId - )?.catch((err) => { - this.ctx.error('failed to call onConnect', { err }) - }) - this.schedulePing(socketId) - return - } else { - Analytics.handleError(new Error(`unexpected response: ${JSON.stringify(resp)}`)) - } - return - } - if (resp.result === pingConst) { - void this.sendRequest({ method: pingConst, params: [] }).catch((err) => { - this.ctx.error('failed to send ping', { err }) - }) - return - } - if (resp.id !== undefined) { - const promise = this.requests.get(resp.id) - - if (promise === undefined) { - console.error( - new Error(`unknown response id: ${resp.id as string} ${this.workspace} ${this.user}`), - JSON.stringify(this.requests) - ) - return - } - - if (resp.chunk !== undefined) { - promise.chunks = [ - ...(promise.chunks ?? []), - { - index: resp.chunk.index, - data: resp.result as FindResult - } - ] - // console.log(socketId, 'chunk', promise.method, promise.params, promise.chunks.length, (resp.result as []).length) - if (resp.chunk.final) { - promise.chunks.sort((a, b) => a.index - b.index) - let result: any[] = [] - let total = -1 - let lookupMap: Record | undefined - - for (const c of promise.chunks) { - if (c.data.total !== 0) { - total = c.data.total - } - if (c.data.lookupMap !== undefined) { - lookupMap = c.data.lookupMap - } - result = result.concat(c.data) - } - resp.result = toFindResult(result, total, lookupMap) - resp.chunk = undefined - } else { - // Not all chunks are available yet. - return - } - } - - const request = this.requests.get(resp.id) - promise.handleTime?.( - Date.now() - promise.startTime, - resp.result, - resp.time ?? 0, - resp.queue ?? 0, - Date.now() - (resp.bfst ?? 0) - ) - this.requests.delete(resp.id) - if (resp.error !== undefined) { - console.log( - 'ERROR', - 'request:', - request?.method, - 'response-id:', - resp.id, - 'error: ', - resp.error, - 'result: ', - resp.result, - this.workspace, - this.user - ) - promise.reject(new PlatformError(resp.error)) - } else { - if (request?.handleResult !== undefined) { - void request - .handleResult(resp.result) - .then(() => { - promise.resolve(resp.result) - }) - .catch((err) => { - this.ctx.error('failed to handleResult', { err }) - }) - } else { - promise.resolve(resp.result) - } - } - } else { - const txArr = Array.isArray(resp.result) ? (resp.result as Tx[]) : [resp.result as Tx] - - for (const tx of txArr) { - if (tx?._class === core.class.TxModelUpgrade) { - console.log('Processing upgrade', this.workspace, this.user) - this.opt?.onUpgrade?.() - return - } - } - this.handlers.forEach((handler) => { - handler(...txArr) - }) - } - } - - checkArrayBufferPing (data: ArrayBuffer): boolean { - if (data.byteLength === pingConst.length || data.byteLength === pongConst.length) { - const text = new TextDecoder().decode(data) - if (text === pingConst) { - void this.sendRequest({ method: pingConst, params: [] }).catch((err) => { - this.ctx.error('failed to send ping', { err }) - }) - return true - } - if (text === pongConst) { - this.pingResponse = Date.now() - return true - } - } - return false - } - - private openConnection (ctx: MeasureContext, socketId: number): void { - this.binaryMode = false - this.helloReceived = false - // Use defined factory or browser default one. - const clientSocketFactory = - this.opt?.socketFactory ?? - getMetadata(client.metadata.ClientSocketFactory) ?? - ((url: string) => { - const s = new WebSocket(url) - // s.binaryType = 'arraybuffer' - return s as ClientSocket - }) - - if (socketId !== this.sockets) { - return - } - const wsocket = ctx.withSync('create-socket', {}, () => - clientSocketFactory(this.url + `?sessionId=${this.sessionId}`) - ) - - if (socketId !== this.sockets) { - wsocket.close() - return - } - this.websocket = wsocket - if (this.dialTimer === undefined) { - this.dialTimer = setTimeout(() => { - this.dialTimer = undefined - if (!this.closed) { - void this.opt?.onDialTimeout?.()?.catch((err) => { - this.ctx.error('failed to handle dial timeout', { err }) - }) - this.scheduleOpen(this.ctx, true) - } - }, dialTimeout) - } - - wsocket.onmessage = (event: MessageEvent) => { - if (this.closed) { - return - } - if (this.websocket !== wsocket) { - return - } - if (event.data === pongConst) { - this.pingResponse = Date.now() - return - } - if (event.data === pingConst) { - void this.sendRequest({ method: pingConst, params: [] }).catch((err) => { - this.ctx.error('failed to send ping', { err }) - }) - return - } - if (event.data instanceof ArrayBuffer && this.checkArrayBufferPing(event.data)) { - return - } - if (event.data instanceof Blob) { - void event.data - .arrayBuffer() - .then((data) => { - if (this.checkArrayBufferPing(data)) { - // Support ping/pong - return - } - if (this.compressionMode && this.helloReceived) { - try { - data = uncompress(data) - } catch (err: any) { - // Ignore - console.error(err) - } - } - try { - const resp = this.rpcHandler.readResponse(data, this.binaryMode) - this.handleMsg(socketId, resp) - } catch (err: any) { - if (!this.helloReceived) { - // Just error and ignore for now. - console.error(err) - } else { - throw err - } - } - }) - .catch((err) => { - this.ctx.error('failed to decode array buffer', { err }) - }) - } else { - let data = event.data - if (this.compressionMode && this.helloReceived) { - try { - data = uncompress(data) - } catch (err: any) { - // Ignore - console.error(err) - } - } - try { - const resp = this.rpcHandler.readResponse(data, this.binaryMode) - this.handleMsg(socketId, resp) - } catch (err: any) { - if (!this.helloReceived) { - // Just error and ignore for now. - console.error(err) - } else { - throw err - } - } - } - } - wsocket.onclose = (ev) => { - if (this.websocket !== wsocket) { - wsocket.close() - return - } - this.scheduleOpen(this.ctx, true) - } - wsocket.onopen = () => { - if (this.websocket !== wsocket) { - return - } - const useBinary = this.opt?.useBinaryProtocol ?? getMetadata(client.metadata.UseBinaryProtocol) ?? true - this.compressionMode = - this.opt?.useProtocolCompression ?? getMetadata(client.metadata.UseProtocolCompression) ?? false - const helloRequest: HelloRequest = { - method: 'hello', - params: [], - id: -1, - binary: useBinary, - compression: this.compressionMode - } - ctx.withSync('send-hello', {}, () => this.websocket?.send(this.rpcHandler.serialize(helloRequest, false))) - } - - // FIX: remove undefined variable 'opened' - wsocket.onerror = () => { - if (this.websocket !== wsocket) { - return - } - if (this.delay < 3) { - this.delay += 1 - } - console.error('client websocket error:', socketId, this.url, this.workspace, this.user) - } - } - - private sendRequest (data: { - method: string - params: any[] - // If not defined, on reconnect with timeout, will retry automatically. - retry?: () => Promise - handleResult?: (result: any) => Promise - once?: boolean // Require handleResult to retrieve result - measure?: (time: number, result: any, serverTime: number, queue: number, toRecieve: number) => void - allowReconnect?: boolean - overrideId?: number - }): Promise { - return this.ctx.with( - 'send-request', - {}, - async (ctx) => { - if (this.closed) { - throw new PlatformError(new Status(Severity.ERROR, platform.status.ConnectionClosed, {})) - } - - if (this.slowDownTimer > 0) { - // We need to wait a bit to avoid ban. - await new Promise((resolve) => setTimeout(resolve, this.slowDownTimer)) - } - - if (data.once === true) { - // Check if has same request already then skip - const dparams = JSON.stringify(data.params) - for (const [, v] of this.requests) { - if (v.method === data.method && JSON.stringify(v.params) === dparams) { - // We have same unanswered, do not add one more. - return - } - } - } - - const id = data.overrideId ?? this.lastId++ - const promise = new RequestPromise(data.method, data.params, data.handleResult) - promise.handleTime = data.measure - - const w = this.waitOpenConnection(ctx) - if (w instanceof Promise) { - await w - } - if (data.method !== pingConst) { - this.requests.set(id, promise) - } - promise.sendData = (): void => { - if (this.websocket?.readyState === ClientSocketReadyState.OPEN) { - promise.startTime = Date.now() - - if (data.method !== pingConst) { - const dta = this.rpcHandler.serialize( - { - method: data.method, - params: data.params, - meta: ctx.extractMeta(), - id, - time: Date.now() - }, - this.binaryMode - ) - - this.websocket?.send(dta) - } else { - this.websocket?.send(pingConst) - } - } - } - if (data.allowReconnect ?? true) { - promise.reconnect = () => { - setTimeout(async () => { - // In case we don't have response yet. - if (this.requests.has(id) && ((await data.retry?.()) ?? true)) { - promise.sendData() - } - }, 50) - } - } - promise.sendData() - if (data.method !== pingConst) { - return await promise.promise - } - }, - { method: data.method }, - { - span: 'skip' - } - ) - } - - loadModel (last: Timestamp, hash?: string): Promise { - return this.sendRequest({ method: 'loadModel', params: [last, hash] }) - } - - getAccount (): Promise { - if (this.account !== undefined) { - return Promise.resolve(clone(this.account)) - } - return this.sendRequest({ method: 'getAccount', params: [] }) - } - - async findAll( - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): Promise> { - const result = await this.sendRequest({ - method: 'findAll', - params: [_class, query, options], - measure: (time, result, serverTime, queue, toReceive) => { - if (typeof window !== 'undefined' && (time > 1000 || serverTime > 500)) { - console.error( - 'measure slow findAll', - time, - serverTime, - toReceive, - queue, - _class, - query, - options, - result, - JSON.stringify(result).length - ) - } - } - }) - if (result.lookupMap !== undefined) { - // We need to extract lookup map to document lookups - for (const d of result) { - if (d.$lookup !== undefined) { - for (const [k, v] of Object.entries(d.$lookup)) { - if (!Array.isArray(v)) { - d.$lookup[k] = result.lookupMap[v as any] - } else { - d.$lookup[k] = v.map((it) => result.lookupMap?.[it]) - } - } - } - } - delete result.lookupMap - } - - // We need to revert deleted query simple values. - // We need to get rid of simple query parameters matched in documents - for (const doc of result) { - for (const [k, v] of Object.entries(query)) { - if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') { - if (doc[k] == null) { - doc[k] = v - } - } - } - if (doc._class == null) { - doc._class = _class - } - } - - return result - } - - tx (tx: Tx): Promise { - return this.sendRequest({ - method: 'tx', - params: [tx], - retry: async () => { - if (tx._class === core.class.TxApplyIf) { - return (await this.findAll(core.class.Tx, { _id: (tx as TxApplyIf).txes[0]._id }, { limit: 1 })).length === 0 - } - return (await this.findAll(core.class.Tx, { _id: tx._id }, { limit: 1 })).length === 0 - } - }) - } - - loadChunk (domain: Domain, idx?: number): Promise { - return this.sendRequest({ method: 'loadChunk', params: [domain, idx] }) - } - - async getDomainHash (domain: Domain): Promise { - return await this.sendRequest({ method: 'getDomainHash', params: [domain] }) - } - - closeChunk (idx: number): Promise { - return this.sendRequest({ method: 'closeChunk', params: [idx] }) - } - - loadDocs (domain: Domain, docs: Ref[]): Promise { - return this.sendRequest({ method: 'loadDocs', params: [domain, docs] }) - } - - upload (domain: Domain, docs: Doc[]): Promise { - return this.sendRequest({ method: 'upload', params: [domain, docs] }) - } - - clean (domain: Domain, docs: Ref[]): Promise { - return this.sendRequest({ method: 'clean', params: [domain, docs] }) - } - - searchFulltext (query: SearchQuery, options: SearchOptions): Promise { - return this.sendRequest({ method: 'searchFulltext', params: [query, options] }) - } - - domainRequest (domain: OperationDomain, params: DomainParams, options?: DomainRequestOptions): Promise { - return this.sendRequest({ - method: 'domainRequest', - params: [domain, params], - retry: async () => { - return options?.retry ?? false - } - }) - } - - sendForceClose (): Promise { - return this.sendRequest({ method: 'forceClose', params: [], allowReconnect: false, overrideId: -2, once: true }) - } -} - -/** - * @public - */ -export function connect ( - url: string, - handler: TxHandler, - workspace: WorkspaceUuid, - user: PersonUuid, - opt?: ClientFactoryOptions -): ClientConnection { - return new Connection( - opt?.ctx?.newChild?.('connection', {}) ?? new MeasureMetricsContext('connection', {}), - url, - handler, - workspace, - user, - opt - ) -} diff --git a/foundations/core/packages/client-resources/src/index.ts b/foundations/core/packages/client-resources/src/index.ts deleted file mode 100644 index 9d126e8987..0000000000 --- a/foundations/core/packages/client-resources/src/index.ts +++ /dev/null @@ -1,305 +0,0 @@ -// -// Copyright © 2020 Anticrm Platform Contributors. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import clientPlugin from '@hcengineering/client' -import type { ClientFactoryOptions } from '@hcengineering/client/src' -import core, { - Client, - LoadModelResponse, - type PersonUuid, - Tx, - TxHandler, - TxPersistenceStore, - TxWorkspaceEvent, - WorkspaceEvent, - type WorkspaceUuid, - concatLink, - createClient, - fillConfiguration, - pluginFilterTx, - type Class, - type ClientConnection, - type Doc, - type ModelFilter, - type PluginConfiguration, - type Ref, - type TxCUD, - platformNow, - ClientConnectEvent -} from '@hcengineering/core' -import platform, { Severity, Status, getMetadata, getPlugins, setPlatformStatus } from '@hcengineering/platform' -import { connect } from './connection' - -export { connect } - -let dbRequest: IDBOpenDBRequest | undefined -let dbPromise: Promise = Promise.resolve(undefined) - -if (typeof localStorage !== 'undefined') { - const st = platformNow() - dbPromise = new Promise((resolve) => { - dbRequest = indexedDB.open('model.db.persistence', 2) - - dbRequest.onupgradeneeded = function () { - const db = (dbRequest as IDBOpenDBRequest).result - if (!db.objectStoreNames.contains('model')) { - db.createObjectStore('model', { keyPath: 'id' }) - } - } - dbRequest.onsuccess = function () { - const db = (dbRequest as IDBOpenDBRequest).result - console.log('init DB complete', platformNow() - st) - resolve(db) - } - }) - void dbPromise.then((res) => { - if (res !== undefined) { - res.onclose = () => { - dbRequest = undefined - dbPromise = Promise.resolve(undefined) - } - } - }) -} - -interface TokenPayload { - workspace?: WorkspaceUuid - account?: PersonUuid - extra?: any -} - -/** - * @public - */ -function decodeTokenPayload (token: string): TokenPayload { - try { - return JSON.parse(atob(token.split('.')[1])) - } catch (err: any) { - console.error(err) - return {} - } -} - -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export default async () => { - return { - function: { - GetClient: async (token: string, endpoint: string, opt?: ClientFactoryOptions): Promise => { - const filterModel = getMetadata(clientPlugin.metadata.FilterModel) ?? 'none' - const extraFilter = getMetadata(clientPlugin.metadata.ExtraFilter) ?? [] - - const handler = async (handler: TxHandler): Promise => { - const url = concatLink(endpoint, `/${token}`) - - const upgradeHandler: TxHandler = (...txes: Tx[]) => { - for (const tx of txes) { - if (tx?._class === core.class.TxModelUpgrade) { - opt?.onUpgrade?.() - return - } - if (tx?._class === core.class.TxWorkspaceEvent) { - const event = tx as TxWorkspaceEvent - if (event.event === WorkspaceEvent.MaintenanceNotification) { - void setPlatformStatus( - new Status(Severity.WARNING, platform.status.MaintenanceWarning, { - time: event.params.timeMinutes, - message: event.params.message ?? '' - }) - ) - } - } - } - handler(...txes) - } - const tokenPayload = decodeTokenPayload(token) - if (tokenPayload.workspace === undefined || tokenPayload.account === undefined) { - throw new Error('Workspace or account not found in token') - } - - const newOpt = { ...opt } - const connectTimeout = opt?.connectionTimeout ?? getMetadata(clientPlugin.metadata.ConnectionTimeout) - let connectPromise: Promise | undefined - if ((connectTimeout ?? 0) > 0) { - connectPromise = new Promise((resolve, reject) => { - const connectTO = setTimeout(() => { - if (!clientConnection.isConnected()) { - newOpt.onConnect = undefined - void clientConnection?.close() - void opt?.onDialTimeout?.() - reject(new Error(`Connection timeout, and no connection established to ${endpoint}`)) - } - }, connectTimeout) - newOpt.onConnect = async (event, lastTx, data) => { - try { - await opt?.onConnect?.(event, lastTx, data) - } catch (error: any) { - void clientConnection?.close() - void opt?.onDialTimeout?.() - reject(error) - return - } - - if (event !== ClientConnectEvent.Maintenance) { - // Any event is fine, it means server is alive. - clearTimeout(connectTO) - resolve() - } - } - }) - } - const clientConnection = connect(url, upgradeHandler, tokenPayload.workspace, tokenPayload.account, newOpt) - if (connectPromise !== undefined) { - await connectPromise - } - return await Promise.resolve(clientConnection) - } - - const modelFilter: ModelFilter = (txes) => { - if (filterModel === 'client') { - return returnClientTxes(txes) - } - if (filterModel === 'ui') { - return returnUITxes(txes, extraFilter) - } - return txes - } - - const client = createClient(handler, modelFilter, createModelPersistence(getWSFromToken(token)), opt?.ctx) - return await client - } - } - } -} -function returnUITxes (txes: Tx[], extraFilter: string[]): Tx[] { - const configs = new Map, PluginConfiguration>() - fillConfiguration(txes, configs) - - const allowedPlugins = [...getPlugins(), ...(getMetadata(clientPlugin.metadata.ExtraPlugins) ?? [])] - const excludedPlugins = Array.from(configs.values()).filter( - (it) => !it.enabled || !allowedPlugins.includes(it.pluginId) || extraFilter.includes(it.pluginId) - ) - return pluginFilterTx(excludedPlugins, configs, txes) -} - -function returnClientTxes (txes: Tx[]): Tx[] { - const configs = new Map, PluginConfiguration>() - fillConfiguration(txes, configs) - const excludedPlugins = Array.from(configs.values()).filter((it) => !it.enabled || it.pluginId.startsWith('server-')) - - const toExclude = new Set([ - 'workbench:class:Application' as Ref>, - 'presentation:class:ComponentPointExtension' as Ref>, - 'presentation:class:ObjectSearchCategory' as Ref>, - 'notification:class:NotificationGroup' as Ref>, - 'notification:class:NotificationType' as Ref>, - 'view:class:Action' as Ref>, - 'view:class:Viewlet' as Ref>, - 'text-editor:class:TextEditorAction' as Ref>, - 'templates:class:TemplateField' as Ref>, - 'activity:class:DocUpdateMessageViewlet' as Ref>, - 'core:class:DomainIndexConfiguration' as Ref>, - 'view:class:ViewletDescriptor' as Ref>, - 'presentation:class:ComponentPointExtension' as Ref>, - 'activity:class:ActivityMessagesFilter' as Ref>, - 'view:class:ActionCategory' as Ref>, - 'activity:class:ActivityExtension' as Ref>, - 'chunter:class:ChatMessageViewlet' as Ref>, - 'activity:class:ActivityMessageControl' as Ref>, - 'notification:class:ActivityNotificationViewlet' as Ref>, - 'setting:class:SettingsCategory' as Ref>, - 'setting:class:WorkspaceSettingCategory' as Ref>, - 'notification:class:NotificationProvider' as Ref> - ]) - - const result = pluginFilterTx(excludedPlugins, configs, txes).filter((tx) => { - // Exclude all matched UI plugins - if ( - tx?._class === core.class.TxCreateDoc || - tx?._class === core.class.TxUpdateDoc || - tx?._class === core.class.TxRemoveDoc - ) { - const cud = tx as TxCUD - if (toExclude.has(cud.objectClass)) { - return false - } - } - return true - }) - return result -} - -function createModelPersistence (workspace: string): TxPersistenceStore | undefined { - const overrideStore = getMetadata(clientPlugin.metadata.OverridePersistenceStore) - if (overrideStore !== undefined) { - return overrideStore - } - - return { - load: async () => { - const db = await dbPromise - if (db !== undefined) { - try { - const transaction = db.transaction('model', 'readwrite') // (1) - const models = transaction.objectStore('model') // (2) - const model = await new Promise<{ id: string, model: LoadModelResponse } | undefined>((resolve) => { - const storedValue: IDBRequest<{ id: string, model: LoadModelResponse }> = models.get(workspace) - storedValue.onsuccess = function () { - resolve(storedValue.result) - } - storedValue.onerror = function () { - resolve(undefined) - } - }) - - if (model == null) { - return { - full: false, - transactions: [], - hash: '' - } - } - return model.model - } catch (err: any) { - // Assume no model is stored. - } - } - return { - full: true, - transactions: [], - hash: '' - } - }, - store: async (model) => { - const db = await dbPromise - if (db !== undefined) { - const transaction = db.transaction('model', 'readwrite') // (1) - const models = transaction.objectStore('model') // (2) - models.put({ id: workspace, model }) - } - } - } -} - -function getWSFromToken (token: string): string { - const parts = token.split('.') - - const payload = parts[1] - - const decodedPayload = atob(payload) - - const parsedPayload = JSON.parse(decodedPayload) - - return parsedPayload.workspace -} diff --git a/foundations/core/packages/client-resources/tsconfig.json b/foundations/core/packages/client-resources/tsconfig.json deleted file mode 100644 index b5ae22f6e4..0000000000 --- a/foundations/core/packages/client-resources/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/foundations/core/packages/client/.eslintrc.js b/foundations/core/packages/client/.eslintrc.js deleted file mode 100644 index 72235dc283..0000000000 --- a/foundations/core/packages/client/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/foundations/core/packages/client/.npmignore b/foundations/core/packages/client/.npmignore deleted file mode 100644 index e3ec093c38..0000000000 --- a/foundations/core/packages/client/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!/lib/** -!CHANGELOG.md -/lib/**/__tests__/ diff --git a/foundations/core/packages/client/CHANGELOG.json b/foundations/core/packages/client/CHANGELOG.json deleted file mode 100644 index ff86834d5d..0000000000 --- a/foundations/core/packages/client/CHANGELOG.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "name": "@hcengineering/client", - "entries": [ - { - "version": "0.7.17", - "tag": "@hcengineering/client_v0.7.17", - "date": "Mon, 27 Oct 2025 13:27:12 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" - } - ] - } - }, - { - "version": "0.7.6", - "tag": "@hcengineering/client_v0.7.6", - "date": "Tue, 14 Oct 2025 04:58:17 GMT", - "comments": { - "patch": [ - { - "comment": "update deps" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.4` to `0.7.5`" - }, - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`" - } - ] - } - }, - { - "version": "0.7.5", - "tag": "@hcengineering/client_v0.7.5", - "date": "Sat, 11 Oct 2025 18:20:33 GMT", - "comments": { - "patch": [ - { - "comment": "Update to latest platform rig" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.3` to `0.7.4`" - }, - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`" - } - ] - } - }, - { - "version": "0.7.4", - "tag": "@hcengineering/client_v0.7.4", - "date": "Thu, 09 Oct 2025 16:57:55 GMT", - "comments": { - "patch": [ - { - "comment": "fix formatting" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.4` to `0.7.5`" - } - ] - } - }, - { - "version": "0.7.3", - "tag": "@hcengineering/client_v0.7.3", - "date": "Wed, 08 Oct 2025 03:40:53 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`" - } - ] - } - } - ] -} diff --git a/foundations/core/packages/client/CHANGELOG.md b/foundations/core/packages/client/CHANGELOG.md deleted file mode 100644 index 00f4521d7d..0000000000 --- a/foundations/core/packages/client/CHANGELOG.md +++ /dev/null @@ -1,35 +0,0 @@ -# Change Log - @hcengineering/client - -This log was last generated on Mon, 27 Oct 2025 13:27:12 GMT and should not be manually modified. - -## 0.7.17 -Mon, 27 Oct 2025 13:27:12 GMT - -_Version update only_ - -## 0.7.6 -Tue, 14 Oct 2025 04:58:17 GMT - -### Patches - -- update deps - -## 0.7.5 -Sat, 11 Oct 2025 18:20:33 GMT - -### Patches - -- Update to latest platform rig - -## 0.7.4 -Thu, 09 Oct 2025 16:57:55 GMT - -### Patches - -- fix formatting - -## 0.7.3 -Wed, 08 Oct 2025 03:40:53 GMT - -_Initial release_ - diff --git a/foundations/core/packages/client/config/rig.json b/foundations/core/packages/client/config/rig.json deleted file mode 100644 index 0110930f55..0000000000 --- a/foundations/core/packages/client/config/rig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig" -} diff --git a/foundations/core/packages/client/jest.config.js b/foundations/core/packages/client/jest.config.js deleted file mode 100644 index ba719f1162..0000000000 --- a/foundations/core/packages/client/jest.config.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ['./src'], - coverageReporters: ['text-summary', 'html', 'lcov'], - testTimeout: 10000 // 10 seconds timeout for all tests -} diff --git a/foundations/core/packages/client/package.json b/foundations/core/packages/client/package.json deleted file mode 100644 index b1e3b31a95..0000000000 --- a/foundations/core/packages/client/package.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "name": "@hcengineering/client", - "version": "0.7.17", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "files": [ - "lib/**/*", - "types/**/*", - "src/**/*", - "!src/**/__test__/**", - "tsconfig.json" - ], - "author": "Anticrm Platform Contributors", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "build:watch": "compile", - "format": "format src", - "test": "jest --passWithNoTests --silent --coverage", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --coverage", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "workspace:^0.7.19", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "@typescript-eslint/parser": "^6.21.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.6.2", - "typescript": "^5.9.3", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5", - "@types/node": "^22.18.1", - "eslint-plugin-svelte": "^2.35.1" - }, - "dependencies": { - "@hcengineering/platform": "workspace:^0.7.18", - "@hcengineering/core": "workspace:^0.7.22" - }, - "repository": "https://github.com/hcengineering/huly.core", - "publishConfig": { - "access": "public" - }, - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.js" - } - } -} diff --git a/foundations/core/packages/client/src/__tests__/client.test.ts b/foundations/core/packages/client/src/__tests__/client.test.ts deleted file mode 100644 index 8e71ac8fb6..0000000000 --- a/foundations/core/packages/client/src/__tests__/client.test.ts +++ /dev/null @@ -1,478 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import core, { - Class, - ClientConnectEvent, - ClientConnection, - createClient, - Doc, - DocChunk, - DocumentQuery, - Domain, - DOMAIN_MODEL, - DOMAIN_TX, - FindOptions, - FindResult, - generateId, - Hierarchy, - LoadModelResponse, - ModelDb, - Ref, - SearchOptions, - SearchQuery, - SearchResult, - Timestamp, - Tx, - TxCreateDoc, - TxDb, - TxHandler, - TxResult, - type DomainParams, - type DomainRequestOptions, - type DomainResult, - type OperationDomain, - WorkspaceEvent, - TxWorkspaceEvent, - Client, - ClassifierKind, - TxFactory, - type Data, - type Obj -} from '@hcengineering/core' -import type { IntlString } from '@hcengineering/platform' - -const txFactory = new TxFactory(core.account.System) - -function createClass (_class: Ref>, attributes: Data>): TxCreateDoc { - return txFactory.createTxCreateDoc(core.class.Class, core.space.Model, attributes, _class) -} - -// Minimal model for testing - similar to query tests -function generateMinimalModel (): Tx[] { - const txes: Tx[] = [] - - // Fill Tx'es with basic model classes - txes.push(createClass(core.class.Obj, { label: 'Obj' as IntlString, kind: ClassifierKind.CLASS })) - txes.push( - createClass(core.class.Doc, { label: 'Doc' as IntlString, extends: core.class.Obj, kind: ClassifierKind.CLASS }) - ) - txes.push( - createClass(core.class.Class, { - label: 'Class' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.CLASS, - domain: DOMAIN_MODEL - }) - ) - txes.push( - createClass(core.class.Space, { - label: 'Space' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.CLASS, - domain: DOMAIN_MODEL - }) - ) - txes.push( - createClass(core.class.Tx, { - label: 'Tx' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.CLASS, - domain: DOMAIN_TX - }) - ) - txes.push( - createClass(core.class.TxCUD, { - label: 'TxCUD' as IntlString, - extends: core.class.Tx, - kind: ClassifierKind.CLASS, - domain: DOMAIN_TX - }) - ) - txes.push( - createClass(core.class.TxCreateDoc, { - label: 'TxCreateDoc' as IntlString, - extends: core.class.TxCUD, - kind: ClassifierKind.CLASS - }) - ) - txes.push( - createClass(core.class.TxUpdateDoc, { - label: 'TxUpdateDoc' as IntlString, - extends: core.class.TxCUD, - kind: ClassifierKind.CLASS - }) - ) - txes.push( - createClass(core.class.TxRemoveDoc, { - label: 'TxRemoveDoc' as IntlString, - extends: core.class.TxCUD, - kind: ClassifierKind.CLASS - }) - ) - - return txes -} - -class TestConnection implements ClientConnection { - private readonly hierarchy: Hierarchy - private readonly model: ModelDb - private readonly transactions: TxDb - private readonly handlers: TxHandler[] = [] - private _connected: boolean = true - onConnect?: (event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise - getLastHash?: () => Promise - - constructor (txes: Tx[]) { - this.hierarchy = new Hierarchy() - for (const tx of txes) { - this.hierarchy.tx(tx) - } - - this.transactions = new TxDb(this.hierarchy) - this.model = new ModelDb(this.hierarchy) - for (const tx of txes) { - void this.transactions.tx(tx) - void this.model.tx(tx) - } - } - - isConnected (): boolean { - return this._connected - } - - setConnected (value: boolean): void { - this._connected = value - } - - pushHandler (handler: TxHandler): void { - this.handlers.push(handler) - } - - async findAll( - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): Promise> { - const domain = this.hierarchy.getClass(_class).domain - if (domain === DOMAIN_TX) { - return await this.transactions.findAll(_class, query, options) - } - return await this.model.findAll(_class, query, options) - } - - async searchFulltext (query: SearchQuery, options: SearchOptions): Promise { - return { docs: [] } - } - - async tx (tx: Tx): Promise { - if (tx.objectSpace === core.space.Model) { - this.hierarchy.tx(tx) - await this.model.tx(tx) - } - await this.transactions.tx(tx) - - // Notify handlers - this.handlers.forEach((h) => { - h(tx) - }) - - return {} - } - - async loadModel (last: Timestamp, hash?: string): Promise { - const txes = await this.transactions.findAll(core.class.Tx, { - objectSpace: core.space.Model, - modifiedOn: { $gt: last } - }) - - if (hash !== undefined) { - return { - transactions: txes, - hash: 'test-hash', - full: false - } - } - - return txes - } - - async close (): Promise { - this._connected = false - } - - async loadChunk (domain: Domain, idx?: number): Promise { - return { - idx: idx ?? 0, - docs: [], - finished: true - } - } - - async getDomainHash (domain: Domain): Promise { - return 'test-hash' - } - - async closeChunk (idx: number): Promise {} - - async loadDocs (domain: Domain, docs: Ref[]): Promise { - return [] - } - - async upload (domain: Domain, docs: Doc[]): Promise {} - - async clean (domain: Domain, docs: Ref[]): Promise {} - - async sendForceClose (): Promise {} - - async domainRequest ( - ctx: OperationDomain, - params: DomainParams, - options?: DomainRequestOptions - ): Promise { - return { domain: ctx, value: null } - } - - // Simulate receiving transactions from server - simulateTransaction (tx: Tx): void { - this.handlers.forEach((h) => { - h(tx) - }) - } -} - -describe('Client Core Implementation', () => { - let testConnection: TestConnection - let client: Client - - beforeEach(async () => { - const txes = generateMinimalModel() - testConnection = new TestConnection(txes) - - client = await createClient(async (handler: TxHandler) => { - testConnection.pushHandler(handler) - return testConnection - }) - }) - - it('should create a client instance', () => { - expect(client).toBeDefined() - expect(client.getHierarchy()).toBeDefined() - expect(client.getModel()).toBeDefined() - }) - - it('should handle findAll operations', async () => { - const spaces = await client.findAll(core.class.Space, {}) - expect(spaces).toBeDefined() - expect(Array.isArray(spaces)).toBe(true) - }) - - it('should handle findOne operations', async () => { - const space = await client.findOne(core.class.Space, { name: 'TestSpace' }) - if (space !== undefined) { - expect(space.name).toBe('TestSpace') - } - }) - - it('should handle tx operations', async () => { - const tx: TxCreateDoc = { - _id: generateId(), - _class: core.class.TxCreateDoc, - space: core.space.Tx, - objectId: generateId(), - objectClass: core.class.Space, - objectSpace: core.space.Model, - modifiedBy: core.account.System, - modifiedOn: Date.now(), - createdOn: Date.now(), - attributes: { - name: 'NewSpace', - description: '', - private: false, - archived: false, - members: [] - } - } - - const result = await client.tx(tx) - expect(result).toBeDefined() - }) - - it('should handle updateFromRemote for model transactions', async () => { - const notifySpy = jest.fn() - client.notify = notifySpy - - const tx: TxCreateDoc = { - _id: generateId(), - _class: core.class.TxCreateDoc, - space: core.space.Tx, - objectId: generateId(), - objectClass: core.class.Space, - objectSpace: core.space.Model, - modifiedBy: core.account.System, - modifiedOn: Date.now(), - createdOn: Date.now(), - attributes: { - name: 'RemoteSpace', - description: '', - private: false, - archived: false, - members: [] - } - } - - testConnection.simulateTransaction(tx) - - // Wait for async operations - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(notifySpy).toHaveBeenCalled() - }) - - it('should close connection properly', async () => { - await client.close() - expect(testConnection.isConnected()).toBe(false) - }) - - it('should handle domainRequest', async () => { - const result = await client.domainRequest('test-domain' as OperationDomain, {}) - expect(result).toBeDefined() - }) - - it('should update hierarchy and model on model transactions', async () => { - const hierarchy = client.getHierarchy() - const model = client.getModel() - - expect(hierarchy).toBeDefined() - expect(model).toBeDefined() - - const tx: TxCreateDoc = { - _id: generateId(), - _class: core.class.TxCreateDoc, - space: core.space.Tx, - objectId: generateId(), - objectClass: core.class.Space, - objectSpace: core.space.Model, - modifiedBy: core.account.System, - modifiedOn: Date.now(), - createdOn: Date.now(), - attributes: { - name: 'ModelSpace', - description: '', - private: false, - archived: false, - members: [] - } - } - - await client.tx(tx) - - // Verify the transaction was applied - const spaces = await client.findAll(core.class.Space, { name: 'ModelSpace' }) - expect(spaces.length).toBeGreaterThanOrEqual(0) - }) - - it('should handle reconnection events', async () => { - let eventReceived: ClientConnectEvent | undefined - - const onConnectHandler = async ( - event: ClientConnectEvent, - lastTx: string | undefined, - data: any - ): Promise => { - eventReceived = event - } - - testConnection.onConnect = onConnectHandler - - if (testConnection.onConnect !== undefined) { - await testConnection.onConnect(ClientConnectEvent.Connected, undefined, {}) - } - - expect(eventReceived).toBe(ClientConnectEvent.Connected) - }) - - it('should handle workspace events', async () => { - const notifySpy = jest.fn() - client.notify = notifySpy - - const workspaceTx: TxWorkspaceEvent = { - _id: generateId(), - _class: core.class.TxWorkspaceEvent, - space: core.space.Tx, - objectSpace: core.space.Workspace, - modifiedBy: core.account.System, - modifiedOn: Date.now(), - createdOn: Date.now(), - event: WorkspaceEvent.LastTx, - params: { lastTx: 'test-last-tx' } - } - - testConnection.simulateTransaction(workspaceTx) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(notifySpy).toHaveBeenCalled() - }) - - it('should handle searchFulltext', async () => { - const query: SearchQuery = { query: 'test' } - const options: SearchOptions = {} - const result = await client.searchFulltext(query, options) - expect(result).toBeDefined() - expect(result.docs).toBeDefined() - }) - - it('should handle mixin updates in findAll', async () => { - const spaces = await client.findAll(core.class.Space, {}, { limit: 10 }) - expect(spaces).toBeDefined() - expect(spaces.total).toBeDefined() - }) - - it('should skip already applied model transactions', async () => { - const tx: TxCreateDoc = { - _id: generateId(), - _class: core.class.TxCreateDoc, - space: core.space.Tx, - objectId: generateId(), - objectClass: core.class.Space, - objectSpace: core.space.Model, - modifiedBy: core.account.System, - modifiedOn: Date.now(), - createdOn: Date.now(), - attributes: { - name: 'DuplicateSpace', - description: '', - private: false, - archived: false, - members: [] - } - } - - // Apply the same transaction twice - await client.tx(tx) - - const notifySpy = jest.fn() - client.notify = notifySpy - - // Simulate receiving the same transaction from remote - testConnection.simulateTransaction(tx) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Should still notify but skip model update - expect(notifySpy).toHaveBeenCalled() - }) -}) diff --git a/foundations/core/packages/client/src/index.ts b/foundations/core/packages/client/src/index.ts deleted file mode 100644 index ae6f530020..0000000000 --- a/foundations/core/packages/client/src/index.ts +++ /dev/null @@ -1,101 +0,0 @@ -// -// Copyright © 2020 Anticrm Platform Contributors. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { Client, ClientConnectEvent, MeasureContext, TxPersistenceStore } from '@hcengineering/core' -import { type Plugin, type Resource, type Metadata, plugin } from '@hcengineering/platform' - -/** - * @public - */ -export const clientId = 'client' as Plugin - -/** - * @public - */ -export type ClientSocketFactory = (url: string) => ClientSocket - -/** - * @public - */ -export interface ClientSocket { - onmessage?: ((this: ClientSocket, ev: MessageEvent) => any) | null - onclose?: ((this: ClientSocket, ev: CloseEvent) => any) | null - onopen?: ((this: ClientSocket, ev: Event) => any) | null - onerror?: ((this: ClientSocket, ev: Event) => any) | null - - send: (data: string | ArrayBufferLike | Blob | ArrayBufferView) => void - - close: (code?: number) => void - - readyState: ClientSocketReadyState - - bufferedAmount?: number -} - -/** - * @public - */ -export enum ClientSocketReadyState { - CONNECTING = 0, - OPEN = 1, - CLOSING = 2, - CLOSED = 3 -} - -export interface ClientFactoryOptions { - socketFactory?: ClientSocketFactory - useBinaryProtocol?: boolean - useProtocolCompression?: boolean - connectionTimeout?: number - onHello?: (serverVersion?: string) => boolean - onUpgrade?: () => void - onUnauthorized?: () => void - onArchived?: () => void - onMigration?: () => void - onConnect?: (event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise - ctx?: MeasureContext - onDialTimeout?: () => void | Promise - - useGlobalRPCHandler?: boolean -} - -/** - * @public - */ -export type ClientFactory = (token: string, endpoint: string, opt?: ClientFactoryOptions) => Promise - -// client - will filter out all server model elements -// It will also filter out all UI Elements, like Actions, View declarations etc. -// ui - will filter out all server element's and all UI disabled elements. -export type FilterMode = 'none' | 'client' | 'ui' - -export const pingConst = 'ping' -export const pongConst = 'pong!' - -export default plugin(clientId, { - metadata: { - ClientSocketFactory: '' as Metadata, - FilterModel: '' as Metadata, - ExtraFilter: '' as Metadata, - ExtraPlugins: '' as Metadata, - UseBinaryProtocol: '' as Metadata, - UseProtocolCompression: '' as Metadata, - ConnectionTimeout: '' as Metadata, - OverridePersistenceStore: '' as Metadata - }, - function: { - GetClient: '' as Resource - } -}) diff --git a/foundations/core/packages/client/tsconfig.json b/foundations/core/packages/client/tsconfig.json deleted file mode 100644 index b5ae22f6e4..0000000000 --- a/foundations/core/packages/client/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/foundations/core/packages/collaborator-client/.eslintrc.js b/foundations/core/packages/collaborator-client/.eslintrc.js deleted file mode 100644 index 72235dc283..0000000000 --- a/foundations/core/packages/collaborator-client/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/foundations/core/packages/collaborator-client/.npmignore b/foundations/core/packages/collaborator-client/.npmignore deleted file mode 100644 index e3ec093c38..0000000000 --- a/foundations/core/packages/collaborator-client/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!/lib/** -!CHANGELOG.md -/lib/**/__tests__/ diff --git a/foundations/core/packages/collaborator-client/CHANGELOG.json b/foundations/core/packages/collaborator-client/CHANGELOG.json deleted file mode 100644 index 22653fa81c..0000000000 --- a/foundations/core/packages/collaborator-client/CHANGELOG.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "name": "@hcengineering/collaborator-client", - "entries": [ - { - "version": "0.7.17", - "tag": "@hcengineering/collaborator-client_v0.7.17", - "date": "Mon, 27 Oct 2025 13:27:12 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" - } - ] - } - }, - { - "version": "0.7.5", - "tag": "@hcengineering/collaborator-client_v0.7.5", - "date": "Tue, 14 Oct 2025 04:58:17 GMT", - "comments": { - "patch": [ - { - "comment": "update deps" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`" - } - ] - } - }, - { - "version": "0.7.4", - "tag": "@hcengineering/collaborator-client_v0.7.4", - "date": "Sat, 11 Oct 2025 18:20:33 GMT", - "comments": { - "patch": [ - { - "comment": "Update to latest platform rig" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`" - } - ] - } - }, - { - "version": "0.7.3", - "tag": "@hcengineering/collaborator-client_v0.7.3", - "date": "Wed, 08 Oct 2025 03:40:53 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`" - } - ] - } - } - ] -} diff --git a/foundations/core/packages/collaborator-client/CHANGELOG.md b/foundations/core/packages/collaborator-client/CHANGELOG.md deleted file mode 100644 index ab60b3d896..0000000000 --- a/foundations/core/packages/collaborator-client/CHANGELOG.md +++ /dev/null @@ -1,28 +0,0 @@ -# Change Log - @hcengineering/collaborator-client - -This log was last generated on Mon, 27 Oct 2025 13:27:12 GMT and should not be manually modified. - -## 0.7.17 -Mon, 27 Oct 2025 13:27:12 GMT - -_Version update only_ - -## 0.7.5 -Tue, 14 Oct 2025 04:58:17 GMT - -### Patches - -- update deps - -## 0.7.4 -Sat, 11 Oct 2025 18:20:33 GMT - -### Patches - -- Update to latest platform rig - -## 0.7.3 -Wed, 08 Oct 2025 03:40:53 GMT - -_Initial release_ - diff --git a/foundations/core/packages/collaborator-client/config/rig.json b/foundations/core/packages/collaborator-client/config/rig.json deleted file mode 100644 index 0110930f55..0000000000 --- a/foundations/core/packages/collaborator-client/config/rig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig" -} diff --git a/foundations/core/packages/collaborator-client/jest.config.js b/foundations/core/packages/collaborator-client/jest.config.js deleted file mode 100644 index 069ea8aa0d..0000000000 --- a/foundations/core/packages/collaborator-client/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ['./src'], - coverageReporters: ['text-summary', 'html', 'lcov'] -} diff --git a/foundations/core/packages/collaborator-client/package.json b/foundations/core/packages/collaborator-client/package.json deleted file mode 100644 index 483f97c769..0000000000 --- a/foundations/core/packages/collaborator-client/package.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "name": "@hcengineering/collaborator-client", - "version": "0.7.17", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "files": [ - "lib/**/*", - "types/**/*", - "src/**/*", - "!src/**/__test__/**", - "tsconfig.json" - ], - "author": "Hardcore Engineering Inc.", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "build:watch": "compile", - "format": "format src", - "test": "jest --passWithNoTests --silent --coverage", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --coverage", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "cross-env": "~7.0.3", - "@hcengineering/platform-rig": "workspace:^0.7.19", - "@types/node": "^22.18.1", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "esbuild": "^0.25.10", - "@typescript-eslint/parser": "^6.21.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.6.2", - "typescript": "^5.9.3", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5", - "eslint-plugin-svelte": "^2.35.1" - }, - "dependencies": { - "@hcengineering/core": "workspace:^0.7.22" - }, - "repository": "https://github.com/hcengineering/huly.core", - "publishConfig": { - "access": "public" - }, - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.js" - } - } -} diff --git a/foundations/core/packages/collaborator-client/src/__tests__/utils.test.ts b/foundations/core/packages/collaborator-client/src/__tests__/utils.test.ts deleted file mode 100644 index 06ca9d9dde..0000000000 --- a/foundations/core/packages/collaborator-client/src/__tests__/utils.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import core, { CollaborativeDoc, Doc, Ref } from '@hcengineering/core' -import { encodeDocumentId, decodeDocumentId } from '../utils' - -describe('utils', () => { - it('encodeDocumentId', () => { - const doc: CollaborativeDoc = { - objectClass: core.class.Doc, - objectId: 'doc1' as Ref, - objectAttr: 'description' - } - expect(encodeDocumentId('ws1', doc)).toEqual('ws1|core:class:Doc|doc1|description') - }) - - describe('decodeDocumentId', () => { - expect(decodeDocumentId('ws1|core:class:Doc|doc1|description')).toEqual({ - workspaceId: 'ws1', - documentId: { - objectClass: core.class.Doc, - objectId: 'doc1' as Ref, - objectAttr: 'description' - } - }) - }) -}) diff --git a/foundations/core/packages/collaborator-client/src/client.ts b/foundations/core/packages/collaborator-client/src/client.ts deleted file mode 100644 index 8cabaf1285..0000000000 --- a/foundations/core/packages/collaborator-client/src/client.ts +++ /dev/null @@ -1,163 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { Blob, CollaborativeDoc, Markup, MarkupBlobRef, Ref, WorkspaceUuid, concatLink } from '@hcengineering/core' -import { encodeDocumentId } from './utils' - -/** @public */ -export interface GetContentRequest { - source?: Ref -} - -/** @public */ -export interface GetContentResponse { - content: Record -} - -/** @public */ -export interface CreateContentRequest { - content: Record -} - -/** @public */ -export interface CreateContentResponse { - content: Record -} - -/** @public */ -export interface UpdateContentRequest { - content: Record -} - -/** @public */ -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface UpdateContentResponse {} - -/** @public */ -export interface CollaboratorClient { - getMarkup: (document: CollaborativeDoc, source?: Ref | null) => Promise - createMarkup: (document: CollaborativeDoc, markup: Markup) => Promise - updateMarkup: (document: CollaborativeDoc, markup: Markup) => Promise - copyContent: (source: CollaborativeDoc, target: CollaborativeDoc) => Promise -} - -/** @public */ -export function getClient (workspaceId: WorkspaceUuid, token: string, collaboratorUrl: string): CollaboratorClient { - const url = collaboratorUrl.replaceAll('wss://', 'https://').replace('ws://', 'http://') - return new CollaboratorClientImpl(workspaceId, token, url) -} - -class CollaboratorClientImpl implements CollaboratorClient { - constructor ( - private readonly workspace: WorkspaceUuid, - private readonly token: string, - private readonly collaboratorUrl: string - ) {} - - private async rpc(document: CollaborativeDoc, method: string, payload: P): Promise { - const workspace = this.workspace - const documentId = encodeDocumentId(workspace, document) - - const url = concatLink(this.collaboratorUrl, `/rpc/${encodeURIComponent(documentId)}`) - - const res = await fetch(url, { - method: 'POST', - headers: { - Authorization: 'Bearer ' + this.token, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ method, payload }) - }) - - if (!res.ok) { - throw new Error('HTTP error ' + res.status) - } - - const result = await res.json() - - if (result.error != null) { - throw new Error(result.error) - } - - return result as R - } - - async getMarkup (document: CollaborativeDoc, source?: Ref | null): Promise { - const payload: GetContentRequest = { - source: source !== null ? source : undefined - } - - const res = await retry( - 3, - async () => { - return await this.rpc(document, 'getContent', payload) - }, - 50 - ) - - return res.content[document.objectAttr] ?? '' - } - - async createMarkup (document: CollaborativeDoc, markup: Markup): Promise { - const content = { - [document.objectAttr]: markup - } - - const res = await retry( - 3, - async () => { - return await this.rpc(document, 'createContent', { content }) - }, - 50 - ) - - return res.content[document.objectAttr] - } - - async updateMarkup (document: CollaborativeDoc, markup: Markup): Promise { - const content = { - [document.objectAttr]: markup - } - - await retry( - 3, - async () => { - await this.rpc(document, 'updateContent', { content }) - }, - 50 - ) - } - - async copyContent (source: CollaborativeDoc, target: CollaborativeDoc, content?: Ref): Promise { - const markup = await this.getMarkup(source, content) - await this.updateMarkup(target, markup) - } -} - -async function retry (retries: number, op: () => Promise, delay: number = 100): Promise { - let error: any - while (retries > 0) { - retries-- - try { - return await op() - } catch (err: any) { - error = err - if (retries !== 0) { - await new Promise((resolve) => setTimeout(resolve, delay)) - } - } - } - throw error -} diff --git a/foundations/core/packages/collaborator-client/src/index.ts b/foundations/core/packages/collaborator-client/src/index.ts deleted file mode 100644 index 70baf83124..0000000000 --- a/foundations/core/packages/collaborator-client/src/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export * from './client' -export * from './utils' diff --git a/foundations/core/packages/collaborator-client/src/utils.ts b/foundations/core/packages/collaborator-client/src/utils.ts deleted file mode 100644 index f6225ba0aa..0000000000 --- a/foundations/core/packages/collaborator-client/src/utils.ts +++ /dev/null @@ -1,38 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { Class, CollaborativeDoc, Doc, Ref } from '@hcengineering/core' - -/** @public */ -export function encodeDocumentId (workspaceId: string, documentId: CollaborativeDoc): string { - const { objectClass, objectId, objectAttr } = documentId - return [workspaceId, objectClass, objectId, objectAttr].join('|') -} - -/** @public */ -export function decodeDocumentId (documentId: string): { - workspaceId: string - documentId: CollaborativeDoc -} { - const [workspaceId, objectClass, objectId, objectAttr] = documentId.split('|') - return { - workspaceId, - documentId: { - objectClass: objectClass as Ref>, - objectId: objectId as Ref, - objectAttr - } - } -} diff --git a/foundations/core/packages/collaborator-client/tsconfig.json b/foundations/core/packages/collaborator-client/tsconfig.json deleted file mode 100644 index b5ae22f6e4..0000000000 --- a/foundations/core/packages/collaborator-client/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/foundations/core/packages/core/.eslintrc.js b/foundations/core/packages/core/.eslintrc.js deleted file mode 100644 index 72235dc283..0000000000 --- a/foundations/core/packages/core/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/foundations/core/packages/core/.npmignore b/foundations/core/packages/core/.npmignore deleted file mode 100644 index e3ec093c38..0000000000 --- a/foundations/core/packages/core/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!/lib/** -!CHANGELOG.md -/lib/**/__tests__/ diff --git a/foundations/core/packages/core/CHANGELOG.json b/foundations/core/packages/core/CHANGELOG.json deleted file mode 100644 index 18df5a14de..0000000000 --- a/foundations/core/packages/core/CHANGELOG.json +++ /dev/null @@ -1,283 +0,0 @@ -{ - "name": "@hcengineering/core", - "entries": [ - { - "version": "0.7.22", - "tag": "@hcengineering/core_v0.7.22", - "date": "Thu, 06 Nov 2025 09:09:58 GMT", - "comments": { - "patch": [ - { - "comment": "Add txMatch to permission" - } - ] - } - }, - { - "version": "0.7.21", - "tag": "@hcengineering/core_v0.7.21", - "date": "Thu, 30 Oct 2025 23:11:55 GMT", - "comments": { - "patch": [ - { - "comment": "extend TxAccessLevel" - } - ] - } - }, - { - "version": "0.7.20", - "tag": "@hcengineering/core_v0.7.20", - "date": "Thu, 30 Oct 2025 08:41:42 GMT", - "comments": { - "patch": [ - { - "comment": "add workspace usae info" - } - ] - } - }, - { - "version": "0.7.19", - "tag": "@hcengineering/core_v0.7.19", - "date": "Wed, 29 Oct 2025 07:46:42 GMT", - "comments": { - "patch": [ - { - "comment": "Suspend exceptions from connect in traces" - } - ] - } - }, - { - "version": "0.7.18", - "tag": "@hcengineering/core_v0.7.18", - "date": "Mon, 27 Oct 2025 13:27:12 GMT", - "comments": { - "patch": [ - { - "comment": "Rank for attributes" - } - ] - } - }, - { - "version": "0.7.10", - "tag": "@hcengineering/core_v0.7.10", - "date": "Tue, 21 Oct 2025 19:04:55 GMT", - "comments": { - "patch": [ - { - "comment": "Add TypeIdentifier for custom attribute incremental IDs" - } - ] - } - }, - { - "version": "0.7.8", - "tag": "@hcengineering/core_v0.7.8", - "date": "Wed, 15 Oct 2025 18:01:30 GMT", - "comments": { - "patch": [ - { - "comment": "adjusted interfaces" - } - ] - } - }, - { - "version": "0.7.7", - "tag": "@hcengineering/core_v0.7.7", - "date": "Tue, 14 Oct 2025 04:58:17 GMT", - "comments": { - "patch": [ - { - "comment": "update deps" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.4` to `0.7.5`" - }, - { - "comment": "Updating dependency \"@hcengineering/analytics\" from `^0.7.4` to `0.7.5`" - } - ] - } - }, - { - "version": "0.7.6", - "tag": "@hcengineering/core_v0.7.6", - "date": "Sat, 11 Oct 2025 18:20:33 GMT", - "comments": { - "patch": [ - { - "comment": "Update to latest platform rig" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.3` to `0.7.4`" - }, - { - "comment": "Updating dependency \"@hcengineering/analytics\" from `^0.7.3` to `0.7.4`" - } - ] - } - }, - { - "version": "0.7.5", - "tag": "@hcengineering/core_v0.7.5", - "date": "Thu, 09 Oct 2025 16:57:55 GMT", - "comments": { - "patch": [ - { - "comment": "Hierarchy improvements" - } - ] - } - }, - { - "version": "0.7.4", - "tag": "@hcengineering/core_v0.7.4", - "date": "Wed, 08 Oct 2025 03:40:53 GMT", - "comments": { - "patch": [ - { - "comment": "Fix issue with time rate limiter" - } - ] - } - }, - { - "version": "0.7.0", - "tag": "@hcengineering/core_v0.6.11", - "date": "Fri, 20 Aug 2021 16:21:03 GMT", - "comments": { - "patch": [ - { - "comment": "Transaction ordering" - } - ] - } - }, - { - "version": "0.7.0", - "tag": "@hcengineering/core_v0.6.10", - "date": "Wed, 11 Aug 2021 09:37:04 GMT", - "comments": { - "patch": [ - { - "comment": "Server support for workspaces" - } - ] - } - }, - { - "version": "0.7.0", - "tag": "@hcengineering/core_v0.6.8", - "date": "Sun, 08 Aug 2021 21:05:26 GMT", - "comments": { - "patch": [ - { - "comment": "Fix server connection" - } - ] - } - }, - { - "version": "0.7.0", - "tag": "@hcengineering/core_v0.6.7", - "date": "Wed, 04 Aug 2021 21:18:44 GMT", - "comments": { - "patch": [ - { - "comment": "fix" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/platform\" from `~0.6.2` to `~0.6.3`" - } - ] - } - }, - { - "version": "0.7.0", - "tag": "@hcengineering/core_v0.6.6", - "date": "Wed, 04 Aug 2021 21:00:14 GMT", - "comments": { - "patch": [ - { - "comment": "npmigonre" - } - ] - } - }, - { - "version": "0.7.0", - "tag": "@hcengineering/core_v0.6.5", - "date": "Wed, 04 Aug 2021 20:26:15 GMT", - "comments": { - "patch": [ - { - "comment": "Add missed docs" - } - ] - } - }, - { - "version": "0.7.0", - "tag": "@hcengineering/core_v0.6.4", - "date": "Wed, 04 Aug 2021 18:12:38 GMT", - "comments": { - "patch": [ - { - "comment": "Add `memdb` docs" - } - ] - } - }, - { - "version": "0.7.0", - "tag": "@hcengineering/core_v0.6.3", - "date": "Wed, 04 Aug 2021 18:05:12 GMT", - "comments": { - "patch": [ - { - "comment": "Add `hierarchy.ts` docs" - } - ] - } - }, - { - "version": "0.7.0", - "tag": "@hcengineering/core_v0.6.2", - "date": "Wed, 04 Aug 2021 17:53:24 GMT", - "comments": { - "patch": [ - { - "comment": "Add documentation" - } - ] - } - }, - { - "version": "0.7.0", - "tag": "@hcengineering/core_v0.6.1", - "date": "Wed, 04 Aug 2021 17:38:30 GMT", - "comments": { - "patch": [ - { - "comment": "Minor changes for publish" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/platform\" from `~0.6.0` to `~0.6.1`" - } - ] - } - } - ] -} diff --git a/foundations/core/packages/core/CHANGELOG.md b/foundations/core/packages/core/CHANGELOG.md deleted file mode 100644 index b1b6d51db5..0000000000 --- a/foundations/core/packages/core/CHANGELOG.md +++ /dev/null @@ -1,151 +0,0 @@ -# Change Log - @hcengineering/core - -This log was last generated on Thu, 06 Nov 2025 09:09:58 GMT and should not be manually modified. - -## 0.7.22 -Thu, 06 Nov 2025 09:09:58 GMT - -### Patches - -- Add txMatch to permission - -## 0.7.21 -Thu, 30 Oct 2025 23:11:55 GMT - -### Patches - -- extend TxAccessLevel - -## 0.7.20 -Thu, 30 Oct 2025 08:41:42 GMT - -### Patches - -- add workspace usae info - -## 0.7.19 -Wed, 29 Oct 2025 07:46:42 GMT - -### Patches - -- Suspend exceptions from connect in traces - -## 0.7.18 -Mon, 27 Oct 2025 13:27:12 GMT - -### Patches - -- Rank for attributes - -## 0.7.10 -Tue, 21 Oct 2025 19:04:55 GMT - -### Patches - -- Add TypeIdentifier for custom attribute incremental IDs - -## 0.7.8 -Wed, 15 Oct 2025 18:01:30 GMT - -### Patches - -- adjusted interfaces - -## 0.7.7 -Tue, 14 Oct 2025 04:58:17 GMT - -### Patches - -- update deps - -## 0.7.6 -Sat, 11 Oct 2025 18:20:33 GMT - -### Patches - -- Update to latest platform rig - -## 0.7.5 -Thu, 09 Oct 2025 16:57:55 GMT - -### Patches - -- Hierarchy improvements - -## 0.7.4 -Wed, 08 Oct 2025 03:40:53 GMT - -### Patches - -- Fix issue with time rate limiter - -## 0.7.0 -Fri, 20 Aug 2021 16:21:03 GMT - -### Patches - -- Transaction ordering - -## 0.7.0 -Wed, 11 Aug 2021 09:37:04 GMT - -### Patches - -- Server support for workspaces - -## 0.7.0 -Sun, 08 Aug 2021 21:05:26 GMT - -### Patches - -- Fix server connection - -## 0.7.0 -Wed, 04 Aug 2021 21:18:44 GMT - -### Patches - -- fix - -## 0.7.0 -Wed, 04 Aug 2021 21:00:14 GMT - -### Patches - -- npmigonre - -## 0.7.0 -Wed, 04 Aug 2021 20:26:15 GMT - -### Patches - -- Add missed docs - -## 0.7.0 -Wed, 04 Aug 2021 18:12:38 GMT - -### Patches - -- Add `memdb` docs - -## 0.7.0 -Wed, 04 Aug 2021 18:05:12 GMT - -### Patches - -- Add `hierarchy.ts` docs - -## 0.7.0 -Wed, 04 Aug 2021 17:53:24 GMT - -### Patches - -- Add documentation - -## 0.7.0 -Wed, 04 Aug 2021 17:38:30 GMT - -### Patches - -- Minor changes for publish - diff --git a/foundations/core/packages/core/config/rig.json b/foundations/core/packages/core/config/rig.json deleted file mode 100644 index 0110930f55..0000000000 --- a/foundations/core/packages/core/config/rig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig" -} diff --git a/foundations/core/packages/core/jest.config.js b/foundations/core/packages/core/jest.config.js deleted file mode 100644 index 069ea8aa0d..0000000000 --- a/foundations/core/packages/core/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ['./src'], - coverageReporters: ['text-summary', 'html', 'lcov'] -} diff --git a/foundations/core/packages/core/lang/cs.json b/foundations/core/packages/core/lang/cs.json deleted file mode 100644 index 8a312b656e..0000000000 --- a/foundations/core/packages/core/lang/cs.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "string": { - "Id": "Id", - "Space": "Prostor", - "Spaces": "Prostory", - "SpacesDescription": "Spravujte typ všech prostor", - "TypedSpace": "Typovaný prostor", - "SpaceType": "Typ prostoru", - "Modified": "Upraveno", - "ModifiedDate": "Datum úpravy", - "ModifiedBy": "Upravil", - "Class": "Třída", - "AttachedTo": "Připojeno k", - "AttachedToClass": "Připojeno k třídě", - "Name": "Název", - "Description": "Popis", - "ShortDescription": "Krátký popis", - "Descriptor": "Popisovač", - "TargetClass": "Cílová třída", - "Role": "Role", - "Roles": "Role", - "Private": "Soukromé", - "Archived": "Archivováno", - "ClassLabel": "Typ", - "ClassPropertyLabel": "Štítek", - "String": "Text", - "Markup": "Značení", - "Number": "Číslo", - "Boolean": "Zaškrtávací políčko", - "Timestamp": "Časové razítko", - "Date": "Datum", - "IntlString": "Mezinárodní řetězec", - "Ref": "Odkaz", - "Collection": "Kolekce", - "Array": "Vícenásobný výběr", - "Enum": "Výběr", - "Members": "Členové", - "Hyperlink": "URL", - "Collaborative": "Spolupráce", - "Object": "Objekt", - "System": "Systém", - "CreatedBy": "Vytvořil", - "CreatedDate": "Datum vytvoření", - "Status": "Stav", - "StatusCategory": "Kategorie stavu", - "Account": "Účet", - "Rank": "Hodnost", - "Owners": "Vlastníci", - "Permission": "Oprávnění", - "CreateObject": "Vytvořit objekt", - "UpdateObject": "Aktualizovat objekt", - "DeleteObject": "Smazat objekt", - "ForbidDeleteObject": "Zakázat mazání objektu", - "UpdateSpace": "Aktualizovat prostor", - "ArchiveSpace": "Archivovat prostor", - "CreateObjectDescription": "Umožňuje uživatelům vytvářet objekty v prostoru", - "UpdateObjectDescription": "Umožňuje uživatelům aktualizovat objekty v prostoru", - "DeleteObjectDescription": "Umožňuje uživatelům mazat objekty v prostoru", - "ForbidDeleteObjectDescription": "Zakazuje uživatelům mazání objektů v prostoru", - "UpdateSpaceDescription": "Umožňuje uživatelům aktualizovat prostor", - "ArchiveSpaceDescription": "Umožňuje uživatelům archivovat prostor", - "AutoJoin": "Automatické připojení", - "AutoJoinDescr": "Automaticky připojit nové zaměstnance k tomuto prostoru", - "BlobSize": "Velikost", - "BlobContentType": "Typ obsahu", - "Relation": "Vztah", - "Relations": "Vztahy", - "AddRelation": "Přidat vztah", - "PersonId": "Osoba", - "AccountId": "Účet" - } -} diff --git a/foundations/core/packages/core/lang/de.json b/foundations/core/packages/core/lang/de.json deleted file mode 100644 index bc8768fa84..0000000000 --- a/foundations/core/packages/core/lang/de.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "string": { - "Id": "Id", - "Space": "Arbeitsbereich", - "Spaces": "Arbeitsbereiche", - "SpacesDescription": "Alle Arbeitsbereichstypen verwalten", - "TypedSpace": "Typisierter Arbeitsbereich", - "SpaceType": "Arbeitsbereichstyp", - "Modified": "Geändert", - "ModifiedDate": "Änderungsdatum", - "ModifiedBy": "Geändert von", - "Class": "Klasse", - "AttachedTo": "Angehängt an", - "AttachedToClass": "Angehängt an Klasse", - "Name": "Name", - "Description": "Beschreibung", - "ShortDescription": "Kurzbeschreibung", - "Descriptor": "Bezeichner", - "TargetClass": "Zielklasse", - "Role": "Rolle", - "Roles": "Rollen", - "Private": "Privat", - "Archived": "Archiviert", - "ClassLabel": "Typ", - "ClassPropertyLabel": "Bezeichnung", - "String": "Text", - "Markup": "Formatierter Text", - "Number": "Zahl", - "Boolean": "Kontrollkästchen", - "Timestamp": "Zeitstempel", - "Date": "Datum", - "IntlString": "Internationaler Text", - "Ref": "Referenz", - "Collection": "Sammlung", - "Array": "Mehrfachauswahl", - "Enum": "Auswahl", - "Members": "Mitglieder", - "Hyperlink": "URL", - "MarkupBlobRef": "Kollaborativ", - "Object": "Objekt", - "System": "System", - "CreatedBy": "Erstellt von", - "CreatedDate": "Erstellungsdatum", - "Status": "Status", - "StatusCategory": "Statuskategorie", - "Account": "Konto", - "Rank": "Rang", - "Owners": "Eigentümer", - "Permission": "Berechtigung", - "CreateObject": "Objekt erstellen", - "UpdateObject": "Objekt aktualisieren", - "DeleteObject": "Objekt löschen", - "ForbidDeleteObject": "Objekt löschen verbieten", - "UpdateSpace": "Arbeitsbereich aktualisieren", - "ArchiveSpace": "Arbeitsbereich archivieren", - "CreateObjectDescription": "Gewährt Benutzern die Möglichkeit, Objekte im Arbeitsbereich zu erstellen", - "UpdateObjectDescription": "Gewährt Benutzern die Möglichkeit, Objekte im Arbeitsbereich zu aktualisieren", - "DeleteObjectDescription": "Gewährt Benutzern die Möglichkeit, Objekte im Arbeitsbereich zu löschen", - "ForbidDeleteObjectDescription": "Verbietet Benutzern das Löschen von Objekten im Arbeitsbereich", - "UpdateSpaceDescription": "Gewährt Benutzern die Möglichkeit, den Arbeitsbereich zu aktualisieren", - "ArchiveSpaceDescription": "Gewährt Benutzern die Möglichkeit, den Arbeitsbereich zu archivieren", - "AutoJoin": "Automatisch beitreten", - "AutoJoinDescr": "Neue Mitarbeiter automatisch diesem Arbeitsbereich hinzufügen", - "BlobSize": "Größe", - "BlobContentType": "Inhaltstyp", - "Relation": "Beziehung", - "Relations": "Beziehungen", - "AddRelation": "Beziehung hinzufügen", - "PersonId": "Person", - "AccountId": "Konto" - } -} diff --git a/foundations/core/packages/core/lang/en.json b/foundations/core/packages/core/lang/en.json deleted file mode 100644 index f9687e3159..0000000000 --- a/foundations/core/packages/core/lang/en.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "string": { - "Id": "Id", - "Space": "Space", - "Spaces": "Spaces", - "SpacesDescription": "Manage all spaces' space type", - "TypedSpace": "Typed space", - "SpaceType": "Space type", - "Modified": "Modified", - "ModifiedDate": "Modified date", - "ModifiedBy": "Modified by", - "Class": "Class", - "AttachedTo": "Attached to", - "AttachedToClass": "Attached to class", - "Name": "Name", - "Description": "Description", - "ShortDescription": "Short description", - "Descriptor": "Descriptor", - "TargetClass": "Target class", - "Role": "Role", - "Roles": "Roles", - "Private": "Private", - "Archived": "Archived", - "ClassLabel": "Type", - "ClassPropertyLabel": "Label", - "String": "Text", - "Markup": "Markup", - "Number": "Number", - "Boolean": "Checkbox", - "Timestamp": "Timestamp", - "Date": "Date", - "IntlString": "IntlString", - "Ref": "Reference", - "Collection": "Collection", - "Array": "Multi-Select", - "Enum": "Select", - "Members": "Members", - "Hyperlink": "URL", - "MarkupBlobRef": "Collaborative", - "Object": "Object", - "System": "System", - "CreatedBy": "Created by", - "CreatedDate": "Created date", - "Status": "Status", - "StatusCategory": "Status category", - "Account": "Account", - "Rank": "Rank", - "Owners": "Owners", - "Permission": "Permission", - "CreateObject": "Create object", - "UpdateObject": "Update object", - "DeleteObject": "Delete object", - "ForbidDeleteObject": "Forbid delete object", - "UpdateSpace": "Update space", - "ArchiveSpace": "Archive space", - "CreateObjectDescription": "Grants users ability to create objects in the space", - "UpdateObjectDescription": "Grants users ability to update objects in the space", - "DeleteObjectDescription": "Grants users ability to delete objects in the space", - "ForbidDeleteObjectDescription": "Forbid users deleting objects in the space", - "UpdateSpaceDescription": "Grants users ability to update the space", - "ArchiveSpaceDescription": "Grants users ability to archive the space", - "AutoJoin": "Auto join", - "AutoJoinDescr": "Automatically join new employees to this space", - "BlobSize": "Size", - "BlobContentType": "Content type", - "Relation": "Relation", - "Relations": "Relations", - "AddRelation": "Add relation", - "PersonId": "Person", - "AccountId": "Account" - } -} diff --git a/foundations/core/packages/core/lang/es.json b/foundations/core/packages/core/lang/es.json deleted file mode 100644 index e6917fa281..0000000000 --- a/foundations/core/packages/core/lang/es.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "string": { - "Id": "Id.", - "Space": "Espacio", - "Spaces": "Espacios", - "SpacesDescription": "Gestionar el tipo de espacio de todos los espacios", - "Modified": "Modificado", - "ModifiedDate": "Fecha de modificación", - "ModifiedBy": "Modificado por", - "Class": "Clase", - "AttachedTo": "Adjunto a", - "AttachedToClass": "Clase adjunta", - "Name": "Nombre", - "Description": "Descripción", - "Private": "Privado", - "Archived": "Archivado", - "ClassLabel": "Tipo", - "ClassPropertyLabel": "Etiqueta", - "String": "Texto", - "Markup": "Marcado", - "Number": "Número", - "Boolean": "Casilla de verificación", - "Timestamp": "Marca de tiempo", - "Date": "Fecha", - "IntlString": "Cadena de texto internacionalizada", - "Ref": "Referencia", - "Collection": "Colección", - "Array": "Selección múltiple", - "Enum": "Selección", - "Members": "Miembros", - "Hyperlink": "Enlace", - "MarkupBlobRef": "Colaborativo", - "Object": "Objeto", - "System": "Sistema", - "CreatedBy": "Creado por", - "CreatedDate": "Fecha de creación", - "Status": "Estado", - "StatusCategory": "Categoría de estado", - "Account": "Cuenta", - "Rank": "Rango", - "Owners": "Propietarios", - "Permission": "Permiso", - "CreateObject": "Crear objeto", - "UpdateObject": "Actualizar objeto", - "DeleteObject": "Eliminar objeto", - "ForbidDeleteObject": "Prohibir eliminar objeto", - "UpdateSpace": "Actualizar espacio", - "ArchiveSpace": "Archivar espacio", - "CreateObjectDescription": "Concede a los usuarios la capacidad de crear objetos en el espacio", - "UpdateObjectDescription": "Concede a los usuarios la capacidad de actualizar objetos en el espacio", - "DeleteObjectDescription": "Concede a los usuarios la capacidad de eliminar objetos en el espacio", - "ForbidDeleteObjectDescription": "Prohíbe a los usuarios eliminar objetos en el espacio", - "UpdateSpaceDescription": "Concede a los usuarios la capacidad de actualizar el espacio", - "ArchiveSpaceDescription": "Concede a los usuarios la capacidad de archivar el espacio", - "AutoJoin": "Auto unirse", - "AutoJoinDescr": "Unirse automáticamente a los nuevos empleados a este espacio", - "BlobSize": "Tamaño", - "BlobContentType": "Tipo de contenido", - "Relation": "Relación", - "Relations": "Relaciones", - "AddRelation": "Añadir relación", - "PersonId": "Id. de persona", - "AccountId": "Cuenta" - } -} diff --git a/foundations/core/packages/core/lang/fr.json b/foundations/core/packages/core/lang/fr.json deleted file mode 100644 index bb4b311b71..0000000000 --- a/foundations/core/packages/core/lang/fr.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "string": { - "Id": "Id", - "Space": "Espace", - "Spaces": "Espaces", - "SpacesDescription": "Gérer le type d'espace de tous les espaces", - "TypedSpace": "Espace typé", - "SpaceType": "Type d'espace", - "Modified": "Modifié", - "ModifiedDate": "Date de modification", - "ModifiedBy": "Modifié par", - "Class": "Classe", - "AttachedTo": "Attaché à", - "AttachedToClass": "Attaché à la classe", - "Name": "Nom", - "Description": "Description", - "ShortDescription": "Description courte", - "Descriptor": "Descripteur", - "TargetClass": "Classe cible", - "Role": "Rôle", - "Roles": "Rôles", - "Private": "Privé", - "Archived": "Archivé", - "ClassLabel": "Type", - "ClassPropertyLabel": "Label", - "String": "Texte", - "Markup": "Balise", - "Number": "Nombre", - "Boolean": "Case à cocher", - "Timestamp": "Horodatage", - "Date": "Date", - "IntlString": "Chaîne internationale", - "Ref": "Référence", - "Collection": "Collection", - "Array": "Sélection multiple", - "Enum": "Sélection", - "Members": "Membres", - "Hyperlink": "URL", - "MarkupBlobRef": "Collaboratif", - "Object": "Objet", - "System": "Système", - "CreatedBy": "Créé par", - "CreatedDate": "Date de création", - "Status": "Statut", - "StatusCategory": "Catégorie de statut", - "Account": "Compte", - "Rank": "Rang", - "Owners": "Propriétaires", - "Permission": "Permission", - "CreateObject": "Créer un objet", - "UpdateObject": "Mettre à jour l'objet", - "DeleteObject": "Supprimer l'objet", - "ForbidDeleteObject": "Interdire la suppression de l'objet", - "UpdateSpace": "Mettre à jour l'espace", - "ArchiveSpace": "Archiver l'espace", - "CreateObjectDescription": "Accorde aux utilisateurs la capacité de créer des objets dans l'espace", - "UpdateObjectDescription": "Accorde aux utilisateurs la capacité de mettre à jour les objets dans l'espace", - "DeleteObjectDescription": "Accorde aux utilisateurs la capacité de supprimer des objets dans l'espace", - "ForbidDeleteObjectDescription": "Interdire aux utilisateurs de supprimer des objets dans l'espace", - "UpdateSpaceDescription": "Accorde aux utilisateurs la capacité de mettre à jour l'espace", - "ArchiveSpaceDescription": "Accorde aux utilisateurs la capacité d'archiver l'espace", - "AutoJoin": "Rejoindre automatiquement", - "AutoJoinDescr": "Ajouter automatiquement les nouveaux employés à cet espace", - "BlobSize": "Taille", - "BlobContentType": "Type de contenu", - "Relation": "Relation", - "Relations": "Relations", - "AddRelation": "Ajouter une relation", - "PersonId": "Id de personne", - "AccountId": "Compte" - } -} diff --git a/foundations/core/packages/core/lang/it.json b/foundations/core/packages/core/lang/it.json deleted file mode 100644 index 68f072205e..0000000000 --- a/foundations/core/packages/core/lang/it.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "string": { - "Id": "Id", - "Space": "Spazio", - "Spaces": "Spazi", - "SpacesDescription": "Gestisci il tipo di spazio di tutti gli spazi", - "TypedSpace": "Spazio tipizzato", - "SpaceType": "Tipo di spazio", - "Modified": "Modificato", - "ModifiedDate": "Data di modifica", - "ModifiedBy": "Modificato da", - "Class": "Classe", - "AttachedTo": "Associato a", - "AttachedToClass": "Associato alla classe", - "Name": "Nome", - "Description": "Descrizione", - "ShortDescription": "Descrizione breve", - "Descriptor": "Descrittore", - "TargetClass": "Classe target", - "Role": "Ruolo", - "Roles": "Ruoli", - "Private": "Privato", - "Archived": "Archiviato", - "ClassLabel": "Tipo", - "ClassPropertyLabel": "Etichetta", - "String": "Testo", - "Markup": "Markup", - "Number": "Numero", - "Boolean": "Casella di controllo", - "Timestamp": "Timestamp", - "Date": "Data", - "IntlString": "StringaIntl", - "Ref": "Riferimento", - "Collection": "Collezione", - "Array": "Selezione multipla", - "Enum": "Selezione", - "Members": "Membri", - "Hyperlink": "Hyperlink", - "Collaborative": "Collaborativo", - "Object": "Oggetto", - "System": "Sistema", - "CreatedBy": "Creato da", - "CreatedDate": "Data di creazione", - "Status": "Stato", - "StatusCategory": "Categoria di stato", - "Account": "Account", - "Rank": "Livello", - "Owners": "Proprietari", - "Permission": "Permesso", - "CreateObject": "Crea oggetto", - "UpdateObject": "Aggiorna oggetto", - "DeleteObject": "Elimina oggetto", - "ForbidDeleteObject": "Proibisci eliminazione oggetto", - "UpdateSpace": "Aggiorna spazio", - "ArchiveSpace": "Archivia spazio", - "CreateObjectDescription": "Concede agli utenti la possibilità di creare oggetti nello spazio", - "UpdateObjectDescription": "Concede agli utenti la possibilità di aggiornare oggetti nello spazio", - "DeleteObjectDescription": "Concede agli utenti la possibilità di eliminare oggetti nello spazio", - "ForbidDeleteObjectDescription": "Proibisce agli utenti di eliminare oggetti nello spazio", - "UpdateSpaceDescription": "Concede agli utenti la possibilità di aggiornare lo spazio", - "ArchiveSpaceDescription": "Concede agli utenti la possibilità di archiviare lo spazio", - "AutoJoin": "Partecipazione automatica", - "AutoJoinDescr": "Aggiungi automaticamente i nuovi dipendenti a questo spazio", - "BlobSize": "Dimensione", - "BlobContentType": "Tipo di contenuto", - "Relation": "Relazione", - "Relations": "Relazioni", - "AddRelation": "Aggiungi relazione", - "PersonId": "ID persona", - "AccountId": "ID account" - } -} diff --git a/foundations/core/packages/core/lang/ja.json b/foundations/core/packages/core/lang/ja.json deleted file mode 100644 index cdb6c0daa1..0000000000 --- a/foundations/core/packages/core/lang/ja.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "string": { - "Id": "ID", - "Space": "スペース", - "Spaces": "スペース一覧", - "SpacesDescription": "すべてのスペースの種類を管理します", - "TypedSpace": "種類付きスペース", - "SpaceType": "スペースの種類", - "Modified": "更新", - "ModifiedDate": "更新日", - "ModifiedBy": "更新者", - "Class": "クラス", - "AttachedTo": "接続先", - "AttachedToClass": "クラスに接続", - "Name": "名前", - "Description": "説明", - "ShortDescription": "短い説明", - "Descriptor": "識別子", - "TargetClass": "対象クラス", - "Role": "ロール", - "Roles": "ロール一覧", - "Private": "非公開", - "Archived": "アーカイブ済み", - "ClassLabel": "タイプ", - "ClassPropertyLabel": "ラベル", - "String": "テキスト", - "Markup": "マークアップ", - "Number": "数値", - "Boolean": "チェックボックス", - "Timestamp": "タイムスタンプ", - "Date": "日付", - "IntlString": "多言語テキスト", - "Ref": "参照", - "Collection": "コレクション", - "Array": "複数選択", - "Enum": "選択肢", - "Members": "メンバー", - "Hyperlink": "URL", - "MarkupBlobRef": "共同編集", - "Object": "オブジェクト", - "System": "システム", - "CreatedBy": "作成者", - "CreatedDate": "作成日", - "Status": "ステータス", - "StatusCategory": "ステータスカテゴリ", - "Account": "アカウント", - "Rank": "ランク", - "Owners": "所有者", - "Permission": "権限", - "CreateObject": "オブジェクトの作成", - "UpdateObject": "オブジェクトの更新", - "DeleteObject": "オブジェクトの削除", - "ForbidDeleteObject": "オブジェクトの削除禁止", - "UpdateSpace": "スペースの更新", - "ArchiveSpace": "スペースのアーカイブ", - "CreateObjectDescription": "ユーザーにスペース内でオブジェクトを作成する権限を付与します", - "UpdateObjectDescription": "ユーザーにスペース内でオブジェクトを更新する権限を付与します", - "DeleteObjectDescription": "ユーザーにスペース内でオブジェクトを削除する権限を付与します", - "ForbidDeleteObjectDescription": "ユーザーによるスペース内のオブジェクトの削除を禁止します", - "UpdateSpaceDescription": "ユーザーにスペースを更新する権限を付与します", - "ArchiveSpaceDescription": "ユーザーにスペースをアーカイブする権限を付与します", - "AutoJoin": "自動参加", - "AutoJoinDescr": "新しいユーザーを自動的にこのスペースに参加させます", - "BlobSize": "サイズ", - "BlobContentType": "コンテンツタイプ", - "Relation": "関係", - "Relations": "関係一覧", - "AddRelation": "関係を追加", - "PersonId": "人物", - "AccountId": "アカウント" - } -} diff --git a/foundations/core/packages/core/lang/pt.json b/foundations/core/packages/core/lang/pt.json deleted file mode 100644 index c7df192c7d..0000000000 --- a/foundations/core/packages/core/lang/pt.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "string": { - "Id": "Id", - "Space": "Espaço", - "Spaces": "Espaços", - "SpacesDescription": "Gestão do tipo de espaço para todas as espaços", - "Modified": "Modificado", - "ModifiedDate": "Data de modificação", - "ModifiedBy": "Modificado por", - "Class": "Classe", - "AttachedTo": "Anexado a", - "AttachedToClass": "Classe anexada", - "Name": "Nome", - "Description": "Descrição", - "Private": "Privado", - "Archived": "Arquivado", - "ClassLabel": "Tipo", - "ClassPropertyLabel": "Rótulo", - "String": "Texto", - "Markup": "Marcação", - "Number": "Número", - "Boolean": "Caixa de seleção", - "Timestamp": "Marca de data/hora", - "Date": "Data", - "IntlString": "Cadeia de texto internacionalizada", - "Ref": "Referência", - "Collection": "Coleção", - "Array": "Seleção múltipla", - "Enum": "Seleção", - "Members": "Membros", - "Hyperlink": "URL", - "MarkupBlobRef": "Colaborativo", - "Object": "Objeto", - "System": "Sistema", - "CreatedBy": "Criado por", - "CreatedDate": "Data de criação", - "Status": "Estado", - "StatusCategory": "Categoria de estado", - "Account": "Conta", - "Rank": "Ranking", - "Owners": "Proprietários", - "Permission": "Permissão", - "CreateObject": "Criar objeto", - "UpdateObject": "Atualizar objeto", - "DeleteObject": "Apagar objeto", - "ForbidDeleteObject": "Proibir apagar objeto", - "UpdateSpace": "Atualizar espaço", - "ArchiveSpace": "Arquivar espaço", - "CreateObjectDescription": "Concede aos usuários a capacidade de criar objetos no espaço", - "UpdateObjectDescription": "Concede aos usuários a capacidade de atualizar objetos no espaço", - "DeleteObjectDescription": "Concede aos usuários a capacidade de apagar objetos no espaço", - "ForbidDeleteObjectDescription": "Proíbe aos usuários a capacidade de apagar objetos no espaço", - "UpdateSpaceDescription": "Concede aos usuários a capacidade de atualizar o espaço", - "ArchiveSpaceDescription": "Concede aos usuários a capacidade de arquivar o espaço", - "AutoJoin": "Auto adesão", - "AutoJoinDescr": "Adesão automática de novos funcionários a este espaço", - "BlobSize": "Tamanho", - "BlobContentType": "Tipo de conteúdo", - "Relation": "Relação", - "Relations": "Relações", - "AddRelation": "Adicionar relação", - "PersonId": "ID de pessoa", - "AccountId": "Conta" - } -} diff --git a/foundations/core/packages/core/lang/ru.json b/foundations/core/packages/core/lang/ru.json deleted file mode 100644 index 24cb255c36..0000000000 --- a/foundations/core/packages/core/lang/ru.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "string": { - "Id": "Id", - "Space": "Пространство", - "Spaces": "Пространства", - "SpacesDescription": "Управлять типом пространства всех пространств", - "TypedSpace": "Типизированное пространство", - "SpaceType": "Тип пространства", - "Modified": "Изменено", - "ModifiedDate": "Дата изменения", - "ModifiedBy": "Изменен", - "Class": "Класс", - "AttachedTo": "Прикреплен к", - "AttachedToClass": "Прикреплен к классу", - "Name": "Название", - "Description": "Описание", - "ShortDescription": "Короткое описание", - "Descriptor": "Дескриптор", - "TargetClass": "Целевой класс", - "Role": "Роль", - "Roles": "Роли", - "Private": "Личный", - "Archived": "Архивный", - "ClassLabel": "Тип", - "ClassPropertyLabel": "Название", - "String": "Текст", - "Markup": "Разметка", - "Number": "Число", - "Boolean": "Чекбокс", - "Timestamp": "Временная отметка", - "Date": "Дата", - "IntlString": "Интернационализированная строка", - "Ref": "Ссылка", - "Collection": "Коллекция", - "Array": "Множественный выбор", - "Enum": "Выбор", - "Members": "Участники", - "Hyperlink": "URL", - "MarkupBlobRef": "Коллаборативный", - "Object": "Объект", - "System": "Система", - "CreatedBy": "Создан", - "CreatedDate": "Дата создания", - "Status": "Статус", - "StatusCategory": "Категория статуса", - "Account": "Аккаунт", - "Rank": "Ранг", - "Owners": "Владельцы", - "Permission": "Разрешение", - "CreateObject": "Создавать объект", - "UpdateObject": "Обновлять объект", - "DeleteObject": "Удалять объект", - "ForbidDeleteObject": "Запретить удалять объект", - "UpdateSpace": "Обновлять пространство", - "ArchiveSpace": "Архивировать пространство", - "CreateObjectDescription": "Дает пользователям разрешение создавать объекты в пространстве", - "UpdateObjectDescription": "Дает пользователям разрешение обновлять объекты в пространстве", - "DeleteObjectDescription": "Дает пользователям разрешение удалять объекты в пространстве", - "ForbidDeleteObjectDescription": "Запрещает пользователям удалять объекты в пространстве", - "UpdateSpaceDescription": "Дает пользователям разрешение обновлять пространство", - "ArchiveSpaceDescription": "Дает пользователям разрешение архивировать пространство", - "AutoJoin": "Автоприсоединение", - "AutoJoinDescr": "Автоматически присоединять новых сотрудников к этому пространству", - "BlobSize": "Размер", - "BlobContentType": "Тип контента", - "Relation": "Связь", - "Relations": "Связи", - "AddRelation": "Добавить связь", - "PersonId": "Персона", - "AccountId": "Аккаунт" - } -} diff --git a/foundations/core/packages/core/lang/tr.json b/foundations/core/packages/core/lang/tr.json deleted file mode 100644 index 1f862d2e08..0000000000 --- a/foundations/core/packages/core/lang/tr.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "string": { - "Id": "Id", - "Space": "Alan", - "Spaces": "Alanlar", - "SpacesDescription": "Tüm alanların alan tipini yönet", - "TypedSpace": "Tipli alan", - "SpaceType": "Alan tipi", - "Modified": "Değiştirildi", - "ModifiedDate": "Değiştirilme tarihi", - "ModifiedBy": "Değiştiren", - "Class": "Sınıf", - "AttachedTo": "Eklenen", - "AttachedToClass": "Eklenen sınıf", - "Name": "İsim", - "Description": "Açıklama", - "ShortDescription": "Kısa açıklama", - "Descriptor": "Tanımlayıcı", - "TargetClass": "Hedef sınıf", - "Role": "Rol", - "Roles": "Roller", - "Private": "Özel", - "Archived": "Arşivlendi", - "ClassLabel": "Tip", - "ClassPropertyLabel": "Etiket", - "String": "Metin", - "Markup": "İşaretleme", - "Number": "Sayı", - "Boolean": "Onay kutusu", - "Timestamp": "Zaman damgası", - "Date": "Tarih", - "IntlString": "IntlString", - "Ref": "Referans", - "Collection": "Koleksiyon", - "Array": "Çoklu Seçim", - "Enum": "Seçim", - "Members": "Üyeler", - "Hyperlink": "URL", - "MarkupBlobRef": "İşbirlikçi", - "Object": "Nesne", - "System": "Sistem", - "CreatedBy": "Oluşturan", - "CreatedDate": "Oluşturulma tarihi", - "Status": "Durum", - "StatusCategory": "Durum kategorisi", - "Account": "Hesap", - "Rank": "Sıra", - "Owners": "Sahipler", - "Permission": "İzin", - "CreateObject": "Nesne oluştur", - "UpdateObject": "Nesneyi güncelle", - "DeleteObject": "Nesneyi sil", - "ForbidDeleteObject": "Nesne silmeyi yasakla", - "UpdateSpace": "Alanı güncelle", - "ArchiveSpace": "Alanı arşivle", - "CreateObjectDescription": "Kullanıcılara alanda nesne oluşturma yetkisi verir", - "UpdateObjectDescription": "Kullanıcılara alandaki nesneleri güncelleme yetkisi verir", - "DeleteObjectDescription": "Kullanıcılara alandaki nesneleri silme yetkisi verir", - "ForbidDeleteObjectDescription": "Kullanıcıların alandaki nesneleri silmesini yasaklar", - "UpdateSpaceDescription": "Kullanıcılara alanı güncelleme yetkisi verir", - "ArchiveSpaceDescription": "Kullanıcılara alanı arşivleme yetkisi verir", - "AutoJoin": "Otomatik katıl", - "AutoJoinDescr": "Yeni çalışanları bu alana otomatik olarak ekle", - "BlobSize": "Boyut", - "BlobContentType": "İçerik türü", - "Relation": "İlişki", - "Relations": "İlişkiler", - "AddRelation": "İlişki ekle", - "PersonId": "Kişi", - "AccountId": "Hesap" - } -} diff --git a/foundations/core/packages/core/lang/zh.json b/foundations/core/packages/core/lang/zh.json deleted file mode 100644 index 696a334f60..0000000000 --- a/foundations/core/packages/core/lang/zh.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "string": { - "Id": "Id", - "Space": "空间", - "Spaces": "空间", - "SpacesDescription": "管理所有空间的空间类型", - "TypedSpace": "类型化空间", - "SpaceType": "空间类型", - "Modified": "已修改", - "ModifiedDate": "修改日期", - "ModifiedBy": "修改者", - "Class": "类", - "AttachedTo": "附加到", - "AttachedToClass": "附加到类", - "Name": "名称", - "Description": "描述", - "ShortDescription": "简短描述", - "Descriptor": "描述符", - "TargetClass": "目标类", - "Role": "角色", - "Roles": "角色", - "Private": "私有", - "Archived": "已归档", - "ClassLabel": "类型", - "ClassPropertyLabel": "标签", - "String": "文字", - "Markup": "标记", - "Number": "数字", - "Boolean": "复选框", - "Timestamp": "时间戳", - "Date": "日期", - "IntlString": "国际字符串", - "Ref": "引用", - "Collection": "集合", - "Array": "多选", - "Enum": "选择", - "Members": "成员", - "Hyperlink": "URL", - "MarkupBlobRef": "协作", - "Object": "对象", - "System": "系统", - "CreatedBy": "创建者", - "CreatedDate": "创建日期", - "Status": "状态", - "StatusCategory": "状态类别", - "Account": "账户", - "Rank": "排名", - "Owners": "所有者", - "Permission": "权限", - "CreateObject": "创建对象", - "UpdateObject": "更新对象", - "DeleteObject": "删除对象", - "ForbidDeleteObject": "禁止删除对象", - "UpdateSpace": "更新空间", - "ArchiveSpace": "归档空间", - "CreateObjectDescription": "授予用户在空间中创建对象的权限", - "UpdateObjectDescription": "授予用户在空间中更新对象的权限", - "DeleteObjectDescription": "授予用户在空间中删除对象的权限", - "ForbidDeleteObjectDescription": "禁止用户在空间中删除对象", - "UpdateSpaceDescription": "授予用户更新空间的权限", - "ArchiveSpaceDescription": "授予用户归档空间的权限", - "AutoJoin": "自动加入", - "AutoJoinDescr": "自动将新员工加入此空间", - "BlobSize": "大小", - "BlobContentType": "內容類型", - "Relation": "关系", - "Relations": "关系", - "AddRelation": "添加关系", - "PersonId": "人员 ID", - "AccountId": "帐户" - } -} diff --git a/foundations/core/packages/core/package.json b/foundations/core/packages/core/package.json deleted file mode 100644 index 2e851ff81b..0000000000 --- a/foundations/core/packages/core/package.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "name": "@hcengineering/core", - "version": "0.7.22", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "files": [ - "lib/**/*", - "!lib/**/__test__/**", - "types/**/*", - "!types/**/__test__/**", - "src/**/*", - "!src/**/__test__/**", - "lang/**/*", - "README.md", - "CHANGELOG.md" - ], - "author": "Anticrm Platform Contributors", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "build:watch": "compile", - "test": "jest --passWithNoTests --silent --coverage", - "format": "format src", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --coverage", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "workspace:^0.7.19", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "@typescript-eslint/parser": "^6.21.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.6.2", - "typescript": "^5.9.3", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5", - "@types/node": "^22.18.1", - "eslint-plugin-svelte": "^2.35.1" - }, - "dependencies": { - "@hcengineering/platform": "workspace:^0.7.18", - "@hcengineering/analytics": "workspace:^0.7.17", - "@hcengineering/measurements": "workspace:^0.7.18", - "fast-equals": "^5.2.2" - }, - "repository": "https://github.com/hcengineering/huly.core", - "publishConfig": { - "access": "public" - }, - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.js" - }, - "./lang/*.json": { - "require": "./lang/*.json", - "import": "./lang/*.json" - }, - "./lang": { - "require": "./lang", - "import": "./lang" - } - } -} diff --git a/foundations/core/packages/core/src/__tests__/client.test.ts b/foundations/core/packages/core/src/__tests__/client.test.ts deleted file mode 100644 index ae01a11545..0000000000 --- a/foundations/core/packages/core/src/__tests__/client.test.ts +++ /dev/null @@ -1,254 +0,0 @@ -// -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// Copyright © 2021, 2022 Hardcore Engineering, Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// -import { type IntlString, type Plugin } from '@hcengineering/platform' -import { ClientConnectEvent, type DocChunk } from '..' -import type { Class, Data, Doc, Domain, PluginConfiguration, Ref, Space, Timestamp } from '../classes' -import { ClassifierKind, DOMAIN_MODEL } from '../classes' -import { type ClientConnection, createClient } from '../client' -import core from '../component' -import { Hierarchy } from '../hierarchy' -import { ModelDb, TxDb } from '../memdb' -import { TxOperations } from '../operations' -import type { - DocumentQuery, - DomainResult, - FindResult, - SearchOptions, - SearchQuery, - SearchResult, - TxResult -} from '../storage' -import { type Tx, TxFactory, TxProcessor } from '../tx' -import { fillConfiguration, generateId, pluginFilterTx } from '../utils' -import { connect } from './connection' -import { genMinModel } from './minmodel' - -function filterPlugin (plugin: Plugin): (txes: Tx[]) => Tx[] { - return (txes) => { - const configs = new Map, PluginConfiguration>() - fillConfiguration(txes, configs) - - const excludedPlugins = Array.from(configs.values()).filter((it) => !it.enabled || it.pluginId !== plugin) - return pluginFilterTx(excludedPlugins, configs, txes) - } -} - -describe('client', () => { - it('should create client and spaces', async () => { - const klass = core.class.Space - const client = new TxOperations(await createClient(connect), core.account.System) - const result = await client.findAll(klass, {}) - expect(result).toHaveLength(2) - - await client.createDoc(klass, core.space.Model, { - private: false, - name: 'NewSpace', - description: '', - archived: false, - members: [] - }) - const result2 = await client.findAll(klass, {}) - expect(result2).toHaveLength(3) - - await client.createDoc(klass, core.space.Model, { - private: false, - name: 'NewSpace', - description: '', - members: [], - archived: false - }) - const result3 = await client.findAll(klass, {}) - expect(result3).toHaveLength(4) - - const result4 = await client.findOne(klass, {}) - expect(result4).toEqual(result3[0]) - }) - - it('should create client with plugins', async () => { - const txFactory = new TxFactory(core.account.System) - const txes = genMinModel() - - txes.push( - txFactory.createTxCreateDoc( - core.class.Class, - core.space.Model, - { - label: 'PluginConfiguration' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.CLASS, - domain: DOMAIN_MODEL - }, - core.class.PluginConfiguration - ) - ) - - async function connectPlugin (handler: (tx: Tx) => void): Promise { - const hierarchy = new Hierarchy() - - for (const tx of txes) hierarchy.tx(tx) - - const transactions = new TxDb(hierarchy) - const model = new ModelDb(hierarchy) - for (const tx of txes) { - await transactions.tx(tx) - await model.tx(tx) - } - - async function findAll (_class: Ref>, query: DocumentQuery): Promise> { - return await transactions.findAll(_class, query) - } - - return new (class implements ClientConnection { - handler?: (event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise - - set onConnect ( - handler: ((event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise) | undefined - ) { - this.handler = handler - void this.handler?.(ClientConnectEvent.Connected, '', {}) - } - - get onConnect (): - | ((event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise) - | undefined { - return this.handler - } - - isConnected = (): boolean => true - findAll = findAll - pushHandler = (): void => {} - - domainRequest (): Promise { - return Promise.resolve({ domain: 'test' as Domain, value: null }) - } - - searchFulltext = async (query: SearchQuery, options: SearchOptions): Promise => { - return { docs: [] } - } - - tx = async (tx: Tx): Promise => { - if (tx.objectSpace === core.space.Model) { - hierarchy.tx(tx) - } - const result = await Promise.all([transactions.tx(tx)]) - return result[0] - } - - close = async (): Promise => {} - - loadChunk = async (domain: Domain, idx?: number): Promise => ({ - idx: -1, - docs: [], - finished: true - }) - - async getDomainHash (domain: Domain): Promise { - return generateId() - } - - async closeChunk (idx: number): Promise {} - async loadDocs (domain: Domain, docs: Ref[]): Promise { - return [] - } - - async upload (domain: Domain, docs: Doc[]): Promise {} - async clean (domain: Domain, docs: Ref[]): Promise {} - async loadModel (last: Timestamp): Promise { - return txes - } - - async sendForceClose (): Promise {} - })() - } - const spyCreate = jest.spyOn(TxProcessor, 'createDoc2Doc') - const spyUpdate = jest.spyOn(TxProcessor, 'updateDoc2Doc') - - const pluginData1: Data = { - pluginId: 'testPlugin1' as Plugin, - label: 'Test Plugin 1' as IntlString, - transactions: [], - beta: true, - enabled: true - } - const txCreateDoc1 = txFactory.createTxCreateDoc(core.class.PluginConfiguration, core.space.Model, pluginData1) - txes.push(txCreateDoc1) - const client1 = new TxOperations( - await createClient(connectPlugin, filterPlugin('testPlugin1' as Plugin)), - core.account.System - ) - const result1 = await client1.findAll(core.class.PluginConfiguration, {}) - - expect(result1).toHaveLength(1) - expect(result1[0]._id).toStrictEqual(txCreateDoc1.objectId) - expect(spyCreate).toHaveBeenLastCalledWith(txCreateDoc1, false) - expect(spyUpdate).toHaveBeenCalledTimes(0) - await client1.close() - - const pluginData2 = { - pluginId: 'testPlugin2' as Plugin, - label: 'Test Plugin 2' as IntlString, - transactions: [], - beta: true, - enabled: true - } - const txCreateDoc2 = txFactory.createTxCreateDoc(core.class.PluginConfiguration, core.space.Model, pluginData2) - txes.push(txCreateDoc2) - const client2 = new TxOperations( - await createClient(connectPlugin, filterPlugin('testPlugin1' as Plugin)), - core.account.System - ) - const result2 = await client2.findAll(core.class.PluginConfiguration, {}) - - expect(result2).toHaveLength(2) - expect(result2[0]._id).toStrictEqual(txCreateDoc1.objectId) - expect(result2[1]._id).toStrictEqual(txCreateDoc2.objectId) - expect(spyCreate).toHaveBeenLastCalledWith(txCreateDoc2, false) - expect(spyUpdate).toHaveBeenCalledTimes(0) - await client2.close() - - const pluginData3 = { - pluginId: 'testPlugin3' as Plugin, - label: 'Test Plugin 3' as IntlString, - transactions: [txCreateDoc1._id], - beta: true, - enabled: true - } - const txUpdateDoc = txFactory.createTxUpdateDoc( - core.class.PluginConfiguration, - core.space.Model, - txCreateDoc1.objectId, - pluginData3 - ) - txes.push(txUpdateDoc) - const client3 = new TxOperations( - await createClient(connectPlugin, filterPlugin('testPlugin2' as Plugin)), - core.account.System - ) - const result3 = await client3.findAll(core.class.PluginConfiguration, {}) - - expect(result3).toHaveLength(1) - expect(result3[0]._id).toStrictEqual(txCreateDoc2.objectId) - expect(spyCreate).toHaveBeenLastCalledWith(txCreateDoc2, false) - expect(spyUpdate.mock.calls[1][1]).toStrictEqual(txUpdateDoc) - expect(spyUpdate).toBeCalledTimes(2) - await client3.close() - - spyCreate.mockReset() - spyCreate.mockRestore() - spyUpdate.mockReset() - spyUpdate.mockRestore() - }) -}) diff --git a/foundations/core/packages/core/src/__tests__/clone.test.ts b/foundations/core/packages/core/src/__tests__/clone.test.ts deleted file mode 100644 index 66d7e651da..0000000000 --- a/foundations/core/packages/core/src/__tests__/clone.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { clone, getTypeOf } from '../clone' - -describe('clone', () => { - it('should handle primitive types', () => { - expect(clone(undefined)).toBeUndefined() - expect(clone(null)).toBeNull() - expect(clone(123)).toBe(123) - expect(clone('test')).toBe('test') - expect(clone(true)).toBe(true) - }) - - it('should clone Date objects', () => { - const date = new Date() - const cloned = clone(date) - expect(cloned).toBeInstanceOf(Date) - expect(cloned.getTime()).toBe(date.getTime()) - }) - - it('should clone Arrays', () => { - const arr = [1, 'test', { a: 1 }, new Date()] - const cloned = clone(arr) - expect(cloned).toEqual(arr) - expect(cloned).not.toBe(arr) - expect(cloned[2]).not.toBe(arr[2]) - expect(cloned[3]).not.toBe(arr[3]) - }) - - it('should clone Objects', () => { - const obj = { - num: 123, - str: 'test', - date: new Date(), - nested: { a: 1 }, - arr: [1, 2, 3] - } - const cloned = clone(obj) - expect(cloned).toEqual(obj) - expect(cloned).not.toBe(obj) - expect(cloned.nested).not.toBe(obj.nested) - expect(cloned.arr).not.toBe(obj.arr) - expect(cloned.date).not.toBe(obj.date) - }) - - it('should respect depth parameter', () => { - const deep = { a: { b: { c: { d: 1 } } } } - expect(clone(deep, undefined, undefined, 0)).toBe(deep) - expect(clone(deep, undefined, undefined, 1)).toEqual({ a: { b: { c: { d: 1 } } } }) - expect(clone(deep, undefined, undefined, 2)).toEqual({ a: { b: { c: { d: 1 } } } }) - expect(clone(deep, undefined, undefined, 3)).toEqual({ a: { b: { c: { d: 1 } } } }) - }) - - it('should handle functions', () => { - const fn: () => any = () => {} - expect(clone(fn)).toBe(fn) - }) -}) - -describe('getTypeOf', () => { - it('should detect types correctly', () => { - expect(getTypeOf(null)).toBe('null') - expect(getTypeOf([])).toBe('Array') - expect(getTypeOf({})).toBe('Object') - expect(getTypeOf(new Date())).toBe('Date') - expect(getTypeOf(/test/)).toBe('RegExp') - expect(getTypeOf(123)).toBe('number') - expect(getTypeOf('test')).toBe('string') - }) -}) diff --git a/foundations/core/packages/core/src/__tests__/collaboration.test.ts b/foundations/core/packages/core/src/__tests__/collaboration.test.ts deleted file mode 100644 index 9a8fbaa964..0000000000 --- a/foundations/core/packages/core/src/__tests__/collaboration.test.ts +++ /dev/null @@ -1,310 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - makeCollabId, - makeDocCollabId, - makeCollabYdocId, - makeCollabJsonId, - type CollaborativeDoc -} from '../collaboration' -import type { Class, Doc, Ref } from '../classes' - -describe('collaboration', () => { - describe('makeCollabId', () => { - it('should create collaborative doc id from parameters', () => { - const objectClass = 'class:core.Doc' as Ref> - const objectId = 'doc123' as Ref - const objectAttr = 'content' - - const result = makeCollabId(objectClass, objectId, objectAttr) - - expect(result).toEqual({ - objectClass, - objectId, - objectAttr - }) - }) - - it('should handle different attribute types', () => { - const objectClass = 'class:task.Task' as Ref> - const objectId = 'task456' as Ref - - const result1 = makeCollabId(objectClass, objectId, 'description') - const result2 = makeCollabId(objectClass, objectId, 'comments') - - expect(result1.objectAttr).toBe('description') - expect(result2.objectAttr).toBe('comments') - }) - - it('should create unique ids for same object but different attributes', () => { - const objectClass = 'class:core.Doc' as Ref> - const objectId = 'doc123' as Ref - - const result1 = makeCollabId(objectClass, objectId, 'attr1') - const result2 = makeCollabId(objectClass, objectId, 'attr2') - - expect(result1.objectAttr).not.toBe(result2.objectAttr) - }) - }) - - describe('makeDocCollabId', () => { - it('should create collaborative doc id from document', () => { - const doc: Doc = { - _id: 'doc789' as Ref, - _class: 'class:core.Doc' as Ref>, - space: 'space1' as any, - modifiedOn: 12345, - modifiedBy: 'user1' as any - } - - const result = makeDocCollabId(doc, 'content') - - expect(result).toEqual({ - objectClass: doc._class, - objectId: doc._id, - objectAttr: 'content' - }) - }) - - it('should work with typed attribute keys', () => { - interface TestDoc extends Doc { - title: string - description: string - } - - const doc: TestDoc = { - _id: 'doc999' as Ref, - _class: 'class:test.TestDoc' as Ref>, - space: 'space1' as any, - modifiedOn: 12345, - modifiedBy: 'user1' as any, - title: 'Test', - description: 'Description' - } - - const result = makeDocCollabId(doc, 'description') - - expect(result.objectAttr).toBe('description') - }) - - it('should handle documents with complex ids', () => { - const doc: Doc = { - _id: 'space:task:project-1:task-123' as Ref, - _class: 'class:task.Task' as Ref>, - space: 'space1' as any, - modifiedOn: 12345, - modifiedBy: 'user1' as any - } - - const result = makeDocCollabId(doc, 'content') - - expect(result.objectId).toBe('space:task:project-1:task-123') - }) - }) - - describe('makeCollabYdocId', () => { - it('should create ydoc id from collaborative doc', () => { - const collabDoc: CollaborativeDoc = { - objectClass: 'class:core.Doc' as Ref>, - objectId: 'doc123' as Ref, - objectAttr: 'content' - } - - const result = makeCollabYdocId(collabDoc) - - expect(result).toBe('doc123%content') - }) - - it('should use % as separator', () => { - const collabDoc: CollaborativeDoc = { - objectClass: 'class:core.Doc' as Ref>, - objectId: 'myDoc' as Ref, - objectAttr: 'myAttr' - } - - const result = makeCollabYdocId(collabDoc) - - expect(result).toContain('%') - expect(result.split('%')).toHaveLength(2) - }) - - it('should create consistent ids', () => { - const collabDoc: CollaborativeDoc = { - objectClass: 'class:core.Doc' as Ref>, - objectId: 'doc456' as Ref, - objectAttr: 'field' - } - - const result1 = makeCollabYdocId(collabDoc) - const result2 = makeCollabYdocId(collabDoc) - - expect(result1).toBe(result2) - }) - - it('should create different ids for different attributes', () => { - const collabDoc1: CollaborativeDoc = { - objectClass: 'class:core.Doc' as Ref>, - objectId: 'doc789' as Ref, - objectAttr: 'content' - } - - const collabDoc2: CollaborativeDoc = { - objectClass: 'class:core.Doc' as Ref>, - objectId: 'doc789' as Ref, - objectAttr: 'description' - } - - const result1 = makeCollabYdocId(collabDoc1) - const result2 = makeCollabYdocId(collabDoc2) - - expect(result1).not.toBe(result2) - }) - - it('should handle special characters in ids', () => { - const collabDoc: CollaborativeDoc = { - objectClass: 'class:core.Doc' as Ref>, - objectId: 'doc:with:colons' as Ref, - objectAttr: 'attr-with-dash' - } - - const result = makeCollabYdocId(collabDoc) - - expect(result).toBe('doc:with:colons%attr-with-dash') - }) - }) - - describe('makeCollabJsonId', () => { - beforeEach(() => { - jest.useFakeTimers() - }) - - afterEach(() => { - jest.useRealTimers() - }) - - it('should create json id with timestamp', () => { - const mockDate = new Date('2024-01-01T00:00:00.000Z') - jest.setSystemTime(mockDate) - - const collabDoc: CollaborativeDoc = { - objectClass: 'class:core.Doc' as Ref>, - objectId: 'doc123' as Ref, - objectAttr: 'content' - } - - const result = makeCollabJsonId(collabDoc) - - expect(result).toBe(`doc123-content-${mockDate.getTime()}`) - }) - - it('should use - as separator', () => { - const collabDoc: CollaborativeDoc = { - objectClass: 'class:core.Doc' as Ref>, - objectId: 'myDoc' as Ref, - objectAttr: 'myAttr' - } - - const result = makeCollabJsonId(collabDoc) - - const parts = result.split('-') - expect(parts.length).toBeGreaterThanOrEqual(3) - expect(parts[0]).toBe('myDoc') - expect(parts[1]).toBe('myAttr') - }) - - it('should create different ids when called at different times', () => { - const collabDoc: CollaborativeDoc = { - objectClass: 'class:core.Doc' as Ref>, - objectId: 'doc456' as Ref, - objectAttr: 'field' - } - - jest.setSystemTime(new Date('2024-01-01T00:00:00.000Z')) - const result1 = makeCollabJsonId(collabDoc) - - jest.setSystemTime(new Date('2024-01-01T00:00:01.000Z')) - const result2 = makeCollabJsonId(collabDoc) - - expect(result1).not.toBe(result2) - }) - - it('should include all three components', () => { - const mockDate = new Date('2024-06-15T12:30:45.000Z') - jest.setSystemTime(mockDate) - - const collabDoc: CollaborativeDoc = { - objectClass: 'class:core.Doc' as Ref>, - objectId: 'testDoc' as Ref, - objectAttr: 'testAttr' - } - - const result = makeCollabJsonId(collabDoc) - - expect(result).toContain('testDoc') - expect(result).toContain('testAttr') - expect(result).toContain(mockDate.getTime().toString()) - }) - - it('should handle objects with ids containing dashes', () => { - const mockDate = new Date('2024-01-01T00:00:00.000Z') - jest.setSystemTime(mockDate) - - const collabDoc: CollaborativeDoc = { - objectClass: 'class:core.Doc' as Ref>, - objectId: 'doc-with-dashes' as Ref, - objectAttr: 'attr-with-dashes' - } - - const result = makeCollabJsonId(collabDoc) - - expect(result).toBe(`doc-with-dashes-attr-with-dashes-${mockDate.getTime()}`) - }) - }) - - describe('integration tests', () => { - it('should create different types of ids from same collaborative doc', () => { - const collabDoc: CollaborativeDoc = { - objectClass: 'class:core.Doc' as Ref>, - objectId: 'doc123' as Ref, - objectAttr: 'content' - } - - const ydocId = makeCollabYdocId(collabDoc) - const jsonId = makeCollabJsonId(collabDoc) - - expect(ydocId).not.toBe(jsonId) - expect(ydocId).toContain(collabDoc.objectId) - expect(jsonId).toContain(collabDoc.objectId) - }) - - it('should work with makeDocCollabId and other functions', () => { - const doc: Doc = { - _id: 'doc999' as Ref, - _class: 'class:core.Doc' as Ref>, - space: 'space1' as any, - modifiedOn: 12345, - modifiedBy: 'user1' as any - } - - const collabDoc = makeDocCollabId(doc, 'content') - const ydocId = makeCollabYdocId(collabDoc) - const jsonId = makeCollabJsonId(collabDoc) - - expect(ydocId).toContain('doc999') - expect(jsonId).toContain('doc999') - }) - }) -}) diff --git a/foundations/core/packages/core/src/__tests__/collaborators.test.ts b/foundations/core/packages/core/src/__tests__/collaborators.test.ts deleted file mode 100644 index 3e69795765..0000000000 --- a/foundations/core/packages/core/src/__tests__/collaborators.test.ts +++ /dev/null @@ -1,201 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { getClassCollaborators } from '../collaborators' -import { ModelDb } from '../memdb' -import { Hierarchy } from '../hierarchy' -import core from '../component' -import type { Class, ClassCollaborators, Doc, Ref } from '../classes' - -describe('collaborators', () => { - let model: ModelDb - let hierarchy: Hierarchy - - beforeEach(() => { - model = new ModelDb(hierarchy) - hierarchy = new Hierarchy() - }) - - describe('getClassCollaborators', () => { - it('should return undefined when no collaborators found', () => { - const classRef = 'class:test.TestClass' as Ref> - - // Mock hierarchy to return empty ancestors - hierarchy.getAncestors = jest.fn().mockReturnValue([classRef]) - - // Mock model to return empty result - model.findAllSync = jest.fn().mockReturnValue([]) - - const result = getClassCollaborators(model, hierarchy, classRef) - - expect(result).toBeUndefined() - }) - - it('should return collaborators for direct class', () => { - const classRef = 'class:test.TestClass' as Ref> - const collaborators: ClassCollaborators = { - _id: 'collab1' as any, - _class: core.class.ClassCollaborators, - space: 'space1' as any, - modifiedOn: Date.now(), - modifiedBy: 'user1' as any, - attachedTo: classRef, - attachedToClass: core.class.Class, - collection: 'collaborators' - } as unknown as ClassCollaborators - - hierarchy.getAncestors = jest.fn().mockReturnValue([classRef]) - model.findAllSync = jest.fn().mockReturnValue([collaborators]) - - const result = getClassCollaborators(model, hierarchy, classRef) - - expect(result).toBe(collaborators) - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(model.findAllSync).toHaveBeenCalledWith(core.class.ClassCollaborators, { attachedTo: { $in: [classRef] } }) - }) - - it('should return collaborators from ancestor class', () => { - const childClass = 'class:test.ChildClass' as Ref> - const parentClass = 'class:test.ParentClass' as Ref> - const grandParentClass = core.class.Doc - - const parentCollaborators: ClassCollaborators = { - _id: 'collab2' as any, - _class: core.class.ClassCollaborators, - space: 'space1' as any, - modifiedOn: Date.now(), - modifiedBy: 'user1' as any, - attachedTo: parentClass, - attachedToClass: core.class.Class, - collection: 'collaborators' - } as unknown as ClassCollaborators - - hierarchy.getAncestors = jest.fn().mockReturnValue([childClass, parentClass, grandParentClass]) - model.findAllSync = jest.fn().mockReturnValue([parentCollaborators]) - - const result = getClassCollaborators(model, hierarchy, childClass) - - expect(result).toBe(parentCollaborators) - }) - - it('should return first matching ancestor collaborators', () => { - const childClass = 'class:test.ChildClass' as Ref> - const parentClass = 'class:test.ParentClass' as Ref> - const grandParentClass = 'class:test.GrandParentClass' as Ref> - - const parentCollaborators: ClassCollaborators = { - _id: 'collab3' as any, - _class: core.class.ClassCollaborators, - space: 'space1' as any, - modifiedOn: Date.now(), - modifiedBy: 'user1' as any, - attachedTo: parentClass, - attachedToClass: core.class.Class, - collection: 'collaborators' - } as unknown as ClassCollaborators - - const grandParentCollaborators: ClassCollaborators = { - _id: 'collab4' as any, - _class: core.class.ClassCollaborators, - space: 'space1' as any, - modifiedOn: Date.now(), - modifiedBy: 'user1' as any, - attachedTo: grandParentClass, - attachedToClass: core.class.Class, - collection: 'collaborators' - } as unknown as ClassCollaborators - - hierarchy.getAncestors = jest.fn().mockReturnValue([childClass, parentClass, grandParentClass]) - model.findAllSync = jest.fn().mockReturnValue([parentCollaborators, grandParentCollaborators]) - - const result = getClassCollaborators(model, hierarchy, childClass) - - // Should return parent collaborators (first in ancestor chain) - expect(result).toBe(parentCollaborators) - }) - - it('should handle single class with no ancestors', () => { - const classRef = core.class.Doc - - hierarchy.getAncestors = jest.fn().mockReturnValue([classRef]) - model.findAllSync = jest.fn().mockReturnValue([]) - - const result = getClassCollaborators(model, hierarchy, classRef) - - expect(result).toBeUndefined() - }) - - it('should handle empty ancestors list', () => { - const classRef = 'class:test.TestClass' as Ref> - - hierarchy.getAncestors = jest.fn().mockReturnValue([]) - model.findAllSync = jest.fn().mockReturnValue([]) - - const result = getClassCollaborators(model, hierarchy, classRef) - - expect(result).toBeUndefined() - }) - - it('should properly query model with $in operator', () => { - const childClass = 'class:test.ChildClass' as Ref> - const parentClass = 'class:test.ParentClass' as Ref> - const ancestors = [childClass, parentClass, core.class.Doc] - - hierarchy.getAncestors = jest.fn().mockReturnValue(ancestors) - model.findAllSync = jest.fn().mockReturnValue([]) - - getClassCollaborators(model, hierarchy, childClass) - - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(model.findAllSync).toHaveBeenCalledWith(core.class.ClassCollaborators, { attachedTo: { $in: ancestors } }) - }) - - it('should iterate through ancestors in order', () => { - const class1 = 'class:test.Class1' as Ref> - const class2 = 'class:test.Class2' as Ref> - const class3 = 'class:test.Class3' as Ref> - - const collab2: ClassCollaborators = { - _id: 'collab5' as any, - _class: core.class.ClassCollaborators, - space: 'space1' as any, - modifiedOn: Date.now(), - modifiedBy: 'user1' as any, - attachedTo: class2, - attachedToClass: core.class.Class, - collection: 'collaborators' - } as unknown as ClassCollaborators - - const collab3: ClassCollaborators = { - _id: 'collab6' as any, - _class: core.class.ClassCollaborators, - space: 'space1' as any, - modifiedOn: Date.now(), - modifiedBy: 'user1' as any, - attachedTo: class3, - attachedToClass: core.class.Class, - collection: 'collaborators' - } as unknown as ClassCollaborators - - hierarchy.getAncestors = jest.fn().mockReturnValue([class1, class2, class3]) - model.findAllSync = jest.fn().mockReturnValue([collab2, collab3]) - - const result = getClassCollaborators(model, hierarchy, class1) - - // Should return collab2 (class2 comes before class3 in ancestors) - expect(result).toBe(collab2) - }) - }) -}) diff --git a/foundations/core/packages/core/src/__tests__/common.test.ts b/foundations/core/packages/core/src/__tests__/common.test.ts deleted file mode 100644 index 32caf9e481..0000000000 --- a/foundations/core/packages/core/src/__tests__/common.test.ts +++ /dev/null @@ -1,283 +0,0 @@ -// -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// Copyright © 2021 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { groupByArray, groupByArrayAsync, flipSet } from '../common' - -describe('common utilities', () => { - describe('groupByArray', () => { - it('should group array items by key', () => { - const items = [ - { id: 1, category: 'A' }, - { id: 2, category: 'B' }, - { id: 3, category: 'A' }, - { id: 4, category: 'C' }, - { id: 5, category: 'B' } - ] - - const result = groupByArray(items, (item) => item.category) - - expect(result.size).toBe(3) - expect(result.get('A')).toEqual([ - { id: 1, category: 'A' }, - { id: 3, category: 'A' } - ]) - expect(result.get('B')).toEqual([ - { id: 2, category: 'B' }, - { id: 5, category: 'B' } - ]) - expect(result.get('C')).toEqual([{ id: 4, category: 'C' }]) - }) - - it('should handle empty array', () => { - const result = groupByArray([], (item) => item) - expect(result.size).toBe(0) - }) - - it('should handle single item', () => { - const items = [{ id: 1, type: 'test' }] - const result = groupByArray(items, (item) => item.type) - - expect(result.size).toBe(1) - expect(result.get('test')).toEqual([{ id: 1, type: 'test' }]) - }) - - it('should handle numeric keys', () => { - const items = [ - { value: 10, bucket: 1 }, - { value: 20, bucket: 2 }, - { value: 30, bucket: 1 }, - { value: 40, bucket: 3 } - ] - - const result = groupByArray(items, (item) => item.bucket) - - expect(result.size).toBe(3) - expect(result.get(1)).toHaveLength(2) - expect(result.get(2)).toHaveLength(1) - expect(result.get(3)).toHaveLength(1) - }) - - it('should handle all items with same key', () => { - const items = [ - { id: 1, status: 'active' }, - { id: 2, status: 'active' }, - { id: 3, status: 'active' } - ] - - const result = groupByArray(items, (item) => item.status) - - expect(result.size).toBe(1) - expect(result.get('active')).toHaveLength(3) - }) - - it('should handle complex key providers', () => { - const items = [ - { firstName: 'John', lastName: 'Doe' }, - { firstName: 'Jane', lastName: 'Doe' }, - { firstName: 'John', lastName: 'Smith' } - ] - - const result = groupByArray(items, (item) => item.lastName) - - expect(result.size).toBe(2) - expect(result.get('Doe')).toHaveLength(2) - expect(result.get('Smith')).toHaveLength(1) - }) - - it('should handle undefined keys', () => { - const items = [{ id: 1, tag: 'a' }, { id: 2 }, { id: 3, tag: 'a' }] - - const result = groupByArray(items, (item: any) => item.tag) - - expect(result.size).toBe(2) - expect(result.get('a')).toHaveLength(2) - expect(result.get(undefined)).toHaveLength(1) - }) - }) - - describe('groupByArrayAsync', () => { - it('should group array items by async key provider', async () => { - const items = [ - { id: 1, value: 10 }, - { id: 2, value: 20 }, - { id: 3, value: 15 }, - { id: 4, value: 25 } - ] - - const result = await groupByArrayAsync(items, async (item) => { - // Simulate async operation - await new Promise((resolve) => setTimeout(resolve, 1)) - return item.value > 15 ? 'high' : 'low' - }) - - expect(result.size).toBe(2) - expect(result.get('low')).toEqual([ - { id: 1, value: 10 }, - { id: 3, value: 15 } - ]) - expect(result.get('high')).toEqual([ - { id: 2, value: 20 }, - { id: 4, value: 25 } - ]) - }) - - it('should handle empty array', async () => { - const result = await groupByArrayAsync([], async (item) => item) - expect(result.size).toBe(0) - }) - - it('should handle async errors', async () => { - const items = [{ id: 1 }, { id: 2 }] - - await expect( - groupByArrayAsync(items, async (item) => { - if (item.id === 2) { - throw new Error('Async error') - } - return 'key' - }) - ).rejects.toThrow('Async error') - }) - - it('should handle single item', async () => { - const items = [{ id: 1, type: 'test' }] - const result = await groupByArrayAsync(items, async (item) => { - await new Promise((resolve) => setTimeout(resolve, 1)) - return item.type - }) - - expect(result.size).toBe(1) - expect(result.get('test')).toEqual([{ id: 1, type: 'test' }]) - }) - - it('should process items sequentially', async () => { - const order: number[] = [] - const items = [1, 2, 3, 4] - - await groupByArrayAsync(items, async (item) => { - order.push(item) - await new Promise((resolve) => setTimeout(resolve, 5 - item)) // Reverse delays - return item % 2 === 0 ? 'even' : 'odd' - }) - - // Should maintain order despite different delays - expect(order).toEqual([1, 2, 3, 4]) - }) - - it('should handle Promise rejections gracefully', async () => { - const items = [1, 2, 3] - - await expect( - groupByArrayAsync(items, async (item) => { - if (item === 2) { - return await Promise.reject(new Error('Failed on 2')) - } - return 'key' - }) - ).rejects.toThrow('Failed on 2') - }) - - it('should handle numeric keys', async () => { - const items = [{ value: 10 }, { value: 20 }, { value: 30 }] - - const result = await groupByArrayAsync(items, async (item) => { - await new Promise((resolve) => setTimeout(resolve, 1)) - return Math.floor(item.value / 10) - }) - - expect(result.size).toBe(3) - expect(result.get(1)).toHaveLength(1) - expect(result.get(2)).toHaveLength(1) - expect(result.get(3)).toHaveLength(1) - }) - }) - - describe('flipSet', () => { - it('should add item if not present', () => { - const set = new Set([1, 2, 3]) - const result = flipSet(set, 4) - - expect(result).toBe(set) // Should return same set - expect(result.has(4)).toBe(true) - expect(result.size).toBe(4) - }) - - it('should remove item if present', () => { - const set = new Set([1, 2, 3]) - const result = flipSet(set, 2) - - expect(result).toBe(set) // Should return same set - expect(result.has(2)).toBe(false) - expect(result.size).toBe(2) - }) - - it('should handle empty set', () => { - const set = new Set() - const result = flipSet(set, 1) - - expect(result.has(1)).toBe(true) - expect(result.size).toBe(1) - }) - - it('should handle string items', () => { - const set = new Set(['a', 'b', 'c']) - - flipSet(set, 'd') - expect(set.has('d')).toBe(true) - - flipSet(set, 'b') - expect(set.has('b')).toBe(false) - }) - - it('should handle object items', () => { - const obj1 = { id: 1 } - const obj2 = { id: 2 } - const set = new Set([obj1]) - - flipSet(set, obj2) - expect(set.has(obj2)).toBe(true) - expect(set.size).toBe(2) - - flipSet(set, obj1) - expect(set.has(obj1)).toBe(false) - expect(set.size).toBe(1) - }) - - it('should work with multiple flips', () => { - const set = new Set([1, 2]) - - flipSet(set, 3) // Add 3 - expect(set.size).toBe(3) - - flipSet(set, 3) // Remove 3 - expect(set.size).toBe(2) - - flipSet(set, 3) // Add 3 again - expect(set.size).toBe(3) - expect(set.has(3)).toBe(true) - }) - - it('should handle single element set', () => { - const set = new Set([42]) - - flipSet(set, 42) - expect(set.size).toBe(0) - - flipSet(set, 42) - expect(set.size).toBe(1) - }) - }) -}) diff --git a/foundations/core/packages/core/src/__tests__/connection.ts b/foundations/core/packages/core/src/__tests__/connection.ts deleted file mode 100644 index 93f2775f59..0000000000 --- a/foundations/core/packages/core/src/__tests__/connection.ts +++ /dev/null @@ -1,118 +0,0 @@ -// -// Copyright © 2020 Anticrm Platform Contributors. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { ClientConnectEvent, type DocChunk, generateId } from '..' -import type { Class, Doc, Domain, Ref, Timestamp } from '../classes' -import { type ClientConnection } from '../client' -import core from '../component' -import { Hierarchy } from '../hierarchy' -import { ModelDb, TxDb } from '../memdb' -import type { - DocumentQuery, - DomainResult, - FindResult, - SearchOptions, - SearchQuery, - SearchResult, - TxResult -} from '../storage' -import type { Tx } from '../tx' -import { DOMAIN_TX } from '../tx' -import { genMinModel } from './minmodel' - -export async function connect (handler: (tx: Tx) => void): Promise { - const txes = genMinModel() - - const hierarchy = new Hierarchy() - for (const tx of txes) hierarchy.tx(tx) - - const transactions = new TxDb(hierarchy) - const model = new ModelDb(hierarchy) - for (const tx of txes) { - await transactions.tx(tx) - await model.tx(tx) - } - - async function findAll (_class: Ref>, query: DocumentQuery): Promise> { - const domain = hierarchy.getClass(_class).domain - if (domain === DOMAIN_TX) return await transactions.findAll(_class, query) - return await model.findAll(_class, query) - } - - class ClientConnectionImpl implements ClientConnection { - isConnected = (): boolean => true - findAll = findAll - - handler?: (event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise - - set onConnect ( - handler: ((event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise) | undefined - ) { - this.handler = handler - void this.handler?.(ClientConnectEvent.Connected, '', {}) - } - - get onConnect (): ((event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise) | undefined { - return this.handler - } - - async searchFulltext (query: SearchQuery, options: SearchOptions): Promise { - return { docs: [] } - } - - pushHandler = (): void => {} - - async tx (tx: Tx): Promise { - if (tx.objectSpace === core.space.Model) { - hierarchy.tx(tx) - } - const result = await Promise.all([model.tx(tx), transactions.tx(tx)]) - return result[0] - } - - async domainRequest (): Promise { - return await Promise.resolve({ domain: 'test' as Domain, value: null }) - } - - async close (): Promise {} - - async loadChunk (domain: Domain, idx?: number): Promise { - return { - idx: -1, - docs: [], - finished: true - } - } - - async getDomainHash (domain: Domain): Promise { - return generateId() - } - - async closeChunk (idx: number): Promise {} - async loadDocs (domain: Domain, docs: Ref[]): Promise { - return [] - } - - async upload (domain: Domain, docs: Doc[]): Promise {} - async clean (domain: Domain, docs: Ref[]): Promise {} - async loadModel (last: Timestamp): Promise { - return txes - } - - async sendForceClose (): Promise {} - } - - return new ClientConnectionImpl() -} diff --git a/foundations/core/packages/core/src/__tests__/contexts.test.ts b/foundations/core/packages/core/src/__tests__/contexts.test.ts deleted file mode 100644 index ff93c75bfe..0000000000 --- a/foundations/core/packages/core/src/__tests__/contexts.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { MeasureMetricsContext } from '@hcengineering/measurements' - -describe('context tests', () => { - it('check withLog proper catch', async () => { - const ctx = new MeasureMetricsContext('test', {}) - - try { - await ctx.with( - 'failed op', - {}, - async () => { - throw new Error('failed') - }, - undefined, - { log: true } - ) - expect(true).toBe(false) - } catch (err: any) { - expect(err.message).toBe('failed') - } - }) -}) diff --git a/foundations/core/packages/core/src/__tests__/hierarchy.test.ts b/foundations/core/packages/core/src/__tests__/hierarchy.test.ts deleted file mode 100644 index b6cdf4c507..0000000000 --- a/foundations/core/packages/core/src/__tests__/hierarchy.test.ts +++ /dev/null @@ -1,1121 +0,0 @@ -// -// Copyright © 2020 Anticrm Platform Contributors. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { AnyAttribute, Class, Doc, Obj, Ref } from '../classes' -import { ClassifierKind, DOMAIN_MODEL } from '../classes' -import type { TxCreateDoc } from '../tx' -import { TxFactory } from '../tx' -import core from '../component' -import { Hierarchy } from '../hierarchy' -import * as Proxy from '../proxy' -import { genMinModel, test } from './minmodel' - -const txes = genMinModel() - -function prepare (): Hierarchy { - const hierarchy = new Hierarchy() - for (const tx of txes) hierarchy.tx(tx) - return hierarchy -} - -describe('hierarchy', () => { - it('should build hierarchy', async () => { - const hierarchy = prepare() - const ancestors = hierarchy.getAncestors(core.class.TxCreateDoc) - expect(ancestors).toContain(core.class.Tx) - }) - - it('isDerived', async () => { - const hierarchy = prepare() - const derived = hierarchy.isDerived(core.class.Space, core.class.Doc) - expect(derived).toBeTruthy() - const notDerived = hierarchy.isDerived(core.class.Space, core.class.Class) - expect(notDerived).not.toBeTruthy() - }) - - it('isImplements', async () => { - const hierarchy = prepare() - let isImplements = hierarchy.isImplements(test.class.Task, test.interface.WithState) - expect(isImplements).toBeTruthy() - - isImplements = hierarchy.isImplements(test.class.TaskCheckItem, test.interface.WithState) - expect(isImplements).toBeTruthy() - - const notImplements = hierarchy.isImplements(core.class.Space, test.interface.WithState) - expect(notImplements).not.toBeTruthy() - }) - - it('getClass', async () => { - const hierarchy = prepare() - const data = hierarchy.getClass(core.class.TxCreateDoc) - expect(data).toMatchObject((txes.find((p) => p.objectId === core.class.TxCreateDoc) as TxCreateDoc).attributes) - const notExistClass = 'class:test.MyClass' as Ref> - expect(() => hierarchy.getClass(notExistClass)).toThrowError('class not found: ' + notExistClass) - }) - - it('getDomain', async () => { - const hierarchy = prepare() - const txDomain = hierarchy.getDomain(core.class.TxCreateDoc) - expect(txDomain).toBe('tx') - const modelDomain = hierarchy.getDomain(core.class.Class) - expect(modelDomain).toBe('model') - }) - - it('should create Mixin proxy', async () => { - const spyProxy = jest.spyOn(Proxy, '_createMixinProxy') - const hierarchy = prepare() - - hierarchy.as(txes[0], test.mixin.TestMixin) - expect(spyProxy).toBeCalledTimes(1) - - hierarchy.as(txes[0], test.mixin.TestMixin) - expect(spyProxy).toBeCalledTimes(1) - - spyProxy.mockReset() - spyProxy.mockRestore() - }) - - it('should call static methods', async () => { - const spyToDoc = jest.spyOn(Proxy, '_toDoc') - Hierarchy.toDoc(txes[0]) - expect(spyToDoc).toBeCalledTimes(1) - spyToDoc.mockReset() - spyToDoc.mockRestore() - - const spyMixinClass = jest.spyOn(Proxy, '_mixinClass') - Hierarchy.mixinClass(txes[0]) - expect(spyMixinClass).toBeCalledTimes(1) - - spyMixinClass.mockImplementationOnce(() => undefined).mockImplementationOnce(() => test.mixin.TestMixin) - let result = Hierarchy.mixinOrClass(txes[0]) - expect(result).toStrictEqual(txes[0]._class) - result = Hierarchy.mixinOrClass(txes[0]) - expect(result).toStrictEqual(test.mixin.TestMixin) - expect(spyMixinClass).toBeCalledTimes(3) - - spyMixinClass.mockReset() - spyMixinClass.mockRestore() - }) - - // Memory optimization tests - ancestors stored as array - it('getAncestors should return array directly', async () => { - const hierarchy = prepare() - const ancestors = hierarchy.getAncestors(core.class.TxCreateDoc) - - // Verify it's an array - expect(Array.isArray(ancestors)).toBeTruthy() - - // Verify it contains expected ancestors - expect(ancestors).toContain(core.class.TxCreateDoc) - expect(ancestors).toContain(core.class.TxCUD) - expect(ancestors).toContain(core.class.Tx) - expect(ancestors).toContain(core.class.Doc) - expect(ancestors).toContain(core.class.Obj) - - // Verify order is consistent - const indexTx = ancestors.indexOf(core.class.Tx) - const indexDoc = ancestors.indexOf(core.class.Doc) - expect(indexDoc).toBeGreaterThan(indexTx) - }) - - it('isDerived should work with array-based ancestors', async () => { - const hierarchy = prepare() - - // Test various inheritance chains - expect(hierarchy.isDerived(core.class.TxCreateDoc, core.class.Tx)).toBeTruthy() - expect(hierarchy.isDerived(core.class.TxCreateDoc, core.class.TxCUD)).toBeTruthy() - expect(hierarchy.isDerived(core.class.TxCreateDoc, core.class.Doc)).toBeTruthy() - expect(hierarchy.isDerived(core.class.TxCreateDoc, core.class.Obj)).toBeTruthy() - - // Test self-derivation (class is in its own ancestors) - expect(hierarchy.isDerived(core.class.TxCreateDoc, core.class.TxCreateDoc)).toBeTruthy() - - // Test non-derived classes - expect(hierarchy.isDerived(core.class.TxCreateDoc, core.class.Space)).toBeFalsy() - expect(hierarchy.isDerived(core.class.Space, core.class.Tx)).toBeFalsy() - - // Test with mixins - expect(hierarchy.isDerived(test.class.TestComment, core.class.AttachedDoc)).toBeTruthy() - expect(hierarchy.isDerived(test.mixin.TaskMixinTodos, test.class.Task)).toBeTruthy() - }) - - it('should handle deep inheritance chains efficiently', async () => { - const hierarchy = prepare() - - // TxCreateDoc has a chain: TxCreateDoc -> TxCUD -> Tx -> Doc -> Obj - const ancestors = hierarchy.getAncestors(core.class.TxCreateDoc) - expect(ancestors.length).toBeGreaterThanOrEqual(5) - - // All intermediate classes should be present - expect(hierarchy.isDerived(core.class.TxCreateDoc, core.class.TxCUD)).toBeTruthy() - expect(hierarchy.isDerived(core.class.TxCUD, core.class.Tx)).toBeTruthy() - expect(hierarchy.isDerived(core.class.Tx, core.class.Doc)).toBeTruthy() - expect(hierarchy.isDerived(core.class.Doc, core.class.Obj)).toBeTruthy() - }) - - // Classifier properties tests - Map-based storage - it('getClassifierProp and setClassifierProp should work with Map', async () => { - const hierarchy = prepare() - - // Set a property - hierarchy.setClassifierProp(core.class.Space, 'testProp', 'testValue') - - // Get the property - const value = hierarchy.getClassifierProp(core.class.Space, 'testProp') - expect(value).toBe('testValue') - - // Update the property - hierarchy.setClassifierProp(core.class.Space, 'testProp', 'updatedValue') - const updatedValue = hierarchy.getClassifierProp(core.class.Space, 'testProp') - expect(updatedValue).toBe('updatedValue') - }) - - it('should handle multiple properties per classifier', async () => { - const hierarchy = prepare() - - // Set multiple properties - hierarchy.setClassifierProp(core.class.Space, 'prop1', 'value1') - hierarchy.setClassifierProp(core.class.Space, 'prop2', 'value2') - hierarchy.setClassifierProp(core.class.Space, 'prop3', 42) - hierarchy.setClassifierProp(core.class.Space, 'prop4', { nested: 'object' }) - - // Verify all properties are stored correctly - expect(hierarchy.getClassifierProp(core.class.Space, 'prop1')).toBe('value1') - expect(hierarchy.getClassifierProp(core.class.Space, 'prop2')).toBe('value2') - expect(hierarchy.getClassifierProp(core.class.Space, 'prop3')).toBe(42) - expect(hierarchy.getClassifierProp(core.class.Space, 'prop4')).toEqual({ nested: 'object' }) - - // Verify undefined for non-existent property - expect(hierarchy.getClassifierProp(core.class.Space, 'nonExistent')).toBeUndefined() - }) - - it('should isolate properties between different classifiers', async () => { - const hierarchy = prepare() - - // Set properties on different classifiers - hierarchy.setClassifierProp(core.class.Space, 'name', 'Space') - hierarchy.setClassifierProp(core.class.Doc, 'name', 'Doc') - hierarchy.setClassifierProp(test.class.Task, 'name', 'Task') - - // Verify isolation - expect(hierarchy.getClassifierProp(core.class.Space, 'name')).toBe('Space') - expect(hierarchy.getClassifierProp(core.class.Doc, 'name')).toBe('Doc') - expect(hierarchy.getClassifierProp(test.class.Task, 'name')).toBe('Task') - }) - - it('should handle property updates without creating new objects', async () => { - const hierarchy = prepare() - - // Set initial value - hierarchy.setClassifierProp(core.class.Space, 'counter', 0) - - // Update multiple times (testing that we're not creating new objects each time) - for (let i = 1; i <= 100; i++) { - hierarchy.setClassifierProp(core.class.Space, 'counter', i) - } - - // Verify final value - expect(hierarchy.getClassifierProp(core.class.Space, 'counter')).toBe(100) - }) - - // Edge cases and integration tests - it('should handle interface implementation checks correctly', async () => { - const hierarchy = prepare() - - // Task implements DummyWithState which extends WithState - expect(hierarchy.isImplements(test.class.Task, test.interface.WithState)).toBeTruthy() - expect(hierarchy.isImplements(test.class.Task, test.interface.DummyWithState)).toBeTruthy() - - // TaskCheckItem directly implements WithState - expect(hierarchy.isImplements(test.class.TaskCheckItem, test.interface.WithState)).toBeTruthy() - - // Negative cases - expect(hierarchy.isImplements(core.class.Space, test.interface.WithState)).toBeFalsy() - }) - - it('should maintain consistency after multiple hierarchy operations', async () => { - const hierarchy = prepare() - - // Perform multiple operations - const ancestors1 = hierarchy.getAncestors(test.class.Task) - const isDerived1 = hierarchy.isDerived(test.class.Task, core.class.Doc) - - // Set some properties - hierarchy.setClassifierProp(test.class.Task, 'test', 'value') - - // Verify operations still work correctly - const ancestors2 = hierarchy.getAncestors(test.class.Task) - const isDerived2 = hierarchy.isDerived(test.class.Task, core.class.Doc) - - expect(ancestors1).toEqual(ancestors2) - expect(isDerived1).toBe(isDerived2) - expect(isDerived2).toBeTruthy() - }) - - it('should handle getDescendants correctly', async () => { - const hierarchy = prepare() - - // Get descendants of Doc (should include many classes) - const descendants = hierarchy.getDescendants(core.class.Doc) - - expect(descendants).toContain(core.class.Space) - expect(descendants).toContain(core.class.Tx) - expect(descendants).toContain(test.class.Task) - expect(Array.isArray(descendants)).toBeTruthy() - }) - - it('should work with getBaseClass', async () => { - const hierarchy = prepare() - - // Get base class of a mixin - const baseClass = hierarchy.getBaseClass(test.mixin.TaskMixinTodos) - expect(baseClass).toBe(test.class.Task) - - // Get base class of a regular class (should return itself) - const baseClass2 = hierarchy.getBaseClass(test.class.Task) - expect(baseClass2).toBe(test.class.Task) - }) - - it('should handle getAllAttributes correctly', async () => { - const hierarchy = prepare() - - // Get all attributes for a class - const attributes = hierarchy.getAllAttributes(core.class.TxCreateDoc) - - // Should return a Map - expect(attributes instanceof Map).toBeTruthy() - - // Test with to parameter - const attributesTo = hierarchy.getAllAttributes(core.class.TxCreateDoc, core.class.Tx) - expect(attributesTo instanceof Map).toBeTruthy() - }) - - it('should maintain immutability of returned ancestors array', async () => { - const hierarchy = prepare() - - // Get ancestors - const ancestors = hierarchy.getAncestors(test.class.Task) - const originalLength = ancestors.length - - // The returned array should be the internal array, but modifying it shouldn't break hierarchy - // (This is a trade-off for memory optimization - callers should treat it as read-only) - expect(ancestors.length).toBe(originalLength) - expect(ancestors).toContain(core.class.Doc) - }) - - it('should handle performance for multiple isDerived checks', async () => { - const hierarchy = prepare() - - // Perform many isDerived checks to ensure array-based lookup is performant - const startTime = Date.now() - for (let i = 0; i < 1000; i++) { - hierarchy.isDerived(core.class.TxCreateDoc, core.class.Tx) - hierarchy.isDerived(test.class.Task, core.class.Doc) - hierarchy.isDerived(test.class.TaskCheckItem, core.class.AttachedDoc) - } - const endTime = Date.now() - - // Should complete in reasonable time (< 100ms for 3000 checks) - expect(endTime - startTime).toBeLessThan(100) - }) - - // Additional comprehensive tests for better coverage - - it('should handle findClass and hasClass correctly', async () => { - const hierarchy = prepare() - - // findClass should return class or undefined - const foundClass = hierarchy.findClass(core.class.Space) - expect(foundClass).toBeDefined() - expect(foundClass?._id).toBe(core.class.Space) - - const notFoundClass = hierarchy.findClass('class:NonExistent' as Ref>) - expect(notFoundClass).toBeUndefined() - - // hasClass should return boolean - expect(hierarchy.hasClass(core.class.Space)).toBeTruthy() - expect(hierarchy.hasClass('class:NonExistent' as Ref>)).toBeFalsy() - - // Interface should not be considered a class - expect(hierarchy.hasClass(test.interface.WithState as any)).toBeFalsy() - }) - - it('should handle getClassOrInterface correctly', async () => { - const hierarchy = prepare() - - // Should get a class - const spaceClass = hierarchy.getClassOrInterface(core.class.Space) - expect(spaceClass._id).toBe(core.class.Space) - - // Should get an interface - const withStateInterface = hierarchy.getClassOrInterface(test.interface.WithState as any) - expect(withStateInterface._id).toBe(test.interface.WithState) - - // Should throw for non-existent - expect(() => hierarchy.getClassOrInterface('class:NonExistent' as Ref>)).toThrowError( - 'class not found: class:NonExistent' - ) - }) - - it('should handle getInterface correctly', async () => { - const hierarchy = prepare() - - // Should get interface - const withStateInterface = hierarchy.getInterface(test.interface.WithState) - expect(withStateInterface._id).toBe(test.interface.WithState) - - // Should throw for non-existent interface - expect(() => hierarchy.getInterface('interface:NonExistent' as any)).toThrowError( - 'interface not found: interface:NonExistent' - ) - - // Should throw for class (not interface) - expect(() => hierarchy.getInterface(core.class.Space as any)).toThrowError() - }) - - it('should handle isMixin correctly', async () => { - const hierarchy = prepare() - - // Should identify mixins - expect(hierarchy.isMixin(test.mixin.TestMixin)).toBeTruthy() - expect(hierarchy.isMixin(test.mixin.TaskMixinTodos)).toBeTruthy() - expect(hierarchy.isMixin(core.class.AttachedDoc)).toBeTruthy() - - // Should not identify classes as mixins - expect(hierarchy.isMixin(core.class.Space)).toBeFalsy() - expect(hierarchy.isMixin(test.class.Task)).toBeFalsy() - - // Should return false for non-existent - expect(hierarchy.isMixin('mixin:NonExistent' as any)).toBeFalsy() - }) - - it('should handle as and asIf with mixins', async () => { - const hierarchy = prepare() - - const doc = { - _id: 'doc1' as any, - _class: core.class.Doc, - space: 'space1' as any, - modifiedOn: 0, - modifiedBy: 'user1' as any - } - - // as should return a proxy - const withMixin = hierarchy.as(doc, test.mixin.TestMixin) - expect(withMixin).toBeDefined() - expect(withMixin._id).toBe(doc._id) - - // asIf should return undefined if mixin not present - const asIfResult = hierarchy.asIf(doc, test.mixin.TestMixin) - expect(asIfResult).toBeUndefined() - - // asIf should return undefined for undefined doc - expect(hierarchy.asIf(undefined, test.mixin.TestMixin)).toBeUndefined() - }) - - it('should handle asIfArray correctly', async () => { - const hierarchy = prepare() - - const docs = [ - { _id: 'doc1' as any, _class: core.class.Doc, space: 'space1' as any, modifiedOn: 0, modifiedBy: 'user1' as any }, - { _id: 'doc2' as any, _class: core.class.Doc, space: 'space1' as any, modifiedOn: 0, modifiedBy: 'user1' as any } - ] - - // Should return empty array if no docs have the mixin - const result = hierarchy.asIfArray(docs, test.mixin.TestMixin) - expect(Array.isArray(result)).toBeTruthy() - expect(result.length).toBe(0) - }) - - it('should handle classHierarchyMixin correctly', async () => { - const hierarchy = prepare() - - // Should find mixin in class hierarchy - const result = hierarchy.classHierarchyMixin(test.mixin.TaskMixinTodos, test.mixin.TestMixin) - // May be undefined if mixin not in hierarchy - expect(result === undefined || typeof result === 'object').toBeTruthy() - - // Test with filter - const filtered = hierarchy.classHierarchyMixin(test.class.Task, test.mixin.TestMixin, (m) => m !== undefined) - expect(filtered === undefined || typeof filtered === 'object').toBeTruthy() - }) - - it('should handle findClassOrMixinMixin correctly', async () => { - const hierarchy = prepare() - - const doc = { - _id: 'doc1' as any, - _class: test.class.Task, - space: 'space1' as any, - modifiedOn: 0, - modifiedBy: 'user1' as any - } - - const result = hierarchy.findClassOrMixinMixin(doc, test.mixin.TestMixin) - // May be undefined if not found - expect(result === undefined || typeof result === 'object').toBeTruthy() - }) - - it('should handle findMixinMixins correctly', async () => { - const hierarchy = prepare() - - const doc = { - _id: 'doc1' as any, - _class: test.class.Task, - space: 'space1' as any, - modifiedOn: 0, - modifiedBy: 'user1' as any - } - - const results = hierarchy.findMixinMixins(doc, test.mixin.TestMixin) - expect(Array.isArray(results)).toBeTruthy() - }) - - it('should handle findAllMixins correctly', async () => { - const hierarchy = prepare() - - const doc = { - _id: 'doc1' as any, - _class: test.class.Task, - space: 'space1' as any, - modifiedOn: 0, - modifiedBy: 'user1' as any - } - - const mixins = hierarchy.findAllMixins(doc) - expect(Array.isArray(mixins)).toBeTruthy() - }) - - it('should handle findDomain correctly', async () => { - const hierarchy = prepare() - - // Should find domain - const domain = hierarchy.findDomain(core.class.Space) - expect(domain).toBe('model') - - // Should return undefined for non-existent class - const noDomain = hierarchy.findDomain('class:NonExistent' as any) - expect(noDomain).toBeUndefined() - }) - - it('should handle getParentClass correctly', async () => { - const hierarchy = prepare() - - // Should get parent class with same domain - const parent = hierarchy.getParentClass(core.class.Space) - expect(parent).toBeDefined() - // Parent should have same domain or be the class itself - const parentDomain = hierarchy.findDomain(parent) - expect(parentDomain === 'model' || parent === core.class.Space).toBeTruthy() - }) - - it('should handle getAttribute correctly', async () => { - const hierarchy = prepare() - - // Should throw for non-existent attribute - expect(() => hierarchy.getAttribute(core.class.Space, 'nonExistentAttr')).toThrowError( - 'attribute not found: nonExistentAttr' - ) - }) - - it('should handle findAttribute correctly', async () => { - const hierarchy = prepare() - - // Should return undefined for non-existent attribute - const attr = hierarchy.findAttribute(core.class.Space, 'nonExistentAttr') - expect(attr).toBeUndefined() - }) - - it('should handle getOwnAttributes correctly', async () => { - const hierarchy = prepare() - - // Should return Map of attributes - const attrs = hierarchy.getOwnAttributes(core.class.Space) - expect(attrs instanceof Map).toBeTruthy() - }) - - it('should handle updateLookupMixin correctly', async () => { - const hierarchy = prepare() - - const doc = { - _id: 'doc1' as any, - _class: test.class.Task, - space: 'space1' as any, - modifiedOn: 0, - modifiedBy: 'user1' as any - } - - // Without $lookup - const result1 = hierarchy.updateLookupMixin(test.class.Task, doc as any) - expect(result1).toBeDefined() - - // With $lookup - const docWithLookup = { - ...doc, - $lookup: {} - } - const result2 = hierarchy.updateLookupMixin(test.class.Task, docWithLookup as any) - expect(result2).toBeDefined() - }) - - it('should handle clone correctly', async () => { - const hierarchy = prepare() - - const obj = { - _id: 'doc1' as any, - _class: core.class.Doc, - space: 'space1' as any, - modifiedOn: 0, - modifiedBy: 'user1' as any, - nested: { - value: 'test' - } - } - - const cloned = hierarchy.clone(obj) - expect(cloned).toEqual(obj) - expect(cloned).not.toBe(obj) - expect(cloned.nested).not.toBe(obj.nested) - }) - - it('should handle domains correctly', async () => { - const hierarchy = prepare() - - const domains = hierarchy.domains() - expect(Array.isArray(domains)).toBeTruthy() - expect(domains).toContain('model') - expect(domains).toContain('tx') - - // Should not have duplicates - const uniqueDomains = [...new Set(domains)] - expect(domains.length).toBe(uniqueDomains.length) - }) - - it('should handle getAncestors error case', async () => { - const hierarchy = prepare() - - // Should throw for non-existent class - expect(() => hierarchy.getAncestors('class:NonExistent' as any)).toThrowError( - 'ancestors not found: class:NonExistent' - ) - }) - - it('should handle getDescendants error case', async () => { - const hierarchy = prepare() - - // Should throw for non-existent class - expect(() => hierarchy.getDescendants('class:NonExistent' as any)).toThrowError( - 'descendants not found: class:NonExistent' - ) - }) - - it('should handle getDomain error case', async () => { - const hierarchy = prepare() - - // Should throw for class without domain - expect(() => hierarchy.getDomain('class:NonExistent' as any)).toThrowError('domain not found: class:NonExistent') - }) - - it('should handle static hasMixin correctly', async () => { - const doc = { - _id: 'doc1' as any, - _class: test.class.Task, - space: 'space1' as any, - modifiedOn: 0, - modifiedBy: 'user1' as any, - [test.mixin.TestMixin]: { arr: [] } - } - - expect(Hierarchy.hasMixin(doc, test.mixin.TestMixin)).toBeTruthy() - expect(Hierarchy.hasMixin(doc, 'mixin:Other' as any)).toBeFalsy() - }) - - it('should maintain descendants list correctly', async () => { - const hierarchy = prepare() - - // Doc should have many descendants - const docDescendants = hierarchy.getDescendants(core.class.Doc) - expect(docDescendants.length).toBeGreaterThan(5) - - // Obj should have even more descendants (everything) - const objDescendants = hierarchy.getDescendants(core.class.Obj) - expect(objDescendants.length).toBeGreaterThanOrEqual(docDescendants.length) - - // TxCreateDoc should have fewer descendants (possibly none) - const txCreateDescendants = hierarchy.getDescendants(core.class.TxCreateDoc) - expect(Array.isArray(txCreateDescendants)).toBeTruthy() - }) - - it('should handle interface extends chains', async () => { - const hierarchy = prepare() - - // DummyWithState extends WithState - const dummyAncestors = hierarchy.getAncestors(test.interface.DummyWithState) - expect(dummyAncestors).toContain(test.interface.WithState) - }) - - it('should handle complex mixin hierarchies', async () => { - const hierarchy = prepare() - - // TaskMixinTodos extends Task - expect(hierarchy.isDerived(test.mixin.TaskMixinTodos, test.class.Task)).toBeTruthy() - expect(hierarchy.isDerived(test.mixin.TaskMixinTodos, core.class.Doc)).toBeTruthy() - - // Should identify as mixin - expect(hierarchy.isMixin(test.mixin.TaskMixinTodos)).toBeTruthy() - - // Base class should be Task - const baseClass = hierarchy.getBaseClass(test.mixin.TaskMixinTodos) - expect(baseClass).toBe(test.class.Task) - }) - - it('should handle getAllAttributes with traverse callback', async () => { - const hierarchy = prepare() - - const traversed: Array<{ name: string, attrId: string }> = [] - const attributes = hierarchy.getAllAttributes(core.class.TxCreateDoc, undefined, (name, attr) => { - traversed.push({ name, attrId: attr._id }) - }) - - expect(attributes instanceof Map).toBeTruthy() - // If there are attributes, traverse should have been called - if (attributes.size > 0) { - expect(traversed.length).toBeGreaterThan(0) - } - }) - - it('should handle property deletions implicitly through cache invalidation', async () => { - const hierarchy = prepare() - - // Set a property - hierarchy.setClassifierProp(core.class.Space, 'cached', 'value') - expect(hierarchy.getClassifierProp(core.class.Space, 'cached')).toBe('value') - - // Properties persist across reads - expect(hierarchy.getClassifierProp(core.class.Space, 'cached')).toBe('value') - }) - - it('should verify memory optimization: no duplicate ancestor storage', async () => { - const hierarchy = prepare() - - // Get ancestors for multiple classes - const ancestors1 = hierarchy.getAncestors(core.class.TxCreateDoc) - const ancestors2 = hierarchy.getAncestors(core.class.TxUpdateDoc) - const ancestors3 = hierarchy.getAncestors(core.class.TxRemoveDoc) - - // All should share some common ancestors - const commonAncestors = ancestors1.filter((a) => ancestors2.includes(a) && ancestors3.includes(a)) - - expect(commonAncestors).toContain(core.class.TxCUD) - expect(commonAncestors).toContain(core.class.Tx) - expect(commonAncestors).toContain(core.class.Doc) - - // Verify arrays are distinct objects (not shared references) - expect(ancestors1).not.toBe(ancestors2) - expect(ancestors2).not.toBe(ancestors3) - }) - - // Additional tests for higher coverage - - it('should handle classHierarchyMixin with filter that returns false', async () => { - const hierarchy = prepare() - - // Create a class that has TestMixin - const clazz = hierarchy.getClass(core.class.Doc) - // Apply mixin to class - const withMixin = hierarchy.as(clazz, test.mixin.TestMixin) - expect(withMixin).toBeDefined() - - // Test with filter that returns false - const result = hierarchy.classHierarchyMixin( - core.class.Doc, - test.mixin.TestMixin, - (m) => false // Filter that always returns false - ) - expect(result).toBeUndefined() - }) - - it('should handle findClassOrMixinMixin with document having mixins', async () => { - const hierarchy = prepare() - - // Create a document with a mixin property - const doc = { - _id: 'doc1' as any, - _class: test.class.Task, - space: 'space1' as any, - modifiedOn: 0, - modifiedBy: 'user1' as any, - [test.mixin.TestMixin]: { arr: ['item1'] } - } - - const result = hierarchy.findClassOrMixinMixin(doc, test.mixin.TestMixin) - expect(result === undefined || typeof result === 'object').toBeTruthy() - }) - - it('should handle findMixinMixins with document having multiple mixins', async () => { - const hierarchy = prepare() - - // Create classes with mixins - const taskClass = hierarchy.getClass(test.class.Task) - // Test that class can have mixin applied - hierarchy.as(taskClass, test.mixin.TestMixin) - - // Create document with mixin applied - const doc = { - _id: 'doc1' as any, - _class: test.class.Task, - space: 'space1' as any, - modifiedOn: 0, - modifiedBy: 'user1' as any, - [test.mixin.TestMixin]: { arr: ['item1'] } - } - - const results = hierarchy.findMixinMixins(doc, test.mixin.TestMixin) - expect(Array.isArray(results)).toBeTruthy() - }) - - it('should handle findAllMixins with document having mixins', async () => { - const hierarchy = prepare() - - // Create document with mixins - const doc = { - _id: 'doc1' as any, - _class: test.class.Task, - space: 'space1' as any, - modifiedOn: 0, - modifiedBy: 'user1' as any, - [test.mixin.TestMixin]: { arr: ['item1'] } - } - - const mixins = hierarchy.findAllMixins(doc) - expect(Array.isArray(mixins)).toBeTruthy() - if (mixins.length > 0) { - expect(mixins).toContain(test.mixin.TestMixin) - } - }) - - it('should handle tx with TxMixin', async () => { - const hierarchy = new Hierarchy() - // Build basic hierarchy - for (const tx of txes) { - hierarchy.tx(tx) - } - - // Create a TxMixin transaction - const txFactory = new TxFactory(core.account.System) - const mixinTx = txFactory.createTxMixin( - core.class.Space as any, - core.class.Class as any, - core.space.Model, - test.mixin.TestMixin, - { arr: ['test'] } - ) - - // Apply the mixin transaction - hierarchy.tx(mixinTx) - - // Verify the mixin was applied - const spaceClass = hierarchy.getClass(core.class.Space) - expect(spaceClass).toBeDefined() - }) - - it('should handle txUpdateDoc with Attribute', async () => { - const hierarchy = new Hierarchy() - // Build basic hierarchy - for (const tx of txes) { - hierarchy.tx(tx) - } - - // Create an attribute first - const txFactory = new TxFactory(core.account.System) - const attrId = 'attr:test' as Ref - - const createAttrTx = txFactory.createTxCreateDoc( - core.class.Attribute, - core.space.Model, - { - attributeOf: core.class.Space, - name: 'testAttr', - type: { _class: 'class:core.Type' as any } - }, - attrId - ) - - hierarchy.tx(createAttrTx) - - // Now update the attribute - const updateAttrTx = txFactory.createTxUpdateDoc(core.class.Attribute, core.space.Model, attrId, { - name: 'updatedAttr' - }) - - hierarchy.tx(updateAttrTx) - - // Verify the attribute was updated - const attr = hierarchy.findAttribute(core.class.Space, 'updatedAttr') - expect(attr).toBeDefined() - }) - - it('should handle txRemoveDoc with Attribute', async () => { - const hierarchy = new Hierarchy() - // Build basic hierarchy - for (const tx of txes) { - hierarchy.tx(tx) - } - - // Create an attribute first - const txFactory = new TxFactory(core.account.System) - const attrId = 'attr:test' as Ref - - const createAttrTx = txFactory.createTxCreateDoc( - core.class.Attribute, - core.space.Model, - { - attributeOf: core.class.Space, - name: 'testAttr', - type: { _class: 'class:core.Type' as any } - }, - attrId - ) - - hierarchy.tx(createAttrTx) - - // Verify attribute exists - let attr = hierarchy.findAttribute(core.class.Space, 'testAttr') - expect(attr).toBeDefined() - - // Now remove the attribute - const removeAttrTx = txFactory.createTxRemoveDoc(core.class.Attribute, core.space.Model, attrId) - - hierarchy.tx(removeAttrTx) - - // Verify the attribute was removed - attr = hierarchy.findAttribute(core.class.Space, 'testAttr') - expect(attr).toBeUndefined() - }) - - it('should handle txUpdateDoc with Classifier', async () => { - const hierarchy = new Hierarchy() - // Build basic hierarchy - for (const tx of txes) { - hierarchy.tx(tx) - } - - const txFactory = new TxFactory(core.account.System) - - // Update a classifier - const updateClassTx = txFactory.createTxUpdateDoc(core.class.Class, core.space.Model, core.class.Space, { - label: 'Updated Space' as any - }) - - hierarchy.tx(updateClassTx) - - const spaceClass = hierarchy.getClass(core.class.Space) - expect(spaceClass.label).toBe('Updated Space' as any) - }) - - it('should handle txRemoveDoc with Classifier', async () => { - const hierarchy = new Hierarchy() - // Build basic hierarchy - for (const tx of txes) { - hierarchy.tx(tx) - } - - const txFactory = new TxFactory(core.account.System) - - // Create a new class - const newClassId = 'class:test.NewClass' as Ref> - const createClassTx = txFactory.createTxCreateDoc( - core.class.Class, - core.space.Model, - { - label: 'NewClass' as any, - extends: core.class.Doc, - kind: ClassifierKind.CLASS, - domain: DOMAIN_MODEL - }, - newClassId - ) - - hierarchy.tx(createClassTx) - - // Verify class exists - expect(hierarchy.hasClass(newClassId)).toBeTruthy() - - // Remove the class - const removeClassTx = txFactory.createTxRemoveDoc(core.class.Class, core.space.Model, newClassId) - - hierarchy.tx(removeClassTx) - - // Verify class was removed - expect(hierarchy.hasClass(newClassId)).toBeFalsy() - }) - - it('should handle updateLookupMixin with lookup containing mixins', async () => { - const hierarchy = prepare() - - const doc = { - _id: 'doc1' as any, - _class: test.class.Task, - space: 'space1' as any, - modifiedOn: 0, - modifiedBy: 'user1' as any, - $lookup: { - space: { - _id: 'space1' as any, - _class: core.class.Space, - name: 'Test Space', - description: '', - private: false, - members: [], - archived: false, - modifiedOn: 0, - modifiedBy: 'user1' as any - } - } - } - - const options = { - lookup: { - space: core.class.Space - } - } - - const result = hierarchy.updateLookupMixin(test.class.Task, doc as any, options as any) - expect(result).toBeDefined() - expect(result.$lookup).toBeDefined() - }) - - it('should handle updateLookupMixin with _id lookup containing mixins', async () => { - const hierarchy = prepare() - - const doc = { - _id: 'doc1' as any, - _class: test.class.Task, - space: 'space1' as any, - modifiedOn: 0, - modifiedBy: 'user1' as any, - $lookup: { - _id: { - _class: core.class.Space, - _id: 'space1' as any, - name: 'Test', - description: '', - private: false, - members: [], - archived: false, - modifiedOn: 0, - modifiedBy: 'user1' as any - } - } - } - - const options = { - lookup: { - _id: { - _class: test.mixin.TestMixin - } - } - } - - const result = hierarchy.updateLookupMixin(test.class.Task, doc as any, options as any) - expect(result).toBeDefined() - }) - - it('should handle updateLookupMixin with array in _id lookup', async () => { - const hierarchy = prepare() - - const doc = { - _id: 'doc1' as any, - _class: test.class.Task, - space: 'space1' as any, - modifiedOn: 0, - modifiedBy: 'user1' as any, - $lookup: { - _id: [ - { - _class: core.class.Space, - _id: 'space1' as any, - name: 'Test', - description: '', - private: false, - members: [], - archived: false, - modifiedOn: 0, - modifiedBy: 'user1' as any - } - ] - } - } - - const options = { - lookup: { - _id: { - _class: [test.mixin.TestMixin] - } - } - } - - const result = hierarchy.updateLookupMixin(test.class.Task, doc as any, options as any) - expect(result).toBeDefined() - }) - - it('should handle updateLookupMixin with null lookup value', async () => { - const hierarchy = prepare() - - const doc = { - _id: 'doc1' as any, - _class: test.class.Task, - space: 'space1' as any, - modifiedOn: 0, - modifiedBy: 'user1' as any, - $lookup: { - space: null - } - } - - const options = { - lookup: { - space: test.mixin.TestMixin - } - } - - const result = hierarchy.updateLookupMixin(test.class.Task, doc as any, options as any) - expect(result).toBeDefined() - expect(result.$lookup?.space).toBeNull() - }) - - it('should handle tx with non-matching class', async () => { - const hierarchy = new Hierarchy() - // Build basic hierarchy - for (const tx of txes) { - hierarchy.tx(tx) - } - - const txFactory = new TxFactory(core.account.System) - - // Create a transaction for a non-classifier class (Space) - const spaceTx = txFactory.createTxCreateDoc(core.class.Space, core.space.Model, { - name: 'Test Space', - description: '', - private: false, - members: [], - archived: false - }) - - // This should not throw, just handle gracefully - hierarchy.tx(spaceTx) - expect(true).toBeTruthy() - }) -}) diff --git a/foundations/core/packages/core/src/__tests__/lang.test.ts b/foundations/core/packages/core/src/__tests__/lang.test.ts deleted file mode 100644 index cf96c6dcf7..0000000000 --- a/foundations/core/packages/core/src/__tests__/lang.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { makeLocalesTest } from '@hcengineering/platform' - -it( - 'Locales are equale', - makeLocalesTest((lang) => import(`../../lang/${lang}.json`)) -) diff --git a/foundations/core/packages/core/src/__tests__/limiter-edge-cases.test.ts b/foundations/core/packages/core/src/__tests__/limiter-edge-cases.test.ts deleted file mode 100644 index 395e3fbfe8..0000000000 --- a/foundations/core/packages/core/src/__tests__/limiter-edge-cases.test.ts +++ /dev/null @@ -1,380 +0,0 @@ -// -// Copyright © 2023 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { RateLimiter, TimeRateLimiter } from '../utils' - -describe('RateLimiter and TimeRateLimiter - Advanced Edge Cases', () => { - describe('RateLimiter - Memory and Resource Management', () => { - it('should not leak memory in notify array', async () => { - const limiter = new RateLimiter(1) - const mockFn = jest.fn().mockImplementation(async () => { - await new Promise((resolve) => setTimeout(resolve, 10)) - return 'result' - }) - - // Execute multiple operations that will queue - const operations = Array(10) - .fill(0) - .map(() => limiter.exec(mockFn)) - - await Promise.all(operations) - - // notify array should be empty after all operations complete - expect(limiter.notify.length).toBe(0) - }) - - it('should clean up processingQueue correctly after many operations', async () => { - const limiter = new RateLimiter(5) - const mockFn = jest.fn().mockImplementation(async (args?: any) => { - await new Promise((resolve) => setTimeout(resolve, Math.random() * 10)) - return args?.id - }) - - for (let batch = 0; batch < 5; batch++) { - const operations = Array(20) - .fill(0) - .map((_, i) => limiter.exec(mockFn, { id: batch * 20 + i })) - - await Promise.all(operations) - expect(limiter.processingQueue.size).toBe(0) - } - - expect(mockFn).toHaveBeenCalledTimes(100) - }) - - it('should handle interleaved exec and add operations', async () => { - const limiter = new RateLimiter(2) - const results: string[] = [] - - const execOp = async (id: string): Promise => { - await new Promise((resolve) => setTimeout(resolve, 10)) - results.push(id) - return id - } - - const addOp = async (id: string): Promise => { - await new Promise((resolve) => setTimeout(resolve, 10)) - results.push(id) - } - - await Promise.all([ - limiter.exec(async () => await execOp('exec1')), - limiter.add(async () => { - await addOp('add1') - }), - limiter.exec(async () => await execOp('exec2')), - limiter.add(async () => { - await addOp('add2') - }) - ]) - - await limiter.waitProcessing() - - expect(results).toHaveLength(4) - expect(results).toContain('exec1') - expect(results).toContain('exec2') - expect(results).toContain('add1') - expect(results).toContain('add2') - }) - - it('should handle rapid creation and destruction of operations', async () => { - const limiter = new RateLimiter(3) - let successCount = 0 - let errorCount = 0 - - const operations = Array(30) - .fill(0) - .map(async (_, i) => { - const mockFn = jest.fn().mockImplementation(async () => { - await new Promise((resolve) => setTimeout(resolve, 5)) - if (i % 5 === 0) { - throw new Error(`Error ${i}`) - } - return `Result ${i}` - }) - - try { - await limiter.exec(mockFn) - successCount++ - } catch (err) { - errorCount++ - } - }) - - await Promise.all(operations) - - expect(successCount).toBe(24) - expect(errorCount).toBe(6) - expect(limiter.processingQueue.size).toBe(0) - }) - }) - - describe('TimeRateLimiter - Memory and Resource Management', () => { - it('should not leak memory in notify array', async () => { - jest.useFakeTimers() - const limiter = new TimeRateLimiter(2, 1000) - const mockFn = jest.fn().mockImplementation(async () => { - await new Promise((resolve) => setTimeout(resolve, 10)) - return 'result' - }) - - // Execute operations that will need to wait - const operations = [limiter.exec(mockFn), limiter.exec(mockFn), limiter.exec(mockFn), limiter.exec(mockFn)] - - jest.advanceTimersByTime(20) - await Promise.resolve() - await Promise.resolve() - - jest.advanceTimersByTime(1001) - await Promise.resolve() - await Promise.resolve() - - jest.advanceTimersByTime(20) - await Promise.resolve() - await Promise.resolve() - - await Promise.all(operations) - - // notify array should be empty after all operations complete - expect(limiter.notify.length).toBe(0) - jest.useRealTimers() - }, 10000) - - it('should not accumulate executions indefinitely', async () => { - jest.useFakeTimers() - const limiter = new TimeRateLimiter(5, 1000) - const mockFn = jest.fn().mockResolvedValue('result') - - // Execute many batches - for (let batch = 0; batch < 10; batch++) { - const operations = Array(5) - .fill(0) - .map(() => limiter.exec(mockFn)) - - await Promise.all(operations) - - jest.advanceTimersByTime(1100) - } - - // Executions should be cleaned up - // Only the most recent batch should remain (or less) - expect(limiter.executions.length).toBeLessThanOrEqual(5) - jest.useRealTimers() - }) - - it('should handle operations that never resolve gracefully', async () => { - const limiter = new TimeRateLimiter(2, 1000) - - const normalOp = jest.fn().mockImplementation(async () => { - await new Promise((resolve) => setTimeout(resolve, 10)) - return 'result' - }) - - // Note: We can't actually test hanging operations without causing issues - // Instead, test that slow operations don't block faster ones - const slowOp = jest.fn().mockImplementation(async () => { - await new Promise((resolve) => setTimeout(resolve, 100)) - return 'slow' - }) - - void limiter.exec(slowOp) - expect(limiter.active).toBe(1) - - // Other operations should still work - const result = await limiter.exec(normalOp) - expect(result).toBe('result') - expect(normalOp).toHaveBeenCalledTimes(1) - }, 10000) - }) - - describe('RateLimiter - Boundary Conditions', () => { - it('should handle operations that complete immediately', async () => { - const limiter = new RateLimiter(3) - const mockFn = jest.fn().mockResolvedValue('instant') - - const results = await Promise.all([limiter.exec(mockFn), limiter.exec(mockFn), limiter.exec(mockFn)]) - - expect(results).toEqual(['instant', 'instant', 'instant']) - expect(limiter.processingQueue.size).toBe(0) - }) - - it('should maintain correct state when operations fail at different stages', async () => { - const limiter = new RateLimiter(2) - const errors: string[] = [] - - const operations = [ - limiter - .exec(async () => { - await new Promise((resolve) => setTimeout(resolve, 10)) - throw new Error('error1') - }) - .catch((e) => { - errors.push(e.message) - }), - - limiter.exec(async () => { - await new Promise((resolve) => setTimeout(resolve, 5)) - return 'success1' - }), - - limiter - .exec(async () => { - throw new Error('error2') - }) - .catch((e) => { - errors.push(e.message) - }), - - limiter.exec(async () => { - await new Promise((resolve) => setTimeout(resolve, 15)) - return 'success2' - }) - ] - - const results = await Promise.all(operations) - - expect(errors).toHaveLength(2) - expect(errors).toContain('error1') - expect(errors).toContain('error2') - expect(results.filter(Boolean)).toContain('success1') - expect(results.filter(Boolean)).toContain('success2') - expect(limiter.processingQueue.size).toBe(0) - }, 10000) - }) - - describe('TimeRateLimiter - Boundary Conditions', () => { - it('should handle operations completing in reverse order', async () => { - jest.useFakeTimers() - const limiter = new TimeRateLimiter(3, 1000) - const completionOrder: number[] = [] - - const createOp = (id: number, delay: number) => async (): Promise => { - await new Promise((resolve) => setTimeout(resolve, delay)) - completionOrder.push(id) - return id - } - - const operations = [limiter.exec(createOp(1, 30)), limiter.exec(createOp(2, 20)), limiter.exec(createOp(3, 10))] - - jest.advanceTimersByTime(11) - await Promise.resolve() - - jest.advanceTimersByTime(10) - await Promise.resolve() - - jest.advanceTimersByTime(10) - await Promise.resolve() - - await Promise.all(operations) - - // Operations should complete in reverse order of their delays - expect(completionOrder).toEqual([3, 2, 1]) - jest.useRealTimers() - }) - - it('should handle rate limit at exact boundaries', async () => { - jest.useFakeTimers() - const limiter = new TimeRateLimiter(2, 1000) - const mockFn = jest.fn().mockResolvedValue('result') - - // Execute exactly at the rate - void limiter.exec(mockFn) - void limiter.exec(mockFn) - - expect(mockFn).toHaveBeenCalledTimes(2) - expect(limiter.active).toBe(2) - - // One more should wait - const thirdOp = limiter.exec(mockFn) - expect(mockFn).toHaveBeenCalledTimes(2) - - // Advance to exactly the period - jest.advanceTimersByTime(1000) - await Promise.resolve() - await Promise.resolve() - - await thirdOp - expect(mockFn).toHaveBeenCalledTimes(3) - jest.useRealTimers() - }) - }) - - describe('Integration Scenarios', () => { - it('should handle mixed RateLimiter operations with varying complexities', async () => { - const limiter = new RateLimiter(3) - const results: Array = [] - - const stringOp = async (val: string): Promise => { - await new Promise((resolve) => setTimeout(resolve, 5)) - results.push(val) - return val - } - - const numberOp = async (val: number): Promise => { - await new Promise((resolve) => setTimeout(resolve, 10)) - results.push(val) - return val - } - - const objectOp = async (obj: { id: number }): Promise<{ id: number }> => { - await new Promise((resolve) => setTimeout(resolve, 7)) - results.push(obj.id) - return obj - } - - await Promise.all([ - limiter.exec(async () => await stringOp('a')), - limiter.exec(async () => await numberOp(1)), - limiter.exec(async () => await objectOp({ id: 100 })), - limiter.exec(async () => await stringOp('b')), - limiter.exec(async () => await numberOp(2)) - ]) - - expect(results).toHaveLength(5) - expect(results).toContain('a') - expect(results).toContain('b') - expect(results).toContain(1) - expect(results).toContain(2) - expect(results).toContain(100) - }) - - it('should handle nested rate limiters', async () => { - const outerLimiter = new RateLimiter(2) - const innerLimiter = new RateLimiter(1) - - let executionCount = 0 - - const nestedOp = async (id: number): Promise => { - return await innerLimiter.exec(async () => { - executionCount++ - await new Promise((resolve) => setTimeout(resolve, 5)) - return id - }) - } - - const results = await Promise.all([ - outerLimiter.exec(async () => await nestedOp(1)), - outerLimiter.exec(async () => await nestedOp(2)), - outerLimiter.exec(async () => await nestedOp(3)), - outerLimiter.exec(async () => await nestedOp(4)) - ]) - - expect(results).toEqual([1, 2, 3, 4]) - expect(executionCount).toBe(4) - expect(outerLimiter.processingQueue.size).toBe(0) - expect(innerLimiter.processingQueue.size).toBe(0) - }) - }) -}) diff --git a/foundations/core/packages/core/src/__tests__/limits.test.ts b/foundations/core/packages/core/src/__tests__/limits.test.ts deleted file mode 100644 index 2bfc263c15..0000000000 --- a/foundations/core/packages/core/src/__tests__/limits.test.ts +++ /dev/null @@ -1,462 +0,0 @@ -// -// Copyright © 2023 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { TimeRateLimiter } from '../utils' - -describe('TimeRateLimiter', () => { - describe('constructor', () => { - it('should initialize with correct rate and period', () => { - const limiter = new TimeRateLimiter(5, 2000) - expect(limiter.rate).toBe(5) - expect(limiter.period).toBe(2000) - expect(limiter.active).toBe(0) - expect(limiter.executions).toEqual([]) - }) - - it('should use default period of 1000ms', () => { - const limiter = new TimeRateLimiter(3) - expect(limiter.period).toBe(1000) - }) - }) - - it('should limit rate of executions', async () => { - jest.useFakeTimers() - const limiter = new TimeRateLimiter(2, 1000) // 2 executions per second - const mockFn = jest.fn().mockResolvedValue('result') - const operations: Promise[] = [] - - // Try to execute 4 operations - for (let i = 0; i < 4; i++) { - operations.push(limiter.exec(mockFn)) - } - - // First 2 should execute immediately - expect(mockFn).toHaveBeenCalledTimes(2) - - // Advance time by 1 second - jest.advanceTimersByTime(1001) - await Promise.resolve() - - // Next 2 should execute after time advance - expect(mockFn).toHaveBeenCalledTimes(4) - - await Promise.all(operations) - jest.useRealTimers() - }) - - it('should cleanup old executions', async () => { - jest.useFakeTimers() - const limiter = new TimeRateLimiter(2, 1000) - const mockFn = jest.fn().mockResolvedValue('result') - - // Execute first operation - await limiter.exec(mockFn) - expect(mockFn).toHaveBeenCalledTimes(1) - expect(limiter.executions.length).toBe(1) - - // Advance time past period - jest.advanceTimersByTime(1001) - - // Execute another operation - await limiter.exec(mockFn) - expect(mockFn).toHaveBeenCalledTimes(2) - expect(limiter.executions.length).toBe(1) // Old execution should be cleaned up - jest.useRealTimers() - }) - - it('should handle concurrent operations', async () => { - jest.useFakeTimers() - const limiter = new TimeRateLimiter(2, 1000) - const mockFn = jest.fn().mockImplementation(async () => { - console.log('start#') - await new Promise((resolve) => setTimeout(resolve, 450)) - console.log('finished#') - return 'result' - }) - - const operations = Promise.all([limiter.exec(mockFn), limiter.exec(mockFn), limiter.exec(mockFn)]) - - expect(mockFn).toHaveBeenCalledTimes(2) - expect(limiter.active).toBe(2) - - jest.advanceTimersByTime(500) - await Promise.resolve() - await Promise.resolve() - jest.advanceTimersByTime(1000) - await Promise.resolve() - - jest.advanceTimersByTime(2001) - await Promise.resolve() - await Promise.resolve() - - expect(limiter.active).toBe(0) - - expect(mockFn).toHaveBeenCalledTimes(3) - - await operations - jest.useRealTimers() - }) - - it('should wait for processing to complete', async () => { - jest.useFakeTimers() - const limiter = new TimeRateLimiter(2, 1000) - const mockFn = jest.fn().mockImplementation(async () => { - await new Promise((resolve) => setTimeout(resolve, 500)) - return 'result' - }) - - const operation = limiter.exec(mockFn) - const waitPromise = limiter.waitProcessing().then(() => { - console.log('wait complete') - }) - - expect(limiter.active).toBe(1) - - jest.advanceTimersByTime(1001) - await Promise.resolve() - await Promise.resolve() - await Promise.resolve() - - await waitPromise - await operation - expect(limiter.active).toBe(0) - jest.useRealTimers() - }) - - describe('execution tracking', () => { - it('should track running executions correctly', async () => { - jest.useFakeTimers() - const limiter = new TimeRateLimiter(3, 1000) - const mockFn = jest.fn().mockImplementation(async () => { - await new Promise((resolve) => setTimeout(resolve, 100)) - return 'result' - }) - - const op1 = limiter.exec(mockFn) - expect(limiter.executions.length).toBe(1) - expect(limiter.executions[0].running).toBe(true) - - const op2 = limiter.exec(mockFn) - expect(limiter.executions.length).toBe(2) - - jest.advanceTimersByTime(101) - await Promise.resolve() - await Promise.resolve() - - await op1 - expect(limiter.executions[0].running).toBe(false) - - await op2 - jest.useRealTimers() - }) - - it('should mark executions as not running after completion', async () => { - const limiter = new TimeRateLimiter(2, 1000) - const mockFn = jest.fn().mockImplementation(async () => { - await new Promise((resolve) => setTimeout(resolve, 10)) - return 'result' - }) - - await limiter.exec(mockFn) - - const execution = limiter.executions[0] - expect(execution.running).toBe(false) - }) - - it('should mark executions as not running even on error', async () => { - const limiter = new TimeRateLimiter(2, 1000) - const mockFn = jest.fn().mockRejectedValue(new Error('test error')) - - await expect(limiter.exec(mockFn)).rejects.toThrow('test error') - - const execution = limiter.executions[0] - expect(execution.running).toBe(false) - }) - }) - - describe('cleanup behavior', () => { - it('should cleanup executions older than period', async () => { - jest.useFakeTimers() - const limiter = new TimeRateLimiter(3, 1000) - const mockFn = jest.fn().mockResolvedValue('result') - - await limiter.exec(mockFn) - expect(limiter.executions.length).toBe(1) - - jest.advanceTimersByTime(1100) - - await limiter.exec(mockFn) - // After cleanup, only the new execution should remain - expect(limiter.executions.length).toBe(1) - jest.useRealTimers() - }) - - it('should keep running executions regardless of time', async () => { - jest.useFakeTimers() - const limiter = new TimeRateLimiter(2, 1000) - let resolveOp: any - const mockFn = jest.fn().mockImplementation( - async () => - await new Promise((resolve) => { - resolveOp = resolve - }) - ) - - const op = limiter.exec(mockFn) - expect(limiter.executions.length).toBe(1) - - jest.advanceTimersByTime(2000) - - // Start another operation to trigger cleanup - const mockFn2 = jest.fn().mockResolvedValue('result') - await limiter.exec(mockFn2) - - // The first operation should still be tracked because it's running - const runningExecution = limiter.executions.find((e) => e.running) - expect(runningExecution).toBeDefined() - - resolveOp('done') - await op - jest.useRealTimers() - }) - }) - - describe('rate limiting behavior', () => { - it('should allow executions up to rate within period', async () => { - jest.useFakeTimers() - const limiter = new TimeRateLimiter(3, 1000) - const mockFn = jest.fn().mockResolvedValue('result') - - const ops = [limiter.exec(mockFn), limiter.exec(mockFn), limiter.exec(mockFn)] - - expect(mockFn).toHaveBeenCalledTimes(3) - await Promise.all(ops) - jest.useRealTimers() - }) - - it('should delay 4th execution when rate is 3', async () => { - jest.useFakeTimers() - const limiter = new TimeRateLimiter(3, 1000) - const mockFn = jest.fn().mockResolvedValue('result') - - void limiter.exec(mockFn) - void limiter.exec(mockFn) - void limiter.exec(mockFn) - const fourthOp = limiter.exec(mockFn) - - expect(mockFn).toHaveBeenCalledTimes(3) - - jest.advanceTimersByTime(1001) - await Promise.resolve() - await Promise.resolve() - - await fourthOp - expect(mockFn).toHaveBeenCalledTimes(4) - jest.useRealTimers() - }) - - it('should respect period for rate limiting', async () => { - jest.useFakeTimers() - const limiter = new TimeRateLimiter(2, 2000) // 2 per 2 seconds - const mockFn = jest.fn().mockResolvedValue('result') - - void limiter.exec(mockFn) - void limiter.exec(mockFn) - const thirdOp = limiter.exec(mockFn) - - expect(mockFn).toHaveBeenCalledTimes(2) - - // 1 second should not be enough - jest.advanceTimersByTime(1001) - await Promise.resolve() - expect(mockFn).toHaveBeenCalledTimes(2) - - // 2 seconds should allow the third - jest.advanceTimersByTime(1001) - await Promise.resolve() - await Promise.resolve() - - await thirdOp - expect(mockFn).toHaveBeenCalledTimes(3) - jest.useRealTimers() - }) - }) - - describe('error handling', () => { - it('should handle operation errors and continue', async () => { - const limiter = new TimeRateLimiter(2, 1000) - const errorFn = jest.fn().mockRejectedValue(new Error('operation error')) - const successFn = jest.fn().mockResolvedValue('success') - - await expect(limiter.exec(errorFn)).rejects.toThrow('operation error') - expect(limiter.active).toBe(0) - - const result = await limiter.exec(successFn) - expect(result).toBe('success') - }) - - it('should decrement active counter on error', async () => { - const limiter = new TimeRateLimiter(2, 1000) - const mockFn = jest.fn().mockRejectedValue(new Error('test error')) - - expect(limiter.active).toBe(0) - await expect(limiter.exec(mockFn)).rejects.toThrow('test error') - expect(limiter.active).toBe(0) - }) - - it('should handle synchronous throws', async () => { - const limiter = new TimeRateLimiter(2, 1000) - const mockFn = jest.fn().mockImplementation(() => { - throw new Error('sync error') - }) - - await expect(limiter.exec(mockFn)).rejects.toThrow('sync error') - expect(limiter.active).toBe(0) - }) - }) - - describe('waitProcessing', () => { - it('should resolve immediately when no operations are active', async () => { - const limiter = new TimeRateLimiter(2, 1000) - - await limiter.waitProcessing() - expect(limiter.active).toBe(0) - }) - - it('should wait for all active operations to complete', async () => { - jest.useFakeTimers() - const limiter = new TimeRateLimiter(2, 1000) - let completed = false - const mockFn = jest.fn().mockImplementation(async () => { - await new Promise((resolve) => setTimeout(resolve, 100)) - completed = true - return 'result' - }) - - void limiter.exec(mockFn) - expect(limiter.active).toBe(1) - - const waitPromise = limiter.waitProcessing() - - jest.advanceTimersByTime(101) - await Promise.resolve() - await Promise.resolve() - await Promise.resolve() - - await waitPromise - expect(completed).toBe(true) - expect(limiter.active).toBe(0) - jest.useRealTimers() - }) - }) - - describe('arguments passing', () => { - it('should pass arguments to operation', async () => { - const limiter = new TimeRateLimiter(2, 1000) - const mockFn = jest.fn().mockImplementation(async (args?: any) => { - return args?.value - }) - - const result = await limiter.exec(mockFn, { value: 42 }) - - expect(result).toBe(42) - expect(mockFn).toHaveBeenCalledWith({ value: 42 }) - }) - - it('should handle operations without arguments', async () => { - const limiter = new TimeRateLimiter(2, 1000) - const mockFn = jest.fn().mockResolvedValue('no-args') - - const result = await limiter.exec(mockFn) - - expect(result).toBe('no-args') - expect(mockFn).toHaveBeenCalledWith(undefined) - }) - }) - - describe('edge cases', () => { - it('should handle rate of 1 correctly', async () => { - jest.useFakeTimers() - const limiter = new TimeRateLimiter(1, 1000) - const mockFn = jest.fn().mockResolvedValue('result') - - void limiter.exec(mockFn) - expect(mockFn).toHaveBeenCalledTimes(1) - - const secondOp = limiter.exec(mockFn) - expect(mockFn).toHaveBeenCalledTimes(1) // Should wait - - jest.advanceTimersByTime(1001) - await Promise.resolve() - await Promise.resolve() - - await secondOp - expect(mockFn).toHaveBeenCalledTimes(2) - jest.useRealTimers() - }) - - it('should handle very short period', async () => { - jest.useFakeTimers() - const limiter = new TimeRateLimiter(2, 10) // 2 per 10ms - const mockFn = jest.fn().mockResolvedValue('result') - - void limiter.exec(mockFn) - void limiter.exec(mockFn) - const thirdOp = limiter.exec(mockFn) - - jest.advanceTimersByTime(11) - await Promise.resolve() - - await thirdOp - expect(mockFn).toHaveBeenCalledTimes(3) - jest.useRealTimers() - }) - - it('should handle large rate', async () => { - const limiter = new TimeRateLimiter(100, 1000) - const mockFn = jest.fn().mockResolvedValue('result') - - const operations = Array(50) - .fill(0) - .map(() => limiter.exec(mockFn)) - - await Promise.all(operations) - expect(mockFn).toHaveBeenCalledTimes(50) - }) - }) - - describe('stress test', () => { - it('should handle many operations correctly', async () => { - const limiter = new TimeRateLimiter(10, 100) - const results: number[] = [] - - const mockFn = jest.fn().mockImplementation(async (args?: any) => { - await new Promise((resolve) => setTimeout(resolve, 5)) - results.push(args?.id) - return args?.id - }) - - const operations = Array(30) - .fill(0) - .map((_, i) => limiter.exec(mockFn, { id: i })) - - await Promise.all(operations) - - expect(mockFn).toHaveBeenCalledTimes(30) - expect(results).toHaveLength(30) - expect(limiter.active).toBe(0) - }) - }) -}) diff --git a/foundations/core/packages/core/src/__tests__/memdb.test.ts b/foundations/core/packages/core/src/__tests__/memdb.test.ts deleted file mode 100644 index 9d4f3b1717..0000000000 --- a/foundations/core/packages/core/src/__tests__/memdb.test.ts +++ /dev/null @@ -1,408 +0,0 @@ -// -// Copyright © 2020 Anticrm Platform Contributors. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { type Client, type DomainParams, type DomainRequestOptions, type DomainResult } from '..' -import type { Class, Doc, Obj, OperationDomain, Ref } from '../classes' -import core from '../component' -import { Hierarchy } from '../hierarchy' -import { ModelDb, TxDb } from '../memdb' -import { TxOperations } from '../operations' -import { - type DocumentQuery, - type FindOptions, - type SearchOptions, - type SearchQuery, - type SearchResult, - SortingOrder, - type WithLookup -} from '../storage' -import { type Tx } from '../tx' -import { genMinModel, test, type TestMixin } from './minmodel' - -const txes = genMinModel() - -class ClientModel extends ModelDb implements Client { - notify?: ((...tx: Tx[]) => void) | undefined - - getHierarchy (): Hierarchy { - return this.hierarchy - } - - getModel (): ModelDb { - return this - } - - async findOne( - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): Promise | undefined> { - return (await this.findAll(_class, query, options)).shift() - } - - async searchFulltext (query: SearchQuery, options: SearchOptions): Promise { - return { docs: [] } - } - - async domainRequest( - domain: OperationDomain, - params: DomainParams, - options?: DomainRequestOptions - ): Promise> { - return { domain, value: null as any } - } - - async close (): Promise {} -} - -async function createModel (modelTxes: Tx[] = txes): Promise<{ model: ClientModel, hierarchy: Hierarchy, txDb: TxDb }> { - const hierarchy = new Hierarchy() - for (const tx of modelTxes) { - hierarchy.tx(tx) - } - const model = new ClientModel(hierarchy) - for (const tx of modelTxes) { - await model.tx(tx) - } - const txDb = new TxDb(hierarchy) - for (const tx of modelTxes) await txDb.tx(tx) - return { model, hierarchy, txDb } -} - -describe('memdb', () => { - it('should save all tx', async () => { - const { txDb } = await createModel() - - const result = await txDb.findAll(core.class.Tx, {}) - expect(result.length).toBe(txes.length) - }) - - it('should create space', async () => { - const { model } = await createModel() - - const client = new TxOperations(model, core.account.System) - const result = await client.findAll(core.class.Space, {}) - expect(result).toHaveLength(2) - - await client.createDoc(core.class.Space, core.space.Model, { - private: false, - name: 'NewSpace', - description: '', - members: [], - archived: false - }) - const result2 = await client.findAll(core.class.Space, {}) - expect(result2).toHaveLength(3) - - await client.createDoc(core.class.Space, core.space.Model, { - private: false, - name: 'NewSpace', - description: '', - members: [], - archived: false - }) - const result3 = await client.findAll(core.class.Space, {}) - expect(result3).toHaveLength(4) - }) - - it('should query model', async () => { - const { model } = await createModel() - const result = await model.findAll(core.class.Class, {}) - const names = result.map((d) => d._id) - expect(names.includes(core.class.Class)).toBe(true) - const result2 = await model.findAll(core.class.Class, { _id: undefined }) - expect(result2.length).toBe(0) - }) - - it('should fail query wrong class', async () => { - const { model } = await createModel() - - await expect(model.findAll('class:workbench.Application' as Ref>, { _id: undefined })).rejects.toThrow() - }) - - it('should create mixin', async () => { - const { model } = await createModel() - const ops = new TxOperations(model, core.account.System) - - await ops.createMixin(core.class.Obj, core.class.Class, core.space.Model, test.mixin.TestMixin, { - arr: ['hello'] - }) - const objClass = (await model.findAll(core.class.Class, { _id: core.class.Obj }))[0] as any - expect(objClass['test:mixin:TestMixin'].arr).toEqual(expect.arrayContaining(['hello'])) - }) - - it('should allow delete', async () => { - const { model } = await createModel() - const result = await model.findAll(core.class.Space, {}) - expect(result.length).toBe(2) - - const ops = new TxOperations(model, core.account.System) - await ops.removeDoc(result[0]._class, result[0].space, result[0]._id) - const result2 = await model.findAll(core.class.Space, {}) - expect(result2).toHaveLength(1) - }) - - it('should query model with params', async () => { - const { model } = await createModel() - const first = await model.findAll(core.class.Class, { - _id: txes[1].objectId as Ref>, - kind: 0 - }) - expect(first.length).toBe(1) - const second = await model.findAll(core.class.Class, { - _id: { $in: [txes[1].objectId as Ref>, txes[3].objectId as Ref>] } - }) - expect(second.length).toBe(2) - const incorrectId = await model.findAll(core.class.Class, { - _id: (txes[1].objectId + 'test') as Ref> - }) - expect(incorrectId.length).toBe(0) - const result = await model.findAll(core.class.Class, { - _id: txes[1].objectId as Ref>, - kind: 1 - }) - expect(result.length).toBe(0) - const multipleParam = await model.findAll(core.class.Doc, { - space: { $in: [core.space.Model, core.space.Tx] } - }) - expect(multipleParam.length).toBeGreaterThan(5) - - const classes = await model.findAll(core.class.Class, {}) - const gt = await model.findAll(core.class.Class, { - kind: { $gt: 1 } - }) - expect(gt.length).toBe(classes.filter((p) => p.kind > 1).length) - const gte = await model.findAll(core.class.Class, { - kind: { $gte: 1 } - }) - expect(gte.length).toBe(classes.filter((p) => p.kind >= 1).length) - const lt = await model.findAll(core.class.Class, { - kind: { $lt: 1 } - }) - expect(lt.length).toBe(classes.filter((p) => p.kind < 1).length) - const lte = await model.findAll(core.class.Class, { - kind: { $lt: 1 } - }) - expect(lte.length).toBe(classes.filter((p) => p.kind <= 1).length) - }) - - it('should query model like params', async () => { - const { model } = await createModel() - const expectedLength = txes.filter((tx) => tx.objectSpace === core.space.Model).length - const without = await model.findAll(core.class.Doc, { - space: { $like: core.space.Model } - }) - expect(without).toHaveLength(expectedLength) - const begin = await model.findAll(core.class.Doc, { - space: { $like: '%Model' } - }) - expect(begin).toHaveLength(expectedLength) - const zero = await model.findAll(core.class.Doc, { - space: { $like: 'Model' } - }) - expect(zero).toHaveLength(0) - const end = await model.findAll(core.class.Doc, { - space: { $like: 'core:space:M%' } - }) - expect(end).toHaveLength(expectedLength) - const mid = await model.findAll(core.class.Doc, { - space: { $like: '%M%de%' } - }) - expect(mid).toHaveLength(expectedLength) - const all = await model.findAll(core.class.Doc, { - space: { $like: '%Mod%' } - }) - expect(all).toHaveLength(expectedLength) - - const regex = await model.findAll(core.class.Doc, { - space: { $regex: '.*Mod.*' } - }) - expect(regex).toHaveLength(expectedLength) - }) - - // TODO: fix this test - // it('should push to array', async () => { - // const hierarchy = new Hierarchy() - // for (const tx of txes) hierarchy.tx(tx) - // const model = new TxOperations(new ClientModel(hierarchy), core.account.System) - // for (const tx of txes) await model.tx(tx) - // const space = await model.createDoc(core.class.Space, core.space.Model, { - // name: 'name', - // description: 'desc', - // private: false, - // members: [], - // archived: false - // }) - // const account = await model.createDoc(core.class.Account, core.space.Model, { - // email: 'email', - // role: AccountRole.User - // }) - // await model.updateDoc(core.class.Space, core.space.Model, space, { $push: { members: account } }) - // const txSpace = await model.findAll(core.class.Space, { _id: space }) - // expect(txSpace[0].members).toEqual(expect.arrayContaining([account])) - // }) - - it('limit and sorting', async () => { - const hierarchy = new Hierarchy() - for (const tx of txes) hierarchy.tx(tx) - const model = new TxOperations(new ClientModel(hierarchy), core.account.System) - for (const tx of txes) await model.tx(tx) - - const without = await model.findAll(core.class.Space, {}) - expect(without).toHaveLength(2) - - const limit = await model.findAll(core.class.Space, {}, { limit: 1 }) - expect(limit).toHaveLength(1) - - const sortAsc = await model.findAll(core.class.Space, {}, { limit: 1, sort: { name: SortingOrder.Ascending } }) - expect(sortAsc[0].name).toMatch('Sp1') - - const sortDesc = await model.findAll(core.class.Space, {}, { limit: 1, sort: { name: SortingOrder.Descending } }) - expect(sortDesc[0].name).toMatch('Sp2') - - const numberSortDesc = await model.findAll(core.class.Doc, {}, { sort: { modifiedOn: SortingOrder.Descending } }) - expect(numberSortDesc[0].modifiedOn).toBeGreaterThanOrEqual(numberSortDesc[numberSortDesc.length - 1].modifiedOn) - - const numberSort = await model.findAll(core.class.Doc, {}, { sort: { modifiedOn: SortingOrder.Ascending } }) - expect(numberSort[0].modifiedOn).toBeLessThanOrEqual(numberSort[numberSortDesc.length - 1].modifiedOn) - }) - - it('should add attached document', async () => { - const { model } = await createModel() - - const client = new TxOperations(model, core.account.System) - const result = await client.findAll(core.class.Space, {}) - expect(result).toHaveLength(2) - - await client.addCollection(test.class.TestComment, core.space.Model, result[0]._id, result[0]._class, 'comments', { - message: 'msg' - }) - const result2 = await client.findAll(test.class.TestComment, {}) - expect(result2).toHaveLength(1) - }) - - it('lookups', async () => { - const { model } = await createModel() - - const client = new TxOperations(model, core.account.System) - const spaces = await client.findAll(core.class.Space, {}) - expect(spaces).toHaveLength(2) - - const first = await client.addCollection( - test.class.TestComment, - core.space.Model, - spaces[0]._id, - spaces[0]._class, - 'comments', - { - message: 'msg' - } - ) - - const second = await client.addCollection( - test.class.TestComment, - core.space.Model, - first, - test.class.TestComment, - 'comments', - { - message: 'msg2' - } - ) - - await client.addCollection(test.class.TestComment, core.space.Model, spaces[0]._id, spaces[0]._class, 'comments', { - message: 'msg3' - }) - - const simple = await client.findAll( - test.class.TestComment, - { _id: first }, - { lookup: { attachedTo: spaces[0]._class } } - ) - expect(simple[0].$lookup?.attachedTo).toEqual(spaces[0]) - - const nested = await client.findAll( - test.class.TestComment, - { _id: second }, - { lookup: { attachedTo: [test.class.TestComment, { attachedTo: spaces[0]._class } as any] } } - ) - expect((nested[0].$lookup?.attachedTo as any).$lookup?.attachedTo).toEqual(spaces[0]) - - const reverse = await client.findAll( - spaces[0]._class, - { _id: spaces[0]._id }, - { lookup: { _id: { comments: test.class.TestComment } } } - ) - expect((reverse[0].$lookup as any).comments).toHaveLength(2) - }) - - it('mixin lookups', async () => { - const { model } = await createModel() - - const client = new TxOperations(model, core.account.System) - const spaces = await client.findAll(core.class.Space, {}) - expect(spaces).toHaveLength(2) - - const task = await client.createDoc(test.class.Task, spaces[0]._id, { - name: 'TSK1', - number: 1, - state: 0 - }) - - await client.createMixin(task, test.class.Task, spaces[0]._id, test.mixin.TaskMixinTodos, { - todos: 0 - }) - - await client.addCollection(test.class.TestMixinTodo, spaces[0]._id, task, test.mixin.TaskMixinTodos, 'todos', { - text: 'qwe' - }) - await client.addCollection(test.class.TestMixinTodo, spaces[0]._id, task, test.mixin.TaskMixinTodos, 'todos', { - text: 'qwe2' - }) - - const results = await client.findAll( - test.class.TestMixinTodo, - {}, - { lookup: { attachedTo: test.mixin.TaskMixinTodos } } - ) - expect(results.length).toEqual(2) - const attached = results[0].$lookup?.attachedTo - expect(attached).toBeDefined() - expect(Hierarchy.mixinOrClass(attached as Doc)).toEqual(test.mixin.TaskMixinTodos) - }) - - it('createDoc for AttachedDoc', async () => { - expect.assertions(1) - const { model } = await createModel() - - const client = new TxOperations(model, core.account.System) - const spaces = await client.findAll(core.class.Space, {}) - const task = await client.createDoc(test.class.Task, spaces[0]._id, { - name: 'TSK1', - number: 1, - state: 0 - }) - try { - await client.createDoc(test.class.TestMixinTodo, spaces[0]._id, { - text: '', - attachedTo: task, - attachedToClass: test.mixin.TaskMixinTodos, - collection: 'todos' - }) - } catch (e) { - expect(e).toEqual(new Error('createDoc cannot be used for objects inherited from AttachedDoc')) - } - }) -}) diff --git a/foundations/core/packages/core/src/__tests__/minmodel.ts b/foundations/core/packages/core/src/__tests__/minmodel.ts deleted file mode 100644 index cf9f493126..0000000000 --- a/foundations/core/packages/core/src/__tests__/minmodel.ts +++ /dev/null @@ -1,275 +0,0 @@ -// -// Copyright © 2020 Anticrm Platform Contributors. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { IntlString, Plugin } from '@hcengineering/platform' -import { plugin } from '@hcengineering/platform' -import type { Arr, AttachedDoc, Class, Data, Doc, Interface, Mixin, Obj, Ref, Space } from '../classes' -import { ClassifierKind, DOMAIN_MODEL } from '../classes' -import core from '../component' -import type { DocumentUpdate, TxCUD, TxCreateDoc, TxRemoveDoc, TxUpdateDoc } from '../tx' -import { DOMAIN_TX, TxFactory } from '../tx' - -const txFactory = new TxFactory(core.account.System) - -function createClass (_class: Ref>, attributes: Data>): TxCreateDoc { - return txFactory.createTxCreateDoc(core.class.Class, core.space.Model, attributes, _class) -} - -function createInterface (_interface: Ref>, attributes: Data>): TxCreateDoc { - return txFactory.createTxCreateDoc(core.class.Interface, core.space.Model, attributes, _interface) -} - -export function createDoc (_class: Ref>, attributes: Data): TxCreateDoc { - return txFactory.createTxCreateDoc(_class, core.space.Model, attributes) -} - -export function updateDoc ( - _class: Ref>, - space: Ref, - objectId: Ref, - operations: DocumentUpdate -): TxUpdateDoc { - return txFactory.createTxUpdateDoc(_class, space, objectId, operations) -} - -export function deleteDoc (_class: Ref>, space: Ref, objectId: Ref): TxRemoveDoc { - return txFactory.createTxRemoveDoc(_class, space, objectId) -} - -export interface TestMixin extends Doc { - arr: Arr -} - -export interface AttachedComment extends AttachedDoc { - message: string -} - -export interface WithState extends Doc { - state: number - number: number -} -export interface Task extends Doc, WithState { - name: string -} - -export interface TaskMixinTodos extends Task { - todos: number -} - -export interface TaskMixinTodo extends AttachedDoc { - text: string -} - -export interface TaskCheckItem extends AttachedDoc, WithState { - name: string - complete: boolean -} - -export const test = plugin('test' as Plugin, { - mixin: { - TestMixin: '' as Ref>, - TaskMixinTodos: '' as Ref> - }, - class: { - Task: '' as Ref>, - TaskCheckItem: '' as Ref>, - TestComment: '' as Ref>, - TestMixinTodo: '' as Ref> - }, - interface: { - WithState: '' as Ref>, - DummyWithState: '' as Ref> - } -}) - -/** - * Generate minimal model for testing purposes. - * @returns R - */ -export function genMinModel (): TxCUD[] { - const txes = [] - // Fill Tx'es with basic model classes. - txes.push(createClass(core.class.Obj, { label: 'Obj' as IntlString, kind: ClassifierKind.CLASS })) - txes.push( - createClass(core.class.Doc, { label: 'Doc' as IntlString, extends: core.class.Obj, kind: ClassifierKind.CLASS }) - ) - txes.push( - createClass(core.class.AttachedDoc, { - label: 'AttachedDoc' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.MIXIN - }) - ) - txes.push( - createClass(core.class.Class, { - label: 'Class' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.CLASS, - domain: DOMAIN_MODEL - }) - ) - txes.push( - createClass(core.class.Interface, { - label: 'Interface' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.CLASS - }) - ) - txes.push( - createClass(core.class.Space, { - label: 'Space' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.CLASS, - domain: DOMAIN_MODEL - }) - ) - // txes.push( - // createClass(core.class.Account, { - // label: 'Account' as IntlString, - // extends: core.class.Doc, - // kind: ClassifierKind.CLASS, - // domain: DOMAIN_MODEL - // }) - // ) - - txes.push( - createInterface(test.interface.WithState, { - label: 'WithState' as IntlString, - extends: [], - kind: ClassifierKind.INTERFACE - }) - ) - - txes.push( - createClass(core.class.Tx, { - label: 'Tx' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.CLASS, - domain: DOMAIN_TX - }) - ) - txes.push( - createClass(core.class.TxCUD, { - label: 'TxCUD' as IntlString, - extends: core.class.Tx, - kind: ClassifierKind.CLASS, - domain: DOMAIN_TX - }) - ) - txes.push( - createClass(core.class.TxCreateDoc, { - label: 'TxCreateDoc' as IntlString, - extends: core.class.TxCUD, - kind: ClassifierKind.CLASS - }) - ) - txes.push( - createClass(core.class.TxUpdateDoc, { - label: 'TxUpdateDoc' as IntlString, - extends: core.class.TxCUD, - kind: ClassifierKind.CLASS - }) - ) - txes.push( - createClass(core.class.TxRemoveDoc, { - label: 'TxRemoveDoc' as IntlString, - extends: core.class.TxCUD, - kind: ClassifierKind.CLASS - }) - ) - - txes.push( - createClass(core.class.Blob, { - label: 'Blob' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.CLASS - }) - ) - - txes.push( - createClass(test.mixin.TestMixin, { - label: 'TestMixin' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.MIXIN - }) - ) - - txes.push( - createInterface(test.interface.DummyWithState, { - label: 'DummyWithState' as IntlString, - extends: [test.interface.WithState], - kind: ClassifierKind.INTERFACE - }) - ) - txes.push( - createClass(test.class.TestComment, { - label: 'TestComment' as IntlString, - extends: core.class.AttachedDoc, - kind: ClassifierKind.CLASS - }) - ) - txes.push( - createClass(test.class.Task, { - label: 'Task' as IntlString, - extends: core.class.Doc, - implements: [test.interface.DummyWithState], - kind: ClassifierKind.CLASS - }) - ) - txes.push( - createClass(test.class.TaskCheckItem, { - label: 'Task' as IntlString, - extends: core.class.AttachedDoc, - implements: [test.interface.WithState], - kind: ClassifierKind.CLASS - }) - ) - - txes.push( - createClass(test.mixin.TaskMixinTodos, { - label: 'TaskMixinTodos' as IntlString, - extends: test.class.Task, - kind: ClassifierKind.MIXIN - }) - ) - txes.push( - createClass(test.class.TestMixinTodo, { - label: 'TestMixinTodo' as IntlString, - extends: core.class.AttachedDoc, - kind: ClassifierKind.CLASS - }) - ) - - txes.push( - createDoc(core.class.Space, { - name: 'Sp1', - description: '', - private: false, - members: [], - archived: false - }) - ) - - txes.push( - createDoc(core.class.Space, { - name: 'Sp2', - description: '', - private: false, - members: [], - archived: false - }) - ) - return txes -} diff --git a/foundations/core/packages/core/src/__tests__/objvalue.test.ts b/foundations/core/packages/core/src/__tests__/objvalue.test.ts deleted file mode 100644 index af0be066f7..0000000000 --- a/foundations/core/packages/core/src/__tests__/objvalue.test.ts +++ /dev/null @@ -1,290 +0,0 @@ -// -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// Copyright © 2021 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { getObjectValue, setObjectValue } from '../objvalue' -import type { Doc } from '../classes' - -describe('objvalue', () => { - describe('getObjectValue', () => { - it('should return the document itself for empty key', () => { - const doc = { _id: '1', name: 'test' } as unknown as Doc - const result = getObjectValue('', doc) - expect(result).toBe(doc) - }) - - it('should get simple property value', () => { - const doc = { _id: '1', name: 'John', age: 30 } as unknown as Doc - expect(getObjectValue('name', doc)).toBe('John') - expect(getObjectValue('age', doc)).toBe(30) - }) - - it('should get nested property value with dot notation', () => { - const doc = { - _id: '1', - user: { - name: 'John', - address: { - city: 'New York', - zip: '10001' - } - } - } as unknown as Doc - - expect(getObjectValue('user.name', doc)).toBe('John') - expect(getObjectValue('user.address.city', doc)).toBe('New York') - expect(getObjectValue('user.address.zip', doc)).toBe('10001') - }) - - it('should handle escaped dollar signs', () => { - const doc = { - _id: '1', - field$name: 'value', - nested: { - prop$test: 123 - } - } as unknown as Doc - - expect(getObjectValue('field\\$name', doc)).toBe('value') - expect(getObjectValue('nested.prop\\$test', doc)).toBe(123) - }) - - it('should return undefined for non-existent properties', () => { - const doc = { _id: '1', name: 'test' } as unknown as Doc - expect(getObjectValue('nonExistent', doc)).toBeUndefined() - expect(getObjectValue('user.name', doc)).toBeUndefined() - }) - - it('should handle array index access', () => { - const doc = { - _id: '1', - items: ['a', 'b', 'c'] - } as unknown as Doc - - expect(getObjectValue('items.0', doc)).toBe('a') - expect(getObjectValue('items.1', doc)).toBe('b') - expect(getObjectValue('items.2', doc)).toBe('c') - }) - - it('should handle nested arrays with named properties', () => { - const doc = { - _id: '1', - users: [ - { name: 'Alice', age: 25 }, - { name: 'Bob', age: 30 } - ] - } as unknown as Doc - - const names = getObjectValue('users.name', doc) - expect(names).toEqual(['Alice', 'Bob']) - }) - - it('should handle deeply nested array queries', () => { - const doc = { - _id: '1', - departments: [ - { - name: 'Engineering', - employees: [ - { name: 'Alice', role: 'Dev' }, - { name: 'Bob', role: 'QA' } - ] - }, - { - name: 'Sales', - employees: [{ name: 'Charlie', role: 'Manager' }] - } - ] - } as unknown as Doc - - const employeeNames = getObjectValue('departments.employees.name', doc) - expect(employeeNames).toEqual(['Alice', 'Bob', 'Charlie']) - }) - - it('should handle null and undefined values in path', () => { - const doc = { - _id: '1', - user: null, - data: undefined - } as unknown as Doc - - expect(getObjectValue('user.name', doc)).toBeUndefined() - expect(getObjectValue('data.value', doc)).toBeUndefined() - }) - - it('should handle mixed nested structures', () => { - const doc = { - _id: '1', - config: { - settings: { - theme: 'dark', - notifications: true - } - } - } as unknown as Doc - - expect(getObjectValue('config.settings.theme', doc)).toBe('dark') - expect(getObjectValue('config.settings.notifications', doc)).toBe(true) - }) - - it('should return array for nested array property queries', () => { - const doc = { - _id: '1', - teams: [{ members: [{ name: 'A' }, { name: 'B' }] }, { members: [{ name: 'C' }] }] - } as unknown as Doc - - const memberNames = getObjectValue('teams.members.name', doc) - expect(memberNames).toEqual(['A', 'B', 'C']) - }) - }) - - describe('setObjectValue', () => { - it('should not do anything for empty key', () => { - const doc = { _id: '1', name: 'test' } as unknown as Doc - setObjectValue('', doc, 'newValue') - expect(doc).toEqual({ _id: '1', name: 'test' }) - }) - - it('should set simple property value', () => { - const doc = { _id: '1', name: 'old' } as unknown as Doc - setObjectValue('name', doc, 'new') - expect((doc as any).name).toBe('new') - }) - - it('should set nested property value', () => { - const doc = { - _id: '1', - user: { - name: 'old' - } - } as unknown as Doc - - setObjectValue('user.name', doc, 'new') - expect((doc as any).user.name).toBe('new') - }) - - it('should create nested objects if they do not exist', () => { - const doc = { _id: '1' } as unknown as Doc - setObjectValue('user.profile.name', doc, 'John') - expect((doc as any).user.profile.name).toBe('John') - }) - - it('should handle escaped dollar signs', () => { - const doc = { _id: '1' } as unknown as Doc - setObjectValue('field\\$name', doc, 'value') - expect((doc as any).field$name).toBe('value') - }) - - it('should clone the value before setting', () => { - const doc = { _id: '1' } as unknown as Doc - const value = { nested: 'object' } - setObjectValue('data', doc, value) - - // Modify original value - value.nested = 'modified' - - // Doc should have the original value (cloned) - expect((doc as any).data.nested).toBe('object') - }) - - it('should create nested objects even when intermediate value is array', () => { - const doc = { - _id: '1', - items: [{ name: 'a' }, { name: 'b' }] - } as unknown as Doc - - // This actually doesn't throw - it creates a 'name' property on the array object - setObjectValue('items.name', doc, 'new') - expect((doc as any).items.name).toBe('new') - }) - - it('should set value in existing nested structure', () => { - const doc = { - _id: '1', - config: { - settings: { - theme: 'dark' - } - } - } as unknown as Doc - - setObjectValue('config.settings.theme', doc, 'light') - expect((doc as any).config.settings.theme).toBe('light') - }) - - it('should handle setting multiple levels', () => { - const doc = { _id: '1' } as unknown as Doc - - setObjectValue('a.b.c.d', doc, 'deep') - expect((doc as any).a.b.c.d).toBe('deep') - }) - - it('should set numeric values', () => { - const doc = { _id: '1' } as unknown as Doc - setObjectValue('count', doc, 42) - expect((doc as any).count).toBe(42) - }) - - it('should set boolean values', () => { - const doc = { _id: '1' } as unknown as Doc - setObjectValue('enabled', doc, true) - expect((doc as any).enabled).toBe(true) - }) - - it('should set null values', () => { - const doc = { _id: '1', data: 'old' } as unknown as Doc - setObjectValue('data', doc, null) - expect((doc as any).data).toBe(null) - }) - - it('should overwrite existing nested structures', () => { - const doc = { - _id: '1', - config: { - old: 'value', - nested: { prop: 'test' } - } - } as unknown as Doc - - setObjectValue('config', doc, { new: 'structure' }) - expect((doc as any).config).toEqual({ new: 'structure' }) - }) - - it('should handle array values', () => { - const doc = { _id: '1' } as unknown as Doc - const arrayValue = [1, 2, 3] - setObjectValue('numbers', doc, arrayValue) - - // Modify original array - arrayValue.push(4) - - // Doc should have cloned array - expect((doc as any).numbers).toEqual([1, 2, 3]) - }) - - it('should handle object values', () => { - const doc = { _id: '1' } as unknown as Doc - setObjectValue('data', doc, { key: 'value', nested: { prop: 123 } }) - expect((doc as any).data).toEqual({ key: 'value', nested: { prop: 123 } }) - }) - - it('should create intermediate objects when needed', () => { - const doc = { _id: '1', existing: 'prop' } as unknown as Doc - setObjectValue('new.path.value', doc, 'data') - expect((doc as any).new.path.value).toBe('data') - expect((doc as any).existing).toBe('prop') - }) - }) -}) diff --git a/foundations/core/packages/core/src/__tests__/operator-bugs.test.ts b/foundations/core/packages/core/src/__tests__/operator-bugs.test.ts deleted file mode 100644 index 50e0744d0c..0000000000 --- a/foundations/core/packages/core/src/__tests__/operator-bugs.test.ts +++ /dev/null @@ -1,245 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { _getOperator } from '../operator' -import type { Doc } from '../classes' - -describe('operator edge cases and potential bugs', () => { - describe('$push operator edge cases', () => { - it('BUG: $push with $each on null field should handle gracefully', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, arr: null } as unknown as Doc - const operator = _getOperator('$push') - - // This exposes a bug: when arr is null and we use $each, nothing happens - operator(doc, { arr: { $each: [1, 2, 3] } }) - - // Expected: should initialize array and add items - // Actual: arr remains null (bug!) - // Uncomment to see the bug: - // expect((doc as any).arr).toEqual([1, 2, 3]) - - // Current behavior (documents the bug): - expect((doc as any).arr).toBe(null) - }) - - it('BUG: $push with $each on non-array field should handle gracefully', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, arr: 'string' } as unknown as Doc - const operator = _getOperator('$push') - - // This exposes a bug: when arr is a non-array and we use $each, nothing happens - operator(doc, { arr: { $each: [1, 2, 3] } }) - - // Expected: should report error and replace with array - // Actual: arr remains a string (bug!) - // Uncomment to see the bug: - // expect((doc as any).arr).toEqual([1, 2, 3]) - - // Current behavior (documents the bug): - expect((doc as any).arr).toBe('string') - }) - - it('$push without $each on null field works correctly', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, arr: null } as unknown as Doc - const operator = _getOperator('$push') - - operator(doc, { arr: 'value' }) - - // This works correctly - replaces null with array - expect((doc as any).arr).toEqual(['value']) - }) - - it('$push without $each on non-array field reports error and fixes', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, arr: 'string' } as unknown as Doc - const operator = _getOperator('$push') - - // This should log an error via Analytics.handleError - operator(doc, { arr: 'value' }) - - // This works - replaces non-array with array - expect((doc as any).arr).toEqual(['value']) - }) - }) - - describe('$pull operator edge cases', () => { - it('should initialize undefined field as empty array', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any } as unknown as Doc - const operator = _getOperator('$pull') - - operator(doc, { arr: 'value' }) - - expect((doc as any).arr).toEqual([]) - }) - - it('should handle $in with empty array', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, arr: [1, 2, 3] } as unknown as Doc - const operator = _getOperator('$pull') - - operator(doc, { arr: { $in: [] } }) - - // Nothing should be removed - expect((doc as any).arr).toEqual([1, 2, 3]) - }) - - it('should handle complex objects with partial matches', () => { - const doc: Doc = { - _id: '1' as any, - _class: 'test' as any, - arr: [ - { a: 1, b: 2, c: 3 }, - { a: 1, b: 2, c: 4 }, - { a: 2, b: 2, c: 3 } - ] - } as unknown as Doc - - const operator = _getOperator('$pull') - - // Should only remove objects where ALL fields match - operator(doc, { arr: { a: 1, b: 2 } }) - - // Neither object should be removed because c doesn't match - expect((doc as any).arr).toEqual([{ a: 2, b: 2, c: 3 }]) - }) - }) - - describe('$update operator edge cases', () => { - it('should handle query with multiple conditions', () => { - const doc: Doc = { - _id: '1' as any, - _class: 'test' as any, - arr: [ - { name: 'a', status: 'active', value: 1 }, - { name: 'a', status: 'inactive', value: 2 }, - { name: 'b', status: 'active', value: 3 } - ] - } as unknown as Doc - - const operator = _getOperator('$update') - - operator(doc, { - arr: { - $query: { name: 'a', status: 'active' }, - $update: { value: 100 } - } - }) - - expect((doc as any).arr).toEqual([ - { name: 'a', status: 'active', value: 100 }, - { name: 'a', status: 'inactive', value: 2 }, - { name: 'b', status: 'active', value: 3 } - ]) - }) - - it('should add new fields in update', () => { - const doc: Doc = { - _id: '1' as any, - _class: 'test' as any, - arr: [{ name: 'a' }] - } as unknown as Doc - - const operator = _getOperator('$update') - - operator(doc, { - arr: { - $query: { name: 'a' }, - $update: { newField: 'newValue' } - } - }) - - expect((doc as any).arr).toEqual([{ name: 'a', newField: 'newValue' }]) - }) - }) - - describe('$inc operator edge cases', () => { - it('should handle floating point numbers', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, value: 1.5 } as unknown as Doc - const operator = _getOperator('$inc') - - operator(doc, { value: 2.3 }) - - expect((doc as any).value).toBeCloseTo(3.8) - }) - - it('should handle very large numbers', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, value: Number.MAX_SAFE_INTEGER - 1 } as unknown as Doc - const operator = _getOperator('$inc') - - operator(doc, { value: 1 }) - - expect((doc as any).value).toBe(Number.MAX_SAFE_INTEGER) - }) - - it('should handle negative numbers to zero', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, count: 5 } as unknown as Doc - const operator = _getOperator('$inc') - - operator(doc, { count: -5 }) - - expect((doc as any).count).toBe(0) - }) - }) - - describe('$rename operator edge cases', () => { - it('BUG: should handle renaming to the same name causes deletion', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, field: 'value' } as unknown as Doc - const operator = _getOperator('$rename') - - operator(doc, { field: 'field' }) - - // BUG: Renaming to same name causes deletion! - // The implementation deletes first, then sets, but uses the same key - expect((doc as any).field).toBeUndefined() - }) - - it('BUG: chain renaming has race condition', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, a: 1, b: 2, c: 3 } as unknown as Doc - const operator = _getOperator('$rename') - - // BUG: This creates a potential issue with chained renames in same operation - operator(doc, { a: 'b', b: 'c' }) - - // The order of operations in the for loop determines outcome - // a -> b happens first (sets b to 1, deletes a) - // b -> c happens second (sets c to 1 [current value of b], deletes b) - expect((doc as any).a).toBeUndefined() - expect((doc as any).b).toBeUndefined() - expect((doc as any).c).toBe(1) // Gets the value that was assigned to b from a - }) - }) - - describe('$unset operator edge cases', () => { - it('should handle unsetting nested properties (only top level)', () => { - const doc: Doc = { - _id: '1' as any, - _class: 'test' as any, - obj: { nested: 'value', other: 'data' } - } as unknown as Doc - const operator = _getOperator('$unset') - - // $unset only works at the key level, not nested paths - operator(doc, { obj: '' }) - - expect((doc as any).obj).toBeUndefined() - }) - - it('should handle unsetting array elements', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, arr: [1, 2, 3] } as unknown as Doc - const operator = _getOperator('$unset') - - operator(doc, { arr: '' }) - - expect((doc as any).arr).toBeUndefined() - }) - }) -}) diff --git a/foundations/core/packages/core/src/__tests__/operator.test.ts b/foundations/core/packages/core/src/__tests__/operator.test.ts deleted file mode 100644 index a3b6d7adad..0000000000 --- a/foundations/core/packages/core/src/__tests__/operator.test.ts +++ /dev/null @@ -1,535 +0,0 @@ -// -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// Copyright © 2021 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { _getOperator, isOperator } from '../operator' -import type { Doc } from '../classes' - -describe('operator', () => { - describe('isOperator', () => { - it('should return true for operator objects', () => { - expect(isOperator({ $push: { arr: 1 } })).toBe(true) - expect(isOperator({ $pull: { arr: 1 } })).toBe(true) - expect(isOperator({ $inc: { count: 1 } })).toBe(true) - expect(isOperator({ $unset: { field: '' } })).toBe(true) - expect(isOperator({ $rename: { oldField: 'newField' } })).toBe(true) - expect(isOperator({ $update: { arr: {} } })).toBe(true) - }) - - it('should return true for multiple operators', () => { - expect(isOperator({ $push: { arr: 1 }, $inc: { count: 1 } })).toBe(true) - }) - - it('should return false for non-operator objects', () => { - expect(isOperator({ field: 'value' })).toBe(false) - expect(isOperator({ _id: '123' })).toBe(false) - expect(isOperator({ name: 'test', value: 123 })).toBe(false) - }) - - it('should return false for empty objects', () => { - expect(isOperator({})).toBe(false) - }) - - it('should return false for null or non-objects', () => { - expect(isOperator(null as any)).toBe(false) - expect(isOperator(undefined as any)).toBe(false) - expect(isOperator('string' as any)).toBe(false) - expect(isOperator(123 as any)).toBe(false) - expect(isOperator([] as any)).toBe(false) - }) - - it('should return false for mixed operator and non-operator keys', () => { - expect(isOperator({ $push: { arr: 1 }, field: 'value' })).toBe(false) - }) - }) - - describe('$push operator', () => { - it('should push a single value to an array', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any } as unknown as Doc - const operator = _getOperator('$push') - operator(doc, { arr: 'value' }) - expect((doc as any).arr).toEqual(['value']) - }) - - it('should push multiple values with $each', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, arr: [1, 2] } as unknown as Doc - const operator = _getOperator('$push') - operator(doc, { arr: { $each: [3, 4, 5] } }) - expect((doc as any).arr).toEqual([3, 4, 5, 1, 2]) - }) - - it('should push with $position', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, arr: [1, 2, 3] } as unknown as Doc - const operator = _getOperator('$push') - operator(doc, { arr: { $each: [10, 20], $position: 1 } }) - expect((doc as any).arr).toEqual([1, 10, 20, 2, 3]) - }) - - it('should push object to array', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, arr: [] } as unknown as Doc - const operator = _getOperator('$push') - const obj = { name: 'test', value: 123 } - operator(doc, { arr: obj }) - expect((doc as any).arr).toEqual([obj]) - }) - - it('should initialize undefined field as array', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any } as unknown as Doc - const operator = _getOperator('$push') - operator(doc, { newArr: 'value' }) - expect((doc as any).newArr).toEqual(['value']) - }) - - it('should handle null array field', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, arr: null } as any - const operator = _getOperator('$push') - operator(doc, { arr: 'value' }) - expect((doc as any).arr).toEqual(['value']) - }) - - it('should handle pushing to non-array field', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, field: 'string' } as unknown as Doc - const operator = _getOperator('$push') - operator(doc, { field: 'value' }) - // Should replace non-array with array - expect((doc as any).field).toEqual(['value']) - }) - }) - - describe('$pull operator', () => { - it('should pull a single value from array', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, arr: [1, 2, 3, 2] } as unknown as Doc - const operator = _getOperator('$pull') - operator(doc, { arr: 2 }) - expect((doc as any).arr).toEqual([1, 3]) - }) - - it('should pull multiple values with $in', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, arr: [1, 2, 3, 4, 5] } as unknown as Doc - const operator = _getOperator('$pull') - operator(doc, { arr: { $in: [2, 4] } }) - expect((doc as any).arr).toEqual([1, 3, 5]) - }) - - it('should pull objects matching all fields', () => { - const doc: Doc = { - _id: '1' as any, - _class: 'test' as any, - arr: [ - { name: 'a', value: 1 }, - { name: 'b', value: 2 }, - { name: 'a', value: 3 } - ] - } as unknown as Doc - const operator = _getOperator('$pull') - operator(doc, { arr: { name: 'a', value: 1 } }) - expect((doc as any).arr).toEqual([ - { name: 'b', value: 2 }, - { name: 'a', value: 3 } - ]) - }) - - it('should handle undefined field', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any } as unknown as Doc - const operator = _getOperator('$pull') - operator(doc, { arr: 'value' }) - expect((doc as any).arr).toEqual([]) - }) - - it('should handle null array', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, arr: null } as any - const operator = _getOperator('$pull') - operator(doc, { arr: 'value' }) - expect((doc as any).arr).toEqual([]) - }) - - it('should not pull when object fields do not all match', () => { - const doc: Doc = { - _id: '1' as any, - _class: 'test' as any, - arr: [ - { name: 'a', value: 1 }, - { name: 'b', value: 2 } - ] - } as unknown as Doc - const operator = _getOperator('$pull') - operator(doc, { arr: { name: 'a', value: 999 } }) - expect((doc as any).arr).toEqual([ - { name: 'a', value: 1 }, - { name: 'b', value: 2 } - ]) - }) - }) - - describe('$update operator', () => { - it('should update matching array elements', () => { - const doc: Doc = { - _id: '1' as any, - _class: 'test' as any, - arr: [ - { name: 'a', value: 1 }, - { name: 'b', value: 2 }, - { name: 'a', value: 3 } - ] - } as unknown as Doc - const operator = _getOperator('$update') - operator(doc, { - arr: { - $query: { name: 'a' }, - $update: { value: 100 } - } - }) - expect((doc as any).arr).toEqual([ - { name: 'a', value: 100 }, - { name: 'b', value: 2 }, - { name: 'a', value: 100 } - ]) - }) - - it('should handle empty array', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, arr: [] } as unknown as Doc - const operator = _getOperator('$update') - operator(doc, { - arr: { - $query: { name: 'a' }, - $update: { value: 100 } - } - }) - expect((doc as any).arr).toEqual([]) - }) - - it('should handle undefined field', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any } as unknown as Doc - const operator = _getOperator('$update') - operator(doc, { - arr: { - $query: { name: 'a' }, - $update: { value: 100 } - } - }) - expect((doc as any).arr).toEqual([]) - }) - - it('should update multiple fields', () => { - const doc: Doc = { - _id: '1' as any, - _class: 'test' as any, - arr: [{ name: 'a', value: 1, extra: 'old' }] - } as unknown as Doc - const operator = _getOperator('$update') - operator(doc, { - arr: { - $query: { name: 'a' }, - $update: { value: 100, extra: 'new' } - } - }) - expect((doc as any).arr).toEqual([{ name: 'a', value: 100, extra: 'new' }]) - }) - - it('should not update non-matching elements', () => { - const doc: Doc = { - _id: '1' as any, - _class: 'test' as any, - arr: [ - { name: 'a', value: 1 }, - { name: 'b', value: 2 } - ] - } as unknown as Doc - const operator = _getOperator('$update') - operator(doc, { - arr: { - $query: { name: 'c' }, - $update: { value: 100 } - } - }) - expect((doc as any).arr).toEqual([ - { name: 'a', value: 1 }, - { name: 'b', value: 2 } - ]) - }) - }) - - describe('$inc operator', () => { - it('should increment existing number', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, count: 5 } as unknown as Doc - const operator = _getOperator('$inc') - operator(doc, { count: 3 }) - expect((doc as any).count).toBe(8) - }) - - it('should initialize undefined field to increment value', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any } as unknown as Doc - const operator = _getOperator('$inc') - operator(doc, { count: 10 }) - expect((doc as any).count).toBe(10) - }) - - it('should handle negative increment', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, count: 10 } as unknown as Doc - const operator = _getOperator('$inc') - operator(doc, { count: -3 }) - expect((doc as any).count).toBe(7) - }) - - it('should increment multiple fields', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, a: 1, b: 2 } as unknown as Doc - const operator = _getOperator('$inc') - operator(doc, { a: 10, b: 20 }) - expect((doc as any).a).toBe(11) - expect((doc as any).b).toBe(22) - }) - - it('should handle zero increment', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, count: 5 } as unknown as Doc - const operator = _getOperator('$inc') - operator(doc, { count: 0 }) - expect((doc as any).count).toBe(5) - }) - }) - - describe('$unset operator', () => { - it('should remove existing field', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, field: 'value' } as unknown as Doc - const operator = _getOperator('$unset') - operator(doc, { field: '' }) - expect((doc as any).field).toBeUndefined() - }) - - it('should handle undefined field', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any } as unknown as Doc - const operator = _getOperator('$unset') - operator(doc, { field: '' }) - expect((doc as any).field).toBeUndefined() - }) - - it('should unset multiple fields', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, a: 1, b: 2, c: 3 } as unknown as Doc - const operator = _getOperator('$unset') - operator(doc, { a: '', c: '' }) - expect((doc as any).a).toBeUndefined() - expect((doc as any).b).toBe(2) - expect((doc as any).c).toBeUndefined() - }) - - it('should unset nested objects', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, obj: { nested: 'value' } } as unknown as Doc - const operator = _getOperator('$unset') - operator(doc, { obj: '' }) - expect((doc as any).obj).toBeUndefined() - }) - }) - - describe('$rename operator', () => { - it('should rename existing field', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, oldName: 'value' } as unknown as Doc - const operator = _getOperator('$rename') - operator(doc, { oldName: 'newName' }) - expect((doc as any).oldName).toBeUndefined() - expect((doc as any).newName).toBe('value') - }) - - it('should handle undefined field', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any } as unknown as Doc - const operator = _getOperator('$rename') - operator(doc, { oldName: 'newName' }) - expect((doc as any).oldName).toBeUndefined() - expect((doc as any).newName).toBeUndefined() - }) - - it('should rename multiple fields', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, a: 1, b: 2 } as unknown as Doc - const operator = _getOperator('$rename') - operator(doc, { a: 'x', b: 'y' }) - expect((doc as any).a).toBeUndefined() - expect((doc as any).b).toBeUndefined() - expect((doc as any).x).toBe(1) - expect((doc as any).y).toBe(2) - }) - - it('should overwrite existing field with same name', () => { - const doc: Doc = { - _id: '1' as any, - _class: 'test' as any, - old: 'oldValue', - new: 'existingValue' - } as unknown as Doc - const operator = _getOperator('$rename') - operator(doc, { old: 'new' }) - expect((doc as any).old).toBeUndefined() - expect((doc as any).new).toBe('oldValue') - }) - - it('should preserve object references when renaming', () => { - const obj = { nested: 'value' } - const doc: Doc = { _id: '1' as any, _class: 'test' as any, oldName: obj } as unknown as Doc - const operator = _getOperator('$rename') - operator(doc, { oldName: 'newName' }) - expect((doc as any).newName).toBe(obj) - }) - }) - - describe('_getOperator', () => { - it('should return correct operator functions', () => { - expect(_getOperator('$push')).toBeDefined() - expect(_getOperator('$pull')).toBeDefined() - expect(_getOperator('$update')).toBeDefined() - expect(_getOperator('$inc')).toBeDefined() - expect(_getOperator('$unset')).toBeDefined() - expect(_getOperator('$rename')).toBeDefined() - }) - - it('should throw error for unknown operator', () => { - expect(() => _getOperator('$unknown')).toThrow('unknown operator: $unknown') - expect(() => _getOperator('$invalid')).toThrow('unknown operator: $invalid') - }) - }) - - describe('operator error handling', () => { - describe('$pull with non-array values', () => { - it('should handle string value instead of array', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, field: 'not-an-array' } as unknown as Doc - const operator = _getOperator('$pull') - operator(doc, { field: 'value' }) - expect((doc as any).field).toEqual([]) - }) - - it('should handle object value instead of array', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, field: { key: 'value' } } as unknown as Doc - const operator = _getOperator('$pull') - operator(doc, { field: 'value' }) - expect((doc as any).field).toEqual([]) - }) - - it('should handle number value instead of array', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, field: 42 } as unknown as Doc - const operator = _getOperator('$pull') - operator(doc, { field: 'value' }) - expect((doc as any).field).toEqual([]) - }) - - it('should handle null value', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, field: null } as unknown as Doc - const operator = _getOperator('$pull') - operator(doc, { field: 'value' }) - expect((doc as any).field).toEqual([]) - }) - - it('should work correctly with valid array', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, field: [1, 2, 3] } as unknown as Doc - const operator = _getOperator('$pull') - operator(doc, { field: 2 }) - expect((doc as any).field).toEqual([1, 3]) - }) - }) - - describe('$update with non-array values', () => { - it('should handle string value instead of array', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, field: 'not-an-array' } as unknown as Doc - const operator = _getOperator('$update') - operator(doc, { field: { $query: { id: 1 }, $update: { value: 'new' } } }) - expect((doc as any).field).toEqual([]) - }) - - it('should handle object value instead of array', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, field: { key: 'value' } } as unknown as Doc - const operator = _getOperator('$update') - operator(doc, { field: { $query: { id: 1 }, $update: { value: 'new' } } }) - expect((doc as any).field).toEqual([]) - }) - - it('should handle null value', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, field: null } as unknown as Doc - const operator = _getOperator('$update') - operator(doc, { field: { $query: { id: 1 }, $update: { value: 'new' } } }) - expect((doc as any).field).toEqual([]) - }) - - it('should work correctly with valid array', () => { - const doc: Doc = { - _id: '1' as any, - _class: 'test' as any, - field: [ - { id: 1, value: 'old' }, - { id: 2, value: 'keep' } - ] - } as unknown as Doc - const operator = _getOperator('$update') - operator(doc, { field: { $query: { id: 1 }, $update: { value: 'new' } } }) - expect((doc as any).field).toEqual([ - { id: 1, value: 'new' }, - { id: 2, value: 'keep' } - ]) - }) - }) - - describe('$inc with non-numeric values', () => { - it('should handle NaN current value', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, count: NaN } as unknown as Doc - const operator = _getOperator('$inc') - operator(doc, { count: 5 }) - expect((doc as any).count).toBe(5) - }) - - it('should handle string current value', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, count: 'not-a-number' } as unknown as Doc - const operator = _getOperator('$inc') - operator(doc, { count: 5 }) - expect((doc as any).count).toBe(5) - }) - - it('should handle NaN increment value', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, count: 10 } as unknown as Doc - const operator = _getOperator('$inc') - operator(doc, { count: NaN }) - expect((doc as any).count).toBe(10) - }) - - it('should handle string increment value', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, count: 10 } as unknown as Doc - const operator = _getOperator('$inc') - operator(doc, { count: 'not-a-number' as any }) - expect((doc as any).count).toBe(10) - }) - - it('should handle undefined current value (should default to 0)', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any } as unknown as Doc - const operator = _getOperator('$inc') - operator(doc, { count: 5 }) - expect((doc as any).count).toBe(5) - }) - - it('should work correctly with valid numbers', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, count: 10 } as unknown as Doc - const operator = _getOperator('$inc') - operator(doc, { count: 5 }) - expect((doc as any).count).toBe(15) - }) - - it('should handle negative increments', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, count: 10 } as unknown as Doc - const operator = _getOperator('$inc') - operator(doc, { count: -3 }) - expect((doc as any).count).toBe(7) - }) - - it('should handle zero increment', () => { - const doc: Doc = { _id: '1' as any, _class: 'test' as any, count: 10 } as unknown as Doc - const operator = _getOperator('$inc') - operator(doc, { count: 0 }) - expect((doc as any).count).toBe(10) - }) - }) - }) -}) diff --git a/foundations/core/packages/core/src/__tests__/rate-limiter.test.ts b/foundations/core/packages/core/src/__tests__/rate-limiter.test.ts deleted file mode 100644 index 5ae22898a3..0000000000 --- a/foundations/core/packages/core/src/__tests__/rate-limiter.test.ts +++ /dev/null @@ -1,449 +0,0 @@ -// -// Copyright © 2023 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { RateLimiter } from '../utils' - -describe('RateLimiter', () => { - describe('constructor', () => { - it('should create limiter with specified rate', () => { - const limiter = new RateLimiter(5) - expect(limiter.rate).toBe(5) - expect(limiter.idCounter).toBe(0) - expect(limiter.processingQueue.size).toBe(0) - }) - - it('should handle rate of 1', () => { - const limiter = new RateLimiter(1) - expect(limiter.rate).toBe(1) - }) - - it('should handle large rates', () => { - const limiter = new RateLimiter(1000) - expect(limiter.rate).toBe(1000) - }) - }) - - describe('exec', () => { - it('should execute single operation immediately', async () => { - const limiter = new RateLimiter(1) - const mockFn = jest.fn().mockResolvedValue('result') - - const result = await limiter.exec(mockFn) - - expect(mockFn).toHaveBeenCalledTimes(1) - expect(result).toBe('result') - expect(limiter.processingQueue.size).toBe(0) - }) - - it('should execute operations up to rate limit concurrently', async () => { - const limiter = new RateLimiter(3) - let runningCount = 0 - let maxRunning = 0 - - const mockFn = jest.fn().mockImplementation(async () => { - runningCount++ - maxRunning = Math.max(maxRunning, runningCount) - await new Promise((resolve) => setTimeout(resolve, 10)) - runningCount-- - return 'result' - }) - - const operations = [ - limiter.exec(mockFn), - limiter.exec(mockFn), - limiter.exec(mockFn), - limiter.exec(mockFn), - limiter.exec(mockFn) - ] - - await Promise.all(operations) - - expect(mockFn).toHaveBeenCalledTimes(5) - expect(maxRunning).toBeLessThanOrEqual(3) - }) - - it('should queue operations beyond rate limit', async () => { - const limiter = new RateLimiter(2) - const order: number[] = [] - - const createOperation = (id: number) => async () => { - order.push(id) - await new Promise((resolve) => setTimeout(resolve, 10)) - return id - } - - const operations = [ - limiter.exec(createOperation(1)), - limiter.exec(createOperation(2)), - limiter.exec(createOperation(3)), - limiter.exec(createOperation(4)) - ] - - await Promise.all(operations) - - expect(order).toEqual([1, 2, 3, 4]) - }) - - it('should handle operation errors correctly', async () => { - const limiter = new RateLimiter(2) - const mockFn = jest.fn().mockRejectedValue(new Error('Operation failed')) - - await expect(limiter.exec(mockFn)).rejects.toThrow('Operation failed') - expect(limiter.processingQueue.size).toBe(0) - }) - - it('should continue processing after error', async () => { - const limiter = new RateLimiter(1) - const successFn = jest.fn().mockResolvedValue('success') - const errorFn = jest.fn().mockRejectedValue(new Error('error')) - - await expect(limiter.exec(errorFn)).rejects.toThrow('error') - const result = await limiter.exec(successFn) - - expect(result).toBe('success') - expect(successFn).toHaveBeenCalledTimes(1) - }) - - it('should pass arguments to operations', async () => { - const limiter = new RateLimiter(1) - const mockFn = jest.fn().mockImplementation(async (args?: any) => { - return args?.value - }) - - const result = await limiter.exec(mockFn, { value: 42 }) - - expect(result).toBe(42) - expect(mockFn).toHaveBeenCalledWith({ value: 42 }) - }) - - it('should handle operations without arguments', async () => { - const limiter = new RateLimiter(1) - const mockFn = jest.fn().mockResolvedValue('no-args') - - const result = await limiter.exec(mockFn) - - expect(result).toBe('no-args') - expect(mockFn).toHaveBeenCalledWith(undefined) - }) - - it('should increment idCounter for each operation', async () => { - const limiter = new RateLimiter(10) - const mockFn = jest.fn().mockResolvedValue('result') - - await limiter.exec(mockFn) - expect(limiter.idCounter).toBe(1) - - await limiter.exec(mockFn) - expect(limiter.idCounter).toBe(2) - - await limiter.exec(mockFn) - expect(limiter.idCounter).toBe(3) - }) - - it('should notify waiting operations when slots become available', async () => { - const limiter = new RateLimiter(1) - const order: string[] = [] - - const op1 = async (): Promise => { - order.push('op1-start') - await new Promise((resolve) => setTimeout(resolve, 20)) - order.push('op1-end') - return 'op1' - } - - const op2 = async (): Promise => { - order.push('op2-start') - await new Promise((resolve) => setTimeout(resolve, 10)) - order.push('op2-end') - return 'op2' - } - - const results = await Promise.all([limiter.exec(op1), limiter.exec(op2)]) - - expect(results).toEqual(['op1', 'op2']) - expect(order).toEqual(['op1-start', 'op1-end', 'op2-start', 'op2-end']) - }) - - it('should handle rapid sequential operations', async () => { - const limiter = new RateLimiter(2) - const mockFn = jest.fn().mockResolvedValue('result') - const results = [] - - for (let i = 0; i < 10; i++) { - results.push(limiter.exec(mockFn)) - } - - await Promise.all(results) - expect(mockFn).toHaveBeenCalledTimes(10) - }) - - it('should clean up processingQueue after completion', async () => { - const limiter = new RateLimiter(5) - const mockFn = jest.fn().mockImplementation(async () => { - await new Promise((resolve) => setTimeout(resolve, 5)) - return 'result' - }) - - const operations = Array(5) - .fill(0) - .map(() => limiter.exec(mockFn)) - - await Promise.all(operations) - expect(limiter.processingQueue.size).toBe(0) - }) - }) - - describe('add', () => { - it('should add operation without waiting for result', async () => { - const limiter = new RateLimiter(1) - let executed = false - const mockFn = jest.fn().mockImplementation(async () => { - await new Promise((resolve) => setTimeout(resolve, 10)) - executed = true - return 'result' - }) - - await limiter.add(mockFn) - - // add returns immediately, but operation may not be complete - expect(mockFn).toHaveBeenCalledTimes(1) - - // Wait for operation to complete - await new Promise((resolve) => setTimeout(resolve, 20)) - expect(executed).toBe(true) - }) - - it('should handle errors with error handler', async () => { - const limiter = new RateLimiter(1) - const errorHandler = jest.fn() - const mockFn = jest.fn().mockRejectedValue(new Error('test error')) - - await limiter.add(mockFn, undefined, errorHandler) - - // Wait for operation to execute - await new Promise((resolve) => setTimeout(resolve, 10)) - - expect(errorHandler).toHaveBeenCalledWith(new Error('test error')) - }) - - it('should log errors when no error handler provided', async () => { - const limiter = new RateLimiter(1) - const consoleSpy = jest.spyOn(console, 'error').mockImplementation() - const mockFn = jest.fn().mockRejectedValue(new Error('test error')) - - await limiter.add(mockFn) - - // Wait for operation to execute - await new Promise((resolve) => setTimeout(resolve, 10)) - - expect(consoleSpy).toHaveBeenCalled() - consoleSpy.mockRestore() - }) - - it('should pass arguments to operation', async () => { - const limiter = new RateLimiter(1) - const mockFn = jest.fn().mockResolvedValue('result') - - await limiter.add(mockFn, { test: 'value' }) - - // Wait for operation to start - await new Promise((resolve) => setTimeout(resolve, 10)) - - expect(mockFn).toHaveBeenCalledWith({ test: 'value' }) - }) - - it('should queue multiple add operations', async () => { - const limiter = new RateLimiter(1) - const order: number[] = [] - - const createOp = (id: number) => async () => { - order.push(id) - await new Promise((resolve) => setTimeout(resolve, 10)) - } - - await limiter.add(createOp(1)) - await limiter.add(createOp(2)) - await limiter.add(createOp(3)) - - // Wait for all operations to complete - await limiter.waitProcessing() - - expect(order).toEqual([1, 2, 3]) - }) - }) - - describe('waitProcessing', () => { - it('should wait for all operations to complete', async () => { - const limiter = new RateLimiter(2) - const mockFn = jest.fn().mockImplementation(async () => { - await new Promise((resolve) => setTimeout(resolve, 20)) - return 'result' - }) - - const operations = [limiter.exec(mockFn), limiter.exec(mockFn), limiter.exec(mockFn)] - - expect(limiter.processingQueue.size).toBeGreaterThan(0) - - await limiter.waitProcessing() - - expect(limiter.processingQueue.size).toBe(0) - await Promise.all(operations) - }) - - it('should resolve immediately when no operations are processing', async () => { - const limiter = new RateLimiter(1) - - await limiter.waitProcessing() - - expect(limiter.processingQueue.size).toBe(0) - }) - - it('should wait for operations added via add method', async () => { - const limiter = new RateLimiter(1) - let completed = false - const mockFn = jest.fn().mockImplementation(async () => { - await new Promise((resolve) => setTimeout(resolve, 20)) - completed = true - }) - - await limiter.add(mockFn) - expect(completed).toBe(false) - - await limiter.waitProcessing() - expect(completed).toBe(true) - }) - }) - - describe('edge cases', () => { - it('should handle zero rate (should not happen but test defensively)', async () => { - const limiter = new RateLimiter(0) - const mockFn = jest.fn().mockResolvedValue('result') - - // This will hang forever, so we use Promise.race with timeout - const timeoutPromise = new Promise((resolve) => { - setTimeout(() => { - resolve('timeout') - }, 100) - }) - const result = await Promise.race([limiter.exec(mockFn), timeoutPromise]) - - expect(result).toBe('timeout') - }) - - it('should handle very long running operations', async () => { - const limiter = new RateLimiter(1) - const mockFn = jest.fn().mockImplementation(async () => { - await new Promise((resolve) => setTimeout(resolve, 50)) - return 'result' - }) - - const start = Date.now() - await limiter.exec(mockFn) - const duration = Date.now() - start - - expect(duration).toBeGreaterThanOrEqual(50) - }) - - it('should handle mixed sync and async operations', async () => { - const limiter = new RateLimiter(2) - const syncOp = jest.fn().mockResolvedValue('sync') - const asyncOp = jest.fn().mockImplementation(async () => { - await new Promise((resolve) => setTimeout(resolve, 10)) - return 'async' - }) - - const results = await Promise.all([limiter.exec(syncOp), limiter.exec(asyncOp)]) - - expect(results).toContain('sync') - expect(results).toContain('async') - }) - - it('should handle operations that throw synchronously', async () => { - const limiter = new RateLimiter(1) - const mockFn = jest.fn().mockImplementation(() => { - throw new Error('Sync error') - }) - - await expect(limiter.exec(mockFn)).rejects.toThrow('Sync error') - expect(limiter.processingQueue.size).toBe(0) - }) - - it('should handle notify array correctly when multiple operations wait', async () => { - const limiter = new RateLimiter(1) - const order: number[] = [] - - const createOp = (id: number) => async () => { - order.push(id) - await new Promise((resolve) => setTimeout(resolve, 10)) - return id - } - - // Start multiple operations that will need to wait - const operations = [ - limiter.exec(createOp(1)), - limiter.exec(createOp(2)), - limiter.exec(createOp(3)), - limiter.exec(createOp(4)) - ] - - await Promise.all(operations) - - expect(order).toEqual([1, 2, 3, 4]) - expect(limiter.notify.length).toBe(0) - }) - }) - - describe('concurrent stress test', () => { - it('should handle many concurrent operations correctly', async () => { - const limiter = new RateLimiter(5) - const mockFn = jest.fn().mockImplementation(async (args?: any) => { - await new Promise((resolve) => setTimeout(resolve, Math.random() * 10)) - return args?.id - }) - - const operations = Array(50) - .fill(0) - .map((_, i) => limiter.exec(mockFn, { id: i })) - - const results = await Promise.all(operations) - - expect(mockFn).toHaveBeenCalledTimes(50) - expect(results).toHaveLength(50) - expect(limiter.processingQueue.size).toBe(0) - }) - - it('should maintain rate limit under heavy load', async () => { - const limiter = new RateLimiter(3) - let currentCount = 0 - let maxConcurrent = 0 - - const mockFn = jest.fn().mockImplementation(async () => { - currentCount++ - maxConcurrent = Math.max(maxConcurrent, currentCount) - await new Promise((resolve) => setTimeout(resolve, 10)) - currentCount-- - }) - - const operations = Array(20) - .fill(0) - .map(() => limiter.exec(mockFn)) - - await Promise.all(operations) - - expect(maxConcurrent).toBeLessThanOrEqual(3) - expect(currentCount).toBe(0) - }) - }) -}) diff --git a/foundations/core/packages/core/src/__tests__/test.json b/foundations/core/packages/core/src/__tests__/test.json deleted file mode 100644 index fc22aa8e01..0000000000 --- a/foundations/core/packages/core/src/__tests__/test.json +++ /dev/null @@ -1,126 +0,0 @@ -[ - { - "_class": "core:class:Class", - "_id": "core:class:Obj", - "createdBy": "core:account:System", - "createdOn": 1729970852414, - "kind": 0, - "label": "Obj", - "modifiedBy": "core:account:System", - "modifiedOn": 1729970852414, - "space": "core:space:Model" - }, - { - "_class": "core:class:Class", - "_id": "core:class:Doc", - "createdBy": "core:account:System", - "createdOn": 1729970852414, - "extends": "core:class:Obj", - "kind": 0, - "label": "Doc", - "modifiedBy": "core:account:System", - "modifiedOn": 1729970852414, - "space": "core:space:Model" - }, - { - "_class": "core:class:Class", - "_id": "core:class:AttachedDoc", - "createdBy": "core:account:System", - "createdOn": 1729970852414, - "extends": "core:class:Doc", - "kind": 2, - "label": "AttachedDoc", - "modifiedBy": "core:account:System", - "modifiedOn": 1729970852414, - "space": "core:space:Model" - }, - { - "_class": "core:class:Class", - "_id": "core:class:Class", - "createdBy": "core:account:System", - "createdOn": 1729970852414, - "domain": "model", - "extends": "core:class:Doc", - "kind": 0, - "label": "Class", - "modifiedBy": "core:account:System", - "modifiedOn": 1729970852414, - "space": "core:space:Model" - }, - { - "_class": "core:class:Class", - "_id": "core:class:Interface", - "createdBy": "core:account:System", - "createdOn": 1729970852414, - "extends": "core:class:Doc", - "kind": 0, - "label": "Interface", - "modifiedBy": "core:account:System", - "modifiedOn": 1729970852414, - "space": "core:space:Model" - }, - { - "_class": "core:class:Class", - "_id": "core:class:Space", - "createdBy": "core:account:System", - "createdOn": 1729970852414, - "domain": "model", - "extends": "core:class:Doc", - "kind": 0, - "label": "Space", - "modifiedBy": "core:account:System", - "modifiedOn": 1729970852414, - "space": "core:space:Model" - }, - { - "_class": "core:class:Class", - "_id": "core:class:Account", - "createdBy": "core:account:System", - "createdOn": 1729970852414, - "domain": "model", - "extends": "core:class:Doc", - "kind": 0, - "label": "Account", - "modifiedBy": "core:account:System", - "modifiedOn": 1729970852414, - "space": "core:space:Model" - }, - { - "_class": "core:class:Interface", - "_id": "test:interface:WithState", - "createdBy": "core:account:System", - "createdOn": 1729970852414, - "extends": [], - "kind": 1, - "label": "WithState", - "modifiedBy": "core:account:System", - "modifiedOn": 1729970852414, - "space": "core:space:Model" - }, - { - "_class": "core:class:Class", - "_id": "core:class:Tx", - "createdBy": "core:account:System", - "createdOn": 1729970852414, - "domain": "tx", - "extends": "core:class:Doc", - "kind": 0, - "label": "Tx", - "modifiedBy": "core:account:System", - "modifiedOn": 1729970852414, - "space": "core:space:Model" - }, - { - "_class": "core:class:Class", - "_id": "core:class:TxCUD", - "createdBy": "core:account:System", - "createdOn": 1729970852414, - "domain": "tx", - "extends": "core:class:Tx", - "kind": 0, - "label": "TxCUD", - "modifiedBy": "core:account:System", - "modifiedOn": 1729970852414, - "space": "core:space:Model" - } -] diff --git a/foundations/core/packages/core/src/__tests__/time.test.ts b/foundations/core/packages/core/src/__tests__/time.test.ts deleted file mode 100644 index f2d756cd13..0000000000 --- a/foundations/core/packages/core/src/__tests__/time.test.ts +++ /dev/null @@ -1,398 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { type Timestamp } from '../classes' -import { getDay, convertToDay } from '..' - -const supportedTimezones: string[] = [ - 'Europe/Andorra', - 'Asia/Dubai', - 'Asia/Kabul', - 'Europe/Tirane', - 'Asia/Yerevan', - 'Antarctica/Casey', - 'Antarctica/Davis', - 'Antarctica/DumontDUrville', - 'Antarctica/Mawson', - 'Antarctica/Palmer', - 'Antarctica/Rothera', - 'Antarctica/Syowa', - 'Antarctica/Troll', - 'Antarctica/Vostok', - 'America/Argentina/Buenos_Aires', - 'America/Argentina/Cordoba', - 'America/Argentina/Salta', - 'America/Argentina/Jujuy', - 'America/Argentina/Tucuman', - 'America/Argentina/Catamarca', - 'America/Argentina/La_Rioja', - 'America/Argentina/San_Juan', - 'America/Argentina/Mendoza', - 'America/Argentina/San_Luis', - 'America/Argentina/Rio_Gallegos', - 'America/Argentina/Ushuaia', - 'Pacific/Pago_Pago', - 'Europe/Vienna', - 'Australia/Lord_Howe', - 'Antarctica/Macquarie', - 'Australia/Hobart', - 'Australia/Currie', - 'Australia/Melbourne', - 'Australia/Sydney', - 'Australia/Broken_Hill', - 'Australia/Brisbane', - 'Australia/Lindeman', - 'Australia/Adelaide', - 'Australia/Darwin', - 'Australia/Perth', - 'Australia/Eucla', - 'Asia/Baku', - 'America/Barbados', - 'Asia/Dhaka', - 'Europe/Brussels', - 'Europe/Sofia', - 'Atlantic/Bermuda', - 'Asia/Brunei', - 'America/La_Paz', - 'America/Noronha', - 'America/Belem', - 'America/Fortaleza', - 'America/Recife', - 'America/Araguaina', - 'America/Maceio', - 'America/Bahia', - 'America/Sao_Paulo', - 'America/Campo_Grande', - 'America/Cuiaba', - 'America/Santarem', - 'America/Porto_Velho', - 'America/Boa_Vista', - 'America/Manaus', - 'America/Eirunepe', - 'America/Rio_Branco', - 'America/Nassau', - 'Asia/Thimphu', - 'Europe/Minsk', - 'America/Belize', - 'America/St_Johns', - 'America/Halifax', - 'America/Glace_Bay', - 'America/Moncton', - 'America/Goose_Bay', - 'America/Blanc-Sablon', - 'America/Toronto', - 'America/Nipigon', - 'America/Thunder_Bay', - 'America/Iqaluit', - 'America/Pangnirtung', - 'America/Atikokan', - 'America/Winnipeg', - 'America/Rainy_River', - 'America/Resolute', - 'America/Rankin_Inlet', - 'America/Regina', - 'America/Swift_Current', - 'America/Edmonton', - 'America/Cambridge_Bay', - 'America/Yellowknife', - 'America/Inuvik', - 'America/Creston', - 'America/Dawson_Creek', - 'America/Fort_Nelson', - 'America/Vancouver', - 'America/Whitehorse', - 'America/Dawson', - 'Indian/Cocos', - 'Europe/Zurich', - 'Africa/Abidjan', - 'Pacific/Rarotonga', - 'America/Santiago', - 'America/Punta_Arenas', - 'Pacific/Easter', - 'Asia/Shanghai', - 'Asia/Urumqi', - 'America/Bogota', - 'America/Costa_Rica', - 'America/Havana', - 'Atlantic/Cape_Verde', - 'America/Curacao', - 'Indian/Christmas', - 'Asia/Nicosia', - 'Asia/Famagusta', - 'Europe/Prague', - 'Europe/Berlin', - 'Europe/Copenhagen', - 'America/Santo_Domingo', - 'Africa/Algiers', - 'America/Guayaquil', - 'Pacific/Galapagos', - 'Europe/Tallinn', - 'Africa/Cairo', - 'Africa/El_Aaiun', - 'Europe/Madrid', - 'Africa/Ceuta', - 'Atlantic/Canary', - 'Europe/Helsinki', - 'Atlantic/Stanley', - 'Pacific/Chuuk', - 'Pacific/Pohnpei', - 'Atlantic/Faroe', - 'Europe/Paris', - 'Europe/London', - 'Asia/Tbilisi', - 'America/Cayenne', - 'Africa/Accra', - 'Europe/Gibraltar', - 'America/Godthab', - 'America/Danmarkshavn', - 'America/Scoresbysund', - 'America/Thule', - 'Europe/Athens', - 'Atlantic/South_Georgia', - 'America/Guatemala', - 'Pacific/Guam', - 'Africa/Bissau', - 'America/Guyana', - 'Asia/Hong_Kong', - 'America/Tegucigalpa', - 'America/Port-au-Prince', - 'Europe/Budapest', - 'Asia/Jakarta', - 'Asia/Pontianak', - 'Asia/Makassar', - 'Asia/Jayapura', - 'Europe/Dublin', - 'Asia/Jerusalem', - 'Asia/Kolkata', - 'Indian/Chagos', - 'Asia/Baghdad', - 'Asia/Tehran', - 'Atlantic/Reykjavik', - 'Europe/Rome', - 'America/Jamaica', - 'Asia/Amman', - 'Asia/Tokyo', - 'Africa/Nairobi', - 'Asia/Bishkek', - 'Asia/Pyongyang', - 'Asia/Seoul', - 'Asia/Almaty', - 'Asia/Qyzylorda', - 'Asia/Qostanay', - 'Asia/Aqtobe', - 'Asia/Aqtau', - 'Asia/Atyrau', - 'Asia/Oral', - 'Asia/Beirut', - 'Asia/Colombo', - 'Africa/Monrovia', - 'Europe/Vilnius', - 'Europe/Luxembourg', - 'Europe/Riga', - 'Africa/Tripoli', - 'Africa/Casablanca', - 'Europe/Monaco', - 'Europe/Chisinau', - 'Asia/Yangon', - 'Asia/Ulaanbaatar', - 'Asia/Hovd', - 'Asia/Choibalsan', - 'Asia/Macau', - 'America/Martinique', - 'Europe/Malta', - 'Indian/Mauritius', - 'Indian/Maldives', - 'America/Mexico_City', - 'America/Cancun', - 'America/Merida', - 'America/Monterrey', - 'America/Matamoros', - 'America/Mazatlan', - 'America/Chihuahua', - 'America/Ojinaga', - 'America/Hermosillo', - 'America/Tijuana', - 'America/Bahia_Banderas', - 'Asia/Kuala_Lumpur', - 'Asia/Kuching', - 'Africa/Maputo', - 'Africa/Windhoek', - 'Pacific/Noumea', - 'Africa/Lagos', - 'America/Managua', - 'Europe/Amsterdam', - 'Europe/Oslo', - 'Asia/Kathmandu', - 'Pacific/Niue', - 'America/Panama', - 'America/Lima', - 'Pacific/Tahiti', - 'Pacific/Marquesas', - 'Pacific/Gambier', - 'Pacific/Port_Moresby', - 'Pacific/Bougainville', - 'Asia/Manila', - 'Asia/Karachi', - 'Europe/Warsaw', - 'America/Miquelon', - 'Pacific/Pitcairn', - 'America/Puerto_Rico', - 'Asia/Gaza', - 'Asia/Hebron', - 'Europe/Lisbon', - 'Atlantic/Madeira', - 'Atlantic/Azores', - 'Pacific/Palau', - 'America/Asuncion', - 'Asia/Qatar', - 'Indian/Reunion', - 'Europe/Bucharest', - 'Europe/Belgrade', - 'Europe/Kaliningrad', - 'Europe/Moscow', - 'Europe/Simferopol', - 'Europe/Kirov', - 'Europe/Astrakhan', - 'Europe/Volgograd', - 'Europe/Saratov', - 'Europe/Ulyanovsk', - 'Europe/Samara', - 'Asia/Yekaterinburg', - 'Asia/Omsk', - 'Asia/Novosibirsk', - 'Asia/Barnaul', - 'Asia/Tomsk', - 'Asia/Novokuznetsk', - 'Asia/Krasnoyarsk', - 'Asia/Irkutsk', - 'Asia/Chita', - 'Asia/Yakutsk', - 'Asia/Khandyga', - 'Asia/Vladivostok', - 'Asia/Ust-Nera', - 'Asia/Magadan', - 'Asia/Sakhalin', - 'Asia/Srednekolymsk', - 'Asia/Riyadh', - 'Pacific/Guadalcanal', - 'Indian/Mahe', - 'Africa/Khartoum', - 'Europe/Stockholm', - 'Asia/Singapore', - 'America/Paramaribo', - 'Africa/Juba', - 'Africa/Sao_Tome', - 'America/El_Salvador', - 'Asia/Damascus', - 'America/Grand_Turk', - 'Africa/Ndjamena', - 'Indian/Kerguelen', - 'Asia/Bangkok', - 'Asia/Dushanbe', - 'Asia/Dili', - 'Asia/Ashgabat', - 'Africa/Tunis', - 'Europe/Istanbul', - 'America/Port_of_Spain', - 'Asia/Taipei', - 'Europe/Kiev', - 'Europe/Uzhgorod', - 'Europe/Zaporozhye', - 'America/New_York', - 'America/Detroit', - 'America/Kentucky/Louisville', - 'America/Kentucky/Monticello', - 'America/Indiana/Indianapolis', - 'America/Indiana/Vincennes', - 'America/Indiana/Winamac', - 'America/Indiana/Marengo', - 'America/Indiana/Petersburg', - 'America/Indiana/Vevay', - 'America/Chicago', - 'America/Indiana/Tell_City', - 'America/Indiana/Knox', - 'America/Menominee', - 'America/North_Dakota/Center', - 'America/North_Dakota/New_Salem', - 'America/North_Dakota/Beulah', - 'America/Denver', - 'America/Boise', - 'America/Phoenix', - 'America/Los_Angeles', - 'America/Anchorage', - 'America/Juneau', - 'America/Sitka', - 'America/Metlakatla', - 'America/Yakutat', - 'America/Nome', - 'America/Adak', - 'Pacific/Honolulu', - 'America/Montevideo', - 'Asia/Samarkand', - 'Asia/Tashkent', - 'America/Caracas', - 'Asia/Ho_Chi_Minh', - 'Pacific/Efate', - 'Africa/Johannesburg' -] - -function testGetDay (date: Date, timezone: string): void { - const timestamp: Timestamp = getDay(date.getTime()) - const convertedDate: Date = new Date(timestamp) - const originalLocaleDate: string = date.toLocaleDateString('en-US', { timeZone: 'Europe/London' }) - const convertedLocaleDate: string = convertedDate.toLocaleDateString('en-US', { timeZone: timezone }) - expect(convertedLocaleDate).toEqual(originalLocaleDate) -} - -function testConvertToDay (date: Date, timezone: string): void { - const convertedDate: Date = convertToDay(date) - const originalLocaleDate: string = date.toLocaleDateString('en-US', { timeZone: 'Europe/London' }) - const convertedLocaleDate: string = convertedDate.toLocaleDateString('en-US', { timeZone: timezone }) - expect(convertedLocaleDate).toEqual(originalLocaleDate) -} - -describe('time', () => { - it.each(supportedTimezones)('dates are matched in `getDay` for locale %p', (timezone: string) => { - testGetDay(new Date(Date.UTC(1995, 11, 3, 12, 0)), timezone) - testGetDay(new Date(Date.UTC(2025, 0, 23, 55, 0)), timezone) - testGetDay(new Date(Date.UTC(2020, 4, 12, 30, 50)), timezone) - testGetDay(new Date(Date.UTC(2024, 5, 12, 30, 50)), timezone) - testGetDay(new Date(Date.UTC(2024, 7, 0, 0, 0)), timezone) - }) - - it.each(supportedTimezones)('dates are matched in `convertToDay` for locale %p', (timezone: string) => { - testConvertToDay(new Date(Date.UTC(1995, 11, 3, 12, 0)), timezone) - testConvertToDay(new Date(Date.UTC(2025, 0, 23, 55, 0)), timezone) - testConvertToDay(new Date(Date.UTC(2020, 4, 12, 30, 50)), timezone) - testConvertToDay(new Date(Date.UTC(2024, 5, 12, 30, 50)), timezone) - testConvertToDay(new Date(Date.UTC(2024, 7, 0, 0, 0)), timezone) - }) - - it.each([ - [0, 0, 0, 0], - [7, 59, 59, 999], - [12, 0, 0, 0], - [23, 59, 59, 999] - ])( - 'dates are matched for time [h: %p, m: %p, s: %p, ms: %p]', - (hours: number, minutes: number, seconds: number, milliSeconds: number) => { - const date: Date = new Date() - const expectedDay: number = date.getDate() - date.setHours(hours, minutes, seconds, milliSeconds) - const convertedDate: Date = convertToDay(date) - expect(convertedDate.getDate()).toEqual(expectedDay) - } - ) -}) diff --git a/foundations/core/packages/core/src/__tests__/utils.test.ts b/foundations/core/packages/core/src/__tests__/utils.test.ts deleted file mode 100644 index 0171bba093..0000000000 --- a/foundations/core/packages/core/src/__tests__/utils.test.ts +++ /dev/null @@ -1,185 +0,0 @@ -// -// Copyright © 2023 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { mergeQueries } from '..' - -describe('mergeQueries', () => { - it('merges query with empty query', () => { - const q1 = { name: 'john', age: { $gt: 42 } } - const q2 = {} - const res = { name: 'john', age: { $gt: 42 } } - - expect(mergeQueries(q1, q2)).toEqual(res) - expect(mergeQueries(q2, q1)).toEqual(res) - }) - - it('merges query with different fields', () => { - const q1 = { name: 'john' } - const q2 = { age: { $gt: 42 } } - const res = { name: 'john', age: { $gt: 42 } } - - expect(mergeQueries(q1, q2)).toEqual(res) - expect(mergeQueries(q2, q1)).toEqual(res) - }) - - it('merges equal field values', () => { - expect(mergeQueries({ value: 42 }, { value: 42 })).toEqual({ value: 42 }) - }) - - it('does not merge different field values', () => { - const q1 = { value: 42 } - const q2 = { value: 'true' } - const res = { value: { $in: [] } } - expect(mergeQueries(q1, q2)).toEqual(res) - expect(mergeQueries(q2, q1)).toEqual(res) - }) - - it('merges predicate with predicate', () => { - expect(mergeQueries({ age: { $in: [41, 42] } }, { age: { $in: [42, 43] } })).toEqual({ age: { $in: [42] } }) - expect(mergeQueries({ age: { $in: [42, 43] } }, { age: { $in: [41, 42] } })).toEqual({ age: { $in: [42] } }) - - expect(mergeQueries({ age: { $nin: [42] } }, { age: { $nin: [42] } })).toEqual({ age: { $nin: [42] } }) - - expect(mergeQueries({ age: { $lt: 45 } }, { age: { $lt: 42 } })).toEqual({ age: { $lt: 42 } }) - expect(mergeQueries({ age: { $lt: 42 } }, { age: { $lt: 45 } })).toEqual({ age: { $lt: 42 } }) - - expect(mergeQueries({ age: { $gt: 40 } }, { age: { $gt: 42 } })).toEqual({ age: { $gt: 42 } }) - expect(mergeQueries({ age: { $gt: 42 } }, { age: { $gt: 40 } })).toEqual({ age: { $gt: 42 } }) - - expect(mergeQueries({ age: { $lte: 45 } }, { age: { $lte: 42 } })).toEqual({ age: { $lte: 42 } }) - expect(mergeQueries({ age: { $lte: 42 } }, { age: { $lte: 45 } })).toEqual({ age: { $lte: 42 } }) - - expect(mergeQueries({ age: { $gte: 40 } }, { age: { $gte: 42 } })).toEqual({ age: { $gte: 42 } }) - expect(mergeQueries({ age: { $gte: 42 } }, { age: { $gte: 40 } })).toEqual({ age: { $gte: 42 } }) - - expect(mergeQueries({ age: { $ne: 42 } }, { age: { $ne: 42 } })).toEqual({ age: { $ne: 42 } }) - }) - - it('merges predicate with value', () => { - // positive - expect(mergeQueries({ age: { $in: [41, 42, 43] } }, { age: 42 })).toEqual({ age: 42 }) - expect(mergeQueries({ age: 42 }, { age: { $in: [41, 42, 43] } })).toEqual({ age: 42 }) - - expect(mergeQueries({ age: { $nin: [41, 43] } }, { age: 42 })).toEqual({ age: 42 }) - expect(mergeQueries({ age: 42 }, { age: { $nin: [41, 43] } })).toEqual({ age: 42 }) - - expect(mergeQueries({ age: { $lt: 45 } }, { age: 42 })).toEqual({ age: 42 }) - expect(mergeQueries({ age: 42 }, { age: { $lt: 45 } })).toEqual({ age: 42 }) - - expect(mergeQueries({ age: { $gt: 40 } }, { age: 42 })).toEqual({ age: 42 }) - expect(mergeQueries({ age: 42 }, { age: { $gt: 40 } })).toEqual({ age: 42 }) - - expect(mergeQueries({ age: { $lte: 42 } }, { age: 42 })).toEqual({ age: 42 }) - expect(mergeQueries({ age: 42 }, { age: { $lte: 42 } })).toEqual({ age: 42 }) - - expect(mergeQueries({ age: { $gte: 42 } }, { age: 42 })).toEqual({ age: 42 }) - expect(mergeQueries({ age: 42 }, { age: { $gte: 42 } })).toEqual({ age: 42 }) - - expect(mergeQueries({ age: { $ne: 40 } }, { age: 42 })).toEqual({ age: 42 }) - expect(mergeQueries({ age: 42 }, { age: { $ne: 40 } })).toEqual({ age: 42 }) - - // negative - expect(mergeQueries({ age: { $in: [31, 32, 33] } }, { age: 42 })).toEqual({ age: { $in: [] } }) - expect(mergeQueries({ age: 42 }, { age: { $in: [31, 32, 33] } })).toEqual({ age: { $in: [] } }) - - expect(mergeQueries({ age: { $nin: [41, 42, 43] } }, { age: 42 })).toEqual({ age: { $in: [] } }) - expect(mergeQueries({ age: 42 }, { age: { $nin: [41, 42, 43] } })).toEqual({ age: { $in: [] } }) - - expect(mergeQueries({ age: { $lt: 42 } }, { age: 42 })).toEqual({ age: { $in: [] } }) - expect(mergeQueries({ age: 42 }, { age: { $lt: 42 } })).toEqual({ age: { $in: [] } }) - - expect(mergeQueries({ age: { $gt: 42 } }, { age: 42 })).toEqual({ age: { $in: [] } }) - expect(mergeQueries({ age: 42 }, { age: { $gt: 42 } })).toEqual({ age: { $in: [] } }) - - expect(mergeQueries({ age: { $lte: 40 } }, { age: 42 })).toEqual({ age: { $in: [] } }) - expect(mergeQueries({ age: 42 }, { age: { $lte: 40 } })).toEqual({ age: { $in: [] } }) - - expect(mergeQueries({ age: { $gte: 43 } }, { age: 42 })).toEqual({ age: { $in: [] } }) - expect(mergeQueries({ age: 42 }, { age: { $gte: 43 } })).toEqual({ age: { $in: [] } }) - - expect(mergeQueries({ age: { $ne: 42 } }, { age: 42 })).toEqual({ age: { $in: [] } }) - expect(mergeQueries({ age: 42 }, { age: { $ne: 42 } })).toEqual({ age: { $in: [] } }) - }) - - it('$in merge', () => { - expect(mergeQueries({ value: { $in: [1, 2] } }, { value: { $in: [2, 3] } })).toEqual({ value: { $in: [2] } }) - expect(mergeQueries({ value: { $in: [2, 3] } }, { value: { $in: [1, 2] } })).toEqual({ value: { $in: [2] } }) - - expect(mergeQueries({ value: { $in: [1, 2] } }, { value: { $in: [3, 4] } })).toEqual({ value: { $in: [] } }) - expect(mergeQueries({ value: { $in: [3, 4] } }, { value: { $in: [1, 2] } })).toEqual({ value: { $in: [] } }) - - expect(mergeQueries({ value: { $in: [] } }, { value: { $in: [] } })).toEqual({ value: { $in: [] } }) - expect(mergeQueries({ value: { $in: [42] } }, { value: { $in: [42] } })).toEqual({ value: { $in: [42] } }) - }) - - it('$nin merge', () => { - expect(mergeQueries({ value: { $nin: [] } }, { value: { $nin: [] } })).toEqual({}) - expect(mergeQueries({ value: { $nin: [42] } }, { value: { $nin: [42] } })).toEqual({ value: { $nin: [42] } }) - }) - - it('$in $nin $ne merge', () => { - // $in and $nin - expect(mergeQueries({ value: { $in: [1, 2] } }, { value: { $nin: [1] } })).toEqual({ value: { $in: [2] } }) - expect(mergeQueries({ value: { $nin: [1] } }, { value: { $in: [1, 2] } })).toEqual({ value: { $in: [2] } }) - - expect(mergeQueries({ value: { $in: ['a', 'b'] } }, { value: { $nin: ['a'] } })).toEqual({ value: { $in: ['b'] } }) - expect(mergeQueries({ value: { $nin: ['a'] } }, { value: { $in: ['a', 'b'] } })).toEqual({ value: { $in: ['b'] } }) - - expect(mergeQueries({ value: { $in: [1, 2] } }, { value: { $nin: [1, 2, 3] } })).toEqual({ value: { $in: [] } }) - expect(mergeQueries({ value: { $nin: [1, 2, 3] } }, { value: { $in: [1, 2] } })).toEqual({ value: { $in: [] } }) - - expect(mergeQueries({ value: { $in: [1, 2] } }, { value: { $nin: [1, 2] } })).toEqual({ value: { $in: [] } }) - - expect(mergeQueries({ value: { $in: [] } }, { value: { $nin: [42] } })).toEqual({ value: { $in: [] } }) - expect(mergeQueries({ value: { $nin: [42] } }, { value: { $in: [] } })).toEqual({ value: { $in: [] } }) - - // $in and $ne - expect(mergeQueries({ value: { $in: [1, 2] } }, { value: { $ne: 1 } })).toEqual({ value: { $in: [2] } }) - expect(mergeQueries({ value: { $ne: 1 } }, { value: { $in: [1, 2] } })).toEqual({ value: { $in: [2] } }) - - expect(mergeQueries({ value: { $in: [1] } }, { value: { $ne: 1 } })).toEqual({ value: { $in: [] } }) - expect(mergeQueries({ value: { $ne: 1 } }, { value: { $in: [1] } })).toEqual({ value: { $in: [] } }) - - expect(mergeQueries({ value: { $in: [] } }, { value: { $ne: 42 } })).toEqual({ value: { $in: [] } }) - expect(mergeQueries({ value: { $ne: 42 } }, { value: { $in: [] } })).toEqual({ value: { $in: [] } }) - }) - - it('$lt and $gt', () => { - expect(mergeQueries({ age: { $lt: 25 } }, { age: { $gt: 20 } })).toEqual({ age: { $lt: 25, $gt: 20 } }) - expect(mergeQueries({ age: { $gt: 20 } }, { age: { $lt: 25 } })).toEqual({ age: { $lt: 25, $gt: 20 } }) - - expect(mergeQueries({ age: { $lt: 20 } }, { age: { $gt: 25 } })).toEqual({ age: { $lt: 20, $gt: 25 } }) - expect(mergeQueries({ age: { $gt: 25 } }, { age: { $lt: 20 } })).toEqual({ age: { $lt: 20, $gt: 25 } }) - }) - - it('complex', () => { - const q1 = { - space: 1, - unique: 'item', - age: { $gt: 10 } - } as any - const q2 = { - space: { $in: [1, 2] }, - age: 30 - } as any - const res = { - space: 1, - unique: 'item', - age: 30 - } as any - expect(mergeQueries(q1, q2)).toEqual(res) - expect(mergeQueries(q2, q1)).toEqual(res) - }) -}) diff --git a/foundations/core/packages/core/src/backup.ts b/foundations/core/packages/core/src/backup.ts deleted file mode 100644 index 348a6606d3..0000000000 --- a/foundations/core/packages/core/src/backup.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { type Doc, type Domain, type Ref } from './classes' -import { type DocInfo } from './server' - -/** - * @public - * Define a st of document + hashcode for chunk - * So backup client could decide to download or not any of documents. - */ -export interface DocChunk { - idx: number - // _id => hash mapping - docs: DocInfo[] - - size?: number // Estimated size of the chunk data - finished: boolean -} - -/** - * @public - */ -export interface BackupClient { - loadChunk: (domain: Domain, idx?: number) => Promise - closeChunk: (idx: number) => Promise - - loadDocs: (domain: Domain, docs: Ref[]) => Promise - upload: (domain: Domain, docs: Doc[]) => Promise - clean: (domain: Domain, docs: Ref[]) => Promise - - getDomainHash: (domain: Domain) => Promise - - sendForceClose: () => Promise -} diff --git a/foundations/core/packages/core/src/benchmark.ts b/foundations/core/packages/core/src/benchmark.ts deleted file mode 100644 index 7ddd61f0d9..0000000000 --- a/foundations/core/packages/core/src/benchmark.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { type Doc, type Domain } from './classes' - -/** - * @public - */ -export const DOMAIN_BENCHMARK = 'benchmark' as Domain - -export type BenchmarkDocRange = - | number - | { - // Or random in range - from: number - to: number - } -export interface BenchmarkDoc extends Doc { - source?: string - // Query fields to perform different set of workload - request?: { - // On response will return a set of BenchmarkDoc with requested fields. - documents: BenchmarkDocRange - - // A random sized document with size from to sizeTo - size: BenchmarkDocRange - - // Produce a set of derived documents payload - derived?: BenchmarkDocRange - } - response?: string // A dummy random data to match document's size -} diff --git a/foundations/core/packages/core/src/classes.ts b/foundations/core/packages/core/src/classes.ts deleted file mode 100644 index 85c351eda4..0000000000 --- a/foundations/core/packages/core/src/classes.ts +++ /dev/null @@ -1,948 +0,0 @@ -// -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// Copyright © 2021, 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { Asset, IntlString, Plugin } from '@hcengineering/platform' -import type { DocumentQuery } from './storage' -import { type WorkspaceDataId, type WorkspaceUuid } from './utils' -import { Tx } from '.' - -/** - * @public - */ -export type Ref = string & { __ref: T } - -/** - * @public - */ -export type PrimitiveType = number | string | boolean | undefined | Ref - -/** - * @public - */ -export type Timestamp = number - -/** - * @public - */ -export type Markup = string - -/** - * @public - */ -export type Hyperlink = string - -/** - * @public - */ -export type CollectionSize = T[]['length'] - -/** - * @public - * - * String representation of {@link https://www.npmjs.com/package/lexorank LexoRank} type - */ -export type Rank = string - -/** - * @public - * - * Reference to blob containing snapshot of collaborative doc. - */ -export type MarkupBlobRef = Ref - -/** - * @public - */ -export interface Obj { - _class: Ref> -} - -export interface Account { - uuid: AccountUuid - role: AccountRole - primarySocialId: PersonId - socialIds: PersonId[] - fullSocialIds: SocialId[] -} - -/** - * @public - * Global person UUID. - */ -export type PersonUuid = string & { __personUuid: true } - -/** - * @public - * Global person account UUID. - * The same UUID as PersonUuid but for when account exists. - */ -export type AccountUuid = PersonUuid & { __accountUuid: true } - -/** - * @public - * Generated identifier of a social id linked to a global person. - */ -export type PersonId = string & { __personId: true } - -export interface BasePerson { - name: string - personUuid?: PersonUuid -} - -/** - * @public - */ -export interface Doc extends Obj { - _id: Ref - space: Ref - modifiedOn: Timestamp - modifiedBy: PersonId - createdBy?: PersonId // Marked as optional since it will be filled by platform. - createdOn?: Timestamp // Marked as optional since it will be filled by platform. -} - -/** - * @public - */ -export type PropertyType = any - -/** - * @public - */ -export interface UXObject extends Obj { - label: IntlString - icon?: Asset - color?: number - hidden?: boolean - readonly?: boolean -} - -/** - * @public - */ -export interface Association extends Doc { - classA: Ref> - classB: Ref> - nameA: string - nameB: string - type: '1:1' | '1:N' | 'N:N' -} - -/** - * @public - */ -export interface Relation extends Doc { - docA: Ref - docB: Ref - association: Ref -} - -/** - * @public - */ -export interface AttachedDoc< - Parent extends Doc = Doc, - Collection extends Extract | string = Extract | string, - S extends Space = Space -> extends Doc { - attachedTo: Ref - attachedToClass: Ref> - collection: Collection -} - -/** - * @public - */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export interface Type extends UXObject {} - -/** - * @public - */ -export enum IndexKind { - /** - * Attribute with this index annotation should be added to elastic for search - * Could be added to string or Ref attribute - * TODO: rename properly for better code readability - */ - FullText, - /** - * For attribute with this annotation should be created an index in mongo database - * - * Also mean to include into Elastic search. - */ - Indexed, - - // Same as indexed but for descending - IndexedDsc -} - -/** - * @public - */ -export interface Enum extends Doc { - name: string - enumValues: string[] -} - -/** - * @public - */ -export interface Attribute extends Doc, UXObject { - attributeOf: Ref> - name: string - type: Type - index?: IndexKind - shortLabel?: IntlString - isCustom?: boolean - defaultValue?: any - automationOnly?: boolean - rank?: Rank - - // Extra customization properties - [key: string]: any -} - -/** - * @public - */ -export type AnyAttribute = Attribute> - -/** - * @public - */ -export enum ClassifierKind { - CLASS, - INTERFACE, - MIXIN -} - -/** - * @public - */ -export interface Classifier extends Doc, UXObject { - kind: ClassifierKind -} - -/** - * @public - */ -export type Domain = string & { __domain: true } - -/** - * @public - */ -export type OperationDomain = string & { __domain: true } - -/** - * @public - */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export interface Interface extends Classifier { - extends?: Ref>[] -} - -/** - * @public - */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export interface Class extends Classifier { - extends?: Ref> - implements?: Ref>[] - domain?: Domain - shortLabel?: string - sortingKey?: string - filteringKey?: string - pluralLabel?: IntlString -} - -/** - * @public - * Define a set of plugin to model document bindings. - */ -export interface PluginConfiguration extends Doc { - pluginId: Plugin - transactions: Ref[] - - label: IntlString - icon?: Asset - description?: IntlString - enabled: boolean - - // If set will not allow to disable this configuration - system?: true - - // If set will not be shown in configuration UI or enabled - hidden?: boolean - - // If specified, will show beta/testing label in UI. - beta: boolean - - // If defined, will only remove classes in list. - classFilter?: Ref>[] -} - -/** - * @public - */ -export type Mixin = Class - -// D A T A - -/** - * @public - */ -export type Data = Omit - -/** - * @public - */ -export type AttachedData = Omit - -/** - * @public - */ -export type DocData = T extends AttachedDoc ? AttachedData : Data - -// T Y P E S - -/** - * @public - */ -export enum DateRangeMode { - DATE = 'date', - TIME = 'time', - DATETIME = 'datetime', - TIMEONLY = 'timeonly' -} - -/** - * @public - */ -export interface TypeDate extends Type { - // If not set date mode default - mode: DateRangeMode - // If not set to true, will be false - withShift: boolean -} - -/** - * @public - */ -export interface TypeIdentifier extends Type { - of: Ref -} - -/** - * @public - */ -export interface TypeNumber extends Type { - min?: number - max?: number - digits?: number // Number of digits after comma -} - -/** - * @public - */ -export interface RefTo extends Type>> { - to: Ref> -} - -/** - * @public - */ -export interface Collection extends Type> { - of: Ref> - itemLabel?: IntlString -} - -/** - * @public - */ -export type Arr = T[] - -/** - * @public - */ -export interface ArrOf extends Type { - of: Type -} - -/** - * @public - */ -export interface EnumOf extends Type { - of: Ref -} - -/** - * @public - */ -export interface TypeHyperlink extends Type {} - -/** - * @public - * - * A type for some custom serialized field with a set of editors - */ -export interface TypeAny extends Type { - presenter: AnyComponent - editor?: AnyComponent -} - -/** - * @public - */ -export const DOMAIN_MODEL = 'model' as Domain - -/** - * @public - */ -export const DOMAIN_MODEL_TX = 'model_tx' as Domain - -/** - * @public - */ -export const DOMAIN_SPACE = 'space' as Domain - -/** - * @public - */ -export const DOMAIN_CONFIGURATION = '_configuration' as Domain - -/** - * @public - */ -export const DOMAIN_MIGRATION = '_migrations' as Domain - -/** - * @public - */ -export const DOMAIN_TRANSIENT = 'transient' as Domain - -/** - * @public - */ -export const DOMAIN_RELATION = 'relation' as Domain - -/** - * @public - */ -export const DOMAIN_COLLABORATOR = 'collaborator' as Domain - -/** - * @public - */ -export interface TransientConfiguration extends Class { - // If set will not store transient objects into memdb - broadcastOnly: boolean -} - -/** - * Special domain to access s3 blob data. - * @public - */ -export const DOMAIN_BLOB = 'blob' as Domain - -/** - * @public - */ -export const DOMAIN_SEQUENCE = 'sequence' as Domain - -// S P A C E - -/** - * @public - */ -export interface Space extends Doc { - name: string - description: string - private: boolean - members: AccountUuid[] - archived: boolean - owners?: AccountUuid[] - autoJoin?: boolean -} - -/** - * @public - */ -export interface SystemSpace extends Space {} - -/** - * @public - * - * Space with custom configured type - */ -export interface TypedSpace extends Space { - type: Ref -} - -/** - * @public - * - * Is used to describe "types" for space type - */ -export interface SpaceTypeDescriptor extends Doc { - name: IntlString - description: IntlString - icon: Asset - baseClass: Ref> // Child class of Space for which the space type can be defined - availablePermissions: Ref[] - system?: boolean -} - -/** - * @public - * - * Customisable space type allowing to configure space roles and permissions within them - */ -export interface SpaceType extends Doc { - name: string - shortDescription?: string - descriptor: Ref - members?: AccountUuid[] // this members will be added automatically to new space, also change this fiield will affect existing spaces - autoJoin?: boolean // if true, all new users will be added to space automatically - targetClass: Ref> // A dynamic mixin for Spaces to hold custom attributes and roles assignment of the space type - roles: CollectionSize -} - -/** - * @public - * Role defines permissions for employees assigned to this role within the space - */ -export interface Role extends AttachedDoc { - name: string - permissions: Ref[] -} - -/** - * @public - * Defines assignment of employees to a role within a space - */ -export type RolesAssignment = Record, AccountUuid[] | undefined> - -/** - * @public - * Permission is a basic access control item in the system - */ -export interface Permission extends Doc { - label: IntlString - txClass?: Ref> - forbid?: boolean - objectClass?: Ref> - scope?: 'space' | 'workspace' - txMatch?: DocumentQuery - description?: IntlString - icon?: Asset -} - -/** - * @public - */ -export enum AccountRole { - ReadOnlyGuest = 'READONLYGUEST', - DocGuest = 'DocGuest', - Guest = 'GUEST', - User = 'USER', - Maintainer = 'MAINTAINER', - Owner = 'OWNER', - Admin = 'ADMIN' -} - -/** - * @public - */ -export const roleOrder: Record = { - [AccountRole.ReadOnlyGuest]: 5, - [AccountRole.DocGuest]: 10, - [AccountRole.Guest]: 20, - [AccountRole.User]: 30, - [AccountRole.Maintainer]: 40, - [AccountRole.Owner]: 50, - [AccountRole.Admin]: 100 -} - -export interface TxAccessLevel extends Class { - createAccessLevel?: AccountRole - removeAccessLevel?: AccountRole - updateAccessLevel?: AccountRole - isIdentity?: boolean -} - -/** - * @public - */ -export interface Person { - uuid: PersonUuid - firstName: string - lastName: string -} - -export interface PersonInfo extends BasePerson { - socialIds: SocialId[] -} - -/** - * @public - */ -// TODO: move to contact -export interface UserStatus extends Doc { - online: boolean - user: AccountUuid -} - -/** - * @public - */ -export interface Version extends Doc { - major: number - minor: number - patch: number -} - -/** - * @public - */ -export interface MigrationState extends Doc { - plugin: string - state: string -} - -/** - * @public - */ -export function versionToString (version: Version | Data): string { - return `${version?.major}.${version?.minor}.${version?.patch}` -} - -/** - * @public - */ -export interface Sequence extends Doc { - attachedTo: Ref> - sequence: number -} - -export interface CustomSequence extends Sequence { - prefix: string -} - -/** - * @public - */ -export type BlobMetadata = Record - -/** - * @public - * - * A blob document to manage blob attached documents. - * - * _id: is a platform ID and it created using our regular generateId(), - * and storageId is a provider specified storage id. - */ -export interface Blob extends Doc { - // Provider - provider: string - // A provider specific id - contentType: string - // A etag for blob - etag: string - // Document version if supported by provider - version: string | null - // A document size - size: number -} - -export interface BlobType { - file: Ref - - type: string - - name: string - size: number - - metadata?: BlobMetadata -} - -export type Blobs = Record - -/** - * For every blob will automatically add a lookup. - * - * It extends Blob to allow for $lookup operations work as expected. - */ -export interface BlobLookup extends Blob { - // An URL document could be downloaded from, with ${id} to put blobId into - downloadUrl: string - downloadUrlExpire?: number -} - -/** - * @public - * - * If defined for class, this class will be enabled for embedding search like openai. - */ -export interface FullTextSearchContext extends Doc { - toClass: Ref> - fullTextSummary?: boolean - forceIndex?: boolean -} - -/** - * @public - */ -export interface ConfigurationElement extends Class { - // Title will be presented to owner. - title: IntlString - // Group for grouping. - group: IntlString -} - -/** - * @public - * - * Define configuration value configuration for workspace. - * - * Configuration is accessible only for owners of workspace and under hood services. - */ -export interface Configuration extends Doc { - enabled: boolean -} - -/** - * @public - */ -export type RelatedDocument = Pick - -/** - * @public - */ -export enum IndexOrder { - Ascending = 1, - Descending = -1 -} - -/** - * @public - */ -export type FieldIndex = { - [P in keyof T]?: IndexOrder -} & Record - -export interface FieldIndexConfig { - sparse?: boolean - filter?: Omit, '$search'> - keys: FieldIndex | string -} - -/** - * @public - * - * Mixin for extra indexing fields. - */ -export interface IndexingConfiguration extends Class { - // Define a list of extra index definitions. - indexes: (string | FieldIndexConfig)[] - searchDisabled?: boolean -} - -export interface DomainIndexConfiguration extends Doc { - domain: Domain - disableCollection?: boolean // For some special cases we could decide to disable collection and index creations at all. - - // A set of indexes we need to disable for domain - // Disabled indexes will be removed - disabled?: (FieldIndex | string)[] - - // Additional indexes we could like to enabled - indexes?: (FieldIndexConfig | string)[] - - skip?: string[] -} - -export type WorkspaceMode = - | 'manual-creation' - | 'pending-creation' // -> 'creating' - | 'creating' // -> 'active - | 'upgrading' // -> 'active' - | 'pending-deletion' // -> 'deleting' - | 'deleting' // -> "deleted" - | 'active' - | 'deleted' - | 'archiving-pending-backup' // -> 'cleaning' - | 'archiving-backup' // -> 'archiving-pending-clean' - | 'archiving-pending-clean' // -> 'archiving-clean' - | 'archiving-clean' // -> 'archived' - | 'archived' - | 'migration-pending-backup' // -> 'migration-backup' - | 'migration-backup' // -> 'migration-pending-cleanup' - | 'migration-pending-clean' // -> 'migration-pending-cleaning' - | 'migration-clean' // -> 'pending-restoring' - | 'pending-restore' // -> 'restoring' - | 'restoring' // -> 'active' - -export type WorkspaceUserOperation = 'archive' | 'migrate-to' | 'unarchive' | 'delete' | 'reset-attempts' - -export function isActiveMode (mode?: WorkspaceMode): boolean { - return mode === 'active' -} -export function isDeletingMode (mode: WorkspaceMode): boolean { - return mode === 'pending-deletion' || mode === 'deleting' || mode === 'deleted' -} -export function isArchivingMode (mode?: WorkspaceMode): boolean { - return ( - mode === 'archiving-pending-backup' || - mode === 'archiving-backup' || - mode === 'archiving-pending-clean' || - mode === 'archiving-clean' || - mode === 'archived' - ) -} - -export function isMigrationMode (mode?: WorkspaceMode): boolean { - return ( - mode === 'migration-pending-backup' || - mode === 'migration-backup' || - mode === 'migration-pending-clean' || - mode === 'migration-clean' - ) -} -export function isRestoringMode (mode?: WorkspaceMode): boolean { - return mode === 'restoring' || mode === 'pending-restore' -} - -export function isUpgradingMode (mode?: WorkspaceMode): boolean { - return mode === 'upgrading' -} - -export type WorkspaceUpdateEvent = - | 'ping' - | 'create-started' - | 'create-done' - | 'upgrade-started' - | 'upgrade-done' - | 'restore-started' - | 'restore-done' - | 'progress' - | 'migrate-backup-started' // -> state = 'migration-backup' - | 'migrate-backup-done' // -> state = 'migration-pending-cleaning' - | 'migrate-clean-started' // -> state = 'migration-cleaning' - | 'migrate-clean-done' // -> state = 'pending-restoring' - | 'archiving-backup-started' // -> state = 'archiving' - | 'archiving-backup-done' // -> state = 'archiving-pending-cleaning' - | 'archiving-clean-started' - | 'archiving-clean-done' - | 'archiving-done' - | 'delete-started' - | 'delete-done' - -export interface WorkspaceInfo { - uuid: WorkspaceUuid - dataId?: WorkspaceDataId // Old workspace identifier. E.g. Database name in Mongo, bucket in R2, etc. - name: string - url: string - region?: string - branding?: string - createdOn: number - createdBy?: PersonUuid // Should always be set for NEW workspaces - billingAccount?: PersonUuid // Should always be set for NEW workspaces - allowReadOnlyGuest?: boolean // Should always be set for NEW workspaces - allowGuestSignUp?: boolean // Should always be set for NEW workspaces -} - -export interface BackupStatus { - dataSize: number - blobsSize: number - - backupSize: number - - lastBackup: Timestamp - backups: number -} - -export interface UsageStatus { - usage: Record - startTime: Timestamp - updateTime: Timestamp -} - -export interface WorkspaceInfoWithStatus extends WorkspaceInfo { - isDisabled?: boolean - versionMajor: number - versionMinor: number - versionPatch: number - lastVisit?: number - mode: WorkspaceMode - processingProgress?: number - backupInfo?: BackupStatus - usageInfo?: UsageStatus - processingAttemps: number -} - -export interface WorkspaceMemberInfo { - person: AccountUuid - role: AccountRole -} - -export enum SocialIdType { - EMAIL = 'email', - GITHUB = 'github', - GOOGLE = 'google', - PHONE = 'phone', - OIDC = 'oidc', - HULY = 'huly', - TELEGRAM = 'telegram', - HULY_ASSISTANT = 'huly-assistant' -} - -export interface SocialId { - // generated ID so the actual social ID can be detached from a person w/o losing the ID in the linked database records - _id: PersonId - - // Should never be changed after creation - type: SocialIdType - value: string - key: string // Calculated from type and value. Just for convenience. - - displayValue?: string - verifiedOn?: number - - isDeleted?: boolean // Social ids are soft-deleted so all objects created with them can still be properly displayed. -} - -export interface AccountInfo { - timezone?: string - locale?: string -} - -export type SocialKey = Pick - -export interface ClassCollaborators extends Doc { - attachedTo: Ref> - fields: (keyof T)[] // PersonId | Ref | PersonId[] | Ref[] - provideSecurity?: boolean // If true, will provide security for collaborators -} - -export interface Collaborator extends AttachedDoc { - collaborator: AccountUuid -} - -/** - * @public - */ -export type IntegrationKind = string & { __IntegrationKind: true } diff --git a/foundations/core/packages/core/src/client.ts b/foundations/core/packages/core/src/client.ts deleted file mode 100644 index 2226f60d6f..0000000000 --- a/foundations/core/packages/core/src/client.ts +++ /dev/null @@ -1,465 +0,0 @@ -// -// Copyright © 2020 Anticrm Platform Contributors. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { Analytics } from '@hcengineering/analytics' -import { type BackupClient, type DocChunk } from './backup' -import { - type Class, - DOMAIN_MODEL, - type Doc, - type Domain, - type OperationDomain, - type Ref, - type Timestamp -} from './classes' -import core from './component' -import { Hierarchy } from './hierarchy' -import { type MeasureContext, MeasureMetricsContext } from '@hcengineering/measurements' -import { ModelDb } from './memdb' -import type { - DocumentQuery, - DomainParams, - DomainResult, - FindOptions, - FindResult, - FulltextStorage, - SearchOptions, - SearchQuery, - SearchResult, - Storage, - TxResult, - WithLookup -} from './storage' -import { type Tx, type TxWorkspaceEvent, WorkspaceEvent } from './tx' -import { platformNow, platformNowDiff, toFindResult } from './utils' - -/** - * @public - */ -export type TxHandler = (...tx: Tx[]) => void - -export interface DomainRequestOptions { - retry?: boolean -} -/** - * @public - */ -export interface Client extends Storage, FulltextStorage { - notify?: (...tx: Tx[]) => void - getHierarchy: () => Hierarchy - getModel: () => ModelDb - findOne: ( - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ) => Promise | undefined> - close: () => Promise - - domainRequest: ( - domain: OperationDomain, - params: DomainParams, - options?: DomainRequestOptions - ) => Promise> -} - -/** - * @public - */ -export interface LoadModelResponse { - // A diff or a full set of transactions. - transactions: Tx[] - // A current hash chain - hash: string - // If full model is returned, on hash diff for request - full: boolean -} - -/** - * @public - */ -export enum ClientConnectEvent { - Connected, // In case we just connected to server, and receive a full model - Reconnected, // In case we re-connected to server and receive and apply diff. - - // Client could cause back a few more states. - Upgraded, // In case client code receive a full new model and need to be rebuild. - Refresh, // In case we detect query refresh is required - Maintenance // In case workspace are in maintenance mode -} - -/** - * @public - */ -export interface ClientConnection extends Storage, FulltextStorage, BackupClient { - isConnected: () => boolean - - close: () => Promise - onConnect?: (event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise - - // If hash is passed, will return LoadModelResponse - loadModel: (last: Timestamp, hash?: string) => Promise - getLastHash?: (ctx: MeasureContext) => Promise - pushHandler: (handler: TxHandler) => void - domainRequest: (ctx: OperationDomain, params: DomainParams, options?: DomainRequestOptions) => Promise -} - -class ClientImpl implements Client, BackupClient { - notify?: (...tx: Tx[]) => void - hierarchy!: Hierarchy - model!: ModelDb - private readonly appliedModelTransactions = new Set>() - constructor (private readonly conn: ClientConnection) {} - - getConnection (): ClientConnection { - return this.conn - } - - setModel (hierarchy: Hierarchy, model: ModelDb): void { - this.hierarchy = hierarchy - this.model = model - } - - getHierarchy (): Hierarchy { - return this.hierarchy - } - - getModel (): ModelDb { - return this.model - } - - async findAll( - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): Promise> { - const domain = this.hierarchy.getDomain(_class) - const data = - domain === DOMAIN_MODEL - ? await this.model.findAll(_class, query, options) - : await this.conn.findAll(_class, query, options) - - // In case of mixin we need to create mixin proxies. - - // Update mixins & lookups - const result = data.map((v) => { - return this.hierarchy.updateLookupMixin(_class, v, options) - }) - return toFindResult(result, data.total) - } - - async searchFulltext (query: SearchQuery, options: SearchOptions): Promise { - return await this.conn.searchFulltext(query, options) - } - - async domainRequest ( - ctx: OperationDomain, - params: DomainParams, - options?: DomainRequestOptions - ): Promise { - return await this.conn.domainRequest(ctx, params, options) - } - - async findOne( - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): Promise | undefined> { - return (await this.findAll(_class, query, { ...options, limit: 1 }))[0] - } - - async tx (tx: Tx): Promise { - if (tx.objectSpace === core.space.Model) { - this.hierarchy.tx(tx) - await this.model.tx(tx) - this.appliedModelTransactions.add(tx._id) - } - // We need to handle it on server, before performing local live query updates. - return await this.conn.tx(tx) - } - - async updateFromRemote (...tx: Tx[]): Promise { - for (const t of tx) { - try { - if (t.objectSpace === core.space.Model) { - const hasTx = this.appliedModelTransactions.has(t._id) - if (!hasTx) { - this.hierarchy.tx(t) - await this.model.tx(t) - } else { - this.appliedModelTransactions.delete(t._id) - } - } - } catch (err) { - // console.error('failed to apply model transaction, skipping', t) - continue - } - } - this.notify?.(...tx) - } - - async close (): Promise { - await this.conn.close() - } - - async loadChunk (domain: Domain, idx?: number): Promise { - return await this.conn.loadChunk(domain, idx) - } - - async getDomainHash (domain: Domain): Promise { - return await this.conn.getDomainHash(domain) - } - - async closeChunk (idx: number): Promise { - await this.conn.closeChunk(idx) - } - - async loadDocs (domain: Domain, docs: Ref[]): Promise { - return await this.conn.loadDocs(domain, docs) - } - - async upload (domain: Domain, docs: Doc[]): Promise { - await this.conn.upload(domain, docs) - } - - async clean (domain: Domain, docs: Ref[]): Promise { - await this.conn.clean(domain, docs) - } - - async sendForceClose (): Promise { - await this.conn.sendForceClose() - } -} - -/** - * @public - */ -export interface TxPersistenceStore { - load: () => Promise - store: (model: LoadModelResponse) => Promise -} - -export type ModelFilter = (tx: Tx[]) => Tx[] - -/** - * @public - */ -export async function createClient ( - connect: (txHandler: TxHandler) => Promise, - // If set will build model with only allowed plugins. - modelFilter?: ModelFilter, - txPersistence?: TxPersistenceStore, - _ctx?: MeasureContext -): Promise { - const ctx = _ctx ?? new MeasureMetricsContext('createClient', {}) - let client: ClientImpl | null = null - - // Temporal buffer, while we apply model - let txBuffer: Tx[] | undefined = [] - - let hierarchy = new Hierarchy() - let model = new ModelDb(hierarchy) - - let lastTx: string | undefined - - function txHandler (...tx: Tx[]): void { - if (tx == null || tx.length === 0) { - return - } - if (client === null) { - txBuffer?.push(...tx) - } else { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - client.updateFromRemote(...tx) - } - for (const t of tx) { - if (t._class === core.class.TxWorkspaceEvent && (t as TxWorkspaceEvent).event === WorkspaceEvent.LastTx) { - lastTx = (t as TxWorkspaceEvent).params.lastTx - } - } - } - const conn = await ctx.with('connect', {}, () => connect(txHandler), {}, { suspendErrors: true }) - - let { mode, current, addition } = await ctx.with('load-model', {}, (ctx) => loadModel(ctx, conn, txPersistence)) - switch (mode) { - case 'same': - case 'upgrade': - ctx.withSync('build-model', {}, (ctx) => { - buildModel(ctx, current, modelFilter, hierarchy, model) - }) - break - case 'addition': - ctx.withSync('build-model', {}, (ctx) => { - buildModel(ctx, current.concat(addition), modelFilter, hierarchy, model) - }) - } - current = [] - addition = [] - - txBuffer = txBuffer.filter((tx) => tx.space !== core.space.Model) - - client = new ClientImpl(conn) - client.setModel(hierarchy, model) - - txHandler(...txBuffer) - txBuffer = undefined - - const oldOnConnect: - | ((event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise) - | undefined = conn.onConnect - conn.onConnect = async (event, _lastTx, data) => { - console.log('Client: onConnect', event) - if (event === ClientConnectEvent.Maintenance) { - lastTx = _lastTx - await oldOnConnect?.(ClientConnectEvent.Maintenance, _lastTx, data) - return - } - // Find all new transactions and apply - let { mode, current, addition } = await ctx.with('load-model', {}, (ctx) => loadModel(ctx, conn, txPersistence)) - - switch (mode) { - case 'upgrade': - // We have upgrade procedure and need rebuild all stuff. - hierarchy = new Hierarchy() - model = new ModelDb(hierarchy) - client.setModel(hierarchy, model) - - ctx.withSync('build-model', {}, (ctx) => { - buildModel(ctx, current, modelFilter, hierarchy, model) - }) - current = [] - await oldOnConnect?.(ClientConnectEvent.Upgraded, _lastTx, data) - // No need to fetch more stuff since upgrade was happened. - break - case 'addition': - ctx.withSync('build-model', {}, (ctx) => { - buildModel(ctx, current.concat(addition), modelFilter, hierarchy, model) - }) - break - } - current = [] - addition = [] - - if (lastTx === undefined) { - // No need to do anything here since we connected. - await oldOnConnect?.(event, _lastTx, data) - lastTx = _lastTx - return - } - - if (lastTx === _lastTx) { - // Same lastTx, no need to refresh - await oldOnConnect?.(ClientConnectEvent.Reconnected, _lastTx, data) - return - } - lastTx = _lastTx - // We need to trigger full refresh on queries, etc. - await oldOnConnect?.(ClientConnectEvent.Refresh, lastTx, data) - } - - return client -} - -async function loadModel ( - ctx: MeasureContext, - conn: ClientConnection, - persistence?: TxPersistenceStore -): Promise<{ mode: 'same' | 'addition' | 'upgrade', current: Tx[], addition: Tx[] }> { - const t = platformNow() - - const current = (await ctx.with('persistence-load', {}, () => persistence?.load())) ?? { - full: true, - transactions: [], - hash: '' - } - - if (conn.getLastHash !== undefined && (await conn.getLastHash(ctx)) === current.hash) { - // We have same model hash. - return { mode: 'same', current: current.transactions, addition: [] } - } - const lastTxTime = getLastTxTime(current.transactions) - const result = await ctx.with('connection-load-model', { hash: current.hash !== '' }, (ctx) => - conn.loadModel(lastTxTime, current.hash) - ) - - if (Array.isArray(result)) { - // Fallback to old behavior, only for tests - return { - mode: 'same', - current: result, - addition: [] - } - } - - // Save concatenated, if have some more of them. - void ctx - .with('persistence-store', {}, (ctx) => - persistence?.store({ - ...result, - // Store concatinated old + new txes - transactions: result.full ? result.transactions : current.transactions.concat(result.transactions) - }) - ) - .catch((err) => { - Analytics.handleError(err) - }) - - if (typeof window !== 'undefined') { - console.log('find' + (result.full ? 'full model' : 'model diff'), result.transactions.length, platformNowDiff(t)) - } - if (result.full) { - return { mode: 'upgrade', current: result.transactions, addition: [] } - } - return { mode: 'addition', current: current.transactions, addition: result.transactions } -} - -export function buildModel ( - ctx: MeasureContext, - transactions: Tx[], - modelFilter: ModelFilter | undefined, - hierarchy: Hierarchy, - model: ModelDb -): void { - let txes = transactions - if (modelFilter !== undefined) { - txes = modelFilter(txes) - } - - ctx.withSync('build hierarchy', {}, () => { - for (const tx of txes) { - try { - hierarchy.tx(tx) - } catch (err: any) { - ctx.warn('failed to apply model transaction, skipping', { - _id: tx._id, - _class: tx._class, - message: err?.message - }) - } - } - }) - ctx.withSync('build model', {}, (ctx) => { - model.addTxes(ctx, txes, false) - }) -} - -function getLastTxTime (txes: Tx[]): number { - let lastTxTime = 0 - for (const tx of txes) { - if (tx.modifiedOn > lastTxTime) { - lastTxTime = tx.modifiedOn - } - } - return lastTxTime -} diff --git a/foundations/core/packages/core/src/clone.ts b/foundations/core/packages/core/src/clone.ts deleted file mode 100644 index 060a7e597b..0000000000 --- a/foundations/core/packages/core/src/clone.ts +++ /dev/null @@ -1,84 +0,0 @@ -const se = typeof Symbol !== 'undefined' -const ste = se && typeof Symbol.toStringTag !== 'undefined' - -export function getTypeOf (obj: any): string { - const typeofObj = typeof obj - if (typeofObj !== 'object') { - return typeofObj - } - if (obj === null) { - return 'null' - } - - if (Array.isArray(obj) && (!ste || !(Symbol.toStringTag in obj))) { - return 'Array' - } - - const stringTag = ste && obj[Symbol.toStringTag] - if (typeof stringTag === 'string') { - return stringTag - } - - const objPrototype = Object.getPrototypeOf(obj) - - if (objPrototype === RegExp.prototype) { - return 'RegExp' - } - if (objPrototype === Date.prototype) { - return 'Date' - } - - if (objPrototype === null) { - return 'Object' - } - return {}.toString.call(obj).slice(8, -1) -} - -export function clone ( - obj: any, - as?: (doc: any, m: any) => any, - needAs?: (value: any) => any | undefined, - depth?: number -): any { - if (typeof obj === 'undefined') { - return undefined - } - if (typeof obj === 'function') { - return obj - } - if (depth === 0) { - return obj - } - depth = depth === undefined ? depth : depth - 1 - const typeOf = getTypeOf(obj) - if (typeOf === 'Date') { - return new Date(obj.getTime()) - } else if (typeOf === 'Array' || typeOf === 'Object') { - const isArray = Array.isArray(obj) - const result: any = isArray ? [] : Object.assign({}, obj) - for (const key in obj) { - // include prototype properties - const value = obj[key] - const type = getTypeOf(value) - if (type === 'Array') { - result[key] = clone(value, as, needAs, depth) - } else if (type === 'Object') { - const valClone = clone(value, as, needAs, depth) - result[key] = valClone - } else if (type === 'Date') { - result[key] = new Date(value.getTime()) - } else { - if (isArray) { - result[key] = value - } - } - } - if (typeOf === 'Object') { - const m = needAs?.(obj) - return m !== undefined && as !== undefined ? as(result, m) : result - } - return result - } else { - return obj - } -} diff --git a/foundations/core/packages/core/src/collaboration.ts b/foundations/core/packages/core/src/collaboration.ts deleted file mode 100644 index ecf14a96b9..0000000000 --- a/foundations/core/packages/core/src/collaboration.ts +++ /dev/null @@ -1,53 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { Blob, Class, Doc, MarkupBlobRef, Ref } from './classes' - -/** @public */ -export interface CollaborativeDoc { - objectClass: Ref> - objectId: Ref - objectAttr: string -} - -/** @public */ -export function makeCollabId ( - objectClass: Ref>, - objectId: Ref, - objectAttr: Extract | string -): CollaborativeDoc { - return { objectClass, objectId, objectAttr } -} - -/** @public */ -export function makeDocCollabId ( - doc: T, - objectAttr: Extract | string -): CollaborativeDoc { - return makeCollabId(doc._class, doc._id, objectAttr) -} - -/** @public */ -export function makeCollabYdocId (doc: CollaborativeDoc): Ref { - const { objectId, objectAttr } = doc - return `${objectId}%${objectAttr}` as Ref -} - -/** @public */ -export function makeCollabJsonId (doc: CollaborativeDoc): MarkupBlobRef { - const timestamp = Date.now() - const { objectId, objectAttr } = doc - return [objectId, objectAttr, timestamp].join('-') as MarkupBlobRef -} diff --git a/foundations/core/packages/core/src/collaborators.ts b/foundations/core/packages/core/src/collaborators.ts deleted file mode 100644 index 1574a2c140..0000000000 --- a/foundations/core/packages/core/src/collaborators.ts +++ /dev/null @@ -1,37 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import core, { Class, ClassCollaborators, Doc, Hierarchy, ModelDb, Ref } from '.' - -export function getClassCollaborators ( - model: ModelDb, - hiearachy: Hierarchy, - _id: Ref> -): ClassCollaborators | undefined { - const ancestors = hiearachy.getAncestors(_id) - const collabs = new Map( - model - .findAllSync(core.class.ClassCollaborators, { - attachedTo: { $in: ancestors } - }) - .map((c) => [c.attachedTo, c]) - ) - for (const ancestor of ancestors) { - const res = collabs.get(ancestor) - if (res !== undefined) { - return res - } - } -} diff --git a/foundations/core/packages/core/src/common.ts b/foundations/core/packages/core/src/common.ts deleted file mode 100644 index fc4ee3158f..0000000000 --- a/foundations/core/packages/core/src/common.ts +++ /dev/null @@ -1,41 +0,0 @@ -export function groupByArray (array: T[], keyProvider: (item: T) => K): Map { - const result = new Map() - - array.forEach((item) => { - const key = keyProvider(item) - - if (!result.has(key)) { - result.set(key, [item]) - } else { - result.get(key)?.push(item) - } - }) - - return result -} - -export async function groupByArrayAsync (array: T[], keyProvider: (item: T) => Promise): Promise> { - const result = new Map() - - for (const item of array) { - const key = await keyProvider(item) - - if (!result.has(key)) { - result.set(key, [item]) - } else { - result.get(key)?.push(item) - } - } - - return result -} - -export function flipSet (set: Set, item: T): Set { - if (set.has(item)) { - set.delete(item) - } else { - set.add(item) - } - - return set -} diff --git a/foundations/core/packages/core/src/component.ts b/foundations/core/packages/core/src/component.ts deleted file mode 100644 index 441c27ed75..0000000000 --- a/foundations/core/packages/core/src/component.ts +++ /dev/null @@ -1,312 +0,0 @@ -// -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// -import type { Asset, IntlString, Metadata, Plugin, StatusCode } from '@hcengineering/platform' -import { plugin } from '@hcengineering/platform' -import type { BenchmarkDoc } from './benchmark' -import { AccountRole, TxAccessLevel } from './classes' -import type { - Account, - AnyAttribute, - ArrOf, - Association, - AttachedDoc, - Blob, - Class, - Collection, - Configuration, - ConfigurationElement, - Doc, - DomainIndexConfiguration, - Enum, - EnumOf, - FullTextSearchContext, - Hyperlink, - IndexingConfiguration, - Interface, - MarkupBlobRef, - MigrationState, - Mixin, - Obj, - Permission, - PersonId, - PluginConfiguration, - Rank, - Ref, - RefTo, - RelatedDocument, - Relation, - Role, - Sequence, - CustomSequence, - Space, - SpaceType, - SpaceTypeDescriptor, - SystemSpace, - Timestamp, - TransientConfiguration, - Type, - TypeAny, - TypedSpace, - UserStatus, - Version, - AccountUuid, - ClassCollaborators, - Collaborator -} from './classes' -import { type Status, type StatusCategory } from './status' -import type { - Tx, - TxApplyIf, - TxCUD, - TxCreateDoc, - TxDomainEvent, - TxMixin, - TxModelUpgrade, - TxRemoveDoc, - TxUpdateDoc, - TxWorkspaceEvent -} from './tx' - -/** - * @public - */ -export const coreId = 'core' as Plugin - -/** - * @public - */ -// TODO: consider removing email? -export const systemAccountEmail = 'anticrm@hc.engineering' -export const systemAccountUuid = '1749089e-22e6-48de-af4e-165e18fbd2f9' as AccountUuid -export const systemAccount: Account = { - uuid: systemAccountUuid, - role: AccountRole.Owner, - primarySocialId: '' as PersonId, - socialIds: [], - fullSocialIds: [] -} - -export const configUserAccountUuid = '0d94731c-0787-4bcd-aefe-304efc3706b1' as AccountUuid - -export const readOnlyGuestAccountUuid = '83bbed9a-0867-4851-be32-31d49d1d42ce' as AccountUuid - -export default plugin(coreId, { - class: { - Obj: '' as Ref>, - Doc: '' as Ref>, - Blob: '' as Ref>, - AttachedDoc: '' as Ref>, - Class: '' as Ref>>, - Mixin: '' as Ref>>, - Interface: '' as Ref>>, - Attribute: '' as Ref>, - Tx: '' as Ref>, - TxModelUpgrade: '' as Ref>, - TxWorkspaceEvent: '' as Ref>, - TxDomainEvent: '' as Ref>, - TxApplyIf: '' as Ref>, - TxCUD: '' as Ref>>, - TxCreateDoc: '' as Ref>>, - TxMixin: '' as Ref>>, - TxUpdateDoc: '' as Ref>>, - TxRemoveDoc: '' as Ref>>, - Space: '' as Ref>, - SystemSpace: '' as Ref>, - TypedSpace: '' as Ref>, - SpaceTypeDescriptor: '' as Ref>, - SpaceType: '' as Ref>, - Role: '' as Ref>, - Permission: '' as Ref>, - Type: '' as Ref>>, - TypeRelation: '' as Ref>>, - TypeString: '' as Ref>>, - TypeBlob: '' as Ref>>>, - TypeIntlString: '' as Ref>>, - TypeHyperlink: '' as Ref>>, - TypeNumber: '' as Ref>>, - TypeFileSize: '' as Ref>>, - TypeMarkup: '' as Ref>>, - TypeRank: '' as Ref>>, - TypeRecord: '' as Ref>>>, - TypeBoolean: '' as Ref>>, - TypeTimestamp: '' as Ref>>, - TypeDate: '' as Ref>>, - TypeCollaborativeDoc: '' as Ref>>, - TypePersonId: '' as Ref>>, - TypeAccountUuid: '' as Ref>>, - TypeIdentifier: '' as Ref>>, - RefTo: '' as Ref>>, - ArrOf: '' as Ref>>, - Enum: '' as Ref>, - EnumOf: '' as Ref>, - Collection: '' as Ref>>, - TypeAny: '' as Ref>, - Version: '' as Ref>, - PluginConfiguration: '' as Ref>, - UserStatus: '' as Ref>, - - TypeRelatedDocument: '' as Ref>>, - DomainIndexConfiguration: '' as Ref>, - - Configuration: '' as Ref>, - - Status: '' as Ref>, - StatusCategory: '' as Ref>, - MigrationState: '' as Ref>, - - BenchmarkDoc: '' as Ref>, - FullTextSearchContext: '' as Ref>, - Association: '' as Ref>, - Relation: '' as Ref>, - Sequence: '' as Ref>, - CustomSequence: '' as Ref>, - ClassCollaborators: '' as Ref>>, - Collaborator: '' as Ref> - }, - icon: { - TypeString: '' as Asset, - TypeBlob: '' as Asset, - TypeHyperlink: '' as Asset, - TypeNumber: '' as Asset, - TypeMarkup: '' as Asset, - TypeRank: '' as Asset, - TypeRecord: '' as Asset, - TypeBoolean: '' as Asset, - TypeDate: '' as Asset, - TypeRef: '' as Asset, - TypeArray: '' as Asset, - TypeEnumOf: '' as Asset, - TypeCollection: '' as Asset - }, - mixin: { - ConfigurationElement: '' as Ref>, - IndexConfiguration: '' as Ref>>, - SpacesTypeData: '' as Ref>, - TransientConfiguration: '' as Ref>, - TxAccessLevel: '' as Ref> - }, - space: { - Tx: '' as Ref, - DerivedTx: '' as Ref, - Model: '' as Ref, - Space: '' as Ref, - Configuration: '' as Ref, - Workspace: '' as Ref, - Domain: '' as Ref - }, - employee: { - System: '' as Ref // An system employee reference. - }, - account: { - System: '' as PersonId, - ConfigUser: '' as PersonId - }, - status: { - ObjectNotFound: '' as StatusCode<{ _id: Ref }>, - ItemNotFound: '' as StatusCode<{ _id: Ref, _localId: string }> - }, - version: { - Model: '' as Ref - }, - string: { - Id: '' as IntlString, - Space: '' as IntlString, - Spaces: '' as IntlString, - SpacesDescription: '' as IntlString, - TypedSpace: '' as IntlString, - SpaceType: '' as IntlString, - Modified: '' as IntlString, - ModifiedDate: '' as IntlString, - ModifiedBy: '' as IntlString, - Class: '' as IntlString, - AttachedTo: '' as IntlString, - AttachedToClass: '' as IntlString, - String: '' as IntlString, - Record: '' as IntlString, - Markup: '' as IntlString, - Relation: '' as IntlString, - Relations: '' as IntlString, - AddRelation: '' as IntlString, - Collaborative: '' as IntlString, - CollaborativeDoc: '' as IntlString, - MarkupBlobRef: '' as IntlString, - PersonId: '' as IntlString, - AccountId: '' as IntlString, - Number: '' as IntlString, - Boolean: '' as IntlString, - Timestamp: '' as IntlString, - Date: '' as IntlString, - IntlString: '' as IntlString, - Ref: '' as IntlString, - Collection: '' as IntlString, - Array: '' as IntlString, - Name: '' as IntlString, - Enum: '' as IntlString, - Size: '' as IntlString, - Description: '' as IntlString, - ShortDescription: '' as IntlString, - Descriptor: '' as IntlString, - TargetClass: '' as IntlString, - Role: '' as IntlString, - Roles: '' as IntlString, - Hyperlink: '' as IntlString, - Private: '' as IntlString, - Object: '' as IntlString, - System: '' as IntlString, - CreatedBy: '' as IntlString, - CreatedDate: '' as IntlString, - Status: '' as IntlString, - Account: '' as IntlString, - StatusCategory: '' as IntlString, - Rank: '' as IntlString, - Members: '' as IntlString, - Owners: '' as IntlString, - Permission: '' as IntlString, - CreateObject: '' as IntlString, - UpdateObject: '' as IntlString, - DeleteObject: '' as IntlString, - ForbidDeleteObject: '' as IntlString, - UpdateSpace: '' as IntlString, - ArchiveSpace: '' as IntlString, - CreateObjectDescription: '' as IntlString, - UpdateObjectDescription: '' as IntlString, - DeleteObjectDescription: '' as IntlString, - ForbidDeleteObjectDescription: '' as IntlString, - UpdateSpaceDescription: '' as IntlString, - ArchiveSpaceDescription: '' as IntlString, - AutoJoin: '' as IntlString, - AutoJoinDescr: '' as IntlString - }, - descriptor: { - SpacesType: '' as Ref - }, - spaceType: { - SpacesType: '' as Ref - }, - permission: { - CreateObject: '' as Ref, - UpdateObject: '' as Ref, - DeleteObject: '' as Ref, - ForbidDeleteObject: '' as Ref, - UpdateSpace: '' as Ref, - ArchiveSpace: '' as Ref - }, - role: { - Admin: '' as Ref - }, - metadata: { - DisablePermissions: '' as Metadata - } -}) diff --git a/foundations/core/packages/core/src/hierarchy.ts b/foundations/core/packages/core/src/hierarchy.ts deleted file mode 100644 index 9b8fc3665c..0000000000 --- a/foundations/core/packages/core/src/hierarchy.ts +++ /dev/null @@ -1,650 +0,0 @@ -// -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { type FindOptions, type Lookup, type ToClassRefT, type WithLookup } from '.' -import type { AnyAttribute, Class, Classifier, Doc, Domain, Interface, Mixin, Obj, Ref } from './classes' -import { ClassifierKind } from './classes' -import { clone as deepClone } from './clone' -import core from './component' -import { _createMixinProxy, _mixinClass, _toDoc, PROXY_MIXIN_CLASS_KEY } from './proxy' -import type { Tx, TxCreateDoc, TxCUD, TxMixin, TxRemoveDoc, TxUpdateDoc } from './tx' -import { TxProcessor } from './tx' - -/** - * @public - */ -export class Hierarchy { - private readonly classifiers = new Map, Classifier>() - private readonly attributes = new Map, Map>() - private readonly attributesById = new Map, AnyAttribute>() - private readonly descendants = new Map, Ref[]>() - private readonly ancestors = new Map, Ref[]>() - private readonly proxies = new Map>, ProxyHandler>() - - private readonly classifierProperties = new Map, Map>() - - private createMixinProxyHandler (mixin: Ref>): ProxyHandler { - const value = this.getClass(mixin) - const ancestor = this.getClass(value.extends as Ref>) - const ancestorProxy = ancestor.kind === ClassifierKind.MIXIN ? this.getMixinProxyHandler(ancestor._id) : null - return _createMixinProxy(value, ancestorProxy) - } - - private getMixinProxyHandler (mixin: Ref>): ProxyHandler { - const handler = this.proxies.get(mixin) - if (handler === undefined) { - const handler = this.createMixinProxyHandler(mixin) - this.proxies.set(mixin, handler) - return handler - } - return handler - } - - as(doc: D, mixin: Ref>): M { - if ((doc as any)[PROXY_MIXIN_CLASS_KEY] === mixin) return doc as M - - return new Proxy(Hierarchy.toDoc(doc), this.getMixinProxyHandler(mixin)) as M - } - - asIf(doc: D | undefined, mixin: Ref>): M | undefined { - if (doc === undefined) { - return undefined - } - return this.hasMixin(doc, mixin) ? this.as(doc, mixin) : undefined - } - - asIfArray(docs: D[], mixin: Ref>): M[] { - return docs.map((it) => this.asIf(it, mixin)).filter((it) => it !== undefined) - } - - static toDoc(doc: D): D { - return _toDoc(doc) - } - - static mixinClass(doc: D): Ref> | undefined { - return _mixinClass(doc) - } - - static mixinOrClass(doc: D): Ref | Class> { - const m = _mixinClass(doc) - return m ?? doc._class - } - - static hasMixin(doc: D, mixin: Ref>): boolean { - const d = Hierarchy.toDoc(doc) - return typeof (d as any)[mixin] === 'object' - } - - hasMixin(doc: D, mixin: Ref>): boolean { - return Hierarchy.hasMixin(doc, mixin) - } - - classHierarchyMixin( - _class: Ref>, - mixin: Ref>, - filter?: (value: M) => boolean - ): M | undefined { - let clazz = this.getClass(_class) - while (true) { - if (this.hasMixin(clazz, mixin)) { - const m = this.as(clazz, mixin) as any as M - if (m !== undefined && (filter?.(m) ?? true)) { - return m - } - } - if (clazz.extends === undefined) return - clazz = this.getClass(clazz.extends) - } - } - - findClassOrMixinMixin(doc: Doc, mixin: Ref>): M | undefined { - const cc = this.classHierarchyMixin(doc._class, mixin) - if (cc !== undefined) { - return cc - } - - const _doc = _toDoc(doc) - // Find all potential mixins of doc - for (const [k, v] of Object.entries(_doc)) { - if (typeof v === 'object' && this.classifiers.has(k as Ref)) { - const cc = this.classHierarchyMixin(k as Ref>, mixin) - if (cc !== undefined) { - return cc - } - } - } - } - - findMixinMixins(doc: Doc, mixin: Ref>): M[] { - const _doc = _toDoc(doc) - const result: M[] = [] - const resultSet = new Set() - // Find all potential mixins of doc - for (const [k, v] of Object.entries(_doc)) { - if (typeof v === 'object' && this.classifiers.has(k as Ref)) { - const clazz = this.getClass(k as Ref) - if (this.hasMixin(clazz, mixin)) { - const cc = this.as(clazz, mixin) as any as M - if (cc !== undefined && !resultSet.has(cc._id)) { - result.push(cc) - resultSet.add(cc._id) - } - } - } - } - return result - } - - findAllMixins(doc: Doc): Ref>[] { - const _doc = _toDoc(doc) - const resultSet = new Set>>() - for (const [k, v] of Object.entries(_doc)) { - if (typeof v === 'object' && this.classifiers.has(k as Ref)) { - if (this.isMixin(k as Ref)) { - if (!resultSet.has(k as Ref)) { - resultSet.add(k as Ref) - } - } - } - } - return Array.from(resultSet) - } - - isMixin (_class: Ref>): boolean { - const data = this.classifiers.get(_class) - return data !== undefined && this._isMixin(data) - } - - getAncestors (_class: Ref): Ref[] { - const result = this.ancestors.get(_class) - if (result === undefined) { - throw new Error('ancestors not found: ' + _class) - } - return result - } - - getClass(_class: Ref>): Class { - const data = this.classifiers.get(_class) - if (data === undefined || this.isInterface(data)) { - throw new Error('class not found: ' + _class) - } - return data - } - - findClass(_class: Ref>): Class | undefined { - const data = this.classifiers.get(_class) - if (data === undefined || this.isInterface(data)) { - return undefined - } - return data - } - - hasClass(_class: Ref>): boolean { - const data = this.classifiers.get(_class) - - return !(data === undefined || this.isInterface(data)) - } - - getClassOrInterface (_class: Ref>): Class { - const data = this.classifiers.get(_class) - if (data === undefined) { - throw new Error('class not found: ' + _class) - } - return data - } - - getInterface (_interface: Ref>): Interface { - const data = this.classifiers.get(_interface) - if (data === undefined || !this.isInterface(data)) { - throw new Error('interface not found: ' + _interface) - } - return data - } - - getDomain (_class: Ref>): Domain { - const domain = this.findDomain(_class) - if (domain === undefined) { - throw new Error(`domain not found: ${_class} `) - } - return domain - } - - public findDomain (_class: Ref>): Domain | undefined { - const klazz = this.findClass(_class) - if (klazz === undefined) return - if (klazz.domain !== undefined) { - return klazz.domain - } - - let _klazz: Class | undefined = klazz - while (_klazz.extends !== undefined) { - _klazz = this.findClass(_klazz.extends) - if (_klazz === undefined) return - if (_klazz.domain !== undefined) { - // Cache for next requests - klazz.domain = _klazz.domain - return _klazz.domain - } - } - } - - tx (tx: Tx): void { - switch (tx._class) { - case core.class.TxCreateDoc: - this.txCreateDoc(tx as TxCreateDoc) - return - case core.class.TxUpdateDoc: - this.txUpdateDoc(tx as TxUpdateDoc) - return - case core.class.TxRemoveDoc: - this.txRemoveDoc(tx as TxRemoveDoc) - return - case core.class.TxMixin: - this.txMixin(tx as TxMixin) - } - } - - private isClassifierTx (tx: TxCUD): boolean { - const base = [core.class.Class, core.class.Mixin, core.class.Interface] - return base.includes(tx.objectClass) || this.isDerived(tx.objectClass, core.class.Class) - } - - private txCreateDoc (tx: TxCreateDoc): void { - if (this.isClassifierTx(tx)) { - const _id = tx.objectId as Ref - this.classifiers.set(_id, TxProcessor.createDoc2Doc(tx as TxCreateDoc)) - this.updateAncestors(_id) - this.updateDescendant(_id) - } else if (tx.objectClass === core.class.Attribute) { - const createTx = tx as TxCreateDoc - this.addAttribute(TxProcessor.createDoc2Doc(createTx)) - } - } - - private txUpdateDoc (tx: TxUpdateDoc): void { - if (tx.objectClass === core.class.Attribute) { - const updateTx = tx as TxUpdateDoc - const doc = this.attributesById.get(updateTx.objectId) - if (doc === undefined) return - this.addAttribute(TxProcessor.updateDoc2Doc(doc, updateTx)) - - this.classifierProperties.delete(doc.attributeOf) - } else if (this.isClassifierTx(tx)) { - const updateTx = tx as TxUpdateDoc>> - const doc = this.classifiers.get(updateTx.objectId) - if (doc === undefined) return - TxProcessor.updateDoc2Doc(doc, updateTx) - this.classifierProperties.delete(doc._id) - } - } - - private txRemoveDoc (tx: TxRemoveDoc): void { - if (tx.objectClass === core.class.Attribute) { - const removeTx = tx as TxRemoveDoc - const doc = this.attributesById.get(removeTx.objectId) - if (doc === undefined) return - const map = this.attributes.get(doc.attributeOf) - map?.delete(doc.name) - this.attributesById.delete(removeTx.objectId) - } else if (this.isClassifierTx(tx)) { - const removeTx = tx as TxRemoveDoc>> - this.updateDescendant(removeTx.objectId, false) - this.updateAncestors(removeTx.objectId, false) - this.classifiers.delete(removeTx.objectId) - } - } - - private txMixin (tx: TxMixin): void { - if (this.isClassifierTx(tx)) { - const obj = this.getClass(tx.objectId as Ref>) as any - TxProcessor.updateMixin4Doc(obj, tx) - } - } - - /** - * Check if passed _class is derived from `from` class. - * It will iterate over parents. - */ - isDerived(_class: Ref>, from: Ref>): boolean { - return this.ancestors.get(_class)?.includes(from) ?? false - } - - /** - * Return first non interface/mixin parent - */ - getBaseClass(_class: Ref>): Ref> { - let cl: Ref> | undefined = _class - while (cl !== undefined) { - const clz: Class = this.getClass(cl) - if (this.isClass(clz)) return cl - cl = clz.extends - } - return core.class.Doc - } - - /** - * Check if passed _class implements passed interfaces `from`. - * It will check for class parents and their interfaces. - */ - isImplements(_class: Ref>, from: Ref>): boolean { - let cl: Ref> | undefined = _class - while (cl !== undefined) { - const klazz: Class = this.getClass(cl) - if (this.isExtends(klazz.implements ?? [], from)) { - return true - } - cl = klazz.extends - } - return false - } - - /** - * Check if interface extends passed interface. - */ - private isExtends(extendsOrImplements: Ref>[], from: Ref>): boolean { - const result: Ref>[] = [] - const toVisit = [...extendsOrImplements] - while (toVisit.length > 0) { - const ref = toVisit.shift() as Ref> - if (ref === from) { - return true - } - addIf(result, ref) - toVisit.push(...this.ancestorsOf(ref)) - } - return false - } - - getDescendants(_class: Ref>): Ref>[] { - const data = this.descendants.get(_class) - if (data === undefined) { - throw new Error('descendants not found: ' + _class) - } - return data - } - - private updateDescendant (_class: Ref, add = true): void { - let hierarchy: Ref[] = [] - - try { - hierarchy = this.getAncestors(_class) - } catch (err) { - if (add) { - throw err - } - } - - for (const cls of hierarchy) { - const list = this.descendants.get(cls) - if (list === undefined) { - if (add) { - this.descendants.set(cls, [_class]) - } - } else { - if (add) { - list.push(_class) - } else { - const pos = list.indexOf(_class) - if (pos !== -1) { - list.splice(pos, 1) - } - } - } - } - } - - private updateAncestors (_class: Ref, add = true): void { - const cl: Ref[] = [_class] - const visited = new Set>() - const ancestorList: Ref[] = [] - - while (cl.length > 0) { - const classifier = cl.shift() as Ref - if (addNew(visited, classifier)) { - ancestorList.push(classifier) - cl.push(...this.ancestorsOf(classifier)) - } - } - - if (add) { - this.ancestors.set(_class, ancestorList) - } else { - this.ancestors.delete(_class) - } - } - - /** - * Return extends and implemnets as combined list of references - */ - private ancestorsOf (classifier: Ref): Ref[] { - const attrs = this.classifiers.get(classifier) - const result: Ref[] = [] - if (this.isClass(attrs) || this._isMixin(attrs)) { - const cls = attrs as Class - if (cls.extends !== undefined) { - result.push(cls.extends) - } - result.push(...(cls.implements ?? [])) - } - if (this.isInterface(attrs)) { - result.push(...((attrs as Interface).extends ?? [])) - } - return result - } - - private isClass (attrs?: Classifier): boolean { - return attrs?.kind === ClassifierKind.CLASS - } - - private _isMixin (attrs?: Classifier): boolean { - return attrs?.kind === ClassifierKind.MIXIN - } - - private isInterface (attrs?: Classifier): boolean { - return attrs?.kind === ClassifierKind.INTERFACE - } - - private addAttribute (attribute: AnyAttribute): void { - const _class = attribute.attributeOf - let attributes = this.attributes.get(_class) - if (attributes === undefined) { - attributes = new Map() - this.attributes.set(_class, attributes) - } - attributes.set(attribute.name, attribute) - this.attributesById.set(attribute._id, attribute) - this.classifierProperties.delete(attribute.attributeOf) - } - - getAllAttributes ( - clazz: Ref, - to?: Ref, - traverse?: (name: string, attr: AnyAttribute) => void - ): Map { - const result = new Map() - let ancestors = this.getAncestors(clazz) - if (to !== undefined) { - const toAncestors = this.getAncestors(to) - for (const uto of toAncestors) { - if (ancestors.includes(uto)) { - to = uto - break - } - } - ancestors = ancestors.filter( - (c) => c !== to && (this.isInterface(this.classifiers.get(c)) || this.isDerived(c, to as Ref>)) - ) - } - - for (let index = ancestors.length - 1; index >= 0; index--) { - const cls = ancestors[index] - const attributes = this.attributes.get(cls) - if (attributes !== undefined) { - for (const [name, attr] of attributes) { - traverse?.(name, attr) - result.set(name, attr) - } - } - } - - return result - } - - getOwnAttributes (clazz: Ref): Map { - const result = new Map() - - const attributes = this.attributes.get(clazz) - if (attributes !== undefined) { - for (const [name, attr] of attributes) { - result.set(name, attr) - } - } - - return result - } - - getParentClass (_class: Ref>): Ref> { - const baseDomain = this.getDomain(_class) - const ancestors = this.getAncestors(_class) - let result: Ref> = _class - for (const ancestor of ancestors) { - try { - const domain = this.getClass(ancestor).domain - if (domain === baseDomain) { - result = ancestor - } - } catch {} - } - return result - } - - getAttribute (classifier: Ref, name: string): AnyAttribute { - const attr = this.findAttribute(classifier, name) - if (attr === undefined) { - throw new Error('attribute not found: ' + name) - } - return attr - } - - public findAttribute (classifier: Ref, name: string): AnyAttribute | undefined { - const list = [classifier] - const visited = new Set>() - while (list.length > 0) { - const cl = list.shift() as Ref - if (addNew(visited, cl)) { - const attribute = this.attributes.get(cl)?.get(name) - if (attribute !== undefined) { - return attribute - } - // Check ancestorsOf - list.push(...this.ancestorsOf(cl)) - } - } - } - - updateLookupMixin( - _class: Ref>, - result: WithLookup, - options?: FindOptions - ): WithLookup { - const baseClass = this.getBaseClass(_class) - const vResult = baseClass !== _class ? this.as(result, _class) : result - const lookup = result.$lookup - if (lookup !== undefined) { - // We need to check if lookup type is mixin and cast to it if required. - const lu = options?.lookup as Lookup - if (lu?._id !== undefined) { - for (const [k, v] of Object.entries(lu._id)) { - const _cl = getClass(v as ToClassRefT) - if (this.isMixin(_cl)) { - const mval = (lookup as any)[k] - if (mval !== undefined) { - if (Array.isArray(mval)) { - ;(lookup as any)[k] = mval.map((it) => this.as(it, _cl)) - } else { - ;(lookup as any)[k] = this.as(mval, _cl) - } - } - } - } - } - for (const [k, v] of Object.entries(lu ?? {})) { - if (k === '_id') { - continue - } - const _cl = getClass(v as ToClassRefT) - if (this.isMixin(_cl)) { - const mval = (lookup as any)[k] - if (mval != null) { - ;(lookup as any)[k] = this.as(mval, _cl) - } - } - } - } - return vResult - } - - clone (obj: any): any { - return deepClone( - obj, - (doc, m) => this.as(doc, m), - (value) => Hierarchy.mixinClass(value) - ) - } - - domains (): Domain[] { - const classes = Array.from(this.classifiers.values()).filter( - (it) => this.isClass(it) || this._isMixin(it) - ) as Class[] - return classes - .map((it) => it.domain) - .filter((it) => it !== undefined) - .filter((it, idx, array) => array.findIndex((pt) => pt === it) === idx) - } - - getClassifierProp (cl: Ref>, prop: string): any | undefined { - return this.classifierProperties.get(cl)?.get(prop) - } - - setClassifierProp (cl: Ref>, prop: string, value: any): void { - let cur = this.classifierProperties.get(cl) - if (cur === undefined) { - cur = new Map() - this.classifierProperties.set(cl, cur) - } - cur.set(prop, value) - } -} - -function addNew (val: Set, value: T): boolean { - if (val.has(value)) { - return false - } - val.add(value) - return true -} - -function addIf (array: T[], value: T): void { - if (!array.includes(value)) { - array.push(value) - } -} - -function getClass (vvv: ToClassRefT): Ref> { - if (Array.isArray(vvv)) { - return vvv[0] - } - return vvv -} diff --git a/foundations/core/packages/core/src/index.ts b/foundations/core/packages/core/src/index.ts deleted file mode 100644 index 5cc09fb86b..0000000000 --- a/foundations/core/packages/core/src/index.ts +++ /dev/null @@ -1,47 +0,0 @@ -// -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. - -import core from './component' - -export * from './classes' -export * from './client' -export * from './collaboration' -export { - coreId, - systemAccountUuid, - readOnlyGuestAccountUuid, - systemAccountEmail, - systemAccount, - configUserAccountUuid -} from './component' -export * from './hierarchy' -export * from '@hcengineering/measurements' -export * from './memdb' -export * from './objvalue' -export * from './operations' -export * from './operator' -export * from './query' -export * from './server' -export * from './storage' -export * from './tx' -export * from './utils' -export * from './backup' -export * from './status' -export * from './clone' -export * from './common' -export * from './time' -export * from './benchmark' -export * from './collaborators' - -export default core diff --git a/foundations/core/packages/core/src/memdb.ts b/foundations/core/packages/core/src/memdb.ts deleted file mode 100644 index 9feddf0201..0000000000 --- a/foundations/core/packages/core/src/memdb.ts +++ /dev/null @@ -1,399 +0,0 @@ -// -// Copyright © 2020 Anticrm Platform Contributors. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { PlatformError, Severity, Status } from '@hcengineering/platform' -import { type Lookup, type MeasureContext, type ReverseLookups, getObjectValue } from '.' -import type { Class, Doc, Ref } from './classes' - -import core from './component' -import { type Hierarchy } from './hierarchy' -import { checkMixinKey, matchQuery, resultSort } from './query' -import type { - AssociationQuery, - DocumentQuery, - FindOptions, - FindResult, - LookupData, - Storage, - TxResult, - WithLookup -} from './storage' -import type { Tx, TxCreateDoc, TxMixin, TxRemoveDoc, TxUpdateDoc } from './tx' -import { TxProcessor } from './tx' -import { toFindResult } from './utils' - -/** - * @public - */ -export abstract class MemDb extends TxProcessor implements Storage { - private readonly objectsByClass = new Map>, Map, Doc>>() - private readonly objectById = new Map, Doc>() - - constructor (protected readonly hierarchy: Hierarchy) { - super() - } - - private getObjectsByClass (_class: Ref>): Map, Doc> { - const result = this.objectsByClass.get(_class) - if (result === undefined) { - const result = new Map, Doc>() - this.objectsByClass.set(_class, result) - return result - } - return result - } - - private cleanObjectByClass (_class: Ref>, _id: Ref): void { - const result = this.objectsByClass.get(_class) - if (result !== undefined) { - result.delete(_id) - } - } - - private getByIdQuery(query: DocumentQuery, _class: Ref>): T[] { - const result: T[] = [] - if (typeof query._id === 'string') { - const obj = this.objectById.get(query._id) as T - if (obj !== undefined && this.hierarchy.isDerived(obj._class, _class)) result.push(obj) - } else if (query._id?.$in !== undefined) { - const ids = new Set(query._id.$in) - for (const id of ids) { - const obj = this.objectById.get(id) as T - if (obj !== undefined && this.hierarchy.isDerived(obj._class, _class)) result.push(obj) - } - } - return result - } - - getObject(_id: Ref): T { - const doc = this.objectById.get(_id) - if (doc === undefined) { - throw new PlatformError(new Status(Severity.ERROR, core.status.ObjectNotFound, { _id })) - } - return doc as T - } - - findObject(_id: Ref): T | undefined { - const doc = this.objectById.get(_id) - return doc as T - } - - private async getLookupValue( - _class: Ref>, - doc: T, - lookup: Lookup, - result: LookupData - ): Promise { - for (const key in lookup) { - if (key === '_id') { - await this.getReverseLookupValue(doc, lookup, result) - continue - } - const value = (lookup as any)[key] - const tkey = checkMixinKey(key, _class, this.hierarchy) - if (Array.isArray(value)) { - const [_class, nested] = value - const objects = await this.findAll(_class, { _id: getObjectValue(tkey, doc) }) - ;(result as any)[key] = objects[0] - const nestedResult = {} - const parent = (result as any)[key] - await this.getLookupValue(_class, parent, nested, nestedResult) - Object.assign(parent, { - $lookup: nestedResult - }) - } else { - const objects = await this.findAll(value, { _id: getObjectValue(tkey, doc) }) - ;(result as any)[key] = objects[0] - } - } - } - - private async getReverseLookupValue( - doc: T, - lookup: ReverseLookups, - result: LookupData - ): Promise { - for (const key in lookup._id) { - const value = lookup._id[key] - if (Array.isArray(value)) { - const objects = await this.findAll(value[0], { [value[1]]: doc._id }) - ;(result as any)[key] = objects - } else { - const objects = await this.findAll(value, { attachedTo: doc._id }) - ;(result as any)[key] = objects - } - } - } - - private async lookup(_class: Ref>, docs: T[], lookup: Lookup): Promise[]> { - const withLookup: WithLookup[] = [] - for (const doc of docs) { - const result: LookupData = {} - await this.getLookupValue(_class, doc, lookup, result) - withLookup.push(Object.assign({}, doc, { $lookup: result })) - } - return withLookup - } - - private async fillAssociations(docs: T[], associations: AssociationQuery[]): Promise[]> { - const withLookup: WithLookup[] = [] - for (const doc of docs) { - const result = await this.getAssociationValue(doc, associations) - withLookup.push(Object.assign({}, doc, { $associations: result })) - } - return withLookup - } - - private async getAssociationValue( - doc: T, - associations: AssociationQuery[] - ): Promise> { - const result: Record = {} - for (const association of associations) { - const _id = association[0] - const assoc = this.findObject(_id) - if (assoc === undefined) continue - const isReverse = association[1] === -1 - const key = !isReverse ? 'docA' : 'docB' - const key2 = !isReverse ? 'docB' : 'docA' - const _class = !isReverse ? assoc.classB : assoc.classA - const relations = await this.findAll(core.class.Relation, { association: _id, [key]: doc._id }) - const objects = await this.findAll(_class, { _id: { $in: relations.map((r) => r[key2]) } }) - result[_id] = objects - } - return result - } - - async findAll( - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): Promise> { - let result: WithLookup[] - const baseClass = this.hierarchy.getBaseClass(_class) - if ( - Object.prototype.hasOwnProperty.call(query, '_id') && - (typeof query._id === 'string' || query._id?.$in !== undefined || query._id === undefined || query._id === null) - ) { - result = this.getByIdQuery(query, baseClass) - } else { - result = Array.from(this.getObjectsByClass(baseClass).values()) - } - - result = matchQuery(result, query, _class, this.hierarchy, true) - - if (baseClass !== _class) { - // We need to filter instances without mixin was set - result = result.filter((r) => (r as any)[_class] !== undefined) - } - - if (options?.lookup !== undefined) { - result = await this.lookup(_class, result as T[], options.lookup) - result = matchQuery(result, query, _class, this.hierarchy) - } - - if (options?.associations !== undefined) { - result = await this.fillAssociations(result, options.associations) - } - - if (options?.sort !== undefined) resultSort(result, options?.sort, _class, this.hierarchy, this) - const total = result.length - result = result.slice(0, options?.limit) - const tresult = this.hierarchy.clone(result) as WithLookup[] - const res = tresult.map((it) => this.hierarchy.updateLookupMixin(_class, it, options)) - return toFindResult(res, total) - } - - async findOne( - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): Promise | undefined> { - return (await this.findAll(_class, query, { ...options, limit: 1 }))[0] - } - - /** - * Only in model find without lookups and sorting. - * Do not clone results, so be aware modifications are not allowed. - */ - findAllSync(_class: Ref>, query: DocumentQuery, options?: FindOptions): FindResult { - let result: WithLookup[] - const baseClass = this.hierarchy.getBaseClass(_class) - if ( - Object.prototype.hasOwnProperty.call(query, '_id') && - (typeof query._id === 'string' || query._id?.$in !== undefined || query._id === undefined || query._id === null) - ) { - result = this.getByIdQuery(query, baseClass) - } else { - result = Array.from(this.getObjectsByClass(baseClass).values()) - } - - result = matchQuery(result, query, _class, this.hierarchy, true) - - if (baseClass !== _class) { - // We need to filter instances without mixin was set - result = result.filter((r) => (r as any)[_class] !== undefined) - } - const total = result.length - result = result.slice(0, options?.limit) - - return toFindResult( - result.map((it) => { - return baseClass !== _class ? this.hierarchy.as(it, _class) : it - }) as WithLookup[], - total - ) - } - - addDoc (doc: Doc): void { - this.hierarchy.getAncestors(doc._class).forEach((_class) => { - const arr = this.getObjectsByClass(_class) - arr.set(doc._id, doc) - }) - - this.objectById.set(doc._id, doc) - } - - delDoc (_id: Ref): void { - const doc = this.objectById.get(_id) - if (doc === undefined) { - throw new PlatformError(new Status(Severity.ERROR, core.status.ObjectNotFound, { _id })) - } - this.objectById.delete(_id) - this.hierarchy.getAncestors(doc._class).forEach((_class) => { - this.cleanObjectByClass(_class, _id) - }) - } - - updateDoc (_id: Ref, doc: Doc, update: TxUpdateDoc | TxMixin): void { - // TODO: track updates on Contact to adjust memdb accounts? - } -} - -/** - * Hold transactions - * - * @public - */ -export class TxDb extends MemDb { - protected txCreateDoc (tx: TxCreateDoc): Promise { - throw new Error('Method not implemented.') - } - - protected txUpdateDoc (tx: TxUpdateDoc): Promise { - throw new Error('Method not implemented.') - } - - protected txRemoveDoc (tx: TxRemoveDoc): Promise { - throw new Error('Method not implemented.') - } - - protected txMixin (tx: TxMixin): Promise { - throw new Error('Method not implemented.') - } - - async tx (tx: Tx): Promise { - this.addDoc(tx) - return [] - } -} - -/** - * Hold model objects and classes - * - * @public - */ -export class ModelDb extends MemDb { - protected override async txCreateDoc (tx: TxCreateDoc): Promise { - this.addDoc(TxProcessor.createDoc2Doc(tx)) - return {} - } - - addTxes (ctx: MeasureContext, txes: Tx[], clone: boolean): void { - for (const tx of txes) { - switch (tx._class) { - case core.class.TxCreateDoc: - this.addDoc(TxProcessor.createDoc2Doc(tx as TxCreateDoc, clone)) - break - case core.class.TxUpdateDoc: { - const cud = tx as TxUpdateDoc - const doc = this.findObject(cud.objectId) - if (doc !== undefined) { - this.updateDoc(cud.objectId, doc, cud) - TxProcessor.updateDoc2Doc(doc, cud) - } else { - ctx.warn('no document found, failed to apply model transaction, skipping', { - _id: tx._id, - _class: tx._class, - objectId: cud.objectId - }) - } - break - } - case core.class.TxRemoveDoc: - try { - this.delDoc((tx as TxRemoveDoc).objectId) - } catch (err: any) { - ctx.warn('no document found, failed to apply model transaction, skipping', { - _id: tx._id, - _class: tx._class, - objectId: (tx as TxRemoveDoc).objectId - }) - } - break - case core.class.TxMixin: { - const mix = tx as TxMixin - const doc = this.findObject(mix.objectId) - if (doc !== undefined) { - this.updateDoc(mix.objectId, doc, mix) - TxProcessor.updateMixin4Doc(doc, mix) - } else { - ctx.warn('no document found, failed to apply model transaction, skipping', { - _id: tx._id, - _class: tx._class, - objectId: mix.objectId - }) - } - break - } - } - } - } - - protected async txUpdateDoc (tx: TxUpdateDoc): Promise { - try { - const doc = this.getObject(tx.objectId) as any - this.updateDoc(tx.objectId, doc, tx) - TxProcessor.updateDoc2Doc(doc, tx) - return tx.retrieve === true ? { object: doc } : {} - } catch (err: any) {} - return {} - } - - protected async txRemoveDoc (tx: TxRemoveDoc): Promise { - try { - this.delDoc(tx.objectId) - } catch (err: any) {} - return {} - } - - // TODO: process ancessor mixins - protected async txMixin (tx: TxMixin): Promise { - const doc = this.getObject(tx.objectId) as any - this.updateDoc(tx.objectId, doc, tx) - TxProcessor.updateMixin4Doc(doc, tx) - return {} - } -} diff --git a/foundations/core/packages/core/src/objvalue.ts b/foundations/core/packages/core/src/objvalue.ts deleted file mode 100644 index 8a7df8ed7a..0000000000 --- a/foundations/core/packages/core/src/objvalue.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { PlatformError, Severity, Status } from '@hcengineering/platform' -import { type Doc } from './classes' -import { clone } from './clone' -import core from './component' - -/** - * @public - */ -export function getObjectValue (key: string, doc: Doc): any { - // Check dot notation - if (key.length === 0) { - return doc - } - key = key.split('\\$').join('$') - const dots = key.split('.') - // Replace escapting, since memdb is not escape keys - - // We have dots, so iterate in depth - let pos = 0 - let value = doc as any - for (const d of dots) { - if (Array.isArray(value) && isNestedArrayQuery(value, d)) { - // Array and d is not an indexed field. - // So return array of nested values. - return getNestedArrayValue(value, dots.slice(pos).join('.')) - } - value = value?.[d] - pos++ - } - return value -} - -/** - * @public - */ -export function setObjectValue (key: string, doc: Doc, newValue: any): void { - // Check dot notation - if (key.length === 0) { - return - } - key = key.split('\\$').join('$') - let dots = key.split('.') - // Replace escapting, since memdb is not escape keys - - const last = dots[dots.length - 1] - dots = dots.slice(0, -1) - - // We have dots, so iterate in depth - let value = doc as any - for (const d of dots) { - if (Array.isArray(value) && isNestedArrayQuery(value, d)) { - // Arrays are not supported - throw new PlatformError(new Status(Severity.ERROR, core.status.ObjectNotFound, { _id: 'dots' })) - } - const lvalue = value?.[d] - if (lvalue === undefined) { - value[d] = {} - value = value?.[d] - } else { - value = lvalue - } - } - value[last] = clone(newValue) - return value -} - -function isNestedArrayQuery (value: any, d: string): boolean { - return Number.isNaN(Number.parseInt(d)) && value?.[d as any] === undefined -} - -function getNestedArrayValue (value: any[], name: string): any[] { - const result = [] - for (const v of value) { - result.push(...arrayOrValue(getObjectValue(name, v))) - } - return result -} - -function arrayOrValue (vv: any): any[] { - return Array.isArray(vv) ? vv : [vv] -} diff --git a/foundations/core/packages/core/src/operations.ts b/foundations/core/packages/core/src/operations.ts deleted file mode 100644 index 8a182e5514..0000000000 --- a/foundations/core/packages/core/src/operations.ts +++ /dev/null @@ -1,653 +0,0 @@ -import { Analytics } from '@hcengineering/analytics' -import { deepEqual } from 'fast-equals' -import { - type DocumentUpdate, - DOMAIN_MODEL, - Hierarchy, - type MixinData, - type MixinUpdate, - type ModelDb, - platformNow, - toFindResult -} from '.' -import type { - AnyAttribute, - AttachedData, - AttachedDoc, - Class, - Data, - Doc, - Mixin, - OperationDomain, - PersonId, - Ref, - Space, - Timestamp -} from './classes' -import { type Client } from './client' -import core from './component' -import type { - DocumentQuery, - DomainParams, - DomainResult, - FindOptions, - FindResult, - SearchOptions, - SearchQuery, - SearchResult, - TxResult, - WithLookup -} from './storage' -import { type DocumentClassQuery, type Tx, type TxApplyResult, type TxCUD, TxFactory, TxProcessor } from './tx' - -/** - * @public - * - * High Level operations with client, will create low level transactions. - * - * `notify` is not supported by TxOperations. - */ -export class TxOperations implements Omit { - readonly txFactory: TxFactory - - constructor ( - readonly client: Client, - readonly user: PersonId, - readonly isDerived: boolean = false - ) { - this.txFactory = new TxFactory(user, isDerived) - } - - getHierarchy (): Hierarchy { - return this.client.getHierarchy() - } - - getModel (): ModelDb { - return this.client.getModel() - } - - async close (): Promise { - await this.client.close() - } - - findAll( - _class: Ref>, - query: DocumentQuery, - options?: FindOptions | undefined - ): Promise> { - return this.client.findAll(_class, query, options) - } - - findOne( - _class: Ref>, - query: DocumentQuery, - options?: FindOptions | undefined - ): Promise | undefined> { - return this.client.findOne(_class, query, options) - } - - domainRequest(domain: OperationDomain, params: DomainParams): Promise> { - return this.client.domainRequest(domain, params) - } - - searchFulltext (query: SearchQuery, options: SearchOptions): Promise { - return this.client.searchFulltext(query, options) - } - - tx (tx: Tx): Promise { - return this.client.tx(tx) - } - - async createDoc( - _class: Ref>, - space: Ref, - attributes: Data, - id?: Ref, - modifiedOn?: Timestamp, - modifiedBy?: PersonId - ): Promise> { - const hierarchy = this.client.getHierarchy() - if (hierarchy.isDerived(_class, core.class.AttachedDoc)) { - throw new Error('createDoc cannot be used for objects inherited from AttachedDoc') - } - if (hierarchy.findDomain(_class) === DOMAIN_MODEL && space !== core.space.Model) { - throw new Error('createDoc cannot be called for DOMAIN_MODEL classes with non-model space') - } - const tx = this.txFactory.createTxCreateDoc(_class, space, attributes, id, modifiedOn, modifiedBy) - await this.tx(tx) - return tx.objectId - } - - async addCollection( - _class: Ref>, - space: Ref, - attachedTo: Ref, - attachedToClass: Ref>, - collection: Extract | string, - attributes: AttachedData

, - id?: Ref

, - modifiedOn?: Timestamp, - modifiedBy?: PersonId - ): Promise> { - const tx = this.txFactory.createTxCollectionCUD( - attachedToClass, - attachedTo, - space, - collection, - this.txFactory.createTxCreateDoc

(_class, space, attributes as unknown as Data

, id, modifiedOn, modifiedBy), - modifiedOn, - modifiedBy - ) - await this.tx(tx) - return tx.objectId as unknown as Ref

- } - - async updateCollection( - _class: Ref>, - space: Ref, - objectId: Ref

, - attachedTo: Ref, - attachedToClass: Ref>, - collection: Extract | string, - operations: DocumentUpdate

, - retrieve?: boolean, - modifiedOn?: Timestamp, - modifiedBy?: PersonId - ): Promise> { - const tx = this.txFactory.createTxCollectionCUD( - attachedToClass, - attachedTo, - space, - collection, - this.txFactory.createTxUpdateDoc(_class, space, objectId, operations, retrieve, modifiedOn, modifiedBy), - modifiedOn, - modifiedBy - ) - await this.tx(tx) - return attachedTo - } - - async removeCollection( - _class: Ref>, - space: Ref, - objectId: Ref

, - attachedTo: Ref, - attachedToClass: Ref>, - collection: Extract | string, - modifiedOn?: Timestamp, - modifiedBy?: PersonId - ): Promise> { - const tx = this.txFactory.createTxCollectionCUD( - attachedToClass, - attachedTo, - space, - collection, - this.txFactory.createTxRemoveDoc(_class, space, objectId, modifiedOn, modifiedBy), - modifiedOn, - modifiedBy - ) - await this.tx(tx) - return attachedTo - } - - updateDoc( - _class: Ref>, - space: Ref, - objectId: Ref, - operations: DocumentUpdate, - retrieve?: boolean, - modifiedOn?: Timestamp, - modifiedBy?: PersonId - ): Promise { - const tx = this.txFactory.createTxUpdateDoc(_class, space, objectId, operations, retrieve, modifiedOn, modifiedBy) - return this.tx(tx) - } - - removeDoc( - _class: Ref>, - space: Ref, - objectId: Ref, - modifiedOn?: Timestamp, - modifiedBy?: PersonId - ): Promise { - const tx = this.txFactory.createTxRemoveDoc(_class, space, objectId, modifiedOn, modifiedBy) - return this.tx(tx) - } - - createMixin( - objectId: Ref, - objectClass: Ref>, - objectSpace: Ref, - mixin: Ref>, - attributes: MixinData, - modifiedOn?: Timestamp, - modifiedBy?: PersonId - ): Promise { - const tx = this.txFactory.createTxMixin( - objectId, - objectClass, - objectSpace, - mixin, - attributes, - modifiedOn, - modifiedBy - ) - return this.tx(tx) - } - - updateMixin( - objectId: Ref, - objectClass: Ref>, - objectSpace: Ref, - mixin: Ref>, - attributes: MixinUpdate, - modifiedOn?: Timestamp, - modifiedBy?: PersonId - ): Promise { - const tx = this.txFactory.createTxMixin( - objectId, - objectClass, - objectSpace, - mixin, - attributes, - modifiedOn, - modifiedBy - ) - return this.tx(tx) - } - - async update( - doc: T, - update: DocumentUpdate, - retrieve?: boolean, - modifiedOn?: Timestamp, - modifiedBy?: PersonId - ): Promise { - const hierarchy = this.client.getHierarchy() - const mixClass = Hierarchy.mixinOrClass(doc) - if (hierarchy.isMixin(mixClass)) { - const baseClass = hierarchy.getBaseClass(doc._class) - - const byClass = splitMixinUpdate(hierarchy, update, mixClass, baseClass) - const ops = this.apply(doc._id) - for (const it of byClass) { - if (hierarchy.isMixin(it[0])) { - await ops.updateMixin(doc._id, baseClass, doc.space, it[0], it[1], modifiedOn, modifiedBy) - } else { - if (hierarchy.isDerived(it[0], core.class.AttachedDoc)) { - const adoc = doc as unknown as AttachedDoc - return await this.updateCollection( - it[0], - doc.space, - adoc._id, - adoc.attachedTo, - adoc.attachedToClass, - adoc.collection, - it[1], - retrieve, - modifiedOn, - modifiedBy - ) - } - await ops.updateDoc(it[0], doc.space, doc._id, it[1], retrieve, modifiedOn, modifiedBy) - } - } - return await ops.commit() - } - if (hierarchy.isDerived(doc._class, core.class.AttachedDoc)) { - const adoc = doc as unknown as AttachedDoc - return await this.updateCollection( - doc._class, - doc.space, - adoc._id, - adoc.attachedTo, - adoc.attachedToClass, - adoc.collection, - update, - retrieve, - modifiedOn, - modifiedBy - ) - } - return await this.updateDoc(doc._class, doc.space, doc._id, update, retrieve, modifiedOn, modifiedBy) - } - - remove(doc: T, modifiedOn?: Timestamp, modifiedBy?: PersonId): Promise { - if (this.client.getHierarchy().isDerived(doc._class, core.class.AttachedDoc)) { - const adoc = doc as unknown as AttachedDoc - return this.removeCollection( - doc._class, - doc.space, - adoc._id, - adoc.attachedTo, - adoc.attachedToClass, - adoc.collection, - modifiedOn, - modifiedBy - ) - } - return this.removeDoc(doc._class, doc.space, doc._id) - } - - apply (scope?: string, measure?: string, derived?: boolean): ApplyOperations { - return new ApplyOperations(this, scope, measure, derived ?? this.isDerived) - } - - async diffUpdate( - doc: T, - update: T | Data | DocumentUpdate, - date?: Timestamp, - account?: PersonId - ): Promise { - const documentUpdate = getDiffUpdate(doc, update) - if (Object.keys(documentUpdate).length > 0) { - await this.update(doc, documentUpdate, false, date ?? Date.now(), account) - TxProcessor.applyUpdate(doc, documentUpdate) - } - return doc - } - - async mixinDiffUpdate ( - doc: Doc, - raw: Doc | Data, - mixin: Ref>>, - modifiedBy: PersonId, - modifiedOn: Timestamp - ): Promise { - // We need to update fields if they are different. - - if (!this.getHierarchy().hasMixin(doc, mixin)) { - await this.createMixin(doc._id, doc._class, doc.space, mixin, raw as MixinData, modifiedOn, modifiedBy) - TxProcessor.applyUpdate(this.getHierarchy().as(doc, mixin), raw) - return doc - } - - const documentUpdate: MixinUpdate = {} - for (const [k, v] of Object.entries(raw)) { - if (['_class', '_id', 'modifiedBy', 'modifiedOn', 'space', 'attachedTo', 'attachedToClass'].includes(k)) { - continue - } - const dv = (doc as any)[k] - if (!deepEqual(dv, v) && v != null) { - ;(documentUpdate as any)[k] = v - } - } - if (Object.keys(documentUpdate).length > 0) { - await this.updateMixin(doc._id, doc._class, doc.space, mixin, documentUpdate, modifiedOn, modifiedBy) - TxProcessor.applyUpdate(this.getHierarchy().as(doc, mixin), documentUpdate) - } - return doc - } -} - -export function getDiffUpdate (doc: T, update: T | Data | DocumentUpdate): DocumentUpdate { - // We need to update fields if they are different. - const documentUpdate: DocumentUpdate = {} - for (const [k, v] of Object.entries(update)) { - if (['_class', '_id', 'modifiedBy', 'modifiedOn', 'space', 'attachedTo', 'attachedToClass'].includes(k)) { - continue - } - const dv = (doc as any)[k] - if (!deepEqual(dv, v) && v !== undefined) { - ;(documentUpdate as any)[k] = v - } - } - return documentUpdate -} - -export function splitMixinUpdate ( - hierarchy: Hierarchy, - update: DocumentUpdate, - mixClass: Ref>, - baseClass: Ref> -): Map>, DocumentUpdate> { - const attributes = hierarchy.getAllAttributes(mixClass) - - const updateAttrs = Object.fromEntries( - Object.entries(update).filter((it) => !it[0].startsWith('$')) - ) as DocumentUpdate - const updateOps = Object.fromEntries( - Object.entries(update).filter((it) => it[0].startsWith('$')) - ) as DocumentUpdate - - const result: Map>, DocumentUpdate> = splitObjectAttributes( - hierarchy, - updateAttrs, - baseClass, - attributes - ) - - for (const [key, value] of Object.entries(updateOps)) { - const updates = splitObjectAttributes(hierarchy, value as object, baseClass, attributes) - - for (const [opsClass, opsUpdate] of updates) { - const upd: DocumentUpdate = result.get(opsClass) ?? {} - result.set(opsClass, { ...upd, [key]: opsUpdate }) - } - } - - return result -} - -function splitObjectAttributes ( - hierarchy: Hierarchy, - obj: T, - objClass: Ref>, - attributes: Map -): Map>, object> { - const result = new Map>, any>() - for (const [key, value] of Object.entries(obj)) { - const attributeOf = attributes.get(key)?.attributeOf - const clazz = attributeOf !== undefined && hierarchy.isMixin(attributeOf) ? attributeOf : objClass - result.set(clazz, { ...(result.get(clazz) ?? {}), [key]: value }) - } - - return result -} - -export interface CommitResult { - result: boolean - time: number - serverTime: number -} - -/** - * @public - * - * Builder for ApplyOperation, with same syntax as TxOperations. - * - * Will send real command on commit and will return boolean of operation success. - */ -export class ApplyOperations extends TxOperations { - txes: TxCUD[] = [] - matches: DocumentClassQuery[] = [] - notMatches: DocumentClassQuery[] = [] - constructor ( - readonly ops: TxOperations, - readonly scope?: string, - readonly measureName?: string, - isDerived?: boolean - ) { - const txClient: Client = { - getHierarchy: () => ops.client.getHierarchy(), - getModel: () => ops.client.getModel(), - close: () => ops.client.close(), - findOne: (_class, query, options?) => ops.client.findOne(_class, query, options), - findAll: (_class, query, options?) => ops.client.findAll(_class, query, options), - searchFulltext: (query, options) => ops.client.searchFulltext(query, options), - domainRequest: (domain, params) => ops.client.domainRequest(domain, params), - tx: async (tx): Promise => { - if (TxProcessor.isExtendsCUD(tx._class)) { - this.txes.push(tx as TxCUD) - } - return {} - } - } - super(txClient, ops.user, isDerived ?? false) - } - - match(_class: Ref>, query: DocumentQuery): ApplyOperations { - this.matches.push({ _class, query }) - return this - } - - notMatch(_class: Ref>, query: DocumentQuery): ApplyOperations { - this.notMatches.push({ _class, query }) - return this - } - - async commit (notify: boolean = true, extraNotify: Ref>[] = []): Promise { - if ( - this.txes.length === 1 && - this.matches.length === 0 && - this.notMatches.length === 0 && - this.measureName == null - ) { - const st = platformNow() - // Individual update, no need for apply - await this.ops.tx(this.txes[0]) - const time = platformNow() - st - this.txes = [] - return { - result: true, - time, - serverTime: time - } - } - if (this.txes.length > 0) { - const st = platformNow() - const aop = this.ops.txFactory.createTxApplyIf( - core.space.Tx, - this.scope, - this.matches, - this.notMatches, - this.txes, - this.measureName, - notify, - extraNotify - ) - const result = (await this.ops.tx(aop)) as TxApplyResult - const dnow = platformNow() - if (typeof window === 'object' && window !== null && this.measureName != null) { - console.log(`measure ${this.measureName}`, dnow - st, 'server time', result.serverTime) - } - this.txes = [] - return { - result: result.success, - time: dnow - st, - serverTime: result.serverTime - } - } - return { result: true, time: 0, serverTime: 0 } - } - - // Apply for this will reuse, same apply context. - apply (scope?: string, measure?: string): ApplyOperations { - return this - } -} - -/** - * @public - * - * Builder for TxOperations. - */ -export class TxBuilder extends TxOperations { - txes: TxCUD[] = [] - matches: DocumentClassQuery[] = [] - constructor ( - readonly hierarchy: Hierarchy, - readonly modelDb: ModelDb, - user: PersonId - ) { - const txClient: Client = { - getHierarchy: () => this.hierarchy, - getModel: () => this.modelDb, - close: async () => {}, - findOne: async (_class, query, options?) => undefined, - findAll: async (_class, query, options?) => toFindResult([]), - searchFulltext: async (query, options) => ({ docs: [] }), - domainRequest: async (domain, params) => ({ domain, value: null as any }), - tx: async (tx): Promise => { - if (TxProcessor.isExtendsCUD(tx._class)) { - this.txes.push(tx as TxCUD) - } - return {} - } - } - super(txClient, user) - } -} - -/** - * @public - */ -export async function updateAttribute ( - client: TxOperations, - object: Doc, - _class: Ref>, - attribute: { key: string, attr: AnyAttribute }, - value: any, - saveModified: boolean = false, - analyticsProps: Record = {} -): Promise { - const doc = object - const attributeKey = attribute.key - if ((doc as any)[attributeKey] === value) return - const modifiedOn = saveModified ? doc.modifiedOn : Date.now() - const modifiedBy = attribute.key === 'modifiedBy' ? value : saveModified ? doc.modifiedBy : undefined - const attr = attribute.attr - - const baseAnalyticsProps = { - objectClass: _class, - objectId: object._id, - attribute: attributeKey, - ...analyticsProps - } - if (client.getHierarchy().isMixin(attr.attributeOf)) { - await client.updateMixin( - doc._id, - _class, - doc.space, - attr.attributeOf, - { [attributeKey]: value }, - modifiedOn, - modifiedBy - ) - Analytics.handleEvent('ChangeAttribute', { ...baseAnalyticsProps, value }) - } else { - if (client.getHierarchy().isDerived(attribute.attr.type._class, core.class.ArrOf)) { - const oldValue: any[] = (object as any)[attributeKey] ?? [] - const val: any[] = Array.isArray(value) ? value : [value] - const toPull = oldValue.filter((it: any) => !val.includes(it)) - - const toPush = val.filter((it) => !oldValue.includes(it)) - if (toPull.length > 0) { - await client.update(object, { $pull: { [attributeKey]: { $in: toPull } } }, false, modifiedOn, modifiedBy) - Analytics.handleEvent('RemoveCollectionItems', { - ...baseAnalyticsProps, - removed: toPull - }) - } - if (toPush.length > 0) { - await client.update( - object, - { $push: { [attributeKey]: { $each: toPush, $position: 0 } } }, - false, - modifiedOn, - modifiedBy - ) - Analytics.handleEvent('AddCollectionItems', { - ...baseAnalyticsProps, - added: toPush - }) - } - } else { - await client.update(object, { [attributeKey]: value }, false, modifiedOn, modifiedBy) - Analytics.handleEvent('SetCollectionItems', { - ...baseAnalyticsProps, - value - }) - } - } -} diff --git a/foundations/core/packages/core/src/operator.ts b/foundations/core/packages/core/src/operator.ts deleted file mode 100644 index ad3eab61b0..0000000000 --- a/foundations/core/packages/core/src/operator.ts +++ /dev/null @@ -1,215 +0,0 @@ -// -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// Copyright © 2021 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { Analytics } from '@hcengineering/analytics' -import type { Doc, PropertyType } from './classes' -import type { Position, PullArray, QueryUpdate } from './tx' - -/** - * @internal - */ -export type _OperatorFunc = (doc: Doc, op: any) => void - -function $push (document: Doc, keyval: Record): void { - const doc = document as any - for (const key in keyval) { - if (doc[key] === undefined) { - doc[key] = [] - } - const kvk = keyval[key] - if (typeof kvk === 'object' && kvk != null) { - const arr = doc[key] as Array - const desc = kvk as Position - if ('$each' in desc) { - if (arr != null && Array.isArray(arr)) { - arr.splice(desc.$position ?? 0, 0, ...desc.$each) - } - } else { - arr.push(kvk) - } - } else { - if (doc[key] === null || doc[key] === undefined) { - doc[key] = [kvk] - } else { - if (Array.isArray(doc[key])) { - doc[key].push(kvk) - } else { - Analytics.handleError(new Error(`invalid array value: ${JSON.stringify(doc[key])} `)) - doc[key] = [kvk] - } - } - } - } -} - -function $pull (document: Doc, keyval: Record): void { - const doc = document as any - for (const key in keyval) { - if (doc[key] === undefined) { - doc[key] = [] - } - // Ensure doc[key] is an array before attempting to filter - if (!Array.isArray(doc[key])) { - Analytics.handleError(new Error(`$pull operation on non-array field: ${key}, value: ${JSON.stringify(doc[key])}`)) - doc[key] = [] - continue - } - const arr = doc[key] - const kvk = keyval[key] - if (typeof kvk === 'object' && kvk !== null) { - const { $in } = kvk as PullArray - - doc[key] = (arr ?? []).filter((val) => { - if ($in !== undefined) { - return !$in.includes(val) - } else { - // We need to match all fields - for (const [kk, kv] of Object.entries(kvk)) { - if (val[kk] !== kv) { - return true - } - } - return false - } - }) - } else { - doc[key] = (arr ?? []).filter((val) => val !== kvk) - } - } -} - -function matchArrayElement (docs: any[], query: Partial): any[] { - let result = [...docs] - for (const key in query) { - const value = (query as any)[key] - - const tresult: any[] = [] - for (const object of result) { - const val = object[key] - if (val === value) { - tresult.push(object) - } - } - result = tresult - if (tresult.length === 0) { - break - } - } - return result -} - -function $update (document: Doc, keyval: Record): void { - const doc = document as any - for (const key in keyval) { - if (doc[key] === undefined) { - doc[key] = [] - } - // Ensure doc[key] is an array before attempting to update - if (!Array.isArray(doc[key])) { - Analytics.handleError( - new Error(`$update operation on non-array field: ${key}, value: ${JSON.stringify(doc[key])}`) - ) - doc[key] = [] - continue - } - const val = keyval[key] - if (typeof val === 'object') { - const arr = doc[key] - const desc = val as QueryUpdate - for (const m of matchArrayElement(arr, desc.$query)) { - for (const [k, v] of Object.entries(desc.$update)) { - m[k] = v - } - } - } - } -} - -function $inc (document: Doc, keyval: Record): void { - const doc = document as unknown as Record - for (const key in keyval) { - const cur = doc[key] ?? 0 - // Ensure current value is a number - if (typeof cur !== 'number' || isNaN(cur)) { - Analytics.handleError( - new Error(`$inc operation on non-numeric field: ${key}, value: ${JSON.stringify(doc[key])}`) - ) - doc[key] = keyval[key] - continue - } - const increment = keyval[key] - // Ensure increment value is a valid number - if (typeof increment !== 'number' || isNaN(increment)) { - Analytics.handleError( - new Error(`$inc operation with invalid increment: ${key}, increment: ${JSON.stringify(increment)}`) - ) - continue - } - doc[key] = cur + increment - } -} - -function $unset (document: Doc, keyval: Record): void { - const doc = document as any - for (const key in keyval) { - if (doc[key] !== undefined) { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete doc[key] - } - } -} - -function $rename (document: Doc, keyval: Record): void { - const doc = document as any - for (const key in keyval) { - if (doc[key] !== undefined) { - doc[keyval[key]] = doc[key] - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete doc[key] - } - } -} - -const operators: Record = { - $push, - $pull, - $update, - $inc, - $unset, - $rename -} - -/** - * @public - */ -export function isOperator (o: Record): boolean { - if (o === null || typeof o !== 'object') { - return false - } - const keys = Object.keys(o) - return keys.length > 0 && keys.every((key) => key.startsWith('$')) -} - -/** - * @internal - * @param name - - * @returns - */ -export function _getOperator (name: string): _OperatorFunc { - const operator = operators[name] - if (operator === undefined) throw new Error('unknown operator: ' + name) - return operator -} diff --git a/foundations/core/packages/core/src/predicate.ts b/foundations/core/packages/core/src/predicate.ts deleted file mode 100644 index 4fbdc47dd1..0000000000 --- a/foundations/core/packages/core/src/predicate.ts +++ /dev/null @@ -1,157 +0,0 @@ -// -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// Copyright © 2021 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { Doc } from './classes' -import { getObjectValue } from './objvalue' -import { escapeLikeForRegexp } from './utils' - -import { deepEqual } from 'fast-equals' - -type Predicate = (docs: Doc[]) => Doc[] -type PredicateFactory = (pred: any, propertyKey: string) => Predicate - -type ExecPredicate = (value: any) => boolean - -function execPredicate (docs: Doc[], propertyKey: string, pred: ExecPredicate): Doc[] { - const result: Doc[] = [] - for (const doc of docs) { - const value = getObjectValue(propertyKey, doc) - if (pred(value)) { - result.push(doc) - } - } - return result -} - -const predicates: Record = { - $in: (o, propertyKey) => { - if (!Array.isArray(o)) { - throw new Error('$in predicate requires array') - } - return (docs) => - execPredicate(docs, propertyKey, (value) => { - if (Array.isArray(value)) { - return o.some((p) => value.includes(p)) - } else { - // eslint-disable-next-line eqeqeq - return o.some((p) => p == value) - } - }) - }, - $all: (o, propertyKey) => { - if (!Array.isArray(o)) { - throw new Error('$all predicate requires array') - } - return (docs) => - execPredicate(docs, propertyKey, (value: any[]) => { - for (const val of o) { - if (!value.includes(val)) return false - } - return true - }) - }, - $nin: (o, propertyKey) => { - if (!Array.isArray(o)) { - throw new Error('$nin predicate requires array') - } - return (docs) => - execPredicate(docs, propertyKey, (value) => { - if (Array.isArray(value)) { - return !o.some((p) => value.includes(p)) - } else { - // eslint-disable-next-line eqeqeq - return !o.some((p) => p == value) - } - }) - }, - - $like: (query: string, propertyKey: string): Predicate => { - const searchString = query - .split('%') - .map((it) => escapeLikeForRegexp(it)) - .join('.*') - const regex = RegExp(`^${searchString}$`, 'i') - - return (docs) => execPredicate(docs, propertyKey, (value) => regex.test(value)) - }, - - $regex: (o: { $regex: string, $options: string }, propertyKey: string): Predicate => { - const re = new RegExp(o.$regex, o.$options) - return (docs) => execPredicate(docs, propertyKey, (value) => value.match(re) !== null) - }, - $gt: (o, propertyKey) => { - return (docs) => execPredicate(docs, propertyKey, (value) => value > o) - }, - $gte: (o, propertyKey) => { - return (docs) => execPredicate(docs, propertyKey, (value) => value >= o) - }, - $lt: (o, propertyKey) => { - return (docs) => execPredicate(docs, propertyKey, (value) => value < o) - }, - $lte: (o, propertyKey) => { - return (docs) => execPredicate(docs, propertyKey, (value) => value <= o) - }, - $exists: (o, propertyKey) => { - return (docs) => execPredicate(docs, propertyKey, (value) => (value !== undefined) === o) - }, - $ne: (o, propertyKey) => { - // eslint-disable-next-line eqeqeq - return (docs) => execPredicate(docs, propertyKey, (value) => (o != null ? !deepEqual(o, value) : value != null)) - }, - $size: (o, propertyKey) => { - return (docs) => - execPredicate(docs, propertyKey, (value) => { - if (!Array.isArray(value)) { - throw new Error('$size predicate requires array') - } - if (typeof o === 'number') { - return value.length === o - } - if (typeof o === 'object' && o.$gt !== undefined) { - return value.length > o.$gt - } - if (typeof o === 'object' && o.$gte !== undefined) { - return value.length >= o.$gte - } - if (typeof o === 'object' && o.$lt !== undefined) { - return value.length < o.$lt - } - if (typeof o === 'object' && o.$lte !== undefined) { - return value.length <= o.$lte - } - return false - }) - } -} - -export function isPredicate (o: Record): boolean { - if (o === null || typeof o !== 'object') { - return false - } - const keys = Object.keys(o) - return keys.length > 0 && keys.every((key) => key.startsWith('$')) -} - -export function createPredicates (o: Record, propertyKey: string): Predicate[] { - const keys = Object.keys(o) - const result: Predicate[] = [] - for (const key of keys) { - const factory = predicates[key] - if (factory === undefined) throw new Error('unknown predicate: ' + keys[0]) - result.push(factory(o[key], propertyKey)) - } - return result -} diff --git a/foundations/core/packages/core/src/proxy.ts b/foundations/core/packages/core/src/proxy.ts deleted file mode 100644 index d5ab1abde6..0000000000 --- a/foundations/core/packages/core/src/proxy.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { PlatformError, unknownError } from '@hcengineering/platform' -import { type Ref } from '.' -import type { Doc, Mixin } from './classes' - -const PROXY_TARGET_KEY = '$___proxy_target' -export const PROXY_MIXIN_CLASS_KEY = '$__mixin' - -/** - * @internal - */ -export function _createMixinProxy (mixin: Mixin, ancestorProxy: ProxyHandler | null): ProxyHandler { - return { - get (target: any, property: string, receiver: any): any { - if (property === PROXY_TARGET_KEY) { - return target - } - // We need to override _class property, to return proper mixin class. - if (property === PROXY_MIXIN_CLASS_KEY) { - return mixin._id - } - const value = target[mixin._id]?.[property] - if (value === undefined) { - return ancestorProxy !== null ? ancestorProxy.get?.(target, property, receiver) : target[property] - } - return value - } - } -} - -export function freeze (value: any): any { - if (value != null && typeof value === 'object') { - if (Array.isArray(value)) { - return value.map((it) => freeze(it)) - } - if (value instanceof Map) { - throw new PlatformError(unknownError('Map is not allowed in model')) - } - if (value instanceof Set) { - throw new PlatformError(unknownError('Set is not allowed in model')) - } - return new Proxy(value, _createFreezeProxy(value)) - } - return value -} -/** - * @internal - */ -export function _createFreezeProxy (doc: Doc): ProxyHandler { - return { - get (target: any, property: string, receiver: any): any { - const value = target[property] - return freeze(value) - }, - set (target, p, newValue, receiver): any { - throw new PlatformError(unknownError('Modification is not allowed')) - }, - defineProperty (target, property, attributes): any { - throw new PlatformError(unknownError('Modification is not allowed')) - }, - - deleteProperty (target, p): any { - throw new PlatformError(unknownError('Modification is not allowed')) - }, - setPrototypeOf (target, v): any { - throw new PlatformError(unknownError('Modification is not allowed')) - } - } -} - -/** - * @internal - */ -export function _toDoc (doc: D): D { - const targetDoc = (doc as any)[PROXY_TARGET_KEY] - if (targetDoc !== undefined) { - return targetDoc as D - } - return doc -} - -/** - * @internal - */ -export function _mixinClass (doc: D): Ref> | undefined { - return (doc as any)[PROXY_MIXIN_CLASS_KEY] -} diff --git a/foundations/core/packages/core/src/query.ts b/foundations/core/packages/core/src/query.ts deleted file mode 100644 index 81f20d9a97..0000000000 --- a/foundations/core/packages/core/src/query.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { type DocumentQuery, type MemDb } from '.' -import { type Class, type Doc, type Enum, type EnumOf, type Ref } from './classes' -import core from './component' -import { type Hierarchy } from './hierarchy' -import { getObjectValue } from './objvalue' -import { createPredicates, isPredicate } from './predicate' -import { type SortQuerySelector, type SortingOrder, type SortingQuery, type SortingRules } from './storage' - -/** - * @public - */ -export function findProperty (objects: Doc[], propertyKey: string, value: any): Doc[] { - if (isPredicate(value)) { - const preds = createPredicates(value, propertyKey) - for (const pred of preds) { - objects = pred(objects) - } - return objects - } - const result: Doc[] = [] - for (const object of objects) { - const val = getObjectValue(propertyKey, object) - if (val === value || (val == null && value == null) || isArrayValueCheck(val, value)) { - result.push(object) - } - } - return result -} - -function isArrayValueCheck (val: T, value: P): boolean { - return Array.isArray(val) && !Array.isArray(value) && val.includes(value) -} - -function getEnumValue ( - key: string, - _class: Ref>, - hierarchy: Hierarchy, - obj: any, - _enum: Enum -): number { - const tkey = checkMixinKey(key, _class, hierarchy) - const value = getObjectValue(tkey, obj) - const index = _enum.enumValues.findIndex((p) => p === value) - return index === -1 ? _enum.enumValues.length : index -} - -/** - * @public - */ -export function resultSort ( - result: T[], - sortOptions: SortingQuery, - _class: Ref>, - hierarchy: Hierarchy, - modelDb: MemDb -): void { - const enums = getEnums(_class, sortOptions, hierarchy, modelDb) - const sortFunc = (a: any, b: any): number => { - for (const key in sortOptions) { - const _enum = enums[key] - const aValue = - _enum !== undefined ? getEnumValue(key, _class, hierarchy, a, _enum) : getValue(key, a, _class, hierarchy) - const bValue = - _enum !== undefined ? getEnumValue(key, _class, hierarchy, b, _enum) : getValue(key, b, _class, hierarchy) - const result = getSortingResult(aValue, bValue, sortOptions[key]) - if (result !== 0) return result - } - return 0 - } - result.sort(sortFunc) -} - -function mapSortingValue (order: SortingOrder | SortingRules, val: any): any { - if (typeof order !== 'object') { - return val - } - for (const r of order.cases) { - if (typeof r.query === 'object') { - const q: SortQuerySelector = r.query - if (q.$in?.includes(val) ?? false) { - return r.index - } - if (q.$nin !== undefined && !q.$nin.includes(val)) { - return r.index - } - if (q.$ne !== undefined && q.$ne !== val) { - return r.index - } - } - if (r.query === val) { - return r.index - } - } -} - -function getSortingResult (aValue: any, bValue: any, order: SortingOrder | SortingRules): number { - let res = 0 - if (typeof aValue === 'undefined') { - return typeof bValue === 'undefined' ? 0 : -1 - } - if (typeof bValue === 'undefined') { - return 1 - } - - const orderOrder = typeof order === 'object' ? order.order : order - - if (Array.isArray(aValue) && Array.isArray(bValue)) { - res = - (aValue.map((it) => mapSortingValue(order, it)).sort((a, b) => (a - b) * orderOrder)[0] ?? 0) - - (bValue.map((it) => mapSortingValue(order, it)).sort((a, b) => (a - b) * orderOrder)[0] ?? 0) - } else { - const aaValue = mapSortingValue(order, aValue) - const bbValue = mapSortingValue(order, bValue) - res = typeof aaValue === 'string' ? aaValue.localeCompare(bbValue) : aaValue - bbValue - } - return res * orderOrder -} - -function getEnums ( - _class: Ref>, - sortOptions: SortingQuery, - hierarchy: Hierarchy, - modelDb: MemDb -): Record { - const res: Record = {} - for (const key in sortOptions) { - const attr = hierarchy.findAttribute(_class, key) - if (attr !== undefined) { - if (attr !== undefined) { - if (attr.type._class === core.class.EnumOf) { - const ref = (attr.type as EnumOf).of - const enu = modelDb.findAllSync(core.class.Enum, { _id: ref }) - res[key] = enu[0] - } - } - } - } - return res -} - -function getValue (key: string, obj: any, _class: Ref>, hierarchy: Hierarchy): any { - const tkey = checkMixinKey(key, _class, hierarchy) - let value = getObjectValue(tkey, obj) - if (typeof value === 'object' && !Array.isArray(value)) { - value = JSON.stringify(value) - } - return value -} -/** - * @public - */ -export function matchQuery ( - docs: Doc[], - query: DocumentQuery, - clazz: Ref>, - hierarchy: Hierarchy, - skipLookup: boolean = false -): Doc[] { - const baseClass = hierarchy.getBaseClass(clazz) - let result = docs.filter((r) => hierarchy.isDerived(r._class, baseClass)) - if (baseClass !== clazz) { - result = docs.filter((r) => hierarchy.hasMixin(r, clazz)) - } - for (const key in query) { - if (skipLookup && key.startsWith('$lookup.')) { - continue - } - const value = (query as any)[key] - const tkey = checkMixinKey(key, clazz, hierarchy) - result = findProperty(result, tkey, value) - if (result.length === 0) { - break - } - } - return result -} - -/** - * @public - */ -export function checkMixinKey (key: string, clazz: Ref>, hierarchy: Hierarchy): string { - if (!key.includes('.')) { - try { - const attr = hierarchy.findAttribute(clazz, key) - if (attr !== undefined && hierarchy.isMixin(attr.attributeOf)) { - // It is mixin - key = attr.attributeOf + '.' + key - } - } catch (err: any) { - // ignore, if - } - } - return key -} diff --git a/foundations/core/packages/core/src/server.ts b/foundations/core/packages/core/src/server.ts deleted file mode 100644 index a1515008ed..0000000000 --- a/foundations/core/packages/core/src/server.ts +++ /dev/null @@ -1,137 +0,0 @@ -// -// Copyright © 2022 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { Account, AccountRole, AccountUuid, Doc, Domain, PersonId, Ref } from './classes' -import { type MeasureContext } from '@hcengineering/measurements' -import { type DocumentQuery, type FindOptions } from './storage' -import type { DocumentUpdate, Tx } from './tx' -import { PermissionsGrant, type WorkspaceIds } from './utils' - -/** - * @public - */ -export interface DocInfo { - id: string - hash: string - - size?: number - - contentType?: string -} -/** - * @public - */ -export interface StorageIterator { - next: (ctx: MeasureContext) => Promise - close: (ctx: MeasureContext) => Promise -} - -export interface BroadcastTargetResult { - target: AccountUuid[] -} - -export interface BroadcastExcludeResult { - exclude: AccountUuid[] -} - -export type BroadcastResult = BroadcastTargetResult | BroadcastExcludeResult | undefined -export type BroadcastTargets = Record Promise> - -export interface SessionData { - broadcast: { - txes: Tx[] - targets: BroadcastTargets // A set of broadcast filters if required - queue: Tx[] // Queue only broadcast - sessions: Record // Session based broadcast - } - contextCache: Map - removedMap: Map, Doc> - account: Account - service: string - sessionId: string - admin?: boolean - isTriggerCtx?: boolean - hasDomainBroadcast?: boolean - workspace: WorkspaceIds - socialStringsToUsers: Map< - PersonId, - { - accontUuid: AccountUuid - role: AccountRole - } - > - grant?: PermissionsGrant - - asyncRequests?: ((ctx: MeasureContext, id?: string) => Promise)[] -} - -/** - * @public - */ -export interface LowLevelStorage { - // Low level streaming API to retrieve information - find: (ctx: MeasureContext, domain: Domain) => StorageIterator - - // Load passed documents from domain - load: (ctx: MeasureContext, domain: Domain, docs: Ref[]) => Promise - - // Upload new versions of documents - // docs - new/updated version of documents. - upload: (ctx: MeasureContext, domain: Domain, docs: Doc[]) => Promise - - // Remove a list of documents. - clean: (ctx: MeasureContext, domain: Domain, docs: Ref[]) => Promise - - // Low level direct group API - groupBy: ( - ctx: MeasureContext, - domain: Domain, - field: string, - query?: DocumentQuery

- ) => Promise> - - // migrations - rawFindAll: (domain: Domain, query: DocumentQuery, options?: FindOptions) => Promise - - rawUpdate: (domain: Domain, query: DocumentQuery, operations: DocumentUpdate) => Promise - - rawDeleteMany: (domain: Domain, query: DocumentQuery) => Promise - - // Traverse documents - traverse: ( - domain: Domain, - query: DocumentQuery, - options?: Pick, 'sort' | 'limit' | 'projection'> - ) => Promise> - - getDomainHash: (ctx: MeasureContext, domain: Domain) => Promise -} - -export interface Iterator { - next: (count: number) => Promise - close: () => Promise -} - -export interface Branding { - key?: string - front?: string - title?: string - language?: string - initWorkspace?: string - lastNameFirst?: string - protocol?: string -} - -export type BrandingMap = Record diff --git a/foundations/core/packages/core/src/status.ts b/foundations/core/packages/core/src/status.ts deleted file mode 100644 index 94937e1f6f..0000000000 --- a/foundations/core/packages/core/src/status.ts +++ /dev/null @@ -1,53 +0,0 @@ -// -// Copyright © 2023 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { type Asset, type IntlString } from '@hcengineering/platform' -import { type Attribute, type Doc, type Domain, type Ref } from './classes' - -/** - * @public - */ -export interface StatusCategory extends Doc { - ofAttribute: Ref> - icon: Asset - label: IntlString - color: number | number[] - defaultStatusName: string - order: number // category order -} -/** - * @public - */ -export const DOMAIN_STATUS = 'status' as Domain - -/** - * @public - * - * Status is attached to attribute, and if user attribute will be removed, all status values will be remove as well. - */ -export interface Status extends Doc { - // We attach to attribute, so we could distinguish between - ofAttribute: Ref> - // Optional category. - category?: Ref - - // Status with case insensitivity name match will be assumed same. - name: string - - // Optional color - color?: number | number[] - // Optional description - description?: string -} diff --git a/foundations/core/packages/core/src/storage.ts b/foundations/core/packages/core/src/storage.ts deleted file mode 100644 index a97a84b076..0000000000 --- a/foundations/core/packages/core/src/storage.ts +++ /dev/null @@ -1,317 +0,0 @@ -// -// Copyright © 2021 Anticrm Platform Contributors. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { Asset, Resource } from '@hcengineering/platform' - -import type { Association, AttachedDoc, Class, Doc, Domain, Ref, Space } from './classes' -import type { Tx } from './tx' -import type { KeysByType } from './utils' - -export type ArraySizeSelector = - | { - $gt: number - } - | { - $lt: number - } - | { - $gte: number - } - | { - $lte: number - } - -/** - * @public - */ -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -export type QuerySelector = { - $in?: T[] - $all?: T extends Array ? T : never - $nin?: T[] - $ne?: T - $gt?: T extends number ? number : never - $gte?: T extends number ? number : never - $lt?: T extends number ? number : never - $lte?: T extends number ? number : never - $exists?: boolean - $like?: string - $regex?: string - $options?: string - $size?: T extends Array ? number | ArraySizeSelector : never -} - -/** - * @public - */ -export type ObjQueryType = (T extends Array ? U | U[] | QuerySelector : T) | QuerySelector - -/** - * @public - */ -export type DocumentQuery = { - [P in keyof T]?: ObjQueryType -} & { - $search?: string - // support nested queries e.g. 'user.friends.name' - // this will mark all unrecognized properties as any (including nested queries) - [key: string]: any -} - -/** - * @public - */ -export type ToClassRefT = T[P] extends Ref | null | undefined - ? Ref> | [Ref>, Lookup] - : never -/** - * @public - */ -export type ToClassRefTA = T[P] extends Array> | null | undefined - ? Ref> | [Ref>, Lookup] - : never -/** - * @public - */ -export type ToClassRef = { - [P in keyof T]?: ToClassRefT | ToClassRefTA -} - -/** - * @public - */ -export type NullableRef = Ref | Array> | null | undefined - -/** - * @public - */ -export type RefKeys = Pick> - -/** - * @public - */ -export type Refs = ToClassRef> - -/** - * @public - */ -export interface ReverseLookups { - _id?: ReverseLookup -} - -/** - * @public - */ -export type ReverseLookup = Record> | [Ref>, string]> - -/** - * @public - */ -export type Lookup = Refs | ReverseLookups | (Refs & ReverseLookups) - -/** - * @public - */ -export type Projection = { - [P in keyof T]?: 0 | 1 -} - -export type AssociationQuery = [Ref, 1 | -1] - -/** - * @public - */ -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -export type FindOptions = { - limit?: number - sort?: SortingQuery - lookup?: Lookup - projection?: Projection - associations?: AssociationQuery[] - - // If specified total will be returned - total?: boolean - - showArchived?: boolean -} - -/** - * @public - */ -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -export type SortQuerySelector = { - $in?: T[] - $nin?: T[] - $ne?: T -} -/** - * @public - */ -export type SortRuleQueryType = (T extends Array ? U | U[] : T) | SortQuerySelector - -/** - * @public - */ -export interface SortingRules { - order: SortingOrder - default?: string | number - cases: { - query: SortRuleQueryType - index: string | number - }[] -} - -/** - * @public - */ -export type SortingQuery = { - [P in keyof T]?: SortingOrder | SortingRules -} & Record> - -/** - * @public - */ -export enum SortingOrder { - Ascending = 1, - Descending = -1 -} - -/** - * @public - */ -export type RefsAsDocs = { - [P in keyof T]: T[P] extends Ref | null | undefined ? (T extends X ? X : X | WithLookup) : AttachedDoc[] -} - -/** - * @public - */ -export type LookupData = Partial> - -/** - * @public - */ -export type WithLookup = T & { - $lookup?: LookupData - $associations?: Record - $source?: { - $score: number // Score for document result - [key: string]: any - } -} - -/** - * @public - */ -export type FindResult = WithLookup[] & { - total: number - lookupMap?: Record -} - -export type DomainParams = Record - -export interface DomainResult { - domain: Domain - value: T -} - -/** - * @public - */ -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface TxResult {} - -/** - * @public - */ -export interface SearchQuery { - query: string - classes?: Ref>[] - spaces?: Ref[] -} - -/** - * @public - */ -export interface SearchOptions { - limit?: number -} - -export interface SearchComponentWithProps { - component?: Resource - props?: Record -} - -/** - * @public - */ -export interface SearchResultDoc { - id: Ref - - icon?: Asset - iconComponent?: SearchComponentWithProps - shortTitle?: string - shortTitleComponent?: SearchComponentWithProps - title?: string - titleComponent?: SearchComponentWithProps - description?: string - emojiIcon?: string - score?: number - doc: Pick & Partial> -} - -/** - * @public - */ -export interface SearchResult { - docs: SearchResultDoc[] - total?: number -} - -/** - * @public - */ -export interface Storage { - findAll: ( - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ) => Promise> - - tx: (tx: Tx) => Promise -} - -/** - * @public - */ -export interface FulltextStorage { - searchFulltext: (query: SearchQuery, options: SearchOptions) => Promise -} - -export function shouldShowArchived ( - query: DocumentQuery, - options: FindOptions | undefined -): boolean { - if (options?.showArchived !== undefined) { - return options.showArchived - } - if (query._id !== undefined && typeof query._id === 'string') { - return true - } - if (query.space !== undefined && typeof query.space === 'string') { - return true - } - return false -} diff --git a/foundations/core/packages/core/src/time.ts b/foundations/core/packages/core/src/time.ts deleted file mode 100644 index 52569d8eca..0000000000 --- a/foundations/core/packages/core/src/time.ts +++ /dev/null @@ -1,68 +0,0 @@ -// -// Copyright © 2023 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// -import { type Timestamp } from './classes' - -export function getDay (time: Timestamp): Timestamp { - const date: Date = new Date(time) - return convertToDay(date).getTime() -} - -export function convertToDay (date: Date): Date { - const originalDay: number = date.getDate() - const convertedDate: Date = new Date(date) - // Set 12 AM UTC time, since it will be the same day in most timezones - convertedDate.setUTCHours(12, 0, 0, 0) - if (convertedDate.getDate() !== originalDay) { - convertedDate.setDate(originalDay) - } - return convertedDate -} - -export function getHour (time: Timestamp): Timestamp { - const date: Date = new Date(time) - date.setMinutes(0, 0, 0) - return date.getTime() -} - -export function getDisplayTime (time: number): string { - let options: Intl.DateTimeFormatOptions = { hour: 'numeric', minute: 'numeric' } - if (!isToday(time)) { - options = { - month: 'numeric', - day: 'numeric', - ...options - } - } - - return new Date(time).toLocaleString('default', options) -} - -export function isOtherDay (time1: Timestamp, time2: Timestamp): boolean { - return getDay(time1) !== getDay(time2) -} - -export function isOtherHour (time1: Timestamp, time2: Timestamp): boolean { - return getHour(time1) !== getHour(time2) -} - -function isToday (time: number): boolean { - const current = new Date() - const target = new Date(time) - return ( - current.getDate() === target.getDate() && - current.getMonth() === target.getMonth() && - current.getFullYear() === target.getFullYear() - ) -} diff --git a/foundations/core/packages/core/src/tx.ts b/foundations/core/packages/core/src/tx.ts deleted file mode 100644 index af3cea4f17..0000000000 --- a/foundations/core/packages/core/src/tx.ts +++ /dev/null @@ -1,606 +0,0 @@ -// -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { - Arr, - AttachedDoc, - Class, - Data, - Doc, - Domain, - Mixin, - OperationDomain, - PersonId, - PropertyType, - Ref, - Space, - Timestamp -} from './classes' -import { clone } from './clone' -import core from './component' -import { setObjectValue } from './objvalue' -import { _getOperator } from './operator' -import { _toDoc } from './proxy' -import type { DocumentQuery, TxResult } from './storage' -import { generateId, type KeysByType } from './utils' - -/** - * @public - */ -export interface Tx extends Doc { - objectSpace: Ref // space where transaction will operate - meta?: Record // meta information about transaction, non persisted to final DB's -} - -/** - * @public - */ -export enum WorkspaceEvent { - UpgradeScheduled, - IndexingUpdate, - SecurityChange, - MaintenanceNotification, - BulkUpdate, - LastTx -} - -/** - * Event to be send by server during model upgrade procedure. - * @public - */ -export interface TxWorkspaceEvent extends Tx { - event: WorkspaceEvent - params: T -} -export interface TxDomainEvent extends Tx { - domain: OperationDomain - event: T -} - -/** - * @public - */ -export interface IndexingUpdateEvent { - _class: Ref>[] -} - -/** - * @public - */ -export interface BulkUpdateEvent { - _class: Ref>[] -} - -/** - * @public - */ -export interface TxModelUpgrade extends Tx {} - -/** - * @public - */ -export interface TxCUD extends Tx { - objectId: Ref - objectClass: Ref> - attachedTo?: Ref - attachedToClass?: Ref> - collection?: string -} - -/** - * @public - */ -export interface TxCreateDoc extends TxCUD { - attributes: Data -} - -/** - * @public - */ -export interface DocumentClassQuery { - _class: Ref> - query: DocumentQuery -} - -/** - * @public - * Apply set of transactions in sequential manner with verification of set of queries. - */ -export interface TxApplyIf extends Tx { - // only one operation per scope is allowed at one time. - scope?: string - - // All matches should be true with at least one document. - match?: DocumentClassQuery[] - - // All matches should be false for all documents. - notMatch?: DocumentClassQuery[] - - // If all matched execute following transactions. - txes: TxCUD[] - - notify?: boolean // If false will not send notifications. - - // If passed, will send WorkspaceEvent.BulkUpdate event with list of classes to update - extraNotify?: Ref>[] - - // If defined will go into a separate measure section - measureName?: string -} - -export interface TxApplyResult { - success: boolean - serverTime: number -} - -/** - * @public - */ -export type MixinData = Omit & -PushOptions> & -IncOptions> - -/** - * @public - */ -export type MixinUpdate = Partial> & -PushOptions> & -IncOptions> - -/** - * Define Create/Update for mixin attributes. - * @public - */ -export interface TxMixin extends TxCUD { - mixin: Ref> - attributes: MixinUpdate -} - -/** - * @public - */ -export type ArrayAsElement = { - [P in keyof T]: T[P] extends Arr ? Partial | PullArray | X : never -} - -/** - * @public - */ -export interface Position { - $each: X[] - $position: number -} - -/** - * @public - */ -export interface QueryUpdate { - $query: Partial - $update: Partial -} - -/** - * @public - */ -export interface PullArray { - $in: X[] -} - -/** - * @public - */ -export interface MoveDescriptor { - $value: X - $position: number -} - -/** - * @public - */ -export type ArrayAsElementPosition = { - [P in keyof T]-?: T[P] extends Arr ? X | Position : never -} - -/** - * @public - */ -export type ArrayAsElementUpdate = { - [P in keyof T]-?: T[P] extends Arr ? X | QueryUpdate : never -} - -/** - * @public - */ -export type ArrayMoveDescriptor = { - [P in keyof T]: T[P] extends Arr ? MoveDescriptor : never -} - -/** - * @public - */ -export type NumberProperties = { - [P in keyof T]: T[P] extends number | undefined | null ? T[P] : never -} - -/** - * @public - */ -export type OmitNever = Omit> - -/** - * @public - */ -export interface PushOptions { - $push?: Partial>>> - $pull?: Partial>>> -} - -/** - * @public - */ -export type UnsetProperties = Record - -/** - * @public - */ -export interface UnsetOptions { - $unset?: UnsetProperties -} - -/** - * @public - */ -export interface SetEmbeddedOptions { - $update?: Partial>>> -} - -/** - * @public - */ -export interface IncOptions { - $inc?: Partial>> -} - -/** - * @public - */ -export interface SpaceUpdate { - space?: Ref -} - -/** - * @public - */ -export type DocumentUpdate = Partial> & -PushOptions & -SetEmbeddedOptions & -IncOptions & -UnsetOptions & -SpaceUpdate - -/** - * @public - */ -export interface TxUpdateDoc extends TxCUD { - operations: DocumentUpdate - retrieve?: boolean -} - -/** - * @public - */ -export interface TxRemoveDoc extends TxCUD {} - -/** - * @public - */ -export const DOMAIN_TX = 'tx' as Domain - -/** - * @public - */ -export interface WithTx { - tx: (...txs: Tx[]) => Promise -} - -/** - * @public - */ -export abstract class TxProcessor implements WithTx { - async tx (...txes: Tx[]): Promise { - const result: TxResult[] = [] - for (const tx of txes) { - switch (tx._class) { - case core.class.TxCreateDoc: - result.push(await this.txCreateDoc(tx as TxCreateDoc)) - break - case core.class.TxUpdateDoc: - result.push(await this.txUpdateDoc(tx as TxUpdateDoc)) - break - case core.class.TxRemoveDoc: - result.push(await this.txRemoveDoc(tx as TxRemoveDoc)) - break - case core.class.TxMixin: - result.push(await this.txMixin(tx as TxMixin)) - break - case core.class.TxApplyIf: - // Apply if processed on server - return await Promise.resolve([]) - } - } - return result - } - - static createDoc2Doc(tx: TxCreateDoc, doClone = true): T { - const attached = - tx.attachedTo !== undefined - ? { - attachedTo: tx.attachedTo, - attachedToClass: tx.attachedToClass, - collection: tx.collection - } - : {} - - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - return { - ...(doClone ? clone(tx.attributes) : tx.attributes), - ...attached, - _id: tx.objectId, - _class: tx.objectClass, - space: tx.objectSpace, - modifiedBy: tx.modifiedBy, - modifiedOn: tx.modifiedOn, - createdBy: tx.createdBy ?? tx.modifiedBy, - createdOn: tx.createdOn ?? tx.modifiedOn - } as T - } - - static updateDoc2Doc(rawDoc: T, tx: TxUpdateDoc): T { - const doc = _toDoc(rawDoc) - TxProcessor.applyUpdate(doc, tx.operations as any) - doc.modifiedBy = tx.modifiedBy - doc.modifiedOn = tx.modifiedOn - return rawDoc - } - - static applyUpdate(doc: T, ops: any): void { - for (const key in ops) { - if (key.startsWith('$')) { - const operator = _getOperator(key) - operator(doc, ops[key]) - } else { - setObjectValue(key, doc, ops[key]) - } - } - } - - static updateMixin4Doc(rawDoc: D, tx: TxMixin): D { - const ops = tx.attributes as any - const doc = _toDoc(rawDoc) - const mixin = (doc as any)[tx.mixin] ?? {} - for (const key in ops) { - if (key.startsWith('$')) { - const operator = _getOperator(key) - operator(mixin, ops[key]) - } else { - setObjectValue(key, mixin, ops[key]) - } - } - rawDoc.modifiedBy = tx.modifiedBy - rawDoc.modifiedOn = tx.modifiedOn - ;(doc as any)[tx.mixin] = mixin - return rawDoc - } - - static buildDoc2Doc(txes: Tx[]): D | undefined | null { - let doc: Doc - const deleteTx = txes.find((tx) => tx._class === core.class.TxRemoveDoc) - if (deleteTx !== undefined) { - return null - } - const createTx = txes.find((tx) => tx._class === core.class.TxCreateDoc) - if (createTx === undefined) { - return - } - doc = TxProcessor.createDoc2Doc(createTx as TxCreateDoc) - for (const tx of txes) { - if (tx._class === core.class.TxUpdateDoc) { - doc = TxProcessor.updateDoc2Doc(doc, tx as TxUpdateDoc) - } else if (tx._class === core.class.TxMixin) { - const mixinTx = tx as TxMixin - doc = TxProcessor.updateMixin4Doc(doc, mixinTx) - } - } - return doc as D - } - - static isExtendsCUD (_class: Ref>): boolean { - return ( - _class === core.class.TxCreateDoc || - _class === core.class.TxUpdateDoc || - _class === core.class.TxRemoveDoc || - _class === core.class.TxMixin - ) - } - - static txHasUpdate(tx: TxUpdateDoc, attribute: string): boolean { - const ops = tx.operations - if ((ops as any)[attribute] !== undefined) return true - for (const op in ops) { - if (op.startsWith('$')) { - const opValue = (ops as any)[op] - for (const key in opValue) { - if (key === attribute || key.startsWith(attribute + '.')) { - return true - } - } - } - } - return false - } - - protected abstract txCreateDoc (tx: TxCreateDoc): Promise - protected abstract txUpdateDoc (tx: TxUpdateDoc): Promise - protected abstract txRemoveDoc (tx: TxRemoveDoc): Promise - protected abstract txMixin (tx: TxMixin): Promise -} - -/** - * @public - */ -export class TxFactory { - private readonly txSpace: Ref - constructor ( - readonly account: PersonId, - readonly isDerived: boolean = false - ) { - this.txSpace = isDerived ? core.space.DerivedTx : core.space.Tx - } - - createTxCreateDoc( - _class: Ref>, - space: Ref, - attributes: Data, - objectId?: Ref, - modifiedOn?: Timestamp, - modifiedBy?: PersonId - ): TxCreateDoc { - return { - _id: generateId(), - _class: core.class.TxCreateDoc, - space: this.txSpace, - objectId: objectId ?? generateId(), - objectClass: _class, - objectSpace: space, - modifiedOn: modifiedOn ?? Date.now(), - modifiedBy: modifiedBy ?? this.account, - createdBy: modifiedBy ?? this.account, - attributes - } - } - - createTxCollectionCUD( - _class: Ref>, - objectId: Ref, - space: Ref, - collection: string, - tx: TxCUD

, - modifiedOn?: Timestamp, - modifiedBy?: PersonId - ): TxCUD

{ - return { - ...tx, - collection, - attachedTo: objectId, - attachedToClass: _class, - modifiedOn: modifiedOn ?? Date.now(), - modifiedBy: modifiedBy ?? this.account - } - } - - createTxUpdateDoc( - _class: Ref>, - space: Ref, - objectId: Ref, - operations: DocumentUpdate, - retrieve?: boolean, - modifiedOn?: Timestamp, - modifiedBy?: PersonId - ): TxUpdateDoc { - return { - _id: generateId(), - _class: core.class.TxUpdateDoc, - space: this.txSpace, - modifiedBy: modifiedBy ?? this.account, - modifiedOn: modifiedOn ?? Date.now(), - objectId, - objectClass: _class, - objectSpace: space, - operations, - retrieve - } - } - - createTxRemoveDoc( - _class: Ref>, - space: Ref, - objectId: Ref, - modifiedOn?: Timestamp, - modifiedBy?: PersonId - ): TxRemoveDoc { - return { - _id: generateId(), - _class: core.class.TxRemoveDoc, - space: this.txSpace, - modifiedBy: modifiedBy ?? this.account, - modifiedOn: modifiedOn ?? Date.now(), - objectId, - objectClass: _class, - objectSpace: space - } - } - - createTxMixin( - objectId: Ref, - objectClass: Ref>, - objectSpace: Ref, - mixin: Ref>, - attributes: MixinUpdate, - modifiedOn?: Timestamp, - modifiedBy?: PersonId - ): TxMixin { - return { - _id: generateId(), - _class: core.class.TxMixin, - space: this.txSpace, - modifiedBy: modifiedBy ?? this.account, - modifiedOn: modifiedOn ?? Date.now(), - objectId, - objectClass, - objectSpace, - mixin, - attributes - } - } - - createTxApplyIf ( - space: Ref, - scope: string | undefined, - match: DocumentClassQuery[], - notMatch: DocumentClassQuery[], - txes: TxCUD[], - measureName: string | undefined, - notify: boolean = true, - extraNotify: Ref>[] = [], - modifiedOn?: Timestamp, - modifiedBy?: PersonId - ): TxApplyIf { - return { - _id: generateId(), - _class: core.class.TxApplyIf, - space: this.txSpace, - modifiedBy: modifiedBy ?? this.account, - modifiedOn: modifiedOn ?? Date.now(), - objectSpace: space, - scope, - match, - notMatch, - txes, - measureName, - notify, - extraNotify - } - } -} diff --git a/foundations/core/packages/core/src/utils.ts b/foundations/core/packages/core/src/utils.ts deleted file mode 100644 index dddc0c0706..0000000000 --- a/foundations/core/packages/core/src/utils.ts +++ /dev/null @@ -1,1010 +0,0 @@ -// -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { getEmbeddedLabel, getMetadata, type IntlString } from '@hcengineering/platform' -import { deepEqual } from 'fast-equals' -import { DOMAIN_BENCHMARK } from './benchmark' -import { - type Account, - AccountRole, - type AccountUuid, - type AnyAttribute, - type AttachedDoc, - type Class, - ClassifierKind, - type Collection, - type Doc, - type DocData, - type Domain, - DOMAIN_BLOB, - DOMAIN_MODEL, - DOMAIN_TRANSIENT, - type FullTextSearchContext, - IndexKind, - type Obj, - type Permission, - type PluginConfiguration, - type Rank, - type Ref, - type Role, - roleOrder, - type SocialId, - SocialIdType, - type SocialKey, - type Space, - type TypedSpace, - type WorkspaceMode -} from './classes' -import core from './component' -import { type Hierarchy } from './hierarchy' -import { type TxOperations } from './operations' -import { isPredicate } from './predicate' -import { type Branding, type BrandingMap } from './server' -import { type DocumentQuery, type FindResult } from './storage' -import { DOMAIN_TX, type Tx, type TxCreateDoc, type TxCUD, TxProcessor, type TxUpdateDoc } from './tx' - -function toHex (value: number, chars: number): string { - const result = value.toString(16) - if (result.length < chars) { - return '0'.repeat(chars - result.length) + result - } - return result -} - -let counter = (Math.random() * (1 << 24)) | 0 -const random = toHex((Math.random() * (1 << 24)) | 0, 6) + toHex((Math.random() * (1 << 16)) | 0, 4) - -function timestamp (): string { - const time = (Date.now() / 1000) | 0 - return toHex(time, 8) -} - -function count (): string { - const val = counter++ & 0xffffff - return toHex(val, 6) -} - -/** - * @public - * @returns - */ -export function generateId (join: string = ''): Ref { - return (timestamp() + join + random + join + count()) as Ref -} - -export function generateUuid (): string { - // Consider own implementation if it will be slow - return crypto.randomUUID() -} - -/** @public */ -export function isId (value: any): value is Ref { - return typeof value === 'string' && /^[0-9a-f]{24,24}$/.test(value) -} - -let currentAccount: Account - -/** - * @public - * @returns - */ -export function getCurrentAccount (): Account { - return currentAccount -} - -/** - * @public - * @param account - - */ -export function setCurrentAccount (account: Account): void { - currentAccount = account -} -/** - * @public - */ -export function escapeLikeForRegexp (value: string): string { - return value.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') -} - -/** - * @public - */ -export function toFindResult (docs: T[], total?: number, lookupMap?: Record): FindResult { - const length = total ?? docs.length - if (Object.keys(lookupMap ?? {}).length === 0) { - lookupMap = undefined - } - return Object.assign(docs, { total: length, lookupMap }) -} - -export type WorkspaceUuid = string & { __workspaceUuid: true } -export type WorkspaceDataId = string & { __workspaceDataId: true } -export interface WorkspaceIds { - uuid: WorkspaceUuid - url: string - dataId?: WorkspaceDataId // Old workspace identifier. E.g. Database name in Mongo, bucket in R2, etc. -} - -/** - * @public - */ -export function isWorkspaceCreating (mode?: WorkspaceMode): boolean { - if (mode === undefined) { - return false - } - - return ['pending-creation', 'creating'].includes(mode) -} - -/** - * @public - */ -export function docKey (name: string, _class?: Ref>): string { - return _class === undefined || _class !== core.class.Doc ? name : `${_class}%${name}` -} - -/** - * @public - */ -export function isFullTextAttribute (attr: AnyAttribute): boolean { - return ( - attr.index === IndexKind.FullText || - attr.type._class === core.class.TypeBlob || - attr.type._class === core.class.EnumOf || - attr.type._class === core.class.TypeCollaborativeDoc - ) -} - -/** - * @public - */ -export function isIndexedAttribute (attr: AnyAttribute): boolean { - return attr.index === IndexKind.Indexed || attr.index === IndexKind.IndexedDsc -} - -/** - * @public - */ -export interface IdMap extends Map, T> {} - -/** - * @public - */ -export function toIdMap (arr: T[]): IdMap { - return new Map(arr.map((p) => [p._id, p])) -} - -/** - * @public - */ -export function concatLink (host: string, path: string): string { - if (!host.endsWith('/') && !path.startsWith('/')) { - return `${host}/${path}` - } else if (host.endsWith('/') && path.startsWith('/')) { - const newPath = path.slice(1) - return `${host}${newPath}` - } else { - return `${host}${path}` - } -} - -/** - * @public - */ -export function fillDefaults ( - hierarchy: Hierarchy, - object: DocData | T, - _class: Ref> -): DocData | T { - const baseClass = hierarchy.isDerived(_class, core.class.AttachedDoc) ? core.class.AttachedDoc : core.class.Doc - const attributes = hierarchy.getAllAttributes(_class, baseClass) - for (const attribute of attributes) { - if (attribute[1].defaultValue !== undefined) { - if ((object as any)[attribute[0]] === undefined) { - // Clone default value as it might be an object (e.g. array) - ;(object as any)[attribute[0]] = structuredClone(attribute[1].defaultValue) - } - } - } - return object -} - -/** - * @public - */ -export class AggregateValueData { - constructor ( - readonly name: string, - readonly _id: Ref, - readonly space: Ref, - readonly rank?: string, - readonly category?: Ref - ) {} - - getRank (): string { - return this.rank ?? '' - } -} - -/** - * @public - */ -export class AggregateValue { - constructor ( - readonly name: string | undefined, - readonly values: AggregateValueData[] - ) {} -} - -/** - * @public - */ -export type CategoryType = number | string | undefined | Ref | AggregateValue - -export interface IDocManager { - get: (ref: Ref) => T | undefined - getDocs: () => T[] - getIdMap: () => IdMap - filter: (predicate: (value: T) => boolean) => T[] -} - -/** - * @public - */ -export class DocManager implements IDocManager { - protected readonly byId: IdMap - - constructor (protected readonly docs: T[]) { - this.byId = toIdMap(docs) - } - - get (ref: Ref): T | undefined { - return this.byId.get(ref) - } - - getDocs (): T[] { - return this.docs - } - - getIdMap (): IdMap { - return this.byId - } - - filter (predicate: (value: T) => boolean): T[] { - return this.docs.filter(predicate) - } -} - -/** - * @public - */ - -export class RateLimiter { - idCounter: number = 0 - processingQueue = new Map>() - last: number = 0 - rate: number - - queue: (() => Promise)[] = [] - - constructor (rate: number) { - this.rate = rate - } - - notify: (() => void)[] = [] - - async exec = any>(op: (args?: B) => Promise, args?: B): Promise { - const processingId = this.idCounter++ - - while (this.processingQueue.size >= this.rate) { - await new Promise((resolve) => { - this.notify.push(resolve) - }) - } - try { - const p = op(args) - this.processingQueue.set(processingId, p as Promise) - return await p - } finally { - this.processingQueue.delete(processingId) - const n = this.notify.shift() - if (n !== undefined) { - n() - } - } - } - - async add = any>( - op: (args?: B) => Promise, - args?: B, - errHandler?: (err: any) => void - ): Promise { - while (this.processingQueue.size >= this.rate) { - await new Promise((resolve) => { - this.notify.push(resolve) - }) - } - void this.exec(op, args).catch((err) => { - if (errHandler !== undefined) { - errHandler(err) - } - console.error('Failed to execute in rate limitter', err) - }) - } - - async waitProcessing (): Promise { - while (this.processingQueue.size > 0) { - await new Promise((resolve) => { - this.notify.push(resolve) - }) - } - } -} - -export function mergeQueries (query1: DocumentQuery, query2: DocumentQuery): DocumentQuery { - const keys1 = Object.keys(query1) - const keys2 = Object.keys(query2) - - const query = {} - - for (const key of keys1) { - if (!keys2.includes(key)) { - Object.assign(query, { [key]: query1[key] }) - } - } - - for (const key of keys2) { - if (!keys1.includes(key)) { - Object.assign(query, { [key]: query2[key] }) - } else { - const value = mergeField(query1[key], query2[key]) - if (value !== undefined) { - Object.assign(query, { [key]: value }) - } - } - } - - return query -} - -function mergeField (field1: any, field2: any): any | undefined { - // this is a special predicate that causes query never return any docs - // it is used in cases when queries intersection is empty - const never = { $in: [] } - // list of ignored predicates, handled separately - const ignored = ['$in', '$nin', '$ne'] - - const isPredicate1 = isPredicate(field1) - const isPredicate2 = isPredicate(field2) - - if (isPredicate1 && isPredicate2) { - // $in, $nin, $eq are related fields so handle them separately here - const result = getInNiN(field1, field2) - - const keys1 = Object.keys(field1) - const keys2 = Object.keys(field2) - - for (const key of keys1) { - if (ignored.includes(key)) continue - - if (!keys2.includes(key)) { - Object.assign(result, { [key]: field1[key] }) - } else { - const value = mergePredicateWithPredicate(key, field1[key], field2[key]) - if (value !== undefined) { - Object.assign(result, { [key]: value }) - } - } - } - - for (const key of keys2) { - if (ignored.includes(key)) continue - - if (!keys1.includes(key)) { - Object.assign(result, { [key]: field2[key] }) - } - } - - return Object.keys(result).length > 0 ? result : undefined - } else if (isPredicate1 || isPredicate2) { - // when one field is a predicate and the other is a simple value - // we need to ensure that the value matches predicate - const predicate = isPredicate1 ? field1 : field2 - const value = isPredicate1 ? field2 : field1 - - for (const x in predicate) { - const result = mergePredicateWithValue(x, predicate[x], value) - if ( - Array.isArray(result?.$in) && - result.$in.length > 0 && - Array.isArray(result?.$nin) && - result.$nin.length === 0 - ) { - delete result.$nin - } - if (result !== undefined) { - return result - } - } - - // if we reached here, the value does not match the predicate - return never - } else { - // both are not predicates, can filter only when values are equal - return deepEqual(field1, field2) ? field1 : never - } -} - -function mergePredicateWithPredicate (predicate: string, val1: any, val2: any): any | undefined { - if (val1 === undefined) return val2 - if (val2 === undefined) return val1 - - switch (predicate) { - case '$lt': - return val1 < val2 ? val1 : val2 - case '$lte': - return val1 <= val2 ? val1 : val2 - case '$gt': - return val1 > val2 ? val1 : val2 - case '$gte': - return val1 >= val2 ? val1 : val2 - } - - // TODO we should properly support all available predicates here - // until then, fallback to the first predicate value - - return val1 -} - -function mergePredicateWithValue (predicate: string, val1: any, val2: any): any | undefined { - switch (predicate) { - case '$in': - return Array.isArray(val1) && val1.includes(val2) ? val2 : undefined - case '$nin': - return Array.isArray(val1) && !val1.includes(val2) ? val2 : undefined - case '$lt': - return val2 < val1 ? val2 : undefined - case '$lte': - return val2 <= val1 ? val2 : undefined - case '$gt': - return val2 > val1 ? val2 : undefined - case '$gte': - return val2 >= val1 ? val2 : undefined - case '$ne': - return val1 !== val2 ? val2 : undefined - } - - // TODO we should properly support all available predicates here - // until then, fallback to the non-predicate value - - return val2 -} - -function getInNiN (query1: any, query2: any): any { - const aIn = typeof query1 === 'object' && '$in' in query1 ? query1.$in : undefined - const bIn = typeof query2 === 'object' && '$in' in query2 ? query2.$in : undefined - const aNIn = - (typeof query1 === 'object' && '$nin' in query1 ? query1.$nin : undefined) ?? - (typeof query1 === 'object' && query1.$ne !== undefined ? [query1.$ne] : []) - const bNIn = - (typeof query2 === 'object' && '$nin' in query2 ? query2.$nin : undefined) ?? - (typeof query1 === 'object' && query2.$ne !== undefined ? [query2.$ne] : []) - - const finalNin = Array.from(new Set([...aNIn, ...bNIn])) - - // we must keep $in if it was in the original query - if (aIn !== undefined || bIn !== undefined) { - const finalIn = - aIn !== undefined && bIn !== undefined - ? aIn.length - bIn.length < 0 - ? bIn.filter((c: any) => aIn.includes(c)) - : aIn.filter((c: any) => bIn.includes(c)) - : (aIn ?? bIn) - return { $in: finalIn.filter((p: any) => !finalNin.includes(p)) } - } - // try to preserve original $ne instead of $nin - if ((typeof query1 === 'object' && '$ne' in query1) || (typeof query2 === 'object' && '$ne' in query2)) { - if (finalNin.length === 1) { - return { $ne: finalNin[0] } - } - } - if (finalNin.length > 0) { - return { $nin: finalNin } - } - return {} -} - -export function cutObjectArray (obj: any): any { - if (obj == null) { - return obj - } - const r = {} - for (const key of Object.keys(obj)) { - if (Array.isArray(obj[key])) { - if (obj[key].length > 3) { - Object.assign(r, { [key]: [...obj[key].slice(0, 3), `... and ${obj[key].length - 3} more`] }) - } else Object.assign(r, { [key]: obj[key] }) - continue - } - if (typeof obj[key] === 'object' && obj[key] !== null) { - Object.assign(r, { [key]: cutObjectArray(obj[key]) }) - continue - } - Object.assign(r, { [key]: obj[key] }) - } - return r -} - -export function includesAny (arr1: string[] | null | undefined, arr2: string[] | null | undefined): boolean { - if (arr1 == null || arr1.length === 0 || arr2 == null || arr2.length === 0) { - return false - } - - return arr1.some((m) => arr2.includes(m)) -} - -export const isEnum = - (e: T) => - (token: any): token is T[keyof T] => { - return typeof token === 'string' && Object.values(e as Record).includes(token) - } - -export async function checkPermission ( - client: TxOperations, - _id: Ref, - _space: Ref, - space?: TypedSpace -): Promise { - const arePermissionsDisabled = getMetadata(core.metadata.DisablePermissions) ?? false - if (arePermissionsDisabled) return true - - return await hasPermission(client, _id, _space, space) -} - -export async function checkForbiddenPermission ( - client: TxOperations, - _id: Ref, - _space: Ref, - space?: TypedSpace -): Promise { - const arePermissionsDisabled = getMetadata(core.metadata.DisablePermissions) ?? false - if (arePermissionsDisabled) return false - - return await hasPermission(client, _id, _space, space) -} - -async function hasPermission ( - client: TxOperations, - _id: Ref, - _space: Ref, - space?: TypedSpace -): Promise { - space = space ?? (await client.findOne(core.class.TypedSpace, { _id: _space })) - const type = await client - .getModel() - .findOne(core.class.SpaceType, { _id: space?.type }, { lookup: { _id: { roles: core.class.Role } } }) - const mixin = type?.targetClass - if (space === undefined || type === undefined || mixin === undefined) { - return false - } - - const me = getCurrentAccount() - const asMixin = client.getHierarchy().as(space, mixin) - const myRoles = type.$lookup?.roles?.filter((role) => ((asMixin as any)[role._id] ?? []).includes(me.uuid)) as Role[] - - if (myRoles === undefined) { - return false - } - - const myPermissions = new Set(myRoles.flatMap((role) => role.permissions)) - - return myPermissions.has(_id) -} - -/** - * @public - */ -export function getRoleAttributeLabel (roleName: string): IntlString { - return getEmbeddedLabel(`Role: ${roleName.trim()}`) -} - -/** - * @public - */ -export function getFullTextIndexableAttributes ( - hierarchy: Hierarchy, - clazz: Ref>, - skipDocs: boolean = false -): AnyAttribute[] { - const allAttributes = hierarchy.getAllAttributes(clazz) - const result: AnyAttribute[] = [] - for (const [, attr] of allAttributes) { - if (skipDocs && (attr.attributeOf === core.class.Doc || attr.attributeOf === core.class.AttachedDoc)) { - continue - } - if (isFullTextAttribute(attr) || isIndexedAttribute(attr)) { - result.push(attr) - } - } - - hierarchy - .getDescendants(clazz) - .filter((m) => hierarchy.getClass(m).kind === ClassifierKind.MIXIN) - .forEach((m) => { - for (const [, v] of hierarchy.getAllAttributes(m, clazz)) { - if (skipDocs && (v.attributeOf === core.class.Doc || v.attributeOf === core.class.AttachedDoc)) { - continue - } - if (isFullTextAttribute(v) || isIndexedAttribute(v)) { - result.push(v) - } - } - }) - return result -} - -const ctxKey = 'indexer_ftc' -/** - * @public - */ -export function getFullTextContext ( - hierarchy: Hierarchy, - objectClass: Ref>, - contexts: Map>, FullTextSearchContext> -): Omit> { - let ctx: Omit> | undefined = hierarchy.getClassifierProp(objectClass, ctxKey) - if (ctx !== undefined) { - return ctx - } - if (typeof ctx !== 'string') { - const anc = hierarchy.getAncestors(objectClass) - for (const oc of anc) { - const ctx = contexts.get(oc) - if (ctx !== undefined) { - hierarchy.setClassifierProp(objectClass, ctxKey, ctx) - return ctx - } - } - } - ctx = { - toClass: objectClass, - fullTextSummary: false, - forceIndex: false - } - hierarchy.setClassifierProp(objectClass, ctxKey, ctx) - return ctx -} - -/** - * @public - */ -export function isClassIndexable ( - hierarchy: Hierarchy, - c: Ref>, - contexts: Map>, FullTextSearchContext> -): boolean { - const indexed = hierarchy.getClassifierProp(c, 'class_indexed') - if (indexed !== undefined) { - return indexed as boolean - } - const domain = hierarchy.findDomain(c) - if (domain === undefined) { - hierarchy.setClassifierProp(c, 'class_indexed', false) - return false - } - - if ( - domain === DOMAIN_TX || - domain === DOMAIN_MODEL || - domain === DOMAIN_BLOB || - domain === ('preference' as Domain) || - domain === DOMAIN_TRANSIENT || - domain === ('settings' as Domain) || - domain === DOMAIN_BENCHMARK - ) { - hierarchy.setClassifierProp(c, 'class_indexed', false) - return false - } - - const indexMixin = hierarchy.classHierarchyMixin(c, core.mixin.IndexConfiguration) - if (indexMixin?.searchDisabled !== undefined && indexMixin?.searchDisabled) { - hierarchy.setClassifierProp(c, 'class_indexed', false) - return false - } - - const attrs = getFullTextIndexableAttributes(hierarchy, c, true) - for (const d of hierarchy.getDescendants(c)) { - if (hierarchy.isMixin(d)) { - attrs.push(...getFullTextIndexableAttributes(hierarchy, d, true)) - } - } - - let result = true - - if (attrs.length === 0 && !(getFullTextContext(hierarchy, c, contexts)?.forceIndex ?? false)) { - result = false - // We need check if document has collections with indexable fields. - const attrs = hierarchy.getAllAttributes(c).values() - for (const attr of attrs) { - if (attr.type._class === core.class.Collection) { - if (isClassIndexable(hierarchy, (attr.type as Collection).of, contexts)) { - result = true - break - } - } - } - } - hierarchy.setClassifierProp(c, 'class_indexed', result) - return result -} - -type ReduceParameters any> = T extends (...args: infer P) => any ? P : never - -interface NextCall { - op: () => Promise -} - -/** - * Utility method to skip middle update calls, optimistically if update function is called multiple times with few different parameters, only the last variant will be executed. - * The last invocation is executed after a few cycles, allowing to skip middle ones. - * - * This method can be used inside Svelte components to collapse complex update logic and handle interactions. - */ -export function reduceCalls) => Promise> ( - operation: T -): (...args: ReduceParameters) => Promise { - let nextCall: NextCall | undefined - let currentCall: NextCall | undefined - - const next = (): void => { - currentCall = nextCall - nextCall = undefined - if (currentCall !== undefined) { - void currentCall.op().catch() - } - } - return async function (...args: ReduceParameters): Promise { - const myOp = async (): Promise => { - try { - await operation(...args) - } catch (err: any) { - console.error('Error occurred in reduceCalls:', err) - } - next() - } - - nextCall = { op: myOp } - await Promise.resolve() - if (currentCall === undefined) { - next() - } - } -} - -export function isOwnerOrMaintainer (): boolean { - const account = getCurrentAccount() - return hasAccountRole(account, AccountRole.Maintainer) -} - -export function hasAccountRole (acc: Account, targerRole: AccountRole): boolean { - return roleOrder[acc.role] >= roleOrder[targerRole] -} - -export function getBranding (brandings: BrandingMap, key: string | undefined): Branding | null { - if (key === undefined) return null - - return Object.values(brandings).find((branding) => branding.key === key) ?? null -} - -export function fillConfiguration (systemTx: Tx[], configs: Map, PluginConfiguration>): void { - for (const t of systemTx) { - if (t._class === core.class.TxCreateDoc) { - const ct = t as TxCreateDoc - if (ct.objectClass === core.class.PluginConfiguration) { - configs.set(ct.objectId as Ref, TxProcessor.createDoc2Doc(ct) as PluginConfiguration) - } - } else if (t._class === core.class.TxUpdateDoc) { - const ut = t as TxUpdateDoc - if (ut.objectClass === core.class.PluginConfiguration) { - const c = configs.get(ut.objectId as Ref) - if (c !== undefined) { - if (c.system !== true || ut.modifiedBy === core.account.ConfigUser) { - TxProcessor.updateDoc2Doc(c, ut) - } - } - } - } - } -} - -export function pluginFilterTx ( - excludedPlugins: PluginConfiguration[], - configs: Map, PluginConfiguration>, - systemTx: Tx[] -): Tx[] { - const stx = toIdMap(systemTx) - const totalExcluded = new Set>() - let msg = '' - for (const a of excludedPlugins) { - for (const c of configs.values()) { - if (a.pluginId === c.pluginId) { - for (const id of c.transactions) { - if (c.classFilter !== undefined) { - const filter = new Set(c.classFilter) - const tx = stx.get(id as Ref) - if ( - tx?._class === core.class.TxCreateDoc || - tx?._class === core.class.TxUpdateDoc || - tx?._class === core.class.TxRemoveDoc - ) { - const cud = tx as TxCUD - if (filter.has(cud.objectClass)) { - totalExcluded.add(id as Ref) - } - } - } else { - totalExcluded.add(id as Ref) - } - } - msg += ` ${c.pluginId}:${c.transactions.length}` - } - } - } - if (typeof window !== 'undefined') { - console.log('exclude plugin', msg) - } - systemTx = systemTx.filter((t) => !totalExcluded.has(t._id)) - return systemTx -} - -/** - * @public - */ -export class TimeRateLimiter { - idCounter: number = 0 - active: number = 0 - last: number = 0 - rate: number - period: number - executions: { time: number, running: boolean }[] = [] - - queue: (() => Promise)[] = [] - notify: (() => void)[] = [] - - constructor (rate: number, period: number = 1000) { - this.rate = rate - this.period = period - } - - private cleanupExecutions (): void { - const now = Date.now() - this.executions = this.executions.filter((time) => time.running || now - time.time < this.period) - } - - async exec = any>(op: (args?: B) => Promise, args?: B): Promise { - while (this.active >= this.rate || this.executions.length >= this.rate) { - this.cleanupExecutions() - if (this.executions.length < this.rate) { - break - } - await new Promise((resolve) => { - setTimeout(resolve, this.period / this.rate) - }) - } - - const v = { time: Date.now(), running: true } - this.active++ - try { - this.executions.push(v) - const p = op(args) - return await p - } finally { - v.running = false - this.active-- - this.cleanupExecutions() - const n = this.notify.shift() - if (n !== undefined) { - n() - } - } - } - - async waitProcessing (): Promise { - while (this.active > 0) { - await new Promise((resolve) => { - this.notify.push(resolve) - }) - } - } -} - -export function combineAttributes ( - attributes: any[], - key: string, - operator: '$push' | '$pull' | '$unset', - arrayKey?: '$each' | '$in' -): any[] { - return Array.from( - new Set( - attributes.flatMap((attr) => { - if (arrayKey === undefined) { - return attr[operator]?.[key] - } - - return Array.isArray(attr[operator]?.[key]?.[arrayKey]) - ? attr[operator]?.[key]?.[arrayKey] - : attr[operator]?.[key] - }) - ) - ).filter((v) => v != null) -} - -export function buildSocialIdString (key: SocialKey): string { - return `${key.type}:${key.value}` -} - -export function parseSocialIdString (id: string): SocialKey { - const [type, value] = id.split(':') - - if (type === undefined || value === undefined) { - throw new Error(`Social id is not valid: ${id}`) - } - - if (!Object.values(SocialIdType).includes(type as SocialIdType)) { - throw new Error(`Social id type is not valid: ${id}`) - } - - return { type: type as SocialIdType, value } -} - -export function pickPrimarySocialId (socialIds: SocialId[]): SocialId { - const activeSocialIds = socialIds.filter((si) => si.isDeleted !== true) - if (activeSocialIds.length === 0) { - throw new Error('No active social ids provided') - } - const hulySocialIds = activeSocialIds.filter((si) => si.type === SocialIdType.HULY) - - return hulySocialIds[0] ?? activeSocialIds[0] -} - -export const loginSocialTypes = [SocialIdType.EMAIL, SocialIdType.GOOGLE, SocialIdType.GITHUB, SocialIdType.OIDC] - -export function notEmpty (id: T | undefined | null): id is T { - return id !== undefined && id !== null && id !== '' -} - -export function unique (arr: T[]): T[] { - return Array.from(new Set(arr)) -} - -export function uniqueNotEmpty> (arr: Array): T[] { - return unique(arr).filter(notEmpty) -} -export { platformNow, platformNowDiff } from '@hcengineering/measurements' - -export interface PermissionsGrant { - spaces?: Ref[] - grantedBy?: AccountUuid -} - -export type KeysByType = { [k in keyof O]-?: O[k] extends T ? k : never }[keyof O] - -export function toRank (str: string | undefined): Rank | undefined { - if (str === undefined) return - if (str.startsWith('0|')) { - return str - } - return '0|' + str.replaceAll(/[-:_]/g, '').toLowerCase() -} diff --git a/foundations/core/packages/core/tsconfig.json b/foundations/core/packages/core/tsconfig.json deleted file mode 100644 index b5ae22f6e4..0000000000 --- a/foundations/core/packages/core/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/foundations/core/packages/hulylake-client/.eslintrc.js b/foundations/core/packages/hulylake-client/.eslintrc.js deleted file mode 100644 index 72235dc283..0000000000 --- a/foundations/core/packages/hulylake-client/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/foundations/core/packages/hulylake-client/.npmignore b/foundations/core/packages/hulylake-client/.npmignore deleted file mode 100644 index e3ec093c38..0000000000 --- a/foundations/core/packages/hulylake-client/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!/lib/** -!CHANGELOG.md -/lib/**/__tests__/ diff --git a/foundations/core/packages/hulylake-client/CHANGELOG.json b/foundations/core/packages/hulylake-client/CHANGELOG.json deleted file mode 100644 index e84136358d..0000000000 --- a/foundations/core/packages/hulylake-client/CHANGELOG.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "name": "@hcengineering/hulylake-client", - "entries": [ - { - "version": "0.7.17", - "tag": "@hcengineering/hulylake-client_v0.7.17", - "date": "Mon, 27 Oct 2025 13:27:12 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" - } - ] - } - }, - { - "version": "0.7.6", - "tag": "@hcengineering/hulylake-client_v0.7.6", - "date": "Tue, 14 Oct 2025 04:58:17 GMT", - "comments": { - "patch": [ - { - "comment": "update deps" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`" - }, - { - "comment": "Updating dependency \"@hcengineering/retry\" from `^0.7.4` to `0.7.5`" - } - ] - } - }, - { - "version": "0.7.5", - "tag": "@hcengineering/hulylake-client_v0.7.5", - "date": "Sat, 11 Oct 2025 18:20:33 GMT", - "comments": { - "patch": [ - { - "comment": "Update to latest platform rig" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`" - }, - { - "comment": "Updating dependency \"@hcengineering/retry\" from `^0.7.3` to `0.7.4`" - } - ] - } - }, - { - "version": "0.7.4", - "tag": "@hcengineering/hulylake-client_v0.7.4", - "date": "Thu, 09 Oct 2025 16:57:55 GMT", - "comments": { - "patch": [ - { - "comment": "refactoring hulylake client to extract multi-workspace client" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.4` to `0.7.5`" - } - ] - } - }, - { - "version": "0.7.3", - "tag": "@hcengineering/hulylake-client_v0.7.3", - "date": "Wed, 08 Oct 2025 03:40:53 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`" - } - ] - } - } - ] -} diff --git a/foundations/core/packages/hulylake-client/CHANGELOG.md b/foundations/core/packages/hulylake-client/CHANGELOG.md deleted file mode 100644 index 8e4c0f294c..0000000000 --- a/foundations/core/packages/hulylake-client/CHANGELOG.md +++ /dev/null @@ -1,35 +0,0 @@ -# Change Log - @hcengineering/hulylake-client - -This log was last generated on Mon, 27 Oct 2025 13:27:12 GMT and should not be manually modified. - -## 0.7.17 -Mon, 27 Oct 2025 13:27:12 GMT - -_Version update only_ - -## 0.7.6 -Tue, 14 Oct 2025 04:58:17 GMT - -### Patches - -- update deps - -## 0.7.5 -Sat, 11 Oct 2025 18:20:33 GMT - -### Patches - -- Update to latest platform rig - -## 0.7.4 -Thu, 09 Oct 2025 16:57:55 GMT - -### Patches - -- refactoring hulylake client to extract multi-workspace client - -## 0.7.3 -Wed, 08 Oct 2025 03:40:53 GMT - -_Initial release_ - diff --git a/foundations/core/packages/hulylake-client/config/rig.json b/foundations/core/packages/hulylake-client/config/rig.json deleted file mode 100644 index 0110930f55..0000000000 --- a/foundations/core/packages/hulylake-client/config/rig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig" -} diff --git a/foundations/core/packages/hulylake-client/jest.config.js b/foundations/core/packages/hulylake-client/jest.config.js deleted file mode 100644 index 069ea8aa0d..0000000000 --- a/foundations/core/packages/hulylake-client/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ['./src'], - coverageReporters: ['text-summary', 'html', 'lcov'] -} diff --git a/foundations/core/packages/hulylake-client/package.json b/foundations/core/packages/hulylake-client/package.json deleted file mode 100644 index 1cea5891eb..0000000000 --- a/foundations/core/packages/hulylake-client/package.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "name": "@hcengineering/hulylake-client", - "version": "0.7.17", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "files": [ - "lib/**/*", - "types/**/*", - "src/**/*", - "!src/**/__test__/**", - "tsconfig.json" - ], - "author": "Hardcore Engineering Inc.", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "build:watch": "compile", - "format": "format src", - "test": "jest --passWithNoTests --silent --coverage", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --coverage", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "workspace:^0.7.19", - "@types/jest": "^29.5.5", - "@types/node": "^22.18.1", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "@typescript-eslint/parser": "^6.21.0", - "cross-env": "~7.0.3", - "esbuild": "^0.25.10", - "eslint": "^8.54.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-n": "^15.4.0", - "eslint-plugin-promise": "^6.1.1", - "jest": "^29.7.0", - "prettier": "^3.6.2", - "ts-jest": "^29.1.1", - "typescript": "^5.9.3", - "eslint-plugin-svelte": "^2.35.1" - }, - "dependencies": { - "@hcengineering/core": "workspace:^0.7.22", - "@hcengineering/retry": "workspace:^0.7.17" - }, - "repository": "https://github.com/hcengineering/huly.core", - "publishConfig": { - "access": "public" - }, - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.js" - } - } -} diff --git a/foundations/core/packages/hulylake-client/src/client.ts b/foundations/core/packages/hulylake-client/src/client.ts deleted file mode 100644 index af88ca5199..0000000000 --- a/foundations/core/packages/hulylake-client/src/client.ts +++ /dev/null @@ -1,349 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { WorkspaceUuid } from '@hcengineering/core' -import { RetryOptions } from '@hcengineering/retry' - -import { fetchSafe, unwrapContentLength, unwrapEtag, unwrapLastModified } from './utils' -import { - HulyHeaders, - HulylakeClient, - HulylakeWorkspaceClient, - HulyMeta, - HulyResponse, - JsonPatch, - PatchOptions, - PutOptions, - Body -} from './types' - -export function getWorkspaceClient (baseUrl: string, workspace: WorkspaceUuid, token: string): HulylakeWorkspaceClient { - const client = new Client(baseUrl, token) - return new WorkspaceClient(client, workspace) -} - -export function getClient (baseUrl: string, token: string): HulylakeClient { - return new Client(baseUrl, token) -} - -class WorkspaceClient implements HulylakeWorkspaceClient { - constructor ( - private readonly client: HulylakeClient, - private readonly workspace: WorkspaceUuid - ) {} - - head (key: string, retryOptions?: RetryOptions): Promise> { - return this.client.head(this.workspace, key, retryOptions) - } - - get (key: string, retryOptions?: RetryOptions): Promise>> { - return this.client.get(this.workspace, key, retryOptions) - } - - put (key: string, body: Body, opts: PutOptions, retryOptions?: RetryOptions): Promise> { - return this.client.put(this.workspace, key, body, opts, retryOptions) - } - - patch (key: string, body: Body, opts: PatchOptions, retryOptions?: RetryOptions): Promise> { - return this.client.patch(this.workspace, key, body, opts, retryOptions) - } - - delete (key: string, retryOptions?: RetryOptions): Promise> { - return this.client.delete(this.workspace, key, retryOptions) - } - - public async getJson(key: string, retryOptions?: RetryOptions): Promise> { - const res = await this.client.get(this.workspace, key, retryOptions) - const body = res.ok && res.body != null ? ((await new Response(res.body).json()) as T) : undefined - return { ...res, body } - } - - public async putJson( - key: string, - json: T, - options?: Omit, - retryOptions?: RetryOptions - ): Promise> { - return await this.put(key, JSON.stringify(json), { ...options, mergeStrategy: 'jsonpatch' }, retryOptions) - } - - public async patchJson ( - key: string, - body: JsonPatch[], - options?: Omit, - retryOptions?: RetryOptions - ): Promise> { - return await this.patch( - key, - JSON.stringify(body), - { ...options, contentType: 'application/json-patch+json' }, - retryOptions - ) - } -} - -class Client implements HulylakeClient { - constructor ( - private readonly baseUrl: string, - private readonly token: string - ) { - this.baseUrl = this.baseUrl.endsWith('/') ? this.baseUrl.slice(0, -1) : this.baseUrl - } - - private authHeaders (init?: HeadersInit): Headers { - const headers = new Headers(init) - headers.set('Authorization', `Bearer ${this.token}`) - return headers - } - - private applyHeaders (h: Headers, headers?: HulyHeaders): void { - if (headers != null) for (const [k, v] of Object.entries(headers)) h.set(`huly-header-${k}`, v) - } - - private applyMeta (h: Headers, meta?: HulyMeta): void { - if (meta != null) for (const [k, v] of Object.entries(meta)) h.set(`huly-meta-${k}`, v) - } - - public async status (): Promise { - try { - const res = await fetchSafe(`${this.baseUrl}/status`) - return res.ok - } catch { - return false - } - } - - public objectUrl (workspace: string, key: string): string { - return `${this.baseUrl}/api/${workspace}/${encodeURIComponent(key)}` - } - - public async head (workspace: string, key: string, retryOptions?: RetryOptions): Promise> { - const res = await fetchSafe( - this.objectUrl(workspace, key), - { - method: 'HEAD', - headers: this.authHeaders() - }, - retryOptions - ) - - return { - ok: res.ok, - status: res.status, - etag: unwrapEtag(res.headers.get('ETag')), - lastModified: unwrapLastModified(res.headers.get('Last-Modified')), - contentType: res.headers.get('Content-Type') ?? 'application/octet-stream', - contentLength: unwrapContentLength(res.headers.get('Content-Length')), - headers: res.headers - } - } - - public async get ( - workspace: string, - key: string, - retryOptions?: RetryOptions - ): Promise>> { - try { - const res = await fetchSafe( - this.objectUrl(workspace, key), - { - method: 'GET', - headers: this.authHeaders() - }, - retryOptions - ) - - return { - ok: res.ok, - status: res.status, - etag: unwrapEtag(res.headers.get('ETag')), - headers: res.headers, - lastModified: unwrapLastModified(res.headers.get('Last-Modified')), - contentType: res.headers.get('Content-Type') ?? 'application/octet-stream', - contentLength: unwrapContentLength(res.headers.get('Content-Length')), - body: res.ok ? (res.body ?? undefined) : undefined - } - } catch (err: any) { - if (err.name === 'NotFoundError') { - return { - ok: false, - status: 404, - etag: undefined, - headers: new Headers(), - body: undefined - } - } - throw err - } - } - - public async partial ( - workspace: string, - key: string, - offset: number, - length?: number, - retryOptions?: RetryOptions - ): Promise>> { - try { - const res = await fetchSafe( - this.objectUrl(workspace, key), - { - method: 'GET', - headers: this.authHeaders({ - Range: length !== undefined ? `bytes=${offset}-${offset + length - 1}` : `bytes=${offset}` - }) - }, - retryOptions - ) - - return { - ok: res.ok, - status: res.status, - etag: unwrapEtag(res.headers.get('ETag')), - headers: res.headers, - body: res.ok ? (res.body ?? undefined) : undefined - } - } catch (err: any) { - if (err.name === 'NotFoundError') { - return { - ok: false, - status: 404, - etag: undefined, - headers: new Headers(), - body: undefined - } - } - throw err - } - } - - public async put ( - workspace: string, - key: string, - body: Body, - opts: PutOptions = {}, - retryOptions?: RetryOptions - ): Promise> { - const { mergeStrategy, headers, meta } = opts - const contentType = 'contentType' in opts ? opts.contentType : undefined - const contentLength = 'contentLength' in opts ? opts.contentLength : undefined - - const h = this.authHeaders() - - if (mergeStrategy != null) { - h.set('Huly-Merge-Strategy', mergeStrategy) - } - - if (contentType != null) { - h.set('Content-Type', contentType) - } else if (mergeStrategy === 'jsonpatch') { - h.set('Content-Type', 'application/json') - } - - if (contentLength != null) { - h.set('Content-Length', contentLength.toString()) - } - - this.applyHeaders(h, headers) - this.applyMeta(h, meta) - - const duplex = body instanceof ReadableStream ? 'half' : undefined - - const res = await fetchSafe( - this.objectUrl(workspace, key), - { - method: 'PUT', - headers: h, - body, - // @ts-expect-error must present for ReadableStream but it is not in the interface - duplex - }, - retryOptions - ) - - return { - ok: res.ok, - status: res.status, - etag: unwrapEtag(res.headers.get('ETag')), - lastModified: unwrapLastModified(res.headers.get('Last-Modified')), - contentLength: unwrapContentLength(res.headers.get('Content-Length')), - headers: res.headers - } - } - - public async patch ( - workspace: string, - key: string, - body: Body, - opts: PatchOptions = {}, - retryOptions?: RetryOptions - ): Promise> { - const { contentType, contentLength, headers, meta } = opts - - const h = this.authHeaders() - - if (contentType != null) { - h.set('Content-Type', contentType) - } - - if (contentLength != null) { - h.set('Content-Length', contentLength.toString()) - } - - this.applyHeaders(h, headers) - this.applyMeta(h, meta) - - const duplex = body instanceof ReadableStream ? 'half' : undefined - - const res = await fetchSafe( - this.objectUrl(workspace, key), - { - method: 'PATCH', - headers: h, - body, - // @ts-expect-error must present for ReadableStream but it is not in the interface - duplex - }, - retryOptions - ) - - return { - ok: res.ok, - status: res.status, - etag: unwrapEtag(res.headers.get('ETag')), - lastModified: unwrapLastModified(res.headers.get('Last-Modified')), - contentLength: unwrapContentLength(res.headers.get('Content-Length')), - contentType: res.headers.get('Content-Type') ?? undefined, - headers: res.headers - } - } - - public async delete (workspace: string, key: string, retryOptions?: RetryOptions): Promise> { - const res = await fetchSafe( - this.objectUrl(workspace, key), - { - method: 'DELETE', - headers: this.authHeaders() - }, - retryOptions - ) - - return { - ok: res.ok, - status: res.status, - headers: res.headers - } - } -} diff --git a/foundations/core/packages/hulylake-client/src/error.ts b/foundations/core/packages/hulylake-client/src/error.ts deleted file mode 100644 index a26cbd711a..0000000000 --- a/foundations/core/packages/hulylake-client/src/error.ts +++ /dev/null @@ -1,28 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export class NetworkError extends Error { - constructor (message: string) { - super(message) - this.name = 'NetworkError' - } -} - -export class HulylakeError extends Error { - constructor (message: string) { - super(message) - this.name = 'HulylakeError' - } -} diff --git a/foundations/core/packages/hulylake-client/src/index.ts b/foundations/core/packages/hulylake-client/src/index.ts deleted file mode 100644 index 0f078ded71..0000000000 --- a/foundations/core/packages/hulylake-client/src/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export * from './client' -export * from './types' diff --git a/foundations/core/packages/hulylake-client/src/types.ts b/foundations/core/packages/hulylake-client/src/types.ts deleted file mode 100644 index c9c53eb0d6..0000000000 --- a/foundations/core/packages/hulylake-client/src/types.ts +++ /dev/null @@ -1,120 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { RetryOptions } from '@hcengineering/retry' - -export interface HulylakeClient { - head: (workspace: string, key: string, retryOptions?: RetryOptions) => Promise> - get: ( - workspace: string, - key: string, - retryOptions?: RetryOptions - ) => Promise>> - partial: ( - workspace: string, - key: string, - offset: number, - length?: number, - retryOptions?: RetryOptions - ) => Promise>> - put: ( - workspace: string, - key: string, - body: Body, - opts: PutOptions, - retryOptions?: RetryOptions - ) => Promise> - patch: ( - workspace: string, - key: string, - body: Body, - opts: PatchOptions, - retryOptions?: RetryOptions - ) => Promise> - delete: (workspace: string, key: string, retryOptions?: RetryOptions) => Promise> - - objectUrl: (workspace: string, key: string) => string -} - -export interface HulylakeWorkspaceClient { - head: (key: string, retryOptions?: RetryOptions) => Promise> - get: (key: string, retryOptions?: RetryOptions) => Promise>> - put: (key: string, body: Body, opts: PutOptions, retryOptions?: RetryOptions) => Promise> - patch: (key: string, body: Body, opts: PatchOptions, retryOptions?: RetryOptions) => Promise> - delete: (key: string, retryOptions?: RetryOptions) => Promise> - - getJson: (key: string, retryOptions?: RetryOptions) => Promise> - putJson: ( - key: string, - json: T, - options?: Omit, - retryOptions?: RetryOptions - ) => Promise> - patchJson: ( - key: string, - body: JsonPatch[], - options?: Omit, - retryOptions?: RetryOptions - ) => Promise> -} - -export type Body = ReadableStream | ArrayBuffer | Blob | string -export type MergeStrategy = 'concatenate' | 'jsonpatch' -export type HulyHeaders = Record -export type HulyMeta = Record - -export type PutOptions = - | { - mergeStrategy?: 'concatenate' - contentLength?: number - contentType?: string - headers?: HulyHeaders - meta?: HulyMeta - } - | { - mergeStrategy: 'jsonpatch' - contentLength?: number - headers?: HulyHeaders - meta?: HulyMeta - } - -export interface PatchOptions { - contentLength?: number - contentType?: string - headers?: HulyHeaders - meta?: HulyMeta -} - -export type JsonPatch = - | { op: 'add', path: string, value: any } - | { op: 'replace', path: string, value: any } - | { op: 'remove', path: string } - | { op: 'move', from: string, path: string } - | { op: 'copy', from: string, path: string } - | { op: 'test', path: string, value: any } - | { hop: 'add', path: string, value: any, safe?: boolean } - | { hop: 'inc', path: string, value: number, safe?: boolean } - | { hop: 'remove', path: string, safe?: boolean } - -export interface HulyResponse { - ok: boolean - status: number - etag?: string - contentType?: string - contentLength?: number - lastModified?: number - headers: Headers - body?: Body -} diff --git a/foundations/core/packages/hulylake-client/src/utils.ts b/foundations/core/packages/hulylake-client/src/utils.ts deleted file mode 100644 index 1d93750e86..0000000000 --- a/foundations/core/packages/hulylake-client/src/utils.ts +++ /dev/null @@ -1,75 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { RetryOptions, withRetry } from '@hcengineering/retry' -import { HulylakeError, NetworkError } from './error' - -async function innerFetchSafe (url: string | URL, init?: RequestInit): Promise { - let response - try { - response = await fetch(url, init) - } catch (err: any) { - console.error('network error', { err }) - throw new NetworkError(`Network error ${err}`) - } - - if (response.ok) { - return response - } - - if (response.status === 404) { - return response - } - - const text = await response.text() - throw new HulylakeError(text) -} - -export async function fetchSafe (url: string | URL, init?: RequestInit, retryOptions?: RetryOptions): Promise { - if (retryOptions != null) { - return await withRetry(async () => await innerFetchSafe(url, init), retryOptions) - } - return await innerFetchSafe(url, init) -} - -export function unwrapEtag (etag: string | null | undefined): string | undefined { - if (etag == null) { - return undefined - } - - if (etag.startsWith('W/')) { - etag = etag.substring(2) - } - - if (etag.startsWith('"') && etag.endsWith('"')) { - etag = etag.slice(1, -1) - } - - return etag -} - -export function unwrapContentLength (length: string | null | undefined): number | undefined { - if (length == null) { - return undefined - } - return parseInt(length, 10) -} - -export function unwrapLastModified (lastModified: string | null | undefined): number | undefined { - if (lastModified == null) { - return undefined - } - return Date.parse(lastModified) -} diff --git a/foundations/core/packages/hulylake-client/tsconfig.json b/foundations/core/packages/hulylake-client/tsconfig.json deleted file mode 100644 index b5ae22f6e4..0000000000 --- a/foundations/core/packages/hulylake-client/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/foundations/core/packages/measurements-otlp/.eslintrc.js b/foundations/core/packages/measurements-otlp/.eslintrc.js deleted file mode 100644 index 72235dc283..0000000000 --- a/foundations/core/packages/measurements-otlp/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/foundations/core/packages/measurements-otlp/.npmignore b/foundations/core/packages/measurements-otlp/.npmignore deleted file mode 100644 index 9b083cb0c4..0000000000 --- a/foundations/core/packages/measurements-otlp/.npmignore +++ /dev/null @@ -1,8 +0,0 @@ -* -!/lib/** -!/types/** -!/src/** -!CHANGELOG.md -/lib/**/__test__/ -/types/**/__test__/ -/src/**/__test__/ \ No newline at end of file diff --git a/foundations/core/packages/measurements-otlp/CHANGELOG.json b/foundations/core/packages/measurements-otlp/CHANGELOG.json deleted file mode 100644 index 97439bfeb5..0000000000 --- a/foundations/core/packages/measurements-otlp/CHANGELOG.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "name": "@hcengineering/measurements-otlp", - "entries": [ - { - "version": "0.7.17", - "tag": "@hcengineering/measurements-otlp_v0.7.17", - "date": "Mon, 27 Oct 2025 15:24:19 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/measurements\" from `^0.7.17` to `0.7.18`" - } - ] - } - }, - { - "version": "0.7.13", - "tag": "@hcengineering/measurements-otlp_v0.7.13", - "date": "Mon, 27 Oct 2025 04:08:57 GMT", - "comments": { - "patch": [ - { - "comment": "Allow to suspend errors on change" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/measurements\" from `^0.7.13` to `0.7.14`" - } - ] - } - }, - { - "version": "0.7.12", - "tag": "@hcengineering/measurements-otlp_v0.7.12", - "date": "Sat, 11 Oct 2025 19:18:56 GMT", - "comments": { - "patch": [ - { - "comment": "rollback eslint" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/measurements\" from `^0.7.11` to `0.7.12`" - }, - { - "comment": "Updating dependency \"@hcengineering/platform-rig\" from `^0.7.14` to `0.7.15`" - } - ] - } - }, - { - "version": "0.7.11", - "tag": "@hcengineering/measurements-otlp_v0.7.11", - "date": "Sat, 11 Oct 2025 17:58:53 GMT", - "comments": { - "none": [ - { - "comment": "Add tests" - } - ], - "patch": [ - { - "comment": "Fix eslint deps" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/measurements\" from `^0.7.10` to `0.7.11`" - }, - { - "comment": "Updating dependency \"@hcengineering/platform-rig\" from `^0.7.12` to `0.7.13`" - } - ] - } - }, - { - "version": "0.7.10", - "tag": "@hcengineering/measurements-otlp_v0.7.10", - "date": "Fri, 10 Oct 2025 12:32:59 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/platform-rig\" from `^0.7.10` to `0.7.11`" - } - ] - } - } - ] -} diff --git a/foundations/core/packages/measurements-otlp/CHANGELOG.md b/foundations/core/packages/measurements-otlp/CHANGELOG.md deleted file mode 100644 index 95464fcff8..0000000000 --- a/foundations/core/packages/measurements-otlp/CHANGELOG.md +++ /dev/null @@ -1,35 +0,0 @@ -# Change Log - @hcengineering/measurements-otlp - -This log was last generated on Mon, 27 Oct 2025 15:24:19 GMT and should not be manually modified. - -## 0.7.17 -Mon, 27 Oct 2025 15:24:19 GMT - -_Version update only_ - -## 0.7.13 -Mon, 27 Oct 2025 04:08:57 GMT - -### Patches - -- Allow to suspend errors on change - -## 0.7.12 -Sat, 11 Oct 2025 19:18:56 GMT - -### Patches - -- rollback eslint - -## 0.7.11 -Sat, 11 Oct 2025 17:58:53 GMT - -### Patches - -- Fix eslint deps - -## 0.7.10 -Fri, 10 Oct 2025 12:32:59 GMT - -_Initial release_ - diff --git a/foundations/core/packages/measurements-otlp/config/rig.json b/foundations/core/packages/measurements-otlp/config/rig.json deleted file mode 100644 index 0110930f55..0000000000 --- a/foundations/core/packages/measurements-otlp/config/rig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig" -} diff --git a/foundations/core/packages/measurements-otlp/jest.config.js b/foundations/core/packages/measurements-otlp/jest.config.js deleted file mode 100644 index 2cfd408b67..0000000000 --- a/foundations/core/packages/measurements-otlp/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ["./src"], - coverageReporters: ["text-summary", "html"] -} diff --git a/foundations/core/packages/measurements-otlp/package.json b/foundations/core/packages/measurements-otlp/package.json deleted file mode 100644 index 6c030b5f6c..0000000000 --- a/foundations/core/packages/measurements-otlp/package.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "name": "@hcengineering/measurements-otlp", - "version": "0.7.17", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "files": [ - "lib/**/*", - "!lib/**/__test__/**", - "types/**/*", - "!types/**/__test__/**", - "src/**/*", - "!src/**/__test__/**", - "README.md", - "CHANGELOG.md" - ], - "author": "Anticrm Platform Contributors", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "build:watch": "compile", - "test": "jest --passWithNoTests --silent", - "format": "format src", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "workspace:^0.7.19", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "@typescript-eslint/parser": "^6.21.0", - "eslint": "^8.54.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-node": "^11.1.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0" - }, - "dependencies": { - "@hcengineering/measurements": "workspace:^0.7.18", - "@opentelemetry/sdk-node": "^0.203.0", - "@opentelemetry/sdk-logs": "^0.203.0", - "@opentelemetry/auto-instrumentations-node": "^0.62.0", - "@opentelemetry/resources": "^2.0.1", - "@opentelemetry/sdk-trace-node": "^2.0.1", - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/core": "^2.0.1", - "@opentelemetry/exporter-trace-otlp-http": "^0.203.0", - "@opentelemetry/exporter-logs-otlp-http": "^0.203.0", - "@opentelemetry/otlp-exporter-base": "^0.203.0", - "@opentelemetry/id-generator-aws-xray": "^2.0.0", - "@opentelemetry/exporter-metrics-otlp-http": "^0.203.0", - "@opentelemetry/api-logs": "^0.203.0", - "@opentelemetry/sdk-metrics": "^2.0.1" - }, - "repository": "https://github.com/hcengineering/huly.utils", - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.js" - } - }, - "publishConfig": { - "access": "public" - } -} diff --git a/foundations/core/packages/measurements-otlp/src/__tests__/telemetry.suspendError.test.ts b/foundations/core/packages/measurements-otlp/src/__tests__/telemetry.suspendError.test.ts deleted file mode 100644 index 3955ca0a89..0000000000 --- a/foundations/core/packages/measurements-otlp/src/__tests__/telemetry.suspendError.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { OpenTelemetryMetricsContext } from '../telemetry' - -describe('OpenTelemetryMetricsContext suspendErrors handling', () => { - test('when suspendErrors is true the span does NOT record exception or set status', async () => { - const mockSpan = { - recordException: jest.fn(), - setStatus: jest.fn(), - end: jest.fn(), - setAttribute: jest.fn() - } as any - - const mockTracer = { - startSpan: jest.fn().mockReturnValue(mockSpan) - } as any - - const ctx = new OpenTelemetryMetricsContext('root', mockTracer, undefined, undefined, {}) - - await expect( - ctx.with( - 'op', - {}, - () => Promise.reject(new Error('boom')), // operation returns a rejected promise - undefined, - { suspendErrors: true } - ) - ).rejects.toThrow('boom') - - expect(mockSpan.recordException).not.toHaveBeenCalled() - expect(mockSpan.setStatus).not.toHaveBeenCalled() - }) - - test('when suspendErrors is not true the span records exception and sets error status', async () => { - const mockSpan = { - recordException: jest.fn(), - setStatus: jest.fn(), - end: jest.fn(), - setAttribute: jest.fn() - } as any - - const mockTracer = { - startSpan: jest.fn().mockReturnValue(mockSpan) - } as any - - const ctx = new OpenTelemetryMetricsContext('root', mockTracer, undefined, undefined, {}) - - await expect(ctx.with('op', {}, () => Promise.reject(new Error('boom')), undefined, {})).rejects.toThrow('boom') - - expect(mockSpan.recordException).toHaveBeenCalled() - expect(mockSpan.setStatus).toHaveBeenCalledWith(expect.objectContaining({ message: 'boom' })) - }) -}) diff --git a/foundations/core/packages/measurements-otlp/src/__tests__/telemetry.test.ts b/foundations/core/packages/measurements-otlp/src/__tests__/telemetry.test.ts deleted file mode 100644 index 660147942f..0000000000 --- a/foundations/core/packages/measurements-otlp/src/__tests__/telemetry.test.ts +++ /dev/null @@ -1,451 +0,0 @@ -import { newMetrics, type MeasureLogger } from '@hcengineering/measurements' -import { context, trace } from '@opentelemetry/api' -import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node' -import { OpenTelemetryMetricsContext } from '../telemetry' - -describe('telemetry', () => { - let tracer: any - let provider: NodeTracerProvider - - beforeAll(() => { - provider = new NodeTracerProvider() - provider.register() - tracer = trace.getTracer('test-tracer') - }) - - afterAll(() => { - void provider.shutdown() - }) - - describe('OpenTelemetryMetricsContext', () => { - const mockLogger: MeasureLogger = { - info: jest.fn(), - error: jest.fn(), - warn: jest.fn(), - close: jest.fn(async () => {}), - logOperation: jest.fn() - } - - beforeEach(() => { - jest.clearAllMocks() - }) - - it('should create a new context with tracer', () => { - const metrics = newMetrics() - const ctx = new OpenTelemetryMetricsContext( - 'test', - tracer, - undefined, - undefined, - { op: 'create' }, - {}, - metrics, - mockLogger - ) - - expect(ctx).toBeDefined() - expect(ctx.metrics).toBe(metrics) - expect(ctx.logger).toBe(mockLogger) - }) - - it('should create child context with span', () => { - const metrics = newMetrics() - const ctx = new OpenTelemetryMetricsContext( - 'parent', - tracer, - context.active(), - undefined, - {}, - {}, - metrics, - mockLogger - ) - - const child = ctx.newChild('child', { op: 'child' }, { span: true }) - - expect(child).toBeDefined() - expect(child.parent).toBe(ctx) - }) - - it('should create child context without span when span is false', () => { - const metrics = newMetrics() - const ctx = new OpenTelemetryMetricsContext( - 'parent', - tracer, - context.active(), - undefined, - {}, - {}, - metrics, - mockLogger - ) - - const child = ctx.newChild('child', { op: 'child' }, { span: false }) - - expect(child).toBeDefined() - }) - - it('should execute async operation with context', async () => { - const metrics = newMetrics() - const ctx = new OpenTelemetryMetricsContext( - 'test', - tracer, - context.active(), - undefined, - {}, - {}, - metrics, - mockLogger - ) - - let executed = false - await ctx.with('operation', { op: 'test' }, async () => { - executed = true - await new Promise((resolve) => setTimeout(resolve, 10)) - }) - - expect(executed).toBe(true) - expect(metrics.measurements.operation).toBeDefined() - }) - - it('should execute sync operation with context', () => { - const metrics = newMetrics() - const ctx = new OpenTelemetryMetricsContext( - 'test', - tracer, - context.active(), - undefined, - {}, - {}, - metrics, - mockLogger - ) - - const result = ctx.withSync('operation', { op: 'test' }, () => { - return 42 - }) - - expect(result).toBe(42) - expect(metrics.measurements.operation).toBeDefined() - }) - - it('should handle errors in async operations', async () => { - const metrics = newMetrics() - const span = tracer.startSpan('test-span') - const ctx = new OpenTelemetryMetricsContext('test', tracer, context.active(), span, {}, {}, metrics, mockLogger) - - await expect( - ctx.with('operation', { op: 'test' }, async () => { - throw new Error('Test error') - }) - ).rejects.toThrow('Test error') - - expect(metrics.measurements.operation).toBeDefined() - }) - - it('should measure custom value with meter', () => { - const metrics = newMetrics() - const mockMeter = { - getCounter: jest.fn(() => ({ - counter: { record: jest.fn() }, - value: 0 - })) - } - - const ctx = new OpenTelemetryMetricsContext( - 'test', - tracer, - undefined, - undefined, - { op: 'test' }, - {}, - metrics, - mockLogger, - undefined, - undefined, - undefined, - mockMeter as any - ) - - ctx.measure('custom', 100) - - expect(mockMeter.getCounter).toHaveBeenCalledWith('custom') - }) - - it('should log info with OTLP logger', () => { - const metrics = newMetrics() - const mockOtlpLogger = { - emit: jest.fn() - } - - const ctx = new OpenTelemetryMetricsContext( - 'test', - tracer, - context.active(), - undefined, - { op: 'test' }, - {}, - metrics, - mockLogger, - undefined, - undefined, - mockOtlpLogger as any - ) - - ctx.info('Test message', { key: 'value' }) - - expect(mockOtlpLogger.emit).toHaveBeenCalled() - expect(mockLogger.info).toHaveBeenCalled() - }) - - it('should log error with OTLP logger', () => { - const metrics = newMetrics() - const mockOtlpLogger = { - emit: jest.fn() - } - - const ctx = new OpenTelemetryMetricsContext( - 'test', - tracer, - context.active(), - undefined, - { op: 'test' }, - {}, - metrics, - mockLogger, - undefined, - undefined, - mockOtlpLogger as any - ) - - ctx.error('Error message', { error: 'details' }) - - expect(mockOtlpLogger.emit).toHaveBeenCalled() - expect(mockLogger.error).toHaveBeenCalled() - }) - - it('should log warn with OTLP logger', () => { - const metrics = newMetrics() - const mockOtlpLogger = { - emit: jest.fn() - } - - const ctx = new OpenTelemetryMetricsContext( - 'test', - tracer, - context.active(), - undefined, - { op: 'test' }, - {}, - metrics, - mockLogger, - undefined, - undefined, - mockOtlpLogger as any - ) - - ctx.warn('Warning message', { warning: 'info' }) - - expect(mockOtlpLogger.emit).toHaveBeenCalled() - expect(mockLogger.warn).toHaveBeenCalled() - }) - - it('should extract metadata from context', () => { - const metrics = newMetrics() - const ctx = new OpenTelemetryMetricsContext( - 'test', - tracer, - context.active(), - undefined, - {}, - {}, - metrics, - mockLogger - ) - - const meta = ctx.extractMeta() - - expect(meta).toBeDefined() - expect(typeof meta).toBe('object') - }) - - it('should get params', () => { - const metrics = newMetrics() - const ctx = new OpenTelemetryMetricsContext( - 'test', - tracer, - undefined, - undefined, - { op: 'test', method: 'GET' }, - {}, - metrics, - mockLogger - ) - - const params = ctx.getParams() - - expect(params).toEqual({ op: 'test', method: 'GET' }) - }) - - it('should share contextData with children', () => { - const metrics = newMetrics() - const ctx = new OpenTelemetryMetricsContext( - 'parent', - tracer, - context.active(), - undefined, - {}, - {}, - metrics, - mockLogger - ) - ctx.contextData = { userId: '123' } - - const child = ctx.newChild('child', {}) - - expect(child.contextData).toBe(ctx.contextData) - }) - - it('should inherit params when inheritParams is true', async () => { - const metrics = newMetrics() - const ctx = new OpenTelemetryMetricsContext( - 'parent', - tracer, - context.active(), - undefined, - { parentKey: 'parentValue' }, - {}, - metrics, - mockLogger - ) - - await ctx.with( - 'child', - { childKey: 'childValue' }, - async (childCtx) => { - expect(childCtx.getParams()).toEqual({ childKey: 'childValue' }) - }, - {}, - { inheritParams: false } - ) - - await ctx.with( - 'child', - { childKey: 'childValue' }, - async (childCtx) => { - expect(childCtx.getParams()).toEqual({ - parentKey: 'parentValue', - childKey: 'childValue' - }) - }, - {}, - { inheritParams: true } - ) - }) - - it('should not call end() twice', () => { - const metrics = newMetrics() - const span = tracer.startSpan('test-span') - const spanEndSpy = jest.spyOn(span, 'end') - - const ctx = new OpenTelemetryMetricsContext('test', tracer, context.active(), span, {}, {}, metrics, mockLogger) - - ctx.end() - ctx.end() // Second call should be no-op - - expect(spanEndSpy).toHaveBeenCalledTimes(1) - }) - - it('should handle null return value', async () => { - const metrics = newMetrics() - const ctx = new OpenTelemetryMetricsContext( - 'test', - tracer, - context.active(), - undefined, - {}, - {}, - metrics, - mockLogger - ) - - const result = await ctx.with('operation', {}, () => null) - - expect(result).toBeUndefined() - }) - - it('should log operation when log option is true', async () => { - const metrics = newMetrics() - const ctx = new OpenTelemetryMetricsContext( - 'test', - tracer, - context.active(), - undefined, - {}, - {}, - metrics, - mockLogger - ) - - await ctx.with( - 'operation', - { op: 'test' }, - async () => { - await new Promise((resolve) => setTimeout(resolve, 10)) - }, - {}, - { log: true } - ) - - expect(mockLogger.logOperation).toHaveBeenCalledWith( - 'operation', - expect.any(Number), - expect.objectContaining({ op: 'test' }) - ) - }) - - it('should suppress tracing when span is disable', () => { - const metrics = newMetrics() - const ctx = new OpenTelemetryMetricsContext( - 'parent', - tracer, - context.active(), - undefined, - {}, - {}, - metrics, - mockLogger - ) - - const child = ctx.newChild('child', {}, { span: 'disable' }) - - expect(child).toBeDefined() - }) - - it('should set span attributes from params', () => { - const metrics = newMetrics() - - const ctx = new OpenTelemetryMetricsContext( - 'parent', - tracer, - context.active(), - undefined, - {}, - {}, - metrics, - mockLogger - ) - - // When creating a child with span: true, attributes are set on the child's span - const child = ctx.newChild( - 'child', - { key1: 'value1', key2: 'value2' }, - { span: true } - ) as OpenTelemetryMetricsContext - - // The child should have been created successfully - expect(child).toBeDefined() - expect(child.parent).toBe(ctx) - }) - }) -}) diff --git a/foundations/core/packages/measurements-otlp/src/index.ts b/foundations/core/packages/measurements-otlp/src/index.ts deleted file mode 100644 index 8b5ba81c39..0000000000 --- a/foundations/core/packages/measurements-otlp/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './telemetry' diff --git a/foundations/core/packages/measurements-otlp/src/telemetry.ts b/foundations/core/packages/measurements-otlp/src/telemetry.ts deleted file mode 100644 index c1770a01a3..0000000000 --- a/foundations/core/packages/measurements-otlp/src/telemetry.ts +++ /dev/null @@ -1,632 +0,0 @@ -import { - childMetrics, - consoleLogger, - MeasureContext, - MeasureMetricsContext, - newMetrics, - noParamsLogger, - nullPromise, - platformNow, - platformNowDiff, - updateMeasure, - type FullParamsType, - type MeasureLogger, - type Metrics, - type ParamsType, - type WithOptions -} from '@hcengineering/measurements' -import { - context, - metrics as otelMetrics, - propagation, - Span, - SpanStatusCode, - trace, - type Context, - type Gauge, - type Meter, - type Tracer -} from '@opentelemetry/api' -import { Logger, SeverityNumber } from '@opentelemetry/api-logs' -import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node' -import { suppressTracing } from '@opentelemetry/core' -import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http' -import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http' -import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http' -import { AWSXRayIdGenerator } from '@opentelemetry/id-generator-aws-xray' -import { CompressionAlgorithm } from '@opentelemetry/otlp-exporter-base' -import { resourceFromAttributes } from '@opentelemetry/resources' -import { BatchLogRecordProcessor, LoggerProvider } from '@opentelemetry/sdk-logs' -import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics' -import { NodeSDK } from '@opentelemetry/sdk-node' -import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-node' - -class MetricsContext { - counters = new Map() - constructor (readonly meter?: Meter) {} - - getCounter (name: string): { counter: Gauge, value: number } | undefined { - if (this.meter === undefined) { - return undefined - } - let counter = this.counters.get(name) - if (counter === undefined) { - counter = { counter: this.meter.createGauge(name), value: 0 } - this.counters.set(name, counter) - } - return counter - } -} - -/** - * @public - */ -export class OpenTelemetryMetricsContext implements MeasureContext { - private readonly name: string - private readonly params: ParamsType - - private readonly fullParams: FullParamsType | (() => FullParamsType) = {} - logger: MeasureLogger - metrics: Metrics - id?: string - - st = platformNow() - contextData: object = {} - isDone = false - doneTrace: string = '' - - private done (value?: number, override?: boolean): void { - if (!this.isDone) { - this.doneTrace = new Error().stack ?? '' - this.isDone = true - updateMeasure(this.metrics, this.st, this.params, this.fullParams, (spend) => {}, value, override) - this.span?.end() - } - } - - constructor ( - name: string, - readonly tracer: Tracer, - readonly context: Context | undefined, - readonly span: Span | undefined, - params: ParamsType, - fullParams: FullParamsType | (() => FullParamsType) = {}, - metrics: Metrics = newMetrics(), - logger?: MeasureLogger, - readonly parent?: MeasureContext, - readonly logParams?: ParamsType, - - readonly otlpLogger?: Logger, - readonly meter?: MetricsContext - ) { - this.name = name - this.params = params - this.fullParams = fullParams - this.metrics = metrics - this.metrics.namedParams = this.metrics.namedParams ?? {} - for (const [k, v] of Object.entries(params)) { - if (this.metrics.namedParams[k] !== v) { - this.metrics.namedParams[k] = v - } else { - this.metrics.namedParams[k] = '*' - } - } - - this.logger = logger ?? (this.logParams != null ? consoleLogger(this.logParams ?? {}) : noParamsLogger) - } - - measure (name: string, value: number, override?: boolean): void { - const cnt = this.meter?.getCounter(name) - if (cnt !== undefined) { - if (cnt.value !== value) { - cnt.counter.record(value, this.params) - cnt.value = value - } - } - } - - newChild ( - name: string, - params: ParamsType, - opt?: { - fullParams?: FullParamsType - logger?: MeasureLogger - span?: WithOptions['span'] // By default true - meta?: Record - } - ): MeasureContext { - let _span: Span | undefined - let childContext: Context | undefined - if (opt?.span === true || opt?.span === 'inherit') { - childContext = opt?.span === 'inherit' ? context.active() : (this.context ?? context.active()) - - if (opt.meta !== undefined && Object.keys(opt.meta).length > 0) { - // We need to set meta params - childContext = propagation.extract(childContext ?? context.active(), opt.meta) - } - _span = this.tracer.startSpan(name, undefined, childContext) - - const spanParams = [...Object.entries(params)] - for (const [k, v] of spanParams) { - _span?.setAttribute(k, v as any) - } - } - if (opt?.span === 'disable') { - childContext = suppressTracing(childContext ?? context.active()) - } - if (childContext !== undefined && _span !== undefined) { - childContext = trace.setSpan(childContext, _span) - } - - const result = new OpenTelemetryMetricsContext( - name, - this.tracer, - childContext, - _span, - params, - opt?.fullParams ?? {}, - childMetrics(this.metrics, [name]), - opt?.logger ?? this.logger, - this, - this.logParams, - this.otlpLogger, - this.meter - ) - result.id = this.id - result.contextData = this.contextData - return result - } - - extractMeta (): Record { - const headers: Record = {} - if (this.context !== undefined) { - propagation.inject(this.context, headers) - } - return headers - } - - with( - name: string, - params: ParamsType, - op: (ctx: MeasureContext) => T | Promise, - fullParams?: ParamsType | (() => FullParamsType), - opt?: WithOptions - ): Promise { - const c = this.newChild(name, opt?.inheritParams === true ? { ...this.params, ...params } : params, { - fullParams, - logger: this.logger, - span: opt?.span ?? true, - meta: opt?.meta - }) - let needFinally = true - try { - const _context = (c as OpenTelemetryMetricsContext).context - - const span = (c as OpenTelemetryMetricsContext).span - - const value = _context !== undefined ? context.with(_context, () => op(c)) : op(c) - if (value instanceof Promise) { - needFinally = false - if (span !== undefined && opt?.suspendErrors !== true) { - void value.catch((err) => { - span?.recordException(err) - span?.setStatus({ - code: SpanStatusCode.ERROR, - message: err.message - }) - }) - } - return value.finally(() => { - if (span !== undefined) { - const fParams = typeof fullParams === 'function' ? fullParams() : fullParams - const spanParams = [...Object.entries(params), ...Object.entries(fParams ?? {})] - for (const [k, v] of spanParams) { - span?.setAttribute(k, typeof v === 'object' ? JSON.stringify(v) : v) - } - } - c.end() - if (opt?.log === true) { - this.logger.logOperation(name, platformNowDiff((c as OpenTelemetryMetricsContext).st), { - ...params, - ...fullParams - }) - } - }) - } else { - if (value == null) { - return nullPromise as Promise - } - return Promise.resolve(value) - } - } finally { - if (needFinally) { - c.end() - } - } - } - - withSync( - name: string, - params: ParamsType, - op: (ctx: MeasureContext) => T, - fullParams?: ParamsType | (() => FullParamsType), - opt?: WithOptions - ): T { - const c = this.newChild(name, params, { fullParams, logger: this.logger, span: opt?.span ?? true }) - const _context = (c as OpenTelemetryMetricsContext).context - try { - return _context !== undefined ? context.with(_context, () => op(c)) : op(c) - } finally { - c.end() - } - } - - error (message: string, args?: Record): void { - if (this.otlpLogger !== undefined) { - this.otlpLogger.emit({ - severityNumber: SeverityNumber.ERROR, - severityText: 'error', - context: this.context, - body: message, - attributes: { - 'service.name': sdkServiceName, - ...(args ?? {}) - } - }) - } - this.logger.error(message, { ...this.params, ...args, ...(this.logParams ?? {}) }) - } - - info (message: string, args?: Record): void { - if (this.otlpLogger !== undefined) { - this.otlpLogger.emit({ - context: this.context, - severityNumber: SeverityNumber.INFO, - severityText: 'info', - body: message, - attributes: { - 'service.name': sdkServiceName, - ...(args ?? {}) - } - }) - } - this.logger.info(message, { ...this.params, ...args, ...(this.logParams ?? {}) }) - } - - warn (message: string, args?: Record): void { - if (this.otlpLogger !== undefined) { - this.otlpLogger.emit({ - severityNumber: SeverityNumber.WARN, - severityText: 'warn', - context: this.context, - body: message, - attributes: { - 'service.name': sdkServiceName, - ...(args ?? {}) - } - }) - } - this.logger.warn(message, { ...this.params, ...args, ...(this.logParams ?? {}) }) - } - - end (): void { - this.done() - } - - getParams (): ParamsType { - return this.params - } -} - -/** - * Parse W3C baggage header format to Record - * - * W3C baggage format: "key1=value1,key2=value2;property=value,key3=value3" - * Returns only the key-value pairs, ignoring properties - */ -function parseBaggage (baggageHeader?: string): Record { - if (baggageHeader == null || typeof baggageHeader !== 'string') { - return {} - } - - const result: Record = {} - - // Split by comma to get individual baggage members - const members = baggageHeader.split(',') - - for (const member of members) { - const trimmedMember = member.trim() - if (trimmedMember === '') continue - - // Split by semicolon to separate key=value from properties - const parts = trimmedMember.split(';') - const keyValuePart = parts[0]?.trim() - - if (keyValuePart == null) continue - - // Split by equals to get key and value - const equalIndex = keyValuePart.indexOf('=') - if (equalIndex === -1) continue - - const key = keyValuePart.substring(0, equalIndex).trim() - const value = keyValuePart.substring(equalIndex + 1).trim() - - if (key != null) { - // URL decode the key and value - try { - const decodedKey = decodeURIComponent(key) - const decodedValue = decodeURIComponent(value) - result[decodedKey] = decodedValue - } catch (error) { - // If decoding fails, use the original values - result[key] = value - } - } - } - - return result -} - -let sdk: NodeSDK | undefined -let sdkServiceName: string | undefined -let sdkServiceVersion: string | undefined -let loggerProvider: LoggerProvider | undefined - -export function initOpenTelemetrySDK (serviceName: string, version: string): boolean { - if (sdk !== undefined) { - return true - } - process.env.OTEL_SERVICE_NAME = serviceName - process.env.OTEL_SERVICE_VERSION = version - - sdkServiceName = serviceName - sdkServiceVersion = version - const tracesUrl = getTracesUrl() - - if (tracesUrl === undefined) { - return false - } - - const traceHeaders = parseTraceExporterHeaders() - - const exporter = new OTLPTraceExporter({ - url: tracesUrl, - headers: traceHeaders, - compression: - (process.env.OTEL_EXPORTER_OTLP_COMPRESSION as CompressionAlgorithm) ?? - (process.env.OTEL_EXPORTER_OTLP_TRACES_COMPRESSION as CompressionAlgorithm) ?? - CompressionAlgorithm.GZIP, - keepAlive: true - }) - - const batchSpanProcessor = new BatchSpanProcessor(exporter, { - maxExportBatchSize: parseInt(process.env.OTEL_EXPORTER_OTLP_TRACES_MAX_EXPORT_BATCH_SIZE ?? '1000'), - maxQueueSize: parseInt(process.env.OTEL_EXPORTER_OTLP_TRACES_MAX_QUEUE_SIZE ?? '1000') - }) - - // Logs - const logsEndpoint = getLogsUrl() - const logHeaders = parseLogsExporterHeaders() - const logExporter = new OTLPLogExporter({ - url: logsEndpoint, - headers: logHeaders, - compression: - (process.env.OTEL_EXPORTER_OTLP_COMPRESSION as CompressionAlgorithm) ?? - (process.env.OTEL_EXPORTER_OTLP_LOGS_COMPRESSION as CompressionAlgorithm) ?? - CompressionAlgorithm.GZIP, - keepAlive: true - }) - - const batchLogProcessor = new BatchLogRecordProcessor(logExporter, { - maxExportBatchSize: parseInt(process.env.OTEL_EXPORTER_OTLP_LOGS_MAX_EXPORT_BATCH_SIZE ?? '1000'), - maxQueueSize: parseInt(process.env.OTEL_EXPORTER_OTLP_LOGS_MAX_QUEUE_SIZE ?? '1000') - }) - - // Metrics - - const metricsUrl = getMetricsUrl() - const metricsHeaders = parseMetricsExporterHeaders() - const metricsExporter = new OTLPMetricExporter({ - url: metricsUrl, - headers: metricsHeaders - }) - const metricReader = new PeriodicExportingMetricReader({ - exporter: metricsExporter, - exportIntervalMillis: 15000 - }) - - // SDK - - sdk = new NodeSDK({ - spanProcessors: [batchSpanProcessor], - serviceName, - traceExporter: exporter, - resource: resourceFromAttributes({ - 'service-name': serviceName, - 'service-version': version ?? '0.7', - 'deployment-environment': process.env.OTEL_ENVIRONMENT - }), - instrumentations: [getNodeAutoInstrumentations()], - idGenerator: new AWSXRayIdGenerator(), - logRecordProcessors: [batchLogProcessor], - metricReader - }) - - sdk.start() - - loggerProvider = new LoggerProvider({ - processors: [batchLogProcessor] - }) - - // Graceful shutdown - process.on('SIGTERM', () => { - sdk - ?.shutdown() - .then(() => { - console.log('Tracing terminated') - }) - .catch((error) => { - console.log('Error terminating tracing', error) - }) - .finally(() => process.exit(0)) - }) - console.log('Using open telemetry metrics context', { - traceEndpoint: tracesUrl, - tracerHeadersSet: Array.from(Object.keys(traceHeaders)), - logsEndpoint, - logHeadersSet: Array.from(Object.keys(logHeaders)) - }) - return true -} - -export function reportOTELError (error: Error, attributes?: Record): void { - if (sdkServiceName !== undefined && sdkServiceVersion !== undefined && loggerProvider !== undefined) { - const otlpLogger = loggerProvider?.getLogger(sdkServiceName, sdkServiceVersion) - otlpLogger?.emit({ - severityNumber: SeverityNumber.ERROR, - severityText: 'error', - body: error.message, - context: context.active(), - attributes: { - ...attributes, - 'service.name': sdkServiceName, - 'service.version': sdkServiceVersion, - 'error.stack': error.stack - } - }) - } -} - -export function reportOTEL ( - severity: 'info' | 'warning', - message: string, - time: number, - attributes?: Record -): void { - if (sdkServiceName !== undefined && sdkServiceVersion !== undefined && loggerProvider !== undefined) { - const otlpLogger = loggerProvider?.getLogger(sdkServiceName, sdkServiceVersion) - otlpLogger?.emit({ - severityNumber: severity === 'info' ? SeverityNumber.INFO : SeverityNumber.WARN, - body: message, - severityText: severity, - timestamp: time, - context: context.active(), - attributes: { - ...attributes, - 'service.name': sdkServiceName, - 'service.version': sdkServiceVersion - } - }) - } -} - -export function createOpenTelemetryMetricsContext ( - name: string, - params: ParamsType, - fullParams: FullParamsType | (() => FullParamsType) = {}, - metrics: Metrics = newMetrics(), - logger?: MeasureLogger, - version?: string -): MeasureContext { - if (!initOpenTelemetrySDK(name, version ?? '')) { - console.warn('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT is not set, OpenTelemetry metrics will not be sent') - return new MeasureMetricsContext(name, params, fullParams, metrics, logger) - } - - // Traces - - const tracer = trace.getTracer(name) - - const otlpLogger = - process.env.OTEL_LOGGER_ENABLED === 'true' ? loggerProvider?.getLogger(sdkServiceName ?? name, version) : undefined - - const meter = otelMetrics.getMeter(name, version) - - const ctx = new OpenTelemetryMetricsContext( - name, - tracer, - undefined, - undefined, - params, - fullParams, - metrics, - logger, - undefined, - undefined, - otlpLogger, - new MetricsContext(meter) - ) - return ctx -} -function parseTraceExporterHeaders (): Record { - const headers: Record = parseBaggage(process.env.OTEL_EXPORTER_OTLP_HEADERS) ?? {} - - if (process.env.OTEL_EXPORTER_OTLP_TRACES_HEADERS !== undefined) { - const extraHeaders = parseBaggage(process.env.OTEL_EXPORTER_OTLP_TRACES_HEADERS) - for (const [key, value] of Object.entries(extraHeaders)) { - headers[key] = value - } - } - return headers -} - -function getTracesUrl (): string | undefined { - let tracesUrl = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ?? process.env.OTEL_EXPORTER_OTLP_ENDPOINT - - if (tracesUrl !== undefined && !tracesUrl.endsWith('/v1/traces')) { - if (tracesUrl.endsWith('/')) { - tracesUrl += 'v1/traces' - } else { - tracesUrl += '/v1/traces' - } - } - return tracesUrl -} - -function getLogsUrl (): string | undefined { - let logsUrl = process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT ?? process.env.OTEL_EXPORTER_OTLP_ENDPOINT - - if (logsUrl !== undefined && !logsUrl.endsWith('/v1/logs')) { - if (logsUrl.endsWith('/')) { - logsUrl += 'v1/logs' - } else { - logsUrl += '/v1/logs' - } - } - return logsUrl -} -function parseLogsExporterHeaders (): Record { - const headers: Record = parseBaggage(process.env.OTEL_EXPORTER_OTLP_HEADERS) ?? {} - - if (process.env.OTEL_EXPORTER_OTLP_LOGS_HEADERS !== undefined) { - const extraHeaders = parseBaggage(process.env.OTEL_EXPORTER_OTLP_LOGS_HEADERS) - for (const [key, value] of Object.entries(extraHeaders)) { - headers[key] = value - } - } - return headers -} - -function getMetricsUrl (): string | undefined { - let metricsUrl = process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT ?? process.env.OTEL_EXPORTER_OTLP_ENDPOINT - - if (metricsUrl !== undefined && !metricsUrl.endsWith('/v1/metrics')) { - if (metricsUrl.endsWith('/')) { - metricsUrl += 'v1/metrics' - } else { - metricsUrl += '/v1/metrics' - } - } - return metricsUrl -} -function parseMetricsExporterHeaders (): Record { - const headers: Record = parseBaggage(process.env.OTEL_EXPORTER_OTLP_HEADERS) ?? {} - - if (process.env.OTEL_EXPORTER_OTLP_METRICS_HEADERS !== undefined) { - const extraHeaders = parseBaggage(process.env.OTEL_EXPORTER_OTLP_METRICS_HEADERS) - for (const [key, value] of Object.entries(extraHeaders)) { - headers[key] = value - } - } - return headers -} diff --git a/foundations/core/packages/measurements-otlp/tsconfig.json b/foundations/core/packages/measurements-otlp/tsconfig.json deleted file mode 100644 index b5ae22f6e4..0000000000 --- a/foundations/core/packages/measurements-otlp/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/foundations/core/packages/measurements/.eslintrc.js b/foundations/core/packages/measurements/.eslintrc.js deleted file mode 100644 index 72235dc283..0000000000 --- a/foundations/core/packages/measurements/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/foundations/core/packages/measurements/.npmignore b/foundations/core/packages/measurements/.npmignore deleted file mode 100644 index 9b083cb0c4..0000000000 --- a/foundations/core/packages/measurements/.npmignore +++ /dev/null @@ -1,8 +0,0 @@ -* -!/lib/** -!/types/** -!/src/** -!CHANGELOG.md -/lib/**/__test__/ -/types/**/__test__/ -/src/**/__test__/ \ No newline at end of file diff --git a/foundations/core/packages/measurements/CHANGELOG.json b/foundations/core/packages/measurements/CHANGELOG.json deleted file mode 100644 index 58fb5458b0..0000000000 --- a/foundations/core/packages/measurements/CHANGELOG.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "name": "@hcengineering/measurements", - "entries": [ - { - "version": "0.7.18", - "tag": "@hcengineering/measurements_v0.7.18", - "date": "Mon, 27 Oct 2025 15:24:19 GMT", - "comments": { - "patch": [ - { - "comment": "Fix withContext to use options" - } - ] - } - }, - { - "version": "0.7.14", - "tag": "@hcengineering/measurements_v0.7.14", - "date": "Mon, 27 Oct 2025 04:08:57 GMT", - "comments": { - "patch": [ - { - "comment": "Allow to suspend errors on change" - } - ] - } - }, - { - "version": "0.7.13", - "tag": "@hcengineering/measurements_v0.7.13", - "date": "Tue, 14 Oct 2025 03:02:36 GMT", - "comments": { - "patch": [ - { - "comment": "Remove zero value measure with override values" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/platform-rig\" from `^0.7.18` to `0.7.19`" - } - ] - } - }, - { - "version": "0.7.12", - "tag": "@hcengineering/measurements_v0.7.12", - "date": "Sat, 11 Oct 2025 19:18:56 GMT", - "comments": { - "patch": [ - { - "comment": "rollback eslint" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/platform-rig\" from `^0.7.14` to `0.7.15`" - } - ] - } - }, - { - "version": "0.7.11", - "tag": "@hcengineering/measurements_v0.7.11", - "date": "Sat, 11 Oct 2025 17:58:53 GMT", - "comments": { - "none": [ - { - "comment": "Add tests" - } - ], - "patch": [ - { - "comment": "Fix eslint deps" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/platform-rig\" from `^0.7.12` to `0.7.13`" - } - ] - } - }, - { - "version": "0.7.10", - "tag": "@hcengineering/measurements_v0.7.10", - "date": "Fri, 10 Oct 2025 12:32:59 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/platform-rig\" from `^0.7.10` to `0.7.11`" - } - ] - } - } - ] -} diff --git a/foundations/core/packages/measurements/CHANGELOG.md b/foundations/core/packages/measurements/CHANGELOG.md deleted file mode 100644 index 70a571fc57..0000000000 --- a/foundations/core/packages/measurements/CHANGELOG.md +++ /dev/null @@ -1,44 +0,0 @@ -# Change Log - @hcengineering/measurements - -This log was last generated on Mon, 27 Oct 2025 15:24:19 GMT and should not be manually modified. - -## 0.7.18 -Mon, 27 Oct 2025 15:24:19 GMT - -### Patches - -- Fix withContext to use options - -## 0.7.14 -Mon, 27 Oct 2025 04:08:57 GMT - -### Patches - -- Allow to suspend errors on change - -## 0.7.13 -Tue, 14 Oct 2025 03:02:36 GMT - -### Patches - -- Remove zero value measure with override values - -## 0.7.12 -Sat, 11 Oct 2025 19:18:56 GMT - -### Patches - -- rollback eslint - -## 0.7.11 -Sat, 11 Oct 2025 17:58:53 GMT - -### Patches - -- Fix eslint deps - -## 0.7.10 -Fri, 10 Oct 2025 12:32:59 GMT - -_Initial release_ - diff --git a/foundations/core/packages/measurements/config/rig.json b/foundations/core/packages/measurements/config/rig.json deleted file mode 100644 index 0110930f55..0000000000 --- a/foundations/core/packages/measurements/config/rig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig" -} diff --git a/foundations/core/packages/measurements/jest.config.js b/foundations/core/packages/measurements/jest.config.js deleted file mode 100644 index 2cfd408b67..0000000000 --- a/foundations/core/packages/measurements/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ["./src"], - coverageReporters: ["text-summary", "html"] -} diff --git a/foundations/core/packages/measurements/package.json b/foundations/core/packages/measurements/package.json deleted file mode 100644 index bdcdd1768c..0000000000 --- a/foundations/core/packages/measurements/package.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "name": "@hcengineering/measurements", - "version": "0.7.18", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "files": [ - "lib/**/*", - "!lib/**/__test__/**", - "types/**/*", - "!types/**/__test__/**", - "src/**/*", - "!src/**/__test__/**", - "README.md", - "CHANGELOG.md" - ], - "author": "Anticrm Platform Contributors", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "build:watch": "compile", - "test": "jest --passWithNoTests --silent", - "format": "format src", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "workspace:^0.7.19", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "@typescript-eslint/parser": "^6.21.0", - "eslint": "^8.54.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-node": "^11.1.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0" - }, - "repository": "https://github.com/hcengineering/platform", - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.js" - } - }, - "publishConfig": { - "access": "public" - } -} diff --git a/foundations/core/packages/measurements/src/__tests__/context.test.ts b/foundations/core/packages/measurements/src/__tests__/context.test.ts deleted file mode 100644 index d59d0a4945..0000000000 --- a/foundations/core/packages/measurements/src/__tests__/context.test.ts +++ /dev/null @@ -1,467 +0,0 @@ -import { - MeasureMetricsContext, - NoMetricsContext, - consoleLogger, - noParamsLogger, - withContext, - setOperationLogProfiling, - registerOperationLog, - updateOperationLog, - addOperation -} from '../context' -import { newMetrics } from '../metrics' -import type { MeasureLogger, MeasureContext } from '../types' - -describe('context', () => { - describe('consoleLogger', () => { - it('should create a logger with params', () => { - const logger = consoleLogger({ service: 'test' }) - - expect(logger).toBeDefined() - expect(typeof logger.info).toBe('function') - expect(typeof logger.error).toBe('function') - expect(typeof logger.warn).toBe('function') - expect(typeof logger.close).toBe('function') - }) - - it('should log info messages', () => { - const consoleSpy = jest.spyOn(console, 'info').mockImplementation() - const logger = consoleLogger({ service: 'test' }) - - logger.info('Test message', { key: 'value' }) - - expect(consoleSpy).toHaveBeenCalled() - consoleSpy.mockRestore() - }) - - it('should log error messages', () => { - const consoleSpy = jest.spyOn(console, 'error').mockImplementation() - const logger = consoleLogger({ service: 'test' }) - - logger.error('Error message', { error: 'details' }) - - expect(consoleSpy).toHaveBeenCalled() - consoleSpy.mockRestore() - }) - - it('should log warn messages', () => { - const consoleSpy = jest.spyOn(console, 'warn').mockImplementation() - const logger = consoleLogger({ service: 'test' }) - - logger.warn('Warning message', { warning: 'info' }) - - expect(consoleSpy).toHaveBeenCalled() - consoleSpy.mockRestore() - }) - - it('should handle errors in params', () => { - const consoleSpy = jest.spyOn(console, 'error').mockImplementation() - const logger = consoleLogger({}) - const error = new Error('Test error') - - logger.error('Error occurred', { error }) - - expect(consoleSpy).toHaveBeenCalled() - const call = consoleSpy.mock.calls[0] - expect(call[0]).toBe('Error occurred') - consoleSpy.mockRestore() - }) - }) - - describe('MeasureMetricsContext', () => { - let logger: MeasureLogger - - beforeEach(() => { - logger = noParamsLogger - }) - - it('should create a new context', () => { - const metrics = newMetrics() - const ctx = new MeasureMetricsContext('test', { op: 'create' }, {}, metrics, logger) - - expect(ctx).toBeDefined() - expect(ctx.metrics).toBe(metrics) - expect(ctx.logger).toBe(logger) - }) - - it('should measure operation duration', async () => { - const metrics = newMetrics() - const ctx = new MeasureMetricsContext('test', { op: 'create' }, {}, metrics, logger) - - await new Promise((resolve) => setTimeout(resolve, 50)) - ctx.end() - - expect(metrics.operations).toBe(1) - expect(metrics.value).toBeGreaterThan(40) - }) - - it('should create child context', () => { - const metrics = newMetrics() - const ctx = new MeasureMetricsContext('parent', { op: 'parent' }, {}, metrics, logger) - const child = ctx.newChild('child', { op: 'child' }) - - expect(child).toBeDefined() - expect(child.parent).toBe(ctx) - }) - - it('should execute async operation with context', async () => { - const metrics = newMetrics() - const ctx = new MeasureMetricsContext('test', {}, {}, metrics, logger) - - let executed = false - await ctx.with('operation', { op: 'test' }, async () => { - executed = true - await new Promise((resolve) => setTimeout(resolve, 10)) - }) - - expect(executed).toBe(true) - expect(metrics.measurements.operation).toBeDefined() - }) - - it('should execute sync operation with context', () => { - const metrics = newMetrics() - const ctx = new MeasureMetricsContext('test', {}, {}, metrics, logger) - - const result = ctx.withSync('operation', { op: 'test' }, () => { - return 42 - }) - - expect(result).toBe(42) - expect(metrics.measurements.operation).toBeDefined() - }) - - it('should measure custom value', () => { - const metrics = newMetrics() - const ctx = new MeasureMetricsContext('test', {}, {}, metrics, logger) - - ctx.measure('custom', 100) - - expect(metrics.measurements['#custom']).toBeDefined() - expect(metrics.measurements['#custom'].value).toBe(100) - }) - - it('should log info messages', () => { - const mockLogger: MeasureLogger = { - info: jest.fn(), - error: jest.fn(), - warn: jest.fn(), - close: jest.fn(async () => {}), - logOperation: jest.fn() - } - - const ctx = new MeasureMetricsContext('test', { op: 'test' }, {}, newMetrics(), mockLogger) - ctx.info('Test message', { key: 'value' }) - - expect(mockLogger.info).toHaveBeenCalledWith( - 'Test message', - expect.objectContaining({ key: 'value', op: 'test' }) - ) - }) - - it('should log error messages', () => { - const mockLogger: MeasureLogger = { - info: jest.fn(), - error: jest.fn(), - warn: jest.fn(), - close: jest.fn(async () => {}), - logOperation: jest.fn() - } - - const ctx = new MeasureMetricsContext('test', { op: 'test' }, {}, newMetrics(), mockLogger) - ctx.error('Error message', { error: 'details' }) - - expect(mockLogger.error).toHaveBeenCalledWith( - 'Error message', - expect.objectContaining({ error: 'details', op: 'test' }) - ) - }) - - it('should log warn messages', () => { - const mockLogger: MeasureLogger = { - info: jest.fn(), - error: jest.fn(), - warn: jest.fn(), - close: jest.fn(async () => {}), - logOperation: jest.fn() - } - - const ctx = new MeasureMetricsContext('test', { op: 'test' }, {}, newMetrics(), mockLogger) - ctx.warn('Warning message', { warning: 'info' }) - - expect(mockLogger.warn).toHaveBeenCalledWith( - 'Warning message', - expect.objectContaining({ warning: 'info', op: 'test' }) - ) - }) - - it('should get params', () => { - const ctx = new MeasureMetricsContext('test', { op: 'test', method: 'GET' }, {}, newMetrics(), logger) - const params = ctx.getParams() - - expect(params).toEqual({ op: 'test', method: 'GET' }) - }) - - it('should share contextData with children', () => { - const metrics = newMetrics() - const ctx = new MeasureMetricsContext('parent', {}, {}, metrics, logger) - ctx.contextData = { userId: '123' } - - const child = ctx.newChild('child', {}) - - expect(child.contextData).toBe(ctx.contextData) - }) - - it('should handle named parameters', () => { - const metrics = newMetrics() - const r = new MeasureMetricsContext('test', { method: 'GET' }, {}, metrics, logger) - expect(r).toBeDefined() - expect(metrics.namedParams.method).toBe('GET') - }) - - it('should update named parameters on multiple contexts', () => { - const metrics = newMetrics() - const ctx1 = new MeasureMetricsContext('test1', { method: 'GET' }, {}, metrics, logger) - expect(ctx1).toBeDefined() - expect(metrics.namedParams.method).toBe('GET') - - // Create another context with different value for same param - const ctx2 = new MeasureMetricsContext('test2', { method: 'POST' }, {}, metrics, logger) - expect(ctx2).toBeDefined() - - // The second context will see existing value is different, so it will update to '*' - // But this happens within the constructor logic - expect(metrics.namedParams.method).toBe('POST') - }) - }) - - describe('NoMetricsContext', () => { - it('should create a no-op context', () => { - const ctx = new NoMetricsContext() - - expect(ctx).toBeDefined() - expect(ctx.logger).toBeDefined() - }) - - it('should execute operations without measuring', async () => { - const ctx = new NoMetricsContext() - - let executed = false - await ctx.with('operation', {}, async () => { - executed = true - }) - - expect(executed).toBe(true) - }) - - it('should create child contexts', () => { - const ctx = new NoMetricsContext() - const child = ctx.newChild('child', {}) - - expect(child).toBeDefined() - expect(child).toBeInstanceOf(NoMetricsContext) - }) - - it('should handle measure calls without error', () => { - const ctx = new NoMetricsContext() - - expect(() => { - ctx.measure('test', 100) - }).not.toThrow() - }) - - it('should handle end calls without error', () => { - const ctx = new NoMetricsContext() - - expect(() => { - ctx.end() - }).not.toThrow() - }) - - it('should return empty params', () => { - const ctx = new NoMetricsContext() - const params = ctx.getParams() - - expect(params).toEqual({}) - }) - }) - - describe('withContext decorator', () => { - it('should wrap method with context', async () => { - class TestClass { - @withContext('testOperation', { service: 'test' }) - async testMethod (ctx: MeasureContext, value: number): Promise { - return value * 2 - } - } - - const instance = new TestClass() - const metrics = newMetrics() - const ctx = new MeasureMetricsContext('root', {}, {}, metrics, noParamsLogger) - - const result = await instance.testMethod(ctx, 21) - - expect(result).toBe(42) - expect(metrics.measurements.testOperation).toBeDefined() - }) - }) - - describe('operation log profiling', () => { - beforeEach(() => { - setOperationLogProfiling(false) - }) - - afterEach(() => { - setOperationLogProfiling(false) - }) - - it('should register operation log when profiling enabled', () => { - setOperationLogProfiling(true) - const metrics = newMetrics() - const ctx = new MeasureMetricsContext('test', {}, {}, metrics, noParamsLogger) - - const { opLogMetrics, op } = registerOperationLog(ctx) - - expect(opLogMetrics).toBe(metrics) - expect(op).toBeDefined() - expect(ctx.id).toBeDefined() - }) - - it('should not register operation log when profiling disabled', () => { - setOperationLogProfiling(false) - const metrics = newMetrics() - const ctx = new MeasureMetricsContext('test', {}, {}, metrics, noParamsLogger) - - const { opLogMetrics, op } = registerOperationLog(ctx) - - expect(opLogMetrics).toBeUndefined() - expect(op).toBeUndefined() - }) - - it('should update operation log', () => { - setOperationLogProfiling(true) - const metrics = newMetrics() - const ctx = new MeasureMetricsContext('test', {}, {}, metrics, noParamsLogger) - - const { opLogMetrics, op } = registerOperationLog(ctx) - updateOperationLog(opLogMetrics, op) - - expect(op?.end).toBeGreaterThan(0) - }) - - it('should add operation to log', async () => { - setOperationLogProfiling(true) - const metrics = newMetrics() - const ctx = new MeasureMetricsContext('test', {}, {}, metrics, noParamsLogger) - - registerOperationLog(ctx) - - let executed = false - await addOperation(ctx, 'asyncOp', { op: 'test' }, async () => { - executed = true - }) - - expect(executed).toBe(true) - expect(metrics.opLog).toBeDefined() - }) - - it('should limit operation log entries', () => { - setOperationLogProfiling(true) - const metrics = newMetrics() - const ctx = new MeasureMetricsContext('test', {}, {}, metrics, noParamsLogger) - - registerOperationLog(ctx) - - // Create many operation log entries - if (metrics.opLog != null && ctx.id != null) { - for (let i = 0; i < 50; i++) { - metrics.opLog[ctx.id].ops.push({ - op: `test${i}`, - start: i, - end: i + 1, - params: {} - }) - } - - const op = metrics.opLog[ctx.id] - updateOperationLog(metrics, op) - - expect(Object.keys(metrics.opLog).length).toBeLessThanOrEqual(31) - } - }) - }) - - describe('extractMeta', () => { - it('should return empty metadata', () => { - const ctx = new MeasureMetricsContext('test', {}, {}, newMetrics(), noParamsLogger) - const meta = ctx.extractMeta() - - expect(meta).toEqual({}) - }) - }) - - describe('async operations', () => { - it('should handle promise rejection', async () => { - const metrics = newMetrics() - const ctx = new MeasureMetricsContext('test', {}, {}, metrics, noParamsLogger) - - await expect( - ctx.with('operation', { op: 'test' }, async () => { - throw new Error('Test error') - }) - ).rejects.toThrow('Test error') - - expect(metrics.measurements.operation).toBeDefined() - }) - - it('should return null promise for null sync result', async () => { - const metrics = newMetrics() - const ctx = new MeasureMetricsContext('test', {}, {}, metrics, noParamsLogger) - - const result = await ctx.with('operation', { op: 'test' }, () => { - return null - }) - - expect(result).toBeUndefined() - }) - - it('should handle sync return value', async () => { - const metrics = newMetrics() - const ctx = new MeasureMetricsContext('test', {}, {}, metrics, noParamsLogger) - - const result = await ctx.with('operation', { op: 'test' }, () => { - return 42 - }) - - expect(result).toBe(42) - }) - - it('should log operation when log option is true', async () => { - const mockLogger: MeasureLogger = { - info: jest.fn(), - error: jest.fn(), - warn: jest.fn(), - close: jest.fn(async () => {}), - logOperation: jest.fn() - } - - const metrics = newMetrics() - const ctx = new MeasureMetricsContext('test', {}, {}, metrics, mockLogger) - - await ctx.with( - 'operation', - { op: 'test' }, - async () => { - await new Promise((resolve) => setTimeout(resolve, 10)) - }, - {}, - { log: true } - ) - - expect(mockLogger.logOperation).toHaveBeenCalledWith( - 'operation', - expect.any(Number), - expect.objectContaining({ op: 'test' }) - ) - }) - }) -}) diff --git a/foundations/core/packages/measurements/src/__tests__/index.test.ts b/foundations/core/packages/measurements/src/__tests__/index.test.ts deleted file mode 100644 index d701edd4e2..0000000000 --- a/foundations/core/packages/measurements/src/__tests__/index.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { platformNow, platformNowDiff } from '../index' - -describe('index', () => { - describe('platformNow', () => { - it('should return a positive number', () => { - const now = platformNow() - expect(typeof now).toBe('number') - expect(now).toBeGreaterThan(0) - }) - - it('should return increasing values over time', async () => { - const first = platformNow() - await new Promise((resolve) => setTimeout(resolve, 10)) - const second = platformNow() - expect(second).toBeGreaterThan(first) - }) - }) - - describe('platformNowDiff', () => { - it('should calculate the difference between timestamps', async () => { - const start = platformNow() - await new Promise((resolve) => setTimeout(resolve, 50)) - const diff = platformNowDiff(start) - - expect(diff).toBeGreaterThan(40) - expect(diff).toBeLessThan(100) - }) - - it('should round to 2 decimal places', async () => { - const start = platformNow() - await new Promise((resolve) => setTimeout(resolve, 10)) - const diff = platformNowDiff(start) - - // Check that it has at most 2 decimal places - const decimalPlaces = (diff.toString().split('.')[1] ?? '').length - expect(decimalPlaces).toBeLessThanOrEqual(2) - }) - - it('should handle very small differences', () => { - const start = platformNow() - const diff = platformNowDiff(start) - - expect(diff).toBeGreaterThanOrEqual(0) - expect(typeof diff).toBe('number') - }) - }) -}) diff --git a/foundations/core/packages/measurements/src/__tests__/metrics.test.ts b/foundations/core/packages/measurements/src/__tests__/metrics.test.ts deleted file mode 100644 index e10136fa72..0000000000 --- a/foundations/core/packages/measurements/src/__tests__/metrics.test.ts +++ /dev/null @@ -1,297 +0,0 @@ -import { - newMetrics, - measure, - childMetrics, - metricsAggregate, - metricsToString, - metricsToJson, - metricsToRows, - updateMeasure -} from '../metrics' -import { platformNow } from '../index' - -describe('metrics', () => { - describe('newMetrics', () => { - it('should create a new metrics object with default values', () => { - const metrics = newMetrics() - - expect(metrics.operations).toBe(0) - expect(metrics.value).toBe(0) - expect(metrics.measurements).toEqual({}) - expect(metrics.params).toEqual({}) - expect(metrics.namedParams).toEqual({}) - }) - }) - - describe('measure', () => { - it('should measure operation duration', async () => { - const metrics = newMetrics() - const done = measure(metrics, { operation: 'test' }) - - await new Promise((resolve) => setTimeout(resolve, 50)) - done() - - expect(metrics.operations).toBe(1) - expect(metrics.value).toBeGreaterThan(40) - }) - - it('should call endOp callback with duration', async () => { - const metrics = newMetrics() - let capturedSpend = 0 - const done = measure(metrics, { operation: 'test' }, {}, (spend) => { - capturedSpend = spend - }) - - await new Promise((resolve) => setTimeout(resolve, 50)) - done() - - expect(capturedSpend).toBeGreaterThan(40) - }) - - it('should handle multiple operations', async () => { - const metrics = newMetrics() - - const done1 = measure(metrics, { operation: 'test' }) - await new Promise((resolve) => setTimeout(resolve, 20)) - done1() - - const done2 = measure(metrics, { operation: 'test' }) - await new Promise((resolve) => setTimeout(resolve, 20)) - done2() - - expect(metrics.operations).toBe(2) - expect(metrics.value).toBeGreaterThan(30) - }) - }) - - describe('updateMeasure', () => { - it('should update metrics with custom value', () => { - const metrics = newMetrics() - const st = platformNow() - - updateMeasure(metrics, st, { op: 'test' }, {}, undefined, 100) - - expect(metrics.operations).toBe(1) - expect(metrics.value).toBe(100) - }) - - it('should override operations when override is true', () => { - const metrics = newMetrics() - const st = platformNow() - - // First call without override - accumulates value and increments operations - updateMeasure(metrics, st, { op: 'test' }, {}, undefined, 50, false) - expect(metrics.value).toBe(50) - expect(metrics.operations).toBe(1) - - // Second call with override - sets operations, doesn't add to value - updateMeasure(metrics, st, { op: 'test' }, {}, undefined, 100, true) - - expect(metrics.operations).toBe(100) // overridden - expect(metrics.value).toBe(50) // not changed when override=true - }) - - it('should track parameters', () => { - const metrics = newMetrics() - const st = platformNow() - - updateMeasure(metrics, st, { method: 'GET' }, {}, undefined, 100) - updateMeasure(metrics, st, { method: 'POST' }, {}, undefined, 200) - - expect(metrics.params.method).toBeDefined() - expect(metrics.params.method.GET).toBeDefined() - expect(metrics.params.method.POST).toBeDefined() - expect(metrics.params.method.GET.value).toBe(100) - expect(metrics.params.method.POST.value).toBe(200) - }) - - it('should handle multiple parameters as counters', () => { - const metrics = newMetrics() - const st = platformNow() - - updateMeasure(metrics, st, { method: 'GET', status: '200' }, {}, undefined, 100) - updateMeasure(metrics, st, { method: 'GET', status: '404' }, {}, undefined, 50) - - expect(metrics.params.method.GET).toBeDefined() - expect(metrics.params.method.GET.topResult).toBeDefined() - expect(metrics.params.method.GET.topResult?.length).toBeGreaterThan(0) - }) - - it('should update top results', () => { - const metrics = newMetrics() - const st = platformNow() - - updateMeasure(metrics, st, {}, { request: 'slow' }, undefined, 100) - updateMeasure(metrics, st, {}, { request: 'fast' }, undefined, 10) - - expect(metrics.topResult).toBeDefined() - expect(metrics.topResult?.length).toBeGreaterThan(0) - }) - }) - - describe('childMetrics', () => { - it('should create child metrics in hierarchy', () => { - const root = newMetrics() - const child = childMetrics(root, ['level1', 'level2']) - - expect(root.measurements.level1).toBeDefined() - expect(root.measurements.level1.measurements.level2).toBeDefined() - expect(child).toBe(root.measurements.level1.measurements.level2) - }) - - it('should reuse existing child metrics', () => { - const root = newMetrics() - const child1 = childMetrics(root, ['level1']) - const child2 = childMetrics(root, ['level1']) - - expect(child1).toBe(child2) - }) - - it('should create nested paths', () => { - const root = newMetrics() - childMetrics(root, ['api', 'users', 'create']) - - expect(root.measurements.api).toBeDefined() - expect(root.measurements.api.measurements.users).toBeDefined() - expect(root.measurements.api.measurements.users.measurements.create).toBeDefined() - }) - }) - - describe('metricsAggregate', () => { - it('should aggregate metrics', () => { - const metrics = newMetrics() - metrics.value = 100 - metrics.operations = 10 - - const child1 = childMetrics(metrics, ['child1']) - child1.value = 50 - child1.operations = 5 - - const child2 = childMetrics(metrics, ['child2']) - child2.value = 30 - child2.operations = 3 - - const aggregated = metricsAggregate(metrics) - - expect(aggregated.value).toBe(80) // child1 + child2 - expect(aggregated.operations).toBe(10) - }) - - it('should limit number of child metrics', () => { - const metrics = newMetrics() - - for (let i = 0; i < 10; i++) { - const child = childMetrics(metrics, [`child${i}`]) - child.value = i * 10 - } - - const aggregated = metricsAggregate(metrics, 3) - - expect(Object.keys(aggregated.measurements).length).toBe(3) - }) - - it('should filter out metrics starting with #', () => { - const metrics = newMetrics() - - const child1 = childMetrics(metrics, ['normal']) - child1.value = 50 - - const child2 = childMetrics(metrics, ['#internal']) - child2.value = 30 - - const aggregated = metricsAggregate(metrics) - - expect(aggregated.value).toBe(50) // only 'normal' counted - }) - }) - - describe('metricsToString', () => { - it('should convert metrics to string', () => { - const metrics = newMetrics() - metrics.value = 100 - metrics.operations = 10 - - const str = metricsToString(metrics, 'TestMetrics', 50) - - expect(str).toContain('TestMetrics') - expect(str).toContain('100') - expect(str).toContain('10') - }) - - it('should include child metrics in string', () => { - const metrics = newMetrics() - const child = childMetrics(metrics, ['operation']) - child.value = 50 - child.operations = 5 - - const str = metricsToString(metrics, 'TestMetrics', 50) - - expect(str).toContain('operation') - }) - }) - - describe('metricsToJson', () => { - it('should convert metrics to JSON', () => { - const metrics = newMetrics() - metrics.value = 100 - metrics.operations = 10 - - const json = metricsToJson(metrics) - - // aggregated value is the total value when no children - expect(json.$total).toBe(100) - expect(json.$ops).toBe(10) - }) - - it('should include child metrics in JSON', () => { - const metrics = newMetrics() - const child = childMetrics(metrics, ['operation']) - child.value = 50 - child.operations = 5 - - const json = metricsToJson(metrics) - - expect(json).toBeDefined() - const keys = Object.keys(json) - expect(keys.some((k) => k.includes('operation'))).toBe(true) - }) - }) - - describe('metricsToRows', () => { - it('should convert metrics to rows', () => { - const metrics = newMetrics() - metrics.value = 100 - metrics.operations = 10 - - const rows = metricsToRows(metrics, 'TestMetrics') - - expect(Array.isArray(rows)).toBe(true) - expect(rows.length).toBeGreaterThan(0) - expect(rows[0]).toContain('TestMetrics') - }) - - it('should include child metrics in rows', () => { - const metrics = newMetrics() - const child = childMetrics(metrics, ['operation']) - child.value = 50 - child.operations = 5 - - const rows = metricsToRows(metrics, 'TestMetrics') - - expect(rows.length).toBeGreaterThan(1) - expect(rows.some((row) => row.includes('operation'))).toBe(true) - }) - - it('should properly format row values', () => { - const metrics = newMetrics() - metrics.value = 100 - metrics.operations = 10 - - const rows = metricsToRows(metrics, 'TestMetrics') - - expect(rows[0]).toHaveLength(5) // offset, name, avg, total, ops - expect(typeof rows[0][0]).toBe('number') // offset - expect(typeof rows[0][1]).toBe('string') // name - }) - }) -}) diff --git a/foundations/core/packages/measurements/src/__tests__/performance.test.ts b/foundations/core/packages/measurements/src/__tests__/performance.test.ts deleted file mode 100644 index 14f0ff3e35..0000000000 --- a/foundations/core/packages/measurements/src/__tests__/performance.test.ts +++ /dev/null @@ -1,260 +0,0 @@ -import { MeasureMetricsContext, NoMetricsContext, noParamsLogger } from '../context' -import { newMetrics, metricsAggregate } from '../metrics' -import type { MeasureContext } from '../types' - -describe('performance', () => { - describe('overhead measurement', () => { - // Reduced iterations to fit within 10 seconds total test time - const iterations = 100 - const depth = 5 - - it('should measure overhead of with() vs raw execution', async () => { - // Baseline: raw execution without measurement - const baselineStart = performance.now() - for (let i = 0; i < iterations; i++) { - await simulateWork(1) - } - const baselineTime = performance.now() - baselineStart - - // With measurement context - const metrics = newMetrics() - const ctx = new MeasureMetricsContext('root', {}, {}, metrics, noParamsLogger) - - const measuredStart = performance.now() - for (let i = 0; i < iterations; i++) { - await ctx.with('operation', { iteration: i }, async () => { - await simulateWork(1) - }) - } - const measuredTime = performance.now() - measuredStart - - const overhead = measuredTime - baselineTime - const overheadPercentage = (overhead / baselineTime) * 100 - - console.log(`\n📊 Overhead Analysis (${iterations} iterations):`) - console.log(` Baseline time: ${baselineTime.toFixed(2)}ms`) - console.log(` Measured time: ${measuredTime.toFixed(2)}ms`) - console.log(` Overhead: ${overhead.toFixed(2)}ms (${overheadPercentage.toFixed(2)}%)`) - console.log(` Per operation: ${(overhead / iterations).toFixed(4)}ms`) - - expect(metrics.measurements.operation).toBeDefined() - expect(metrics.measurements.operation.operations).toBe(iterations) - - // Overhead should be reasonable (typically < 50% for simple operations) - // This is informational rather than a strict assertion - expect(overheadPercentage).toBeLessThan(200) - }) - - it('should measure overhead with deep nested contexts', async () => { - const metrics = newMetrics() - const ctx = new MeasureMetricsContext('root', {}, {}, metrics, noParamsLogger) - - // Baseline: single level - const singleLevelStart = performance.now() - for (let i = 0; i < iterations; i++) { - await ctx.with('shallow', {}, async () => { - await simulateWork(1) - }) - } - const singleLevelTime = performance.now() - singleLevelStart - - // Deep nesting - const deepStart = performance.now() - for (let i = 0; i < iterations; i++) { - await deepNestedExecution(ctx, depth, 1) - } - const deepTime = performance.now() - deepStart - - const nestingOverhead = deepTime - singleLevelTime - const overheadPerLevel = nestingOverhead / (iterations * depth) - - console.log(`\n📊 Deep Nesting Overhead (${iterations} iterations, depth=${depth}):`) - console.log(` Single level: ${singleLevelTime.toFixed(2)}ms`) - console.log(` Deep nested: ${deepTime.toFixed(2)}ms`) - console.log(` Nesting overhead: ${nestingOverhead.toFixed(2)}ms`) - console.log(` Per level: ${overheadPerLevel.toFixed(4)}ms`) - - expect(metrics.measurements.shallow).toBeDefined() - expect(metrics.measurements.level0).toBeDefined() - }) - - it('should measure overhead with complex parameter tracking', async () => { - const metrics = newMetrics() - const ctx = new MeasureMetricsContext('root', {}, {}, metrics, noParamsLogger) - - // Simple params - const simpleStart = performance.now() - for (let i = 0; i < iterations; i++) { - await ctx.with('simple', { id: i }, async () => { - await simulateWork(1) - }) - } - const simpleTime = performance.now() - simpleStart - - // Complex params with multiple tracked values - const complexStart = performance.now() - for (let i = 0; i < iterations; i++) { - await ctx.with( - 'complex', - { - method: i % 3 === 0 ? 'GET' : i % 3 === 1 ? 'POST' : 'PUT', - status: i % 2 === 0 ? 200 : 404, - cached: i % 4 === 0 - }, - async () => { - await simulateWork(1) - }, - { - userId: `user_${i % 10}`, - endpoint: `/api/v1/resource/${i % 5}`, - timestamp: Date.now() - } - ) - } - const complexTime = performance.now() - complexStart - - const paramOverhead = complexTime - simpleTime - - console.log(`\n📊 Parameter Tracking Overhead (${iterations} iterations):`) - console.log(` Simple params: ${simpleTime.toFixed(2)}ms`) - console.log(` Complex params: ${complexTime.toFixed(2)}ms`) - console.log(` Param overhead: ${paramOverhead.toFixed(2)}ms`) - console.log(` Per operation: ${(paramOverhead / iterations).toFixed(4)}ms`) - - expect(metrics.measurements.simple).toBeDefined() - expect(metrics.measurements.complex).toBeDefined() - expect(Object.keys(metrics.measurements.complex.params).length).toBeGreaterThan(0) - }) - - it('should measure overhead with NoMetricsContext', async () => { - // NoMetricsContext should have minimal overhead - const noMetricsCtx = new NoMetricsContext(noParamsLogger) - - const start = performance.now() - for (let i = 0; i < iterations; i++) { - await noMetricsCtx.with('operation', { iteration: i }, async () => { - await simulateWork(1) - }) - } - const noMetricsTime = performance.now() - start - - // Compare with raw execution - const rawStart = performance.now() - for (let i = 0; i < iterations; i++) { - await simulateWork(1) - } - const rawTime = performance.now() - rawStart - - const overhead = noMetricsTime - rawTime - const overheadPercentage = (overhead / rawTime) * 100 - - console.log(`\n📊 NoMetricsContext Overhead (${iterations} iterations):`) - console.log(` Raw time: ${rawTime.toFixed(2)}ms`) - console.log(` NoMetrics time: ${noMetricsTime.toFixed(2)}ms`) - console.log(` Overhead: ${overhead.toFixed(2)}ms (${overheadPercentage.toFixed(2)}%)`) - - // NoMetricsContext should have very low overhead - expect(overheadPercentage).toBeLessThan(50) - }) - }) - - describe('realistic workload simulation', () => { - it('should measure overhead in complex realistic scenario', async () => { - const metrics = newMetrics() - const ctx = new MeasureMetricsContext('app', { service: 'api' }, {}, metrics, noParamsLogger) - - const requests = 50 - const baselineStart = performance.now() - - // Simulate without metrics - for (let i = 0; i < requests; i++) { - await simulateAPIRequest(null, i) - } - const baselineTime = performance.now() - baselineStart - - // Reset for measured run - const measuredStart = performance.now() - - // Simulate with metrics - for (let i = 0; i < requests; i++) { - await simulateAPIRequest(ctx, i) - } - const measuredTime = performance.now() - measuredStart - - const overhead = measuredTime - baselineTime - const overheadPercentage = (overhead / baselineTime) * 100 - - console.log(`\n📊 Realistic Workload Analysis (${requests} API requests):`) - console.log(` Baseline: ${baselineTime.toFixed(2)}ms`) - console.log(` With metrics: ${measuredTime.toFixed(2)}ms`) - console.log(` Overhead: ${overhead.toFixed(2)}ms (${overheadPercentage.toFixed(2)}%)`) - console.log(` Per request: ${(overhead / requests).toFixed(4)}ms`) - - // Check metrics structure - const aggregated = metricsAggregate(metrics, 10) - console.log(` Collected operations: ${aggregated.measurements.request?.operations ?? 0}`) - - expect(aggregated.measurements.request).toBeDefined() - expect(overheadPercentage).toBeLessThan(100) // Should be less than 100% overhead - }) - }) -}) - -// Helper functions - -async function simulateWork (durationMs: number): Promise { - const end = performance.now() + durationMs - while (performance.now() < end) { - // Busy wait to simulate work - const r = Math.random() * Math.random() - expect(r).toBeGreaterThanOrEqual(0) - } -} - -async function deepNestedExecution ( - ctx: MeasureContext, - depth: number, - workMs: number, - currentLevel: number = 0 -): Promise { - if (currentLevel >= depth) { - await simulateWork(workMs) - return - } - - await ctx.with(`level${currentLevel}`, { level: currentLevel }, async (childCtx) => { - await deepNestedExecution(childCtx, depth, workMs, currentLevel + 1) - }) -} - -async function simulateAPIRequest (ctx: MeasureContext | null, requestId: number): Promise { - const method = ['GET', 'POST', 'PUT'][requestId % 3] - const endpoint = `/api/resource/${requestId % 10}` - - if (ctx === null) { - // No metrics version - await simulateWork(1) - // Simulate DB query - await simulateWork(2) - // Simulate processing - await simulateWork(1) - return - } - - // With metrics version - await ctx.with('request', { method, endpoint }, async (reqCtx) => { - await reqCtx.with('auth', { userId: `user_${requestId % 50}` }, async () => { - await simulateWork(1) - }) - - await reqCtx.with('database', { query: 'SELECT' }, async (dbCtx) => { - await dbCtx.with('query_execution', {}, async () => { - await simulateWork(2) - }) - }) - - await reqCtx.with('processing', { items: requestId % 20 }, async () => { - await simulateWork(1) - }) - }) -} diff --git a/foundations/core/packages/measurements/src/context.ts b/foundations/core/packages/measurements/src/context.ts deleted file mode 100644 index 62dbb347cc..0000000000 --- a/foundations/core/packages/measurements/src/context.ts +++ /dev/null @@ -1,392 +0,0 @@ -// Basic performance metrics suite. - -import { platformNow, platformNowDiff } from '.' -import { childMetrics, newMetrics, updateMeasure } from './metrics' -import { - type FullParamsType, - type MeasureContext, - type MeasureLogger, - type Metrics, - type ParamsType, - type OperationLog, - type OperationLogEntry, - type WithOptions -} from './types' - -const errorPrinter = ({ message, stack, ...rest }: Error): object => ({ - message, - stack, - ...rest -}) -function replacer (value: any): any { - return value instanceof Error ? errorPrinter(value) : value -} - -export const consoleLogger = (logParams: Record): MeasureLogger => ({ - info: (msg, args) => { - console.info( - msg, - ...Object.entries({ ...(args ?? {}), ...(logParams ?? {}) }).map( - (it) => `${it[0]}=${JSON.stringify(replacer(it[1]))}` - ) - ) - }, - error: (msg, args) => { - console.error( - msg, - ...Object.entries({ ...(args ?? {}), ...(logParams ?? {}) }).map( - (it) => `${it[0]}=${JSON.stringify(replacer(it[1]))}` - ) - ) - }, - warn: (msg, args) => { - console.warn(msg, ...Object.entries(args ?? {}).map((it) => `${it[0]}=${JSON.stringify(replacer(it[1]))}`)) - }, - close: async () => {}, - logOperation: (operation, time, params) => {} -}) - -export const noParamsLogger = consoleLogger({}) - -export const nullPromise = Promise.resolve() - -/** - * @public - */ -export class MeasureMetricsContext implements MeasureContext { - private readonly name: string - private readonly params: ParamsType - - private readonly fullParams: FullParamsType | (() => FullParamsType) = {} - logger: MeasureLogger - metrics: Metrics - id?: string - - st = platformNow() - contextData: object = {} - private done (value?: number, override?: boolean): void { - updateMeasure(this.metrics, this.st, this.params, this.fullParams, (spend) => {}, value, override) - } - - constructor ( - name: string, - params: ParamsType, - fullParams: FullParamsType | (() => FullParamsType) = {}, - metrics: Metrics = newMetrics(), - logger?: MeasureLogger, - readonly parent?: MeasureContext, - readonly logParams?: ParamsType - ) { - this.name = name - this.params = params - this.fullParams = fullParams - this.metrics = metrics - this.metrics.namedParams = this.metrics.namedParams ?? {} - for (const [k, v] of Object.entries(params)) { - if (this.metrics.namedParams[k] !== v) { - this.metrics.namedParams[k] = v - } else { - this.metrics.namedParams[k] = '*' - } - } - - this.logger = logger ?? (this.logParams != null ? consoleLogger(this.logParams ?? {}) : noParamsLogger) - } - - measure (name: string, value: number, override?: boolean): void { - const c = new MeasureMetricsContext('#' + name, {}, {}, childMetrics(this.metrics, ['#' + name]), this.logger, this) - c.contextData = this.contextData - c.done(value, override) - } - - newChild ( - name: string, - params: ParamsType, - opt?: { - fullParams?: FullParamsType - logger?: MeasureLogger - span?: WithOptions['span'] // By default true - } - ): MeasureContext { - const result = new MeasureMetricsContext( - name, - params, - opt?.fullParams ?? {}, - childMetrics(this.metrics, [name]), - opt?.logger ?? this.logger, - this, - this.logParams - ) - result.id = this.id - result.contextData = this.contextData - return result - } - - with( - name: string, - params: ParamsType, - op: (ctx: MeasureContext) => T | Promise, - fullParams?: ParamsType | (() => FullParamsType), - opt?: WithOptions - ): Promise { - const c = this.newChild(name, params, { fullParams, logger: this.logger }) - let needFinally = true - try { - const value = op(c) - if (value instanceof Promise) { - needFinally = false - return value.finally(() => { - c.end() - if (opt?.log === true) { - this.logger.logOperation(name, platformNowDiff((c as MeasureMetricsContext).st), { - ...params, - ...fullParams - }) - } - }) - } else { - if (value == null) { - return nullPromise as Promise - } - return Promise.resolve(value) - } - } finally { - if (needFinally) { - c.end() - } - } - } - - extractMeta (): Record { - return {} - } - - withSync( - name: string, - params: ParamsType, - op: (ctx: MeasureContext) => T, - fullParams?: ParamsType | (() => FullParamsType) - ): T { - const c = this.newChild(name, params, { fullParams, logger: this.logger }) - try { - return op(c) - } finally { - c.end() - } - } - - error (message: string, args?: Record): void { - this.logger.error(message, { ...this.params, ...args, ...(this.logParams ?? {}) }) - } - - info (message: string, args?: Record): void { - this.logger.info(message, { ...this.params, ...args, ...(this.logParams ?? {}) }) - } - - warn (message: string, args?: Record): void { - this.logger.warn(message, { ...this.params, ...args, ...(this.logParams ?? {}) }) - } - - end (): void { - this.done() - } - - getParams (): ParamsType { - return this.params - } -} - -export class NoMetricsContext implements MeasureContext { - logger: MeasureLogger - id?: string - - contextData: object = {} - - constructor (logger?: MeasureLogger) { - this.logger = logger ?? consoleLogger({}) - } - - measure (name: string, value: number, override?: boolean): void {} - - newChild ( - name: string, - params: ParamsType, - fullParams?: FullParamsType | (() => FullParamsType), - logger?: MeasureLogger - ): MeasureContext { - const result = new NoMetricsContext(logger ?? this.logger) - result.id = this.id - result.contextData = this.contextData - return result - } - - with( - name: string, - params: ParamsType, - op: (ctx: MeasureContext) => T | Promise, - fullParams?: ParamsType | (() => FullParamsType) - ): Promise { - const r = op(this.newChild(name, params, fullParams, this.logger)) - return r instanceof Promise ? r : Promise.resolve(r) - } - - extractMeta (): Record { - return {} - } - - withSync( - name: string, - params: ParamsType, - op: (ctx: MeasureContext) => T, - fullParams?: ParamsType | (() => FullParamsType) - ): T { - const c = this.newChild(name, params, fullParams, this.logger) - return op(c) - } - - withLog( - name: string, - params: ParamsType, - op: (ctx: MeasureContext) => T | Promise, - fullParams?: ParamsType - ): Promise { - const r = op(this.newChild(name, params, fullParams, this.logger)) - return r instanceof Promise ? r : Promise.resolve(r) - } - - error (message: string, args?: Record): void { - this.logger.error(message, { ...args }) - } - - info (message: string, args?: Record): void { - this.logger.info(message, { ...args }) - } - - warn (message: string, args?: Record): void { - this.logger.warn(message, { ...args }) - } - - end (): void {} - - getParams (): ParamsType { - return {} - } -} - -/** - * Allow to use decorator for context enabled functions - */ -export function withContext (name: string, params: ParamsType = {}, options?: WithOptions): any { - return (target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor => { - const originalMethod = descriptor.value - descriptor.value = function (...args: any[]): Promise { - const ctx = args[0] as MeasureContext - return ctx.with( - name, - params, - (ctx) => originalMethod.apply(this, [ctx, ...args.slice(1)]) as Promise, - {}, - options - ) - } - return descriptor - } -} - -let operationProfiling = false - -export function setOperationLogProfiling (value: boolean): void { - operationProfiling = value -} - -let globalId: number = 0 - -export function registerOperationLog (ctx: MeasureContext): { opLogMetrics?: Metrics, op?: OperationLog } { - if (!operationProfiling) { - return {} - } - const op: OperationLog = { start: platformNow(), ops: [], end: -1 } - let opLogMetrics: Metrics | undefined - - if (ctx.id === undefined) { - ctx.id = 'op_' + (++globalId).toString(16) - } - if (ctx.metrics !== undefined) { - if (ctx.metrics.opLog === undefined) { - ctx.metrics.opLog = {} - } - ctx.metrics.opLog[ctx.id] = op - opLogMetrics = ctx.metrics - } - return { opLogMetrics, op } -} - -export function updateOperationLog (opLogMetrics: Metrics | undefined, op: OperationLog | undefined): void { - if (!operationProfiling) { - return - } - if (op !== undefined) { - op.end = platformNow() - } - // We should keep only longest one entry - if (opLogMetrics?.opLog !== undefined) { - const entries = Object.entries(opLogMetrics.opLog) - - const incomplete = entries.filter((it) => it[1].end === -1) - const complete = entries.filter((it) => it[1].end !== -1) - complete.sort((a, b) => a[1].start - b[1].start) - if (complete.length > 30) { - complete.splice(0, complete.length - 30) - } - - opLogMetrics.opLog = Object.fromEntries(incomplete.concat(complete)) - } -} - -export function addOperation ( - ctx: MeasureContext, - name: string, - params: ParamsType, - op: (ctx: MeasureContext) => Promise, - fullParams?: FullParamsType -): Promise { - if (!operationProfiling) { - return op(ctx) - } - let opEntry: OperationLogEntry | undefined - - let p: MeasureContext | undefined = ctx - let opLogMetrics: Metrics | undefined - let id: string | undefined - - while (p !== undefined) { - if (p.metrics?.opLog !== undefined) { - opLogMetrics = p.metrics - } - if (id === undefined && p.id !== undefined) { - id = p.id - } - p = p.parent - } - const opLog = id !== undefined ? opLogMetrics?.opLog?.[id] : undefined - - if (opLog !== undefined) { - opEntry = { - op: name, - start: performance.now(), - params: {}, - end: -1 - } - } - const result = op(ctx) - if (opEntry !== undefined && opLog !== undefined) { - void result.finally(() => { - if (opEntry !== undefined && opLog !== undefined) { - opEntry.end = performance.now() - opEntry.params = { ...params, ...(typeof fullParams === 'function' ? fullParams() : fullParams) } - opLog.ops.push(opEntry) - } - }) - } - return result -} diff --git a/foundations/core/packages/measurements/src/index.ts b/foundations/core/packages/measurements/src/index.ts deleted file mode 100644 index 16e4d95df6..0000000000 --- a/foundations/core/packages/measurements/src/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -export * from './context' -export * from './metrics' -export type * from './types' - -/** - * Return a current performance timestamp - */ -export const platformNow: () => number = () => performance.now() - -/** - * Return a diff with previous performance snapshot with 2 digits after . max. - */ -export const platformNowDiff = (old: number): number => Math.round((performance.now() - old) * 100) / 100 diff --git a/foundations/core/packages/measurements/src/metrics.ts b/foundations/core/packages/measurements/src/metrics.ts deleted file mode 100644 index 695dc0372a..0000000000 --- a/foundations/core/packages/measurements/src/metrics.ts +++ /dev/null @@ -1,343 +0,0 @@ -// Basic performance metrics suite. - -import { platformNow, type MetricsData } from '.' -import { type FullParamsType, type Metrics, type ParamsType } from './types' - -/** - * @public - */ -export const globals: Metrics = newMetrics() - -/** - * @public - * @returns - */ -export function newMetrics (): Metrics { - return { - operations: 0, - value: 0, - measurements: {}, - params: {}, - namedParams: {} - } -} - -function getUpdatedTopResult ( - current: Metrics['topResult'], - time: number, - params: FullParamsType -): Metrics['topResult'] { - if (time === 0) { - return current - } - const result: Metrics['topResult'] = current ?? [] - - const newValue = { - value: time, - params - } - - if (result.length > 6) { - if (result[0].value < newValue.value) { - result[0] = newValue - return result - } - if (result[result.length - 1].value > newValue.value) { - result[result.length - 1] = newValue - return result - } - - // Shift the middle - return [result[0], newValue, ...result.slice(1, 3), result[5]] - } else { - result.push(newValue) - return result - } -} - -/** - * Measure with tree expansion. Operation counter will be added only to leaf's. - * @public - */ -export function measure ( - metrics: Metrics, - params: ParamsType, - fullParams: FullParamsType | (() => FullParamsType) = {}, - endOp?: (spend: number) => void -): () => void { - const st = platformNow() - return () => { - updateMeasure(metrics, st, params, fullParams, endOp) - } -} -export function updateMeasure ( - metrics: Metrics, - st: number, - params: ParamsType, - fullParams: FullParamsType | (() => FullParamsType), - endOp?: (spend: number) => void, - value?: number, - override?: boolean -): void { - const ed = platformNow() - - const fParams = typeof fullParams === 'function' ? fullParams() : fullParams - // Update params if required - const pparams = Object.entries(params) - if (pparams.length > 0) { - const [k, v] = pparams[0] - let params = metrics.params[k] - if (params === undefined) { - params = {} - metrics.params[k] = params - } - const vKey = `${v?.toString() ?? ''}` - let param = params[vKey] - if (param === undefined) { - param = { - operations: 0, - value: 0 - } - params[vKey] = param - } - if (override === true) { - if (value === 0) { - // We need to delete value, to preserve sending zero values. - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete params[vKey] - } else { - param.operations = value ?? ed - st - } - } else { - param.value += value ?? ed - st - param.operations++ - } - // Do not update top results for params. - if (pparams.length > 1) { - // We need to update all other params as counters. - if (param.topResult === undefined) { - param.topResult = [] - } - for (const [, v] of pparams.slice(1)) { - const r = (param.topResult ?? []).find((it) => it.params[`${v}`] === true) - if (r !== undefined) { - r.value += 1 // Counter of operations - r.time = (r.time ?? 0) + (value ?? ed - st) - } else { - param.topResult.push({ params: { [`${v}`]: true }, value: 1, time: value ?? ed - st }) - } - } - param.topResult.sort((a, b) => b.value - a.value) - } - } - // Update leaf data - if (override === true) { - metrics.operations = value ?? ed - st - } else { - metrics.value += value ?? ed - st - metrics.operations++ - } - - metrics.topResult = getUpdatedTopResult(metrics.topResult, ed - st, fParams) - endOp?.(ed - st) -} - -/** - * @public - */ -export function childMetrics (root: Metrics, path: string[]): Metrics { - const segments = path - let oop = root - for (const p of segments) { - const v = oop.measurements[p] ?? { operations: 0, value: 0, measurements: {}, params: {} } - oop.measurements[p] = v - oop = v - } - return oop -} - -/** - * @public - */ -export function metricsAggregate (m: Metrics, limit: number = -1, roundMath: boolean = false): Metrics { - let ms = aggregateMetrics(m.measurements, limit) - - // Use child overage, if there is no top level value specified. - const me = Object.entries(ms) - const sumVal: number = - (me.length === 0 ? m.value : 0) + - me - .filter((it) => !it[0].startsWith('#')) - .map((it) => it[1]) - .reduce((p, v) => { - return p + v.value - }, 0) - - if (limit !== -1) { - // We need to keep only top limit items in ms - if (Object.keys(ms).length > 0) { - const newMs: typeof ms = {} - let added = 0 - for (const [k, v] of Object.entries(ms)) { - newMs[k] = v - added++ - if (added >= limit) { - break - } - } - ms = newMs - } - } - - return { - operations: m.operations, - measurements: ms, - params: m.params, - value: sumVal, - topResult: m.topResult, - namedParams: m.namedParams, - opLog: m.opLog - } -} - -function aggregateMetrics (m: Record, limit: number = -1): Record { - const result: Record = {} - for (const [k, v] of Object.entries(m).sort((a, b) => b[1].value - a[1].value)) { - result[k] = metricsAggregate(v, limit) - } - return result -} - -function toLen (val: string, sep: string, len: number): string { - while (val.length < len) { - val += sep - } - return val -} - -function printMetricsChildren (params: Record, offset: number, length: number): string { - let r = '' - if (Object.keys(params).length > 0) { - r += '\n' + toLen('', ' ', offset) - r += Object.entries(params) - .filter((it) => it[1].value > 0.1) - .map(([k, vv]) => toString(k, vv, offset, length)) - .join('\n' + toLen('', ' ', offset)) - } - return r -} - -function printMetricsParams ( - params: Record>, - offset: number, - length: number -): string { - let r = '' - const joinP = (key: string, data: Record): string[] => { - return Object.entries(data) - .filter((it) => it[1].value >= 0.1) - .map(([k, vv]) => - `${toLen('', ' ', offset)}${toLen(key + '=' + k, '-', length - offset)}: avg ${ - Math.round((vv.value / (vv.operations > 0 ? vv.operations : 1)) * 100) / 100 - } total: ${Math.round(vv.value * 100) / 100} ops: ${vv.operations}`.trim() - ) - } - const joinParams = Object.entries(params).reduce((p, c) => [...p, ...joinP(c[0], c[1])], []) - if (Object.keys(joinParams).length > 0) { - r += '\n' + toLen('', ' ', offset) - r += joinParams.join('\n' + toLen('', ' ', offset)) - } - return r -} - -function toString (name: string, m: Metrics, offset: number, length: number): string { - let r = `${toLen('', ' ', offset)}${toLen(name, '-', length - offset)}: avg ${ - Math.round((m.value / (m.operations > 0 ? m.operations : 1)) * 100) / 100 - } total: ${Math.round(m.value * 100) / 100} ops: ${m.operations}`.trim() - r += printMetricsParams(m.params, offset + 4, length) - r += printMetricsChildren(m.measurements, offset + 4, length) - return r -} - -function toJson (m: Metrics): any { - const obj: any = { - $total: m.value, - $ops: m.operations - } - if (m.operations > 1) { - obj.avg = Math.round((m.value / (m.operations > 0 ? m.operations : 1)) * 100) / 100 - } - if (Object.keys(m.params).length > 0) { - obj.params = m.params - } - for (const [k, v] of Object.entries(m.measurements ?? {})) { - obj[ - `${k} ${v.value} ${v.operations} ${ - v.operations > 1 ? Math.round((v.value / (v.operations > 0 ? m.operations : 1)) * 100) / 100 : '' - }` - ] = toJson(v) - } - - return obj -} - -/** - * @public - */ -export function metricsToString (metrics: Metrics, name = 'System', length: number): string { - return toString(name, metricsAggregate(metrics, 50, true), 0, length) -} - -export function metricsToJson (metrics: Metrics): any { - return toJson(metricsAggregate(metrics)) -} - -function printMetricsParamsRows ( - params: Record>, - offset: number -): (string | number)[][] { - const r: (string | number)[][] = [] - function joinP (key: string, data: Record): (string | number)[][] { - return Object.entries(data).map(([k, vv]) => [ - offset, - `${key}=${k}`, - Math.round((vv.value / (vv.operations > 0 ? vv.operations : 1)) * 100) / 100, - Math.round(vv.value * 100) / 100, - vv.operations - ]) - } - for (const [k, v] of Object.entries(params)) { - r.push(...joinP(k, v)) - } - return r -} - -function printMetricsChildrenRows (params: Record, offset: number): (string | number)[][] { - const r: (string | number)[][] = [] - if (Object.keys(params).length > 0) { - Object.entries(params).forEach(([k, vv]) => r.push(...toStringRows(k, vv, offset))) - } - return r -} - -function toStringRows (name: string, m: Metrics, offset: number): (number | string)[][] { - const r: (number | string)[][] = [ - [ - offset, - name, - Math.round((m.value / (m.operations > 0 ? m.operations : 1)) * 100) / 100, - Math.round(m.value * 100) / 100, - m.operations - ] - ] - r.push(...printMetricsParamsRows(m.params, offset + 1)) - r.push(...printMetricsChildrenRows(m.measurements, offset + 1)) - return r -} - -/** - * @public - */ -export function metricsToRows (metrics: Metrics, name = 'System'): (number | string)[][] { - return toStringRows(name, metricsAggregate(metrics, 50, true), 0) -} diff --git a/foundations/core/packages/measurements/src/types.ts b/foundations/core/packages/measurements/src/types.ts deleted file mode 100644 index d2d2e1366b..0000000000 --- a/foundations/core/packages/measurements/src/types.ts +++ /dev/null @@ -1,135 +0,0 @@ -/** - * @public - */ -export type ParamType = string | number | boolean | undefined - -/** - * @public - */ -export type ParamsType = Record - -/** - * @public - */ -export type FullParamsType = Record - -/** - * @public - */ -export interface MetricsData { - operations: number - value: number - topResult?: { - value: number - time?: number - params: FullParamsType - }[] -} - -export interface OperationLogEntry { - op: string - params: ParamsType - start: number - end: number -} -export interface OperationLog { - ops: OperationLogEntry[] - start: number - end: number -} - -/** - * @public - */ -export interface Metrics extends MetricsData { - namedParams: ParamsType - params: Record> - measurements: Record - - opLog?: Record -} - -/** - * @public - */ -export interface MeasureLogger { - info: (message: string, obj?: Record) => void - error: (message: string, obj?: Record) => void - - warn: (message: string, obj?: Record) => void - - logOperation: (operation: string, time: number, params: ParamsType) => void - - childLogger?: (name: string, params: Record) => MeasureLogger - - close: () => Promise -} - -export interface WithOptions { - span?: true | false | 'disable' | 'skip' | 'inherit' // 'none' means no span will be created, 'disable' means context will be tracing disabled - log?: boolean - inheritParams?: boolean - - // Passed context metadata - meta?: Record - - // If passed, will not send an error into span, for some cases we need to throw error from with, without reporting it. - suspendErrors?: boolean -} - -/** - * @public - */ -export interface MeasureContext { - id?: string - - // Context data will be copied referenced for all child contexts. - contextData: Q - // Create a child metrics context - newChild: ( - name: string, - params: ParamsType, - opt?: { - fullParams?: FullParamsType - logger?: MeasureLogger - span?: WithOptions['span'] // By default true - meta?: Record - } - ) => MeasureContext - - metrics?: Metrics - - with: ( - name: string, - params: ParamsType, - op: (ctx: MeasureContext) => T | Promise, - fullParams?: FullParamsType | (() => FullParamsType), - opt?: WithOptions - ) => Promise - - withSync: ( - name: string, - params: ParamsType, - op: (ctx: MeasureContext) => T, - fullParams?: FullParamsType | (() => FullParamsType), - opt?: WithOptions - ) => T - - extractMeta: () => Record - - logger: MeasureLogger - - parent?: MeasureContext - getParams: () => ParamsType - - measure: (name: string, value: number, override?: boolean) => void - - // Capture error - error: (message: string, obj?: Record) => void - info: (message: string, obj?: Record) => void - warn: (message: string, obj?: Record) => void - - // Mark current context as complete - // If no value is passed, time difference will be used. - end: (value?: number) => void -} diff --git a/foundations/core/packages/measurements/tsconfig.json b/foundations/core/packages/measurements/tsconfig.json deleted file mode 100644 index b5ae22f6e4..0000000000 --- a/foundations/core/packages/measurements/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/foundations/core/packages/model/.eslintrc.js b/foundations/core/packages/model/.eslintrc.js deleted file mode 100644 index ce90fb9646..0000000000 --- a/foundations/core/packages/model/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/foundations/core/packages/model/.npmignore b/foundations/core/packages/model/.npmignore deleted file mode 100644 index e3ec093c38..0000000000 --- a/foundations/core/packages/model/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!/lib/** -!CHANGELOG.md -/lib/**/__tests__/ diff --git a/foundations/core/packages/model/CHANGELOG.json b/foundations/core/packages/model/CHANGELOG.json deleted file mode 100644 index 5fbed02733..0000000000 --- a/foundations/core/packages/model/CHANGELOG.json +++ /dev/null @@ -1,127 +0,0 @@ -{ - "name": "@hcengineering/model", - "entries": [ - { - "version": "0.7.17", - "tag": "@hcengineering/model_v0.7.17", - "date": "Mon, 27 Oct 2025 13:27:12 GMT", - "comments": { - "none": [ - { - "comment": "formatting" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" - } - ] - } - }, - { - "version": "0.7.6", - "tag": "@hcengineering/model_v0.7.6", - "date": "Tue, 21 Oct 2025 19:04:55 GMT", - "comments": { - "patch": [ - { - "comment": "Add TypeIdentifier for custom attribute incremental IDs" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.8` to `0.7.10`" - } - ] - } - }, - { - "version": "0.7.5", - "tag": "@hcengineering/model_v0.7.5", - "date": "Tue, 14 Oct 2025 04:58:17 GMT", - "comments": { - "patch": [ - { - "comment": "update deps" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`" - }, - { - "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.4` to `0.7.5`" - }, - { - "comment": "Updating dependency \"@hcengineering/storage\" from `^0.7.4` to `0.7.5`" - }, - { - "comment": "Updating dependency \"@hcengineering/analytics\" from `^0.7.4` to `0.7.5`" - }, - { - "comment": "Updating dependency \"@hcengineering/rank\" from `^0.7.4` to `0.7.5`" - }, - { - "comment": "Updating dependency \"@hcengineering/account-client\" from `^0.7.4` to `0.7.5`" - } - ] - } - }, - { - "version": "0.7.4", - "tag": "@hcengineering/model_v0.7.4", - "date": "Sat, 11 Oct 2025 18:20:33 GMT", - "comments": { - "patch": [ - { - "comment": "Update to latest platform rig" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`" - }, - { - "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.3` to `0.7.4`" - }, - { - "comment": "Updating dependency \"@hcengineering/storage\" from `^0.7.3` to `0.7.4`" - }, - { - "comment": "Updating dependency \"@hcengineering/analytics\" from `^0.7.3` to `0.7.4`" - }, - { - "comment": "Updating dependency \"@hcengineering/rank\" from `^0.7.3` to `0.7.4`" - }, - { - "comment": "Updating dependency \"@hcengineering/account-client\" from `^0.7.3` to `0.7.4`" - } - ] - } - }, - { - "version": "0.7.3", - "tag": "@hcengineering/model_v0.7.3", - "date": "Wed, 08 Oct 2025 03:40:53 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`" - } - ] - } - }, - { - "version": "0.7.0", - "tag": "@hcengineering/model_v0.6.0", - "date": "Sun, 08 Aug 2021 10:14:57 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/platform\" from `~0.6.3` to `~0.6.4`" - } - ] - } - } - ] -} diff --git a/foundations/core/packages/model/CHANGELOG.md b/foundations/core/packages/model/CHANGELOG.md deleted file mode 100644 index 5dbecb05d8..0000000000 --- a/foundations/core/packages/model/CHANGELOG.md +++ /dev/null @@ -1,40 +0,0 @@ -# Change Log - @hcengineering/model - -This log was last generated on Mon, 27 Oct 2025 13:27:12 GMT and should not be manually modified. - -## 0.7.17 -Mon, 27 Oct 2025 13:27:12 GMT - -_Version update only_ - -## 0.7.6 -Tue, 21 Oct 2025 19:04:55 GMT - -### Patches - -- Add TypeIdentifier for custom attribute incremental IDs - -## 0.7.5 -Tue, 14 Oct 2025 04:58:17 GMT - -### Patches - -- update deps - -## 0.7.4 -Sat, 11 Oct 2025 18:20:33 GMT - -### Patches - -- Update to latest platform rig - -## 0.7.3 -Wed, 08 Oct 2025 03:40:53 GMT - -_Version update only_ - -## 0.7.0 -Sun, 08 Aug 2021 10:14:57 GMT - -_Initial release_ - diff --git a/foundations/core/packages/model/config/rig.json b/foundations/core/packages/model/config/rig.json deleted file mode 100644 index 78cc5a1733..0000000000 --- a/foundations/core/packages/model/config/rig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig", - "rigProfile": "node" -} diff --git a/foundations/core/packages/model/jest.config.js b/foundations/core/packages/model/jest.config.js deleted file mode 100644 index 069ea8aa0d..0000000000 --- a/foundations/core/packages/model/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ['./src'], - coverageReporters: ['text-summary', 'html', 'lcov'] -} diff --git a/foundations/core/packages/model/package.json b/foundations/core/packages/model/package.json deleted file mode 100644 index feaba7885c..0000000000 --- a/foundations/core/packages/model/package.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "name": "@hcengineering/model", - "version": "0.7.17", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "author": "Anticrm Platform Contributors", - "template": "@hcengineering/node-package", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "build:watch": "compile", - "format": "format src", - "test": "jest --passWithNoTests --silent --forceExit --coverage", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --forceExit --coverage", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "workspace:^0.7.19", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "@types/toposort": "^2.0.3", - "@typescript-eslint/parser": "^6.21.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.6.2", - "typescript": "^5.9.3", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5", - "@types/node": "^22.18.1", - "eslint-plugin-svelte": "^2.35.1" - }, - "dependencies": { - "@hcengineering/core": "workspace:^0.7.22", - "@hcengineering/platform": "workspace:^0.7.18", - "@hcengineering/storage": "workspace:^0.7.17", - "@hcengineering/analytics": "workspace:^0.7.17", - "@hcengineering/rank": "workspace:^0.7.17", - "@hcengineering/account-client": "workspace:^0.7.19", - "toposort": "^2.0.2", - "fast-equals": "^5.2.2" - }, - "repository": "https://github.com/hcengineering/huly.core", - "publishConfig": { - "access": "public" - }, - "files": [ - "lib/**/*", - "!lib/**/__test__/**", - "types/**/*", - "!types/**/__test__/**", - "src/**/*", - "!src/**/__test__/**", - "tsconfig.json" - ], - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.js" - } - } -} diff --git a/foundations/core/packages/model/src/dsl.ts b/foundations/core/packages/model/src/dsl.ts deleted file mode 100644 index dfe0313ae1..0000000000 --- a/foundations/core/packages/model/src/dsl.ts +++ /dev/null @@ -1,544 +0,0 @@ -// -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import core, { - type AccountUuid, - type AttachedDoc, - type Attribute, - type Class, - type Classifier, - ClassifierKind, - type CustomSequence, - type Data, - DateRangeMode, - type Doc, - type Domain, - type Enum, - type EnumOf, - Hierarchy, - type Hyperlink, - type Mixin as IMixin, - type IndexKind, - type Interface, - type Markup, - type MarkupBlobRef, - type MixinData, - type Obj, - type PersonId, - type PropertyType, - type Rank, - type Ref, - type RefTo, - type Space, - type Timestamp, - type Tx, - type TxCreateDoc, - TxFactory, - TxProcessor, - type Type, - type TypeAny as TypeAnyType, - type ArrOf as TypeArrOf, - type Collection as TypeCollection, - type TypeDate as TypeDateType, - type TypeIdentifier as TypeIdentifierType, - type TypeNumber as TypeNumberType, - generateId -} from '@hcengineering/core' -import type { Asset, IntlString } from '@hcengineering/platform' -import toposort from 'toposort' - -const targets = new Map>() - -function setIndex (target: any, property: string, index: IndexKind): void { - let indexes = targets.get(target) - if (indexes === undefined) { - indexes = new Map() - targets.set(target, indexes) - } - indexes.set(property, index) -} - -function getIndex (target: any, property: string): IndexKind | undefined { - return targets.get(target)?.get(property) -} - -interface ClassTxes { - _id: Ref - extends?: Ref> - implements?: Ref>[] - domain?: Domain - label: IntlString - icon?: Asset - txes: Array - kind: ClassifierKind - shortLabel?: string | IntlString - sortingKey?: string - filteringKey?: string - pluralLabel?: IntlString -} - -const transactions = new Map() - -function getTxes (target: any): ClassTxes { - const txes = transactions.get(target) - if (txes === undefined) { - const txes = { txes: [] } as unknown as ClassTxes - transactions.set(target, txes) - return txes - } - return txes -} - -const attributes = new Map>>() -function setAttr (target: any, prop: string, key: string, value: any): void { - const props = attributes.get(target) ?? new Map>() - const attrs = props.get(prop) ?? {} - attrs[key] = value - - props.set(prop, attrs) - attributes.set(target, props) -} - -function clearAttrs (target: any, prop: string): void { - const props = attributes.get(target) - props?.delete(prop) - - if (props !== undefined && props.size === 0) { - attributes.delete(target) - } -} - -function getAttrs (target: any, prop: string): Record { - return attributes.get(target)?.get(prop) ?? {} -} - -/** - * @public - */ -export function Prop (type: Type, label: IntlString, extra: Partial> = {}) { - return function (target: any, propertyKey: string): void { - const txes = getTxes(target) - const tx: TxCreateDoc> = { - _id: generateId(), - _class: core.class.TxCreateDoc, - space: core.space.Tx, - modifiedBy: core.account.System, - modifiedOn: Date.now(), - objectSpace: core.space.Model, - objectId: extra._id ?? (propertyKey as Ref>), - objectClass: core.class.Attribute, - attributes: { - ...extra, - name: propertyKey, - index: getIndex(target, propertyKey), - type, - label, - attributeOf: txes._id, // undefined, need to fix later - ...getAttrs(target, propertyKey) - } - } - - clearAttrs(target, propertyKey) - - txes.txes.push(tx) - } -} - -/** - * @public - */ -export function Hidden () { - return function (target: any, propertyKey: string): void { - setAttr(target, propertyKey, 'hidden', true) - } -} - -/** - * @public - */ -export function ReadOnly () { - return function (target: any, propertyKey: string): void { - setAttr(target, propertyKey, 'readonly', true) - } -} - -/** - * @public - */ -export function Index (kind: IndexKind) { - return function (target: any, propertyKey: string): void { - setIndex(target, propertyKey, kind) - } -} - -/** - * @public - */ -export function Model ( - _class: Ref>, - _extends: Ref>, - domain?: Domain, - _implements?: Ref>[] -) { - return function classDecorator T> (constructor: C): void { - const txes = getTxes(constructor.prototype) - txes._id = _class - txes.extends = _class !== core.class.Obj ? _extends : undefined - txes.implements = _implements - txes.domain = domain - txes.kind = ClassifierKind.CLASS - } -} - -/** - * @public - */ -export function Implements (_interface: Ref>, _extends?: Ref>[]) { - return function classDecorator T> (constructor: C): void { - const txes = getTxes(constructor.prototype) - txes._id = _interface - txes.implements = _extends - txes.kind = ClassifierKind.INTERFACE - } -} - -/** - * @public - */ -export function Mixin (_class: Ref>, _extends: Ref>) { - return function classDecorator T> (constructor: C): void { - const txes = getTxes(constructor.prototype) - txes._id = _class - txes.extends = _extends - txes.kind = ClassifierKind.MIXIN - } -} - -/** - * @public - */ -export function UX ( - label: IntlString, - icon?: Asset, - shortLabel?: string, - sortingKey?: string, - filteringKey?: string, - pluralLabel?: IntlString -) { - return function classDecorator T> (constructor: C): void { - const txes = getTxes(constructor.prototype) - txes.label = label - txes.icon = icon - txes.shortLabel = shortLabel - txes.sortingKey = sortingKey - txes.filteringKey = filteringKey ?? sortingKey - txes.pluralLabel = pluralLabel - } -} - -function generateIds (objectId: Ref, txes: TxCreateDoc>[]): Tx[] { - return txes.map((tx) => { - const withId = { - ...tx, - // Do not override custom attribute id if specified - objectId: tx.objectId !== tx.attributes.name ? tx.objectId : `${objectId}_${tx.objectId}` - } - withId.attributes.attributeOf = objectId as Ref> - return withId - }) -} - -const txFactory = new TxFactory(core.account.System) - -function _generateTx (tx: ClassTxes): Tx[] { - const objectId = tx._id - const _cl = { - [ClassifierKind.CLASS]: core.class.Class, - [ClassifierKind.INTERFACE]: core.class.Interface, - [ClassifierKind.MIXIN]: core.class.Mixin - } - const createTx = txFactory.createTxCreateDoc( - _cl[tx.kind], - core.space.Model, - { - ...(tx.domain !== undefined ? { domain: tx.domain } : {}), - kind: tx.kind, - label: tx.label, - icon: tx.icon, - ...(tx.kind === ClassifierKind.INTERFACE - ? { extends: tx.implements } - : { extends: tx.extends, implements: tx.implements }), - ...(tx.kind === ClassifierKind.INTERFACE - ? { extends: tx.implements } - : { - shortLabel: tx.shortLabel, - sortingKey: tx.sortingKey, - filteringKey: tx.filteringKey, - pluralLabel: tx.pluralLabel - }) - }, - objectId - ) - return [createTx, ...generateIds(objectId, tx.txes as TxCreateDoc>[])] -} - -/** - * @public - */ -export class Builder { - private readonly txes: Tx[] = [] - readonly hierarchy = new Hierarchy() - - onTx?: (tx: Tx) => void - - createModel (...classes: Array Obj>): void { - const txes = classes.map((ctor) => getTxes(ctor.prototype)) - const byId = new Map() - - txes.forEach((tx) => { - byId.set(tx._id, tx) - }) - - Array.from(byId.entries()).forEach(([id, txes]) => { - if (txes.kind === ClassifierKind.CLASS && txes.domain !== undefined && txes.extends !== undefined) { - let parentTxes: ClassTxes | undefined = txes - let parentDomain: Domain | undefined - do { - parentTxes = parentTxes.extends === undefined ? undefined : byId.get(parentTxes.extends) - parentDomain = parentTxes === undefined ? undefined : parentTxes.domain - } while (parentTxes !== undefined && parentDomain === undefined) - if (parentDomain !== undefined) { - throw new Error( - `Class '${id}' should not specify its own domain '${txes.domain}', as it already extends class '${parentTxes?._id}' in domain '${parentDomain}'` - ) - } - } - }) - - const generated = this.generateTransactions(txes, byId) - - for (const tx of generated) { - this.txes.push(tx) - this.onTx?.(tx) - this.hierarchy.tx(tx) - } - } - - private generateTransactions (txes: ClassTxes[], byId: Map): Tx[] { - const graph = this.createGraph(txes) - const sorted = toposort(graph) - .reverse() - .map((edge) => byId.get(edge)) - return sorted.flatMap((tx) => (tx != null ? _generateTx(tx) : [])) - } - - private createGraph (txes: ClassTxes[]): [string, string | undefined][] { - return txes.map((tx) => [tx._id, tx.extends] as [string, string | undefined]) - } - - // do we need this? - createDoc( - _class: Ref>, - space: Ref, - attributes: Data, - objectId?: Ref, - modifiedBy?: PersonId - ): T { - const tx = txFactory.createTxCreateDoc(_class, space, attributes, objectId) - if (modifiedBy !== undefined) { - tx.modifiedBy = modifiedBy - } - this.txes.push(tx) - this.onTx?.(tx) - this.hierarchy.tx(tx) - return TxProcessor.createDoc2Doc(tx) - } - - mixin( - objectId: Ref, - objectClass: Ref>, - mixin: Ref>, - attributes: MixinData - ): void { - const tx = txFactory.createTxMixin(objectId, objectClass, core.space.Model, mixin, attributes) - this.txes.push(tx) - this.onTx?.(tx) - this.hierarchy.tx(tx) - } - - getTxes (): Tx[] { - return [...this.txes] - } -} - -// T Y P E S - -/** - * @public - */ -export function TypeString (): Type { - return { _class: core.class.TypeString, label: core.string.String, icon: core.icon.TypeString } -} - -/** - * @public - */ -export function TypeRelation (): Type { - return { _class: core.class.TypeRelation, label: core.string.Relation, icon: core.icon.TypeRef } -} - -/** - * @public - */ -export function TypeBlob (): Type { - return { _class: core.class.TypeBlob, label: core.string.String, icon: core.icon.TypeBlob } -} - -/** - * @public - */ -export function TypeHyperlink (): Type { - return { _class: core.class.TypeHyperlink, label: core.string.Hyperlink, icon: core.icon.TypeHyperlink } -} - -/** - * @public - */ -export function TypeNumber (min?: number, max?: number, digits?: number): TypeNumberType { - return { _class: core.class.TypeNumber, label: core.string.Number, icon: core.icon.TypeNumber, min, max, digits } -} - -/** - * @public - */ -export function TypeMarkup (): Type { - return { _class: core.class.TypeMarkup, label: core.string.Markup, icon: core.icon.TypeMarkup } -} - -/** - * @public - */ -export function TypeRecord (): Type> { - return { _class: core.class.TypeRecord, label: core.string.Record, icon: core.icon.TypeRecord } -} - -/** - * @public - */ -export function TypeIntlString (): Type { - return { _class: core.class.TypeIntlString, label: core.string.IntlString, icon: core.icon.TypeRef } -} - -/** - * @public - */ -export function TypeBoolean (): Type { - return { _class: core.class.TypeBoolean, label: core.string.Boolean, icon: core.icon.TypeBoolean } -} - -/** - * @public - */ -export function TypeTimestamp (): Type { - return { _class: core.class.TypeTimestamp, label: core.string.Timestamp, icon: core.icon.TypeDate } -} - -/** - * @public - */ -export function TypeDate (mode: DateRangeMode = DateRangeMode.DATE, withShift: boolean = true): TypeDateType { - return { _class: core.class.TypeDate, label: core.string.Date, icon: core.icon.TypeDate, mode, withShift } -} - -/** - * @public - */ -export function TypeRef (_class: Ref>): RefTo { - return { _class: core.class.RefTo, label: core.string.Ref, icon: core.icon.TypeRef, to: _class } -} - -/** - * @public - */ -export function TypeIdentifier (of: Ref): TypeIdentifierType { - return { _class: core.class.TypeIdentifier, label: core.string.Id, icon: core.icon.TypeRef, of } -} - -/** - * @public - */ -export function TypeEnum (of: Ref): EnumOf { - return { _class: core.class.EnumOf, label: core.string.Enum, icon: core.icon.TypeEnumOf, of } -} - -/** - * @public - */ -export function TypeFileSize (): Type { - return { _class: core.class.TypeFileSize, label: core.string.Size, icon: core.icon.TypeNumber } -} - -/** - * @public - */ -export function TypeAny ( - presenter: AnyComponent, - label: IntlString, - editor?: AnyComponent -): TypeAnyType { - return { _class: core.class.TypeAny, label, presenter, editor } -} - -/** - * @public - */ -export function Collection (clazz: Ref>, itemLabel?: IntlString): TypeCollection { - return { - _class: core.class.Collection, - label: core.string.Collection, - icon: core.icon.TypeCollection, - of: clazz, - itemLabel - } -} - -/** - * @public - */ -export function ArrOf> (type: Type): TypeArrOf { - return { _class: core.class.ArrOf, label: core.string.Array, of: type, icon: core.icon.TypeArray } -} - -/** - * @public - */ -export function TypeCollaborativeDoc (): Type { - return { _class: core.class.TypeCollaborativeDoc, label: core.string.MarkupBlobRef, icon: core.icon.TypeMarkup } -} - -/** - * @public - */ -export function TypeRank (): Type { - return { _class: core.class.TypeRank, label: core.string.Rank, icon: core.icon.TypeRank } -} - -export function TypePersonId (): Type { - return { _class: core.class.TypePersonId, label: core.string.PersonId } -} - -export function TypeAccountUuid (): Type { - return { _class: core.class.TypeAccountUuid, label: core.string.AccountId } -} diff --git a/foundations/core/packages/model/src/index.ts b/foundations/core/packages/model/src/index.ts deleted file mode 100644 index a6d143ca66..0000000000 --- a/foundations/core/packages/model/src/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -// -// Copyright © 2020 Anticrm Platform Contributors. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export * from './dsl' -export * from './migration' -export * from './utils' diff --git a/foundations/core/packages/model/src/migration.ts b/foundations/core/packages/model/src/migration.ts deleted file mode 100644 index 0c97d1ebb0..0000000000 --- a/foundations/core/packages/model/src/migration.ts +++ /dev/null @@ -1,312 +0,0 @@ -import { type AccountClient } from '@hcengineering/account-client' -import { Analytics } from '@hcengineering/analytics' -import core, { - type Class, - type Client, - DOMAIN_MIGRATION, - DOMAIN_TX, - type Data, - type Doc, - type DocumentQuery, - type Domain, - type FindOptions, - type Hierarchy, - type MeasureContext, - type MigrationState, - type ModelDb, - type ObjQueryType, - type Rank, - type Ref, - SortingOrder, - type Space, - TxOperations, - type UnsetOptions, - type WorkspaceIds, - generateId -} from '@hcengineering/core' -import { makeRank } from '@hcengineering/rank' -import { type StorageAdapter } from '@hcengineering/storage' -import { type ModelLogger } from './utils' - -/** - * @public - */ -export type MigrateUpdate = Partial & UnsetOptions & Record - -/** - * @public - */ -export interface MigrationResult { - matched: number - updated: number -} - -/** - * @public - */ -export type MigrationDocumentQuery = { - [P in keyof T]?: ObjQueryType | null -} & { - $search?: string - // support nested queries e.g. 'user.friends.name' - // this will mark all unrecognized properties as any (including nested queries) - [key: string]: any -} - -/** - * @public - */ -export interface MigrationIterator { - next: (count: number) => Promise - close: () => Promise -} - -/** - * @public - * Client to perform model upgrades - */ -export interface MigrationClient { - // Raw collection operations - - // Raw FIND, allow to find documents inside domain. - find: ( - domain: Domain, - query: MigrationDocumentQuery, - options?: Omit, 'lookup'> - ) => Promise - - // Raw group by, allow to group documents inside domain. - groupBy: (domain: Domain, field: string, query?: DocumentQuery

) => Promise> - - // Traverse documents - traverse: ( - domain: Domain, - query: MigrationDocumentQuery, - options?: Pick, 'sort' | 'limit' | 'projection'> - ) => Promise> - - // Allow to raw update documents inside domain. - update: ( - domain: Domain, - query: MigrationDocumentQuery, - operations: MigrateUpdate - ) => Promise - - bulk: ( - domain: Domain, - operations: { filter: MigrationDocumentQuery, update: MigrateUpdate }[] - ) => Promise - - // Move documents per domain - move: ( - sourceDomain: Domain, - query: DocumentQuery, - targetDomain: Domain, - size?: number - ) => Promise - - create: (domain: Domain, doc: T | T[]) => Promise - delete: (domain: Domain, _id: Ref) => Promise - deleteMany: (domain: Domain, query: DocumentQuery) => Promise - - hierarchy: Hierarchy - model: ModelDb - - migrateState: Map> - storageAdapter: StorageAdapter - accountClient: AccountClient - - wsIds: WorkspaceIds - - fullReindex: () => Promise - reindex: (domain: Domain, classes: Ref>[]) => Promise - readonly logger: ModelLogger - readonly ctx: MeasureContext -} - -/** - * @public - */ -export type MigrationUpgradeClient = Client -export type MigrateMode = 'create' | 'upgrade' - -/** - * @public - */ -export interface MigrateOperation { - // Perform low level migration prior to the model update - preMigrate?: (client: MigrationClient, logger: ModelLogger, mode: MigrateMode) => Promise - // Perform low level migration - migrate: (client: MigrationClient, mode: MigrateMode) => Promise - // Perform high level upgrade operations. - upgrade: ( - state: Map>, - client: () => Promise, - mode: MigrateMode - ) => Promise -} - -/** - * @public - */ -export interface Migrations { - state: string - mode?: MigrateMode // If set only applied to specified mode - func: (client: MigrationClient, mode: MigrateMode) => Promise -} - -/** - * @public - */ -export interface UpgradeOperations { - state: string - mode?: MigrateMode // If set only applied to specified mode - func: (client: MigrationUpgradeClient, mode: MigrateMode) => Promise -} - -/** - * @public - */ -export async function tryMigrate ( - mode: MigrateMode, - client: MigrationClient, - plugin: string, - migrations: Migrations[] -): Promise { - const states = client.migrateState.get(plugin) ?? new Set() - for (const migration of migrations) { - if (states.has(migration.state)) continue - if (migration.mode == null || migration.mode === mode) { - try { - client.logger.log('running migration', { plugin, state: migration.state }) - await migration.func(client, mode) - } catch (err: any) { - client.logger.error('Failed to run migration', { plugin, state: migration.state, err }) - Analytics.handleError(err) - continue - } - } - const st: MigrationState = { - plugin, - state: migration.state, - space: core.space.Configuration, - modifiedBy: core.account.System, - modifiedOn: Date.now(), - _class: core.class.MigrationState, - _id: generateId() - } - await client.create(DOMAIN_MIGRATION, st) - } -} - -/** - * @public - */ -export async function tryUpgrade ( - mode: MigrateMode, - state: Map>, - client: () => Promise, - plugin: string, - migrations: UpgradeOperations[] -): Promise { - const states = state.get(plugin) ?? new Set() - for (const upgrades of migrations) { - if (states.has(upgrades.state)) continue - const _client = await client() - if (upgrades.mode == null || upgrades.mode === mode) { - try { - await upgrades.func(_client, mode) - } catch (err: any) { - console.error(err) - Analytics.handleError(err) - continue - } - } - const st: Data = { - plugin, - state: upgrades.state - } - const tx = new TxOperations(_client, core.account.System) - await tx.createDoc(core.class.MigrationState, core.space.Configuration, st) - } -} - -type DefaultSpaceData = Pick -type RequiredData = Omit, keyof DefaultSpaceData> & Partial> - -/** - * @public - */ -export async function createDefaultSpace ( - client: MigrationUpgradeClient, - _id: Ref, - props: RequiredData, - _class: Ref> = core.class.SystemSpace -): Promise { - const defaults: DefaultSpaceData = { - description: '', - private: false, - archived: false, - members: [] - } - const data: Data = { - ...defaults, - ...props - } - const tx = new TxOperations(client, core.account.System) - const current = await tx.findOne(core.class.Space, { - _id - }) - if (current === undefined || current._class !== _class) { - if (current !== undefined && current._class !== _class) { - await tx.remove(current) - } - await tx.createDoc(_class, core.space.Space, data, _id) - } -} - -/** - * @public - */ -export async function migrateSpace ( - client: MigrationClient, - from: Ref, - to: Ref, - domains: Domain[] -): Promise { - for (const domain of domains) { - await client.update(domain, { space: from }, { space: to }) - } - await client.update(DOMAIN_TX, { objectSpace: from }, { objectSpace: to }) -} - -export async function migrateSpaceRanks (client: MigrationClient, domain: Domain, space: Space): Promise { - type WithRank = Doc & { rank: Rank } - - const iterator = await client.traverse( - domain, - { space: space._id, rank: { $exists: true } }, - { sort: { rank: SortingOrder.Ascending } } - ) - - try { - let rank = '0|100000:' - - while (true) { - const docs = await iterator.next(1000) - if (docs === null || docs.length === 0) { - break - } - - const updates: { filter: MigrationDocumentQuery>, update: MigrateUpdate> }[] = [] - for (const doc of docs) { - rank = makeRank(rank, undefined) - updates.push({ filter: { _id: doc._id }, update: { rank } }) - } - - await client.bulk(domain, updates) - } - } finally { - await iterator.close() - } -} diff --git a/foundations/core/packages/model/src/utils.ts b/foundations/core/packages/model/src/utils.ts deleted file mode 100644 index e333ebbde3..0000000000 --- a/foundations/core/packages/model/src/utils.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { - type Class, - type Data, - type Doc, - type DocumentUpdate, - type Ref, - type Space, - type TxOperations, - type IdMap -} from '@hcengineering/core' -import { deepEqual } from 'fast-equals' - -function toUndef (value: any): any { - return value === null ? undefined : value -} - -function diffAttributes (doc: Data, newDoc: Data): DocumentUpdate { - const result: DocumentUpdate = {} - const allDocuments = new Map(Object.entries(doc)) - const newDocuments = new Map(Object.entries(newDoc)) - - for (const [key, value] of allDocuments) { - if (!newDocuments.has(key)) { - continue - } - - const newValue = toUndef(newDocuments.get(key)) - if (!deepEqual(newValue, toUndef(value))) { - // update is required, since values are different - result[key] = newValue - } - } - for (const [key, value] of newDocuments) { - const oldValue = toUndef(allDocuments.get(key)) - if (oldValue === undefined && value !== undefined) { - // Update with new value. - result[key] = value - } - } - return result -} - -/** - * Create or update document if modified only by system account. - * @public - */ -export async function createOrUpdate ( - client: TxOperations, - _class: Ref>, - space: Ref, - data: Data, - _id: Ref, - cache?: IdMap -): Promise { - const existingDoc = cache !== undefined ? cache.get(_id) : await client.findOne(_class, { _id }) - if (existingDoc !== undefined) { - const { _class: _oldClass, _id, space: _oldSpace, modifiedBy, modifiedOn, ...oldData } = existingDoc - if (modifiedBy === client.txFactory.account) { - const updateOp = diffAttributes(oldData, data) - if (Object.keys(updateOp).length > 0) { - await client.update(existingDoc, updateOp) - } - } - } else { - await client.createDoc(_class, space, data, _id) - } -} - -/** - * @public - */ -export interface ModelLogger { - log: (msg: string, data: any) => void - error: (msg: string, err: any) => void -} - -const errorPrinter = ({ message, stack, ...rest }: Error): object => ({ - message, - stack, - ...rest -}) -function replacer (value: any): any { - return value instanceof Error ? errorPrinter(value) : value -} - -/** - * @public - */ -export const consoleModelLogger: ModelLogger = { - log (msg: string, data: any): void { - console.log(msg, data) - }, - error (msg: string, data: any): void { - console.error(msg, replacer(data)) - } -} diff --git a/foundations/core/packages/model/tsconfig.json b/foundations/core/packages/model/tsconfig.json deleted file mode 100644 index c6a877cf6c..0000000000 --- a/foundations/core/packages/model/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/node/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/foundations/core/packages/platform/.eslintrc.js b/foundations/core/packages/platform/.eslintrc.js deleted file mode 100644 index 72235dc283..0000000000 --- a/foundations/core/packages/platform/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/foundations/core/packages/platform/.npmignore b/foundations/core/packages/platform/.npmignore deleted file mode 100644 index e3ec093c38..0000000000 --- a/foundations/core/packages/platform/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!/lib/** -!CHANGELOG.md -/lib/**/__tests__/ diff --git a/foundations/core/packages/platform/CHANGELOG.json b/foundations/core/packages/platform/CHANGELOG.json deleted file mode 100644 index 5419f6f502..0000000000 --- a/foundations/core/packages/platform/CHANGELOG.json +++ /dev/null @@ -1,101 +0,0 @@ -{ - "name": "@hcengineering/platform", - "entries": [ - { - "version": "0.7.18", - "tag": "@hcengineering/platform_v0.7.18", - "date": "Fri, 31 Oct 2025 20:11:13 GMT", - "comments": { - "patch": [ - { - "comment": "add pwd login locked status" - } - ] - } - }, - { - "version": "0.7.5", - "tag": "@hcengineering/platform_v0.7.5", - "date": "Tue, 14 Oct 2025 04:58:17 GMT", - "comments": { - "patch": [ - { - "comment": "update deps" - } - ] - } - }, - { - "version": "0.7.4", - "tag": "@hcengineering/platform_v0.7.4", - "date": "Sat, 11 Oct 2025 18:20:33 GMT", - "comments": { - "patch": [ - { - "comment": "Update to latest platform rig" - } - ] - } - }, - { - "version": "0.7.0", - "tag": "@hcengineering/platform_v0.6.5", - "date": "Sun, 08 Aug 2021 11:34:23 GMT", - "comments": { - "patch": [ - { - "comment": "ES6 target" - } - ] - } - }, - { - "version": "0.7.0", - "tag": "@hcengineering/platform_v0.6.4", - "date": "Sun, 08 Aug 2021 10:14:57 GMT", - "comments": { - "patch": [ - { - "comment": "Export status codes" - } - ] - } - }, - { - "version": "0.7.0", - "tag": "@hcengineering/platform_v0.6.3", - "date": "Wed, 04 Aug 2021 21:18:44 GMT", - "comments": { - "patch": [ - { - "comment": "fix" - } - ] - } - }, - { - "version": "0.7.0", - "tag": "@hcengineering/platform_v0.6.2", - "date": "Wed, 04 Aug 2021 20:48:46 GMT", - "comments": { - "patch": [ - { - "comment": "npmignore" - } - ] - } - }, - { - "version": "0.7.0", - "tag": "@hcengineering/platform_v0.6.1", - "date": "Wed, 04 Aug 2021 17:38:30 GMT", - "comments": { - "patch": [ - { - "comment": "Minor changes for publish" - } - ] - } - } - ] -} diff --git a/foundations/core/packages/platform/CHANGELOG.md b/foundations/core/packages/platform/CHANGELOG.md deleted file mode 100644 index 5e302bba35..0000000000 --- a/foundations/core/packages/platform/CHANGELOG.md +++ /dev/null @@ -1,60 +0,0 @@ -# Change Log - @hcengineering/platform - -This log was last generated on Fri, 31 Oct 2025 20:11:13 GMT and should not be manually modified. - -## 0.7.18 -Fri, 31 Oct 2025 20:11:13 GMT - -### Patches - -- add pwd login locked status - -## 0.7.5 -Tue, 14 Oct 2025 04:58:17 GMT - -### Patches - -- update deps - -## 0.7.4 -Sat, 11 Oct 2025 18:20:33 GMT - -### Patches - -- Update to latest platform rig - -## 0.7.0 -Sun, 08 Aug 2021 11:34:23 GMT - -### Patches - -- ES6 target - -## 0.7.0 -Sun, 08 Aug 2021 10:14:57 GMT - -### Patches - -- Export status codes - -## 0.7.0 -Wed, 04 Aug 2021 21:18:44 GMT - -### Patches - -- fix - -## 0.7.0 -Wed, 04 Aug 2021 20:48:46 GMT - -### Patches - -- npmignore - -## 0.7.0 -Wed, 04 Aug 2021 17:38:30 GMT - -### Patches - -- Minor changes for publish - diff --git a/foundations/core/packages/platform/config/rig.json b/foundations/core/packages/platform/config/rig.json deleted file mode 100644 index 0110930f55..0000000000 --- a/foundations/core/packages/platform/config/rig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig" -} diff --git a/foundations/core/packages/platform/jest.config.js b/foundations/core/packages/platform/jest.config.js deleted file mode 100644 index 069ea8aa0d..0000000000 --- a/foundations/core/packages/platform/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ['./src'], - coverageReporters: ['text-summary', 'html', 'lcov'] -} diff --git a/foundations/core/packages/platform/lang/cs.json b/foundations/core/packages/platform/lang/cs.json deleted file mode 100644 index aa90e18d92..0000000000 --- a/foundations/core/packages/platform/lang/cs.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "status": { - "LoadingPlugin": "Načítání pluginu {plugin}...", - "UnknownError": "Neznámá chyba: {message}", - "InvalidId": "Neplatné ID: {id}", - "BadRequest": "Špatný požadavek", - "Forbidden": "Zakázáno", - "Conflict": "Konflikt", - "ExpiredLink": "Tento odkaz na pozvánku vypršel", - "Unauthorized": "Neoprávněný přístup", - "UnknownMethod": "Neznámá metoda: {method}", - "InternalServerError": "Interní chyba serveru", - "MaintenanceWarning": "Plánovaná údržba za", - "MaintenanceWarningTime": "{time, plural, =1 {méně než minutu} other {# minut}}", - "AccountNotFound": "Účet nenalezen", - "AccountNotConfirmed": "Účet není potvrzen", - "WorkspaceNotFound": "Pracovní prostor nenalezen", - "InvalidPassword": "Neplatné heslo", - "AccountAlreadyExists": "Účet již existuje", - "WorkspaceRateLimit": "Server je zaneprázdněný. Prosím, chvíli počkejte a zkuste to znovu", - "AccountAlreadyConfirmed": "Účet již byl potvrzen", - "WorkspaceAlreadyExists": "Pracovní prostor již existuje", - "InvalidOtp": "Neplatný kód", - "PasswordLoginLocked": "Přihlášení heslem je zablokováno z důvodu příliš mnoha neúspěšných pokusů. Pomocí metody přihlášení OTP odemkněte svůj účet.", - "InviteNotFound": "Pozvánka s e-mailem:{email} nenalezena.", - "WorkspaceLimitReached": "Dosáhli jste limitu pracovních prostorů. Kontaktujte nás...", - "ReadOnlyAccount": "Anonymní hostovská ukázka", - "SystemAccount": "Systémový účet", - "SocialIdAlreadyExists": "Sociální ID již existuje" - } -} diff --git a/foundations/core/packages/platform/lang/de.json b/foundations/core/packages/platform/lang/de.json deleted file mode 100644 index 2e70d7586b..0000000000 --- a/foundations/core/packages/platform/lang/de.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "status": { - "LoadingPlugin": "Plugin {plugin} wird geladen...", - "UnknownError": "Unbekannter Fehler: {message}", - "InvalidId": "Ungültige ID: {id}", - "BadRequest": "Fehlerhafte Anfrage", - "Forbidden": "Zugriff verweigert", - "Conflict": "Konflikt", - "ExpiredLink": "Dieser Einladungslink ist abgelaufen", - "Unauthorized": "Nicht autorisiert", - "UnknownMethod": "Unbekannte Methode: {method}", - "InternalServerError": "Interner Serverfehler", - "MaintenanceWarning": "Wartung geplant in", - "MaintenanceWarningTime": "{time, plural, =1 {weniger als einer Minute} other {# Minuten}}", - "AccountNotFound": "Konto nicht gefunden", - "AccountNotConfirmed": "Konto nicht bestätigt", - "WorkspaceNotFound": "Arbeitsbereich nicht gefunden", - "InvalidPassword": "Ungültiges Passwort", - "AccountAlreadyExists": "Konto existiert bereits", - "WorkspaceRateLimit": "Server ist ausgelastet, bitte warten Sie einen Moment und versuchen Sie es erneut", - "AccountAlreadyConfirmed": "Konto wurde bereits bestätigt", - "WorkspaceAlreadyExists": "Arbeitsbereich existiert bereits", - "InvalidOtp": "Ungültiger Code", - "PasswordLoginLocked": "Die Passwort-Anmeldung ist aufgrund zu vieler fehlgeschlagener Versuche gesperrt. Bitte verwenden Sie eine OTP-Anmeldungsmethode, um Ihr Konto freizuschalten.", - "InviteNotFound": "Einladung mit E-Mail:{email} nicht gefunden.", - "WorkspaceLimitReached": "Sie haben das Arbeitsbereichslimit erreicht. Bitte kontaktieren Sie uns...", - "ReadOnlyAccount": "Anonymer Gast-Demo", - "SystemAccount": "Systemkonto", - "SocialIdAlreadyExists": "Social ID existiert bereits" - } -} diff --git a/foundations/core/packages/platform/lang/en.json b/foundations/core/packages/platform/lang/en.json deleted file mode 100644 index 80a3469ab4..0000000000 --- a/foundations/core/packages/platform/lang/en.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "status": { - "LoadingPlugin": "Loading plugin {plugin}...", - "UnknownError": "Unknown error: {message}", - "InvalidId": "Invalid Id: {id}", - "BadRequest": "Bad request", - "Forbidden": "Forbidden", - "Conflict": "Conflict", - "ExpiredLink": "This invite link is expired", - "Unauthorized": "Unauthorized", - "UnknownMethod": "Unknown method: {method}", - "InternalServerError": "Internal server error", - "MaintenanceWarning": "Maintenance Scheduled in", - "MaintenanceWarningTime": "{time, plural, =1 {less than a minute} other {# minutes}}", - "AccountNotFound": "Account not found or the provided credentials are incorrect", - "AccountNotConfirmed": "Account not confirmed", - "WorkspaceNotFound": "Workspace not found", - "AccountAlreadyExists": "Account already exists", - "WorkspaceRateLimit": "Server is busy, Please wait a bit and try again", - "AccountAlreadyConfirmed": "Account already confirmed", - "WorkspaceAlreadyExists": "Workspace already exists", - "InvalidOtp": "Invalid code", - "PasswordLoginLocked": "Password login is locked due to too many failed attempts. Please use an OTP login method to unlock your account.", - "InviteNotFound": "Invitation with email:{email} not found.", - "WorkspaceLimitReached": "You have reached the workspace limit. Please contact us...", - "ReadOnlyAccount": "Anonymous Guest Demo", - "SystemAccount": "System account", - "SocialIdAlreadyExists": "Social ID already exists" - } -} diff --git a/foundations/core/packages/platform/lang/es.json b/foundations/core/packages/platform/lang/es.json deleted file mode 100644 index f1aa67ba95..0000000000 --- a/foundations/core/packages/platform/lang/es.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "status": { - "LoadingPlugin": "Cargando complemento {plugin}...", - "UnknownError": "Error desconocido: {message}", - "InvalidId": "Id no válido: {id}", - "BadRequest": "Solicitud incorrecta", - "Forbidden": "Prohibido", - "Conflict": "Conflicto", - "ExpiredLink": "Este enlace de invitación ha caducado", - "Unauthorized": "No autorizado", - "UnknownMethod": "Método desconocido: {method}", - "InternalServerError": "Error interno del servidor", - "MaintenanceWarning": "Mantenimiento programado dentro de", - "MaintenanceWarningTime": "{time, plural, =1 {menos de un minuto} other {# minutos}}", - "AccountNotFound": "Cuenta no encontrada", - "AccountNotConfirmed": "Cuenta no confirmada", - "WorkspaceNotFound": "Espacio de trabajo no encontrado", - "InvalidPassword": "Contraseña inválida", - "AccountAlreadyExists": "La cuenta ya existe", - "WorkspaceRateLimit": "El servidor está ocupado. Espere un momento e inténtelo de nuevo", - "AccountAlreadyConfirmed": "La cuenta ya está confirmada", - "WorkspaceAlreadyExists": "El espacio de trabajo ya existe", - "InvalidOtp": "Código inválido", - "PasswordLoginLocked": "El inicio de sesión con contraseña está bloqueado debido a demasiados intentos fallidos. Use el método de inicio de sesión OTP para desbloquear su cuenta.", - "InviteNotFound": "Invitación con correo electrónico:{email} no encontrada.", - "WorkspaceLimitReached": "Ha alcanzado el límite de espacios de trabajo. Póngase en contacto con nosotros...", - "ReadOnlyAccount": "Demo de invitado anónimo", - "SystemAccount": "Cuenta del sistema", - "SocialIdAlreadyExists": "El ID social ya existe" - } -} diff --git a/foundations/core/packages/platform/lang/fr.json b/foundations/core/packages/platform/lang/fr.json deleted file mode 100644 index cae2833fc1..0000000000 --- a/foundations/core/packages/platform/lang/fr.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "status": { - "LoadingPlugin": "Chargement du plugin {plugin}...", - "UnknownError": "Erreur inconnue : {message}", - "InvalidId": "Id invalide : {id}", - "BadRequest": "Mauvaise requête", - "Forbidden": "Interdit", - "Conflict": "Conflit", - "ExpiredLink": "Ce lien d'invitation est expiré", - "Unauthorized": "Non autorisé", - "UnknownMethod": "Méthode inconnue : {method}", - "InternalServerError": "Erreur interne du serveur", - "MaintenanceWarning": "Maintenance prévue dans", - "MaintenanceWarningTime": "{time, plural, =1 {moins d'une minute} other {# minutes}}", - "AccountNotFound": "Compte non trouvé", - "AccountNotConfirmed": "Compte non confirmé", - "WorkspaceNotFound": "Espace de travail non trouvé", - "InvalidPassword": "Mot de passe invalide", - "AccountAlreadyExists": "Le compte existe déjà", - "WorkspaceRateLimit": "Le serveur est occupé, veuillez patienter un moment et réessayer", - "AccountAlreadyConfirmed": "Compte déjà confirmé", - "WorkspaceAlreadyExists": "L'espace de travail existe déjà", - "InvalidOtp": "Code invalide", - "PasswordLoginLocked": "La connexion par mot de passe est verrouillée en raison de trop nombreuses tentatives échouées. Veuillez utiliser une méthode de connexion OTP pour déverrouiller votre compte.", - "InviteNotFound": "Invitation avec e-mail:{email} introuvable.", - "WorkspaceLimitReached": "Vous avez atteint la limite d'espace de travail. Veuillez contacter nous...", - "ReadOnlyAccount": "Démo invité anonyme", - "SystemAccount": "Compte système", - "SocialIdAlreadyExists": "L'ID social existe déjà" - } -} diff --git a/foundations/core/packages/platform/lang/it.json b/foundations/core/packages/platform/lang/it.json deleted file mode 100644 index a8577635e2..0000000000 --- a/foundations/core/packages/platform/lang/it.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "status": { - "LoadingPlugin": "Caricamento del plugin {plugin}...", - "UnknownError": "Errore sconosciuto: {message}", - "InvalidId": "Id non valido: {id}", - "BadRequest": "Richiesta non valida", - "Forbidden": "Proibito", - "Conflict": "Conflitto", - "ExpiredLink": "Questo link di invito è scaduto", - "Unauthorized": "Non autorizzato", - "UnknownMethod": "Metodo sconosciuto: {method}", - "InternalServerError": "Errore interno del server", - "MaintenanceWarning": "Manutenzione programmata tra", - "MaintenanceWarningTime": "{time, plural, =1 {meno di un minuto} other {# minuti}}", - "AccountNotFound": "Account non trovato", - "AccountNotConfirmed": "Account non confermato", - "WorkspaceNotFound": "Spazio di lavoro non trovato", - "InvalidPassword": "Password non valida", - "AccountAlreadyExists": "Account già esistente", - "WorkspaceRateLimit": "Il server è occupato, attendere un momento e riprovare", - "AccountAlreadyConfirmed": "Account già confermato", - "WorkspaceAlreadyExists": "Spazio di lavoro già esistente", - "InvalidOtp": "Codice non valido", - "PasswordLoginLocked": "L'accesso con password è bloccato a causa di troppi tentativi non riusciti. Utilizza un metodo di accesso OTP per sbloccare il tuo account.", - "InviteNotFound": "Invito con email:{email} non trovato.", - "WorkspaceLimitReached": "Hai raggiunto il limite di spazi di lavoro. Contattaci...", - "ReadOnlyAccount": "Demo ospite anonimo", - "SystemAccount": "Account di sistema", - "SocialIdAlreadyExists": "L'ID social esiste già" - } -} diff --git a/foundations/core/packages/platform/lang/ja.json b/foundations/core/packages/platform/lang/ja.json deleted file mode 100644 index 8a54330f22..0000000000 --- a/foundations/core/packages/platform/lang/ja.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "status": { - "LoadingPlugin": "プラグイン {plugin} を読み込み中…", - "UnknownError": "不明なエラー: {message}", - "InvalidId": "無効なID: {id}", - "BadRequest": "不正なリクエスト", - "Forbidden": "アクセスが禁止されています", - "Conflict": "競合", - "ExpiredLink": "この招待リンクは期限切れです", - "Unauthorized": "認証されていません", - "UnknownMethod": "不明なメソッド: {method}", - "InternalServerError": "サーバー内部エラー", - "MaintenanceWarning": "メンテナンスが予定されています", - "MaintenanceWarningTime": "{time, plural, =1 {1分以内に} other {#分後に}}", - "AccountNotFound": "アカウントが見つからないか、認証情報が間違っています", - "AccountNotConfirmed": "アカウントが確認されていません", - "WorkspaceNotFound": "ワークスペースが見つかりません", - "AccountAlreadyExists": "このアカウントはすでに存在します", - "WorkspaceRateLimit": "サーバーが混雑しています。しばらく待ってから再度お試しください", - "AccountAlreadyConfirmed": "このアカウントはすでに確認済みです", - "WorkspaceAlreadyExists": "ワークスペースはすでに存在します", - "InvalidOtp": "無効なコード", - "PasswordLoginLocked": "ログイン試行の失敗が多すぎるため、パスワードログインはロックされています。OTPログイン方法を使用してアカウントのロックを解除してください。", - "InviteNotFound": "メール:{email} の招待が見つかりません。", - "WorkspaceLimitReached": "作成可能なワークスペースの上限に達しました。お問い合わせください", - "ReadOnlyAccount": "匿名ゲストデモ", - "SystemAccount": "システムアカウント", - "SocialIdAlreadyExists": "ソーシャルIDは既に存在します" - } -} diff --git a/foundations/core/packages/platform/lang/pt.json b/foundations/core/packages/platform/lang/pt.json deleted file mode 100644 index a66bf1fcc7..0000000000 --- a/foundations/core/packages/platform/lang/pt.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "status": { - "LoadingPlugin": "Carregando plugin {plugin}...", - "UnknownError": "Erro desconhecido: {message}", - "InvalidId": "Id inválido: {id}", - "BadRequest": "Pedido inválido", - "Forbidden": "Proibido", - "Conflict": "Conflito", - "ExpiredLink": "Este link de convite expirou", - "Unauthorized": "Não autorizado", - "UnknownMethod": "Método desconhecido: {method}", - "InternalServerError": "Erro interno do servidor", - "MaintenanceWarning": "Manutenção programada dentro de", - "MaintenanceWarningTime": "{time, plural, =1 {menos de um minuto} other {# minutos}}", - "AccountNotFound": "Conta não encontrada", - "AccountNotConfirmed": "Conta não confirmada", - "WorkspaceNotFound": "Espaço de trabalho não encontrado", - "InvalidPassword": "Senha inválida", - "AccountAlreadyExists": "Conta já existe", - "WorkspaceRateLimit": "O servidor está ocupado. Por favor, espere um pouco e tente novamente", - "AccountAlreadyConfirmed": "Conta já confirmada", - "WorkspaceAlreadyExists": "Espaço de trabalho já existe", - "InvalidOtp": "Código inválido", - "PasswordLoginLocked": "O login com senha está bloqueado devido a muitas tentativas falhadas. Use um método de login OTP para desbloquear sua conta.", - "InviteNotFound": "Convite com e-mail:{email} não encontrado.", - "WorkspaceLimitReached": "Você atingiu o limite de espaço de trabalho. Entre em contato conosco...", - "ReadOnlyAccount": "Demonstração anônima de convidado", - "SystemAccount": "Conta do sistema", - "SocialIdAlreadyExists": "ID social já existe" - } -} diff --git a/foundations/core/packages/platform/lang/ru.json b/foundations/core/packages/platform/lang/ru.json deleted file mode 100644 index 8715a2ba63..0000000000 --- a/foundations/core/packages/platform/lang/ru.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "status": { - "LoadingPlugin": "Загрузка плагина {plugin}...", - "UnknownError": "Неизвестная ошибка: {message}", - "InvalidId": "Некорректный Id: {id}", - "BadRequest": "Некорректный запрос", - "Forbidden": "Запрещено", - "Conflict": "Конфликт", - "ExpiredLink": "Ссылка истекла", - "Unauthorized": "Неавторизован", - "UnknownMethod": "Неизвестный метод: {method}", - "InternalServerError": "Внутренняя ошибка сервера", - "MaintenanceWarning": "Серверные работы запланированы через", - "MaintenanceWarningTime": "{time, plural, one {# минуту} few {# минуты} other {# минут}}", - "AccountNotFound": "Аккаунт не найден", - "AccountNotConfirmed": "Аккаунт не подтвержден", - "WorkspaceNotFound": "Рабочее пространство не найдено", - "InvalidPassword": "Неверный пароль", - "AccountAlreadyExists": "Аккаунт уже существует", - "WorkspaceRateLimit": "Сервер перегружен, пожалуйста, подождите", - "AccountAlreadyConfirmed": "Аккаунт уже подтвержден", - "WorkspaceAlreadyExists": "Рабочее пространство уже существует", - "InvalidOtp": "Неверный код", - "PasswordLoginLocked": "Вход по паролю заблокирован из-за слишком большого количества неудачных попыток. Используйте метод входа OTP для разблокировки аккаунта.", - "InviteNotFound": "Приглашение с электронной почтой:{email} не найдено.", - "WorkspaceLimitReached": "Вы достигли лимита рабочих пространств. Свяжитесь с нами...", - "ReadOnlyAccount": "Анонимное гостевое демо", - "SystemAccount": "Системный аккаунт", - "SocialIdAlreadyExists": "Социальный ID уже существует" - } -} diff --git a/foundations/core/packages/platform/lang/tr.json b/foundations/core/packages/platform/lang/tr.json deleted file mode 100644 index 681b17fb03..0000000000 --- a/foundations/core/packages/platform/lang/tr.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "status": { - "LoadingPlugin": "{plugin} eklentisi yükleniyor...", - "UnknownError": "Bilinmeyen hata: {message}", - "InvalidId": "Geçersiz Id: {id}", - "BadRequest": "Hatalı istek", - "Forbidden": "Yasak", - "Conflict": "Çakışma", - "ExpiredLink": "Bu davet bağlantısının süresi dolmuş", - "Unauthorized": "Yetkisiz", - "UnknownMethod": "Bilinmeyen metod: {method}", - "InternalServerError": "Dahili sunucu hatası", - "MaintenanceWarning": "Bakım Planlandı:", - "MaintenanceWarningTime": "{time, plural, =1 {bir dakikadan az} other {# dakika}}", - "AccountNotFound": "Hesap bulunamadı veya girilen kimlik bilgileri yanlış", - "AccountNotConfirmed": "Hesap doğrulanmadı", - "WorkspaceNotFound": "Çalışma alanı bulunamadı", - "AccountAlreadyExists": "Hesap zaten mevcut", - "WorkspaceRateLimit": "Sunucu meşgul, Lütfen biraz bekleyin ve tekrar deneyin", - "AccountAlreadyConfirmed": "Hesap zaten doğrulandı", - "WorkspaceAlreadyExists": "Çalışma alanı zaten mevcut", - "InvalidOtp": "Geçersiz kod", - "PasswordLoginLocked": "Çok fazla başarısız deneme nedeniyle parola girişi kilitlendi. Hesabınızın kilidini açmak için OTP giriş yöntemini kullanın.", - "InviteNotFound": "E-mail:{email} ile ilgili davet bulunamadı.", - "WorkspaceLimitReached": "Çalışma alanı limitine ulaştınız. Lütfen bizimle iletişime geçin...", - "ReadOnlyAccount": "Anonim Misafir Demo", - "SystemAccount": "Sistem hesabı", - "SocialIdAlreadyExists": "Sosyal ID zaten mevcut" - } -} diff --git a/foundations/core/packages/platform/lang/zh.json b/foundations/core/packages/platform/lang/zh.json deleted file mode 100644 index 034b0824e5..0000000000 --- a/foundations/core/packages/platform/lang/zh.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "status": { - "LoadingPlugin": "正在加载插件 {plugin}...", - "UnknownError": "未知错误: {message}", - "InvalidId": "无效的 Id: {id}", - "BadRequest": "错误的请求", - "Forbidden": "禁止访问", - "Conflict": "冲突", - "ExpiredLink": "此邀请链接已过期", - "Unauthorized": "未授权", - "UnknownMethod": "未知方法: {method}", - "InternalServerError": "内部服务器错误", - "MaintenanceWarning": "计划在 内进行维护 ", - "MaintenanceWarningTime": "{time, plural, =1 {不到一分钟} other {# 分钟}}", - "AccountNotFound": "账户未找到", - "AccountNotConfirmed": "账户未确认", - "WorkspaceNotFound": "工作区未找到", - "InvalidPassword": "无效的密码", - "AccountAlreadyExists": "账户已存在", - "WorkspaceRateLimit": "服务器繁忙,请稍后再试", - "AccountAlreadyConfirmed": "账户已确认", - "WorkspaceAlreadyExists": "工作区已存在", - "InvalidOtp": "无效的代码", - "PasswordLoginLocked": "由于登录尝试失败次数过多,密码登录已被锁定。请使用 OTP 登录方式解锁您的账户。", - "InviteNotFound": "未找到包含以下电子邮件的邀请:{email}。", - "WorkspaceLimitReached": "您已达到工作区限制。请联系我们...", - "ReadOnlyAccount": "匿名访客演示", - "SocialIdAlreadyExists": "社交ID已存在" - } -} diff --git a/foundations/core/packages/platform/package.json b/foundations/core/packages/platform/package.json deleted file mode 100644 index 841cc6d919..0000000000 --- a/foundations/core/packages/platform/package.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "name": "@hcengineering/platform", - "version": "0.7.18", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "files": [ - "lib/**/*", - "types/**/*", - "src/**/*", - "!src/**/__test__/**", - "tsconfig.json", - "lang/**/*" - ], - "author": "Anticrm Platform Contributors", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "build:watch": "compile", - "test": "jest --passWithNoTests --silent --coverage", - "format": "format src", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --coverage", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "workspace:^0.7.19", - "@types/jest": "^29.5.5", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "@typescript-eslint/parser": "^6.21.0", - "eslint": "^8.54.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-n": "^15.4.0", - "eslint-plugin-promise": "^6.1.1", - "jest": "^29.7.0", - "prettier": "^3.6.2", - "ts-jest": "^29.1.1", - "typescript": "^5.9.3", - "eslint-plugin-svelte": "^2.35.1" - }, - "dependencies": { - "intl-messageformat": "^10.7.14" - }, - "repository": "https://github.com/hcengineering/huly.core", - "publishConfig": { - "access": "public" - }, - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.js" - }, - "./lang/*.json": { - "require": "./lang/*.json", - "import": "./lang/*.json" - }, - "./lang": { - "require": "./lang", - "import": "./lang" - } - } -} diff --git a/foundations/core/packages/platform/src/__tests__/i18n.test.ts b/foundations/core/packages/platform/src/__tests__/i18n.test.ts deleted file mode 100644 index 53211fe106..0000000000 --- a/foundations/core/packages/platform/src/__tests__/i18n.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -// -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// Copyright © 2021 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { Plugin, IntlString } from '../platform' -import platform, { plugin } from '../platform' -import { Severity, Status } from '../status' - -import { addStringsLoader, translate } from '../i18n' -import { addEventListener, PlatformEvent, removeEventListener } from '../event' - -const testId = 'test-strings' as Plugin - -const test = plugin(testId, { - string: { - loadingPlugin: '' as IntlString<{ plugin: string }> - } -}) - -describe('i18n', () => { - it('should translate string', async () => { - addStringsLoader(testId, async (locale: string) => await import(`./lang/${locale}.json`)) - const translated = await translate(test.string.loadingPlugin, { plugin: 'xxx' }) - expect(translated).toBe('Loading plugin xxx...') - }) - - it('should return id when no translation found', async () => { - const id = (testId + '.inexistent') as IntlString - const inexistent = await translate(id, {}) - expect(inexistent).toBe(id) - }) - - it('should cache translated string', async () => { - const translated = await translate(test.string.loadingPlugin, { plugin: 'xxx' }) - expect(translated).toBe('Loading plugin xxx...') - }) - - it('should emit status and return id when no loader', async () => { - expect.assertions(2) - const plugin = 'plugin-without-string-loader' - const message = `${plugin}:string:id` - - const checkStatus = new Status(Severity.ERROR, platform.status.NoLoaderForStrings, { plugin }) - const eventListener = async (event: string, data: any): Promise => { - expect(data).toEqual(checkStatus) - } - addEventListener(PlatformEvent, eventListener) - const translated = await translate(message as IntlString, {}) - expect(translated).toBe(message) - removeEventListener(PlatformEvent, eventListener) - }) - - it('should emit status and return id when bad loader', async () => { - expect.assertions(2) - const plugin = 'component-for-bad-loader' - const message = `${plugin}:string:id` - const errorMessage = 'bad loader' - addStringsLoader(plugin as Plugin, (locale: string) => { - throw new Error(errorMessage) - }) - - const checkStatus = new Status(Severity.ERROR, platform.status.UnknownError, { message: errorMessage }) - const eventListener = async (event: string, data: any): Promise => { - expect(data).toEqual(checkStatus) - } - addEventListener(PlatformEvent, eventListener) - const translated = await translate(message as IntlString, {}) - expect(translated).toBe(message) - removeEventListener(PlatformEvent, eventListener) - }) - - it('should cache error', async () => { - const plugin = 'component' - const message = `${plugin}:string:id` - - const checkStatus = new Status(Severity.ERROR, platform.status.NoLoaderForStrings, { plugin }) - let calls = 0 - const eventListener = async (event: string, data: any): Promise => { - ++calls - expect(data).toEqual(checkStatus) - } - - addEventListener(PlatformEvent, eventListener) - const t1 = await translate(message as IntlString, {}) - const t2 = await translate(message as IntlString, {}) - expect(t1).toBe(t2) - removeEventListener(PlatformEvent, eventListener) - expect(calls).toBe(1) - }) - - it('should return message when bad id', async () => { - expect.assertions(2) - const message = 'testMessage' as IntlString - const checkStatus = new Status(Severity.ERROR, platform.status.InvalidId, { id: message }) - const eventListener = async (event: string, data: any): Promise => { - expect(data).toEqual(checkStatus) - } - addEventListener(PlatformEvent, eventListener) - const translated = await translate(message, {}) - expect(translated).toBe(message) - removeEventListener(PlatformEvent, eventListener) - }) -}) diff --git a/foundations/core/packages/platform/src/__tests__/ident.test.ts b/foundations/core/packages/platform/src/__tests__/ident.test.ts deleted file mode 100644 index 784294b313..0000000000 --- a/foundations/core/packages/platform/src/__tests__/ident.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -// -// Copyright © 2020 Anticrm Platform Contributors. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { StatusCode, Plugin, Id } from '../platform' -import { plugin, mergeIds } from '../platform' -import { _parseId } from '../ident' - -describe('ident', () => { - const test = 'test' as Plugin - - it('should identify resources', () => { - const ids = plugin(test, { - status: { - MyString: '' as StatusCode - } - }) - expect(ids.status.MyString).toBe('test:status:MyString') - }) - - it('should merge ids', () => { - const ids = plugin(test, { - resource: { - MyString: '' as StatusCode - } - }) - const merged = mergeIds(test, ids, { - resource: { - OneMore: '' as StatusCode - }, - more: { - X: '' as StatusCode - } - }) - expect(merged.resource.MyString).toBe('test:resource:MyString') - expect(merged.resource.OneMore).toBe('test:resource:OneMore') - expect(merged.more.X).toBe('test:more:X') - }) - - it('should fail overwriting ids', () => { - const ids = plugin(test, { - resource: { - MyString: '' as StatusCode - } - }) - const f = (): any => - mergeIds(test, ids, { - resource: { - MyString: 'xxx' as StatusCode - } - }) - expect(f).toThrowError("'identify' overwrites") - }) - - it('should fail to parse id', () => { - expect(() => _parseId('bad id' as Id)).toThrowError('ERROR: platform:status:InvalidId') - }) - - it('should parse id', () => { - expect(_parseId('comp:res:X' as Id)).toEqual({ - kind: 'res', - component: 'comp', - name: 'X' - }) - }) -}) diff --git a/foundations/core/packages/platform/src/__tests__/lang/de.json b/foundations/core/packages/platform/src/__tests__/lang/de.json deleted file mode 100644 index 34b6b8a228..0000000000 --- a/foundations/core/packages/platform/src/__tests__/lang/de.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "string": { - "loadingPlugin": "Plugin ''{plugin}'' wird geladen..." - } -} \ No newline at end of file diff --git a/foundations/core/packages/platform/src/__tests__/lang/en.json b/foundations/core/packages/platform/src/__tests__/lang/en.json deleted file mode 100644 index e041a73fa6..0000000000 --- a/foundations/core/packages/platform/src/__tests__/lang/en.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "string": { - "loadingPlugin": "Loading plugin ''{plugin}''..." - } -} diff --git a/foundations/core/packages/platform/src/__tests__/lang/es.json b/foundations/core/packages/platform/src/__tests__/lang/es.json deleted file mode 100644 index ebd344cec5..0000000000 --- a/foundations/core/packages/platform/src/__tests__/lang/es.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "string": { - "loadingPlugin": "Cargando plugin ''{plugin}''..." - } -} \ No newline at end of file diff --git a/foundations/core/packages/platform/src/__tests__/lang/fr.json b/foundations/core/packages/platform/src/__tests__/lang/fr.json deleted file mode 100644 index c53944f623..0000000000 --- a/foundations/core/packages/platform/src/__tests__/lang/fr.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "string": { - "loadingPlugin": "Chargement du plugin ''{plugin}''..." - } -} \ No newline at end of file diff --git a/foundations/core/packages/platform/src/__tests__/lang/it.json b/foundations/core/packages/platform/src/__tests__/lang/it.json deleted file mode 100644 index c037596d8d..0000000000 --- a/foundations/core/packages/platform/src/__tests__/lang/it.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "string": { - "loadingPlugin": "Caricamento del plugin ''{plugin}''..." - } -} diff --git a/foundations/core/packages/platform/src/__tests__/lang/ja.json b/foundations/core/packages/platform/src/__tests__/lang/ja.json deleted file mode 100644 index 703e186d39..0000000000 --- a/foundations/core/packages/platform/src/__tests__/lang/ja.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "string": { - "loadingPlugin": "プラグイン ''{plugin}'' を読み込み中…" - } -} \ No newline at end of file diff --git a/foundations/core/packages/platform/src/__tests__/lang/pt.json b/foundations/core/packages/platform/src/__tests__/lang/pt.json deleted file mode 100644 index 9e57429683..0000000000 --- a/foundations/core/packages/platform/src/__tests__/lang/pt.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "string": { - "loadingPlugin": "A Carregar plugin ''{plugin}''..." - } -} \ No newline at end of file diff --git a/foundations/core/packages/platform/src/__tests__/lang/zh.json b/foundations/core/packages/platform/src/__tests__/lang/zh.json deleted file mode 100644 index bbd2593c6b..0000000000 --- a/foundations/core/packages/platform/src/__tests__/lang/zh.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "string": { - "loadingPlugin": "正在加载插件 ''{plugin}''..." - } -} diff --git a/foundations/core/packages/platform/src/__tests__/plugin.ts b/foundations/core/packages/platform/src/__tests__/plugin.ts deleted file mode 100644 index 95961aafe6..0000000000 --- a/foundations/core/packages/platform/src/__tests__/plugin.ts +++ /dev/null @@ -1,21 +0,0 @@ -// -// Copyright © 2020 Anticrm Platform Contributors. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export default async () => ({ - test: { - X: 'Test' - } -}) diff --git a/foundations/core/packages/platform/src/__tests__/resource.test.ts b/foundations/core/packages/platform/src/__tests__/resource.test.ts deleted file mode 100644 index a5b0ad0742..0000000000 --- a/foundations/core/packages/platform/src/__tests__/resource.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -// -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// Copyright © 2021 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { Plugin, IntlString, Resource } from '../platform' -import { plugin } from '../platform' -import { addLocation, getResource } from '../resource' - -describe('resource', () => { - const test = 'test' as Plugin - - const testPlugin = plugin(test, { - string: { - Hello: '' as IntlString<{ name: string }> - }, - test: { - X: '' as Resource - } - }) - - addLocation(test, async () => await import('./plugin')) - - it('should load resource', async () => { - const string = await getResource(testPlugin.test.X) - expect(string).toBe('Test') - const cached = await getResource(testPlugin.test.X) - expect(cached).toBe('Test') - }) -}) diff --git a/foundations/core/packages/platform/src/event.ts b/foundations/core/packages/platform/src/event.ts deleted file mode 100644 index 138dbe6087..0000000000 --- a/foundations/core/packages/platform/src/event.ts +++ /dev/null @@ -1,99 +0,0 @@ -// -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// Copyright © 2021 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { Status, OK, unknownError, Severity } from './status' - -/** - * @public - */ -export const PlatformEvent = 'platform-event' - -/** - * @public - */ -export type EventListener = (event: string, data: any) => Promise - -const eventListeners = new Map() - -/** - * @public - * @param event - - * @param listener - - */ -export function addEventListener (event: string, listener: EventListener): void { - const listeners = eventListeners.get(event) - if (listeners !== undefined) { - listeners.push(listener) - } else { - eventListeners.set(event, [listener]) - } -} - -/** - * @public - * @param event - - * @param listener - - */ -export function removeEventListener (event: string, listener: EventListener): void { - const listeners = eventListeners.get(event) - if (listeners !== undefined) { - listeners.splice(listeners.indexOf(listener), 1) - } -} - -/** - * @public - */ -export async function broadcastEvent (event: string, data: any): Promise { - const listeners = eventListeners.get(event) - if (listeners !== undefined) { - const promises = listeners.map(async (listener) => { - await listener(event, data) - }) - await (Promise.all(promises) as unknown as Promise) - } -} - -/** - * @public - * @param status - - * @returns - */ -export async function setPlatformStatus (status: Status): Promise { - if (status.severity === Severity.ERROR) { - console.trace('Platform Error Status', status) - } - await broadcastEvent(PlatformEvent, status) -} - -/** - * @public - * @param status - - * @param promise - - * @returns - */ -export async function monitor (status: Status, promise: Promise): Promise { - void setPlatformStatus(status) // eslint-disable-line no-void - try { - const result = await promise - void setPlatformStatus(OK) // eslint-disable-line no-void - return result - } catch (err) { - void setPlatformStatus(unknownError(err)) // eslint-disable-line no-void - console.error(err) - throw err - } -} diff --git a/foundations/core/packages/platform/src/i18n.ts b/foundations/core/packages/platform/src/i18n.ts deleted file mode 100644 index 4534f01139..0000000000 --- a/foundations/core/packages/platform/src/i18n.ts +++ /dev/null @@ -1,246 +0,0 @@ -// -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// Copyright © 2021 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { IntlMessageFormat } from 'intl-messageformat' -import { setPlatformStatus } from './event' -import { _IdInfo, _parseId } from './ident' -import type { IntlString, Plugin } from './platform' -import { Severity, Status, unknownError } from './status' - -import { getMetadata } from './metadata' -import platform, { _EmbeddedId } from './platform' - -/** - * @public - */ -export type Loader = (locale: string) => Promise>> - -type Messages = Record> - -const loaders = new Map() -const translations = new Map>() -const cache = new Map>() -const englishTranslationsForMissing = new Map() -/** - * @public - * @param plugin - - * @param loader - - */ -export function addStringsLoader (plugin: Plugin, loader: Loader): void { - loaders.set(plugin, loader) -} - -/** - * Perform load of all internationalization sources for all plugins available. - * @public - */ -export async function loadPluginStrings (locale: string, force: boolean = false): Promise { - if (force) { - cache.clear() - } - for (const [plugin] of loaders) { - const localtTanslations = translations.get(locale) ?? new Map>() - if (!translations.has(locale)) { - translations.set(locale, localtTanslations) - } - let messages = localtTanslations.get(plugin) - if (messages === undefined || force) { - messages = await loadTranslationsForComponent(plugin, locale) - localtTanslations.set(plugin, messages) - } - } -} - -async function loadTranslationsForComponent (plugin: Plugin, locale: string): Promise { - const loader = loaders.get(plugin) - if (loader === undefined) { - const status = new Status(Severity.ERROR, platform.status.NoLoaderForStrings, { plugin }) - await setPlatformStatus(status) - return status - } - try { - return (await loader(locale)) as Record | Status - } catch (err) { - console.error('No translations found for plugin', plugin, err) - try { - return (await loader('en')) as Record | Status - } catch (err: any) { - const status = unknownError(err) - await setPlatformStatus(status) - return status - } - } -} - -function getCachedTranslation (id: _IdInfo, locale: string): IntlString | Status | undefined { - const localtTanslations = translations.get(locale) - if (localtTanslations === undefined) { - return undefined - } - const messages = localtTanslations.get(id.component) - if (messages === undefined) { - return undefined - } - if (messages instanceof Status) { - return messages - } - if (id.kind !== undefined) { - if ((messages[id.kind] as Record)?.[id.name] !== undefined) { - return (messages[id.kind] as Record)?.[id.name] - } - } -} - -async function getTranslation (id: _IdInfo, locale: string): Promise { - try { - const localtTanslations = translations.get(locale) ?? new Map>() - if (!translations.has(locale)) { - translations.set(locale, localtTanslations) - } - let messages = localtTanslations.get(id.component) - if (messages === undefined) { - messages = await loadTranslationsForComponent(id.component, locale) - localtTanslations.set(id.component, messages) - } - if (messages instanceof Status) { - return messages - } - if (id.kind !== undefined) { - if ((messages[id.kind] as Record)?.[id.name] !== undefined) { - return (messages[id.kind] as Record)?.[id.name] - } else { - let eng = englishTranslationsForMissing.get(id.component) - if (eng === undefined) { - eng = await loadTranslationsForComponent(id.component, 'en') - englishTranslationsForMissing.set(id.component, eng) - } - if (eng instanceof Status) { - return eng - } - return (eng[id.kind] as Record)?.[id.name] - } - } else { - return messages[id.name] as IntlString - } - } catch (err) { - const status = unknownError(err) - await setPlatformStatus(status) - return status - } -} - -/** - * @public - * @param message - - * @param params - - * @returns - */ -export async function translate

> ( - message: IntlString

, - params: P, - language?: string -): Promise { - const locale = language ?? getMetadata(platform.metadata.locale) ?? 'en' - const localCache = cache.get(locale) ?? new Map() - if (!cache.has(locale)) { - cache.set(locale, localCache) - } - const compiled = localCache.get(message) - - if (compiled !== undefined) { - if (compiled instanceof Status) { - return message - } - return compiled.format(params) - } else { - try { - const id = _parseId(message) - if (id.component === _EmbeddedId) { - return id.name - } - const translation = getCachedTranslation(id, locale) ?? (await getTranslation(id, locale)) ?? message - if (translation instanceof Status) { - localCache.set(message, translation) - return message - } - const compiled = new IntlMessageFormat(translation, locale, undefined, { ignoreTag: true }) - localCache.set(message, compiled) - return compiled.format(params) - } catch (err) { - const status = unknownError(err) - void setPlatformStatus(status) - localCache.set(message, status) - return message - } - } -} -/** - * Will do a translation in case language file already in cache, a translate is called and Promise is returned overwise - */ -export function translateCB

> ( - message: IntlString

, - params: P, - language: string | undefined, - resolve: (value: string) => void -): void { - const locale = language ?? getMetadata(platform.metadata.locale) ?? 'en' - const localCache = cache.get(locale) ?? new Map() - if (!cache.has(locale)) { - cache.set(locale, localCache) - } - const compiled = localCache.get(message) - - if (compiled !== undefined) { - if (compiled instanceof Status) { - resolve(message) - return - } - resolve(compiled.format(params)) - } else { - let id: _IdInfo - try { - id = _parseId(message) - if (id.component === _EmbeddedId) { - resolve(id.name) - return - } - } catch (err) { - const status = unknownError(err) - void setPlatformStatus(status) - localCache.set(message, status) - resolve(message) - return - } - const translation = getCachedTranslation(id, locale) - if (translation === undefined || translation instanceof Status) { - void translate(message, params, language) - .then((res) => { - resolve(res) - }) - .catch((err) => { - const status = unknownError(err) - void setPlatformStatus(status) - localCache.set(message, status) - resolve(message) - }) - return - } - - const compiled = new IntlMessageFormat(translation, locale, undefined, { ignoreTag: true }) - localCache.set(message, compiled) - resolve(compiled.format(params)) - } -} diff --git a/foundations/core/packages/platform/src/ident.ts b/foundations/core/packages/platform/src/ident.ts deleted file mode 100644 index 52caa1d9ff..0000000000 --- a/foundations/core/packages/platform/src/ident.ts +++ /dev/null @@ -1,43 +0,0 @@ -// -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// Copyright © 2021 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { Id, Plugin } from './platform' -import { PlatformError, Status, Severity } from './status' -import platform, { _ID_SEPARATOR } from './platform' - -/** - * @internal - */ -export interface _IdInfo { - component: Plugin - kind: string - name: string -} - -/** - * @internal - */ -export function _parseId (id: Id): _IdInfo { - const path = id.split(_ID_SEPARATOR) - if (path.length < 3) { - throw new PlatformError(new Status(Severity.ERROR, platform.status.InvalidId, { id })) - } - return { - component: path[0] as Plugin, - kind: path[1], - name: path.slice(2).join(_ID_SEPARATOR) - } -} diff --git a/foundations/core/packages/platform/src/index.ts b/foundations/core/packages/platform/src/index.ts deleted file mode 100644 index 3d87bcb723..0000000000 --- a/foundations/core/packages/platform/src/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -// -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// Copyright © 2021 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { Metadata } from './metadata' - -export * from './event' -export * from './i18n' -export * from './metadata' -export * from './platform' -export * from './ident' -export { default } from './platform' -export * from './resource' -export * from './status' -export * from './testUtils' - -/** - * @public - */ -export type URL = string - -/** - * @public - */ -export type Asset = Metadata diff --git a/foundations/core/packages/platform/src/metadata.ts b/foundations/core/packages/platform/src/metadata.ts deleted file mode 100644 index df8774f7e8..0000000000 --- a/foundations/core/packages/platform/src/metadata.ts +++ /dev/null @@ -1,70 +0,0 @@ -// -// Copyright © 2020 Anticrm Platform Contributors. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { Id } from './platform' - -/** - * Platform Metadata Identifier (PMI). - * - * 'Metadata' is simply any JavaScript object, which is used to configure platform, e.g. IP addresses. - * Another example of metadata is an asset URL. The logic behind providing asset URLs as metadata is - * we know URL at compile time only and URLs vary depending on deployment options. - * - * @public - */ -export type Metadata = Id & { __metadata: T } - -/** - * @public - */ -export type ExtractType>> = { - [P in keyof X]: X[P] extends Metadata ? Z : never -} - -const metadata = new Map, any>() - -/** - * @public - * @param id - - * @returns - */ -export function getMetadata (id: Metadata): T | undefined { - return metadata.get(id) -} - -/** - * @public - * @param id - - * @param value - - */ -export function setMetadata (id: Metadata, value: T): void { - metadata.set(id, value) -} - -/** - * @public - * @param ids - - * @param data - - */ -export function loadMetadata>> (ids: X, data: ExtractType): void { - for (const key in ids) { - const id = ids[key] - const resource = data[key] - if (resource === undefined) { - throw new Error(`no metadata provided, key: ${key}, id: ${String(id)}`) - } - metadata.set(id, resource) - } -} diff --git a/foundations/core/packages/platform/src/platform.ts b/foundations/core/packages/platform/src/platform.ts deleted file mode 100644 index 9e61edc9c6..0000000000 --- a/foundations/core/packages/platform/src/platform.ts +++ /dev/null @@ -1,185 +0,0 @@ -/*! -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// Copyright © 2021 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -*/ -import { Metadata, PluginLoader, PluginModule, Resources } from '.' - -/** - * Id in format 'plugin.resource-kind.id' - * - * @public - */ -export type Id = string & { __id: true } - -/** - * Plugin Id - * - * @public - */ -export type Plugin = string & { __plugin: true } - -/** - * Platform Resource Identifier (PRI) - * - * @remarks - * - * Almost anything in the Anticrm Platform is a `Resource`. Resources referenced by Platform Resource Identifier (PRI). - * - * @example - * ```typescript - * `core.string.ClassLabel` as Resource // translated string according to current language and i18n settings - * `workbench.icon.Add` as Resource // URL to SVG sprites - * ``` - * - * @public - */ -export type Resource = Id & { __resource: T } - -/** - * Internationalized string Id - * - * @public - */ -export type IntlString = any> = Id & { __intl_string: T } - -/** - * Status Code. Also works as i18n string Id for status description. - * - * @public - */ -export type StatusCode = any> = IntlString - -/** - * @public - */ -export type Namespace = Record> - -/** - * @internal - */ -export const _ID_SEPARATOR = ':' - -/** - * @internal - */ -export const _EmbeddedId = 'embedded' - -function identify (result: Record, prefix: string, namespace: Record): Namespace { - for (const key in namespace) { - const value = namespace[key] - if (typeof result[key] === 'string') { - throw new Error(`'identify' overwrites '${key}' for ${prefix}`) - } - const ident = prefix + _ID_SEPARATOR + key - result[key] = typeof value === 'string' ? ident : identify(result[key] ?? {}, ident, value) - } - return result -} - -/** - * @public - */ -export function getEmbeddedLabel (str: string): IntlString { - return (_EmbeddedId + _ID_SEPARATOR + _EmbeddedId + _ID_SEPARATOR + str) as IntlString -} - -/** - * Defines plugin Ids. - * - * @public - * @param plugin - - * @param namespace - - * @returns - */ -export function plugin (plugin: Plugin, namespace: N): N { - return identify({}, plugin, namespace) as N -} - -/** - * Merges plugin Ids with Ids provided. - * - * @public - * @param plugin - - * @param ns - - * @param merge - - * @returns - */ -export function mergeIds (plugin: Plugin, ns: N, merge: M): N & M { - return identify({ ...ns }, plugin, merge) as N & M -} - -/** - * @public - */ -export const platformId = 'platform' as Plugin - -export default plugin(platformId, { - status: { - OK: '' as StatusCode, - BadError: '' as StatusCode, - UnknownError: '' as StatusCode<{ message: string }>, - InvalidId: '' as StatusCode<{ id: string }>, - ConnectionClosed: '' as StatusCode, - - LoadingPlugin: '' as StatusCode<{ plugin: string }>, - NoLocationForPlugin: '' as StatusCode<{ plugin: Plugin }>, - ResourceNotFound: '' as StatusCode<{ resource: Resource }>, - - NoLoaderForStrings: '' as StatusCode<{ plugin: Plugin }>, - - BadRequest: '' as StatusCode, - Forbidden: '' as StatusCode, // 403 - Unauthorized: '' as StatusCode, // 401 - TokenExpired: '' as StatusCode, // 401 - TokenNotActive: '' as StatusCode<{ notBefore: number }>, // 401 - Conflict: '' as StatusCode, // 409 - ExpiredLink: '' as StatusCode, - UnknownMethod: '' as StatusCode<{ method: string }>, - InternalServerError: '' as StatusCode, - MaintenanceWarning: '' as StatusCode<{ time: number, message?: string }>, - MaintenanceWarningTime: '' as IntlString, - AccountNotFound: '' as StatusCode<{ account?: string }>, - AccountMismatch: '' as StatusCode<{ account?: string, requiredAccount?: string }>, - AccountNotConfirmed: '' as StatusCode, - WorkspaceNotFound: '' as StatusCode<{ workspaceUuid?: string, workspaceName?: string, workspaceUrl?: string }>, - WorkspaceArchived: '' as StatusCode<{ workspaceUuid: string }>, - WorkspaceMigration: '' as StatusCode<{ workspaceUuid: string }>, - SocialIdNotFound: '' as StatusCode<{ value?: string, type?: string, _id?: string }>, - SocialIdNotConfirmed: '' as StatusCode<{ socialId: string, type: string }>, - SocialIdAlreadyConfirmed: '' as StatusCode<{ socialId: string, type: string }>, - IntegrationExists: '' as StatusCode, - IntegrationAlreadyExists: '' as StatusCode, - IntegrationNotFound: '' as StatusCode, - IntegrationSecretAlreadyExists: '' as StatusCode, - IntegrationSecretNotFound: '' as StatusCode, - PersonNotFound: '' as StatusCode<{ person: string }>, - InvalidPassword: '' as StatusCode<{ account: string }>, - PasswordLoginLocked: '' as StatusCode, - AccountAlreadyExists: '' as StatusCode, - WorkspaceAlreadyExists: '' as StatusCode<{ workspace: string }>, - WorkspaceRateLimit: '' as StatusCode<{ workspace: string }>, - WorkspaceLimitReached: '' as StatusCode<{ workspace: string }>, - InvalidOtp: '' as StatusCode, - InviteNotFound: '' as StatusCode<{ email: string }>, - MailboxError: '' as StatusCode<{ reason: string }>, - SocialIdAlreadyExists: '' as StatusCode, - ReadOnlyAccount: '' as StatusCode, - RegularAccount: '' as StatusCode, - SystemAccount: '' as StatusCode - }, - metadata: { - locale: '' as Metadata, - LoadHelper: '' as Metadata<(loader: PluginLoader) => Promise>> - } -}) diff --git a/foundations/core/packages/platform/src/resource.ts b/foundations/core/packages/platform/src/resource.ts deleted file mode 100644 index e679a8e32c..0000000000 --- a/foundations/core/packages/platform/src/resource.ts +++ /dev/null @@ -1,170 +0,0 @@ -// -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// Copyright © 2021 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { monitor } from './event' -import { _parseId } from './ident' -import type { Plugin, Resource } from './platform' -import { PlatformError, Severity, Status } from './status' - -import { getMetadata } from './metadata' -import platform from './platform' - -/** - * @public - */ -export type Resources = Record> - -/** - * @public - */ -export interface PluginModule { - default: () => Promise -} - -/** - * @public - */ -export type PluginLoader = () => Promise> - -const locations = new Map>() - -/** - * @public - * @param plugin - - * @param module - - */ -export function addLocation (plugin: Plugin, module: PluginLoader): void { - locations.set(plugin, module) -} - -/** - * @public - * return list of registred plugins. - */ -export function getPlugins (): Plugin[] { - return Array.from(locations.keys()) -} - -function getLocation (plugin: Plugin): PluginLoader { - const location = locations.get(plugin) - if (location === undefined) { - throw new PlatformError( - new Status(Severity.ERROR, platform.status.NoLocationForPlugin, { - plugin - }) - ) - } - return location -} - -const loading = new Map>() - -function loadPlugin (id: Plugin): Resources | Promise { - let pluginLoader = loading.get(id) - if (pluginLoader === undefined) { - const status = new Status(Severity.INFO, platform.status.LoadingPlugin, { - plugin: id - }) - - const loadHelper = getMetadata(platform.metadata.LoadHelper) - - const locationLoader = getLocation(id) - pluginLoader = monitor(status, loadHelper !== undefined ? loadHelper(locationLoader) : locationLoader()).then( - async (plugin) => { - try { - // In case of ts-node, we have a bit different import structure, so let's check for it. - if (typeof plugin.default === 'object') { - // eslint-disable-next-line @typescript-eslint/return-await - return await (plugin as any).default.default() - } - return await plugin.default() - } catch (err: any) { - console.error(err) - throw err - } - } - ) - loading.set(id, pluginLoader) - } - return pluginLoader -} - -const cachedResource = new Map() - -/** - * @public - * @param resource - - * @returns - */ -export async function getResource (resource: Resource): Promise { - const cached = cachedResource.get(resource) - if (cached !== undefined) { - return cached - } - const info = _parseId(resource) - let resources = loading.get(info.component) ?? loadPlugin(info.component) - if (resources instanceof Promise) { - resources = await resources - loading.set(info.component, resources) - } - const value = resources[info.kind]?.[info.name] - if (value === undefined) { - throw new PlatformError(new Status(Severity.ERROR, platform.status.ResourceNotFound, { resource })) - } - cachedResource.set(resource, value) - return value -} - -/** - * @public - * @param resource - - * @returns - */ -export function getResourceP (resource: Resource): T | Promise { - return cachedResource.get(resource) ?? getResource(resource) -} - -/** - * @public - * @param resource - - * @returns - */ -export function getResourceC (resource: Resource | undefined, callback: (resource: T | undefined) => void): void { - if (resource === undefined) { - callback(undefined) - return - } - const cached = cachedResource.get(resource) - if (cached !== undefined) { - callback(cached) - } else { - void getResource(resource) - .then((r) => { - callback(r) - }) - .catch(() => { - callback(undefined) - }) - } -} - -/** - * @public - */ -export function getResourcePlugin (resource: Resource): Plugin { - const info = _parseId(resource) - return info.component -} diff --git a/foundations/core/packages/platform/src/status.ts b/foundations/core/packages/platform/src/status.ts deleted file mode 100644 index 807a0d79ba..0000000000 --- a/foundations/core/packages/platform/src/status.ts +++ /dev/null @@ -1,100 +0,0 @@ -// -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -/** - * Anticrm Platform Foundation Types - * @packageDocumentation - */ - -import type { StatusCode } from './platform' -import platform from './platform' - -/** - * Status severity - * @public - */ -export enum Severity { - OK = 'OK', - INFO = 'INFO', - WARNING = 'WARNING', - ERROR = 'ERROR' -} - -/** - * Status of an operation - * @public - */ -export class Status

= any> { - readonly severity: Severity - readonly code: StatusCode

- readonly params: P - - constructor (severity: Severity, code: StatusCode

, params: P) { - this.severity = severity - this.code = code - this.params = params - } -} - -/** - * Error object wrapping `Status` - * @public - */ -export class PlatformError

> extends Error { - readonly status: Status

- - constructor (status: Status

) { - super(`${status.severity}: ${status.code} ${JSON.stringify(status.params)}`) - this.status = status - } -} - -/** - * OK Status - * @public - */ -export const OK = new Status(Severity.OK, platform.status.OK, {}) - -/** - * Error Status - * @public - */ -export const ERROR = new Status(Severity.ERROR, platform.status.BadError, {}) - -/** - * Error Status for Unauthorized - * @public - */ -export const UNAUTHORIZED = new Status(Severity.ERROR, platform.status.Unauthorized, {}) - -/** - * @public - * @param message - - * @returns - */ -export function unknownStatus (message: string): Status { - return new Status(Severity.ERROR, platform.status.UnknownError, { message }) -} - -/** - * Creates unknown error status - * @public - */ -export function unknownError (err: unknown): Status { - if (err instanceof PlatformError) return err.status - if (err instanceof Error) return unknownStatus(err.message) - if (typeof err === 'string') return unknownStatus(err) - return ERROR -} diff --git a/foundations/core/packages/platform/src/testUtils.ts b/foundations/core/packages/platform/src/testUtils.ts deleted file mode 100644 index ac431aa993..0000000000 --- a/foundations/core/packages/platform/src/testUtils.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Loader } from './i18n' - -function makeLocaleMatcher (target: object): object { - return Object.entries(target).reduce( - (obj, [key, value]) => ({ - ...obj, - [key]: typeof value === 'string' ? expect.any(String) : makeLocaleMatcher(value) - }), - {} - ) -} - -const langs = ['en', 'ru'] - -/** - * @public - * @param loader - - * @returns - */ -export function makeLocalesTest (loader: Loader) { - return async () => { - const [target, ...rest] = await Promise.all(langs.map(loader)) - const matcher = makeLocaleMatcher(target) - rest.forEach((loc) => { - expect(loc).toEqual(matcher) - }) - } -} diff --git a/foundations/core/packages/platform/tsconfig.json b/foundations/core/packages/platform/tsconfig.json deleted file mode 100644 index b5ae22f6e4..0000000000 --- a/foundations/core/packages/platform/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/foundations/core/packages/postgres-base/.eslintrc.js b/foundations/core/packages/postgres-base/.eslintrc.js deleted file mode 100644 index ce90fb9646..0000000000 --- a/foundations/core/packages/postgres-base/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/foundations/core/packages/postgres-base/.npmignore b/foundations/core/packages/postgres-base/.npmignore deleted file mode 100644 index 9b083cb0c4..0000000000 --- a/foundations/core/packages/postgres-base/.npmignore +++ /dev/null @@ -1,8 +0,0 @@ -* -!/lib/** -!/types/** -!/src/** -!CHANGELOG.md -/lib/**/__test__/ -/types/**/__test__/ -/src/**/__test__/ \ No newline at end of file diff --git a/foundations/core/packages/postgres-base/CHANGELOG.json b/foundations/core/packages/postgres-base/CHANGELOG.json deleted file mode 100644 index 3f964c9c17..0000000000 --- a/foundations/core/packages/postgres-base/CHANGELOG.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "name": "@hcengineering/postgres-base", - "entries": [ - { - "version": "0.7.12", - "tag": "@hcengineering/postgres-base_v0.7.12", - "date": "Sat, 11 Oct 2025 19:18:56 GMT", - "comments": { - "patch": [ - { - "comment": "rollback eslint" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/platform-rig\" from `^0.7.14` to `0.7.15`" - } - ] - } - }, - { - "version": "0.7.11", - "tag": "@hcengineering/postgres-base_v0.7.11", - "date": "Sat, 11 Oct 2025 17:58:53 GMT", - "comments": { - "patch": [ - { - "comment": "Fix eslint deps" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/platform-rig\" from `^0.7.12` to `0.7.13`" - } - ] - } - }, - { - "version": "0.7.10", - "tag": "@hcengineering/postgres-base_v0.7.10", - "date": "Fri, 10 Oct 2025 12:32:59 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/platform-rig\" from `^0.7.10` to `0.7.11`" - } - ] - } - } - ] -} diff --git a/foundations/core/packages/postgres-base/CHANGELOG.md b/foundations/core/packages/postgres-base/CHANGELOG.md deleted file mode 100644 index f81c959a6a..0000000000 --- a/foundations/core/packages/postgres-base/CHANGELOG.md +++ /dev/null @@ -1,23 +0,0 @@ -# Change Log - @hcengineering/postgres-base - -This log was last generated on Sat, 11 Oct 2025 19:18:56 GMT and should not be manually modified. - -## 0.7.12 -Sat, 11 Oct 2025 19:18:56 GMT - -### Patches - -- rollback eslint - -## 0.7.11 -Sat, 11 Oct 2025 17:58:53 GMT - -### Patches - -- Fix eslint deps - -## 0.7.10 -Fri, 10 Oct 2025 12:32:59 GMT - -_Initial release_ - diff --git a/foundations/core/packages/postgres-base/config/rig.json b/foundations/core/packages/postgres-base/config/rig.json deleted file mode 100644 index 78cc5a1733..0000000000 --- a/foundations/core/packages/postgres-base/config/rig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig", - "rigProfile": "node" -} diff --git a/foundations/core/packages/postgres-base/jest.config.js b/foundations/core/packages/postgres-base/jest.config.js deleted file mode 100644 index 2cfd408b67..0000000000 --- a/foundations/core/packages/postgres-base/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ["./src"], - coverageReporters: ["text-summary", "html"] -} diff --git a/foundations/core/packages/postgres-base/package.json b/foundations/core/packages/postgres-base/package.json deleted file mode 100644 index 923f665fda..0000000000 --- a/foundations/core/packages/postgres-base/package.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "name": "@hcengineering/postgres-base", - "version": "0.7.17", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "author": "Copyright © Hardcore Engineering Inc.", - "template": "@hcengineering/node-package", - "license": "EPL-2.0", - "files": [ - "lib/**/*", - "!lib/**/__test__/**", - "types/**/*", - "!types/**/__test__/**", - "src/**/*", - "!src/**/__test__/**", - "README.md", - "CHANGELOG.md" - ], - "scripts": { - "build": "compile", - "build:watch": "compile", - "test": "jest --passWithNoTests --silent --forceExit", - "format": "format src", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --forceExit", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "workspace:^0.7.19", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5", - "@types/node": "^22.18.1", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "@typescript-eslint/parser": "^6.21.0", - "eslint": "^8.54.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-node": "^11.1.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0" - }, - "dependencies": { - "postgres": "^3.4.7" - }, - "repository": "https://github.com/hcengineering/huly.utils", - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.js" - } - }, - "publishConfig": { - "access": "public" - } -} diff --git a/foundations/core/packages/postgres-base/src/index.ts b/foundations/core/packages/postgres-base/src/index.ts deleted file mode 100644 index 4f820c16be..0000000000 --- a/foundations/core/packages/postgres-base/src/index.ts +++ /dev/null @@ -1,439 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import postgres, { type Options, type ParameterOrJSON } from 'postgres' - -const clientRefs = new Map() - -let clId = 0 - -export type DBResult = any[] & { count: number } -export interface DBClient { - execute: (query: string, parameters?: ParameterOrJSON[] | undefined) => Promise - - release: () => void - - reserve: () => Promise - - raw: () => postgres.Sql -} - -export function createDBClient (client: postgres.Sql, release: () => void = () => {}): DBClient { - return { - execute: (query, parameters) => - client.unsafe(query, doFetchTypes ? parameters : convertArrayParams(parameters), getPrepare()), - release, - reserve: async () => { - const reserved = await client.reserve() - return createDBClient(reserved, () => { - reserved.release() - }) - }, - raw: () => client - } -} - -export function convertArrayParams (params?: unknown[]): any[] | undefined { - if (params === undefined) return undefined - - return params.map((param) => { - if (!Array.isArray(param)) return param - - if (param.length === 0) return '{}' - - const sanitized = param.map((item) => { - if (item === null || item === undefined) return 'NULL' - - if (typeof item === 'number' || typeof item === 'boolean') { - return String(item) - } - - if (typeof item === 'string') { - const escaped = item.replace(/\\/g, '\\\\').replace(/"/g, '\\"') - return `"${escaped}"` - } - - const json = JSON.stringify(item) - const escapedJson = json.replace(/\\/g, '\\\\').replace(/"/g, '\\"') - return `"${escapedJson}"` - }) - - return `{${sanitized.join(',')}}` - }) -} - -export async function retryTxn ( - pool: postgres.Sql, - operation: (client: postgres.TransactionSql) => Promise -): Promise { - await pool.begin(async (client) => { - const result = await operation(client) - return result - }) -} - -/** - * @public - */ -export async function shutdownPostgres (): Promise { - for (const c of connections.values()) { - c.close(true) - } - connections.clear() -} - -export interface PostgresClientReference { - getClient: () => Promise - - mgr: ConnectionMgr - close: () => void - url: () => string -} - -class PostgresClientReferenceImpl { - count: number - client: postgres.Sql - - mgr: ConnectionMgr - - constructor ( - readonly connectionString: string, - client: postgres.Sql, - readonly onclose: () => void - ) { - this.count = 0 - this.client = client - this.mgr = new ConnectionMgr(createDBClient(this.client)) - } - - url (): string { - return this.connectionString - } - - getClient (): postgres.Sql { - return this.client - } - - close (force: boolean = false): void { - this.count-- - if (this.count === 0 || force) { - if (force) { - this.count = 0 - } - void (async () => { - this.mgr.close() - this.onclose() - const cl = this.client - await cl.end({ timeout: 1 }) - })() - } - } - - addRef (): void { - this.count++ - } -} -export class ClientRef implements PostgresClientReference { - id = ++clId - constructor ( - readonly client: PostgresClientReferenceImpl, - readonly mgr: ConnectionMgr - ) { - clientRefs.set(this.id, this) - } - - url (): string { - return this.client.url() - } - - closed = false - async getClient (): Promise { - if (!this.closed) { - return this.client.getClient() - } else { - throw Error('DB client is already closed') - } - } - - close (): void { - // Do not allow double close of mongo connection client - if (!this.closed) { - clientRefs.delete(this.id) - this.closed = true - this.client.close() - } - } -} - -export let dbExtraOptions: Partial> = {} -export function setDBExtraOptions (options: Partial>): void { - dbExtraOptions = options -} - -export function getPrepare (): { prepare: boolean } { - return { prepare: dbExtraOptions.prepare ?? false } -} - -export const doFetchTypes = true - -const connections = new Map() - -/** - * Initialize a connection to DB - * @public - */ -export function getDBClient ( - connectionString: string, - database?: string, - serviceName: string = 'transactor' -): PostgresClientReference { - const extraOptions = JSON.parse(process.env.POSTGRES_OPTIONS ?? '{}') - const key = `${connectionString}${extraOptions}` - - let existing = connections.get(key) - - if (existing === undefined) { - const sql = postgres(connectionString, { - connection: { - application_name: serviceName - }, - database, - max: 10, - min: 2, - connect_timeout: 30, - idle_timeout: 0, - transform: { - undefined: null - }, - debug: false, - notice: false, - onnotice (notice) {}, - onparameter (key, value) {}, - ...dbExtraOptions, - ...extraOptions, - fetch_types: doFetchTypes - }) - - existing = new PostgresClientReferenceImpl(connectionString, sql, () => { - connections.delete(key) - }) - connections.set(key, existing) - } - // Add reference and return once closable - existing.addRef() - return new ClientRef(existing, existing.mgr) -} - -class ConnectionInfo { - // It should preserve at least one available connection in pool, other connection should be closed - available: DBClient[] = [] - - released: boolean = false - - constructor ( - readonly connectionId: string, - protected readonly client: DBClient, - readonly managed: boolean, - readonly mgrId: string - ) {} - - async withReserve (action: (reservedClient: DBClient) => Promise, forced: boolean = false): Promise { - let reserved: DBClient | undefined - - // Check if we have at least one available connection and reserve one more if required. - if (this.available.length === 0) { - if (this.managed || forced) { - reserved = await this.client.reserve() - } - } else { - reserved = this.available.shift() as DBClient - } - - try { - // Use reserved or pool - return await action(reserved ?? this.client) - } catch (err: any) { - console.error(err) - throw err - } finally { - if (this.released) { - try { - reserved?.release() - } catch (err: any) { - console.error('failed to release', err) - } - } else if (reserved !== undefined) { - if (this.available.length > 0) { - reserved?.release() - } else { - this.available.push(reserved) - } - } - } - } - - release (): void { - for (const c of [...this.available]) { - c.release() - } - this.available = [] - } -} - -export class ConnectionMgr { - private readonly connections = new Map() - constructor (protected readonly client: DBClient) {} - - async write (id: string | undefined, mgrId: string, fn: (client: DBClient) => Promise): Promise { - const backoffInterval = 25 // millis - const maxTries = 5 - let tries = 0 - - const realId = id ?? `${++clId}` - - const connection = this.getConnection(realId, mgrId, false) - - try { - while (true) { - const retry: boolean | Error = await connection.withReserve(async (client) => { - tries++ - try { - await client.execute('BEGIN;') - await fn(client) - await client.execute('COMMIT;') - return true - } catch (err: any) { - await client.execute('ROLLBACK;') - console.error({ message: 'failed to process tx', error: err.message, cause: err }) - - if (!this.isRetryableError(err) || tries === maxTries) { - return err - } else { - console.log('Transaction failed. Retrying.') - console.log(err.message) - return false - } - } - }, true) - if (retry === true) { - break - } - if (retry instanceof Error) { - // Pass it to exit - throw retry - } - // Retry for a timeout - await new Promise((resolve) => setTimeout(resolve, backoffInterval)) - } - } finally { - if (!connection.managed) { - // We need to relase in case it temporaty connection was used - connection.release() - } - } - } - - async retry (id: string | undefined, mgrId: string, fn: (client: DBClient) => Promise): Promise { - const backoffInterval = 25 // millis - const maxTries = 5 - let tries = 0 - - const realId = id ?? `${++clId}` - // Will reuse reserved if had and use new one if not - const connection = this.getConnection(realId, mgrId, false) - - try { - while (true) { - const retry: false | { result: any } | Error = await connection.withReserve(async (client) => { - tries++ - try { - return { result: await fn(client) } - } catch (err: any) { - console.error({ message: 'failed to process sql', error: err.message, cause: err }) - if (!this.isRetryableError(err) || tries === maxTries) { - return err - } else { - console.log('Read Transaction failed. Retrying.') - console.log(err.message) - return false - } - } - }) - if (retry instanceof Error) { - // Pass it to exit - throw retry - } - if (retry === false) { - // Retry for a timeout - await new Promise((resolve) => setTimeout(resolve, backoffInterval)) - continue - } - return retry.result - } - } finally { - if (!connection.managed) { - // We need to relase in case it temporaty connection was used - connection.release() - } - } - } - - release (id: string): void { - const conn = this.connections.get(id) - if (conn !== undefined) { - conn.released = true - this.connections.delete(id) // We need to delete first - conn.release() - } - } - - close (mgrId?: string): void { - const cnts = this.connections - for (const [k, conn] of Array.from(cnts.entries())) { - if (mgrId !== undefined && conn.mgrId !== mgrId) { - continue - } - cnts.delete(k) - try { - conn.release() - } catch (err: any) { - console.error('failed to release connection') - } - } - } - - getConnection (id: string, mgrId: string, managed: boolean = true): ConnectionInfo { - let conn = this.connections.get(id) - if (conn === undefined) { - conn = new ConnectionInfo(id, this.client, managed, mgrId) - } - if (managed) { - this.connections.set(id, conn) - } - return conn - } - - private isRetryableError (err: any): boolean { - const msg: string = err?.message ?? '' - - return ( - err.code === '40001' || // Retry transaction - err.code === '55P03' || // Lock not available - err.code === 'CONNECTION_CLOSED' || // This error is thrown if the connection was closed without an error. - err.code === 'CONNECTION_DESTROYED' || // This error is thrown for any queries that were pending when the timeout to sql.end({ timeout: X }) was reached. If the DB client is being closed completely retry will result in CONNECTION_ENDED which is not retried so should be fine. - msg.includes('RETRY_SERIALIZABLE') - ) - } -} diff --git a/foundations/core/packages/postgres-base/tsconfig.json b/foundations/core/packages/postgres-base/tsconfig.json deleted file mode 100644 index c6a877cf6c..0000000000 --- a/foundations/core/packages/postgres-base/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/node/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/foundations/core/packages/query/.eslintrc.js b/foundations/core/packages/query/.eslintrc.js deleted file mode 100644 index 72235dc283..0000000000 --- a/foundations/core/packages/query/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/foundations/core/packages/query/.npmignore b/foundations/core/packages/query/.npmignore deleted file mode 100644 index e3ec093c38..0000000000 --- a/foundations/core/packages/query/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!/lib/** -!CHANGELOG.md -/lib/**/__tests__/ diff --git a/foundations/core/packages/query/CHANGELOG.json b/foundations/core/packages/query/CHANGELOG.json deleted file mode 100644 index b943b991d3..0000000000 --- a/foundations/core/packages/query/CHANGELOG.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "name": "@hcengineering/query", - "entries": [ - { - "version": "0.7.17", - "tag": "@hcengineering/query_v0.7.17", - "date": "Mon, 27 Oct 2025 13:27:12 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" - } - ] - } - }, - { - "version": "0.7.6", - "tag": "@hcengineering/query_v0.7.6", - "date": "Tue, 14 Oct 2025 04:58:17 GMT", - "comments": { - "patch": [ - { - "comment": "update deps" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.4` to `0.7.5`" - }, - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`" - }, - { - "comment": "Updating dependency \"@hcengineering/analytics\" from `^0.7.4` to `0.7.5`" - } - ] - } - }, - { - "version": "0.7.5", - "tag": "@hcengineering/query_v0.7.5", - "date": "Sat, 11 Oct 2025 18:20:33 GMT", - "comments": { - "patch": [ - { - "comment": "Update to latest platform rig" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.3` to `0.7.4`" - }, - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`" - }, - { - "comment": "Updating dependency \"@hcengineering/analytics\" from `^0.7.3` to `0.7.4`" - } - ] - } - }, - { - "version": "0.7.4", - "tag": "@hcengineering/query_v0.7.4", - "date": "Thu, 09 Oct 2025 16:57:55 GMT", - "comments": { - "patch": [ - { - "comment": "Fix bug in queue cleanup and add more tests to it" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.4` to `0.7.5`" - } - ] - } - }, - { - "version": "0.7.3", - "tag": "@hcengineering/query_v0.7.3", - "date": "Wed, 08 Oct 2025 03:40:53 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`" - } - ] - } - }, - { - "version": "0.7.0", - "tag": "@hcengineering/query_v0.6.1", - "date": "Sun, 08 Aug 2021 21:05:26 GMT", - "comments": { - "patch": [ - { - "comment": "Fix server connection" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `~0.6.3` to `~0.6.8`" - } - ] - } - }, - { - "version": "0.7.0", - "tag": "@hcengineering/query_v0.6.0", - "date": "Sun, 08 Aug 2021 10:14:57 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/platform\" from `~0.6.3` to `~0.6.4`" - } - ] - } - } - ] -} diff --git a/foundations/core/packages/query/CHANGELOG.md b/foundations/core/packages/query/CHANGELOG.md deleted file mode 100644 index 2350664087..0000000000 --- a/foundations/core/packages/query/CHANGELOG.md +++ /dev/null @@ -1,47 +0,0 @@ -# Change Log - @hcengineering/query - -This log was last generated on Mon, 27 Oct 2025 13:27:12 GMT and should not be manually modified. - -## 0.7.17 -Mon, 27 Oct 2025 13:27:12 GMT - -_Version update only_ - -## 0.7.6 -Tue, 14 Oct 2025 04:58:17 GMT - -### Patches - -- update deps - -## 0.7.5 -Sat, 11 Oct 2025 18:20:33 GMT - -### Patches - -- Update to latest platform rig - -## 0.7.4 -Thu, 09 Oct 2025 16:57:55 GMT - -### Patches - -- Fix bug in queue cleanup and add more tests to it - -## 0.7.3 -Wed, 08 Oct 2025 03:40:53 GMT - -_Version update only_ - -## 0.7.0 -Sun, 08 Aug 2021 21:05:26 GMT - -### Patches - -- Fix server connection - -## 0.7.0 -Sun, 08 Aug 2021 10:14:57 GMT - -_Initial release_ - diff --git a/foundations/core/packages/query/config/rig.json b/foundations/core/packages/query/config/rig.json deleted file mode 100644 index 0110930f55..0000000000 --- a/foundations/core/packages/query/config/rig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig" -} diff --git a/foundations/core/packages/query/jest.config.js b/foundations/core/packages/query/jest.config.js deleted file mode 100644 index 069ea8aa0d..0000000000 --- a/foundations/core/packages/query/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ['./src'], - coverageReporters: ['text-summary', 'html', 'lcov'] -} diff --git a/foundations/core/packages/query/package.json b/foundations/core/packages/query/package.json deleted file mode 100644 index bdccbbfdb5..0000000000 --- a/foundations/core/packages/query/package.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "name": "@hcengineering/query", - "version": "0.7.17", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "files": [ - "lib/**/*", - "types/**/*", - "src/**/*", - "!src/**/__test__/**", - "tsconfig.json" - ], - "author": "Anticrm Platform Contributors", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "test": "jest --passWithNoTests --silent --coverage", - "build:watch": "compile", - "format": "format src", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --coverage", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "workspace:^0.7.19", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "@typescript-eslint/parser": "^6.21.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.6.2", - "typescript": "^5.9.3", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5", - "eslint-plugin-svelte": "^2.35.1" - }, - "dependencies": { - "@hcengineering/platform": "workspace:^0.7.18", - "@hcengineering/core": "workspace:^0.7.22", - "@hcengineering/analytics": "workspace:^0.7.17", - "fast-equals": "^5.2.2" - }, - "repository": "https://github.com/hcengineering/huly.core", - "publishConfig": { - "access": "public" - }, - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.js" - } - } -} diff --git a/foundations/core/packages/query/src/__tests__/advanced-coverage.test.ts b/foundations/core/packages/query/src/__tests__/advanced-coverage.test.ts deleted file mode 100644 index bcb1046eaa..0000000000 --- a/foundations/core/packages/query/src/__tests__/advanced-coverage.test.ts +++ /dev/null @@ -1,488 +0,0 @@ -// Advanced tests for complex LiveQuery scenarios -// -// Copyright © 2024 Hardcore Engineering Inc. -// - -import core, { createClient, Ref, SortingOrder, TxOperations } from '@hcengineering/core' -import { LiveQuery } from '..' -import { connect } from './connection' - -async function getClient (): Promise<{ liveQuery: LiveQuery, factory: TxOperations, close: () => Promise }> { - const storage = await createClient(connect) - const liveQuery = new LiveQuery(storage) - storage.notify = (...tx) => { - void liveQuery.tx(...tx) - } - return { - liveQuery, - factory: new TxOperations(storage, core.account.System), - close: async () => { - await liveQuery.close() - } - } -} - -describe('LiveQuery Advanced Coverage Tests', () => { - it('should handle complex sorting scenarios', async () => { - const { liveQuery, factory, close } = await getClient() - - // Create multiple documents - await factory.createDoc(core.class.Space, core.space.Model, { - name: 'Z-space', - description: 'last', - private: false, - members: [], - archived: false - }) - - await factory.createDoc(core.class.Space, core.space.Model, { - name: 'A-space', - description: 'first', - private: false, - members: [], - archived: false - }) - - await factory.createDoc(core.class.Space, core.space.Model, { - name: 'M-space', - description: 'middle', - private: false, - members: [], - archived: false - }) - - const callback = jest.fn() - - // Query with sorting - liveQuery.query(core.class.Space, {}, callback, { sort: { name: SortingOrder.Ascending }, limit: 10 }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - const lastResult = callback.mock.calls[callback.mock.calls.length - 1][0] - expect(lastResult.length).toBeGreaterThan(0) - - await close() - }) - - it('should handle complex query with multiple conditions', async () => { - const { liveQuery, factory, close } = await getClient() - - const spaces = [] - for (let i = 0; i < 5; i++) { - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: `multi-${i}`, - description: `desc-${i}`, - private: i % 2 === 0, - members: [], - archived: i > 3 - }) - spaces.push(space) - } - - const callback = jest.fn() - - // Complex query - liveQuery.query(core.class.Space, { private: false, archived: false }, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - - await close() - }) - - it('should handle findAll with complex options', async () => { - const { liveQuery, factory, close } = await getClient() - - await factory.createDoc(core.class.Space, core.space.Model, { - name: 'findall-1', - description: 'test', - private: false, - members: [], - archived: false - }) - - await factory.createDoc(core.class.Space, core.space.Model, { - name: 'findall-2', - description: 'test', - private: false, - members: [], - archived: false - }) - - // Use findAll with various options - const result = await liveQuery.findAll( - core.class.Space, - { private: false }, - { - limit: 10, - sort: { modifiedOn: SortingOrder.Descending } - } - ) - - expect(result.length).toBeGreaterThan(0) - - await close() - }) - - it('should handle rapid document creation and updates', async () => { - const { liveQuery, factory, close } = await getClient() - - const callback = jest.fn() - - liveQuery.query(core.class.Space, { private: false }, callback) - - await new Promise((resolve) => setTimeout(resolve, 50)) - - // Rapid creation - const spaces: Array> = [] - for (let i = 0; i < 10; i++) { - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: `rapid-${i}`, - description: 'test', - private: false, - members: [], - archived: false - }) - spaces.push(space) - } - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Rapid updates - for (const space of spaces) { - await factory.updateDoc(core.class.Space, core.space.Model, space, { - description: 'updated' - }) - } - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback.mock.calls.length).toBeGreaterThan(1) - - await close() - }) - - it('should handle document removal from live query', async () => { - const { liveQuery, factory, close } = await getClient() - - const space1 = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'remove-1', - description: 'test', - private: false, - members: [], - archived: false - }) - - const callback = jest.fn() - - liveQuery.query(core.class.Space, { _id: space1 }, callback, { total: true }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - callback.mockClear() - - // Remove the document - await factory.removeDoc(core.class.Space, core.space.Model, space1) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Callback should be called with updated results - expect(callback).toHaveBeenCalled() - - await close() - }) - - it('should handle query with $in operator', async () => { - const { liveQuery, factory, close } = await getClient() - - const space1 = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'in-1', - description: 'test', - private: false, - members: [], - archived: false - }) - - const space2 = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'in-2', - description: 'test', - private: false, - members: [], - archived: false - }) - - const callback = jest.fn() - - liveQuery.query(core.class.Space, { _id: { $in: [space1, space2] } }, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - - await close() - }) - - it('should handle nested document updates', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'nested-update', - description: 'original', - private: false, - members: [], - archived: false - }) - - const callback = jest.fn() - - liveQuery.query(core.class.Space, { _id: space }, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - callback.mockClear() - - // Multiple rapid updates - await factory.updateDoc(core.class.Space, core.space.Model, space, { - description: 'update-1' - }) - - await factory.updateDoc(core.class.Space, core.space.Model, space, { - description: 'update-2' - }) - - await factory.updateDoc(core.class.Space, core.space.Model, space, { - description: 'update-3' - }) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - expect(callback).toHaveBeenCalled() - - await close() - }) - - it('should handle query with empty results', async () => { - const { liveQuery, close } = await getClient() - - const callback = jest.fn() - - liveQuery.query(core.class.Space, { name: 'non-existent-document-name-12345' }, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - const result = callback.mock.calls[0][0] - expect(result.length).toBe(0) - - await close() - }) - - it('should handle concurrent queries on same data', async () => { - const { liveQuery, factory, close } = await getClient() - - await factory.createDoc(core.class.Space, core.space.Model, { - name: 'concurrent-test', - description: 'test', - private: false, - members: [], - archived: false - }) - - const callback1 = jest.fn() - const callback2 = jest.fn() - const callback3 = jest.fn() - - // Start multiple queries simultaneously - liveQuery.query(core.class.Space, { private: false }, callback1, { limit: 5 }) - liveQuery.query(core.class.Space, { private: false }, callback2, { limit: 10 }) - liveQuery.query(core.class.Space, { private: false }, callback3) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback1).toHaveBeenCalled() - expect(callback2).toHaveBeenCalled() - expect(callback3).toHaveBeenCalled() - - await close() - }) - - it('should handle total count in queries', async () => { - const { liveQuery, factory, close } = await getClient() - - for (let i = 0; i < 5; i++) { - await factory.createDoc(core.class.Space, core.space.Model, { - name: `total-${i}`, - description: 'test', - private: false, - members: [], - archived: false - }) - } - - const callback = jest.fn() - - liveQuery.query(core.class.Space, { private: false }, callback, { limit: 2, total: true }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - const result = callback.mock.calls[callback.mock.calls.length - 1][0] - expect(result.total).toBeGreaterThanOrEqual(5) - - await close() - }) - - it('should handle query with archived documents', async () => { - const { liveQuery, factory, close } = await getClient() - - await factory.createDoc(core.class.Space, core.space.Model, { - name: 'archived-1', - description: 'test', - private: false, - members: [], - archived: true - }) - - await factory.createDoc(core.class.Space, core.space.Model, { - name: 'active-1', - description: 'test', - private: false, - members: [], - archived: false - }) - - const callback = jest.fn() - - // Query for non-archived - liveQuery.query(core.class.Space, { archived: false }, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - - await close() - }) - - it('should handle updating query multiple times', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'multi-update', - description: 'v1', - private: false, - members: [], - archived: false - }) - - const callback = jest.fn() - - liveQuery.query(core.class.Space, { _id: space }, callback) - - await new Promise((resolve) => setTimeout(resolve, 50)) - - // Update multiple times - for (let i = 2; i <= 5; i++) { - await factory.updateDoc(core.class.Space, core.space.Model, space, { - description: `v${i}` - }) - await new Promise((resolve) => setTimeout(resolve, 30)) - } - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Should have been called multiple times - expect(callback.mock.calls.length).toBeGreaterThan(1) - - await close() - }) - - it('should handle query unsubscribe and resubscribe cycle', async () => { - const { liveQuery, close } = await getClient() - - const callback1 = jest.fn() - - // Subscribe - const unsub1 = liveQuery.query(core.class.Space, {}, callback1) - - await new Promise((resolve) => setTimeout(resolve, 50)) - - // Unsubscribe - unsub1() - - await new Promise((resolve) => setTimeout(resolve, 50)) - - callback1.mockClear() - - const callback2 = jest.fn() - - // Resubscribe - liveQuery.query(core.class.Space, {}, callback2) - - await new Promise((resolve) => setTimeout(resolve, 50)) - - expect(callback2).toHaveBeenCalled() - - await close() - }) - - it('should handle findOne on non-existent document', async () => { - const { liveQuery, close } = await getClient() - - const result = await liveQuery.findOne(core.class.Space, { _id: 'non-existent-id-12345' as Ref }) - - expect(result).toBeUndefined() - - await close() - }) - - it('should handle mixed operations on same query', async () => { - const { liveQuery, factory, close } = await getClient() - - const spaces: Array> = [] - - // Create initial documents - for (let i = 0; i < 3; i++) { - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: `mixed-${i}`, - description: 'test', - private: false, - members: [], - archived: false - }) - spaces.push(space) - } - - const callback = jest.fn() - - liveQuery.query(core.class.Space, { private: false }, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Update one - await factory.updateDoc(core.class.Space, core.space.Model, spaces[0], { - description: 'updated' - }) - - // Create new one - await factory.createDoc(core.class.Space, core.space.Model, { - name: 'mixed-new', - description: 'new', - private: false, - members: [], - archived: false - }) - - // Remove one - await factory.removeDoc(core.class.Space, core.space.Model, spaces[1]) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - // Should have been notified multiple times - expect(callback.mock.calls.length).toBeGreaterThan(1) - - await close() - }) -}) diff --git a/foundations/core/packages/query/src/__tests__/bug-hunting.test.ts b/foundations/core/packages/query/src/__tests__/bug-hunting.test.ts deleted file mode 100644 index 72163f1559..0000000000 --- a/foundations/core/packages/query/src/__tests__/bug-hunting.test.ts +++ /dev/null @@ -1,496 +0,0 @@ -// Bug hunting tests - targeting specific edge cases and potential issues -// -// Copyright © 2024 Hardcore Engineering Inc. -// - -import core, { createClient, Ref, SortingOrder, TxOperations } from '@hcengineering/core' -import { LiveQuery } from '..' -import { connect } from './connection' -import { test } from './minmodel' - -async function getClient (): Promise<{ liveQuery: LiveQuery, factory: TxOperations, close: () => Promise }> { - const storage = await createClient(connect) - const liveQuery = new LiveQuery(storage) - storage.notify = (...tx) => { - void liveQuery.tx(...tx) - } - return { - liveQuery, - factory: new TxOperations(storage, core.account.System), - close: async () => { - await liveQuery.close() - } - } -} - -describe('Bug Hunting Tests', () => { - describe('Potential Bug: Sort order after updates with limit', () => { - it('should maintain correct sort order when updating document that exceeds limit', async () => { - const { liveQuery, factory, close } = await getClient() - - // Create documents with specific sort order - const spaces: Array> = [] - for (let i = 0; i < 5; i++) { - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: `space-${String(i).padStart(2, '0')}`, - description: `priority-${i}`, - private: false, - members: [], - archived: false - }) - spaces.push(space) - } - - const callback = jest.fn() - - // Query with limit and sort - liveQuery.query(core.class.Space, {}, callback, { - limit: 3, - sort: { name: SortingOrder.Ascending } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - const initialResult = callback.mock.calls[callback.mock.calls.length - 1][0] - expect(initialResult.length).toBeLessThanOrEqual(3) - - // Update a document outside the limit to see if it enters - await factory.updateDoc(core.class.Space, core.space.Model, spaces[4], { - name: 'space-00-updated' // Should now be first - }) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - // Check that result was updated correctly - const finalResult = callback.mock.calls[callback.mock.calls.length - 1][0] - expect(finalResult.length).toBeLessThanOrEqual(3) - - await close() - }) - }) - - describe('Potential Bug: Match function with mixins', () => { - it('should correctly match documents with mixin classes', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'mixin-match-test', - description: 'test', - private: false, - members: [], - archived: false - }) - - const callback = jest.fn() - - // Query for the space - liveQuery.query(core.class.Space, { _id: space }, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - - await close() - }) - }) - - describe('Potential Bug: getCurrentDoc when document no longer matches', () => { - it('should handle document that stops matching query after update', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'match-test', - description: 'test', - private: false, - members: [], - archived: false - }) - - const callback = jest.fn() - - // Query for private=false - liveQuery.query(core.class.Space, { _id: space, private: false }, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - const initialResult = callback.mock.calls[callback.mock.calls.length - 1][0] - expect(initialResult.length).toBe(1) - - callback.mockClear() - - // Update to private=true, should no longer match - await factory.updateDoc(core.class.Space, core.space.Model, space, { - private: true - }) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - // Should have been called with empty results or document removed - expect(callback).toHaveBeenCalled() - - await close() - }) - }) - - describe('Potential Bug: Lookup updates with $push operations', () => { - it('should correctly handle $push operations on lookup fields', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'push-test', - description: 'test', - private: false, - members: [], - archived: false - }) - - const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { - message: 'parent' - }) - - const callback = jest.fn() - - // Query with reverse lookup - liveQuery.query(test.class.TestComment, { _id: comment }, callback, { - lookup: { - _id: { comments: test.class.TestComment } - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - const initialCalls = callback.mock.calls.length - - // Add a child comment (this should trigger $push like behavior) - await factory.addCollection(test.class.TestComment, space, comment, test.class.TestComment, 'comments', { - message: 'child1' - }) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - // Should have been updated - expect(callback.mock.calls.length).toBeGreaterThan(initialCalls) - - await close() - }) - }) - - describe('Potential Bug: Sort with nested operations', () => { - it('should detect need for re-sort when nested operations affect sort fields', async () => { - const { liveQuery, factory, close } = await getClient() - - const spaces: Array> = [] - for (let i = 0; i < 3; i++) { - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: `sort-${i}`, - description: 'test', - private: false, - members: [], - archived: false - }) - spaces.push(space) - } - - const callback = jest.fn() - - // Query with sort by name - liveQuery.query(core.class.Space, {}, callback, { - sort: { name: SortingOrder.Ascending } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Update name which should trigger re-sort - await factory.updateDoc(core.class.Space, core.space.Model, spaces[0], { - name: 'sort-zzz' // Should move to end - }) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - expect(callback.mock.calls.length).toBeGreaterThan(1) - - await close() - }) - }) - - describe('Potential Bug: checkUpdatedDocMatch with limit', () => { - it('should refresh query when document stops matching and at limit', async () => { - const { liveQuery, factory, close } = await getClient() - - const spaces: Array> = [] - // Create more docs than limit - for (let i = 0; i < 5; i++) { - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: `limit-match-${i}`, - description: 'test', - private: false, - members: [], - archived: false - }) - spaces.push(space) - } - - const callback = jest.fn() - - // Query with limit - liveQuery.query(core.class.Space, { private: false }, callback, { - limit: 3 - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - const initialResult = callback.mock.calls[callback.mock.calls.length - 1][0] - expect(initialResult.length).toBeLessThanOrEqual(3) - - // Make one of the documents not match - await factory.updateDoc(core.class.Space, core.space.Model, spaces[0], { - private: true - }) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - // Should have refreshed to include another document - expect(callback.mock.calls.length).toBeGreaterThan(1) - - await close() - }) - }) - - describe('Potential Bug: updatedDocCallback with limit boundary', () => { - it('should handle updates when document is exactly at limit boundary', async () => { - const { liveQuery, factory, close } = await getClient() - - const spaces: Array> = [] - for (let i = 0; i < 4; i++) { - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: `boundary-${String(i).padStart(2, '0')}`, - description: 'test', - private: false, - members: [], - archived: false - }) - spaces.push(space) - } - - const callback = jest.fn() - - // Query with limit=3, we have 4 documents - liveQuery.query(core.class.Space, {}, callback, { - limit: 3, - sort: { name: SortingOrder.Ascending } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Update the 3rd document (at the limit boundary) - await factory.updateDoc(core.class.Space, core.space.Model, spaces[2], { - description: 'updated at boundary' - }) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - expect(callback.mock.calls.length).toBeGreaterThan(1) - - await close() - }) - }) - - describe('Potential Bug: Reverse lookup with undefined values', () => { - it('should handle reverse lookups when field is undefined', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'undefined-lookup', - description: 'test', - private: false, - members: [], - archived: false - }) - - const callback = jest.fn() - - // Query with reverse lookup that may have undefined fields - liveQuery.query(core.class.Space, { _id: space }, callback, { - lookup: { - _id: { comments: test.class.TestComment } - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - - await close() - }) - }) - - describe('Potential Bug: Match with skipLookup flag', () => { - it('should correctly skip $lookup keys when matching', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'skip-lookup-test', - description: 'test', - private: false, - members: [], - archived: false - }) - - const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { - message: 'test' - }) - - const callback = jest.fn() - - // Query with lookup - liveQuery.query(test.class.TestComment, { _id: comment }, callback, { - lookup: { space: core.class.Space } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - - await close() - }) - }) - - describe('Potential Bug: Query with $search and updates', () => { - it('should handle updates on documents with $search query', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'searchable document', - description: 'findable content', - private: false, - members: [], - archived: false - }) - - const callback = jest.fn() - - // Query with search (though it won't actually search in mock) - liveQuery.query(core.class.Space, { $search: 'searchable' }, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Update the document - await factory.updateDoc(core.class.Space, core.space.Model, space, { - description: 'updated content' - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - - await close() - }) - }) - - describe('Potential Bug: Multiple rapid unsubscribe/subscribe cycles', () => { - it('should handle rapid subscribe/unsubscribe cycles correctly', async () => { - const { liveQuery, close } = await getClient() - - const callbacks = [] - const unsubscribes = [] - - // Rapidly subscribe and unsubscribe - for (let i = 0; i < 10; i++) { - const callback = jest.fn() - callbacks.push(callback) - const unsub = liveQuery.query(core.class.Space, {}, callback) - unsubscribes.push(unsub) - - if (i % 2 === 0) { - unsub() // Unsubscribe every other one immediately - } - } - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Clean up remaining subscriptions - unsubscribes.forEach((unsub, i) => { - if (i % 2 !== 0) { - unsub() - } - }) - - await new Promise((resolve) => setTimeout(resolve, 50)) - - await close() - }) - }) - - describe('Potential Bug: Empty query result set operations', () => { - it('should handle operations on empty result sets', async () => { - const { liveQuery, factory, close } = await getClient() - - const callback = jest.fn() - - // Query that will have no results initially - liveQuery.query(core.class.Space, { name: 'does-not-exist-initially' }, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - const initialResult = callback.mock.calls[callback.mock.calls.length - 1][0] - expect(initialResult.length).toBe(0) - - // Now create a matching document - await factory.createDoc(core.class.Space, core.space.Model, { - name: 'does-not-exist-initially', - description: 'test', - private: false, - members: [], - archived: false - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - const finalResult = callback.mock.calls[callback.mock.calls.length - 1][0] - expect(finalResult.length).toBe(1) - - await close() - }) - }) - - describe('Potential Bug: Mixin updates with reverse lookup', () => { - it('should handle mixin updates on documents with reverse lookups', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'mixin-reverse-lookup', - description: 'test', - private: false, - members: [], - archived: false - }) - - const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { - message: 'test' - }) - - const callback = jest.fn() - - liveQuery.query(test.class.TestComment, { _id: comment }, callback, { - lookup: { - _id: { comments: test.class.TestComment } - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Add child comment - await factory.addCollection(test.class.TestComment, space, comment, test.class.TestComment, 'comments', { - message: 'child' - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback.mock.calls.length).toBeGreaterThan(1) - - await close() - }) - }) -}) diff --git a/foundations/core/packages/query/src/__tests__/connection.ts b/foundations/core/packages/query/src/__tests__/connection.ts deleted file mode 100644 index 6c3acc7b65..0000000000 --- a/foundations/core/packages/query/src/__tests__/connection.ts +++ /dev/null @@ -1,180 +0,0 @@ -// -// Copyright © 2020 Anticrm Platform Contributors. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import core, { - BackupClient, - Class, - Client, - ClientConnectEvent, - ClientConnection, - Doc, - DocChunk, - DocumentQuery, - Domain, - DOMAIN_TX, - FindOptions, - FindResult, - FulltextStorage, - generateId, - Hierarchy, - LoadModelResponse, - ModelDb, - Ref, - SearchOptions, - SearchQuery, - SearchResult, - Timestamp, - Tx, - TxDb, - TxResult, - type DomainParams, - type DomainRequestOptions, - type DomainResult, - type OperationDomain, - type TxHandler -} from '@hcengineering/core' -import { genMinModel } from './minmodel' - -export async function connect (handler: (tx: Tx) => void): Promise< -Client & -BackupClient & -FulltextStorage & { - isConnected: () => boolean - loadModel: (last: Timestamp, hash?: string) => Promise - pushHandler: (handler: TxHandler) => void -} -> { - const txes = genMinModel() - - const hierarchy = new Hierarchy() - for (const tx of txes) hierarchy.tx(tx) - - const transactions = new TxDb(hierarchy) - const model = new ModelDb(hierarchy) - for (const tx of txes) { - await transactions.tx(tx) - await model.tx(tx) - } - - class TestConnection implements ClientConnection { - private readonly hierarchy: Hierarchy - private readonly model: ModelDb - private readonly transactions: TxDb - - constructor (hierarchy: Hierarchy, model: ModelDb, transactions: TxDb) { - this.hierarchy = hierarchy - this.model = model - this.transactions = transactions - } - - isConnected (): boolean { - return true - } - - pushHandler (): void {} - - async findAll( - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): Promise> { - const domain = this.hierarchy.getClass(_class).domain - if (domain === DOMAIN_TX) return await this.transactions.findAll(_class, query, options) - return await this.model.findAll(_class, query, options) - } - - async findOne( - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): Promise { - return (await this.findAll(_class, query, { ...options, limit: 1 })).shift() - } - - async domainRequest ( - domain: OperationDomain, - params: DomainParams, - options?: DomainRequestOptions - ): Promise { - return { domain, value: null } - } - - getHierarchy (): Hierarchy { - return this.hierarchy - } - - getModel (): ModelDb { - return this.model - } - - async tx (tx: Tx): Promise { - if (tx.objectSpace === core.space.Model) { - this.hierarchy.tx(tx) - } - await Promise.all([this.model.tx(tx), this.transactions.tx(tx)]) - handler(tx) - return {} - } - - async close (): Promise {} - - async loadChunk (domain: Domain, idx?: number): Promise { - return { - idx: -1, - docs: [], - finished: true - } - } - - async getDomainHash (domain: Domain): Promise { - return generateId() - } - - async loadModel (lastTxTime: Timestamp): Promise { - return txes - } - - async closeChunk (idx: number): Promise {} - - async loadDocs (domain: Domain, docs: Ref[]): Promise { - return [] - } - - async upload (domain: Domain, docs: Doc[]): Promise {} - - async clean (domain: Domain, docs: Ref[]): Promise {} - - async searchFulltext (query: SearchQuery, options: SearchOptions): Promise { - return { docs: [] } - } - - async sendForceClose (): Promise {} - - handler?: (event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise - - set onConnect ( - handler: ((event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise) | undefined - ) { - this.handler = handler - void this.handler?.(ClientConnectEvent.Connected, '', {}) - } - - get onConnect (): ((event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise) | undefined { - return this.handler - } - } - - return new TestConnection(hierarchy, model, transactions) -} diff --git a/foundations/core/packages/query/src/__tests__/deep-lookup.test.ts b/foundations/core/packages/query/src/__tests__/deep-lookup.test.ts deleted file mode 100644 index c75b416cd2..0000000000 --- a/foundations/core/packages/query/src/__tests__/deep-lookup.test.ts +++ /dev/null @@ -1,502 +0,0 @@ -// Deep lookup and association tests -// -// Copyright © 2024 Hardcore Engineering Inc. -// - -import core, { createClient, Ref, TxOperations } from '@hcengineering/core' -import { LiveQuery } from '..' -import { connect } from './connection' -import { test } from './minmodel' - -async function getClient (): Promise<{ liveQuery: LiveQuery, factory: TxOperations, close: () => Promise }> { - const storage = await createClient(connect) - const liveQuery = new LiveQuery(storage) - storage.notify = (...tx) => { - void liveQuery.tx(...tx) - } - return { - liveQuery, - factory: new TxOperations(storage, core.account.System), - close: async () => { - await liveQuery.close() - } - } -} - -describe('Deep Lookup Tests', () => { - describe('Nested lookup with missing parent', () => { - it('should handle nested lookup when parent document is undefined', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'nested-missing-parent', - description: 'test', - private: false, - members: [], - archived: false - }) - - // Create a comment with attachedTo pointing to non-existent parent - const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { - message: 'orphan comment' - }) - - const callback = jest.fn() - - // Nested lookup where parent might not exist - liveQuery.query(test.class.TestComment, { _id: comment }, callback, { - lookup: { - attachedTo: core.class.Space - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - - await close() - }) - }) - - describe('Reverse lookup with array values', () => { - it('should handle reverse lookup with custom attribute', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'reverse-custom-attr', - description: 'test', - private: false, - members: [], - archived: false - }) - - const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { - message: 'parent' - }) - - const callback = jest.fn() - - // Reverse lookup with specific attribute (array format) - liveQuery.query(test.class.TestComment, { _id: comment }, callback, { - lookup: { - _id: { - children: [test.class.TestComment, 'attachedTo'] // Custom attribute - } - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - - // Add child with custom attachedTo - await factory.addCollection(test.class.TestComment, space, comment, test.class.TestComment, 'comments', { - message: 'child' - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback.mock.calls.length).toBeGreaterThan(1) - - await close() - }) - }) - - describe('Reverse lookup with undefined or zero values', () => { - it('should skip reverse lookup fields that are undefined or 0', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'reverse-undefined', - description: 'test', - private: false, - members: [], - archived: false - }) - - const callback = jest.fn() - - // This should handle cases where the document might have undefined reverse lookup fields - liveQuery.query(core.class.Space, { _id: space }, callback, { - lookup: { - _id: { - // This might not exist on the document - somethingThatDoesNotExist: test.class.TestComment - } - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - - await close() - }) - }) - - describe('Lookup with mixin keys', () => { - it('should handle lookup with mixin property keys', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'mixin-key-lookup', - description: 'test', - private: false, - members: [], - archived: false - }) - - const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { - message: 'test' - }) - - const callback = jest.fn() - - // Lookup that will use checkMixinKey internally - liveQuery.query(test.class.TestComment, { _id: comment }, callback, { - lookup: { - space: core.class.Space, - attachedTo: core.class.Space - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - - await close() - }) - }) - - describe('Complex nested lookup updates', () => { - it('should handle updates in nested lookup chain', async () => { - const { liveQuery, factory, close } = await getClient() - - const space1 = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'nested-space-1', - description: 'level 1', - private: false, - members: [], - archived: false - }) - - const comment1 = await factory.addCollection( - test.class.TestComment, - space1, - space1, - core.class.Space, - 'comments', - { - message: 'level 2' - } - ) - - const comment2 = await factory.addCollection( - test.class.TestComment, - space1, - comment1, - test.class.TestComment, - 'comments', - { - message: 'level 3' - } - ) - - const callback = jest.fn() - - // Deep nested lookup - liveQuery.query(test.class.TestComment, { _id: comment2 }, callback, { - lookup: { - attachedTo: [ - test.class.TestComment, - { - space: core.class.Space - } - ] - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Update the space (should propagate through nested lookup) - await factory.updateDoc(core.class.Space, core.space.Model, space1, { - description: 'level 1 updated' - }) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - expect(callback).toHaveBeenCalled() - - await close() - }) - }) - - describe('Lookup update with $pull operation', () => { - it('should handle $pull operations on lookup arrays', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'pull-test', - description: 'test', - private: false, - members: [], - archived: false - }) - - const parentComment = await factory.addCollection( - test.class.TestComment, - space, - space, - core.class.Space, - 'comments', - { - message: 'parent' - } - ) - - const childComments: Array> = [] - for (let i = 0; i < 3; i++) { - const child = await factory.addCollection( - test.class.TestComment, - space, - parentComment, - test.class.TestComment, - 'comments', - { - message: `child-${i}` - } - ) - childComments.push(child) - } - - const callback = jest.fn() - - liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, { - lookup: { - _id: { comments: test.class.TestComment } - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - const initialCalls = callback.mock.calls.length - - // Remove a child comment (should trigger $pull-like behavior) - await factory.removeCollection( - test.class.TestComment, - space, - childComments[1], - parentComment, - test.class.TestComment, - 'comments' - ) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - expect(callback.mock.calls.length).toBeGreaterThan(initialCalls) - - await close() - }) - }) - - describe('Lookup with attached doc updates', () => { - it('should handle reverse lookup updates when attached doc changes attachedTo', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'attached-move', - description: 'test', - private: false, - members: [], - archived: false - }) - - const comment1 = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { - message: 'parent1' - }) - - const comment2 = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { - message: 'parent2' - }) - - const childComment = await factory.addCollection( - test.class.TestComment, - space, - comment1, - test.class.TestComment, - 'comments', - { - message: 'child' - } - ) - - const callback = jest.fn() - - // Watch parent1 with reverse lookup - liveQuery.query(test.class.TestComment, { _id: comment1 }, callback, { - lookup: { - _id: { comments: test.class.TestComment } - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - const initialCalls = callback.mock.calls.length - - // Move child from comment1 to comment2 - await factory.updateCollection( - test.class.TestComment, - space, - childComment, - comment2, // New parent - test.class.TestComment, - 'comments', - { - attachedTo: comment2 - } - ) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - // Should have been notified about the change - expect(callback.mock.calls.length).toBeGreaterThan(initialCalls) - - await close() - }) - }) - - describe('Lookup with single document vs array', () => { - it('should handle both single document and array lookups', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'single-vs-array', - description: 'test', - private: false, - members: [], - archived: false - }) - - const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { - message: 'test' - }) - - const callback = jest.fn() - - // Single document lookup (space) and array lookup (_id.comments) - liveQuery.query(test.class.TestComment, { _id: comment }, callback, { - lookup: { - space: core.class.Space, // Single document - _id: { comments: test.class.TestComment } // Array - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - const result = callback.mock.calls[callback.mock.calls.length - 1][0][0] - - // Should have both lookups - expect(result.$lookup).toBeDefined() - - await close() - }) - }) - - describe('Mixin class matching in queries', () => { - it('should handle queries on mixin classes', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'mixin-query', - description: 'test', - private: false, - members: [], - archived: false - }) - - const callback = jest.fn() - - // Query on space class - liveQuery.query(core.class.Space, { _id: space }, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - - await close() - }) - }) - - describe('Lookup update edge cases', () => { - it('should handle updates to documents not matching current query', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'edge-lookup', - description: 'test', - private: false, - members: [], - archived: false - }) - - const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { - message: 'test' - }) - - const callback = jest.fn() - - // Query specific comments - liveQuery.query(test.class.TestComment, { message: 'test' }, callback, { - lookup: { space: core.class.Space } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Update message so it no longer matches - await factory.updateCollection(test.class.TestComment, space, comment, space, core.class.Space, 'comments', { - message: 'updated-no-match' - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - - await close() - }) - }) - - describe('GetObjectValue with nested properties', () => { - it('should handle lookups with nested property access', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'nested-property', - description: 'test', - private: false, - members: [], - archived: false - }) - - const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { - message: 'test' - }) - - const callback = jest.fn() - - // Lookup that uses getObjectValue internally - liveQuery.query(test.class.TestComment, { _id: comment }, callback, { - lookup: { - space: core.class.Space, - attachedTo: core.class.Space - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - - await close() - }) - }) -}) diff --git a/foundations/core/packages/query/src/__tests__/final-coverage.test.ts b/foundations/core/packages/query/src/__tests__/final-coverage.test.ts deleted file mode 100644 index 1d239bb045..0000000000 --- a/foundations/core/packages/query/src/__tests__/final-coverage.test.ts +++ /dev/null @@ -1,519 +0,0 @@ -// Final comprehensive tests targeting uncovered scenarios -// -// Copyright © 2024 Hardcore Engineering Inc. -// - -import core, { createClient, Ref, SortingOrder, TxOperations } from '@hcengineering/core' -import { LiveQuery } from '..' -import { connect } from './connection' -import { test } from './minmodel' - -async function getClient (): Promise<{ liveQuery: LiveQuery, factory: TxOperations, close: () => Promise }> { - const storage = await createClient(connect) - const liveQuery = new LiveQuery(storage) - storage.notify = (...tx) => { - void liveQuery.tx(...tx) - } - return { - liveQuery, - factory: new TxOperations(storage, core.account.System), - close: async () => { - await liveQuery.close() - } - } -} - -describe('LiveQuery Final Coverage Tests', () => { - it('should handle complex update with attached documents', async () => { - const { liveQuery, factory, close } = await getClient() - - // Create a space - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'attach-test', - description: 'test', - private: false, - members: [], - archived: false - }) - - // Add attached comment - const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { - message: 'original' - }) - - const callback = jest.fn() - - liveQuery.query(test.class.TestComment, { _id: comment }, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Update the comment - await factory.updateCollection(test.class.TestComment, space, comment, space, core.class.Space, 'comments', { - message: 'updated' - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback.mock.calls.length).toBeGreaterThan(1) - - await close() - }) - - it('should handle reverse lookup updates on attached documents', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'reverse-lookup-test', - description: 'test', - private: false, - members: [], - archived: false - }) - - const parentComment = await factory.addCollection( - test.class.TestComment, - space, - space, - core.class.Space, - 'comments', - { - message: 'parent' - } - ) - - const callback = jest.fn() - - // Query with reverse lookup - liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, { - lookup: { - _id: { - comments: test.class.TestComment - } - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Add child comment - await factory.addCollection(test.class.TestComment, space, parentComment, test.class.TestComment, 'comments', { - message: 'child1' - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Should have updated with new child - expect(callback.mock.calls.length).toBeGreaterThan(1) - - await close() - }) - - it('should handle nested lookups with multiple levels', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'nested-lookup', - description: 'test', - private: false, - members: [], - archived: false - }) - - const parentComment = await factory.addCollection( - test.class.TestComment, - space, - space, - core.class.Space, - 'comments', - { - message: 'parent' - } - ) - - const childComment = await factory.addCollection( - test.class.TestComment, - space, - parentComment, - test.class.TestComment, - 'comments', - { - message: 'child' - } - ) - - const callback = jest.fn() - - // Nested lookup - liveQuery.query(test.class.TestComment, { _id: childComment }, callback, { - lookup: { - attachedTo: [test.class.TestComment, { space: core.class.Space }] - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - - await close() - }) - - it('should handle query with limit and sorting together', async () => { - const { liveQuery, factory, close } = await getClient() - - // Create multiple documents - for (let i = 0; i < 10; i++) { - await factory.createDoc(core.class.Space, core.space.Model, { - name: `sorted-${String(i).padStart(2, '0')}`, - description: 'test', - private: false, - members: [], - archived: false - }) - } - - const callback = jest.fn() - - // Query with limit and sort - liveQuery.query(core.class.Space, {}, callback, { - limit: 3, - sort: { name: SortingOrder.Ascending } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - const result = callback.mock.calls[callback.mock.calls.length - 1][0] - expect(result.length).toBeLessThanOrEqual(3) - - await close() - }) - - it('should handle projection with lookup', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'projection-lookup', - description: 'test description', - private: false, - members: [], - archived: false - }) - - const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { - message: 'test' - }) - - const callback = jest.fn() - - liveQuery.query(test.class.TestComment, { _id: comment }, callback, { - projection: { message: 1, _id: 1 }, - lookup: { space: core.class.Space } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - - await close() - }) - - it('should handle updates to documents with limit queries', async () => { - const { liveQuery, factory, close } = await getClient() - - const spaces: Array> = [] - - // Create documents - for (let i = 0; i < 5; i++) { - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: `limit-${i}`, - description: 'test', - private: false, - members: [], - archived: false - }) - spaces.push(space) - } - - const callback = jest.fn() - - // Query with limit - liveQuery.query(core.class.Space, {}, callback, { limit: 3 }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Update a document - await factory.updateDoc(core.class.Space, core.space.Model, spaces[0], { - description: 'updated' - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback.mock.calls.length).toBeGreaterThan(1) - - await close() - }) - - it('should handle removing attached documents', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'remove-attached', - description: 'test', - private: false, - members: [], - archived: false - }) - - const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { - message: 'to be removed' - }) - - const callback = jest.fn() - - liveQuery.query(test.class.TestComment, { _id: comment }, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Remove the comment - await factory.removeCollection(test.class.TestComment, space, comment, space, core.class.Space, 'comments') - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback.mock.calls.length).toBeGreaterThan(1) - - await close() - }) - - it('should handle multiple simultaneous updates', async () => { - const { liveQuery, factory, close } = await getClient() - - const spaces: Array> = [] - - for (let i = 0; i < 3; i++) { - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: `multi-${i}`, - description: 'original', - private: false, - members: [], - archived: false - }) - spaces.push(space) - } - - const callback = jest.fn() - - liveQuery.query(core.class.Space, {}, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - const initialCalls = callback.mock.calls.length - - // Simultaneous updates - await Promise.all( - spaces.map((space) => - factory.updateDoc(core.class.Space, core.space.Model, space, { - description: 'updated' - }) - ) - ) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - expect(callback.mock.calls.length).toBeGreaterThan(initialCalls) - - await close() - }) - - it('should handle complex query conditions with $ne', async () => { - const { liveQuery, factory, close } = await getClient() - - await factory.createDoc(core.class.Space, core.space.Model, { - name: 'not-this', - description: 'test', - private: false, - members: [], - archived: false - }) - - await factory.createDoc(core.class.Space, core.space.Model, { - name: 'this-one', - description: 'test', - private: false, - members: [], - archived: false - }) - - const callback = jest.fn() - - liveQuery.query(core.class.Space, { name: { $ne: 'not-this' } }, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - - await close() - }) - - it('should handle attached document updates with reverse lookup', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'reverse-update', - description: 'test', - private: false, - members: [], - archived: false - }) - - const parentComment = await factory.addCollection( - test.class.TestComment, - space, - space, - core.class.Space, - 'comments', - { - message: 'parent' - } - ) - - const childComment = await factory.addCollection( - test.class.TestComment, - space, - parentComment, - test.class.TestComment, - 'comments', - { - message: 'original child' - } - ) - - const callback = jest.fn() - - liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, { - lookup: { - _id: { comments: test.class.TestComment } - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Update child comment - await factory.updateCollection( - test.class.TestComment, - space, - childComment, - parentComment, - test.class.TestComment, - 'comments', - { - message: 'updated child' - } - ) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback.mock.calls.length).toBeGreaterThan(1) - - await close() - }) - - it('should handle query reactivation from queue with updates', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'reactivate', - description: 'test', - private: false, - members: [], - archived: false - }) - - const callback1 = jest.fn() - - // Subscribe - const unsub = liveQuery.query(core.class.Space, { _id: space }, callback1) - - await new Promise((resolve) => setTimeout(resolve, 50)) - - // Unsubscribe to move to queue - unsub() - - await new Promise((resolve) => setTimeout(resolve, 50)) - - // Update while in queue - await factory.updateDoc(core.class.Space, core.space.Model, space, { - description: 'updated while queued' - }) - - await new Promise((resolve) => setTimeout(resolve, 50)) - - const callback2 = jest.fn() - - // Reactivate from queue - liveQuery.query(core.class.Space, { _id: space }, callback2) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback2).toHaveBeenCalled() - - await close() - }) - - it('should handle findAll with total option', async () => { - const { liveQuery, factory, close } = await getClient() - - // Create more docs than limit - for (let i = 0; i < 10; i++) { - await factory.createDoc(core.class.Space, core.space.Model, { - name: `total-${i}`, - description: 'test', - private: false, - members: [], - archived: false - }) - } - - const result = await liveQuery.findAll( - core.class.Space, - {}, - { - limit: 5, - total: true - } - ) - - expect(result.length).toBe(5) - expect(result.total).toBeGreaterThanOrEqual(10) - - await close() - }) - - it('should handle complex attachedTo relationships', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'complex-attached', - description: 'test', - private: false, - members: [], - archived: false - }) - - const comment1 = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { - message: 'comment1' - }) - - await factory.addCollection(test.class.TestComment, space, comment1, test.class.TestComment, 'comments', { - message: 'comment2' - }) - - const callback = jest.fn() - - liveQuery.query(test.class.TestComment, { attachedTo: comment1 }, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - - await close() - }) -}) diff --git a/foundations/core/packages/query/src/__tests__/init-utility.test.ts b/foundations/core/packages/query/src/__tests__/init-utility.test.ts deleted file mode 100644 index 58bfa9f6e5..0000000000 --- a/foundations/core/packages/query/src/__tests__/init-utility.test.ts +++ /dev/null @@ -1,452 +0,0 @@ -// Tests for initialization, error handling, and utility code paths -// -// Copyright © 2024 Hardcore Engineering Inc. -// - -import core, { createClient, Ref, TxOperations } from '@hcengineering/core' -import { LiveQuery } from '..' -import { connect } from './connection' -import { test } from './minmodel' - -async function getClient (): Promise<{ liveQuery: LiveQuery, factory: TxOperations, close: () => Promise }> { - const storage = await createClient(connect) - const liveQuery = new LiveQuery(storage) - storage.notify = (...tx) => { - void liveQuery.tx(...tx) - } - return { - liveQuery, - factory: new TxOperations(storage, core.account.System), - close: async () => { - await liveQuery.close() - } - } -} - -describe('Initialization and Utility Code', () => { - describe('refreshConnect with clean flag', () => { - it('should clean queries when refreshConnect is called with clean=true', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'clean-refresh-test', - description: 'test', - private: false, - members: [], - archived: false - }) - - await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { - message: 'test' - }) - - const callback = jest.fn() - - // Create query - liveQuery.query(test.class.TestComment, {}, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - const initialCalls = callback.mock.calls.length - - // Call refreshConnect with clean=true - await liveQuery.refreshConnect(true) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - // Should have been called with empty result during clean, then refreshed - expect(callback.mock.calls.length).toBeGreaterThan(initialCalls) - - await close() - }) - - it('should handle queued queries during refreshConnect with clean', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'queue-clean-refresh', - description: 'test', - private: false, - members: [], - archived: false - }) - - const callback = jest.fn() - - // Create and unsubscribe to move to queue - const unsubscribe = liveQuery.query(test.class.TestComment, { space }, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - unsubscribe() - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Clean refresh - await liveQuery.refreshConnect(true) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - expect(callback).toHaveBeenCalled() - - await close() - }) - - it('should remove queries with no callbacks during refreshConnect', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'remove-no-callbacks', - description: 'test', - private: false, - members: [], - archived: false - }) - - const callback = jest.fn() - - // Create query - const unsubscribe = liveQuery.query(test.class.TestComment, { space }, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Unsubscribe to remove all callbacks - unsubscribe() - - await new Promise((resolve) => setTimeout(resolve, 50)) - - // Now refresh - queries with no callbacks should be removed - await liveQuery.refreshConnect(false) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Query should still work - expect(callback).toHaveBeenCalled() - - await close() - }) - }) - - describe('findOne with projection', () => { - it('should add required fields to projection in findOne', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'projection-test', - description: 'test description', - private: false, - members: [], - archived: false - }) - - // findOne with projection - should auto-add _class, space, modifiedOn - const result = await liveQuery.findOne( - core.class.Space, - { _id: space }, - { - projection: { - name: 1 - } - } - ) - - expect(result).toBeDefined() - expect(result?.name).toBe('projection-test') - expect(result?._class).toBeDefined() - expect(result?.space).toBeDefined() - - await close() - }) - - it('should handle findOne with empty options (auto-create options)', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'no-options-test', - description: 'test', - private: false, - members: [], - archived: false - }) - - // findOne without options - should create options with limit=1 - const result = await liveQuery.findOne(core.class.Space, { _id: space }) - - expect(result).toBeDefined() - expect(result?.name).toBe('no-options-test') - - await close() - }) - - it('should handle findOne from refs cache', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'refs-cache-test', - description: 'test', - private: false, - members: [], - archived: false - }) - - // First call - populates refs cache - const result1 = await liveQuery.findOne(core.class.Space, { _id: space }, { limit: 1 }) - - expect(result1).toBeDefined() - - // Second call - should come from refs cache - const result2 = await liveQuery.findOne(core.class.Space, { _id: space }, { limit: 1 }) - - expect(result2).toBeDefined() - expect(result2?._id).toBe(space) - - await close() - }) - }) - - describe('findOne with queued queries', () => { - it('should move query from queue back to active queries on findOne', async () => { - const { liveQuery, factory, close } = await getClient() - - await factory.createDoc(core.class.Space, core.space.Model, { - name: 'queue-to-active', - description: 'test', - private: false, - members: [], - archived: false - }) - - const callback = jest.fn() - - // Create query then unsubscribe to move to queue - const unsubscribe = liveQuery.query(core.class.Space, { name: 'queue-to-active' }, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - unsubscribe() - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Now call findOne with same query - should find it in queue - const result = await liveQuery.findOne(core.class.Space, { name: 'queue-to-active' }) - - expect(result).toBeDefined() - expect(result?.name).toBe('queue-to-active') - - await close() - }) - }) - - describe('Query result promise handling', () => { - it('should await query result if it is a promise', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'promise-result', - description: 'test', - private: false, - members: [], - archived: false - }) - - // Create a query that will have a promise result initially - const result = await liveQuery.findOne(core.class.Space, { _id: space }) - - expect(result).toBeDefined() - expect(result?.name).toBe('promise-result') - - await close() - }) - }) - - describe('Query cleanup', () => { - it('should clean query result when cleanQuery is called', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'clean-query', - description: 'test', - private: false, - members: [], - archived: false - }) - - const callback = jest.fn() - - liveQuery.query(test.class.TestComment, { space }, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Refresh with clean will call cleanQuery - await liveQuery.refreshConnect(true) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - // Should have received empty result during clean, then new results - expect(callback).toHaveBeenCalled() - - await close() - }) - }) - - describe('Multiple simultaneous operations', () => { - it('should handle multiple findOne calls in parallel', async () => { - const { liveQuery, factory, close } = await getClient() - - const spaces: Array> = [] - for (let i = 0; i < 5; i++) { - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: `parallel-${i}`, - description: 'test', - private: false, - members: [], - archived: false - }) - spaces.push(space) - } - - // Call findOne for all spaces in parallel - const results = await Promise.all(spaces.map((space) => liveQuery.findOne(core.class.Space, { _id: space }))) - - expect(results).toHaveLength(5) - results.forEach((result, idx) => { - expect(result).toBeDefined() - expect(result?.name).toBe(`parallel-${idx}`) - }) - - await close() - }) - - it('should handle multiple query subscriptions in parallel', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'parallel-queries', - description: 'test', - private: false, - members: [], - archived: false - }) - - const callbacks = [jest.fn(), jest.fn(), jest.fn()] - - // Create multiple subscriptions in parallel - callbacks.forEach((callback) => { - liveQuery.query(core.class.Space, { _id: space }, callback) - }) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - // All callbacks should have been called - callbacks.forEach((callback) => { - expect(callback).toHaveBeenCalled() - }) - - await close() - }) - }) - - describe('Edge case: Empty findAll result', () => { - it('should handle findAll returning empty results', async () => { - const { liveQuery, factory, close } = await getClient() - - // Just make sure liveQuery is initialized - await factory.createDoc(core.class.Space, core.space.Model, { - name: 'dummy', - description: 'test', - private: false, - members: [], - archived: false - }) - - const result = await liveQuery.findAll(test.class.TestComment, { message: 'does-not-exist-ever' }) - - expect(result).toHaveLength(0) - - await close() - }) - - it('should handle findOne returning undefined', async () => { - const { liveQuery, factory, close } = await getClient() - - // Just make sure liveQuery is initialized - await factory.createDoc(core.class.Space, core.space.Model, { - name: 'dummy', - description: 'test', - private: false, - members: [], - archived: false - }) - - const result = await liveQuery.findOne(test.class.TestComment, { message: 'does-not-exist-ever' }) - - expect(result).toBeUndefined() - - await close() - }) - }) - - describe('Query with limit and projection', () => { - it('should handle query with both limit and projection', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'limit-projection', - description: 'test description', - private: false, - members: [], - archived: false - }) - - const callback = jest.fn() - - liveQuery.query(core.class.Space, { _id: space }, callback, { - limit: 10, - projection: { - name: 1, - description: 1 - } - }) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - expect(callback).toHaveBeenCalled() - - const result = callback.mock.calls[callback.mock.calls.length - 1][0][0] - expect(result.name).toBe('limit-projection') - expect(result._class).toBeDefined() - - await close() - }) - }) - - describe('Result clone operations', () => { - it('should return cloned result from findOne', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'clone-test', - description: 'test', - private: false, - members: [], - archived: false - }) - - const result = await liveQuery.findOne(core.class.Space, { _id: space }) - - expect(result).toBeDefined() - - // Modify the result - should not affect internal state - if (result !== undefined) { - ;(result as any).name = 'modified' - } - - // Query again - should get original value - const result2 = await liveQuery.findOne(core.class.Space, { _id: space }) - - expect(result2?.name).toBe('clone-test') - - await close() - }) - }) -}) diff --git a/foundations/core/packages/query/src/__tests__/livequery-coverage.test.ts b/foundations/core/packages/query/src/__tests__/livequery-coverage.test.ts deleted file mode 100644 index 0d71440532..0000000000 --- a/foundations/core/packages/query/src/__tests__/livequery-coverage.test.ts +++ /dev/null @@ -1,379 +0,0 @@ -// Coverage improvement tests for LiveQuery functionality -// -// Copyright © 2024 Hardcore Engineering Inc. -// - -import core, { createClient, TxOperations } from '@hcengineering/core' -import { LiveQuery } from '..' -import { connect } from './connection' - -async function getClient (): Promise<{ liveQuery: LiveQuery, factory: TxOperations, close: () => Promise }> { - const storage = await createClient(connect) - const liveQuery = new LiveQuery(storage) - storage.notify = (...tx) => { - void liveQuery.tx(...tx) - } - return { - liveQuery, - factory: new TxOperations(storage, core.account.System), - close: async () => { - await liveQuery.close() - } - } -} - -describe('LiveQuery Coverage Tests', () => { - it('should handle refreshConnect with clean=true', async () => { - const { liveQuery, close } = await getClient() - - const callback = jest.fn() - - // Create a query - const unsubscribe = liveQuery.query(core.class.Space, {}, callback) - - // Wait for initial results - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - callback.mockClear() - - // Unsubscribe to move to queue - unsubscribe() - await new Promise((resolve) => setTimeout(resolve, 50)) - - // Refresh with clean=true should reset the query - await liveQuery.refreshConnect(true) - - // Verify the query was cleaned - await close() - }) - - it('should handle isClosed', async () => { - const { liveQuery } = await getClient() - - expect(liveQuery.isClosed()).toBe(false) - - await liveQuery.close() - - expect(liveQuery.isClosed()).toBe(true) - }) - - it('should handle findOne with projection', async () => { - const { liveQuery, factory, close } = await getClient() - - // Create a document - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'test-space', - description: 'test', - private: false, - members: [], - archived: false - }) - - // FindOne with projection - const result = await liveQuery.findOne(core.class.Space, { _id: space }, { projection: { name: 1 } }) - - expect(result).toBeDefined() - expect(result?.name).toBe('test-space') - - await close() - }) - - it('should pass searchFulltext to client', async () => { - const { liveQuery, close } = await getClient() - - const result = await liveQuery.searchFulltext({ query: 'test' }, {}) - - expect(result).toBeDefined() - - await close() - }) - - it('should return hierarchy and model from client', async () => { - const { liveQuery, close } = await getClient() - - const hierarchy = liveQuery.getHierarchy() - const model = liveQuery.getModel() - - expect(hierarchy).toBeDefined() - expect(model).toBeDefined() - - await close() - }) - - it('should handle multiple callbacks for same query', async () => { - const { liveQuery, close } = await getClient() - - const callback1 = jest.fn() - const callback2 = jest.fn() - const callback3 = jest.fn() - - liveQuery.query(core.class.Space, {}, callback1) - liveQuery.query(core.class.Space, {}, callback2) - liveQuery.query(core.class.Space, {}, callback3) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // All callbacks should be registered and called - expect(callback1).toHaveBeenCalled() - expect(callback2).toHaveBeenCalled() - expect(callback3).toHaveBeenCalled() - - await close() - }) - - it('should remove only specific callback on unsubscribe', async () => { - const { liveQuery, close } = await getClient() - - const callback1 = jest.fn() - const callback2 = jest.fn() - - liveQuery.query(core.class.Space, {}, callback1) - const unsubscribe2 = liveQuery.query(core.class.Space, {}, callback2) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - callback1.mockClear() - callback2.mockClear() - - unsubscribe2() - - // callback1 should still be active - await new Promise((resolve) => setTimeout(resolve, 50)) - - await close() - }) - - it('should handle query moving from active to queue and back', async () => { - const { liveQuery, close } = await getClient() - - const callback1 = jest.fn() - const callback2 = jest.fn() - - // Subscribe - const unsubscribe1 = liveQuery.query(core.class.Space, {}, callback1) - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback1).toHaveBeenCalled() - - // Unsubscribe - moves to queue - unsubscribe1() - await new Promise((resolve) => setTimeout(resolve, 50)) - - callback1.mockClear() - callback2.mockClear() - - // Subscribe again with same query - should reuse from queue - liveQuery.query(core.class.Space, {}, callback2) - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback2).toHaveBeenCalled() - - await close() - }) - - it('should handle TxUpdateDoc with search query', async () => { - const { liveQuery, factory, close } = await getClient() - - // Create a document - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'searchable-space', - description: 'test document for search', - private: false, - members: [], - archived: false - }) - - const callback = jest.fn() - - // Query with search (though actual search won't work in mock, it tests the code path) - liveQuery.query(core.class.Space, { _id: space }, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - - // Update the document - await factory.updateDoc(core.class.Space, core.space.Model, space, { - name: 'updated-space' - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Callback should have been called again with updated results - expect(callback.mock.calls.length).toBeGreaterThan(1) - - await close() - }) - - it('should handle TxRemoveDoc with total tracking', async () => { - const { liveQuery, factory, close } = await getClient() - - // Create a document - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'to-be-removed', - description: 'test', - private: false, - members: [], - archived: false - }) - - const callback = jest.fn() - - // Query with total tracking - liveQuery.query(core.class.Space, { _id: space }, callback, { total: true }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - callback.mockClear() - - // Remove the document - await factory.removeDoc(core.class.Space, core.space.Model, space) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Callback should have been called with updated results - expect(callback).toHaveBeenCalled() - - await close() - }) - - it('should handle lookup queries', async () => { - const { liveQuery, close } = await getClient() - - const callback = jest.fn() - - // Query with reverse lookup - liveQuery.query(core.class.Space, {}, callback, { - lookup: { - _id: { - attachedDocs: core.class.Doc - } - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - - await close() - }) - - it('should compare options correctly ignoring ctx', async () => { - const { liveQuery, close } = await getClient() - - const callback1 = jest.fn() - const callback2 = jest.fn() - - // Query with same options (should reuse) - liveQuery.query(core.class.Space, {}, callback1, { limit: 10, sort: { modifiedOn: 1 } }) - - liveQuery.query(core.class.Space, {}, callback2, { limit: 10, sort: { modifiedOn: 1 } }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback1).toHaveBeenCalled() - expect(callback2).toHaveBeenCalled() - - await close() - }) - - it('should treat different options as different queries', async () => { - const { liveQuery, close } = await getClient() - - const callback1 = jest.fn() - const callback2 = jest.fn() - - liveQuery.query(core.class.Space, {}, callback1, { limit: 10 }) - - liveQuery.query(core.class.Space, {}, callback2, { limit: 20 }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback1).toHaveBeenCalled() - expect(callback2).toHaveBeenCalled() - - await close() - }) - - it('should handle TxMixin updates', async () => { - const { liveQuery, factory, close } = await getClient() - - // Create a space document - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'mixin-test', - description: 'test', - private: false, - members: [], - archived: false - }) - - const callback = jest.fn() - - liveQuery.query(core.class.Space, { _id: space }, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - - await close() - }) - - it('should handle document creation via transaction', async () => { - const { liveQuery, factory, close } = await getClient() - - const callback = jest.fn() - - // Set up query before creating document - liveQuery.query(core.class.Space, { name: 'new-space' }, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - const initialCallCount = callback.mock.calls.length - - // Create matching document - await factory.createDoc(core.class.Space, core.space.Model, { - name: 'new-space', - description: 'newly created', - private: false, - members: [], - archived: false - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Should have been called again with new document - expect(callback.mock.calls.length).toBeGreaterThan(initialCallCount) - - await close() - }) - - it('should handle refreshConnect without clean', async () => { - const { liveQuery, close } = await getClient() - - const callback = jest.fn() - - liveQuery.query(core.class.Space, {}, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Refresh without clean - await liveQuery.refreshConnect(false) - - await close() - }) - - it('should handle associations option', async () => { - const { liveQuery, close } = await getClient() - - const callback = jest.fn() - - liveQuery.query(core.class.Space, {}, callback, { associations: [] }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - - await close() - }) -}) diff --git a/foundations/core/packages/query/src/__tests__/minmodel.ts b/foundations/core/packages/query/src/__tests__/minmodel.ts deleted file mode 100644 index 6cb584cc6e..0000000000 --- a/foundations/core/packages/query/src/__tests__/minmodel.ts +++ /dev/null @@ -1,269 +0,0 @@ -// -// Copyright © 2020 Anticrm Platform Contributors. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { - PersonId, - Arr, - Class, - Data, - Doc, - Domain, - Mixin, - Obj, - Ref, - Space, - TxCreateDoc, - TxCUD, - AccountUuid -} from '@hcengineering/core' -import core, { AttachedDoc, ClassifierKind, DOMAIN_MODEL, DOMAIN_TX, TxFactory } from '@hcengineering/core' -import type { IntlString, Plugin } from '@hcengineering/platform' -import { plugin } from '@hcengineering/platform' - -const txFactory = new TxFactory(core.account.System) - -function createClass (_class: Ref>, attributes: Data>): TxCreateDoc { - return txFactory.createTxCreateDoc(core.class.Class, core.space.Model, attributes, _class) -} - -/** - * @public - */ -export function createDoc ( - _class: Ref>, - attributes: Data, - id?: Ref, - modifiedBy?: PersonId -): TxCreateDoc { - const result = txFactory.createTxCreateDoc(_class, core.space.Model, attributes, id) - if (modifiedBy !== undefined) { - result.modifiedBy = modifiedBy - } - return result -} - -/** - * @public - */ -export interface TestMixin extends Doc { - arr: Arr -} - -/** - * @public - */ -export interface AttachedComment extends AttachedDoc { - message: string -} - -interface TestProject extends Space { - prjName: string -} - -interface TestProjectMixin extends TestProject { - someField?: string -} - -/** - * @public - */ -export const test = plugin('test' as Plugin, { - mixin: { - TestMixin: '' as Ref>, - TestProjectMixin: '' as Ref> - }, - class: { - TestComment: '' as Ref>, - ParticipantsHolder: '' as Ref>, - TestProject: '' as Ref> - } -}) - -/** - * @public - */ -export interface ParticipantsHolder extends Doc { - participants?: Ref[] -} - -const DOMAIN_TEST: Domain = 'test' as Domain - -/** - * @public - * Generate minimal model for testing purposes. - * @returns R - */ -export function genMinModel (): TxCUD[] { - const txes = [] - // Fill Tx'es with basic model classes. - txes.push(createClass(core.class.Obj, { label: 'Obj' as IntlString, kind: ClassifierKind.CLASS })) - txes.push( - createClass(core.class.Doc, { label: 'Doc' as IntlString, extends: core.class.Obj, kind: ClassifierKind.CLASS }) - ) - txes.push( - createClass(core.class.Class, { - label: 'Class' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.CLASS, - domain: DOMAIN_MODEL - }) - ) - txes.push( - createClass(core.class.Mixin, { - label: 'Mixin' as IntlString, - extends: core.class.Class, - kind: ClassifierKind.CLASS, - domain: DOMAIN_MODEL - }) - ) - txes.push( - createClass(core.class.AttachedDoc, { - label: 'AttachedDoc' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.MIXIN - }) - ) - txes.push( - createClass(core.class.Space, { - label: 'Space' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.CLASS, - domain: DOMAIN_MODEL - }) - ) - // TODO: fixme! - // txes.push( - // createClass(core.class.Account, { - // label: 'Account' as IntlString, - // extends: core.class.Doc, - // kind: ClassifierKind.CLASS, - // domain: DOMAIN_MODEL - // }) - // ) - - txes.push( - createClass(core.class.Tx, { - label: 'Tx' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.CLASS, - domain: DOMAIN_TX - }) - ) - txes.push( - createClass(core.class.TxCUD, { - label: 'TxCUD' as IntlString, - extends: core.class.Tx, - kind: ClassifierKind.CLASS, - domain: DOMAIN_TX - }) - ) - txes.push( - createClass(core.class.TxCreateDoc, { - label: 'TxCreateDoc' as IntlString, - extends: core.class.TxCUD, - kind: ClassifierKind.CLASS - }) - ) - txes.push( - createClass(core.class.TxUpdateDoc, { - label: 'TxUpdateDoc' as IntlString, - extends: core.class.TxCUD, - kind: ClassifierKind.CLASS - }) - ) - txes.push( - createClass(core.class.TxRemoveDoc, { - label: 'TxRemoveDoc' as IntlString, - extends: core.class.TxCUD, - kind: ClassifierKind.CLASS - }) - ) - txes.push( - createClass(core.class.TxMixin, { - label: 'TxMixin' as IntlString, - extends: core.class.TxCUD, - kind: ClassifierKind.CLASS - }) - ) - - txes.push( - createClass(test.mixin.TestMixin, { - label: 'TestMixin' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.MIXIN - }) - ) - - txes.push( - createClass(test.class.TestProject, { - label: 'TestProject' as IntlString, - extends: core.class.Space, - kind: ClassifierKind.CLASS, - domain: DOMAIN_TEST - }) - ) - - txes.push( - createClass(test.mixin.TestProjectMixin, { - label: 'TestProjectMixin' as IntlString, - extends: test.class.TestProject, - kind: ClassifierKind.MIXIN - }) - ) - - txes.push( - createClass(test.class.TestComment, { - label: 'TestComment' as IntlString, - extends: core.class.AttachedDoc, - kind: ClassifierKind.CLASS, - domain: DOMAIN_TEST - }) - ) - - txes.push( - createClass(test.class.ParticipantsHolder, { - label: 'ParticipantsHolder' as IntlString, - extends: core.class.Doc, - kind: ClassifierKind.CLASS, - domain: DOMAIN_TEST - }) - ) - - const u1 = 'User1' as AccountUuid - const u2 = 'User2' as AccountUuid - // TODO: fixme! - txes.push( - // createDoc(core.class.Account, { email: 'user1@site.com', role: AccountRole.User }, u1), - // createDoc(core.class.Account, { email: 'user2@site.com', role: AccountRole.User }, u2), - createDoc(core.class.Space, { - name: 'Sp1', - description: '', - private: false, - members: [u1, u2], - archived: false - }) - ) - - txes.push( - createDoc(core.class.Space, { - name: 'Sp2', - description: '', - private: false, - members: [u1], - archived: false - }) - ) - return txes -} diff --git a/foundations/core/packages/query/src/__tests__/push-pull.test.ts b/foundations/core/packages/query/src/__tests__/push-pull.test.ts deleted file mode 100644 index a0ee4cb042..0000000000 --- a/foundations/core/packages/query/src/__tests__/push-pull.test.ts +++ /dev/null @@ -1,546 +0,0 @@ -// Tests for $push/$pull operations in lookups -// -// Copyright © 2024 Hardcore Engineering Inc. -// - -import core, { createClient, Ref, TxOperations } from '@hcengineering/core' -import { LiveQuery } from '..' -import { connect } from './connection' -import { test } from './minmodel' - -async function getClient (): Promise<{ liveQuery: LiveQuery, factory: TxOperations, close: () => Promise }> { - const storage = await createClient(connect) - const liveQuery = new LiveQuery(storage) - storage.notify = (...tx) => { - void liveQuery.tx(...tx) - } - return { - liveQuery, - factory: new TxOperations(storage, core.account.System), - close: async () => { - await liveQuery.close() - } - } -} - -describe('Push/Pull Operations with Lookups', () => { - describe('$push with array lookup values', () => { - it('should update lookup when $push is used with array of IDs', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'push-array-test', - description: 'test', - private: false, - members: [], - archived: false - }) - - const parentComment = await factory.addCollection( - test.class.TestComment, - space, - space, - core.class.Space, - 'comments', - { - message: 'parent' - } - ) - - const callback = jest.fn() - - // Query with reverse lookup - liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, { - lookup: { - _id: { comments: test.class.TestComment } - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Create multiple child comments - await factory.addCollection(test.class.TestComment, space, parentComment, test.class.TestComment, 'comments', { - message: 'child1' - }) - - await factory.addCollection(test.class.TestComment, space, parentComment, test.class.TestComment, 'comments', { - message: 'child2' - }) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - expect(callback).toHaveBeenCalled() - - const lastResult = callback.mock.calls[callback.mock.calls.length - 1][0][0] - expect(lastResult.$lookup).toBeDefined() - - await close() - }) - - it('should handle $push with single ID value in lookup', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'push-single-test', - description: 'test', - private: false, - members: [], - archived: false - }) - - const parentComment = await factory.addCollection( - test.class.TestComment, - space, - space, - core.class.Space, - 'comments', - { - message: 'parent' - } - ) - - const callback = jest.fn() - - liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, { - lookup: { - _id: { comments: test.class.TestComment } - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - const initialCalls = callback.mock.calls.length - - // Add a single child - await factory.addCollection(test.class.TestComment, space, parentComment, test.class.TestComment, 'comments', { - message: 'single child' - }) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - expect(callback.mock.calls.length).toBeGreaterThan(initialCalls) - - await close() - }) - }) - - describe('$pull with array lookup values', () => { - it('should update lookup when $pull is used with array of IDs', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'pull-array-test', - description: 'test', - private: false, - members: [], - archived: false - }) - - const parentComment = await factory.addCollection( - test.class.TestComment, - space, - space, - core.class.Space, - 'comments', - { - message: 'parent' - } - ) - - // Create multiple children - const children: Array> = [] - for (let i = 0; i < 3; i++) { - const child = await factory.addCollection( - test.class.TestComment, - space, - parentComment, - test.class.TestComment, - 'comments', - { - message: `child-${i}` - } - ) - children.push(child) - } - - await new Promise((resolve) => setTimeout(resolve, 100)) - - const callback = jest.fn() - - liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, { - lookup: { - _id: { comments: test.class.TestComment } - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - const initialCalls = callback.mock.calls.length - - // Remove multiple children - await factory.removeCollection( - test.class.TestComment, - space, - children[0], - parentComment, - test.class.TestComment, - 'comments' - ) - - await factory.removeCollection( - test.class.TestComment, - space, - children[1], - parentComment, - test.class.TestComment, - 'comments' - ) - - await new Promise((resolve) => setTimeout(resolve, 200)) - - expect(callback.mock.calls.length).toBeGreaterThan(initialCalls) - - await close() - }) - - it('should handle $pull with single ID value in lookup', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'pull-single-test', - description: 'test', - private: false, - members: [], - archived: false - }) - - const parentComment = await factory.addCollection( - test.class.TestComment, - space, - space, - core.class.Space, - 'comments', - { - message: 'parent' - } - ) - - const childComment = await factory.addCollection( - test.class.TestComment, - space, - parentComment, - test.class.TestComment, - 'comments', - { - message: 'single child' - } - ) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - const callback = jest.fn() - - liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, { - lookup: { - _id: { comments: test.class.TestComment } - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - const initialCalls = callback.mock.calls.length - - // Remove single child - await factory.removeCollection( - test.class.TestComment, - space, - childComment, - parentComment, - test.class.TestComment, - 'comments' - ) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - expect(callback.mock.calls.length).toBeGreaterThan(initialCalls) - - await close() - }) - }) - - describe('Lookup initialization with undefined $lookup', () => { - it('should initialize $lookup array when undefined on $push', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'init-push-test', - description: 'test', - private: false, - members: [], - archived: false - }) - - const parentComment = await factory.addCollection( - test.class.TestComment, - space, - space, - core.class.Space, - 'comments', - { - message: 'parent with no children initially' - } - ) - - const callback = jest.fn() - - liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, { - lookup: { - _id: { comments: test.class.TestComment } - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Add first child - should initialize $lookup.comments array - await factory.addCollection(test.class.TestComment, space, parentComment, test.class.TestComment, 'comments', { - message: 'first child' - }) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - expect(callback).toHaveBeenCalled() - - await close() - }) - - it('should initialize $lookup array when undefined on $pull', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'init-pull-test', - description: 'test', - private: false, - members: [], - archived: false - }) - - const parentComment = await factory.addCollection( - test.class.TestComment, - space, - space, - core.class.Space, - 'comments', - { - message: 'parent' - } - ) - - const childComment = await factory.addCollection( - test.class.TestComment, - space, - parentComment, - test.class.TestComment, - 'comments', - { - message: 'child' - } - ) - - const callback = jest.fn() - - liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, { - lookup: { - _id: { comments: test.class.TestComment } - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - const initialCalls = callback.mock.calls.length - - // Remove child - should handle undefined $lookup gracefully - await factory.removeCollection( - test.class.TestComment, - space, - childComment, - parentComment, - test.class.TestComment, - 'comments' - ) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - expect(callback.mock.calls.length).toBeGreaterThan(initialCalls) - - await close() - }) - }) - - describe('Complex lookup scenarios', () => { - it('should handle nested lookups with $push operations', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'nested-push-test', - description: 'test', - private: false, - members: [], - archived: false - }) - - const rootComment = await factory.addCollection( - test.class.TestComment, - space, - space, - core.class.Space, - 'comments', - { - message: 'root' - } - ) - - const callback = jest.fn() - - liveQuery.query(test.class.TestComment, { _id: rootComment }, callback, { - lookup: { - _id: { comments: test.class.TestComment }, - space: core.class.Space - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Add nested comment - await factory.addCollection(test.class.TestComment, space, rootComment, test.class.TestComment, 'comments', { - message: 'nested' - }) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - expect(callback).toHaveBeenCalled() - - await close() - }) - - it('should handle multiple simultaneous $push operations', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'multi-push-test', - description: 'test', - private: false, - members: [], - archived: false - }) - - const parentComment = await factory.addCollection( - test.class.TestComment, - space, - space, - core.class.Space, - 'comments', - { - message: 'parent' - } - ) - - const callback = jest.fn() - - liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, { - lookup: { - _id: { comments: test.class.TestComment } - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Add multiple children rapidly - await Promise.all([ - factory.addCollection(test.class.TestComment, space, parentComment, test.class.TestComment, 'comments', { - message: 'child-a' - }), - factory.addCollection(test.class.TestComment, space, parentComment, test.class.TestComment, 'comments', { - message: 'child-b' - }), - factory.addCollection(test.class.TestComment, space, parentComment, test.class.TestComment, 'comments', { - message: 'child-c' - }) - ]) - - await new Promise((resolve) => setTimeout(resolve, 200)) - - expect(callback).toHaveBeenCalled() - - await close() - }) - - it('should handle mixed $push and $pull operations', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'mixed-ops-test', - description: 'test', - private: false, - members: [], - archived: false - }) - - const parentComment = await factory.addCollection( - test.class.TestComment, - space, - space, - core.class.Space, - 'comments', - { - message: 'parent' - } - ) - - const existingChild = await factory.addCollection( - test.class.TestComment, - space, - parentComment, - test.class.TestComment, - 'comments', - { - message: 'existing' - } - ) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - const callback = jest.fn() - - liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, { - lookup: { - _id: { comments: test.class.TestComment } - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Add new child - await factory.addCollection(test.class.TestComment, space, parentComment, test.class.TestComment, 'comments', { - message: 'new' - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Remove existing child - await factory.removeCollection( - test.class.TestComment, - space, - existingChild, - parentComment, - test.class.TestComment, - 'comments' - ) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - expect(callback).toHaveBeenCalled() - - await close() - }) - }) -}) diff --git a/foundations/core/packages/query/src/__tests__/query.test.ts b/foundations/core/packages/query/src/__tests__/query.test.ts deleted file mode 100644 index bd6ea9526c..0000000000 --- a/foundations/core/packages/query/src/__tests__/query.test.ts +++ /dev/null @@ -1,1023 +0,0 @@ -// -// Copyright © 2021 Anticrm Platform Contributors. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import core, { - createClient, - Doc, - generateId, - MeasureMetricsContext, - Ref, - SortingOrder, - Space, - systemAccountUuid, - Tx, - TxCreateDoc, - TxOperations, - WithLookup -} from '@hcengineering/core' -import { LiveQuery } from '..' -import { connect } from './connection' -import { AttachedComment, genMinModel, ParticipantsHolder, test } from './minmodel' - -interface Channel extends Space { - x: number -} - -async function getClient (): Promise<{ liveQuery: LiveQuery, factory: TxOperations }> { - const storage = await createClient(connect) - const liveQuery = new LiveQuery(storage) - storage.notify = (...tx: Tx[]) => { - liveQuery.tx(...tx).catch((err) => { - console.log(err) - }) - } - return { liveQuery, factory: new TxOperations(storage, core.account.System) } -} - -describe('query', () => { - it('findAll', async () => { - const { liveQuery } = await getClient() - const result = await liveQuery.findAll(core.class.Space, {}) - expect(result).toHaveLength(2) - }) - - it('query with param', async () => { - const { liveQuery } = await getClient() - - let expectedLength = 0 - const txes = genMinModel() - for (let i = 0; i < txes.length; i++) { - if (liveQuery.getHierarchy().isDerived((txes[i] as TxCreateDoc).objectClass, core.class.Space)) { - expectedLength++ - } - } - - const result = await new Promise((resolve) => { - liveQuery.query(core.class.Space, { private: false }, (result) => { - resolve(result) - }) - }) - expect(result).toHaveLength(expectedLength) - }) - - it('query should be live', async () => { - const { liveQuery, factory } = await getClient() - - let expectedLength = 0 - const txes = genMinModel() - for (let i = 0; i < txes.length; i++) { - if (liveQuery.getHierarchy().isDerived((txes[i] as TxCreateDoc).objectClass, core.class.Space)) { - expectedLength++ - } - } - - let attempt = 0 - const pp = new Promise((resolve) => { - liveQuery.query(core.class.Space, { private: false }, (result) => { - expect(result).toHaveLength(expectedLength + attempt) - if (attempt > 0) { - expect((result[expectedLength + attempt - 1] as any).x).toBe(attempt) - } - if (attempt++ === 3) { - // check underlying storage received all data. - liveQuery - .findAll(core.class.Space, { private: false }) - .then((result) => { - expect(result).toHaveLength(expectedLength + attempt - 1) - resolve(null) - }) - .catch((err) => { - expect(err).toBeUndefined() - }) - } - }) - }) - - // TODO: fixme! - // await factory.createDoc(core.class.Account, core.space.Model, { - // email: 'user1@site.com', - // role: AccountRole.User - // }) - await factory.createDoc(core.class.Space, core.space.Model, { - private: true, - name: '#0', - description: '', - members: [], - archived: false, - x: 0 - }) - await factory.createDoc(core.class.Space, core.space.Model, { - private: false, - name: '#1', - description: '', - members: [], - archived: false, - x: 1 - }) - await factory.createDoc(core.class.Space, core.space.Model, { - private: false, - name: '#2', - description: '', - members: [], - archived: false, - x: 2 - }) - await factory.createDoc(core.class.Space, core.space.Model, { - private: false, - name: '#3', - description: '', - members: [], - archived: false, - x: 3 - }) - await pp - }) - - it('unsubscribe query', async () => { - const { liveQuery, factory } = await getClient() - - let expectedLength = 0 - const txes = genMinModel() - for (let i = 0; i < txes.length; i++) { - if (liveQuery.getHierarchy().isDerived((txes[i] as TxCreateDoc).objectClass, core.class.Space)) { - expectedLength++ - } - } - - const unsubscribe = liveQuery.query(core.class.Space, { private: false }, (result) => { - expect(result).toHaveLength(expectedLength) - }) - - unsubscribe() - - await factory.createDoc(core.class.Space, core.space.Model, { - private: false, - name: '#1', - description: '', - archived: false, - members: [] - }) - await factory.createDoc(core.class.Space, core.space.Model, { - private: false, - name: '#2', - description: '', - archived: false, - members: [] - }) - await factory.createDoc(core.class.Space, core.space.Model, { - private: false, - name: '#3', - description: '', - archived: false, - members: [] - }) - }) - - it('query against core client', async () => { - const { liveQuery, factory } = await getClient() - - const expectedLength = 2 - let attempt = 0 - const pp = new Promise((resolve) => { - liveQuery.query(core.class.Space, { private: false }, (result) => { - expect(result).toHaveLength(expectedLength + attempt) - if (attempt > 0) { - expect((result[expectedLength + attempt - 1] as any).x).toBe(attempt) - } - if (attempt++ === 1) resolve(null) - }) - }) - - await factory.createDoc(core.class.Space, core.space.Model, { - x: 1, - private: false, - name: '#1', - description: '', - archived: false, - members: [] - }) - await factory.createDoc(core.class.Space, core.space.Model, { - x: 2, - private: false, - name: '#2', - description: '', - archived: false, - members: [] - }) - await factory.createDoc(core.class.Space, core.space.Model, { - x: 3, - private: false, - name: '#3', - description: '', - archived: false, - members: [] - }) - await pp - }) - - it('limit and sorting', async () => { - const { liveQuery, factory } = await getClient() - - const limit = 1 - let attempt = 0 - let descAttempt = 0 - - const pp1 = new Promise((resolve) => { - liveQuery.query( - core.class.Space, - { private: true }, - (result) => { - if (result.length > 0) { - expect(result.length).toEqual(limit) - expect(result[0].name).toMatch('0') - attempt++ - } - if (attempt === 1) resolve(null) - }, - { limit, sort: { name: SortingOrder.Ascending } } - ) - }) - - const pp2 = new Promise((resolve) => { - liveQuery.query( - core.class.Space, - { private: true }, - (result) => { - if (result.length > 0) { - expect(result.length).toEqual(limit) - expect(result[0].name).toMatch(descAttempt.toString()) - descAttempt++ - } - if (descAttempt === 10) resolve(null) - }, - { limit, sort: { name: SortingOrder.Descending } } - ) - }) - - for (let i = 0; i < 10; i++) { - await factory.createDoc(core.class.Space, core.space.Model, { - private: true, - name: i.toString(), - description: '', - archived: false, - members: [] - }) - } - await Promise.all([pp1, pp2]) - }) - - it('remove', async () => { - const { liveQuery, factory } = await getClient() - - const expectedLength = 2 - let attempt = 0 - let x: undefined | ((s: any) => void) - const y = new Promise((resolve) => (x = resolve)) - - const pp = new Promise((resolve) => { - liveQuery.query(core.class.Space, { private: false }, (result) => { - expect(result).toHaveLength(expectedLength - attempt) - if (attempt === 0) x?.(null) - if (attempt++ === expectedLength) resolve(null) - }) - }) - await y - const spaces = await liveQuery.findAll(core.class.Space, {}) - for (const space of spaces) { - await factory.removeDoc(space._class, space.space, space._id) - } - await pp - }) - - it('remove with limit', async () => { - const { liveQuery, factory } = await getClient() - - const expectedLength = 2 - let attempt = 0 - let x: undefined | ((s: any) => void) - const y = new Promise((resolve) => (x = resolve)) - const pp = new Promise((resolve) => { - liveQuery.query( - core.class.Space, - { private: false }, - (result) => { - if (attempt === 0) x?.(null) - expect(result).toHaveLength(attempt++ === expectedLength ? 0 : 1) - if (attempt === expectedLength) resolve(null) - }, - { limit: 1 } - ) - }) - - await y - const spaces = await liveQuery.findAll(core.class.Space, {}) - for (const space of spaces) { - await factory.removeDoc(space._class, space.space, space._id) - } - await pp - }) - - it('update', async () => { - const { liveQuery, factory } = await getClient() - - const spaces = await liveQuery.findAll(core.class.Space, {}) - let attempt = 0 - const pp = new Promise((resolve) => { - liveQuery.query( - core.class.Space, - { private: false }, - (result) => { - if (attempt > 0) { - expect(result[attempt - 1].name === attempt.toString()) - expect(result[attempt - 1].members.length === 1) - if (attempt === spaces.length) resolve(null) - } - }, - { sort: { private: SortingOrder.Ascending } } - ) - }) - - for (const space of spaces) { - attempt++ - await factory.updateDoc(space._class, space.space, space._id, { - name: attempt.toString(), - $push: { members: systemAccountUuid } - }) - } - await pp - }) - - it('update with no match query', async () => { - const { liveQuery, factory } = await getClient() - - const spaces = await liveQuery.findAll(core.class.Space, {}) - let attempt = 0 - const pp = new Promise((resolve) => { - liveQuery.query( - core.class.Space, - { private: false }, - (result) => { - if (attempt > 0) { - expect(result.length === spaces.length - attempt) - if (attempt === spaces.length) resolve(null) - } - }, - { sort: { private: SortingOrder.Ascending } } - ) - }) - - for (const space of spaces) { - attempt++ - await factory.updateDoc(space._class, space.space, space._id, { - private: true - }) - } - await pp - }) - - it('lookup query add doc', async () => { - const { liveQuery, factory } = await getClient() - const futureSpace: Space = { - _id: generateId(), - _class: core.class.Space, - private: false, - members: [], - space: core.space.Model, - name: 'new space', - description: '', - archived: false, - modifiedBy: core.account.System, - modifiedOn: 0 - } - const comment = await factory.addCollection( - test.class.TestComment, - futureSpace._id, - futureSpace._id, - core.class.Space, - 'comments', - { - message: 'test' - } - ) - let attempt = 0 - const pp = new Promise((resolve) => { - liveQuery.query( - test.class.TestComment, - { _id: comment }, - (result) => { - const comment = result[0] - if (comment !== undefined) { - if (attempt > 0) { - expect(comment.$lookup?.space?._id).toEqual(futureSpace._id) - resolve(null) - } else { - expect(comment.$lookup?.space).toBeUndefined() - attempt++ - void factory.createDoc( - core.class.Space, - futureSpace.space, - { - ...futureSpace - }, - futureSpace._id - ) - } - } - }, - { lookup: { space: core.class.Space } } - ) - }) - - await pp - }) - - it('lookup nested query add doc', async () => { - const { liveQuery, factory } = await getClient() - const futureSpace: Space = { - _id: generateId(), - _class: core.class.Space, - private: false, - members: [], - space: core.space.Model, - name: 'new space', - description: '', - archived: false, - modifiedBy: core.account.System, - modifiedOn: 0 - } - const comment = await factory.addCollection( - test.class.TestComment, - futureSpace._id, - futureSpace._id, - core.class.Space, - 'comments', - { - message: 'test' - } - ) - const childComment = await factory.addCollection( - test.class.TestComment, - futureSpace._id, - comment, - test.class.TestComment, - 'comments', - { - message: 'child' - } - ) - const pp = new Promise((resolve) => { - liveQuery.query( - test.class.TestComment, - { _id: childComment }, - (result) => { - const comment = result[0] - if (comment !== undefined) { - expect((comment.$lookup?.attachedTo as WithLookup)?.$lookup?.space?._id).toEqual( - futureSpace._id - ) - resolve(null) - } - }, - { lookup: { attachedTo: [test.class.TestComment, { space: core.class.Space }] } } - ) - }) - - await factory.createDoc( - core.class.Space, - futureSpace.space, - { - ...futureSpace - }, - futureSpace._id - ) - await pp - }) - - it('lookup reverse query add doc', async () => { - const { liveQuery, factory } = await getClient() - const spaces = await liveQuery.findAll(core.class.Space, {}) - const parentComment = await factory.addCollection( - test.class.TestComment, - spaces[0]._id, - spaces[0]._id, - spaces[0]._class, - 'comments', - { - message: 'test' - } - ) - const childLength = 3 - const pp = new Promise((resolve) => { - liveQuery.query( - test.class.TestComment, - { _id: parentComment }, - (result) => { - const comment = result[0] - const res = (comment.$lookup as any)?.comments?.length - - if (res !== undefined) { - expect(res).toBeGreaterThanOrEqual(1) - expect(res).toBeLessThanOrEqual(childLength) - } - if ((res ?? 0) === childLength) { - resolve(null) - } - }, - { lookup: { _id: { comments: test.class.TestComment } } } - ) - }) - - for (let index = 0; index < childLength; index++) { - await factory.addCollection( - test.class.TestComment, - spaces[0]._id, - parentComment, - test.class.TestComment, - 'comments', - { - message: index.toString() - } - ) - } - await pp - }) - - it('lookup query remove doc', async () => { - const { liveQuery, factory } = await getClient() - const futureSpace = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'new space', - description: '', - archived: false, - private: false, - members: [] - }) - const comment = await factory.addCollection( - test.class.TestComment, - futureSpace, - futureSpace, - core.class.Space, - 'comments', - { - message: 'test' - } - ) - let attempt = 0 - const pp = new Promise((resolve) => { - liveQuery.query( - test.class.TestComment, - { _id: comment }, - (result) => { - const comment = result[0] - if (comment !== undefined) { - if (attempt > 0) { - expect(comment.$lookup?.space).toBeUndefined() - resolve(null) - } else { - expect((comment.$lookup?.space as Doc)?._id).toEqual(futureSpace) - attempt++ - void factory.removeDoc(core.class.Space, core.space.Model, futureSpace) - } - } - }, - { lookup: { space: core.class.Space } } - ) - }) - - await pp - }) - - it('lookup nested query remove doc', async () => { - const { liveQuery, factory } = await getClient() - const futureSpace = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'new space', - description: '', - archived: false, - private: false, - members: [] - }) - const comment = await factory.addCollection( - test.class.TestComment, - futureSpace, - futureSpace, - core.class.Space, - 'comments', - { - message: 'test' - } - ) - const childComment = await factory.addCollection( - test.class.TestComment, - futureSpace, - comment, - test.class.TestComment, - 'comments', - { - message: 'child' - } - ) - const pp = new Promise((resolve) => { - liveQuery.query( - test.class.TestComment, - { _id: childComment }, - (result) => { - const comment = result[0] - if (comment !== undefined) { - expect((comment.$lookup?.attachedTo as WithLookup)?.$lookup?.space).toBeUndefined() - resolve(null) - } - }, - { lookup: { attachedTo: [test.class.TestComment, { space: core.class.Space }] } } - ) - }) - - await factory.removeDoc(core.class.Space, core.space.Model, futureSpace) - - await pp - }) - - it('lookup reverse query remove doc', async () => { - const { liveQuery, factory } = await getClient() - const spaces = await liveQuery.findAll(core.class.Space, {}) - const comments = await liveQuery.findAll(test.class.TestComment, {}) - expect(comments).toHaveLength(0) - const parentComment = await factory.addCollection( - test.class.TestComment, - spaces[0]._id, - spaces[0]._id, - spaces[0]._class, - 'comments', - { - message: 'test' - } - ) - let attempt = -1 - const childLength = 3 - const childs: Ref[] = [] - for (let index = 0; index < childLength; index++) { - childs.push( - await factory.addCollection( - test.class.TestComment, - spaces[0]._id, - parentComment, - test.class.TestComment, - 'comments', - { - message: index.toString() - } - ) - ) - } - - let secondPromise: Promise | undefined - const firstCallback = new Promise((resolve) => { - secondPromise = new Promise((_resolve) => { - liveQuery.query( - test.class.TestComment, - { _id: parentComment }, - (result) => { - attempt++ - if (attempt === 0) { - resolve() - } - const comment = result[0] - if (comment !== undefined) { - expect((comment.$lookup as any)?.comments).toHaveLength(childLength - attempt) - } - if (attempt === childLength) { - _resolve() - } - }, - { lookup: { _id: { comments: test.class.TestComment } } } - ) - }) - }) - - await firstCallback - - for (const child of childs) { - await factory.removeCollection( - test.class.TestComment, - spaces[0]._id, - child, - parentComment, - test.class.TestComment, - 'comments' - ) - } - await secondPromise - }) - - it('lookup query update doc', async () => { - const { liveQuery, factory } = await getClient() - let attempt = 0 - const futureSpace = await factory.createDoc(core.class.Space, core.space.Model, { - name: '0', - description: '', - archived: false, - private: false, - members: [] - }) - - const comment = await factory.addCollection( - test.class.TestComment, - futureSpace, - futureSpace, - core.class.Space, - 'comments', - { - message: 'test' - } - ) - const pp = new Promise((resolve) => { - liveQuery.query( - test.class.TestComment, - { _id: comment }, - (result) => { - const comment = result[0] - if (comment !== undefined) { - expect((comment.$lookup?.space as Space).name).toEqual(attempt.toString()) - } - if (attempt > 0) { - resolve(null) - } else { - attempt++ - } - }, - { lookup: { space: core.class.Space } } - ) - }) - - await new Promise((resolve) => { - setTimeout(resolve, 1) - }) - - await factory.updateDoc(core.class.Space, core.space.Model, futureSpace, { - name: '1' - }) - await pp - }) - - it('lookup nested query update doc', async () => { - const { liveQuery, factory } = await getClient() - let attempt = -1 - const futureSpace = await factory.createDoc(core.class.Space, core.space.Model, { - name: '0', - description: '', - archived: false, - private: false, - members: [] - }) - const comment = await factory.addCollection( - test.class.TestComment, - futureSpace, - futureSpace, - core.class.Space, - 'comments', - { - message: 'test' - } - ) - const childComment = await factory.addCollection( - test.class.TestComment, - futureSpace, - comment, - test.class.TestComment, - 'comments', - { - message: 'child' - } - ) - const pp = new Promise((resolve) => { - liveQuery.query( - test.class.TestComment, - { _id: childComment }, - (result) => { - attempt++ - const comment = result[0] - if (comment !== undefined) { - expect( - ((comment.$lookup?.attachedTo as WithLookup)?.$lookup?.space as Space).name - ).toEqual(attempt.toString()) - } - if (attempt > 0) { - resolve(null) - } - }, - { lookup: { attachedTo: [test.class.TestComment, { space: core.class.Space }] } } - ) - }) - - await factory.updateDoc(core.class.Space, core.space.Model, futureSpace, { - name: '1' - }) - await pp - }) - - it('lookup reverse query update doc', async () => { - const { liveQuery, factory } = await getClient() - const spaces = await liveQuery.findAll(core.class.Space, {}) - const parentComment = await factory.addCollection( - test.class.TestComment, - spaces[0]._id, - spaces[0]._id, - spaces[0]._class, - 'comments', - { - message: 'test' - } - ) - let attempt = -1 - const childComment = await factory.addCollection( - test.class.TestComment, - spaces[0]._id, - parentComment, - test.class.TestComment, - 'comments', - { - message: '0' - } - ) - const pp = new Promise((resolve) => { - liveQuery.query( - test.class.TestComment, - { _id: parentComment }, - (result) => { - attempt++ - const comment = result[0] - if (comment !== undefined) { - expect(((comment.$lookup as any)?.comments[0] as AttachedComment).message).toEqual(attempt.toString()) - } - if (attempt > 0) { - resolve(null) - } - }, - { lookup: { _id: { comments: test.class.TestComment } } } - ) - }) - - await factory.updateCollection( - test.class.TestComment, - spaces[0]._id, - childComment, - parentComment, - test.class.TestComment, - 'comments', - { - message: '1' - } - ) - await pp - }) - - // it('update with over limit', async () => { - // const { liveQuery, factory } = await getClient() - - // const spaces = await liveQuery.findAll(core.class.Space, {}) - // let attempt = 0 - // const pp = new Promise((resolve) => { - // liveQuery.query( - // core.class.Space, - // {}, - // (result) => { - // expect(result[0].name).toEqual(`Sp${++attempt}`) - // if (attempt === spaces.length + 1) resolve(null) - // }, - // { sort: { name: SortingOrder.Ascending }, limit: 1 } - // ) - // }) - - // for (let index = 0; index < spaces.length; index++) { - // const space = spaces[index] - // await factory.updateDoc(space._class, space.space, space._id, { - // name: `Sp${index + spaces.length + 1}` - // }) - // } - // await pp - // }) - - it('update-array-value', async () => { - const { liveQuery, factory } = await getClient() - - const spaces = await liveQuery.findAll(core.class.Space, {}) - await factory.createDoc(test.class.ParticipantsHolder, spaces[0]._id, { - participants: ['a' as Ref] - }) - const a2 = await factory.createDoc(test.class.ParticipantsHolder, spaces[0]._id, { - participants: ['b' as Ref] - }) - - const holderBefore = await liveQuery.findAll(test.class.ParticipantsHolder, { participants: 'a' as Ref }) - expect(holderBefore.length).toEqual(1) - - let attempt = 0 - let resolvePpv: (value: Doc[] | PromiseLike) => void - - const resolveP = new Promise((resolve) => { - resolvePpv = resolve - }) - const pp = await new Promise((resolve) => { - liveQuery.query( - test.class.ParticipantsHolder, - { participants: 'a' as Ref }, - (result) => { - if (attempt > 0) { - resolvePpv(result) - } else { - resolve(null) - } - }, - { sort: { private: SortingOrder.Ascending } } - ) - }) - - await pp // We have first value returned - - attempt++ - await factory.updateDoc(test.class.ParticipantsHolder, spaces[0]._id, a2, { - $push: { - participants: 'a' as Ref - } - }) - const result = await resolveP - expect(result.length).toEqual(2) - }) - - it('check query mixin projection', async () => { - const { liveQuery, factory } = await getClient() - - let projects = await liveQuery.queryFind(test.mixin.TestProjectMixin, {}, { projection: { _id: 1 } }) - expect(projects.length).toEqual(0) - const project = await factory.createDoc(test.class.TestProject, core.space.Space, { - archived: false, - description: '', - members: [], - private: false, - prjName: 'test project', - name: 'qwe' - }) - - projects = await liveQuery.queryFind(test.mixin.TestProjectMixin, {}, { projection: { _id: 1 } }) - expect(projects.length).toEqual(0) - await factory.createMixin(project, test.class.TestProject, core.space.Space, test.mixin.TestProjectMixin, { - someField: 'qwe' - }) - // We need to process all events before we could do query again - await new Promise((resolve) => { - setTimeout(resolve, 100) - }) - projects = await liveQuery.queryFind(test.mixin.TestProjectMixin, {}, { projection: { _id: 1 } }) - expect(projects.length).toEqual(1) - }) - - jest.setTimeout(25000) - it('test clone ops', async () => { - const { liveQuery, factory } = await getClient() - - const counter = 1000 - const ctx = new MeasureMetricsContext('tool', {}) - let data: Space[] = [] - const pp = new Promise((resolve) => { - liveQuery.query( - test.class.TestProject, - { private: false }, - (result) => { - data = result - if (data.length % 1000 === 0) { - console.info(data.length) - } - if (data.length === counter) { - resolve(null) - } - }, - {} - ) - }) - - for (let i = 0; i < counter; i++) { - await ctx.with('create-doc', {}, () => - factory.createDoc(test.class.TestProject, core.space.Space, { - archived: false, - description: '', - members: [], - private: false, - prjName: 'test project', - name: 'qwe' - }) - ) - } - expect(data.length).toBe(counter) - await pp - }) -}) diff --git a/foundations/core/packages/query/src/__tests__/queue-bugs.test.ts b/foundations/core/packages/query/src/__tests__/queue-bugs.test.ts deleted file mode 100644 index d40cac51ba..0000000000 --- a/foundations/core/packages/query/src/__tests__/queue-bugs.test.ts +++ /dev/null @@ -1,432 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import core, { createClient, SortingOrder, Space, Tx, TxOperations } from '@hcengineering/core' -import { LiveQuery } from '..' -import { connect } from './connection' - -async function getClient (): Promise<{ liveQuery: LiveQuery, factory: TxOperations }> { - const storage = await createClient(connect) - const liveQuery = new LiveQuery(storage) - storage.notify = (...tx: Tx[]) => { - liveQuery.tx(...tx).catch((err) => { - console.log(err) - }) - } - return { liveQuery, factory: new TxOperations(storage, core.account.System) } -} - -describe('LiveQuery - Queue Management Bugs', () => { - describe('Queue and Queries Map Consistency', () => { - it('should properly synchronize queue and queries map when removing queries', async () => { - const { liveQuery } = await getClient() - - const unsubscribe1 = liveQuery.query(core.class.Space, { private: false }, (result) => { - // Callback 1 - }) - - const unsubscribe2 = liveQuery.query(core.class.Space, { private: false }, (result) => { - // Callback 2 - same query - }) - - // Both callbacks should share the same query - // Let's verify internal state - const queriesMap = (liveQuery as any).queries.get(core.class.Space) - expect(queriesMap?.size).toBe(1) - - // Unsubscribe first callback - unsubscribe1() - - // Query should still exist because second callback is still active - expect(queriesMap?.size).toBe(1) - - // Unsubscribe second callback - unsubscribe2() - - // Now the query should be moved to the queue (cached for reuse) - const queue = (liveQuery as any).queue - expect(queue.size).toBeGreaterThan(0) - }) - - it('should handle rapid subscribe and unsubscribe correctly', async () => { - const { liveQuery } = await getClient() - - const callbacks: Array<() => void> = [] - - // Rapidly subscribe 10 times to the same query - for (let i = 0; i < 10; i++) { - const unsubscribe = liveQuery.query(core.class.Space, { private: false }, (result) => { - // Callback - }) - callbacks.push(unsubscribe) - } - - const queriesMap = (liveQuery as any).queries.get(core.class.Space) - // Should only have 1 query since they're all the same - expect(queriesMap?.size).toBe(1) - - // Unsubscribe all - callbacks.forEach((unsub) => { - unsub() - }) - - // Query should be in the queue - const queue = (liveQuery as any).queue - expect(queue.size).toBeGreaterThan(0) - }) - - it('should not leak memory when queries are unsubscribed', async () => { - const { liveQuery } = await getClient() - - const unsubscribeCallbacks: Array<() => void> = [] - - // Create many different queries - for (let i = 0; i < 50; i++) { - const unsubscribe = liveQuery.query(core.class.Space, { name: `query-${i}` }, (result) => { - // Callback - }) - unsubscribeCallbacks.push(unsubscribe) - } - - // All queries should be in the queries map - const queriesMap = (liveQuery as any).queries.get(core.class.Space) - expect(queriesMap?.size).toBe(50) - - // Unsubscribe all - unsubscribeCallbacks.forEach((unsub) => { - unsub() - }) - - // All queries should now be in the queue - const queue = (liveQuery as any).queue - expect(queue.size).toBe(50) - - // Wait a bit for any async operations - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Still should have 50 queries in queue - expect(queue.size).toBe(50) - }) - - it('should properly handle queue cleanup when exceeding CACHE_SIZE', async () => { - const { liveQuery } = await getClient() - - const CACHE_SIZE = 125 // From the code - const unsubscribeCallbacks: Array<() => void> = [] - - // Create more queries than CACHE_SIZE - for (let i = 0; i < CACHE_SIZE + 20; i++) { - const unsubscribe = liveQuery.query(core.class.Space, { name: `query-${i}` }, (result) => { - // Callback - }) - unsubscribeCallbacks.push(unsubscribe) - } - - const queriesMapBefore = (liveQuery as any).queries.get(core.class.Space) - expect(queriesMapBefore?.size).toBe(CACHE_SIZE + 20) - - // Unsubscribe all to move them to queue - unsubscribeCallbacks.forEach((unsub) => { - unsub() - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - const queue = (liveQuery as any).queue - // Queue should not exceed CACHE_SIZE due to cleanup - expect(queue.size).toBeLessThanOrEqual(CACHE_SIZE) - - const queriesMapAfter = (liveQuery as any).queries.get(core.class.Space) - // Some queries should have been removed from the queries map too - expect(queriesMapAfter?.size).toBeLessThan(CACHE_SIZE + 20) - }) - }) - - describe('Query Callback Management', () => { - it('should handle multiple callbacks on the same query correctly', async () => { - const { liveQuery, factory } = await getClient() - - const results1: any[] = [] - const results2: any[] = [] - - const unsubscribe1 = liveQuery.query(core.class.Space, { private: false }, (result) => { - results1.push(result.length) - }) - - const unsubscribe2 = liveQuery.query(core.class.Space, { private: false }, (result) => { - results2.push(result.length) - }) - - // Wait for initial callbacks - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Both should have received the same initial data - expect(results1.length).toBeGreaterThan(0) - expect(results2.length).toBeGreaterThan(0) - expect(results1[0]).toBe(results2[0]) - - // Create a new document - await factory.createDoc(core.class.Space, core.space.Model, { - private: false, - name: 'Test Space', - description: '', - members: [], - archived: false - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Both callbacks should have been called again - expect(results1.length).toBeGreaterThan(1) - expect(results2.length).toBeGreaterThan(1) - - unsubscribe1() - unsubscribe2() - }) - - it('should stop sending updates after unsubscribe', async () => { - const { liveQuery, factory } = await getClient() - - const results: any[] = [] - - const unsubscribe = liveQuery.query(core.class.Space, { private: false }, (result) => { - results.push(result.length) - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - const countBeforeUnsubscribe = results.length - - unsubscribe() - - // Create new documents after unsubscribe - for (let i = 0; i < 5; i++) { - await factory.createDoc(core.class.Space, core.space.Model, { - private: false, - name: `Space ${i}`, - description: '', - members: [], - archived: false - }) - } - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Should not have received new callbacks - expect(results.length).toBe(countBeforeUnsubscribe) - }) - }) - - describe('Query Reuse from Queue', () => { - it('should reuse cached query from queue when re-subscribing', async () => { - const { liveQuery } = await getClient() - - const unsubscribe1 = liveQuery.query(core.class.Space, { private: false }, (result) => { - // First callback - }) - - await new Promise((resolve) => setTimeout(resolve, 50)) - - const queriesMap = (liveQuery as any).queries.get(core.class.Space) - const firstQuery: any = Array.from(queriesMap.values())[0] - const initialQueryId = firstQuery?.id - - unsubscribe1() - - // Query should be in queue now - const queue = (liveQuery as any).queue - expect(queue.size).toBeGreaterThan(0) - - // Subscribe again to the same query - const unsubscribe2 = liveQuery.query(core.class.Space, { private: false }, (result) => { - // Second callback - }) - - await new Promise((resolve) => setTimeout(resolve, 50)) - - const queriesMapAfter = (liveQuery as any).queries.get(core.class.Space) - const secondQuery: any = Array.from(queriesMapAfter.values())[0] - const reusedQueryId = secondQuery?.id - - // Should reuse the same query - expect(reusedQueryId).toBe(initialQueryId) - - unsubscribe2() - }) - - it('should handle query options comparison correctly', async () => { - const { liveQuery } = await getClient() - - const unsubscribe1 = liveQuery.query(core.class.Space, { private: false }, (result) => {}, { - limit: 10, - sort: { name: SortingOrder.Ascending } - }) - - const unsubscribe2 = liveQuery.query(core.class.Space, { private: false }, (result) => {}, { - limit: 10, - sort: { name: SortingOrder.Ascending } - }) - - const queriesMap = (liveQuery as any).queries.get(core.class.Space) - // Should share the same query because options match - expect(queriesMap?.size).toBe(1) - - unsubscribe1() - unsubscribe2() - - // Different options should create different query - const unsubscribe3 = liveQuery.query(core.class.Space, { private: false }, (result) => {}, { - limit: 20, - sort: { name: SortingOrder.Descending } - }) - - await new Promise((resolve) => setTimeout(resolve, 50)) - - // Now should have 2 queries (1 in queue, 1 active) - const totalQueries = queriesMap?.size ?? 0 + ((liveQuery as any).queue.size ?? 0) - expect(totalQueries).toBeGreaterThan(1) - - unsubscribe3() - }) - }) - - describe('Edge Cases and Error Conditions', () => { - it('should handle empty query results', async () => { - const { liveQuery } = await getClient() - - const results: any[] = [] - - const unsubscribe = liveQuery.query(core.class.Space, { name: 'NonExistentSpace123456' }, (result) => { - results.push(result) - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(results.length).toBeGreaterThan(0) - expect(results[0]).toHaveLength(0) - - unsubscribe() - }) - - it('should handle query on closed LiveQuery', async () => { - const { liveQuery } = await getClient() - - await liveQuery.close() - - // Attempting to query after close should not crash - expect(() => { - liveQuery.query(core.class.Space, {}, (result) => {}) - }).not.toThrow() - }) - - it('should handle concurrent findAll and query operations', async () => { - const { liveQuery } = await getClient() - - const promises = [] - - // Start multiple findAll operations - for (let i = 0; i < 10; i++) { - promises.push(liveQuery.findAll(core.class.Space, { private: false })) - } - - // Start multiple query operations - for (let i = 0; i < 10; i++) { - liveQuery.query(core.class.Space, { private: false }, (result) => {}) - } - - const results = await Promise.all(promises) - - results.forEach((result) => { - expect(Array.isArray(result)).toBe(true) - }) - }) - }) - - describe('Query Counter and ID Management', () => { - it('should generate unique query IDs', async () => { - const { liveQuery } = await getClient() - - const queryIds = new Set() - const unsubscribes: Array<() => void> = [] - - // Create many queries - for (let i = 0; i < 100; i++) { - const unsubscribe = liveQuery.query(core.class.Space, { name: `unique-query-${i}` }, (result) => {}) - unsubscribes.push(unsubscribe) - } - - const queriesMap = (liveQuery as any).queries.get(core.class.Space) - if (queriesMap !== undefined) { - for (const query of queriesMap.values()) { - queryIds.add(query.id) - } - } - - // All IDs should be unique - expect(queryIds.size).toBe(100) - - unsubscribes.forEach((unsub) => { - unsub() - }) - }) - }) - - describe('Query Result Consistency', () => { - it('should maintain result consistency across multiple callbacks', async () => { - const { liveQuery, factory } = await getClient() - - const callback1Results: number[] = [] - const callback2Results: number[] = [] - - const unsubscribe1 = liveQuery.query(core.class.Space, { private: false }, (result) => { - callback1Results.push(result.length) - }) - - // Wait for first callback to complete - await new Promise((resolve) => setTimeout(resolve, 100)) - - const unsubscribe2 = liveQuery.query(core.class.Space, { private: false }, (result) => { - callback2Results.push(result.length) - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Create a document - await factory.createDoc(core.class.Space, core.space.Model, { - private: false, - name: 'Consistency Test', - description: '', - members: [], - archived: false - }) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - // Both callbacks should have received updates after document creation - // Note: Due to setTimeout(0) in pushCallback, timing may vary slightly - // but both should see at least 2 updates (initial + after create) - expect(callback1Results.length).toBeGreaterThanOrEqual(2) - expect(callback2Results.length).toBeGreaterThanOrEqual(2) - - // Final counts should match after everything settles - const final1 = callback1Results[callback1Results.length - 1] - const final2 = callback2Results[callback2Results.length - 1] - expect(final1).toBe(final2) - - unsubscribe1() - unsubscribe2() - }) - }) -}) diff --git a/foundations/core/packages/query/src/__tests__/refs-coverage.test.ts b/foundations/core/packages/query/src/__tests__/refs-coverage.test.ts deleted file mode 100644 index fbf27df928..0000000000 --- a/foundations/core/packages/query/src/__tests__/refs-coverage.test.ts +++ /dev/null @@ -1,298 +0,0 @@ -// Tests for Refs class to improve coverage -// -// Copyright © 2024 Hardcore Engineering Inc. -// - -import core, { createClient, TxOperations } from '@hcengineering/core' -import { LiveQuery } from '..' -import { connect } from './connection' - -async function getClient (): Promise<{ liveQuery: LiveQuery, factory: TxOperations, close: () => Promise }> { - const storage = await createClient(connect) - const liveQuery = new LiveQuery(storage) - storage.notify = (...tx) => { - void liveQuery.tx(...tx) - } - return { - liveQuery, - factory: new TxOperations(storage, core.account.System), - close: async () => { - await liveQuery.close() - } - } -} - -describe('Refs Class Coverage Tests', () => { - it('should find document from refs cache with specific _id', async () => { - const { liveQuery, factory, close } = await getClient() - - // Create a document - const space1 = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'cached-space-1', - description: 'test', - private: false, - members: [], - archived: false - }) - - // Query it to cache it - const callback = jest.fn() - liveQuery.query(core.class.Space, { _id: space1 }, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Now findOne should use cached version - const result = await liveQuery.findOne(core.class.Space, { _id: space1 }) - - expect(result).toBeDefined() - expect(result?._id).toBe(space1) - - await close() - }) - - it('should handle findOne with limit=1 without sort', async () => { - const { liveQuery, factory, close } = await getClient() - - // Create multiple documents - await factory.createDoc(core.class.Space, core.space.Model, { - name: 'test-1', - description: 'test', - private: false, - members: [], - archived: false - }) - - await factory.createDoc(core.class.Space, core.space.Model, { - name: 'test-2', - description: 'test', - private: false, - members: [], - archived: false - }) - - // findOne with limit should use refs optimization - const result = await liveQuery.findOne(core.class.Space, { private: false }, { limit: 1 }) - - expect(result).toBeDefined() - - await close() - }) - - it('should handle findOne with associations and lookup', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'lookup-test', - description: 'test', - private: false, - members: [], - archived: false - }) - - // Query with lookup to populate refs cache - const callback = jest.fn() - liveQuery.query(core.class.Space, { _id: space }, callback, { lookup: { _id: { docs: core.class.Doc } } }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // FindOne should use refs cache - const result = await liveQuery.findOne( - core.class.Space, - { _id: space }, - { lookup: { _id: { docs: core.class.Doc } } } - ) - - expect(result).toBeDefined() - - await close() - }) - - it('should strip $lookup and $associations when finding from cache without them', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'strip-test', - description: 'test', - private: false, - members: [], - archived: false - }) - - // Query with lookup to cache with $lookup - const callback = jest.fn() - liveQuery.query(core.class.Space, { _id: space }, callback, { lookup: { _id: { docs: core.class.Doc } } }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // FindOne without lookup should strip $lookup from cached doc - const result = await liveQuery.findOne(core.class.Space, { _id: space }) - - expect(result).toBeDefined() - expect((result as any).$lookup).toBeUndefined() - - await close() - }) - - it('should handle mixin class in findOne', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'mixin-findone', - description: 'test', - private: false, - members: [], - archived: false - }) - - // Query to cache - const callback = jest.fn() - liveQuery.query(core.class.Space, { _id: space }, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Find with different class to test mixin path - const result = await liveQuery.findOne(core.class.Space, { _id: space }) - - expect(result).toBeDefined() - - await close() - }) - - it('should handle query with lookup and associations together', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'both-options', - description: 'test', - private: false, - members: [], - archived: false - }) - - const callback = jest.fn() - - liveQuery.query(core.class.Space, { _id: space }, callback, { - lookup: { _id: { docs: core.class.Doc } }, - associations: [] - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - - await close() - }) - - it('should handle updating refs cache on document changes', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'update-refs', - description: 'original', - private: false, - members: [], - archived: false - }) - - // Cache it - const callback = jest.fn() - liveQuery.query(core.class.Space, { _id: space }, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Update it - await factory.updateDoc(core.class.Space, core.space.Model, space, { - description: 'updated' - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Find should have updated version - const result = await liveQuery.findOne(core.class.Space, { _id: space }) - - expect(result).toBeDefined() - expect(result?.description).toBe('updated') - - await close() - }) - - it('should clean refs cache when query is removed', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'clean-refs', - description: 'test', - private: false, - members: [], - archived: false - }) - - // Cache it - const callback = jest.fn() - const unsubscribe = liveQuery.query(core.class.Space, { _id: space }, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Unsubscribe to clean refs - unsubscribe() - - await new Promise((resolve) => setTimeout(resolve, 100)) - - await close() - }) - - it('should handle multiple queries referencing same document', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'multi-ref', - description: 'test', - private: false, - members: [], - archived: false - }) - - // Multiple queries on same document - const callback1 = jest.fn() - const callback2 = jest.fn() - const callback3 = jest.fn() - - liveQuery.query(core.class.Space, { _id: space }, callback1) - liveQuery.query(core.class.Space, { _id: space }, callback2) - liveQuery.query(core.class.Space, { _id: space }, callback3) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback1).toHaveBeenCalled() - expect(callback2).toHaveBeenCalled() - expect(callback3).toHaveBeenCalled() - - await close() - }) - - it('should use refs cache for descendants check', async () => { - const { liveQuery, factory, close } = await getClient() - - // Create documents - const space1 = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'desc-test-1', - description: 'test', - private: false, - members: [], - archived: false - }) - - // Cache with callback - const callback = jest.fn() - liveQuery.query(core.class.Space, {}, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // FindOne by _id should check descendants - const result = await liveQuery.findOne(core.class.Space, { _id: space1 }) - - expect(result).toBeDefined() - - await close() - }) -}) diff --git a/foundations/core/packages/query/src/__tests__/remaining-edge-cases.test.ts b/foundations/core/packages/query/src/__tests__/remaining-edge-cases.test.ts deleted file mode 100644 index d140cc3039..0000000000 --- a/foundations/core/packages/query/src/__tests__/remaining-edge-cases.test.ts +++ /dev/null @@ -1,579 +0,0 @@ -// Additional edge case tests for remaining uncovered code -// -// Copyright © 2024 Hardcore Engineering Inc. -// - -import core, { createClient, TxOperations } from '@hcengineering/core' -import { LiveQuery } from '..' -import { connect } from './connection' -import { test } from './minmodel' - -async function getClient (): Promise<{ liveQuery: LiveQuery, factory: TxOperations, close: () => Promise }> { - const storage = await createClient(connect) - const liveQuery = new LiveQuery(storage) - storage.notify = (...tx) => { - void liveQuery.tx(...tx) - } - return { - liveQuery, - factory: new TxOperations(storage, core.account.System), - close: async () => { - await liveQuery.close() - } - } -} - -describe('Edge Cases for Uncovered Code', () => { - describe('Reverse lookup update scenarios', () => { - it('should handle updates to documents in reverse lookup arrays', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'reverse-update-test', - description: 'test', - private: false, - members: [], - archived: false - }) - - const parentComment = await factory.addCollection( - test.class.TestComment, - space, - space, - core.class.Space, - 'comments', - { - message: 'parent' - } - ) - - const childComment = await factory.addCollection( - test.class.TestComment, - space, - parentComment, - test.class.TestComment, - 'comments', - { - message: 'child original' - } - ) - - const callback = jest.fn() - - // Query with reverse lookup - liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, { - lookup: { - _id: { comments: test.class.TestComment } - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - const initialCalls = callback.mock.calls.length - - // Update the child comment - await factory.updateCollection( - test.class.TestComment, - space, - childComment, - parentComment, - test.class.TestComment, - 'comments', - { - message: 'child updated' - } - ) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - expect(callback.mock.calls.length).toBeGreaterThan(initialCalls) - - // Verify lookup data was updated - const lastResult = callback.mock.calls[callback.mock.calls.length - 1][0][0] - expect(lastResult.$lookup).toBeDefined() - - await close() - }) - - it('should handle mixin updates to documents in reverse lookup', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'mixin-reverse-lookup', - description: 'test', - private: false, - members: [], - archived: false - }) - - const parentComment = await factory.addCollection( - test.class.TestComment, - space, - space, - core.class.Space, - 'comments', - { - message: 'parent' - } - ) - - const childComment = await factory.addCollection( - test.class.TestComment, - space, - parentComment, - test.class.TestComment, - 'comments', - { - message: 'child' - } - ) - - const callback = jest.fn() - - liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, { - lookup: { - _id: { comments: test.class.TestComment } - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - const initialCalls = callback.mock.calls.length - - // Create mixin on child - await factory.createMixin(childComment, test.class.TestComment, space, test.mixin.TestMixin, {}) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - expect(callback.mock.calls.length).toBeGreaterThanOrEqual(initialCalls) - - await close() - }) - }) - - describe('Regular lookup update scenarios', () => { - it('should update $lookup when linked document is updated via TxUpdateDoc', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'lookup-update-test', - description: 'original', - private: false, - members: [], - archived: false - }) - - const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { - message: 'test' - }) - - const callback = jest.fn() - - // Query with lookup to space - liveQuery.query(test.class.TestComment, { _id: comment }, callback, { - lookup: { - space: core.class.Space - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - const initialCalls = callback.mock.calls.length - - // Update the space (lookup target) - await factory.updateDoc(core.class.Space, core.space.Model, space, { - description: 'updated' - }) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - expect(callback.mock.calls.length).toBeGreaterThan(initialCalls) - - // Verify lookup was updated - const lastResult = callback.mock.calls[callback.mock.calls.length - 1][0][0] - expect(lastResult.$lookup?.space).toBeDefined() - - await close() - }) - - it('should update $lookup when linked document receives mixin via TxMixin', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'mixin-lookup-test', - description: 'test', - private: false, - members: [], - archived: false - }) - - const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { - message: 'test' - }) - - const callback = jest.fn() - - liveQuery.query(test.class.TestComment, { _id: comment }, callback, { - lookup: { - space: core.class.Space - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - const initialCalls = callback.mock.calls.length - - // Add mixin to space - await factory.createMixin(space, core.class.Space, core.space.Model, test.mixin.TestMixin, {}) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - expect(callback.mock.calls.length).toBeGreaterThanOrEqual(initialCalls) - - await close() - }) - }) - - describe('Array-based reverse lookup edge cases', () => { - it('should handle reverse lookup with array value containing undefined document', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'undefined-in-array', - description: 'test', - private: false, - members: [], - archived: false - }) - - const parentComment = await factory.addCollection( - test.class.TestComment, - space, - space, - core.class.Space, - 'comments', - { - message: 'parent' - } - ) - - // Create child, get its ID, then we'll query before it's properly looked up - const childComment = await factory.addCollection( - test.class.TestComment, - space, - parentComment, - test.class.TestComment, - 'comments', - { - message: 'child' - } - ) - - const callback = jest.fn() - - // Query with reverse lookup - liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, { - lookup: { - _id: { comments: test.class.TestComment } - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Try to update a non-existent child (index will be -1) - await factory.updateCollection( - test.class.TestComment, - space, - childComment, - parentComment, - test.class.TestComment, - 'comments', - { - message: 'updated' - } - ) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - expect(callback).toHaveBeenCalled() - - await close() - }) - - it('should handle updates when reverse lookup value is not yet in array', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'not-in-array', - description: 'test', - private: false, - members: [], - archived: false - }) - - const parentComment = await factory.addCollection( - test.class.TestComment, - space, - space, - core.class.Space, - 'comments', - { - message: 'parent' - } - ) - - const callback = jest.fn() - - liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, { - lookup: { - _id: { comments: test.class.TestComment } - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Create a child which should trigger adding to lookup array - const childComment = await factory.addCollection( - test.class.TestComment, - space, - parentComment, - test.class.TestComment, - 'comments', - { - message: 'new child' - } - ) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Update it - should find it in array now - await factory.updateCollection( - test.class.TestComment, - space, - childComment, - parentComment, - test.class.TestComment, - 'comments', - { - message: 'child updated' - } - ) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - expect(callback).toHaveBeenCalled() - - await close() - }) - }) - - describe('Nested lookup scenarios', () => { - it('should handle nested lookup where parent document exists', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'nested-exists', - description: 'test', - private: false, - members: [], - archived: false - }) - - const parentComment = await factory.addCollection( - test.class.TestComment, - space, - space, - core.class.Space, - 'comments', - { - message: 'parent' - } - ) - - const childComment = await factory.addCollection( - test.class.TestComment, - space, - parentComment, - test.class.TestComment, - 'comments', - { - message: 'child' - } - ) - - const callback = jest.fn() - - // Nested lookup: child -> attachedTo (parent) -> space - liveQuery.query(test.class.TestComment, { _id: childComment }, callback, { - lookup: { - attachedTo: [test.class.TestComment, { space: core.class.Space }] - } - }) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - expect(callback).toHaveBeenCalled() - - const result = callback.mock.calls[callback.mock.calls.length - 1][0][0] - expect(result.$lookup?.attachedTo).toBeDefined() - - await close() - }) - - it('should handle nested lookup where parent document is undefined', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'nested-undefined', - description: 'test', - private: false, - members: [], - archived: false - }) - - // Create orphan comment with no valid parent - const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { - message: 'orphan' - }) - - const callback = jest.fn() - - // Try nested lookup where parent might not exist - liveQuery.query(test.class.TestComment, { _id: comment }, callback, { - lookup: { - attachedTo: [core.class.Space, {}] // Will find space, nested is empty - } - }) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - expect(callback).toHaveBeenCalled() - - await close() - }) - }) - - describe('Reverse lookup with custom attribute', () => { - it('should handle reverse lookup with custom attribute name (array format)', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'custom-attr', - description: 'test', - private: false, - members: [], - archived: false - }) - - const parentComment = await factory.addCollection( - test.class.TestComment, - space, - space, - core.class.Space, - 'comments', - { - message: 'parent' - } - ) - - const callback = jest.fn() - - // Reverse lookup with explicit attribute - liveQuery.query(test.class.TestComment, { _id: parentComment }, callback, { - lookup: { - _id: { - children: [test.class.TestComment, 'attachedTo'] - } - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Add child - await factory.addCollection(test.class.TestComment, space, parentComment, test.class.TestComment, 'comments', { - message: 'child with custom attr' - }) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - expect(callback).toHaveBeenCalled() - - const result = callback.mock.calls[callback.mock.calls.length - 1][0][0] - expect(result.$lookup).toBeDefined() - - await close() - }) - }) - - describe('Lookup with mixin key checks', () => { - it('should handle lookup keys that might be mixin properties', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'mixin-key', - description: 'test', - private: false, - members: [], - archived: false - }) - - const comment = await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { - message: 'test' - }) - - const callback = jest.fn() - - // Lookup with keys that will be checked via checkMixinKey - liveQuery.query(test.class.TestComment, { _id: comment }, callback, { - lookup: { - space: core.class.Space, - attachedTo: core.class.Space - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - - const result = callback.mock.calls[callback.mock.calls.length - 1][0][0] - expect(result.$lookup?.space).toBeDefined() - - await close() - }) - }) - - describe('Reverse lookup with 0 or undefined values', () => { - it('should skip reverse lookup when field value is 0', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'zero-value', - description: 'test', - private: false, - members: [], - archived: false - }) - - const callback = jest.fn() - - // Query with reverse lookup on a field that might be 0 or undefined - liveQuery.query(core.class.Space, { _id: space }, callback, { - lookup: { - _id: { - nonExistentField: test.class.TestComment - } - } - }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(callback).toHaveBeenCalled() - - // Should not crash and result should be valid - const result = callback.mock.calls[callback.mock.calls.length - 1][0][0] - expect(result).toBeDefined() - - await close() - }) - }) -}) diff --git a/foundations/core/packages/query/src/__tests__/workspace-events.test.ts b/foundations/core/packages/query/src/__tests__/workspace-events.test.ts deleted file mode 100644 index 6a92c88844..0000000000 --- a/foundations/core/packages/query/src/__tests__/workspace-events.test.ts +++ /dev/null @@ -1,371 +0,0 @@ -// Workspace event handling tests -// -// Copyright © 2024 Hardcore Engineering Inc. -// - -import core, { - BulkUpdateEvent, - createClient, - IndexingUpdateEvent, - Ref, - TxOperations, - TxWorkspaceEvent, - WorkspaceEvent -} from '@hcengineering/core' -import { LiveQuery } from '..' -import { connect } from './connection' -import { test } from './minmodel' - -async function getClient (): Promise<{ liveQuery: LiveQuery, factory: TxOperations, close: () => Promise }> { - const storage = await createClient(connect) - const liveQuery = new LiveQuery(storage) - storage.notify = (...tx) => { - void liveQuery.tx(...tx) - } - return { - liveQuery, - factory: new TxOperations(storage, core.account.System), - close: async () => { - await liveQuery.close() - } - } -} - -describe('Workspace Events', () => { - describe('IndexingUpdate event with $search queries', () => { - it('should refresh queries with $search when IndexingUpdate event occurs', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'indexing-test', - description: 'test', - private: false, - members: [], - archived: false - }) - - await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { - message: 'searchable message' - }) - - const callback = jest.fn() - - // Query with $search - liveQuery.query(test.class.TestComment, { $search: 'searchable' }, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - const initialCalls = callback.mock.calls.length - - // Send IndexingUpdate event - const params: IndexingUpdateEvent = { - _class: [test.class.TestComment] - } - const indexingEvent: TxWorkspaceEvent = { - _id: 'indexing-event' as Ref, - _class: core.class.TxWorkspaceEvent, - space: core.space.DerivedTx, - modifiedOn: Date.now(), - modifiedBy: core.account.System, - objectSpace: space, - event: WorkspaceEvent.IndexingUpdate, - params - } - - await liveQuery.tx(indexingEvent) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - // Should have triggered a refresh - expect(callback.mock.calls.length).toBeGreaterThan(initialCalls) - - await close() - }) - - it('should handle IndexingUpdate for queued queries with $search', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'queue-indexing', - description: 'test', - private: false, - members: [], - archived: false - }) - - const callback = jest.fn() - - // Create a query with $search - const unsubscribe = liveQuery.query(test.class.TestComment, { $search: 'test' }, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Unsubscribe to move to queue - unsubscribe() - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Send IndexingUpdate event - const params: IndexingUpdateEvent = { - _class: [test.class.TestComment] - } - const indexingEvent: TxWorkspaceEvent = { - _id: 'indexing-queue-event' as Ref, - _class: core.class.TxWorkspaceEvent, - space: core.space.DerivedTx, - modifiedOn: Date.now(), - modifiedBy: core.account.System, - objectSpace: space, - event: WorkspaceEvent.IndexingUpdate, - params - } - - await liveQuery.tx(indexingEvent) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - // Query should have been handled (either removed or refreshed) - expect(callback).toHaveBeenCalled() - - await close() - }) - }) - - describe('BulkUpdate event', () => { - it('should refresh queries when BulkUpdate event occurs for matching class', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'bulk-update', - description: 'test', - private: false, - members: [], - archived: false - }) - - await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { - message: 'bulk test' - }) - - const callback = jest.fn() - - liveQuery.query(test.class.TestComment, {}, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - const initialCalls = callback.mock.calls.length - - // Send BulkUpdate event - const params: BulkUpdateEvent = { - _class: [test.class.TestComment] - } - const bulkEvent: TxWorkspaceEvent = { - _id: 'bulk-event' as Ref, - _class: core.class.TxWorkspaceEvent, - space: core.space.DerivedTx, - modifiedOn: Date.now(), - modifiedBy: core.account.System, - objectSpace: space, - event: WorkspaceEvent.BulkUpdate, - params - } - - await liveQuery.tx(bulkEvent) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - // Should have triggered a refresh - expect(callback.mock.calls.length).toBeGreaterThan(initialCalls) - - await close() - }) - - it('should handle BulkUpdate for queued queries', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'queue-bulk', - description: 'test', - private: false, - members: [], - archived: false - }) - - const callback = jest.fn() - - // Create and then unsubscribe to move to queue - const unsubscribe = liveQuery.query(test.class.TestComment, {}, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - unsubscribe() - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Send BulkUpdate event - const params: BulkUpdateEvent = { - _class: [test.class.TestComment] - } - const bulkEvent: TxWorkspaceEvent = { - _id: 'bulk-queue-event' as Ref, - _class: core.class.TxWorkspaceEvent, - space: core.space.DerivedTx, - modifiedOn: Date.now(), - modifiedBy: core.account.System, - objectSpace: space, - event: WorkspaceEvent.BulkUpdate, - params - } - - await liveQuery.tx(bulkEvent) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - // Query should have been handled - expect(callback).toHaveBeenCalled() - - await close() - }) - }) - - describe('SecurityChange event', () => { - it('should refresh queries when SecurityChange event occurs for matching space', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'security-test', - description: 'test', - private: false, - members: [], - archived: false - }) - - await factory.addCollection(test.class.TestComment, space, space, core.class.Space, 'comments', { - message: 'security test' - }) - - const callback = jest.fn() - - liveQuery.query(test.class.TestComment, { space }, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - const initialCalls = callback.mock.calls.length - - // Send SecurityChange event - const securityEvent: TxWorkspaceEvent = { - _id: 'security-event' as Ref, - _class: core.class.TxWorkspaceEvent, - space: core.space.DerivedTx, - modifiedOn: Date.now(), - modifiedBy: core.account.System, - objectSpace: space, - event: WorkspaceEvent.SecurityChange, - params: null - } - - await liveQuery.tx(securityEvent) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - // Should have triggered a refresh - expect(callback.mock.calls.length).toBeGreaterThan(initialCalls) - - await close() - }) - - it('should handle SecurityChange for queries with non-string space (e.g., $in)', async () => { - const { liveQuery, factory, close } = await getClient() - - const space1 = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'security-1', - description: 'test', - private: false, - members: [], - archived: false - }) - - const space2 = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'security-2', - description: 'test', - private: false, - members: [], - archived: false - }) - - const callback = jest.fn() - - // Query with $in for space (non-string) - liveQuery.query(test.class.TestComment, { space: { $in: [space1, space2] } }, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - const initialCalls = callback.mock.calls.length - - // Send SecurityChange event - const securityEvent: TxWorkspaceEvent = { - _id: 'security-nonstring-event' as Ref, - _class: core.class.TxWorkspaceEvent, - space: core.space.DerivedTx, - modifiedOn: Date.now(), - modifiedBy: core.account.System, - objectSpace: space1, - event: WorkspaceEvent.SecurityChange, - params: null - } - - await liveQuery.tx(securityEvent) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - // Should have triggered a refresh since space query is non-string - expect(callback.mock.calls.length).toBeGreaterThan(initialCalls) - - await close() - }) - - it('should handle SecurityChange for queued queries', async () => { - const { liveQuery, factory, close } = await getClient() - - const space = await factory.createDoc(core.class.Space, core.space.Model, { - name: 'security-queue', - description: 'test', - private: false, - members: [], - archived: false - }) - - const callback = jest.fn() - - // Create and unsubscribe to move to queue - const unsubscribe = liveQuery.query(test.class.TestComment, { space }, callback) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - unsubscribe() - - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Send SecurityChange event - const securityEvent: TxWorkspaceEvent = { - _id: 'security-queue-event' as Ref, - _class: core.class.TxWorkspaceEvent, - space: core.space.DerivedTx, - modifiedOn: Date.now(), - modifiedBy: core.account.System, - objectSpace: space, - event: WorkspaceEvent.SecurityChange, - params: null - } - - await liveQuery.tx(securityEvent) - - await new Promise((resolve) => setTimeout(resolve, 150)) - - // Query should have been handled - expect(callback).toHaveBeenCalled() - - await close() - }) - }) -}) diff --git a/foundations/core/packages/query/src/index.ts b/foundations/core/packages/query/src/index.ts deleted file mode 100644 index 1ebe6bb662..0000000000 --- a/foundations/core/packages/query/src/index.ts +++ /dev/null @@ -1,1610 +0,0 @@ -// -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { Analytics } from '@hcengineering/analytics' -import core, { - Association, - BulkUpdateEvent, - Class, - Client, - DOMAIN_MODEL, - Doc, - DocumentQuery, - FindOptions, - FindResult, - Hierarchy, - IndexingUpdateEvent, - Lookup, - LookupData, - Mixin, - ModelDb, - Ref, - Relation, - ReverseLookups, - SearchOptions, - SearchQuery, - SearchResult, - SortingQuery, - Space, - Tx, - TxCreateDoc, - TxMixin, - TxProcessor, - TxRemoveDoc, - TxResult, - TxUpdateDoc, - TxWorkspaceEvent, - WithLookup, - WithTx, - WorkspaceEvent, - checkMixinKey, - clone, - findProperty, - generateId, - getObjectValue, - matchQuery, - platformNow, - reduceCalls, - shouldShowArchived, - toFindResult, - type DomainParams, - type DomainRequestOptions, - type DomainResult, - type OperationDomain -} from '@hcengineering/core' -import { PlatformError } from '@hcengineering/platform' -import { deepEqual } from 'fast-equals' -import { Refs } from './refs' -import { ResultArray } from './results' -import { Callback, Query, type QueryId } from './types' - -const CACHE_SIZE = 125 - -/** - * @public - */ -export class LiveQuery implements WithTx, Client { - private readonly client: Client - private readonly queries = new Map>, Map>() - private readonly queue = new Map() - private queryCounter: number = 0 - private closed: boolean = false - - private readonly queriesToUpdate = new Map() - - private readonly refs = new Refs(() => this.getHierarchy()) - - constructor (client: Client) { - this.client = client - } - - public isClosed (): boolean { - return this.closed - } - - async close (): Promise { - this.closed = true - await this.client.close() - } - - getHierarchy (): Hierarchy { - return this.client.getHierarchy() - } - - getModel (): ModelDb { - return this.client.getModel() - } - - // Perform refresh of content since connection established. - async refreshConnect (clean: boolean): Promise { - for (const q of [...this.queue.values()]) { - if (!this.removeFromQueue(q)) { - try { - if (clean) { - this.cleanQuery(q) - } - // No need to refresh, since it will be on next for - } catch (err: any) { - if (err instanceof PlatformError) { - if (err.message === 'connection closed') { - continue - } - } - Analytics.handleError(err) - console.error(err) - } - } else { - // No callbacks, let's remove it on conenct - this.removeQueue(q) - } - } - for (const v of this.queries.values()) { - for (const q of v.values()) { - try { - if (clean) { - this.cleanQuery(q) - } - void this.refresh(q) - } catch (err: any) { - if (err instanceof PlatformError) { - if (err.message === 'connection closed') { - continue - } - } - Analytics.handleError(err) - console.error(err) - } - } - } - } - - private cleanQuery (q: Query): void { - q.callbacks.forEach((callback) => { - callback(toFindResult([], 0)) - }) - q.result = new ResultArray([], this.getHierarchy()) - q.total = -1 - } - - private match (q: Query, doc: Doc, skipLookup = false): boolean { - if (this.getHierarchy().isMixin(q._class)) { - if (this.getHierarchy().hasMixin(doc, q._class)) { - doc = this.getHierarchy().as(doc, q._class) - } else { - return false - } - } - if (!this.getHierarchy().isDerived(doc._class, q._class)) { - // Check if it is not a mixin and not match class - const mixinClass = Hierarchy.mixinClass(doc) - if (mixinClass === undefined || !this.getHierarchy().isDerived(mixinClass, q._class)) { - return false - } - } - const query = q.query - for (const key in query) { - if (key === '$search') continue - if (skipLookup && key.startsWith('$lookup')) continue - const value = (query as any)[key] - const result = findProperty([doc], key, value) - if (result.length === 0) { - return false - } - } - return true - } - - private createDumpQuery( - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): Query { - const q = this.createQuery(_class, query, undefined, options) - this.queue.set(q.id, { ...q, lastUsed: platformNow() }) - if (!(q.result instanceof Promise)) { - q.result.clean() - } - return q - } - - async findAll( - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): Promise> { - if (this.client.getHierarchy().getDomain(_class) === DOMAIN_MODEL) { - return await this.client.findAll(_class, query, options) - } - const opt = { ...(options ?? {}) } - if (opt.projection !== undefined) { - opt.projection = { - ...opt.projection, - _class: 1, - space: 1, - modifiedOn: 1 - } - } - - // Perform one document queries if applicable. - const d = this.refs.findFromDocs(_class, query, opt) - if (d !== null) { - return d - } - - const q = this.findQuery(_class, query, opt) ?? this.createDumpQuery(_class, query, opt) - if (q.result instanceof Promise) { - q.result = await q.result - } - if (this.removeFromQueue(q, false)) { - this.queue.set(q.id, { ...q, lastUsed: platformNow() }) - q.result.clean() - } - return toFindResult(q.result.getClone(), q.total) - } - - async domainRequest( - domain: OperationDomain, - params: DomainParams, - options?: DomainRequestOptions - ): Promise> { - return await this.client.domainRequest(domain, params, options) - } - - searchFulltext (query: SearchQuery, options: SearchOptions): Promise { - return this.client.searchFulltext(query, options) - } - - async findOne( - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): Promise | undefined> { - if (this.client.getHierarchy().getDomain(_class) === DOMAIN_MODEL) { - return await this.client.findOne(_class, query, options) - } - if (options?.projection !== undefined) { - options.projection = { - ...options.projection, - _class: 1, - space: 1, - modifiedOn: 1 - } - } - - if (options === undefined) { - options = {} - } - options.limit = 1 - - const d = this.refs.findFromDocs(_class, query, options) - if (d !== null) { - return d[0] - } - - const q = this.findQuery(_class, query, options) ?? this.createDumpQuery(_class, query, options) - if (q.result instanceof Promise) { - q.result = await q.result - } - if (this.removeFromQueue(q, false)) { - this.queue.set(q.id, { ...q, lastUsed: platformNow() }) - q.result.clean() - } - return q.result.getClone>().shift() - } - - private optionsCompare (opt1?: FindOptions, opt2?: FindOptions): boolean { - const { ctx: _1, ..._opt1 } = (opt1 ?? {}) as any - const { ctx: _2, ..._opt2 } = (opt2 ?? {}) as any - return deepEqual(_opt1, _opt2) - } - - private queryCompare (q1: DocumentQuery, q2: DocumentQuery): boolean { - if (Object.keys(q1).length !== Object.keys(q2).length) { - return false - } - return deepEqual(q1, q2) - } - - private findQuery( - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): Query | undefined { - const queries = this.getQueueMap(_class) - if (queries === undefined) return - - for (const q of queries.values()) { - if (!this.queryCompare(query, q.query) || !this.optionsCompare(options, q.options)) continue - return q - } - } - - private removeFromQueue (q: Query, update = true): boolean { - if (q.callbacks.size === 0) { - const removed = this.queue.delete(q.id) - if (removed) { - if (update) { - if (!(q.result instanceof Promise)) { - this.refs.updateDocuments(q, q.result.getDocs(), true) - } - } - return true - } - } - return false - } - - private pushCallback ( - q: Query, - callback: { - callback: (result: Doc[]) => void - callbackId: string - } - ): void { - q.callbacks.set(callback.callbackId, callback.callback) - setTimeout(async () => { - if (q !== undefined) { - if (q.result instanceof Promise) { - q.result = await q.result - } - callback.callback(toFindResult(q.result.getResult(callback.callbackId), q.total)) - } - }, 0) - } - - private getQuery( - _class: Ref>, - query: DocumentQuery, - callback: { - callback: (result: Doc[]) => void - callbackId: string - }, - options?: FindOptions - ): Query | undefined { - const current = this.findQuery(_class, query, options) - if (current !== undefined) { - this.removeFromQueue(current, false) - this.pushCallback(current, callback) - - return current - } - } - - private getQueueMap (_class: Ref>): Map { - let cq = this.queries.get(_class) - if (cq === undefined) { - cq = new Map() - this.queries.set(_class, cq) - } - return cq - } - - private createQuery( - _class: Ref>, - query: DocumentQuery, - callback: { callback: (result: FindResult) => void, callbackId: string } | undefined, - options?: FindOptions - ): Query { - const _query: DocumentQuery = clone(query) - const localResult = this.refs.findFromDocs(_class, query, options) - const result = localResult != null ? Promise.resolve(localResult) : this.client.findAll(_class, query, options) - const q: Query = { - id: ++this.queryCounter, - _class, - query: _query, - result: result.then((docs) => new ResultArray(docs, this.getHierarchy())), - total: 0, - options: options as FindOptions, - callbacks: new Map(), - refresh: reduceCalls(() => this.doRefresh(q)), - refreshId: 0 - } - if (callback !== undefined) { - q.callbacks.set(callback.callbackId, callback.callback as unknown as Callback) - } - this.getQueueMap(_class).set(q.id, q) - result - .then(async (result) => { - q.total = result.total - await this.callback(q) - }) - .catch((err: any) => { - Analytics.handleError(err) - console.log('failed to update Live Query: ', err) - }) - - if (this.queue.size > CACHE_SIZE) { - this.remove() - } - return q - } - - private remove (): void { - const used = Array.from(this.queue.values()).sort((a, b) => a.lastUsed - b.lastUsed) - // Remove enough queries to bring the queue back down to 80% of CACHE_SIZE - // This prevents constant cleanup cycles - const targetSize = Math.floor(CACHE_SIZE * 0.8) - const toRemove = Math.max(0, this.queue.size - targetSize) - for (let i = 0; i < toRemove; i++) { - const q = used.shift() - if (q === undefined) return - this.removeQueue(q) - } - } - - removeQueue (q: Query): void { - const queries = this.getQueueMap(q._class) - const removed = queries.delete(q.id) - this.queue.delete(q.id) - if (removed) { - if (!(q.result instanceof Promise)) { - this.refs.updateDocuments(q, q.result.getDocs(), true) - } - } - } - - query( - _class: Ref>, - query: DocumentQuery, - callback: (result: FindResult) => void, - options?: FindOptions - ): () => void { - if (options?.projection !== undefined) { - options.projection = { - ...options.projection, - _class: 1, - space: 1, - modifiedOn: 1 - } - } - const callbackId = generateId() - const q = - this.getQuery(_class, query, { callback: callback as (result: Doc[]) => void, callbackId }, options) ?? - this.createQuery(_class, query, { callback, callbackId }, options) - - return () => { - q.callbacks.delete(callbackId) - if (q.callbacks.size === 0) { - if (!(q.result instanceof Promise)) { - q.result.clean() - } - this.queue.set(q.id, { ...q, lastUsed: platformNow() }) - // Check if we need to clean up the queue after adding this query - if (this.queue.size > CACHE_SIZE) { - this.remove() - } - } - } - } - - async queryFind( - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): Promise> { - if (options?.projection !== undefined) { - options.projection = { - ...options.projection, - _class: 1, - space: 1, - modifiedOn: 1 - } - } - const current = this.findQuery(_class, query, options) - if (current === undefined) { - const q = this.createQuery( - _class, - query, - undefined, // No need of callback - options - ) - if (q.result instanceof Promise) { - q.result = await q.result - } - return toFindResult(q.result.getClone(), q.total) - } - if (current.result instanceof Promise) { - current.result = await current.result - } - return toFindResult(current.result.getClone(), current.total) - } - - private async checkSearch (q: Query, _id: Ref): Promise { - const match = await this.client.findOne(q._class, { $search: q.query.$search, _id }, q.options) - if (q.result instanceof Promise) { - q.result = await q.result - } - if (match === undefined) { - if (q.options?.limit === q.result.length) { - await this.refresh(q) - return true - } else { - const doc = q.result.delete(_id) - if (doc !== undefined) { - this.refs.updateDocuments(q, [doc], true) - if (q.options?.total === true) { - q.total-- - } - } - } - } else { - const doc = q.result.findDoc(_id) - if (doc !== undefined) { - q.result.updateDoc(match, false) - this.refs.updateDocuments(q, [match]) - } - } - return false - } - - private async getDocFromCache ( - docCache: Map, - _id: Ref, - _class: Ref>, - space: Ref, - q: Query - ): Promise { - const options: any = {} - if (q.options?.associations !== undefined) { - options.associations = q.options?.associations - } - if (q.options?.lookup !== undefined) { - options.lookup = q.options?.lookup - } - - const showArchived = shouldShowArchived(q.query, q.options) - - options.showArchived = showArchived - const docIdKey = _id + JSON.stringify(options ?? {}) + q._class - - const current = docCache.get(docIdKey) ?? (await this.client.findOne(q._class, { _id, space }, options)) - if (current !== undefined) { - docCache.set(docIdKey, current) - } else { - docCache.delete(docIdKey) - } - return current - } - - private asMixin (doc: Doc, mixin: Ref>): Doc { - if (this.getHierarchy().isMixin(mixin)) { - return this.getHierarchy().as(doc, mixin) - } - return doc - } - - private async getCurrentDoc ( - q: Query, - _id: Ref, - space: Ref, - docCache: Map - ): Promise { - let current = await this.getDocFromCache(docCache, _id, q._class, space, q) - if (q.result instanceof Promise) { - q.result = await q.result - } - - const pos = q.result.findDoc(_id) - if (current !== undefined) { - current = this.asMixin(current, q._class) - } - if (current !== undefined && this.match(q, current)) { - q.result.updateDoc(current, false) - this.refs.updateDocuments(q, [current]) - } else { - if (q.options?.limit === q.result.length) { - await this.refresh(q) - return true - } else if (pos !== undefined) { - q.result.delete(_id) - this.refs.updateDocuments(q, [pos], true) - if (q.options?.total === true) { - q.total-- - } - } - } - return false - } - - private async __updateMixinDoc (q: Query, updatedDoc: WithLookup, tx: TxMixin): Promise { - updatedDoc = TxProcessor.updateMixin4Doc(updatedDoc, tx) - - const ops = { - ...tx.attributes, - modifiedBy: tx.modifiedBy, - modifiedOn: tx.modifiedOn - } - await this.__updateLookup(q, updatedDoc, ops) - } - - private checkUpdatedDocMatch (q: Query, result: ResultArray, updatedDoc: WithLookup): boolean { - if (!this.match(q, updatedDoc)) { - if (q.options?.limit === result.length) { - void this.refresh(q) - return true - } else { - result.delete(updatedDoc._id) - this.refs.updateDocuments(q, [updatedDoc], true) - if (q.options?.total === true) { - q.total-- - } - } - } else { - result.updateDoc(updatedDoc, false) - this.refs.updateDocuments(q, [updatedDoc]) - } - return false - } - - protected async txMixin (tx: TxMixin, docCache: Map): Promise { - const hierarchy = this.client.getHierarchy() - - for (const queries of this.queries.entries()) { - const isTx = hierarchy.isDerived(queries[0], core.class.Tx) - - for (const q of queries[1].values()) { - if (isTx) { - // handle add since Txes are immutable - if (this.match(q, tx, q.options?.lookup !== undefined)) { - await this.handleDocAdd(q, tx, true, docCache) - } - await this.handleDocAddLookup(q, tx) - continue - } - if (q.result instanceof Promise) { - q.result = await q.result - } - let updatedDoc = q.result.findDoc(tx.objectId) - if (updatedDoc !== undefined) { - // If query contains search we must check use fulltext - if (q.query.$search != null && q.query.$search.length > 0) { - const searchRefresh = await this.checkSearch(q, tx.objectId) - if (searchRefresh) { - continue - } - } else { - if (updatedDoc.modifiedOn < tx.modifiedOn) { - await this.__updateMixinDoc(q, updatedDoc, tx) - updatedDoc = this.asMixin(updatedDoc, q._class) - const updateRefresh = this.checkUpdatedDocMatch(q, q.result, updatedDoc) - if (updateRefresh) { - continue - } - } else { - const currentRefresh = await this.getCurrentDoc(q, updatedDoc._id, updatedDoc.space, docCache) - if (currentRefresh) { - continue - } - } - } - await this.sort(q, tx) - const udoc = q.result.findDoc(tx.objectId) - await this.updatedDocCallback(q, q.result, udoc) - } else if (queries[0] === tx.mixin) { - // Mixin potentially added to object we doesn't have in out results - const doc = await this.client.findOne(q._class, { ...q.query, _id: tx.objectId }, q.options) - if (doc !== undefined) { - if (this.match(q, doc, q.options?.lookup !== undefined)) { - await this.handleDocAdd(q, doc, false, docCache) - } - await this.handleDocAddLookup(q, doc) - } - } - await this.handleDocUpdateLookup(q, tx) - await this.handleDocUpdateRelation(q, tx) - } - } - return {} - } - - async txUpdateDoc (tx: TxUpdateDoc, docCache: Map): Promise { - for (const queries of this.queries.entries()) { - const isTx = this.client.getHierarchy().isDerived(queries[0], core.class.Tx) - for (const q of queries[1].values()) { - if (isTx) { - // handle add since Txes are immutable - // await this.handleDocAdd(q, tx, true, docCache) - if (this.match(q, tx, q.options?.lookup !== undefined)) { - await this.handleDocAdd(q, tx, true, docCache) - } - await this.handleDocAddLookup(q, tx) - continue - } - await this.handleDocUpdate(q, tx, docCache) - } - } - return {} - } - - private async handleDocUpdate (q: Query, tx: TxUpdateDoc, docCache: Map): Promise { - if (q.result instanceof Promise) { - q.result = await q.result - } - const updatedDoc = q.result.findDoc(tx.objectId) - if (updatedDoc !== undefined) { - // If query contains search we must check use fulltext - if (q.query.$search != null && q.query.$search.length > 0) { - const searchRefresh = await this.checkSearch(q, tx.objectId) - if (searchRefresh) return - } else { - if (updatedDoc.modifiedOn < tx.modifiedOn) { - await this.__updateDoc(q, updatedDoc, tx) - const updateRefresh = this.checkUpdatedDocMatch(q, q.result, updatedDoc) - if (updateRefresh) { - return - } - } else { - const currentRefresh = await this.getCurrentDoc(q, updatedDoc._id, updatedDoc.space, docCache) - if (currentRefresh) { - return - } - } - } - await this.sort(q, tx) - const udoc = q.result.findDoc(tx.objectId) - await this.updatedDocCallback(q, q.result, udoc) - } else if (this.matchQuerySync(q, tx) && (await this.matchQuery(q, tx, docCache))) { - await this.sort(q, tx) - const udoc = q.result.findDoc(tx.objectId) - await this.updatedDocCallback(q, q.result, udoc) - } else if ( - this.client.getHierarchy().isDerived(tx.objectClass, q._class) && - q.options?.total === true && - q.options.limit === q.result.length - ) { - // we can make object is not matching criteria, but it can be in not limited results, total can be changed - await this.refresh(q) - return - } - await this.handleDocUpdateLookup(q, tx) - await this.handleDocUpdateRelation(q, tx) - } - - private isPossibleAssociationTx (tx: TxUpdateDoc | TxMixin, association: Association): boolean { - const h = this.getHierarchy() - const byClass = - h.isDerived(tx.objectClass, association.classA) || - h.isDerived(tx.objectClass, association.classB) || - h.isDerived(association.classA, tx.objectClass) || - h.isDerived(association.classB, tx.objectClass) - if (byClass) { - return true - } - if (tx._class === core.class.TxMixin) { - const mixinTx = tx as TxMixin - return h.isDerived(mixinTx.mixin, association.classA) || h.isDerived(mixinTx.mixin, association.classB) - } - return false - } - - private async handleDocUpdateRelation (q: Query, tx: TxUpdateDoc | TxMixin): Promise { - if (q.options?.associations === undefined) return - for (const assoc of q.options.associations) { - const association = this.getModel().findObject(assoc[0]) - if (association === undefined) continue - if (this.isPossibleAssociationTx(tx, association)) { - if (q.result instanceof Promise) { - q.result = await q.result - } - const docs = q.result.getDocs() - for (const doc of docs) { - const docToUpdate = doc.$associations?.[association._id]?.find((it) => it._id === tx.objectId) - if (docToUpdate !== undefined) { - if (tx._class === core.class.TxMixin) { - TxProcessor.updateMixin4Doc(docToUpdate, tx as TxMixin) - } else { - TxProcessor.updateDoc2Doc(docToUpdate, tx as TxUpdateDoc) - } - q.result.updateDoc(doc, false) - this.queriesToUpdate.set(q.id, q) - } - } - } - } - } - - private async handleDocUpdateLookup (q: Query, tx: TxUpdateDoc | TxMixin): Promise { - if (q.options?.lookup === undefined) return - const lookup = q.options.lookup - if (q.result instanceof Promise) { - q.result = await q.result - } - let needCallback = false - needCallback = await this.processLookupUpdateDoc(q.result, lookup, tx) - - if (needCallback) { - if (q.options?.sort !== undefined) { - q.result.sort(q._class, q.options.sort, this.getHierarchy(), this.client.getModel()) - } - await this.callback(q, true) - } - } - - private async processLookupUpdateDoc ( - docs: ResultArray, - lookup: Lookup, - tx: TxUpdateDoc | TxMixin - ): Promise { - let needCallback = false - const lookupWays = this.getLookupWays(lookup, tx.objectClass) - for (const lookupWay of lookupWays) { - const [objWay, key, reverseLookupKey] = lookupWay - for (const resDoc of docs.getDocs()) { - const obj = getObjectValue(objWay, resDoc) - if (obj === undefined) continue - const value = getObjectValue('$lookup.' + key, obj) - if (Array.isArray(value)) { - let index = value.findIndex((p) => p._id === tx.objectId) - if (this.client.getHierarchy().isDerived(tx.objectClass, core.class.AttachedDoc)) { - if (reverseLookupKey !== undefined) { - const reverseLookupValue = ( - tx._class === core.class.TxMixin - ? ((tx as TxMixin).attributes as any) - : ((tx as TxUpdateDoc).operations as any) - )[reverseLookupKey] - if (index !== -1 && reverseLookupValue !== undefined && reverseLookupValue !== obj._id) { - value.splice(index, 1) - index = -1 - needCallback = true - docs.updateDoc(resDoc, false) - } else if (index === -1 && reverseLookupValue === obj._id) { - const doc = await this.findOne(tx.objectClass, { _id: tx.objectId }) - if (doc !== undefined) { - value.push(doc) - index = value.length - 1 - } - needCallback = true - docs.updateDoc(resDoc, false) - } - } - } - if (index !== -1) { - if (tx._class === core.class.TxMixin) { - TxProcessor.updateMixin4Doc(value[index], tx as TxMixin) - } else { - TxProcessor.updateDoc2Doc(value[index], tx as TxUpdateDoc) - } - needCallback = true - docs.updateDoc(resDoc, false) - } - } else { - if (obj[key] === tx.objectId) { - if (obj.$lookup[key] !== undefined) { - if (tx._class === core.class.TxMixin) { - TxProcessor.updateMixin4Doc(obj.$lookup[key], tx as TxMixin) - } else { - TxProcessor.updateDoc2Doc(obj.$lookup[key], tx as TxUpdateDoc) - } - needCallback = true - docs.updateDoc(resDoc, false) - } - } - } - } - } - return needCallback - } - - private async refresh (q: Query): Promise { - this.queriesToUpdate.delete(q.id) - await q.refresh() - } - - private async doRefresh (q: Query): Promise { - const qid = ++q.refreshId - const res = await this.client.findAll(q._class, q.query, q.options) - if (q.refreshId === qid && (!deepEqual(res, q.result) || (res.total !== q.total && q.options?.total === true))) { - q.result = new ResultArray(res, this.getHierarchy()) - q.total = res.total - await this.callback(q) - } - } - - private matchQuerySync (q: Query, tx: TxUpdateDoc): boolean { - const clazz = this.getHierarchy().isMixin(q._class) ? this.getHierarchy().getBaseClass(q._class) : q._class - const target = (tx.operations as any)._class ?? tx.objectClass - if (!this.client.getHierarchy().isDerived(target, clazz)) { - return false - } - return true - } - - // Check if query is partially matched. - private async matchQuery (q: Query, tx: TxUpdateDoc, docCache: Map): Promise { - const doc: Doc = { - _id: tx.objectId, - _class: tx.objectClass, - modifiedBy: tx.modifiedBy, - modifiedOn: tx.modifiedOn, - space: tx.objectSpace - } - - // we cannot handle $inc correctly, let's skip it - const { $inc, ...ops } = tx.operations - - const emptyOps = Object.keys(ops).length === 0 - let matched = emptyOps || Object.keys(q.query).length === 0 - if (!emptyOps) { - const virtualTx = { - ...tx, - operations: ops - } - - TxProcessor.updateDoc2Doc(doc, virtualTx) - - for (const key in q.query) { - const value = (q.query as any)[key] - const tkey = checkMixinKey(key, q._class, this.client.getHierarchy()) - if ((doc as any)[tkey] === undefined) continue - const res = findProperty([doc], tkey, value) - if (res.length === 0) { - return false - } else { - matched = true - } - } - } - - if (matched) { - const realDoc = await this.getDocFromCache(docCache, doc._id, Hierarchy.mixinOrClass(doc), doc.space, q) - - if (realDoc == null) return false - - if (this.getHierarchy().isMixin(q._class)) { - if (!this.getHierarchy().hasMixin(realDoc, q._class)) { - return false - } - } - const res = matchQuery([realDoc], q.query, q._class, this.client.getHierarchy()) - if (res.length === 1) { - if (q.result instanceof Promise) { - q.result = await q.result - } - const doc = res[0] - const pos = q.result.findDoc(doc._id) - if (pos !== undefined) { - q.result.updateDoc(doc) - this.refs.updateDocuments(q, [doc]) - } else { - q.result.push(doc) - if (q.options?.total === true) { - q.total++ - } - } - return true - } - } - return false - } - - private async getLookupValue( - _class: Ref>, - doc: T, - lookup: Lookup, - result: LookupData - ): Promise { - for (const key in lookup) { - if (key === '_id') { - await this.getReverseLookupValue(doc, lookup, result) - continue - } - const value = (lookup as any)[key] - const tkey = checkMixinKey(key, _class, this.client.getHierarchy()) - if (Array.isArray(value)) { - const [_class, nested] = value - ;(result as any)[key] = await this.findOne(_class, { _id: getObjectValue(tkey, doc) }) - const nestedResult = {} - const parent = (result as any)[key] - if (parent !== undefined) { - await this.getLookupValue(_class, parent, nested, nestedResult) - Object.assign(parent, { - $lookup: nestedResult - }) - } - } else { - ;(result as any)[key] = await this.findOne(value, { _id: getObjectValue(tkey, doc) }) - } - } - } - - private async getReverseLookupValue( - doc: T, - lookup: ReverseLookups, - result: LookupData - ): Promise { - for (const key in lookup._id) { - if ((doc as any)[key] === undefined || (doc as any)[key] === 0) { - continue - } - - const value = lookup._id[key] - - let _class: Ref> - let attr = 'attachedTo' - - if (Array.isArray(value)) { - _class = value[0] - attr = value[1] - } else { - _class = value - } - ;(result as any)[key] = await this.findAll(_class, { [attr]: doc._id }) - } - } - - private async lookup(_class: Ref>, doc: T, lookup: Lookup): Promise { - const result: LookupData = {} - await this.getLookupValue(_class, doc, lookup, result) - ;(doc as WithLookup).$lookup = result - } - - protected async txCreateDoc (tx: TxCreateDoc, docCache: Map): Promise { - const docTx = TxProcessor.createDoc2Doc(tx) - for (const queries of this.queries.entries()) { - const doc = this.client.getHierarchy().isDerived(queries[0], core.class.Tx) ? tx : docTx - for (const q of queries[1].values()) { - // await this.handleDocAdd(q, doc, true, docCache) - if (this.match(q, doc, q.options?.lookup !== undefined)) { - await this.handleDocAdd(q, doc, true, docCache) - } - - await this.handleDocAddLookup(q, doc) - await this.handleDocAddRelation(q, doc) - } - } - return {} - } - - private async handleDocAdd (q: Query, doc: Doc, handleLookup = true, docCache: Map): Promise { - let needPush = true - if (q.result instanceof Promise) { - q.result = await q.result - } - if (q.options?.lookup !== undefined && handleLookup) { - await this.lookup(q._class, doc, q.options.lookup) - const matched = this.match(q, doc) - if (!matched) needPush = false - } - if (needPush) { - // We could already have document inside results, if query is created during processing of document create transaction and not yet handled on client. - const pos = q.result.findDoc(doc._id) - if (pos !== undefined) { - // No need to update, document already in results. - needPush = false - } - } - if (needPush) { - // If query contains search we must check use fulltext - if (q.query.$search != null && q.query.$search.length > 0) { - const match = await this.client.findOne(q._class, { $search: q.query.$search, _id: doc._id }, q.options) - if (match === undefined) return - } - - q.result.push(doc) - if (q.options?.total === true) { - q.total++ - } - - if (q.options?.sort !== undefined) { - q.result.sort(q._class, q.options.sort, this.getHierarchy(), this.client.getModel()) - } - - if (q.options?.limit !== undefined && q.result.length > q.options.limit) { - if (q.result.pop()?._id !== doc._id || q.options?.total === true) { - await this.callback(q, true) - } - } else { - await this.callback(q, true) - } - } - } - - private async callback (q: Query, bulkUpdate = false): Promise { - if (q.result instanceof Promise) { - q.result = await q.result - } - - const result = q.result - - this.refs.updateDocuments(q, result.getDocs()) - - if (bulkUpdate) { - this.queriesToUpdate.set(q.id, q) - } else { - this.queriesToUpdate.delete(q.id) - for (const [id, callback] of q.callbacks.entries()) { - callback(toFindResult(result.getResult(id), q.total)) - } - } - } - - private async handleDocAddRelation (q: Query, doc: Doc): Promise { - if (q.options?.associations === undefined) return - if (doc._class !== core.class.Relation) return - const relation = doc as Relation - const assoc = q.options.associations.find((p) => p[0] === relation.association) - if (assoc !== undefined) { - if (q.result instanceof Promise) { - q.result = await q.result - } - const direct = assoc[1] === 1 - const res = q.result.findDoc(direct ? relation.docA : relation.docB) - if (res === undefined) return - const association = this.getModel().findObject(assoc[0]) - if (association === undefined) return - const docToPush = await this.findOne(direct ? association.classB : association.classA, { - _id: direct ? relation.docB : relation.docA - }) - if (docToPush === undefined) return - const arr = res?.$associations?.[relation.association] ?? [] - arr.push(docToPush) - if (res?.$associations === undefined) { - res.$associations = {} - } - res.$associations[relation.association] = arr - q.result.updateDoc(res, false) - this.queriesToUpdate.set(q.id, q) - } - } - - private async handleDocAddLookup (q: Query, doc: Doc): Promise { - if (q.options?.lookup === undefined) return - const lookup = q.options.lookup - if (q.result instanceof Promise) { - q.result = await q.result - } - let needCallback = false - needCallback = this.proccesLookupAddDoc(q.result, lookup, doc) - - if (needCallback) { - if (q.options?.sort !== undefined) { - q.result.sort(q._class, q.options.sort, this.getHierarchy(), this.client.getModel()) - } - await this.callback(q, true) - } - } - - private proccesLookupAddDoc (docs: ResultArray, lookup: Lookup, doc: Doc): boolean { - let needCallback = false - const lookupWays = this.getLookupWays(lookup, doc._class) - for (const lookupWay of lookupWays) { - const [objWay, key, reverseLookupKey] = lookupWay - for (const resDoc of docs.getDocs()) { - const obj = getObjectValue(objWay, resDoc) - if (obj === undefined) continue - let value = getObjectValue('$lookup.' + key, obj) - const reverseCheck = reverseLookupKey !== undefined && (doc as any)[reverseLookupKey] === obj._id - if (value == null && reverseCheck) { - value = [] - obj.$lookup[key] = value - needCallback = true - docs.updateDoc(resDoc, false) - } - if (Array.isArray(value)) { - if (this.client.getHierarchy().isDerived(doc._class, core.class.AttachedDoc) && reverseCheck) { - const idx = (value as Doc[]).findIndex((p) => p._id === doc._id) - if (idx === -1) { - value.push(doc) - } else { - value[idx] = doc - } - needCallback = true - docs.updateDoc(resDoc, false) - } - } else { - if (obj[key] === doc._id) { - obj.$lookup[key] = doc - needCallback = true - docs.updateDoc(resDoc, false) - } - } - } - } - return needCallback - } - - protected async txRemoveDoc (tx: TxRemoveDoc, docCache: Map): Promise { - for (const queries of this.queries.entries()) { - const isTx = this.client.getHierarchy().isDerived(queries[0], core.class.Tx) - for (const q of queries[1].values()) { - if (isTx) { - // handle add since Txes are immutable - // await this.handleDocAdd(q, tx, true, docCache) - if (this.match(q, tx, q.options?.lookup !== undefined)) { - await this.handleDocAdd(q, tx, true, docCache) - } - - await this.handleDocAddLookup(q, tx) - continue - } - await this.handleDocRemove(q, tx) - } - } - return {} - } - - private async handleDocRemove (q: Query, tx: TxRemoveDoc): Promise { - const h = this.client.getHierarchy() - if (q._class === tx.objectClass || h.isDerived(q._class, tx.objectClass) || h.isDerived(tx.objectClass, q._class)) { - if (q.result instanceof Promise) { - q.result = await q.result - } - const index = q.result.getDocs().find((p) => p._id === tx.objectId) - if (index !== undefined) { - if (q.options?.limit !== undefined && q.options.limit === q.result.length && q.query._id !== tx.objectId) { - await this.refresh(q) - return - } - q.result.delete(index._id) - this.refs.updateDocuments(q, [index], true) - - if (q.options?.total === true) { - q.total-- - } - await this.callback(q, true) - } - } - await this.handleDocRemoveLookup(q, tx) - await this.handleDocRemoveRelation(q, tx) - } - - private async handleDocRemoveRelation (q: Query, tx: TxRemoveDoc): Promise { - if (q.options?.associations === undefined) return - if (tx.objectClass !== core.class.Relation) return - await this.refresh(q) - } - - private async handleDocRemoveLookup (q: Query, tx: TxRemoveDoc): Promise { - if (q.options?.lookup === undefined) return - let needCallback = false - const lookupWays = this.getLookupWays(q.options.lookup, tx.objectClass) - if (lookupWays.length === 0) return - if (q.result instanceof Promise) { - q.result = await q.result - } - for (const lookupWay of lookupWays) { - const [objWay, key] = lookupWay - const docs = q.result - for (const doc of docs.getDocs()) { - const obj = getObjectValue(objWay, doc) - if (obj === undefined) continue - const value = getObjectValue('$lookup.' + key, obj) - if (value === undefined) continue - if (Array.isArray(value)) { - const index = value.findIndex((p) => p._id === tx.objectId) - if (index !== -1) { - value.splice(index, 1) - needCallback = true - docs.updateDoc(doc, false) - } - } else { - if (value._id === tx.objectId) { - obj.$lookup[key] = undefined - needCallback = true - docs.updateDoc(doc, false) - } - } - } - } - if (needCallback) { - if (q.options?.sort !== undefined) { - q.result.sort(q._class, q.options.sort, this.getHierarchy(), this.client.getModel()) - } - await this.callback(q, true) - } - } - - private getLookupWays ( - lookup: Lookup, - _class: Ref>, - parent: string = '' - ): [string, string, string?][] { - const result: [string, string, string?][] = [] - const hierarchy = this.client.getHierarchy() - if (lookup._id !== undefined) { - for (const key in lookup._id) { - const value = (lookup._id as any)[key] - const [valueClass, reverseLookupKey] = Array.isArray(value) ? value : [value, 'attachedTo'] - const clazz = hierarchy.isMixin(valueClass) ? hierarchy.getBaseClass(valueClass) : valueClass - if (hierarchy.isDerived(_class, clazz)) { - result.push([parent, key, reverseLookupKey]) - } - } - } - for (const key in lookup) { - if (key === '_id') continue - const value = (lookup as any)[key] - if (Array.isArray(value)) { - const clazz = hierarchy.isMixin(value[0]) ? hierarchy.getBaseClass(value[0]) : value[0] - if (hierarchy.isDerived(_class, clazz)) { - result.push([parent, key]) - } - const lookupKey = '$lookup.' + key - const newParent = parent.length > 0 ? parent + '.' + lookupKey : lookupKey - const nested = this.getLookupWays(value[1], _class, newParent) - if (nested.length > 0) { - result.push(...nested) - } - } else { - const clazz = hierarchy.isMixin(value) ? hierarchy.getBaseClass(value) : value - if (hierarchy.isDerived(_class, clazz)) { - result.push([parent, key]) - } - } - } - return result - } - - async _tx (tx: Tx, docCache: Map): Promise { - switch (tx._class) { - case core.class.TxCreateDoc: - return await this.txCreateDoc(tx as TxCreateDoc, docCache) - case core.class.TxUpdateDoc: - return await this.txUpdateDoc(tx as TxUpdateDoc, docCache) - case core.class.TxRemoveDoc: - return await this.txRemoveDoc(tx as TxRemoveDoc, docCache) - case core.class.TxMixin: - return await this.txMixin(tx as TxMixin, docCache) - case core.class.TxApplyIf: - return await Promise.resolve([]) - } - return {} - } - - async tx (...txes: Tx[]): Promise { - const result: TxResult[] = [] - const docCache = new Map() - for (const tx of txes) { - if (tx._class === core.class.TxWorkspaceEvent) { - const evt = tx as TxWorkspaceEvent - await this.checkUpdateEvents(evt) - await this.changePrivateHandler(evt) - } - result.push(await this._tx(tx, docCache)) - } - - if (this.queriesToUpdate.size > 0) { - const copy = new Map(this.queriesToUpdate) - this.queriesToUpdate.clear() - - for (const q of copy.values()) { - if (q.result instanceof Promise) { - q.result = await q.result - } - const qr = q.result - for (const [id, callback] of q.callbacks.entries()) { - callback(toFindResult(qr.getResult(id), q.total)) - } - } - } - return result - } - - private async checkUpdateEvents (evt: TxWorkspaceEvent, trigger = true): Promise { - const h = this.client.getHierarchy() - function hasClass (q: Query, classes: Ref>[]): boolean { - return classes.includes(q._class) || classes.some((it) => h.isDerived(q._class, it) || h.isDerived(it, q._class)) - } - if (evt.event === WorkspaceEvent.IndexingUpdate) { - const indexingParam = evt.params as IndexingUpdateEvent - for (const q of [...this.queue.values()]) { - if (hasClass(q, indexingParam._class) && q.query.$search !== undefined) { - if (!this.removeFromQueue(q)) { - try { - await this.refresh(q) - } catch (err: any) { - Analytics.handleError(err) - console.error(err) - } - } else { - this.removeQueue(q) - } - } - } - for (const v of this.queries.values()) { - for (const q of v.values()) { - if (hasClass(q, indexingParam._class) && q.query.$search !== undefined) { - try { - await this.refresh(q) - } catch (err: any) { - Analytics.handleError(err) - console.error(err) - } - } - } - } - } - if (evt.event === WorkspaceEvent.BulkUpdate) { - const params = evt.params as BulkUpdateEvent - for (const q of [...this.queue.values()]) { - if (hasClass(q, params._class)) { - if (!this.removeFromQueue(q)) { - try { - await this.refresh(q) - } catch (err: any) { - Analytics.handleError(err) - console.error(err) - } - } - } - } - for (const v of this.queries.values()) { - for (const q of v.values()) { - if (hasClass(q, params._class)) { - try { - await this.refresh(q) - } catch (err: any) { - Analytics.handleError(err) - console.error(err) - } - } - } - } - } - } - - private async changePrivateHandler (evt: TxWorkspaceEvent): Promise { - if (evt.event === WorkspaceEvent.SecurityChange) { - for (const q of [...this.queue.values()]) { - if (typeof q.query.space !== 'string' || q.query.space === evt.objectSpace) { - if (!this.removeFromQueue(q)) { - try { - await this.refresh(q) - } catch (err: any) { - Analytics.handleError(err) - console.error(err) - } - } - } - } - for (const v of this.queries.values()) { - for (const q of v.values()) { - if (typeof q.query.space !== 'string' || q.query.space === evt.objectSpace) { - try { - await this.refresh(q) - } catch (err: any) { - Analytics.handleError(err) - console.error(err) - } - } - } - } - } - } - - private async __updateLookup (q: Query, updatedDoc: WithLookup, ops: any): Promise { - for (const key in ops) { - if (!key.startsWith('$')) { - if (q.options !== undefined) { - const lookup = (q.options.lookup as any)?.[key] - if (lookup !== undefined) { - const lookupClass = getLookupClass(lookup) - const nestedLookup = getNestedLookup(lookup) - if (Array.isArray(ops[key])) { - ;(updatedDoc.$lookup as any)[key] = await this.findAll( - lookupClass, - { _id: { $in: ops[key] } }, - { lookup: nestedLookup } - ) - } else { - ;(updatedDoc.$lookup as any)[key] = await this.findOne( - lookupClass, - { _id: ops[key] }, - { lookup: nestedLookup } - ) - } - } - } - } else { - if (key === '$push') { - const pops = ops[key] ?? {} - for (const pkey of Object.keys(pops)) { - if (q.options !== undefined) { - const lookup = (q.options.lookup as any)?.[pkey] - if (lookup !== undefined) { - const lookupClass = getLookupClass(lookup) - const nestedLookup = getNestedLookup(lookup) - const pp = updatedDoc.$lookup as any - if (pp[pkey] === undefined) { - pp[pkey] = [] - } - if (Array.isArray(pops[pkey])) { - const pushData = await this.findAll( - lookupClass, - { _id: { $in: pops[pkey] } }, - { lookup: nestedLookup } - ) - pp[pkey].push(...pushData) - } else { - const d = await this.findOne(lookupClass, { _id: pops[pkey] }, { lookup: nestedLookup }) - if (d !== undefined) { - pp[pkey].push(d) - } - } - } - } - } - } else if (key === '$pull') { - const pops = ops[key] ?? {} - for (const pkey of Object.keys(pops)) { - if (q.options !== undefined) { - const lookup = (q.options.lookup as any)?.[pkey] - if (lookup !== undefined) { - const pid = pops[pkey] - const pp = updatedDoc.$lookup as any - if (pp[pkey] === undefined) { - pp[pkey] = [] - } - if (Array.isArray(pid)) { - pp[pkey] = pp[pkey].filter((it: Doc) => !pid.includes(it._id)) - } else { - pp[pkey] = pp[pkey].filter((it: Doc) => it._id !== pid) - } - } - } - } - } - } - } - } - - private async __updateDoc (q: Query, updatedDoc: WithLookup, tx: TxUpdateDoc): Promise { - TxProcessor.updateDoc2Doc(updatedDoc, tx) - - const ops = { - ...tx.operations, - modifiedBy: tx.modifiedBy, - modifiedOn: tx.modifiedOn - } - await this.__updateLookup(q, updatedDoc, ops) - } - - private async sort (q: Query, tx: TxUpdateDoc | TxMixin): Promise { - const sort = q.options?.sort - if (sort === undefined) return - let needSort = sort.modifiedBy !== undefined || sort.modifiedOn !== undefined - if (!needSort) needSort = this.checkNeedSort(sort, tx) - - if (needSort) { - if (q.result instanceof Promise) { - q.result = await q.result - } - q.result.sort(q._class, sort, this.getHierarchy(), this.client.getModel()) - } - } - - private checkNeedSort (sort: SortingQuery, tx: TxUpdateDoc | TxMixin): boolean { - const ops = - tx._class === core.class.TxMixin - ? (tx as TxMixin).attributes - : ((tx as TxUpdateDoc).operations as any) - for (const key in ops) { - if (key.startsWith('$')) { - for (const opKey in ops[key]) { - if (opKey in sort) return true - } - } else { - if (key in sort) return true - } - } - return false - } - - private async updatedDocCallback (q: Query, res: ResultArray, updatedDoc: Doc | undefined): Promise { - if (q.options?.limit !== undefined && res.length > q.options.limit) { - if (updatedDoc === undefined) { - await this.refresh(q) - return - } - if (res.getDocs()[q.options?.limit]._id === updatedDoc._id) { - await this.refresh(q) - return - } - if (res.pop()?._id !== updatedDoc._id) { - await this.callback(q, true) - } - } else { - await this.callback(q, true) - } - } -} - -function getNestedLookup (lookup: Ref> | [Ref>, Lookup]): Lookup | undefined { - return Array.isArray(lookup) ? lookup[1] : undefined -} - -function getLookupClass (lookup: Ref> | [Ref>, Lookup]): Ref> { - return Array.isArray(lookup) ? lookup[0] : lookup -} diff --git a/foundations/core/packages/query/src/refs.ts b/foundations/core/packages/query/src/refs.ts deleted file mode 100644 index a044270328..0000000000 --- a/foundations/core/packages/query/src/refs.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { - Hierarchy, - matchQuery, - toFindResult, - type Class, - type Doc, - type DocumentQuery, - type FindOptions, - type FindResult, - type Ref, - type Timestamp -} from '@hcengineering/core' -import type { Query, QueryId } from './types' - -export interface DocumentRef { - doc: Doc - queries: QueryId[] - lastUsed: Timestamp -} - -export class Refs { - // A map of _class to documents. - private readonly documentRefs = new Map, DocumentRef>>() - - constructor (readonly getHierarchy: () => Hierarchy) {} - - public updateDocuments (q: Query, docs: Doc[], clean: boolean = false): void { - if (q.options?.projection !== undefined) { - return - } - const params = ':' + JSON.stringify(q.options?.lookup ?? {}) + ':' + JSON.stringify(q.options?.associations ?? {}) - for (const d of docs) { - const classKey = Hierarchy.mixinOrClass(d) + params - - let docMap = this.documentRefs.get(classKey) - if (docMap === undefined) { - if (clean) { - continue - } - docMap = new Map() - this.documentRefs.set(classKey, docMap) - } - const queries = (docMap.get(d._id)?.queries ?? []).filter((it) => it !== q.id) - if (!clean) { - queries.push(q.id) - } - if (queries.length === 0) { - docMap.delete(d._id) - } else { - const q = docMap.get(d._id) - if ((q?.lastUsed ?? 0) < d.modifiedOn) { - docMap.set(d._id, { ...(q ?? {}), doc: d, queries, lastUsed: d.modifiedOn }) - } - } - } - } - - public findFromDocs( - _class: Ref>, - query: DocumentQuery, - options?: FindOptions - ): FindResult | null { - if (typeof query._id === 'string') { - const desc = this.getHierarchy().getDescendants(_class) - for (const des of desc) { - const classKey = - des + ':' + JSON.stringify(options?.lookup ?? {}) + ':' + JSON.stringify(options?.associations ?? {}) - // One document query - const doc = this.documentRefs.get(classKey)?.get(query._id)?.doc - if (doc !== undefined) { - const q = matchQuery([doc], query, _class, this.getHierarchy()) - if (q.length > 0) { - return toFindResult(this.getHierarchy().clone([doc]), 1) - } - } - } - } - if ( - options?.limit === 1 && - options.total !== true && - options?.sort === undefined && - options?.projection === undefined - ) { - const classKey = - _class + ':' + JSON.stringify(options?.lookup ?? {}) + ':' + JSON.stringify(options?.associations ?? {}) - const docs = this.documentRefs.get(classKey) - if (docs !== undefined) { - const _docs = Array.from(docs.values()).map((it) => it.doc) - - const q = matchQuery(_docs, query, _class, this.getHierarchy()) - if (q.length > 0) { - return toFindResult(this.getHierarchy().clone([q[0]]), 1) - } - } - if (options.lookup === undefined && options.associations === undefined) { - const keys = Array.from(this.documentRefs.keys()) - for (const key of keys) { - if (key.startsWith(_class + ':')) { - const docs = this.documentRefs.get(key) - if (docs !== undefined) { - const _docs = Array.from(docs.values()).map((it) => it.doc) - - const q = matchQuery(_docs, query, _class, this.getHierarchy()) - if (q.length > 0) { - const clonedDoc = this.getHierarchy().clone(q[0]) - const { $lookup, $associations, ...clean } = clonedDoc - if (this.getHierarchy().isMixin(_class)) { - return toFindResult([this.getHierarchy().as(clean, _class)] as T[], 1) - } - return toFindResult([clean], 1) - } - } - } - } - } - } - return null - } -} diff --git a/foundations/core/packages/query/src/results.ts b/foundations/core/packages/query/src/results.ts deleted file mode 100644 index bc10576de8..0000000000 --- a/foundations/core/packages/query/src/results.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { - resultSort, - WithLookup, - type Class, - type Doc, - type Hierarchy, - type MemDb, - type Ref, - type SortingQuery -} from '@hcengineering/core' - -export class ResultArray { - private docs: Map, WithLookup> - - private readonly clones = new Map, WithLookup>>() - - get length (): number { - return this.docs.size - } - - constructor ( - docs: Doc[], - readonly hierarchy: Hierarchy - ) { - this.docs = new Map(docs.map((it) => [it._id, it])) - } - - clean (): void { - this.clones.clear() - } - - getDocs (): WithLookup[] { - return Array.from(this.docs.values()) - } - - findDoc (_id: Ref): WithLookup | undefined { - return this.docs.get(_id) - } - - getClone(): T[] { - return this.hierarchy.clone(this.getDocs()) - } - - getResult (id: string): Doc[] { - // Lets form a new list based on clones we have already. - const info = this.clones.get(id) - if (info === undefined) { - const docs = this.getClone() - this.clones.set(id, new Map(docs.map((it) => [it._id, it]))) - return docs - } else { - return Array.from(info.values()) - } - } - - delete (_id: Ref): Doc | undefined { - const doc = this.docs.get(_id) - this.docs.delete(_id) - for (const [, v] of this.clones.entries()) { - v.delete(_id) - } - return doc - } - - updateDoc (doc: WithLookup, mainClone = true): void { - this.docs.set(doc._id, mainClone ? this.hierarchy.clone(doc) : doc) - for (const [, v] of this.clones.entries()) { - v.set(doc._id, this.hierarchy.clone(doc)) - } - } - - push (doc: WithLookup): void { - this.docs.set(doc._id, this.hierarchy.clone(doc)) - for (const [, v] of this.clones.entries()) { - v.set(doc._id, this.hierarchy.clone(doc)) - } - // this.changes.add(doc._id) - } - - pop (): WithLookup | undefined { - const lastElement = Array.from(this.docs)[this.docs.size - 1] - if (lastElement !== undefined) { - this.docs.delete(lastElement[0]) - for (const [, v] of this.clones.entries()) { - v.delete(lastElement[0]) - } - return lastElement[1] - } - return undefined - } - - sort(_class: Ref>, sort: SortingQuery, hierarchy: Hierarchy, memdb: MemDb): void { - const docs = Array.from(this.docs.values()) - resultSort(docs, sort, _class, hierarchy, memdb) - this.docs = new Map(docs.map((it) => [it._id, it])) - for (const [k, v] of this.clones.entries()) { - this.clones.set(k, new Map(docs.map((it) => [it._id, v.get(it._id) ?? this.hierarchy.clone(it)]))) - } - } -} diff --git a/foundations/core/packages/query/src/types.ts b/foundations/core/packages/query/src/types.ts deleted file mode 100644 index 5dec89ed02..0000000000 --- a/foundations/core/packages/query/src/types.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Class, Doc, DocumentQuery, FindOptions, FindResult, Ref } from '@hcengineering/core' -import type { ResultArray } from './results' - -export type Callback = (result: FindResult) => void - -export type QueryId = number -export interface Query { - id: QueryId // uniq query identifier. - _class: Ref> - query: DocumentQuery - result: ResultArray | Promise - options?: FindOptions - total: number - callbacks: Map - refresh: () => Promise - refreshId: number -} diff --git a/foundations/core/packages/query/tsconfig.json b/foundations/core/packages/query/tsconfig.json deleted file mode 100644 index b5ae22f6e4..0000000000 --- a/foundations/core/packages/query/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/foundations/core/packages/rank/.eslintrc.js b/foundations/core/packages/rank/.eslintrc.js deleted file mode 100644 index 72235dc283..0000000000 --- a/foundations/core/packages/rank/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/foundations/core/packages/rank/CHANGELOG.json b/foundations/core/packages/rank/CHANGELOG.json deleted file mode 100644 index 5518e2c82b..0000000000 --- a/foundations/core/packages/rank/CHANGELOG.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "name": "@hcengineering/rank", - "entries": [ - { - "version": "0.7.17", - "tag": "@hcengineering/rank_v0.7.17", - "date": "Mon, 27 Oct 2025 13:27:12 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" - } - ] - } - }, - { - "version": "0.7.5", - "tag": "@hcengineering/rank_v0.7.5", - "date": "Tue, 14 Oct 2025 04:58:17 GMT", - "comments": { - "patch": [ - { - "comment": "update deps" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`" - } - ] - } - }, - { - "version": "0.7.4", - "tag": "@hcengineering/rank_v0.7.4", - "date": "Sat, 11 Oct 2025 18:20:33 GMT", - "comments": { - "patch": [ - { - "comment": "Update to latest platform rig" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`" - } - ] - } - }, - { - "version": "0.7.3", - "tag": "@hcengineering/rank_v0.7.3", - "date": "Wed, 08 Oct 2025 03:40:53 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`" - } - ] - } - } - ] -} diff --git a/foundations/core/packages/rank/CHANGELOG.md b/foundations/core/packages/rank/CHANGELOG.md deleted file mode 100644 index 4e97a1317e..0000000000 --- a/foundations/core/packages/rank/CHANGELOG.md +++ /dev/null @@ -1,28 +0,0 @@ -# Change Log - @hcengineering/rank - -This log was last generated on Mon, 27 Oct 2025 13:27:12 GMT and should not be manually modified. - -## 0.7.17 -Mon, 27 Oct 2025 13:27:12 GMT - -_Version update only_ - -## 0.7.5 -Tue, 14 Oct 2025 04:58:17 GMT - -### Patches - -- update deps - -## 0.7.4 -Sat, 11 Oct 2025 18:20:33 GMT - -### Patches - -- Update to latest platform rig - -## 0.7.3 -Wed, 08 Oct 2025 03:40:53 GMT - -_Initial release_ - diff --git a/foundations/core/packages/rank/config/rig.json b/foundations/core/packages/rank/config/rig.json deleted file mode 100644 index 0110930f55..0000000000 --- a/foundations/core/packages/rank/config/rig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig" -} diff --git a/foundations/core/packages/rank/jest.config.js b/foundations/core/packages/rank/jest.config.js deleted file mode 100644 index 069ea8aa0d..0000000000 --- a/foundations/core/packages/rank/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ['./src'], - coverageReporters: ['text-summary', 'html', 'lcov'] -} diff --git a/foundations/core/packages/rank/package.json b/foundations/core/packages/rank/package.json deleted file mode 100644 index 5103c831ed..0000000000 --- a/foundations/core/packages/rank/package.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "name": "@hcengineering/rank", - "version": "0.7.17", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "files": [ - "lib/**/*", - "types/**/*", - "src/**/*", - "!src/**/__test__/**", - "tsconfig.json" - ], - "author": "Anticrm Platform Contributors", - "license": "EPL-2.0", - "scripts": { - "_phase:build": "compile transpile src", - "_phase:validate": "compile validate", - "_phase:test": "jest --passWithNoTests --silent --coverage", - "_phase:format": "format src", - "build": "compile", - "test": "jest --passWithNoTests --silent --coverage", - "build:watch": "compile", - "format": "format src" - }, - "devDependencies": { - "@hcengineering/platform-rig": "workspace:^0.7.19", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "@typescript-eslint/parser": "^6.21.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-n": "^15.4.0", - "eslint-plugin-promise": "^6.1.1", - "eslint": "^8.54.0", - "prettier": "^3.6.2", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5", - "typescript": "^5.9.3", - "eslint-plugin-svelte": "^2.35.1" - }, - "dependencies": { - "@hcengineering/core": "workspace:^0.7.22", - "lexorank": "~1.0.4" - }, - "repository": "https://github.com/hcengineering/huly.core", - "publishConfig": { - "access": "public" - }, - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.js" - } - } -} diff --git a/foundations/core/packages/rank/src/__tests__/utils.test.ts b/foundations/core/packages/rank/src/__tests__/utils.test.ts deleted file mode 100644 index d077ea93e7..0000000000 --- a/foundations/core/packages/rank/src/__tests__/utils.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { makeRank } from '..' - -describe('makeRank', () => { - it('calculates rank when no prev and next', () => { - expect(makeRank(undefined, undefined)).toBe('0|hzzzzz:') - }) - - it('check rank on empty string', () => { - expect(makeRank(undefined, '')).toBe('0|hzzzzz:') - }) - - it.each([ - ['0|hzzzzz:', '0|i00007:'], - ['0|i00007:', '0|i0000f:'], - ['0|i0000f:', '0|i0000n:'], - ['0|zzzzzz:', '0|zzzzzz:'] - ])('calculates rank value when prev is %p', (prev, expected) => { - expect(makeRank(prev, undefined)).toBe(expected) - }) - - it.each([ - ['0|hzzzzz:', '0|hzzzzr:'], - ['0|hzzzzr:', '0|hzzzzj:'], - ['0|hzzzzj:', '0|hzzzzb:'], - ['0|000000:', '0|000000:'] - ])('calculates rank value when next is %p', (next, expected) => { - expect(makeRank(undefined, next)).toBe(expected) - }) - - it.each([ - ['0|000000:', '0|000001:', '0|000000:i'], - ['0|hzzzzz:', '0|i0000f:', '0|i00007:'], - ['0|hzzzzz:', '0|hzzzzz:', '0|i00007:'], - ['0|i00007:', '0|i00007:', '0|i0000f:'], - ['0|i00007:', '0|i00008:', '0|i00007:i'] - ])('calculates rank value when prev is %p and next is %p', (prev, next, expected) => { - expect(makeRank(prev, next)).toBe(expected) - }) - - it.each([ - [10, '0|hzzzxr:'], - [100, '0|hzzzdr:'], - [1000, '0|hzzttr:'], - [10000, '0|hzya9r:'] - ])('produces prev rank of reasonable length for %p generations', (count, expected) => { - let rank = '0|hzzzzz:' - for (let i = 0; i < count; i++) { - rank = makeRank(undefined, rank) - } - expect(rank).toBe(expected) - }) - - it.each([ - [5, '0|zfqzzz:'], - [10, '0|zzd7vh:'], - [50, '0|zzzzzy:zzzi'], - [100, '0|zzzzzy:zzzzzzzzzzzv'] - ])('produces middle rank of reasonable length for %p generations', (count, expected) => { - let rank = '0|hzzzzz:' - for (let i = 0; i < count; i++) { - rank = makeRank(rank, '0|zzzzzz') - } - expect(rank).toBe(expected) - }) - - it.each([ - [10, '0|i00027:'], - [100, '0|i000m7:'], - [1000, '0|i00667:'], - [10000, '0|i01pq7:'], - [100000, '0|i0h5a7:'], - [1000000, '0|i4rgu7:'] - ])('produces next rank of reasonable length for %p generations', (count, expected) => { - let rank = '0|hzzzzz:' - for (let i = 0; i < count; i++) { - rank = makeRank(rank, undefined) - } - expect(rank).toBe(expected) - }) -}) diff --git a/foundations/core/packages/rank/src/index.ts b/foundations/core/packages/rank/src/index.ts deleted file mode 100644 index f6894a9426..0000000000 --- a/foundations/core/packages/rank/src/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export * from './types' -export * from './utils' diff --git a/foundations/core/packages/rank/src/types.ts b/foundations/core/packages/rank/src/types.ts deleted file mode 100644 index 6a7bb43685..0000000000 --- a/foundations/core/packages/rank/src/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export { type Rank } from '@hcengineering/core' diff --git a/foundations/core/packages/rank/src/utils.ts b/foundations/core/packages/rank/src/utils.ts deleted file mode 100644 index 4f43e8925b..0000000000 --- a/foundations/core/packages/rank/src/utils.ts +++ /dev/null @@ -1,62 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import LexoRankBucket from 'lexorank/lib/lexoRank/lexoRankBucket' -import type { Rank } from './types' -import { LexoDecimal, LexoNumeralSystem36, LexoRank } from 'lexorank' - -/** @public */ -export function genRanks (count: number): Rank[] { - const sys = new LexoNumeralSystem36() - const base = 36 - const max = base ** 6 - const gap = LexoDecimal.parse(Math.trunc(max / (count + 2)).toString(base), sys) - let cur = LexoDecimal.parse('0', sys) - const res: string[] = [] - for (let i = 0; i < count; i++) { - cur = cur.add(gap) - res.push(new LexoRank(LexoRankBucket.BUCKET_0, cur).toString()) - } - return res -} - -/** @public */ -export function makeRank (prev: Rank | undefined, next: Rank | undefined): Rank { - try { - if (prev != null && prev.trim() === '') { - prev = undefined - } - if (next != null && next.trim() === '') { - next = undefined - } - if (prev !== undefined && next !== undefined) { - const prevLexoRank = LexoRank.parse(prev) - const nextLexoRank = LexoRank.parse(next) - return prevLexoRank.equals(nextLexoRank) - ? prevLexoRank.genNext().toString() - : prevLexoRank.between(nextLexoRank).toString() - } else if (prev !== undefined) { - const prevLexoRank = LexoRank.parse(prev) - return prevLexoRank.genNext().toString() - } else if (next !== undefined) { - const nextLexoRank = LexoRank.parse(next) - return nextLexoRank.genPrev().toString() - } else { - return LexoRank.middle().toString() - } - } catch (err: any) { - throw new Error(`Failed to make rank: ${prev} ${next} ${err.message}`) - } -} diff --git a/foundations/core/packages/rank/tsconfig.json b/foundations/core/packages/rank/tsconfig.json deleted file mode 100644 index b5ae22f6e4..0000000000 --- a/foundations/core/packages/rank/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/foundations/core/packages/retry/.eslintrc.js b/foundations/core/packages/retry/.eslintrc.js deleted file mode 100644 index ce90fb9646..0000000000 --- a/foundations/core/packages/retry/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/foundations/core/packages/retry/.npmignore b/foundations/core/packages/retry/.npmignore deleted file mode 100644 index e3ec093c38..0000000000 --- a/foundations/core/packages/retry/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!/lib/** -!CHANGELOG.md -/lib/**/__tests__/ diff --git a/foundations/core/packages/retry/CHANGELOG.json b/foundations/core/packages/retry/CHANGELOG.json deleted file mode 100644 index 621fc9eb8d..0000000000 --- a/foundations/core/packages/retry/CHANGELOG.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "@hcengineering/retry", - "entries": [ - { - "version": "0.7.5", - "tag": "@hcengineering/retry_v0.7.5", - "date": "Tue, 14 Oct 2025 04:58:17 GMT", - "comments": { - "patch": [ - { - "comment": "update deps" - } - ] - } - }, - { - "version": "0.7.4", - "tag": "@hcengineering/retry_v0.7.4", - "date": "Sat, 11 Oct 2025 18:20:33 GMT", - "comments": { - "patch": [ - { - "comment": "Update to latest platform rig" - } - ] - } - } - ] -} diff --git a/foundations/core/packages/retry/CHANGELOG.md b/foundations/core/packages/retry/CHANGELOG.md deleted file mode 100644 index 7fa2f71d0f..0000000000 --- a/foundations/core/packages/retry/CHANGELOG.md +++ /dev/null @@ -1,18 +0,0 @@ -# Change Log - @hcengineering/retry - -This log was last generated on Tue, 14 Oct 2025 04:58:17 GMT and should not be manually modified. - -## 0.7.5 -Tue, 14 Oct 2025 04:58:17 GMT - -### Patches - -- update deps - -## 0.7.4 -Sat, 11 Oct 2025 18:20:33 GMT - -### Patches - -- Update to latest platform rig - diff --git a/foundations/core/packages/retry/config/rig.json b/foundations/core/packages/retry/config/rig.json deleted file mode 100644 index 78cc5a1733..0000000000 --- a/foundations/core/packages/retry/config/rig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig", - "rigProfile": "node" -} diff --git a/foundations/core/packages/retry/jest.config.js b/foundations/core/packages/retry/jest.config.js deleted file mode 100644 index 069ea8aa0d..0000000000 --- a/foundations/core/packages/retry/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ['./src'], - coverageReporters: ['text-summary', 'html', 'lcov'] -} diff --git a/foundations/core/packages/retry/package.json b/foundations/core/packages/retry/package.json deleted file mode 100644 index 5a94f3c87d..0000000000 --- a/foundations/core/packages/retry/package.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "name": "@hcengineering/retry", - "version": "0.7.17", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "author": "Copyright © Hardcore Engineering Inc.", - "template": "@hcengineering/node-package", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "build:watch": "compile", - "test": "jest --passWithNoTests --silent --forceExit --coverage", - "format": "format src", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --forceExit --coverage", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "workspace:^0.7.19", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "@typescript-eslint/parser": "^6.21.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.6.2", - "typescript": "^5.9.3", - "@types/node": "^22.18.1", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5", - "eslint-plugin-svelte": "^2.35.1" - }, - "files": [ - "lib/**/*", - "!lib/**/__test__/**", - "types/**/*", - "!types/**/__test__/**", - "src/**/*", - "!src/**/__test__/**", - "tsconfig.json" - ], - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.js" - } - }, - "repository": "https://github.com/hcengineering/huly.core", - "publishConfig": { - "access": "public" - } -} diff --git a/foundations/core/packages/retry/readme.md b/foundations/core/packages/retry/readme.md deleted file mode 100644 index 55eda694b5..0000000000 --- a/foundations/core/packages/retry/readme.md +++ /dev/null @@ -1,252 +0,0 @@ -# Retry Utility - -A comprehensive TypeScript utility for handling transient failures with exponential backoff, jitter, and customizable retry conditions. - -## Features - -- ✅ **Exponential backoff** with configurable parameters -- ✅ **Jitter support** to prevent thundering herd problems -- ✅ **Customizable retry conditions** to control which errors should be retried -- ✅ **TypeScript decorators** for clean, declarative retry logic -- ✅ **Function wrappers** for retrofitting existing code with retry capabilities -- ✅ **Comprehensive logging** of retry attempts and failures - -## Usage - -### Basic Usage - -Wrap any async operation with the `withRetry` function: - -```typescript -import { withRetry } from '@hcengineering/retry' - -async function fetchData() { - const data = await withRetry( - async () => { - // Your async operation that might fail transiently - return await api.getData() - }, - { maxRetries: 3 } - ) - return data -} -``` - -### Using Decorators - -For class methods, you can use the `@Retryable` decorator for clean, declarative retry logic: - -```typescript -import { Retryable } from '@hcengineering/retry' - -class UserService { - @Retryable({ maxRetries: 5 }) - async getUserProfile(userId: string): Promise { - // This method will automatically retry on failure - return await this.api.fetchUserProfile(userId) - } -} -``` - -### Delay Strategies - -The package provides several delay strategies to control the timing between retry attempts: - -Exponential Backoff -Increases the delay exponentially between retries, which is ideal for backing off from overloaded services: - -```typescript -import { withRetry, DelayStrategyFactory } from '@hcengineering/retry' - -await withRetry( - async () => await api.getData(), - { - maxRetries: 5, - delayStrategy: DelayStrategyFactory.exponentialBackoff({ - initialDelayMs: 100, // Start with 100ms - maxDelayMs: 10000, // Cap at 10 seconds - backoffFactor: 2, // Double the delay each time (100, 200, 400, 800, 1600) - jitter: 0.2 // Add ±20% randomness - }) - } -) -``` - -Fixed Delay -Uses the same delay for all retry attempts, useful when retrying after a fixed cooldown period: -```typescript -import { withRetry, DelayStrategyFactory } from '@hcengineering/retry' - -await withRetry( - async () => await api.getData(), - { - maxRetries: 3, - delayStrategy: DelayStrategyFactory.fixed({ - delayMs: 1000, // Always wait 1 second between retries - jitter: 0.1 // Optional: add ±10% randomness - }) - } -) -``` -Fibonacci Delay -Uses the Fibonacci sequence to calculate delays, providing a more moderate growth rate than exponential backoff: -```typescript -import { withRetry, DelayStrategyFactory } from '@hcengineering/retry' - -await withRetry( - async () => await api.getData(), - { - maxRetries: 6, - delayStrategy: DelayStrategyFactory.fibonacci({ - baseDelayMs: 100, // Base unit for Fibonacci sequence - maxDelayMs: 10000, // Maximum delay cap - jitter: 0.2 // Add ±20% randomness - }) - } -) -// Delays follow Fibonacci sequence: 100ms, 200ms, 300ms, 500ms, 800ms, ... -``` - -### Custom Retry Conditions - -You can specify which errors should trigger retries: - -```typescript -import { withRetry, retryNetworkErrors } from '@platform/utils/retry' - -async function fetchData() { - return await withRetry( - async () => await api.getData(), - { - // Only retry network-related errors - isRetryable: retryNetworkErrors, - maxRetries: 5 - } - ) -} -``` - -Create your own custom retry condition: - -```typescript -import { type IsRetryable } from '@platform/utils/retry' - -// Custom retry condition -const retryDatabaseErrors: IsRetryable = (error: unknown): boolean => { - if (error instanceof DatabaseError) { - // Only retry specific database errors - return error.code === 'CONNECTION_LOST' || - error.code === 'DEADLOCK' || - error.code === 'TIMEOUT' - } - return false -} - -// Use it -await withRetry( - async () => await db.query('SELECT * FROM users'), - { isRetryable: retryDatabaseErrors } -) -``` - -## API Reference - -### `withRetry(operation, options?, operationName?): Promise` - -Executes an async operation with retry logic. - -- `operation`: `() => Promise` - The async operation to execute -- `options`: `Partial` - Retry configuration (optional) -- `operationName`: `string` - Name for logging (optional) -- Returns: `Promise` - The result of the operation - -### `createRetryableFunction(fn, options?, operationName?): T` - -Creates a retryable function from an existing function. - -- `fn`: `T extends (...args: any[]) => Promise` - The function to make retryable -- `options`: `Partial` - Retry configuration (optional) -- `operationName`: `string` - Name for logging (optional) -- Returns: `T` - A wrapped function with retry logic - -### `@Retryable(options?)` - -Method decorator for adding retry functionality to class methods. - -- `options`: `Partial` - Retry configuration (optional) - -### RetryOptions - -Configuration options for the retry mechanism: - -| Option | Type | Default | Description | -|--------|------|---------|-------------| -| `initialDelayMs` | `number` | `1000` | Initial delay between retries in milliseconds | -| `maxDelayMs` | `number` | `30000` | Maximum delay between retries in milliseconds | -| `maxRetries` | `number` | `5` | Maximum number of retry attempts | -| `backoffFactor` | `number` | `1.5` | Backoff factor for exponential delay increase | -| `jitter` | `number` | `0.2` | Jitter factor (0-1) to add randomness to delay times | -| `isRetryable` | `IsRetryable` | `retryAllErrors` | Function to determine if an error is retriable | -| `logger` | `Logger` | `defaultLogger` | Logger to use | - -### Retry Condition Functions - -| Function | Description | -|----------|-------------| -| `retryAllErrors` | Retry on any error (default) | -| `retryNetworkErrors` | Retry only on network-related errors | - -## Examples - -### Basic Retry with Custom Options - -```typescript -import { withRetry } from '@platform/utils/retry' - -async function fetchDataWithRetry() { - return await withRetry( - async () => { - const response = await fetch('https://api.example.com/data') - if (!response.ok) { - throw new Error(`HTTP error: ${response.status}`) - } - return await response.json() - }, - { - initialDelayMs: 300, // Start with 300ms delay - maxDelayMs: 10000, // Max delay of 10 seconds - maxRetries: 4, // Try up to 4 times (1 initial + 3 retries) - backoffFactor: 2, // Double the delay each time - jitter: 0.25 // Add 25% randomness to delay - }, - 'fetchApiData' // Name for logging - ) -} -``` - -### Class with Multiple Retryable Methods - -```typescript -import { Retryable, retryNetworkErrors } from '@platform/utils/retry' - -class DataService { - @Retryable({ - maxRetries: 3, - initialDelayMs: 200 - }) - async fetchUsers(): Promise { - // Will retry up to 3 times with initial 200ms delay - return await this.api.getUsers() - } - - @Retryable({ - maxRetries: 5, - initialDelayMs: 1000, - isRetryable: retryNetworkErrors - }) - async uploadFile(file: File): Promise { - // Will retry up to 5 times, but only for network errors - return await this.api.uploadFile(file) - } -} -``` diff --git a/foundations/core/packages/retry/src/__test__/decorator.test.ts b/foundations/core/packages/retry/src/__test__/decorator.test.ts deleted file mode 100644 index 557661d011..0000000000 --- a/foundations/core/packages/retry/src/__test__/decorator.test.ts +++ /dev/null @@ -1,395 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { DelayStrategyFactory } from '../delay' -import { Retryable } from '../decorator' -import { type RetryOptions } from '../retry' -import { retryAllErrors } from '../retryable' - -// Instead of mocking withRetry, we'll mock setTimeout to avoid waiting in tests -jest.spyOn(global, 'setTimeout').mockImplementation((fn: any) => { - fn() - return 1 as any -}) - -describe('Retryable decorator', () => { - const mockLogger = { - warn: jest.fn(), - error: jest.fn(), - info: jest.fn() - } - - // Update the mock options to use delay strategy - const mockOptions: Partial = { - maxRetries: 3, - delayStrategy: DelayStrategyFactory.exponentialBackoff({ - initialDelayMs: 10, - maxDelayMs: 100, - backoffFactor: 2 - }), - isRetryable: retryAllErrors, - logger: mockLogger - } - - beforeEach(() => { - jest.clearAllMocks() - }) - - it('should retry failed operations', async () => { - // Create a test class with decorated method that fails initially then succeeds - const error = new Error('First attempt failed') - class TestService { - callCount = 0 - - @Retryable(mockOptions) - async testMethod (param1: string, param2: number): Promise { - this.callCount++ - if (this.callCount === 1) { - throw error - } - return `${param1}-${param2}` - } - } - - const service = new TestService() - const result = await service.testMethod('test', 123) - - // Check results - expect(result).toBe('test-123') - expect(service.callCount).toBe(2) // Called once, failed, then succeeded on retry - - // Check logs - expect(mockLogger.warn).toHaveBeenCalledTimes(1) - expect(mockLogger.warn).toHaveBeenCalledWith( - expect.stringContaining('testMethod failed'), - expect.objectContaining({ - error, - attempt: 1 - }) - ) - expect(mockLogger.error).not.toHaveBeenCalled() - }) - - it('should work with default options', async () => { - class TestService { - callCount = 0 - - @Retryable() - async testMethod (): Promise { - this.callCount++ - if (this.callCount === 1) { - throw new Error('network error') - } - return 'success' - } - } - - const service = new TestService() - const result = await service.testMethod() - - expect(result).toBe('success') - expect(service.callCount).toBe(2) // Should have retried once - }) - - it('should preserve class instance context (this)', async () => { - class TestService { - private counter = 0 - - @Retryable(mockOptions) - async incrementAndGet (): Promise { - if (this.counter === 0) { - this.counter++ - throw new Error('network error') - } - this.counter++ - return this.counter - } - - getCounter (): number { - return this.counter - } - } - - const service = new TestService() - const result = await service.incrementAndGet() - - // Check that the class context was preserved across retries - expect(result).toBe(2) - expect(service.getCounter()).toBe(2) // Incremented once per attempt - }) - - it('should throw after max retries are exhausted', async () => { - class TestService { - @Retryable({ - maxRetries: 2, // Only try twice total (initial + 1 retry) - delayStrategy: DelayStrategyFactory.fixed({ - delayMs: 10 - }), - logger: mockLogger, - isRetryable: retryAllErrors - }) - async alwaysFailingMethod (): Promise { - throw new Error('Persistent failure') - } - } - - const service = new TestService() - await expect(service.alwaysFailingMethod()).rejects.toThrow('Persistent failure') - - // Should have tried twice in total (initial + 1 retry) - expect(mockLogger.warn).toHaveBeenCalledTimes(1) - expect(mockLogger.error).toHaveBeenCalledTimes(1) - expect(mockLogger.error).toHaveBeenCalledWith( - expect.stringContaining('alwaysFailingMethod failed after 2 attempts'), - expect.any(Object) - ) - }) - - it('should handle async methods correctly', async () => { - let resolutionCount = 0 - - // Create a class with an async method that fails then resolves - class TestService { - @Retryable(mockOptions) - async delayedMethod (): Promise { - return await new Promise((resolve, reject) => { - resolutionCount++ - if (resolutionCount === 1) { - reject(new Error('Delayed error')) - } else { - resolve('delayed success') - } - }) - } - } - - const service = new TestService() - const result = await service.delayedMethod() - - expect(result).toBe('delayed success') - expect(resolutionCount).toBe(2) - expect(mockLogger.warn).toHaveBeenCalledTimes(1) - }) - - it('should retry according to the specified retry count', async () => { - let callCount = 0 - - class TestService { - @Retryable({ - maxRetries: 5, // Should try up to 5 times total - delayStrategy: DelayStrategyFactory.fixed({ - delayMs: 10 - }), - logger: mockLogger, - isRetryable: retryAllErrors - }) - async unstableMethod (): Promise { - callCount++ - if (callCount < 4) { - // Succeed on the 4th attempt - throw new Error(`Failure #${callCount}`) - } - return 'success after retries' - } - } - - const service = new TestService() - const result = await service.unstableMethod() - - expect(result).toBe('success after retries') - expect(callCount).toBe(4) // Initial attempt + 3 retries = 4 total calls - expect(mockLogger.warn).toHaveBeenCalledTimes(3) // Should have logged 3 warnings - }) - - it('should respect different delay strategies', async () => { - // Override the setTimeout mock to capture delay values - const delayValues: number[] = [] - jest.spyOn(global, 'setTimeout').mockImplementation((fn: any, delay: number | undefined) => { - delayValues.push(delay ?? 0) - fn() - return 1 as any - }) - - let callCount = 0 - - class TestService { - @Retryable({ - maxRetries: 4, - delayStrategy: DelayStrategyFactory.exponentialBackoff({ - initialDelayMs: 100, - maxDelayMs: 500, - backoffFactor: 2, - jitter: 0 // Disable jitter for predictable tests - }), - isRetryable: retryAllErrors - }) - async delayingMethod (): Promise { - callCount++ - if (callCount < 4) { - throw new Error(`Attempt ${callCount} failed`) - } - return 'success' - } - } - - const service = new TestService() - await service.delayingMethod() - - // Should have recorded 3 delays: initial, 2x initial, 4x initial (capped at maxDelayMs) - expect(delayValues).toHaveLength(3) - expect(delayValues[0]).toBe(100) // initial delay - expect(delayValues[1]).toBe(200) // 2x initial - expect(delayValues[2]).toBe(400) // 4x initial - }) - - it('should test various delay strategies', async () => { - // Override the setTimeout mock to capture delay values - const delayValues: number[] = [] - jest.spyOn(global, 'setTimeout').mockImplementation((fn: any, delay: number | undefined) => { - delayValues.push(delay ?? 0) - fn() - return 1 as any - }) - - // Test Fixed strategy - let callCount = 0 - class FixedTestService { - @Retryable({ - maxRetries: 3, - delayStrategy: DelayStrategyFactory.fixed({ - delayMs: 100, - jitter: 0 // Disable jitter for predictable tests - }), - isRetryable: retryAllErrors - }) - async method (): Promise { - callCount++ - if (callCount < 3) { - throw new Error(`Attempt ${callCount} failed`) - } - return 'success' - } - } - - delayValues.length = 0 // Reset captured delays - await new FixedTestService().method() - expect(delayValues).toEqual([100, 100]) // Should be constant - - // Test Fibonacci strategy - callCount = 0 - class FibonacciTestService { - @Retryable({ - maxRetries: 4, - delayStrategy: DelayStrategyFactory.fibonacci({ - baseDelayMs: 100, - maxDelayMs: 1000, - jitter: 0 // Disable jitter for predictable tests - }), - isRetryable: retryAllErrors - }) - async method (): Promise { - callCount++ - if (callCount < 4) { - throw new Error(`Attempt ${callCount} failed`) - } - return 'success' - } - } - - delayValues.length = 0 // Reset captured delays - await new FibonacciTestService().method() - // Fibonacci sequence delay pattern - expect(delayValues).toEqual([100, 200, 300]) // fib(2)=1, fib(3)=2, fib(4)=3 multiplied by baseDelayMs - }) - - it('should handle methods returning non-promises', async () => { - let callCount = 0 - - class TestService { - @Retryable(mockOptions) - nonAsyncMethod (input: string): string { - callCount++ - if (callCount === 1) { - throw new Error('Sync error') - } - return `processed-${input}` - } - } - - const service = new TestService() - // Even though the original method is not async, the decorated method returns a Promise - // eslint-disable-next-line @typescript-eslint/await-thenable - const result = await service.nonAsyncMethod('test') - - expect(result).toBe('processed-test') - expect(callCount).toBe(2) // Should have retried once - expect(mockLogger.warn).toHaveBeenCalledTimes(1) - }) - - it('should handle static methods', async () => { - let callCount = 0 - - // eslint-disable-next-line @typescript-eslint/no-extraneous-class - class TestService { - @Retryable(mockOptions) - static async staticMethod (input: string): Promise { - callCount++ - if (callCount === 1) { - throw new Error('Static method error') - } - return `static-${input}` - } - } - - const result = await TestService.staticMethod('test') - - expect(result).toBe('static-test') - expect(callCount).toBe(2) // Should have retried once - }) - - it('should respect isRetryable option', async () => { - class TestService { - callCount = 0 - - @Retryable({ - maxRetries: 3, - delayStrategy: DelayStrategyFactory.fixed({ delayMs: 10 }), - logger: mockLogger, - isRetryable: (err) => { - // Only retry errors with "retry" in the message - return err instanceof Error && err.message.includes('Please retry') - } - }) - async conditionalRetryMethod (): Promise { - this.callCount++ - if (this.callCount === 1) { - throw new Error('Please retry this') // should be retried - } - if (this.callCount === 2) { - throw new Error('Do not retry this') // should not be retried - } - return 'success' - } - } - - const service = new TestService() - // Should fail with the second error since it won't be retried - await expect(service.conditionalRetryMethod()).rejects.toThrow('Do not retry this') - - expect(service.callCount).toBe(2) // Should have called twice (original + 1 retry) - expect(mockLogger.warn).toHaveBeenCalledTimes(1) // Only first error logged as warning - expect(mockLogger.error).toHaveBeenCalledTimes(1) // Second error logged as error - }) -}) diff --git a/foundations/core/packages/retry/src/__test__/delay.test.ts b/foundations/core/packages/retry/src/__test__/delay.test.ts deleted file mode 100644 index 67df348b8b..0000000000 --- a/foundations/core/packages/retry/src/__test__/delay.test.ts +++ /dev/null @@ -1,296 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { DelayStrategyFactory, ExponentialBackoffStrategy, FibonacciDelayStrategy, FixedDelayStrategy } from '../delay' - -describe('Delay Strategies', () => { - // Mock Math.random to return fixed values in tests - let randomMock: jest.SpyInstance - - beforeEach(() => { - // Default mock returns 0.5 for Math.random, which creates 0 jitter effect - randomMock = jest.spyOn(Math, 'random').mockReturnValue(0.5) - }) - - afterEach(() => { - randomMock.mockRestore() - }) - - describe('FixedDelayStrategy', () => { - it('should return the same delay for all attempts without jitter', () => { - const delay = 1000 - const strategy = new FixedDelayStrategy({ delayMs: delay }) - - expect(strategy.getDelay(1)).toBe(delay) - expect(strategy.getDelay(2)).toBe(delay) - expect(strategy.getDelay(5)).toBe(delay) - expect(strategy.getDelay(10)).toBe(delay) - }) - - it('should apply jitter correctly', () => { - const delay = 1000 - const jitter = 0.2 // 20% jitter - const strategy = new FixedDelayStrategy({ delayMs: delay, jitter }) - - // Mock random to return different values - randomMock.mockReturnValueOnce(0.6) // => +0.2 * delay - randomMock.mockReturnValueOnce(0.4) // => -0.2 * delay - randomMock.mockReturnValueOnce(1.0) // => +1.0 * delay - - // With 0.6 random value, jitter effect is (0.6-0.5)*2*0.2 = +0.04 => 4% increase - expect(strategy.getDelay(1)).toBe(delay + delay * 0.2 * 0.2) - - // With 0.4 random value, jitter effect is (0.4-0.5)*2*0.2 = -0.04 => 4% decrease - expect(strategy.getDelay(2)).toBe(delay - delay * 0.2 * 0.2) - - // With 1.0 random value, jitter effect is (1.0-0.5)*2*0.2 = +0.2 => 20% increase - expect(strategy.getDelay(3)).toBe(delay + delay * 0.2 * 1.0) - }) - - it('should never return negative values even with high jitter', () => { - const delay = 100 - const jitter = 1.0 // 100% jitter, extreme case - const strategy = new FixedDelayStrategy({ delayMs: delay, jitter }) - - // Mock random to return min value (full negative jitter) - randomMock.mockReturnValue(0) // => -1.0 * delay - - // With 0 random value and 1.0 jitter, the effect would be -100%, but should be capped at 0 - expect(strategy.getDelay(1)).toBe(0) - }) - - it('should be creatable through factory method', () => { - const strategy = DelayStrategyFactory.fixed({ - delayMs: 1000, - jitter: 0.1 - }) - expect(strategy).toBeInstanceOf(FixedDelayStrategy) - expect(strategy.getDelay(1)).toBe(1000) // With 0.5 mock random, no jitter effect - }) - }) - - describe('ExponentialBackoffStrategy', () => { - it('should increase delay exponentially without jitter', () => { - const initial = 1000 - const max = 60000 - const factor = 2 - const strategy = new ExponentialBackoffStrategy({ - initialDelayMs: initial, - maxDelayMs: max, - backoffFactor: factor - }) - - expect(strategy.getDelay(1)).toBe(initial) // 1000 - expect(strategy.getDelay(2)).toBe(initial * factor) // 2000 - expect(strategy.getDelay(3)).toBe(initial * Math.pow(factor, 2)) // 4000 - expect(strategy.getDelay(4)).toBe(initial * Math.pow(factor, 3)) // 8000 - }) - - it('should respect maximum delay', () => { - const strategy = new ExponentialBackoffStrategy({ - initialDelayMs: 1000, - maxDelayMs: 5000, - backoffFactor: 2 - }) - - expect(strategy.getDelay(1)).toBe(1000) - expect(strategy.getDelay(2)).toBe(2000) - expect(strategy.getDelay(3)).toBe(4000) - // Should be capped at 5000 - expect(strategy.getDelay(4)).toBe(5000) - expect(strategy.getDelay(5)).toBe(5000) - }) - - it('should apply jitter correctly', () => { - const strategy = new ExponentialBackoffStrategy({ - initialDelayMs: 1000, - maxDelayMs: 60000, - backoffFactor: 2, - jitter: 0.2 - }) - - // Mock random to return different values - randomMock.mockReturnValueOnce(0.6) // => +0.2 * delay - randomMock.mockReturnValueOnce(0.4) // => -0.2 * delay - - // First attempt: 1000ms with jitter +4% - expect(strategy.getDelay(1)).toBe(1000 + 1000 * 0.2 * 0.2) - - // Second attempt: 2000ms with jitter -4% - expect(strategy.getDelay(2)).toBe(2000 - 2000 * 0.2 * 0.2) - }) - - it('should be creatable through factory method', () => { - const strategy = DelayStrategyFactory.exponentialBackoff({ - initialDelayMs: 1000, - maxDelayMs: 60000, - backoffFactor: 2, - jitter: 0.1 - }) - expect(strategy).toBeInstanceOf(ExponentialBackoffStrategy) - expect(strategy.getDelay(1)).toBe(1000) // With 0.5 mock random, no jitter effect - expect(strategy.getDelay(2)).toBe(2000) - }) - }) - - describe('FibonacciDelayStrategy', () => { - it('should follow Fibonacci sequence without jitter', () => { - const baseDelay = 100 - const maxDelay = 10000 - const strategy = new FibonacciDelayStrategy({ - baseDelayMs: baseDelay, - maxDelayMs: maxDelay - }) - - expect(strategy.getDelay(1)).toBe(baseDelay * 1) - expect(strategy.getDelay(2)).toBe(baseDelay * 2) - expect(strategy.getDelay(3)).toBe(baseDelay * 3) - expect(strategy.getDelay(4)).toBe(baseDelay * 5) - expect(strategy.getDelay(5)).toBe(baseDelay * 8) - expect(strategy.getDelay(6)).toBe(baseDelay * 13) - }) - - it('should respect maximum delay', () => { - const strategy = new FibonacciDelayStrategy({ - baseDelayMs: 100, - maxDelayMs: 500 - }) - - expect(strategy.getDelay(1)).toBe(100) - expect(strategy.getDelay(2)).toBe(200) - expect(strategy.getDelay(3)).toBe(300) - expect(strategy.getDelay(4)).toBe(500) - expect(strategy.getDelay(5)).toBe(500) // Capped at maxDelayMs - expect(strategy.getDelay(6)).toBe(500) // Capped at maxDelayMs - }) - - it('should cache Fibonacci calculations for performance', () => { - const strategy = new FibonacciDelayStrategy({ - baseDelayMs: 100, - maxDelayMs: 10000 - }) - - // Access private cache to verify it's working - const fibCache = (strategy as any).fibCache - expect(fibCache.size).toBe(2) // Initial cache has 0->0 and 1->1 - - strategy.getDelay(7) // Should calculate fib(8) = 21 - - expect(fibCache.size).toBeGreaterThan(2) - expect(fibCache.get(8)).toBe(21) - }) - - it('should apply jitter correctly', () => { - const strategy = new FibonacciDelayStrategy({ - baseDelayMs: 100, - maxDelayMs: 10000, - jitter: 0.2 - }) - - // Mock random to return different values - randomMock.mockReturnValueOnce(0.6) // => +0.2 * delay - randomMock.mockReturnValueOnce(0.4) // => -0.2 * delay - - // First attempt: 100ms (fib(2)=1 * 100) with jitter +4% - expect(strategy.getDelay(1)).toBe(100 + 100 * 0.2 * 0.2) - - // Second attempt: 100ms (fib(3)=1 * 100) with jitter -4% - expect(strategy.getDelay(2)).toBe(200 - 200 * 0.2 * 0.2) - }) - - it('should handle large Fibonacci numbers efficiently', () => { - const strategy = new FibonacciDelayStrategy({ - baseDelayMs: 1, - maxDelayMs: Number.MAX_SAFE_INTEGER - }) - - // This would be extremely slow without memoization - const start = performance.now() - const delay = strategy.getDelay(40) // fib(41) = 165580141 - const duration = performance.now() - start - - // Should be much faster than calculating naively - expect(duration).toBeLessThan(100) - expect(delay).toBe(165580141) // fib(41) * 1 - }) - - it('should be creatable through factory method', () => { - const strategy = DelayStrategyFactory.fibonacci({ - baseDelayMs: 100, - maxDelayMs: 10000, - jitter: 0.1 - }) - expect(strategy).toBeInstanceOf(FibonacciDelayStrategy) - expect(strategy.getDelay(1)).toBe(100) - expect(strategy.getDelay(2)).toBe(200) - expect(strategy.getDelay(3)).toBe(300) - expect(strategy.getDelay(4)).toBe(500) - }) - }) - - describe('DelayStrategyFactory', () => { - it('should create strategies with correct types', () => { - expect( - DelayStrategyFactory.fixed({ - delayMs: 1000 - }) - ).toBeInstanceOf(FixedDelayStrategy) - - expect( - DelayStrategyFactory.exponentialBackoff({ - initialDelayMs: 1000, - maxDelayMs: 60000, - backoffFactor: 2 - }) - ).toBeInstanceOf(ExponentialBackoffStrategy) - - expect( - DelayStrategyFactory.fibonacci({ - baseDelayMs: 100, - maxDelayMs: 10000 - }) - ).toBeInstanceOf(FibonacciDelayStrategy) - }) - - it('should pass parameters correctly to strategies', () => { - const fixed = DelayStrategyFactory.fixed({ - delayMs: 1000, - jitter: 0.1 - }) as FixedDelayStrategy - expect((fixed as any).delayMs).toBe(1000) - expect((fixed as any).jitter).toBe(0.1) - - const exponential = DelayStrategyFactory.exponentialBackoff({ - initialDelayMs: 1000, - maxDelayMs: 60000, - backoffFactor: 2.5, - jitter: 0.2 - }) as ExponentialBackoffStrategy - expect((exponential as any).initialDelayMs).toBe(1000) - expect((exponential as any).maxDelayMs).toBe(60000) - expect((exponential as any).backoffFactor).toBe(2.5) - expect((exponential as any).jitter).toBe(0.2) - - const fibonacci = DelayStrategyFactory.fibonacci({ - baseDelayMs: 100, - maxDelayMs: 10000, - jitter: 0.15 - }) as FibonacciDelayStrategy - expect((fibonacci as any).baseDelayMs).toBe(100) - expect((fibonacci as any).maxDelayMs).toBe(10000) - expect((fibonacci as any).jitter).toBe(0.15) - }) - }) -}) diff --git a/foundations/core/packages/retry/src/__test__/retry.test.ts b/foundations/core/packages/retry/src/__test__/retry.test.ts deleted file mode 100644 index 7118520e46..0000000000 --- a/foundations/core/packages/retry/src/__test__/retry.test.ts +++ /dev/null @@ -1,560 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { DelayStrategyFactory } from '../delay' -import { withRetry, createRetryableFunction, type RetryOptions } from '../retry' -import { type IsRetryable, retryAllErrors } from '../retryable' - -// Mock the sleep function to speed up tests -jest.mock('../delay', () => { - const originalModule = jest.requireActual('../delay') - return { - ...originalModule, - // Override the internal sleep function to resolve immediately - sleep: jest.fn().mockImplementation(() => Promise.resolve()) - } -}) - -describe('withRetry', () => { - // Create a mock logger to capture logs - const mockLogger = { - warn: jest.fn(), - error: jest.fn(), - info: jest.fn() - } - - // Use the new delayStrategy option - const mockOptions: Partial = { - maxRetries: 3, - delayStrategy: DelayStrategyFactory.exponentialBackoff({ - initialDelayMs: 10, - maxDelayMs: 100, - backoffFactor: 2, - jitter: 0 - }), - logger: mockLogger, - isRetryable: retryAllErrors - } - - beforeEach(() => { - jest.clearAllMocks() - }) - - it('should return the result when operation succeeds on first try', async () => { - const mockOperation = jest.fn().mockResolvedValue('success') - - const result = await withRetry(mockOperation, mockOptions) - - expect(result).toBe('success') - expect(mockOperation).toHaveBeenCalledTimes(1) - expect(mockLogger.warn).not.toHaveBeenCalled() - expect(mockLogger.error).not.toHaveBeenCalled() - }) - - it('should retry when operation fails and eventually succeed', async () => { - const mockOperation = jest - .fn() - .mockRejectedValueOnce(new Error('first failure')) - .mockRejectedValueOnce(new Error('second failure')) - .mockResolvedValueOnce('success after retries') - - const result = await withRetry(mockOperation, mockOptions) - - expect(result).toBe('success after retries') - expect(mockOperation).toHaveBeenCalledTimes(3) - expect(mockLogger.warn).toHaveBeenCalledTimes(2) - expect(mockLogger.error).not.toHaveBeenCalled() - }) - - it('should throw an error after maximum retries are exhausted', async () => { - const mockError = new Error('persistent failure') - const mockOperation = jest.fn().mockRejectedValue(mockError) - - await expect(withRetry(mockOperation, mockOptions)).rejects.toThrow('persistent failure') - - expect(mockOperation).toHaveBeenCalledTimes(mockOptions.maxRetries ?? -1) - expect(mockLogger.error).toHaveBeenCalledTimes(1) - }) - - it('should use default options when none are provided', async () => { - const mockOperation = jest.fn().mockResolvedValue('success') - - const result = await withRetry(mockOperation) - - expect(result).toBe('success') - expect(mockOperation).toHaveBeenCalledTimes(1) - }) - - it('should use provided operation name in log messages', async () => { - const mockOperation = jest.fn().mockRejectedValueOnce(new Error('failure')).mockResolvedValueOnce('success') - - await withRetry(mockOperation, mockOptions, 'custom-operation') - - expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('custom-operation failed'), expect.any(Object)) - }) - - it('should apply jitter to delay calculation', async () => { - const mockOperation = jest.fn().mockRejectedValueOnce(new Error('failure')).mockResolvedValueOnce('success') - - // Use Math.random mock to make jitter predictable - const mockRandom = jest.spyOn(Math, 'random').mockReturnValue(0.5) - - // Create options with jitter enabled - const jitterOptions = { - ...mockOptions, - delayStrategy: DelayStrategyFactory.exponentialBackoff({ - initialDelayMs: 10, - maxDelayMs: 100, - backoffFactor: 2, - jitter: 0.2 - }) - } - - await withRetry(mockOperation, jitterOptions) - - // With Math.random = 0.5, jitter should be 0 - // (since 0.5 * 2 - 1 = 0) - expect(mockLogger.warn).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ delayMs: 10 }) // Should still be the initial delay - ) - - mockRandom.mockRestore() - }) - - it('should respect maximum delay', async () => { - const mockOperation = jest - .fn() - .mockRejectedValueOnce(new Error('failure 1')) - .mockRejectedValueOnce(new Error('failure 2')) - .mockRejectedValueOnce(new Error('failure 3')) - .mockResolvedValueOnce('success') - - // Use high backoff factor to test maximum delay cap - const maxDelayOptions = { - maxRetries: 4, - delayStrategy: DelayStrategyFactory.exponentialBackoff({ - initialDelayMs: 50, - maxDelayMs: 1000, - backoffFactor: 10 // Would normally go 50 -> 500 -> 5000, but should cap at 1000 - }), - isRetryable: retryAllErrors, - logger: mockLogger - } - - await withRetry(mockOperation, maxDelayOptions) - - // Check that delays are correctly calculated and capped - expect(mockLogger.warn).toHaveBeenNthCalledWith( - 1, - expect.any(String), - expect.objectContaining({ delayMs: expect.any(Number) }) - ) - - // Second retry delay - expect(mockLogger.warn).toHaveBeenNthCalledWith( - 2, - expect.any(String), - expect.objectContaining({ delayMs: 500 }) // 50 * 10 = 500 - ) - - // Third retry delay (should be capped) - expect(mockLogger.warn).toHaveBeenNthCalledWith( - 3, - expect.any(String), - expect.objectContaining({ delayMs: 1000 }) // 500 * 10 = 5000, capped at 1000 - ) - - // Function should have been called 4 times total - expect(mockOperation).toHaveBeenCalledTimes(4) - }) - - it('should work with different delay strategies', async () => { - // Test with fixed delay - const fixedDelayOperation = jest - .fn() - .mockRejectedValueOnce(new Error('failure 1')) - .mockRejectedValueOnce(new Error('failure 2')) - .mockResolvedValueOnce('success') - - const fixedDelayOptions = { - maxRetries: 3, - delayStrategy: DelayStrategyFactory.fixed({ - delayMs: 200, - jitter: 0 - }), - isRetryable: retryAllErrors, - logger: mockLogger - } - - await withRetry(fixedDelayOperation, fixedDelayOptions) - - // Both retries should have the same delay - expect(mockLogger.warn).toHaveBeenNthCalledWith(1, expect.any(String), expect.objectContaining({ delayMs: 200 })) - expect(mockLogger.warn).toHaveBeenNthCalledWith(2, expect.any(String), expect.objectContaining({ delayMs: 200 })) - - // Clear mocks for next test - jest.clearAllMocks() - - // Test with Fibonacci delay - const fibonacciDelayOperation = jest - .fn() - .mockRejectedValueOnce(new Error('failure 1')) - .mockRejectedValueOnce(new Error('failure 2')) - .mockResolvedValueOnce('success') - - const fibonacciDelayOptions = { - maxRetries: 3, - delayStrategy: DelayStrategyFactory.fibonacci({ - baseDelayMs: 100, - maxDelayMs: 10000, - jitter: 0 - }), - isRetryable: retryAllErrors, - logger: mockLogger - } - - await withRetry(fibonacciDelayOperation, fibonacciDelayOptions) - - // Delays should follow Fibonacci sequence - expect(mockLogger.warn).toHaveBeenNthCalledWith( - 1, - expect.any(String), - expect.objectContaining({ delayMs: 100 }) // fib(2) = 1 * 100 - ) - expect(mockLogger.warn).toHaveBeenNthCalledWith( - 2, - expect.any(String), - expect.objectContaining({ delayMs: 200 }) // fib(3) = 2 * 100 - ) - }) -}) - -describe('createRetryableFunction', () => { - const mockLogger = { - warn: jest.fn(), - error: jest.fn(), - info: jest.fn() - } - - const mockOptions: Partial = { - maxRetries: 2, - delayStrategy: DelayStrategyFactory.exponentialBackoff({ - initialDelayMs: 10, - maxDelayMs: 100, - backoffFactor: 2 - }), - isRetryable: retryAllErrors, - logger: mockLogger - } - - beforeEach(() => { - jest.clearAllMocks() - }) - - it('should create a function that applies retry logic', async () => { - const mockFn = jest.fn().mockRejectedValueOnce(new Error('first failure')).mockResolvedValueOnce('success') - - const retryableFn = createRetryableFunction(mockFn, mockOptions) - - const result = await retryableFn('arg1', 123) - - expect(result).toBe('success') - expect(mockFn).toHaveBeenCalledTimes(2) - expect(mockFn).toHaveBeenCalledWith('arg1', 123) - expect(mockLogger.warn).toHaveBeenCalledTimes(1) - }) - - it('should pass through function parameters correctly', async () => { - const mockFn = jest.fn().mockResolvedValue('success') - - const retryableFn = createRetryableFunction(mockFn, mockOptions) - - await retryableFn('arg1', 123, { complex: true }) - - expect(mockFn).toHaveBeenCalledWith('arg1', 123, { complex: true }) - }) - - it('should use custom operation name in logs', async () => { - const mockFn = jest.fn().mockRejectedValueOnce(new Error('failure')).mockResolvedValueOnce('success') - - const retryableFn = createRetryableFunction(mockFn, mockOptions, 'custom-function') - - await retryableFn() - - expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('custom-function failed'), expect.any(Object)) - }) - - it('should propagate the final error if all retries fail', async () => { - const mockError = new Error('network error') - const mockFn = jest.fn().mockRejectedValue(mockError) - - const retryableFn = createRetryableFunction(mockFn, mockOptions) - - await expect(retryableFn()).rejects.toThrow('network error') - - expect(mockOptions.maxRetries).toBeDefined() - expect(mockFn).toHaveBeenCalledTimes(mockOptions.maxRetries ?? -1) - }) -}) - -// Test with a decorated class -describe('Using retry in class methods', () => { - class TestService { - counter = 0 - - async unstableFunction (): Promise { - this.counter++ - if (this.counter < 3) { - throw new Error(`network error ${this.counter}`) - } - return 'success' - } - } - - it('should work with instance methods', async () => { - const service = new TestService() - - // Create a retryable version of the method that's bound to the service - const retryableMethod = createRetryableFunction(service.unstableFunction.bind(service), { - maxRetries: 3, - delayStrategy: DelayStrategyFactory.fixed({ - delayMs: 10 - }) - }) - - const result = await retryableMethod() - - expect(result).toBe('success') - expect(service.counter).toBe(3) - }) -}) - -describe('withRetry with isRetryable option', () => { - // Create a mock logger to capture logs - const mockLogger = { - warn: jest.fn(), - error: jest.fn(), - info: jest.fn() - } - - beforeEach(() => { - jest.clearAllMocks() - // Mock the sleep function to speed up tests - jest.spyOn(global, 'setTimeout').mockImplementation((fn: any) => { - fn() - return 1 as any - }) - }) - - it('should retry errors that are marked as retriable', async () => { - // Custom isRetryable function that only retries certain errors - const customRetriable: IsRetryable = (err: any) => { - return err.message.includes('retriable') - } - - const mockOperation = jest - .fn() - .mockRejectedValueOnce(new Error('This is a retriable error')) - .mockRejectedValueOnce(new Error('This is a retriable error again')) - .mockResolvedValueOnce('success') - - const result = await withRetry(mockOperation, { - maxRetries: 5, - logger: mockLogger, - delayStrategy: DelayStrategyFactory.fixed({ - delayMs: 10 - }), - isRetryable: customRetriable - }) - - expect(result).toBe('success') - expect(mockOperation).toHaveBeenCalledTimes(3) - expect(mockLogger.warn).toHaveBeenCalledTimes(2) - expect(mockLogger.error).not.toHaveBeenCalled() - }) - - it('should not retry errors that are not marked as retriable', async () => { - // Custom isRetryable function that never retries - const neverRetry: IsRetryable = (_err: any) => { - return false - } - - const mockOperation = jest - .fn() - .mockRejectedValueOnce(new Error('This error should not be retried')) - .mockResolvedValueOnce('success') - - await expect( - withRetry(mockOperation, { - maxRetries: 5, - logger: mockLogger, - delayStrategy: DelayStrategyFactory.fixed({ - delayMs: 10 - }), - isRetryable: neverRetry - }) - ).rejects.toThrow('This error should not be retried') - - expect(mockOperation).toHaveBeenCalledTimes(1) - expect(mockLogger.warn).not.toHaveBeenCalled() - expect(mockLogger.error).toHaveBeenCalledWith( - expect.stringContaining('failed with non-retriable error'), - expect.any(Object) - ) - }) - - it('should have different behavior for different error types', async () => { - // Custom isRetryable function that retries only NetworkErrors - const retryOnlyNetworkErrors: IsRetryable = (err: any) => { - return err.name === 'NetworkError' - } - - // Create different error types - const networkError = new Error('Network failed') - networkError.name = 'NetworkError' - - const validationError = new Error('Validation failed') - validationError.name = 'ValidationError' - - // First test with network error - should be retried - const mockNetworkOp = jest.fn().mockRejectedValueOnce(networkError).mockResolvedValueOnce('network success') - - const result1 = await withRetry(mockNetworkOp, { - maxRetries: 3, - logger: mockLogger, - delayStrategy: DelayStrategyFactory.fixed({ - delayMs: 10 - }), - isRetryable: retryOnlyNetworkErrors - }) - - expect(result1).toBe('network success') - expect(mockNetworkOp).toHaveBeenCalledTimes(2) - - // Reset mocks - jest.clearAllMocks() - - // Then test with validation error - should not be retried - const mockValidationOp = jest.fn().mockRejectedValueOnce(validationError) - - await expect( - withRetry(mockValidationOp, { - maxRetries: 3, - logger: mockLogger, - delayStrategy: DelayStrategyFactory.fixed({ - delayMs: 10 - }), - isRetryable: retryOnlyNetworkErrors - }) - ).rejects.toThrow('Validation failed') - - expect(mockValidationOp).toHaveBeenCalledTimes(1) - expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining('non-retriable error'), expect.any(Object)) - }) - - it('should use the default retryNetworkErrors if isRetryable is not provided', async () => { - // All errors should be retried by default - const mockOperation = jest - .fn() - .mockRejectedValueOnce(new Error('unreachable')) - .mockRejectedValueOnce(new Error('ECONNREFUSED')) - .mockResolvedValueOnce('success') - - const result = await withRetry(mockOperation, { - maxRetries: 5, - logger: mockLogger, - delayStrategy: DelayStrategyFactory.fixed({ - delayMs: 10 - }) - // isRetryable not provided, should use default - }) - - expect(result).toBe('success') - expect(mockOperation).toHaveBeenCalledTimes(3) - expect(mockLogger.warn).toHaveBeenCalledTimes(2) - }) - - it('should pass the error to isRetryable function', async () => { - // Mock isRetryable function to track calls - const mockisRetryable = jest.fn().mockReturnValue(true) - - const testError = new Error('test error') - const mockOperation = jest.fn().mockRejectedValueOnce(testError).mockResolvedValueOnce('success') - - await withRetry(mockOperation, { - maxRetries: 3, - logger: mockLogger, - delayStrategy: DelayStrategyFactory.fixed({ - delayMs: 10 - }), - isRetryable: mockisRetryable - }) - - // Verify isRetryable was called with the actual error - expect(mockisRetryable).toHaveBeenCalledTimes(1) - expect(mockisRetryable).toHaveBeenCalledWith(testError) - }) -}) - -describe('createRetryableFunction with isRetryable', () => { - const mockLogger = { - warn: jest.fn(), - error: jest.fn(), - info: jest.fn() - } - - beforeEach(() => { - jest.clearAllMocks() - // Mock the sleep function to speed up tests - jest.spyOn(global, 'setTimeout').mockImplementation((fn: any) => { - fn() - return 1 as any - }) - }) - - it('should respect isRetryable when applied to a function', async () => { - // Function to wrap - const unstableFunction = jest - .fn() - .mockRejectedValueOnce(new Error('retriable network error')) - .mockRejectedValueOnce(new Error('retriable network error')) - .mockResolvedValueOnce('success') - - // Custom isRetryable that only retries network errors - const customRetriable: IsRetryable = (err: any) => { - return err.message.includes('network') - } - - // Create retryable version with custom isRetryable - const retryableFunction = createRetryableFunction( - unstableFunction, - { - maxRetries: 3, - logger: mockLogger, - delayStrategy: DelayStrategyFactory.exponentialBackoff({ - initialDelayMs: 10, - maxDelayMs: 100, - backoffFactor: 2 - }), - isRetryable: customRetriable - }, - 'custom-operation' - ) - - const result = await retryableFunction() - expect(result).toBe('success') - }) -}) diff --git a/foundations/core/packages/retry/src/__test__/retryable.test.ts b/foundations/core/packages/retry/src/__test__/retryable.test.ts deleted file mode 100644 index 8814cc8008..0000000000 --- a/foundations/core/packages/retry/src/__test__/retryable.test.ts +++ /dev/null @@ -1,158 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { retryAllErrors, retryNetworkErrors } from '../retryable' - -describe('retryAllErrors', () => { - it('should return true for any error', () => { - expect(retryAllErrors(new Error('any error'))).toBe(true) - expect(retryAllErrors(new TypeError('type error'))).toBe(true) - expect(retryAllErrors(null)).toBe(true) - expect(retryAllErrors(undefined)).toBe(true) - expect(retryAllErrors({ custom: 'error object' })).toBe(true) - expect(retryAllErrors('error string')).toBe(true) - }) -}) - -describe('retryNetworkErrors', () => { - it('should return false for null or undefined', () => { - expect(retryNetworkErrors(null)).toBe(false) - expect(retryNetworkErrors(undefined)).toBe(false) - }) - - it('should return true for errors with network-related names', () => { - const networkErrorNames = [ - 'NetworkError', - 'FetchError', - 'AbortError', - 'TimeoutError', - 'ConnectionError', - 'ConnectionRefusedError', - 'ETIMEDOUT', - 'ECONNREFUSED', - 'ECONNRESET', - 'ENOTFOUND', - 'EAI_AGAIN' - ] - - networkErrorNames.forEach((name) => { - // Create an error with the specified name - const error = new Error('Test error') - error.name = name - expect(retryNetworkErrors(error)).toBe(true) - }) - }) - - it('should return true for errors with network-related message patterns', () => { - const networkErrorMessages = [ - 'Network error occurred', - 'Connection timed out', - 'Connection refused', - 'Connection reset', - 'Socket hang up', - 'DNS lookup failed', - 'getaddrinfo ENOTFOUND api.example.com', - 'connect ECONNREFUSED 127.0.0.1:8080', - 'read ECONNRESET', - 'connect ETIMEDOUT 192.168.1.1:443', - 'getaddrinfo EAI_AGAIN myserver.local' - ] - - networkErrorMessages.forEach((message) => { - expect(retryNetworkErrors(new Error(message))).toBe(true) - }) - }) - - it('should return false for non-network related errors', () => { - const nonNetworkErrors = [ - new Error('Invalid input'), - new TypeError('Cannot read property of undefined'), - new RangeError('Value out of range'), - new SyntaxError('Unexpected token'), - new Error('File not found'), - new Error('Permission denied'), - new Error('Invalid state') - ] - - nonNetworkErrors.forEach((error) => { - expect(retryNetworkErrors(error)).toBe(false) - }) - }) - - it('should return true for errors with server error status codes (5xx)', () => { - const serverErrors = [ - createErrorWithStatus(500), - createErrorWithStatus(501), - createErrorWithStatus(502), - createErrorWithStatus(503), - createErrorWithStatus(504), - createErrorWithStatus(599) - ] - - serverErrors.forEach((error) => { - expect(retryNetworkErrors(error)).toBe(true) - }) - }) - - it('should return true for specific client error status codes', () => { - const retriableClientErrors = [ - createErrorWithStatus(408), // Request Timeout - createErrorWithStatus(423), // Locked - createErrorWithStatus(425), // Too Early - createErrorWithStatus(429), // Too Many Requests - createErrorWithStatus(449) // Retry With - ] - - retriableClientErrors.forEach((error) => { - expect(retryNetworkErrors(error)).toBe(true) - }) - }) - - it('should return false for non-retriable client error status codes', () => { - const nonRetriableClientErrors = [ - createErrorWithStatus(400), // Bad Request - createErrorWithStatus(401), // Unauthorized - createErrorWithStatus(403), // Forbidden - createErrorWithStatus(404), // Not Found - createErrorWithStatus(422) // Unprocessable Entity - ] - - nonRetriableClientErrors.forEach((error) => { - expect(retryNetworkErrors(error)).toBe(false) - }) - }) - - it('should return false for non-Error objects without network-related properties', () => { - const nonNetworkErrorObjects = [ - { code: 'INVALID_INPUT' }, - { code: 'AUTH_FAILED' }, - { message: 'Invalid credentials' }, - { error: 'Not found' } - ] - - nonNetworkErrorObjects.forEach((errorObj) => { - expect(retryNetworkErrors(errorObj)).toBe(false) - }) - }) -}) - -/** - * Helper function to create an Error object with a status property - */ -function createErrorWithStatus (status: number): Error { - const error: any = new Error(`HTTP Error ${status}`) - error.status = status - return error -} diff --git a/foundations/core/packages/retry/src/decorator.ts b/foundations/core/packages/retry/src/decorator.ts deleted file mode 100644 index cb6080e0ae..0000000000 --- a/foundations/core/packages/retry/src/decorator.ts +++ /dev/null @@ -1,35 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// -import { type RetryOptions, DEFAULT_RETRY_OPTIONS, withRetry } from './retry' - -/** - * Method decorator for adding retry functionality to class methods - * - * @param options - Retry configuration options - * @param operationName - Name of the operation for logging (defaults to method name) - * @returns Method decorator - */ -export function Retryable (options: Partial = DEFAULT_RETRY_OPTIONS): MethodDecorator { - return function (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) { - const originalMethod = descriptor.value as (...args: any[]) => any - - descriptor.value = async function (...args: any[]) { - const methodName = propertyKey.toString() - return await withRetry(() => originalMethod.apply(this, args), options, methodName) - } - - return descriptor - } -} diff --git a/foundations/core/packages/retry/src/delay.ts b/foundations/core/packages/retry/src/delay.ts deleted file mode 100644 index 5adbd17d36..0000000000 --- a/foundations/core/packages/retry/src/delay.ts +++ /dev/null @@ -1,171 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export const DelayStrategyFactory = { - /** - * Create a fixed delay strategy - */ - fixed (options: FixedDelayOptions): DelayStrategy { - return new FixedDelayStrategy(options) - }, - - /** - * Create an exponential backoff delay strategy - */ - exponentialBackoff (options: ExponentialBackoffOptions): DelayStrategy { - return new ExponentialBackoffStrategy(options) - }, - - /** - * Create a Fibonacci delay strategy - */ - fibonacci (options: FibonacciDelayOptions): DelayStrategy { - return new FibonacciDelayStrategy(options) - } -} - -export interface DelayStrategy { - getDelay: (attempt: number) => number -} - -export interface FixedDelayOptions { - /** Delay between retries in milliseconds */ - delayMs: number - /** Optional jitter factor (0-1) to add randomness to delay times */ - jitter?: number -} - -/** - * Fixed delay strategy - uses the same delay for all retry attempts - */ -export class FixedDelayStrategy implements DelayStrategy { - private readonly delayMs: number - private readonly jitter: number - - constructor (options: FixedDelayOptions) { - this.delayMs = options.delayMs - this.jitter = options.jitter ?? 0 - } - - getDelay (_attempt: number): number { - if (this.jitter > 0) { - const jitterAmount = this.delayMs * this.jitter * (Math.random() * 2 - 1) - return Math.max(0, this.delayMs + jitterAmount) - } - return this.delayMs - } -} - -export interface ExponentialBackoffOptions { - /** Initial delay between retries in milliseconds */ - initialDelayMs: number - /** Maximum delay between retries in milliseconds */ - maxDelayMs: number - /** Backoff factor for exponential delay increase */ - backoffFactor: number - /** Optional jitter factor (0-1) to add randomness to delay times */ - jitter?: number -} - -/** - * Exponential backoff delay strategy - increases delay exponentially with each attempt - */ -export class ExponentialBackoffStrategy implements DelayStrategy { - private readonly initialDelayMs: number - private readonly maxDelayMs: number - private readonly backoffFactor: number - private readonly jitter: number - - constructor (options: ExponentialBackoffOptions) { - this.initialDelayMs = options.initialDelayMs - this.maxDelayMs = options.maxDelayMs - this.backoffFactor = options.backoffFactor - this.jitter = options.jitter ?? 0 - } - - getDelay (attempt: number): number { - const baseDelay = Math.min(this.initialDelayMs * Math.pow(this.backoffFactor, attempt - 1), this.maxDelayMs) - - if (this.jitter > 0) { - const jitterAmount = baseDelay * this.jitter * (Math.random() * 2 - 1) - return Math.min(Math.max(0, baseDelay + jitterAmount), this.maxDelayMs) - } - - return baseDelay - } -} - -export interface FibonacciDelayOptions { - /** Base unit for calculating Fibonacci sequence in milliseconds */ - baseDelayMs: number - /** Maximum delay between retries in milliseconds */ - maxDelayMs: number - /** Optional jitter factor (0-1) to add randomness to delay times */ - jitter?: number -} - -export class FibonacciDelayStrategy implements DelayStrategy { - private readonly baseDelayMs: number - private readonly maxDelayMs: number - private readonly jitter: number - - // Cache for Fibonacci numbers to improve performance - private readonly fibCache: Map - - constructor (options: FibonacciDelayOptions) { - this.baseDelayMs = options.baseDelayMs - this.maxDelayMs = options.maxDelayMs - this.jitter = options.jitter ?? 0 - this.fibCache = new Map([ - [0, 0], - [1, 1] - ]) - } - - private fibonacci (n: number): number { - // Return from cache if available - if (this.fibCache.has(n)) { - return this.fibCache.get(n) as number - } - - if (n <= 1) { - return n - } - - // Calculate using recursion with memoization - const result = this.fibonacci(n - 1) + this.fibonacci(n - 2) - this.fibCache.set(n, result) - return result - } - - getDelay (attempt: number): number { - const fibNumber = this.fibonacci(attempt + 1) - const baseDelay = Math.min(fibNumber * this.baseDelayMs, this.maxDelayMs) - - if (this.jitter > 0) { - const jitterAmount = baseDelay * this.jitter * (Math.random() * 2 - 1) - return Math.min(Math.max(0, baseDelay + jitterAmount), this.maxDelayMs) - } - - return baseDelay - } -} - -/** - * Promise-based sleep function - */ -export function sleep (ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)) -} diff --git a/foundations/core/packages/retry/src/index.ts b/foundations/core/packages/retry/src/index.ts deleted file mode 100644 index 3ce12b411b..0000000000 --- a/foundations/core/packages/retry/src/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export * from './retry' -export * from './decorator' -export * from './retryable' diff --git a/foundations/core/packages/retry/src/logger.ts b/foundations/core/packages/retry/src/logger.ts deleted file mode 100644 index 6bf7b99066..0000000000 --- a/foundations/core/packages/retry/src/logger.ts +++ /dev/null @@ -1,31 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// -export interface Logger { - warn: (message: string, meta?: Record) => void - error: (message: string, meta?: Record) => void - info: (message: string, meta?: Record) => void -} - -export const defaultLogger: Logger = { - warn: (message: string, meta?: Record) => { - console.warn(`[WARN] ${message}`, meta) - }, - error: (message: string, meta?: Record) => { - console.error(`[ERROR] ${message}`, meta) - }, - info: (message: string, meta?: Record) => { - console.info(`[INFO] ${message}`, meta) - } -} diff --git a/foundations/core/packages/retry/src/retry.ts b/foundations/core/packages/retry/src/retry.ts deleted file mode 100644 index 692b602660..0000000000 --- a/foundations/core/packages/retry/src/retry.ts +++ /dev/null @@ -1,128 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// -import { defaultLogger, type Logger } from './logger' -import { type IsRetryable, retryNetworkErrors } from './retryable' -import { type DelayStrategy, DelayStrategyFactory, sleep } from './delay' - -/** - * Configuration options for the retry mechanism - */ -export interface RetryOptions { - /** Maximum number of retry attempts */ - maxRetries: number - /** Function to determine if an error is retriable */ - isRetryable: IsRetryable - /** Strategy for calculating delay between retries */ - delayStrategy: DelayStrategy - /** Logger to use (defaults to console logger) */ - logger?: Logger -} - -/** - * Default retry options - */ -export const DEFAULT_RETRY_OPTIONS: RetryOptions = { - maxRetries: 5, - isRetryable: retryNetworkErrors, - delayStrategy: DelayStrategyFactory.exponentialBackoff({ - initialDelayMs: 1000, - maxDelayMs: 30000, - backoffFactor: 1.5, - jitter: 0.2 - }), - logger: defaultLogger -} - -/** - * Executes an operation with exponential backoff retry - * - * @param operation - Async operation to execute - * @param options - Retry configuration options - * @param operationName - Name of the operation for logging - * @returns The result of the operation - * @throws The last error encountered after all retries have been exhausted - */ -export async function withRetry ( - operation: () => Promise, - options: Partial = {}, - operationName: string = 'operation' -): Promise { - const config: RetryOptions = { ...DEFAULT_RETRY_OPTIONS, ...options } - const logger = config.logger ?? defaultLogger - let attempt = 1 - let lastError: Error | undefined - - while (attempt <= config.maxRetries) { - try { - return await operation() - } catch (error: any) { - lastError = error - const isLastAttempt = attempt >= config.maxRetries - - if (isLastAttempt) { - logger.error(`${operationName} failed after ${attempt} attempts`, { - error, - attempt, - maxRetries: config.maxRetries - }) - throw error - } - if (!config.isRetryable(error)) { - logger.error(`${operationName} failed with non-retriable error`, { - error, - attempt, - maxRetries: config.maxRetries - }) - throw error - } - - // Get delay for next attempt from strategy - const delayMs = Math.round(config.delayStrategy.getDelay(attempt)) - - logger.warn(`${operationName} failed, retrying in ${delayMs}ms`, { - error, - attempt, - nextAttempt: attempt + 1, - delayMs - }) - - // Wait before retry - await sleep(delayMs) - attempt++ - } - } - - // This should not be reached due to the throw in the last iteration - throw lastError ?? new Error(`${operationName} failed for unknown reason`) -} - -/** - * Creates a retryable function from a base function - * Returns a wrapped function that will apply retry logic automatically - * - * @param fn - The function to make retryable - * @param options - Retry configuration options - * @param operationName - Name of the operation for logging - * @returns A wrapped function that applies retry logic - */ -export function createRetryableFunction Promise> ( - fn: T, - options: Partial = {}, - operationName: string = 'operation' -): T { - return (async (...args: Parameters): Promise> => { - return await (withRetry(() => fn(...args), options, operationName) as Promise>) - }) as T -} diff --git a/foundations/core/packages/retry/src/retryable.ts b/foundations/core/packages/retry/src/retryable.ts deleted file mode 100644 index d7e5ac2efb..0000000000 --- a/foundations/core/packages/retry/src/retryable.ts +++ /dev/null @@ -1,96 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export type IsRetryable = (error: Error | unknown) => boolean - -export const retryAllErrors: IsRetryable = (_error: Error | unknown): boolean => { - return true -} - -const NETWORK_ERROR_NAMES = new Set([ - 'NetworkError', - 'FetchError', - 'AbortError', - 'TimeoutError', - 'ConnectionError', - 'ConnectionRefusedError', - 'ETIMEDOUT', - 'ECONNREFUSED', - 'ECONNRESET', - 'ENOTFOUND', - 'EAI_AGAIN' -]) - -/** - * Patterns in error messages that suggest network issues - */ -const NETWORK_ERROR_PATTERNS = [ - /network/i, - /connection/i, - /timeout/i, - /unreachable/i, - /refused/i, - /reset/i, - /socket/i, - /DNS/i, - /ENOTFOUND/, - /ECONNREFUSED/, - /ECONNRESET/, - /ETIMEDOUT/, - /EAI_AGAIN/ -] - -/** - * Determine if an error is related to network issues - */ -export const retryNetworkErrors: IsRetryable = (error: Error | unknown): boolean => { - if (error == null) { - return false - } - - // Check for error name - if (error instanceof Error) { - // Check if the error name is in our set of network errors - if (NETWORK_ERROR_NAMES.has(error.name)) { - return true - } - - // Check if the error message matches our network error patterns - for (const pattern of NETWORK_ERROR_PATTERNS) { - if (pattern.test(error.message)) { - return true - } - } - - // Check for status codes in response errors - if ('status' in error && typeof (error as any).status === 'number') { - const status = (error as any).status - // Retry server errors (5xx) and some specific client errors - return ( - (status >= 500 && status < 600) || // Server errors - status === 429 || // Too Many Requests - status === 408 || // Request Timeout - status === 423 || // Locked - status === 425 || // Too Early - status === 449 || // Retry With - status === 503 || // Service Unavailable - status === 504 // Gateway Timeout - ) - } - } - - // If we couldn't identify it as a network error, don't retry - return false -} diff --git a/foundations/core/packages/retry/tsconfig.json b/foundations/core/packages/retry/tsconfig.json deleted file mode 100644 index c6a877cf6c..0000000000 --- a/foundations/core/packages/retry/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/node/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/foundations/core/packages/rpc/.eslintrc.js b/foundations/core/packages/rpc/.eslintrc.js deleted file mode 100644 index 72235dc283..0000000000 --- a/foundations/core/packages/rpc/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/foundations/core/packages/rpc/.npmignore b/foundations/core/packages/rpc/.npmignore deleted file mode 100644 index e3ec093c38..0000000000 --- a/foundations/core/packages/rpc/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!/lib/** -!CHANGELOG.md -/lib/**/__tests__/ diff --git a/foundations/core/packages/rpc/CHANGELOG.json b/foundations/core/packages/rpc/CHANGELOG.json deleted file mode 100644 index e5c5509e80..0000000000 --- a/foundations/core/packages/rpc/CHANGELOG.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "name": "@hcengineering/rpc", - "entries": [ - { - "version": "0.7.17", - "tag": "@hcengineering/rpc_v0.7.17", - "date": "Mon, 27 Oct 2025 13:27:12 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" - } - ] - } - }, - { - "version": "0.7.5", - "tag": "@hcengineering/rpc_v0.7.5", - "date": "Tue, 14 Oct 2025 04:58:17 GMT", - "comments": { - "patch": [ - { - "comment": "update deps" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`" - }, - { - "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.4` to `0.7.5`" - } - ] - } - }, - { - "version": "0.7.4", - "tag": "@hcengineering/rpc_v0.7.4", - "date": "Sat, 11 Oct 2025 18:20:33 GMT", - "comments": { - "patch": [ - { - "comment": "Update to latest platform rig" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`" - }, - { - "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.3` to `0.7.4`" - } - ] - } - }, - { - "version": "0.7.3", - "tag": "@hcengineering/rpc_v0.7.3", - "date": "Wed, 08 Oct 2025 03:40:53 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`" - } - ] - } - } - ] -} diff --git a/foundations/core/packages/rpc/CHANGELOG.md b/foundations/core/packages/rpc/CHANGELOG.md deleted file mode 100644 index 719a9af13e..0000000000 --- a/foundations/core/packages/rpc/CHANGELOG.md +++ /dev/null @@ -1,28 +0,0 @@ -# Change Log - @hcengineering/rpc - -This log was last generated on Mon, 27 Oct 2025 13:27:12 GMT and should not be manually modified. - -## 0.7.17 -Mon, 27 Oct 2025 13:27:12 GMT - -_Version update only_ - -## 0.7.5 -Tue, 14 Oct 2025 04:58:17 GMT - -### Patches - -- update deps - -## 0.7.4 -Sat, 11 Oct 2025 18:20:33 GMT - -### Patches - -- Update to latest platform rig - -## 0.7.3 -Wed, 08 Oct 2025 03:40:53 GMT - -_Initial release_ - diff --git a/foundations/core/packages/rpc/config/rig.json b/foundations/core/packages/rpc/config/rig.json deleted file mode 100644 index 0110930f55..0000000000 --- a/foundations/core/packages/rpc/config/rig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig" -} diff --git a/foundations/core/packages/rpc/jest.config.js b/foundations/core/packages/rpc/jest.config.js deleted file mode 100644 index 069ea8aa0d..0000000000 --- a/foundations/core/packages/rpc/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ['./src'], - coverageReporters: ['text-summary', 'html', 'lcov'] -} diff --git a/foundations/core/packages/rpc/package.json b/foundations/core/packages/rpc/package.json deleted file mode 100644 index 972ecc2e59..0000000000 --- a/foundations/core/packages/rpc/package.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "name": "@hcengineering/rpc", - "version": "0.7.17", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "files": [ - "lib/**/*", - "types/**/*", - "src/**/*", - "!src/**/__test__/**", - "tsconfig.json" - ], - "author": "Anticrm Platform Contributors", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "build:watch": "compile", - "format": "format src", - "test": "jest --passWithNoTests --silent --coverage", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --coverage", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "workspace:^0.7.19", - "@types/node": "^22.18.1", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "@typescript-eslint/parser": "^6.21.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.6.2", - "typescript": "^5.9.3", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5", - "eslint-plugin-svelte": "^2.35.1" - }, - "dependencies": { - "@hcengineering/core": "workspace:^0.7.22", - "@hcengineering/platform": "workspace:^0.7.18", - "msgpackr": "^1.11.2" - }, - "repository": "https://github.com/hcengineering/huly.core", - "publishConfig": { - "access": "public" - }, - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.js" - } - } -} diff --git a/foundations/core/packages/rpc/src/index.ts b/foundations/core/packages/rpc/src/index.ts deleted file mode 100644 index 17c0812118..0000000000 --- a/foundations/core/packages/rpc/src/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -// -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// Copyright © 2021 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export * from './rpc' -export * from './sliding' diff --git a/foundations/core/packages/rpc/src/rpc.ts b/foundations/core/packages/rpc/src/rpc.ts deleted file mode 100644 index d1aa70f22d..0000000000 --- a/foundations/core/packages/rpc/src/rpc.ts +++ /dev/null @@ -1,216 +0,0 @@ -// -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { Account } from '@hcengineering/core' -import platform, { PlatformError, Severity, Status } from '@hcengineering/platform' -import { Packr } from 'msgpackr' - -/** - * @public - */ -export type ReqId = string | number - -/** - * @public - */ -export interface Request

{ - id?: ReqId - method: string - params: P - - meta?: Record - - time?: number // Server time to perform operation -} - -/** - * @public - */ -export interface HelloRequest extends Request { - binary?: boolean - compression?: boolean -} -/** - * @public - */ -export interface HelloResponse extends Response { - binary: boolean - reconnect?: boolean - serverVersion: string - lastTx?: string - lastHash?: string // Last model hash - account: Account - useCompression?: boolean -} - -function isTotalArray (value: any): value is { total?: number, lookupMap?: Record } & any[] { - return Array.isArray(value) && ((value as any).total !== undefined || (value as any).lookupMap !== undefined) -} -export function rpcJSONReplacer (key: string, value: any): any { - if (isTotalArray(value)) { - return { - dataType: 'TotalArray', - total: value.total, - lookupMap: value.lookupMap, - value: [...value] - } - } else if ( - typeof value === 'object' && - value !== null && - 'domain' in value && - typeof value.domain === 'string' && - 'value' in value && - isTotalArray(value.value) - ) { - return { - ...value, - value: { - dataType: 'TotalArray', - total: value.value.total, - lookupMap: value.value.lookupMap, - value: [...value.value] - } - } - } else { - return value ?? null - } -} - -export function rpcJSONReceiver (key: string, value: any): any { - if (typeof value === 'object' && value !== null) { - if (value.dataType === 'TotalArray') { - return Object.assign(value.value, { total: value.total, lookupMap: value.lookupMap }) - } else if ( - 'domain' in value && - typeof value.domain === 'string' && - 'value' in value && - value.value != null && - value.value.dataType === 'TotalArray' - ) { - return { - ...value, - value: Object.assign(value.value.value, { total: value.value.total, lookupMap: value.value.lookupMap }) - } - } - } - return value -} - -export interface RateLimitInfo { - remaining: number - limit: number - - current: number // in milliseconds - reset: number // in milliseconds - retryAfter?: number // in milliseconds -} - -/** - * Response object define a server response on transaction request. - * Also used to inform other clients about operations being performed by server. - * - * @public - */ -export interface Response { - result?: R - id?: ReqId - error?: Status - terminate?: boolean - - rateLimit?: RateLimitInfo - chunk?: { - index: number - final: boolean - } - time?: number // Server time to perform operation - bfst?: number // Server time to perform operation - queue?: number -} - -export class RPCHandler { - packr = new Packr({ structuredClone: true, bundleStrings: true, copyBuffers: false }) - protoSerialize (object: object, binary: boolean): any { - if (!binary) { - return JSON.stringify(object, rpcJSONReplacer) - } - return new Uint8Array(this.packr.pack(object)) - } - - protoDeserialize (data: any, binary: boolean): any { - if (!binary) { - let _data = data - if (_data instanceof ArrayBuffer) { - const decoder = new TextDecoder() - _data = decoder.decode(_data) - } - try { - return JSON.parse(_data.toString(), rpcJSONReceiver) - } catch (err: any) { - if (((err.message as string) ?? '').includes('Unexpected token')) { - return this.packr.unpack(new Uint8Array(data)) - } - } - } - return this.packr.unpack(new Uint8Array(data)) - } - - /** - * @public - * @param object - - * @returns - */ - serialize (object: Request | Response, binary: boolean): any { - if ((object as any).result !== undefined) { - ;(object as any).result = rpcJSONReplacer('result', (object as any).result) - } - return this.protoSerialize(object, binary) - } - - /** - * @public - * @param response - - * @returns - */ - readResponse(response: any, binary: boolean): Response { - const data = this.protoDeserialize(response, binary) - if (data.result !== undefined) { - data.result = rpcJSONReceiver('result', data.result) - } - return data - } - - /** - * @public - * @param request - - * @returns - */ - readRequest

(request: any, binary: boolean): Request

{ - const result: Request

= this.protoDeserialize(request, binary) - if (typeof result.method !== 'string') { - throw new PlatformError(new Status(Severity.ERROR, platform.status.BadRequest, {})) - } - return result - } -} - -/** - * @public - * @param status - - * @param id - - * @returns - */ -export function fromStatus (status: Status, id?: ReqId): Response { - return { id, error: status } -} diff --git a/foundations/core/packages/rpc/src/sliding.ts b/foundations/core/packages/rpc/src/sliding.ts deleted file mode 100644 index 15ffbfa275..0000000000 --- a/foundations/core/packages/rpc/src/sliding.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { RateLimitInfo } from './rpc' - -export class SlidingWindowRateLimitter { - private readonly rateLimits = new Map< - string, - { - requests: number[] - rejectedRequests: number // Counter for rejected requests - resetTime: number - } - >() - - constructor ( - readonly rateLimitMax: number, - readonly rateLimitWindow: number, - readonly now: () => number = Date.now - ) { - this.rateLimitMax = rateLimitMax - this.rateLimitWindow = rateLimitWindow - } - - public checkRateLimit (groupId: string): RateLimitInfo { - const now = this.now() - const windowStart = now - this.rateLimitWindow - - let rateLimit = this.rateLimits.get(groupId) - if (rateLimit == null) { - rateLimit = { requests: [], resetTime: now + this.rateLimitWindow, rejectedRequests: 0 } - this.rateLimits.set(groupId, rateLimit) - } - - // Remove requests outside the current window - rateLimit.requests = rateLimit.requests.filter((time) => time > windowStart) - - // Reset rejected requests counter when window changes - if (rateLimit.requests.length === 0) { - rateLimit.rejectedRequests = 0 - } - - // Update reset time - rateLimit.resetTime = now + this.rateLimitWindow - - if (rateLimit.requests.length <= this.rateLimitMax) { - rateLimit.requests.push(now + (rateLimit.rejectedRequests > this.rateLimitMax * 2 ? this.rateLimitWindow * 5 : 0)) - } - - if (rateLimit.requests.length >= this.rateLimitMax) { - rateLimit.rejectedRequests++ - - // Find when the oldest request will exit the window - const nextAvailableTime = rateLimit.requests[0] + this.rateLimitWindow - - return { - remaining: 0, - limit: this.rateLimitMax, - current: rateLimit.requests.length, - reset: rateLimit.resetTime, - retryAfter: Math.max(1, nextAvailableTime - now + 1) - } - } - - return { - remaining: this.rateLimitMax - rateLimit.requests.length, - current: rateLimit.requests.length, - limit: this.rateLimitMax, - reset: rateLimit.resetTime - } - } - - // Add a reset method for testing purposes - public reset (): void { - this.rateLimits.clear() - } -} diff --git a/foundations/core/packages/rpc/src/test/rateLimit.spec.ts b/foundations/core/packages/rpc/src/test/rateLimit.spec.ts deleted file mode 100644 index cdafa573db..0000000000 --- a/foundations/core/packages/rpc/src/test/rateLimit.spec.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { SlidingWindowRateLimitter } from '../sliding' - -describe('SlidingWindowRateLimitter', () => { - let clock = 100000 - - beforeEach(() => { - // Mock Date.now to control time - clock = 100000 - }) - - afterEach(() => { - jest.restoreAllMocks() - }) - - it('should allow requests within the limit', () => { - const limiter = new SlidingWindowRateLimitter(5, 60000, () => clock) - - for (let i = 0; i < 5; i++) { - const result = limiter.checkRateLimit('user1') - expect(result.remaining).toBe(5 - i - 1) - expect(result.limit).toBe(5) - } - - // The next request should hit the limit - const result = limiter.checkRateLimit('user1') - expect(result.remaining).toBe(0) - expect(result.retryAfter).toBeDefined() - }) - - it('should reject requests beyond the limit', () => { - const limiter = new SlidingWindowRateLimitter(3, 60000, () => clock) - - // Use up the limit - limiter.checkRateLimit('user1') - limiter.checkRateLimit('user1') - limiter.checkRateLimit('user1') - - // This should be limited - const result = limiter.checkRateLimit('user1') - expect(result.remaining).toBe(0) - expect(result.retryAfter).toBeDefined() - }) - - it('should allow new requests as the window slides', () => { - const limiter = new SlidingWindowRateLimitter(2, 10000, () => clock) - - // Use up the limit - limiter.checkRateLimit('user1') - limiter.checkRateLimit('user1') - - // This should be limited - expect(limiter.checkRateLimit('user1').remaining).toBe(0) - - // Move time forward by 5 seconds (half the window) - clock += 5 * 1000 // 5 seconds - - // Should still have one request outside the current window - // and one within, so we can make one more request - const result = limiter.checkRateLimit('user1') - expect(result.remaining).toBe(0) // Now at limit again - - // Move time forward by full window - clock += 11 * 1000 // 1011 seconds - - // All previous requests should be outside the window - const newResult = limiter.checkRateLimit('user1') - expect(newResult.remaining).toBe(1) // One request used, one remaining - expect(limiter.checkRateLimit('user1').remaining).toBe(0) // Now at limit again - }) - - it('should handle different identifiers separately', () => { - const limiter = new SlidingWindowRateLimitter(2, 60000, () => clock) - - limiter.checkRateLimit('user1') - limiter.checkRateLimit('user1') - - // User1 should be at limit - expect(limiter.checkRateLimit('user1').remaining).toBe(0) - - // Different user should have separate limit - expect(limiter.checkRateLimit('user2').remaining).toBe(1) - expect(limiter.checkRateLimit('user2').remaining).toBe(0) - - // Both users should be at their limits - expect(limiter.checkRateLimit('user1').remaining).toBe(0) - expect(limiter.checkRateLimit('user2').remaining).toBe(0) - }) - - it('should handle sliding window correctly', () => { - const limiter = new SlidingWindowRateLimitter(10, 60000, () => clock) - - // Use up half the capacity - for (let i = 0; i < 5; i++) { - limiter.checkRateLimit('user1') - } - - // Move halfway through the window - clock += 30 * 1000 + 1 // 30 seconds - - // Make some more requests - for (let i = 0; i < 7; i++) { - const result = limiter.checkRateLimit('user1') - if (i < 5) { - expect(result.remaining).toBeGreaterThanOrEqual(0) - } else { - expect(result.remaining).toBe(0) - expect(result.retryAfter).toBeDefined() - break - } - } - }) - - it('check for ban', () => { - const limiter = new SlidingWindowRateLimitter(10, 10000, () => clock) - - for (let i = 0; i < 50; i++) { - limiter.checkRateLimit('user1') - } - - const r1 = limiter.checkRateLimit('user1') - expect(r1.remaining).toBe(0) - // Pass all window time. - clock += 10000 - - const r2 = limiter.checkRateLimit('user1') - expect(r2.remaining).toBe(9) - }) -}) diff --git a/foundations/core/packages/rpc/tsconfig.json b/foundations/core/packages/rpc/tsconfig.json deleted file mode 100644 index b5ae22f6e4..0000000000 --- a/foundations/core/packages/rpc/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/foundations/core/packages/storage-client/.eslintrc.js b/foundations/core/packages/storage-client/.eslintrc.js deleted file mode 100644 index 72235dc283..0000000000 --- a/foundations/core/packages/storage-client/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/foundations/core/packages/storage-client/.npmignore b/foundations/core/packages/storage-client/.npmignore deleted file mode 100644 index e3ec093c38..0000000000 --- a/foundations/core/packages/storage-client/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!/lib/** -!CHANGELOG.md -/lib/**/__tests__/ diff --git a/foundations/core/packages/storage-client/CHANGELOG.json b/foundations/core/packages/storage-client/CHANGELOG.json deleted file mode 100644 index afb0fb0f70..0000000000 --- a/foundations/core/packages/storage-client/CHANGELOG.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "name": "@hcengineering/storage-client", - "entries": [ - { - "version": "0.7.17", - "tag": "@hcengineering/storage-client_v0.7.17", - "date": "Mon, 27 Oct 2025 13:27:12 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" - } - ] - } - }, - { - "version": "0.7.8", - "tag": "@hcengineering/storage-client_v0.7.8", - "date": "Thu, 23 Oct 2025 18:03:30 GMT", - "comments": { - "patch": [ - { - "comment": "use proper content type in multipart upload" - } - ] - } - }, - { - "version": "0.7.7", - "tag": "@hcengineering/storage-client_v0.7.7", - "date": "Tue, 21 Oct 2025 19:04:55 GMT", - "comments": { - "patch": [ - { - "comment": "more accurate upload progress" - }, - { - "comment": "update integration tests" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.8` to `0.7.10`" - } - ] - } - }, - { - "version": "0.7.6", - "tag": "@hcengineering/storage-client_v0.7.6", - "date": "Tue, 14 Oct 2025 15:36:13 GMT", - "comments": { - "patch": [ - { - "comment": "fix front service upload" - } - ] - } - }, - { - "version": "0.7.5", - "tag": "@hcengineering/storage-client_v0.7.5", - "date": "Tue, 14 Oct 2025 06:29:02 GMT", - "comments": { - "patch": [ - { - "comment": "new storage client" - } - ] - } - } - ] -} diff --git a/foundations/core/packages/storage-client/CHANGELOG.md b/foundations/core/packages/storage-client/CHANGELOG.md deleted file mode 100644 index 0244089935..0000000000 --- a/foundations/core/packages/storage-client/CHANGELOG.md +++ /dev/null @@ -1,38 +0,0 @@ -# Change Log - @hcengineering/storage-client - -This log was last generated on Mon, 27 Oct 2025 13:27:12 GMT and should not be manually modified. - -## 0.7.17 -Mon, 27 Oct 2025 13:27:12 GMT - -_Version update only_ - -## 0.7.8 -Thu, 23 Oct 2025 18:03:30 GMT - -### Patches - -- use proper content type in multipart upload - -## 0.7.7 -Tue, 21 Oct 2025 19:04:55 GMT - -### Patches - -- more accurate upload progress -- update integration tests - -## 0.7.6 -Tue, 14 Oct 2025 15:36:13 GMT - -### Patches - -- fix front service upload - -## 0.7.5 -Tue, 14 Oct 2025 06:29:02 GMT - -### Patches - -- new storage client - diff --git a/foundations/core/packages/storage-client/config/rig.json b/foundations/core/packages/storage-client/config/rig.json deleted file mode 100644 index 0110930f55..0000000000 --- a/foundations/core/packages/storage-client/config/rig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig" -} diff --git a/foundations/core/packages/storage-client/jest.config.js b/foundations/core/packages/storage-client/jest.config.js deleted file mode 100644 index 069ea8aa0d..0000000000 --- a/foundations/core/packages/storage-client/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ['./src'], - coverageReporters: ['text-summary', 'html', 'lcov'] -} diff --git a/foundations/core/packages/storage-client/package.json b/foundations/core/packages/storage-client/package.json deleted file mode 100644 index 46968f9f8e..0000000000 --- a/foundations/core/packages/storage-client/package.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "name": "@hcengineering/storage-client", - "version": "0.7.17", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "files": [ - "lib/**/*", - "types/**/*", - "src/**/*", - "!src/**/__test__/**", - "tsconfig.json" - ], - "author": "Hardcore Engineering Inc.", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "build:watch": "compile", - "format": "format src", - "test": "jest --passWithNoTests --silent --coverage", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --coverage", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "cross-env": "~7.0.3", - "@hcengineering/platform-rig": "workspace:^0.7.19", - "@types/node": "^22.18.1", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "esbuild": "^0.25.10", - "@typescript-eslint/parser": "^6.21.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.6.2", - "typescript": "^5.9.3", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5" - }, - "dependencies": { - "@hcengineering/core": "workspace:^0.7.22" - }, - "repository": "https://github.com/hcengineering/huly.core", - "publishConfig": { - "access": "public" - }, - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.js" - } - } -} diff --git a/foundations/core/packages/storage-client/src/__tests__/create-file-storage.test.ts b/foundations/core/packages/storage-client/src/__tests__/create-file-storage.test.ts deleted file mode 100644 index 933f513364..0000000000 --- a/foundations/core/packages/storage-client/src/__tests__/create-file-storage.test.ts +++ /dev/null @@ -1,343 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { createFileStorage, FileStorageConfig } from '../client' -import { DatalakeStorage } from '../client/datalake' -import { FrontStorage } from '../client/front' -import { HulylakeStorage } from '../client/hulylake' - -describe('createFileStorage factory', () => { - beforeEach(() => { - jest.clearAllMocks() - }) - - describe('DatalakeStorage creation', () => { - it('should create DatalakeStorage when datalakeUrl is provided', () => { - const config: FileStorageConfig = { - uploadUrl: 'https://upload.example.com', - datalakeUrl: 'https://datalake.example.com' - } - - const storage = createFileStorage(config) - - expect(storage).toBeInstanceOf(DatalakeStorage) - }) - - it('should create DatalakeStorage when both datalakeUrl and hulylakeUrl are provided (datalake takes precedence)', () => { - const config: FileStorageConfig = { - uploadUrl: 'https://upload.example.com', - datalakeUrl: 'https://datalake.example.com', - hulylakeUrl: 'https://hulylake.example.com' - } - - const storage = createFileStorage(config) - - expect(storage).toBeInstanceOf(DatalakeStorage) - }) - - it('should create DatalakeStorage when datalakeUrl is non-empty string', () => { - const config: FileStorageConfig = { - uploadUrl: 'https://upload.example.com', - datalakeUrl: 'https://datalake.example.com' - } - - const storage = createFileStorage(config) - - expect(storage).toBeInstanceOf(DatalakeStorage) - }) - - it('should not create DatalakeStorage when datalakeUrl is empty string', () => { - const config: FileStorageConfig = { - uploadUrl: 'https://upload.example.com', - datalakeUrl: '' - } - - const storage = createFileStorage(config) - - expect(storage).not.toBeInstanceOf(DatalakeStorage) - expect(storage).toBeInstanceOf(FrontStorage) - }) - - it('should not create DatalakeStorage when datalakeUrl is undefined', () => { - const config: FileStorageConfig = { - uploadUrl: 'https://upload.example.com', - datalakeUrl: undefined - } - - const storage = createFileStorage(config) - - expect(storage).not.toBeInstanceOf(DatalakeStorage) - expect(storage).toBeInstanceOf(FrontStorage) - }) - }) - - describe('HulylakeStorage creation', () => { - it('should create HulylakeStorage when hulylakeUrl is provided and datalakeUrl is not', () => { - const config: FileStorageConfig = { - uploadUrl: 'https://upload.example.com', - hulylakeUrl: 'https://hulylake.example.com' - } - - const storage = createFileStorage(config) - - expect(storage).toBeInstanceOf(HulylakeStorage) - }) - - it('should create HulylakeStorage when hulylakeUrl is provided and datalakeUrl is empty', () => { - const config: FileStorageConfig = { - uploadUrl: 'https://upload.example.com', - datalakeUrl: '', - hulylakeUrl: 'https://hulylake.example.com' - } - - const storage = createFileStorage(config) - - expect(storage).toBeInstanceOf(HulylakeStorage) - }) - - it('should create HulylakeStorage when hulylakeUrl is provided and datalakeUrl is undefined', () => { - const config: FileStorageConfig = { - uploadUrl: 'https://upload.example.com', - datalakeUrl: undefined, - hulylakeUrl: 'https://hulylake.example.com' - } - - const storage = createFileStorage(config) - - expect(storage).toBeInstanceOf(HulylakeStorage) - }) - - it('should not create HulylakeStorage when hulylakeUrl is empty string', () => { - const config: FileStorageConfig = { - uploadUrl: 'https://upload.example.com', - hulylakeUrl: '' - } - - const storage = createFileStorage(config) - - expect(storage).not.toBeInstanceOf(HulylakeStorage) - expect(storage).toBeInstanceOf(FrontStorage) - }) - - it('should not create HulylakeStorage when hulylakeUrl is undefined', () => { - const config: FileStorageConfig = { - uploadUrl: 'https://upload.example.com', - hulylakeUrl: undefined - } - - const storage = createFileStorage(config) - - expect(storage).not.toBeInstanceOf(HulylakeStorage) - expect(storage).toBeInstanceOf(FrontStorage) - }) - }) - - describe('FrontStorage creation', () => { - it('should create FrontStorage when only uploadUrl is provided', () => { - const config: FileStorageConfig = { - uploadUrl: 'https://upload.example.com' - } - - const storage = createFileStorage(config) - - expect(storage).toBeInstanceOf(FrontStorage) - }) - - it('should create FrontStorage when all URLs are empty strings', () => { - const config: FileStorageConfig = { - uploadUrl: 'https://upload.example.com', - datalakeUrl: '', - hulylakeUrl: '' - } - - const storage = createFileStorage(config) - - expect(storage).toBeInstanceOf(FrontStorage) - }) - - it('should create FrontStorage when all optional URLs are undefined', () => { - const config: FileStorageConfig = { - uploadUrl: 'https://upload.example.com', - datalakeUrl: undefined, - hulylakeUrl: undefined - } - - const storage = createFileStorage(config) - - expect(storage).toBeInstanceOf(FrontStorage) - }) - - it('should create FrontStorage as fallback', () => { - const config: FileStorageConfig = { - uploadUrl: 'https://upload.example.com' - } - - const storage = createFileStorage(config) - - expect(storage).toBeInstanceOf(FrontStorage) - }) - }) - - describe('priority and precedence', () => { - it('should prioritize DatalakeStorage over HulylakeStorage', () => { - const config: FileStorageConfig = { - uploadUrl: 'https://upload.example.com', - datalakeUrl: 'https://datalake.example.com', - hulylakeUrl: 'https://hulylake.example.com' - } - - const storage = createFileStorage(config) - - expect(storage).toBeInstanceOf(DatalakeStorage) - expect(storage).not.toBeInstanceOf(HulylakeStorage) - }) - - it('should prioritize HulylakeStorage over FrontStorage', () => { - const config: FileStorageConfig = { - uploadUrl: 'https://upload.example.com', - hulylakeUrl: 'https://hulylake.example.com' - } - - const storage = createFileStorage(config) - - expect(storage).toBeInstanceOf(HulylakeStorage) - expect(storage).not.toBeInstanceOf(FrontStorage) - }) - - it('should use FrontStorage when no specialized storage is available', () => { - const config: FileStorageConfig = { - uploadUrl: 'https://upload.example.com' - } - - const storage = createFileStorage(config) - - expect(storage).toBeInstanceOf(FrontStorage) - expect(storage).not.toBeInstanceOf(DatalakeStorage) - expect(storage).not.toBeInstanceOf(HulylakeStorage) - }) - }) - - describe('URL handling', () => { - it('should pass correct URL to DatalakeStorage', () => { - const datalakeUrl = 'https://datalake.example.com' - const config: FileStorageConfig = { - uploadUrl: 'https://upload.example.com', - datalakeUrl - } - - const storage = createFileStorage(config) as DatalakeStorage - - // Test that the URL was passed correctly by checking the generated file URL - const fileUrl = storage.getFileUrl('test-workspace', 'test-file', 'test.txt') - expect(fileUrl).toContain(datalakeUrl) - }) - - it('should pass correct URL to HulylakeStorage', () => { - const hulylakeUrl = 'https://hulylake.example.com' - const config: FileStorageConfig = { - uploadUrl: 'https://upload.example.com', - hulylakeUrl - } - - const storage = createFileStorage(config) as HulylakeStorage - - // Test that the URL was passed correctly by checking the generated file URL - const fileUrl = storage.getFileUrl('test-workspace', 'test-file', 'test.txt') - expect(fileUrl).toContain(hulylakeUrl) - }) - - it('should pass correct URL to FrontStorage', () => { - const uploadUrl = 'https://upload.example.com' - const config: FileStorageConfig = { - uploadUrl - } - - const storage = createFileStorage(config) as FrontStorage - - // Test that the URL was passed correctly by checking the generated file URL - const fileUrl = storage.getFileUrl('test-workspace', 'test-file', 'test.txt') - expect(fileUrl).toContain(uploadUrl) - }) - }) - - describe('edge cases', () => { - it('should handle config with extra properties', () => { - const config: FileStorageConfig & { extraProp: string } = { - uploadUrl: 'https://upload.example.com', - datalakeUrl: 'https://datalake.example.com', - extraProp: 'ignored' - } - - const storage = createFileStorage(config) - - expect(storage).toBeInstanceOf(DatalakeStorage) - }) - - it('should handle URLs with trailing slashes', () => { - const config: FileStorageConfig = { - uploadUrl: 'https://upload.example.com/', - datalakeUrl: 'https://datalake.example.com/' - } - - const storage = createFileStorage(config) - - expect(storage).toBeInstanceOf(DatalakeStorage) - }) - - it('should handle URLs without trailing slashes', () => { - const config: FileStorageConfig = { - uploadUrl: 'https://upload.example.com', - hulylakeUrl: 'https://hulylake.example.com' - } - - const storage = createFileStorage(config) - - expect(storage).toBeInstanceOf(HulylakeStorage) - }) - - it('should handle URLs with different protocols', () => { - const config: FileStorageConfig = { - uploadUrl: 'http://upload.example.com', - datalakeUrl: 'http://datalake.example.com' - } - - const storage = createFileStorage(config) - - expect(storage).toBeInstanceOf(DatalakeStorage) - }) - - it('should handle URLs with ports', () => { - const config: FileStorageConfig = { - uploadUrl: 'https://upload.example.com:8080', - hulylakeUrl: 'https://hulylake.example.com:9090' - } - - const storage = createFileStorage(config) - - expect(storage).toBeInstanceOf(HulylakeStorage) - }) - - it('should handle localhost URLs', () => { - const config: FileStorageConfig = { - uploadUrl: 'http://localhost:3000', - datalakeUrl: 'http://localhost:4000' - } - - const storage = createFileStorage(config) - - expect(storage).toBeInstanceOf(DatalakeStorage) - }) - }) -}) diff --git a/foundations/core/packages/storage-client/src/__tests__/datalake-storage.test.ts b/foundations/core/packages/storage-client/src/__tests__/datalake-storage.test.ts deleted file mode 100644 index fcdfa9099c..0000000000 --- a/foundations/core/packages/storage-client/src/__tests__/datalake-storage.test.ts +++ /dev/null @@ -1,464 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { DatalakeStorage } from '../client/datalake' -import * as upload from '../upload' - -// Mock the upload module -jest.mock('../upload') -const mockUploadXhr = jest.mocked(upload.uploadXhr) -const mockUploadMultipart = jest.mocked(upload.uploadMultipart) - -// Mock fetch -const mockFetch = jest.fn() -global.fetch = mockFetch as any - -describe('DatalakeStorage', () => { - let storage: DatalakeStorage - const baseUrl = 'https://datalake.example.com' - - beforeEach(() => { - storage = new DatalakeStorage(baseUrl) - jest.clearAllMocks() - mockFetch.mockClear() - mockUploadXhr.mockClear() - mockUploadMultipart.mockClear() - }) - - describe('getFileUrl', () => { - it('should generate correct URL with filename', () => { - const workspace = 'test-workspace' - const file = 'file-123' - const filename = 'document.pdf' - - const url = storage.getFileUrl(workspace, file, filename) - - expect(url).toBe(`${baseUrl}/blob/${workspace}/${file}/${filename}`) - }) - - it('should generate correct URL without filename', () => { - const workspace = 'test-workspace' - const file = 'file-123' - - const url = storage.getFileUrl(workspace, file) - - expect(url).toBe(`${baseUrl}/blob/${workspace}/${file}`) - }) - - it('should handle special characters in parameters', () => { - const workspace = 'test workspace' - const file = 'file 123' - const filename = 'my document.pdf' - - const url = storage.getFileUrl(workspace, file, filename) - - expect(url).toBe(`${baseUrl}/blob/${workspace}/${file}/${filename}`) - }) - - it('should handle base URL with trailing slash', () => { - const storageWithSlash = new DatalakeStorage('https://datalake.example.com/') - const workspace = 'test-workspace' - const file = 'file-123' - const filename = 'test.txt' - - const url = storageWithSlash.getFileUrl(workspace, file, filename) - - expect(url).toBe('https://datalake.example.com/blob/test-workspace/file-123/test.txt') - }) - }) - - describe('getFileMeta', () => { - it('should fetch file metadata successfully', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const file = 'file-123' - const expectedMeta = { - size: 1024, - contentType: 'text/plain', - lastModified: '2023-01-01T00:00:00Z' - } - - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve(expectedMeta) - }) - - const meta = await storage.getFileMeta(token, workspace, file) - - expect(meta).toEqual(expectedMeta) - expect(mockFetch).toHaveBeenCalledWith( - `${baseUrl}/meta/${encodeURIComponent(workspace)}/${encodeURIComponent(file)}`, - { - headers: { - Authorization: `Bearer ${token}` - } - } - ) - }) - - it('should return empty object when metadata fetch fails', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const file = 'file-123' - - mockFetch.mockResolvedValueOnce({ - ok: false, - status: 404 - }) - - const meta = await storage.getFileMeta(token, workspace, file) - - expect(meta).toEqual({}) - }) - - it('should return empty object when network error occurs', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const file = 'file-123' - - mockFetch.mockRejectedValueOnce(new Error('Network error')) - - const meta = await storage.getFileMeta(token, workspace, file) - - expect(meta).toEqual({}) - }) - - it('should handle special characters in workspace and file names', async () => { - const token = 'test-token' - const workspace = 'test workspace' - const file = 'file 123' - - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({}) - }) - - await storage.getFileMeta(token, workspace, file) - - expect(mockFetch).toHaveBeenCalledWith( - `${baseUrl}/meta/${encodeURIComponent(workspace)}/${encodeURIComponent(file)}`, - { - headers: { - Authorization: `Bearer ${token}` - } - } - ) - }) - }) - - describe('deleteFile', () => { - it('should delete file successfully', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const file = 'file-123' - - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 200 - }) - - await storage.deleteFile(token, workspace, file) - - expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/blob/${workspace}/${file}`, { - method: 'DELETE', - headers: { - Authorization: `Bearer ${token}` - } - }) - }) - - it('should throw error when delete fails', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const file = 'file-123' - - mockFetch.mockResolvedValueOnce({ - ok: false, - status: 404, - statusText: 'Not Found' - }) - - await expect(storage.deleteFile(token, workspace, file)).rejects.toThrow('Failed to delete file') - }) - - it('should handle network errors during delete', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const file = 'file-123' - - mockFetch.mockRejectedValueOnce(new Error('Network error')) - - await expect(storage.deleteFile(token, workspace, file)).rejects.toThrow('Network error') - }) - }) - - describe('uploadFile', () => { - describe('small file upload (≤ 10MB)', () => { - it('should upload small file using form-data', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const uuid = 'file-uuid-123' - const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) - Object.defineProperty(file, 'size', { value: 5 * 1024 * 1024 }) // 5MB - - mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) - - await storage.uploadFile(token, workspace, uuid, file) - - expect(mockUploadXhr).toHaveBeenCalledWith( - { - url: `${baseUrl}/upload/form-data/${encodeURIComponent(workspace)}`, - method: 'POST', - headers: { Authorization: `Bearer ${token}` }, - body: expect.any(FormData) - }, - undefined - ) - - // Check FormData contents - const uploadCall = mockUploadXhr.mock.calls[0] - const formData = uploadCall[0].body as FormData - const uploadedFile = formData.get('file') as File - expect(uploadedFile).toBeInstanceOf(File) - expect(uploadedFile.name).toBe(uuid) // DatalakeStorage renames the file to uuid - - expect(mockUploadMultipart).not.toHaveBeenCalled() - }) - - it('should upload exactly 10MB file using form-data', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const uuid = 'file-uuid-123' - const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) - Object.defineProperty(file, 'size', { value: 10 * 1024 * 1024 }) // exactly 10MB - - mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) - - await storage.uploadFile(token, workspace, uuid, file) - - expect(mockUploadXhr).toHaveBeenCalled() - expect(mockUploadMultipart).not.toHaveBeenCalled() - }) - - it('should pass upload options to uploadXhr', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const uuid = 'file-uuid-123' - const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) - Object.defineProperty(file, 'size', { value: 1024 }) // 1KB - const onProgress = jest.fn() - const controller = new AbortController() - - mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) - - await storage.uploadFile(token, workspace, uuid, file, { - onProgress, - signal: controller.signal - }) - - expect(mockUploadXhr).toHaveBeenCalledWith(expect.any(Object), { onProgress, signal: controller.signal }) - }) - }) - - describe('large file upload (> 10MB)', () => { - it('should upload large file using multipart', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const uuid = 'large-file-uuid' - const file = new File(['large content'], 'large.txt', { type: 'text/plain' }) - Object.defineProperty(file, 'size', { value: 15 * 1024 * 1024 }) // 15MB - - mockUploadMultipart.mockResolvedValueOnce() - - await storage.uploadFile(token, workspace, uuid, file) - - expect(mockUploadMultipart).toHaveBeenCalledWith( - { - url: `${baseUrl}/upload/multipart/${encodeURIComponent(workspace)}/${encodeURIComponent(uuid)}`, - headers: { Authorization: `Bearer ${token}` }, - body: file - }, - undefined - ) - - expect(mockUploadXhr).not.toHaveBeenCalled() - }) - - it('should pass upload options to uploadMultipart', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const uuid = 'large-file-uuid' - const file = new File(['large content'], 'large.txt', { type: 'text/plain' }) - Object.defineProperty(file, 'size', { value: 20 * 1024 * 1024 }) // 20MB - const onProgress = jest.fn() - const controller = new AbortController() - - mockUploadMultipart.mockResolvedValueOnce() - - await storage.uploadFile(token, workspace, uuid, file, { - onProgress, - signal: controller.signal - }) - - expect(mockUploadMultipart).toHaveBeenCalledWith(expect.any(Object), { onProgress, signal: controller.signal }) - }) - - it('should handle special characters in workspace and uuid for multipart', async () => { - const token = 'test-token' - const workspace = 'test workspace' - const uuid = 'file uuid 123' - const file = new File(['large content'], 'large.txt', { type: 'text/plain' }) - Object.defineProperty(file, 'size', { value: 15 * 1024 * 1024 }) // 15MB - - mockUploadMultipart.mockResolvedValueOnce() - - await storage.uploadFile(token, workspace, uuid, file) - - expect(mockUploadMultipart).toHaveBeenCalledWith( - { - url: `${baseUrl}/upload/multipart/${encodeURIComponent(workspace)}/${encodeURIComponent(uuid)}`, - headers: { Authorization: `Bearer ${token}` }, - body: file - }, - undefined - ) - }) - }) - - describe('error handling', () => { - it('should handle small file upload errors', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const uuid = 'file-uuid-123' - const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) - Object.defineProperty(file, 'size', { value: 1024 }) // 1KB - - mockUploadXhr.mockRejectedValueOnce(new Error('Upload failed')) - - await expect(storage.uploadFile(token, workspace, uuid, file)).rejects.toThrow('Upload failed') - }) - - it('should handle large file upload errors', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const uuid = 'large-file-uuid' - const file = new File(['large content'], 'large.txt', { type: 'text/plain' }) - Object.defineProperty(file, 'size', { value: 15 * 1024 * 1024 }) // 15MB - - mockUploadMultipart.mockRejectedValueOnce(new Error('Multipart upload failed')) - - await expect(storage.uploadFile(token, workspace, uuid, file)).rejects.toThrow('Multipart upload failed') - }) - }) - }) - - describe('integration scenarios', () => { - it('should handle complete file lifecycle with small file', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const uuid = 'lifecycle-uuid' - const filename = 'lifecycle.txt' - const file = new File(['test content'], filename, { type: 'text/plain' }) - Object.defineProperty(file, 'size', { value: 1024 }) // 1KB - - // Upload small file - mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) - await storage.uploadFile(token, workspace, uuid, file) - - // Get file URL - const url = storage.getFileUrl(workspace, uuid, filename) - expect(url).toBe(`${baseUrl}/blob/${workspace}/${uuid}/${filename}`) - - // Get file meta - const expectedMeta = { size: 1024, contentType: 'text/plain' } - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve(expectedMeta) - }) - const meta = await storage.getFileMeta(token, workspace, uuid) - expect(meta).toEqual(expectedMeta) - - // Delete file - mockFetch.mockResolvedValueOnce({ ok: true, status: 200 }) - await storage.deleteFile(token, workspace, uuid) - - expect(mockUploadXhr).toHaveBeenCalledTimes(1) - expect(mockFetch).toHaveBeenCalledTimes(2) // meta + delete - }) - - it('should handle complete file lifecycle with large file', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const uuid = 'large-lifecycle-uuid' - const filename = 'large-lifecycle.txt' - const file = new File(['large content'], filename, { type: 'text/plain' }) - Object.defineProperty(file, 'size', { value: 15 * 1024 * 1024 }) // 15MB - - // Upload large file - mockUploadMultipart.mockResolvedValueOnce() - await storage.uploadFile(token, workspace, uuid, file) - - // Get file URL - const url = storage.getFileUrl(workspace, uuid, filename) - expect(url).toBe(`${baseUrl}/blob/${workspace}/${uuid}/${filename}`) - - // Get file meta - const expectedMeta = { size: 15 * 1024 * 1024, contentType: 'text/plain' } - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve(expectedMeta) - }) - const meta = await storage.getFileMeta(token, workspace, uuid) - expect(meta).toEqual(expectedMeta) - - // Delete file - mockFetch.mockResolvedValueOnce({ ok: true, status: 200 }) - await storage.deleteFile(token, workspace, uuid) - - expect(mockUploadMultipart).toHaveBeenCalledTimes(1) - expect(mockUploadXhr).not.toHaveBeenCalled() - expect(mockFetch).toHaveBeenCalledTimes(2) // meta + delete - }) - - it('should handle upload size threshold boundary', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - - // Test file exactly at 10MB (should use form-data) - const smallUuid = 'small-boundary-uuid' - const smallFile = new File(['content'], 'small.txt', { type: 'text/plain' }) - Object.defineProperty(smallFile, 'size', { value: 10 * 1024 * 1024 }) - - mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) - await storage.uploadFile(token, workspace, smallUuid, smallFile) - expect(mockUploadXhr).toHaveBeenCalled() - expect(mockUploadMultipart).not.toHaveBeenCalled() - - // Reset mocks - mockUploadXhr.mockClear() - mockUploadMultipart.mockClear() - - // Test file just over 10MB (should use multipart) - const largeUuid = 'large-boundary-uuid' - const largeFile = new File(['content'], 'large.txt', { type: 'text/plain' }) - Object.defineProperty(largeFile, 'size', { value: 10 * 1024 * 1024 + 1 }) - - mockUploadMultipart.mockResolvedValueOnce() - await storage.uploadFile(token, workspace, largeUuid, largeFile) - expect(mockUploadMultipart).toHaveBeenCalled() - expect(mockUploadXhr).not.toHaveBeenCalled() - }) - }) -}) diff --git a/foundations/core/packages/storage-client/src/__tests__/front-storage.test.ts b/foundations/core/packages/storage-client/src/__tests__/front-storage.test.ts deleted file mode 100644 index 5c8e80c6ef..0000000000 --- a/foundations/core/packages/storage-client/src/__tests__/front-storage.test.ts +++ /dev/null @@ -1,312 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { FrontStorage } from '../client/front' -import * as upload from '../upload' - -// Mock the upload module -jest.mock('../upload') -const mockUploadXhr = jest.mocked(upload.uploadXhr) - -// Mock fetch -const mockFetch = jest.fn() -global.fetch = mockFetch as any - -describe('FrontStorage', () => { - let storage: FrontStorage - const baseUrl = 'https://files.example.com' - - beforeEach(() => { - storage = new FrontStorage(baseUrl) - jest.clearAllMocks() - mockFetch.mockClear() - mockUploadXhr.mockClear() - }) - - describe('getFileUrl', () => { - it('should generate correct URL with file ID only', () => { - const workspace = 'test-workspace' - const file = 'file-123' - - const url = storage.getFileUrl(workspace, file) - - expect(url).toBe(`${baseUrl}/${workspace}/${file}?file=${file}&workspace=${workspace}`) - }) - - it('should generate correct URL with custom filename', () => { - const workspace = 'test-workspace' - const file = 'file-123' - const filename = 'document.pdf' - - const url = storage.getFileUrl(workspace, file, filename) - - expect(url).toBe(`${baseUrl}/${workspace}/${filename}?file=${file}&workspace=${workspace}`) - }) - - it('should handle special characters in workspace and file names', () => { - const workspace = 'test workspace' - const file = 'file 123' - const filename = 'my document.pdf' - - const url = storage.getFileUrl(workspace, file, filename) - - expect(url).toBe(`${baseUrl}/${workspace}/${filename}?file=${file}&workspace=${workspace}`) - }) - - it('should handle base URL with trailing slash', () => { - const storageWithSlash = new FrontStorage('https://files.example.com/') - const workspace = 'test-workspace' - const file = 'file-123' - - const url = storageWithSlash.getFileUrl(workspace, file) - - expect(url).toBe('https://files.example.com/test-workspace/file-123?file=file-123&workspace=test-workspace') - }) - - it('should handle base URL without trailing slash', () => { - const storageWithoutSlash = new FrontStorage('https://files.example.com') - const workspace = 'test-workspace' - const file = 'file-123' - - const url = storageWithoutSlash.getFileUrl(workspace, file) - - expect(url).toBe('https://files.example.com/test-workspace/file-123?file=file-123&workspace=test-workspace') - }) - }) - - describe('getFileMeta', () => { - it('should return empty object', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const file = 'file-123' - - const meta = await storage.getFileMeta(token, workspace, file) - - expect(meta).toEqual({}) - }) - }) - - describe('deleteFile', () => { - it('should delete file successfully', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const file = 'file-123' - - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 200 - }) - - await storage.deleteFile(token, workspace, file) - - expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/${workspace}/${file}?file=${file}&workspace=${workspace}`, { - method: 'DELETE', - headers: { - Authorization: `Bearer ${token}` - } - }) - }) - - it('should throw error when delete fails', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const file = 'file-123' - - mockFetch.mockResolvedValueOnce({ - ok: false, - status: 404, - statusText: 'Not Found' - }) - - await expect(storage.deleteFile(token, workspace, file)).rejects.toThrow('Failed to delete file') - - expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/${workspace}/${file}?file=${file}&workspace=${workspace}`, { - method: 'DELETE', - headers: { - Authorization: `Bearer ${token}` - } - }) - }) - - it('should handle network errors during delete', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const file = 'file-123' - - mockFetch.mockRejectedValueOnce(new Error('Network error')) - - await expect(storage.deleteFile(token, workspace, file)).rejects.toThrow('Network error') - }) - }) - - describe('uploadFile', () => { - it('should upload file successfully', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const uuid = 'file-uuid-123' - const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) - - mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) - - await storage.uploadFile(token, workspace, uuid, file) - - expect(mockUploadXhr).toHaveBeenCalledWith( - { - url: `${baseUrl}/${workspace}`, - method: 'POST', - headers: { Authorization: `Bearer ${token}` }, - body: expect.any(FormData) - }, - undefined - ) - - // Check FormData contents - const uploadCall = mockUploadXhr.mock.calls[0] - const formData = uploadCall[0].body as FormData - const uploadedFile = formData.get('file') as File - expect(uploadedFile).toBeInstanceOf(File) - expect(uploadedFile.name).toBe(uuid) - expect(uploadedFile.type).toBe('text/plain') - }) - - it('should upload file with progress tracking', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const uuid = 'file-uuid-123' - const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) - const onProgress = jest.fn() - - mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) - - await storage.uploadFile(token, workspace, uuid, file, { onProgress }) - - expect(mockUploadXhr).toHaveBeenCalledWith( - { - url: `${baseUrl}/${workspace}`, - method: 'POST', - headers: { Authorization: `Bearer ${token}` }, - body: expect.any(FormData) - }, - { onProgress } - ) - }) - - it('should upload file with abort signal', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const uuid = 'file-uuid-123' - const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) - const controller = new AbortController() - - mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) - - await storage.uploadFile(token, workspace, uuid, file, { signal: controller.signal }) - - expect(mockUploadXhr).toHaveBeenCalledWith( - { - url: `${baseUrl}/${workspace}`, - method: 'POST', - headers: { Authorization: `Bearer ${token}` }, - body: expect.any(FormData) - }, - { signal: controller.signal } - ) - }) - - it('should handle upload errors', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const uuid = 'file-uuid-123' - const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) - - mockUploadXhr.mockRejectedValueOnce(new Error('Upload failed')) - - await expect(storage.uploadFile(token, workspace, uuid, file)).rejects.toThrow('Upload failed') - }) - - it('should handle different file types', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const uuid = 'image-uuid-123' - const file = new File(['binary data'], 'image.png', { type: 'image/png' }) - - mockUploadXhr.mockResolvedValueOnce({ status: 201, responseText: '' }) - - await storage.uploadFile(token, workspace, uuid, file) - - expect(mockUploadXhr).toHaveBeenCalledWith( - expect.objectContaining({ - url: `${baseUrl}/${workspace}`, - method: 'POST', - headers: { Authorization: `Bearer ${token}` } - }), - undefined - ) - - const formData = (mockUploadXhr.mock.calls[0][0] as any).body as FormData - const uploadedFile = formData.get('file') as File - expect(uploadedFile).toBeInstanceOf(File) - expect(uploadedFile.name).toBe(uuid) - expect(uploadedFile.type).toBe('image/png') - }) - - it('should handle empty files', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const uuid = 'empty-uuid-123' - const file = new File([''], 'empty.txt', { type: 'text/plain' }) - - mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) - - await storage.uploadFile(token, workspace, uuid, file) - - expect(mockUploadXhr).toHaveBeenCalled() - const formData = (mockUploadXhr.mock.calls[0][0] as any).body as FormData - const uploadedFile = formData.get('file') as File - expect(uploadedFile).toBeInstanceOf(File) - expect(uploadedFile.name).toBe(uuid) - expect(uploadedFile.type).toBe('text/plain') - }) - }) - - describe('integration scenarios', () => { - it('should handle complete file lifecycle', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const uuid = 'lifecycle-uuid' - const file = new File(['test content'], 'lifecycle.txt', { type: 'text/plain' }) - - // Upload file - mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) - await storage.uploadFile(token, workspace, uuid, file) - - // Get file URL - const url = storage.getFileUrl(workspace, uuid, 'lifecycle.txt') - expect(url).toContain(uuid) - expect(url).toContain('lifecycle.txt') - - // Get file meta (should return empty object for FrontStorage) - const meta = await storage.getFileMeta(token, workspace, uuid) - expect(meta).toEqual({}) - - // Delete file - mockFetch.mockResolvedValueOnce({ ok: true, status: 200 }) - await storage.deleteFile(token, workspace, uuid) - - expect(mockUploadXhr).toHaveBeenCalledTimes(1) - expect(mockFetch).toHaveBeenCalledTimes(1) - }) - }) -}) diff --git a/foundations/core/packages/storage-client/src/__tests__/hulylake-storage.test.ts b/foundations/core/packages/storage-client/src/__tests__/hulylake-storage.test.ts deleted file mode 100644 index 6f7e6d4e83..0000000000 --- a/foundations/core/packages/storage-client/src/__tests__/hulylake-storage.test.ts +++ /dev/null @@ -1,283 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { HulylakeStorage } from '../client/hulylake' -import * as upload from '../upload' - -// Mock the upload module -jest.mock('../upload') -const mockUploadXhr = jest.mocked(upload.uploadXhr) - -// Mock fetch -const mockFetch = jest.fn() -global.fetch = mockFetch as any - -describe('HulylakeStorage', () => { - let storage: HulylakeStorage - const baseUrl = 'https://hulylake.example.com' - - beforeEach(() => { - storage = new HulylakeStorage(baseUrl) - jest.clearAllMocks() - mockFetch.mockClear() - mockUploadXhr.mockClear() - }) - - describe('getFileUrl', () => { - it('should generate correct URL', () => { - const workspace = 'test-workspace' - const file = 'file-123' - - const url = storage.getFileUrl(workspace, file) - - expect(url).toBe(`${baseUrl}/api/${workspace}/${file}`) - }) - - it('should handle special characters in workspace and file names', () => { - const workspace = 'test workspace' - const file = 'file 123' - - const url = storage.getFileUrl(workspace, file) - - expect(url).toBe(`${baseUrl}/api/${workspace}/${file}`) - }) - - it('should handle base URL with trailing slash', () => { - const storageWithSlash = new HulylakeStorage('https://hulylake.example.com/') - const workspace = 'test-workspace' - const file = 'file-123' - - const url = storageWithSlash.getFileUrl(workspace, file) - - expect(url).toBe('https://hulylake.example.com/api/test-workspace/file-123') - }) - - it('should handle base URL without trailing slash', () => { - const storageWithoutSlash = new HulylakeStorage('https://hulylake.example.com') - const workspace = 'test-workspace' - const file = 'file-123' - - const url = storageWithoutSlash.getFileUrl(workspace, file) - - expect(url).toBe('https://hulylake.example.com/api/test-workspace/file-123') - }) - - it('should ignore filename parameter', () => { - const workspace = 'test-workspace' - const file = 'file-123' - const filename = 'ignored.txt' - - // The current implementation ignores the filename parameter - const url = storage.getFileUrl(workspace, file, filename) - - expect(url).toBe(`${baseUrl}/api/${workspace}/${file}`) - }) - }) - - describe('getFileMeta', () => { - it('should return empty object', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const file = 'file-123' - - const meta = await storage.getFileMeta(token, workspace, file) - - expect(meta).toEqual({}) - }) - }) - - describe('deleteFile', () => { - it('should delete file successfully', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const file = 'file-123' - - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 200 - }) - - await storage.deleteFile(token, workspace, file) - - expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/api/${workspace}/${file}`, { - method: 'DELETE', - headers: { - Authorization: `Bearer ${token}` - } - }) - }) - - it('should throw error when delete fails', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const file = 'file-123' - - mockFetch.mockResolvedValueOnce({ - ok: false, - status: 404, - statusText: 'Not Found' - }) - - await expect(storage.deleteFile(token, workspace, file)).rejects.toThrow('Failed to delete file') - }) - - it('should handle network errors during delete', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const file = 'file-123' - - mockFetch.mockRejectedValueOnce(new Error('Network error')) - - await expect(storage.deleteFile(token, workspace, file)).rejects.toThrow('Network error') - }) - }) - - describe('uploadFile', () => { - it('should upload file successfully with PUT method', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const uuid = 'file-uuid-123' - const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) - - mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) - - await storage.uploadFile(token, workspace, uuid, file) - - expect(mockUploadXhr).toHaveBeenCalledWith( - { - url: `${baseUrl}/api/${workspace}/${uuid}`, - method: 'PUT', - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': file.type, - 'Content-Length': file.size.toString() - }, - body: file - }, - undefined - ) - }) - - it('should upload file with progress tracking', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const uuid = 'file-uuid-123' - const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) - const onProgress = jest.fn() - - mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) - - await storage.uploadFile(token, workspace, uuid, file, { onProgress }) - - expect(mockUploadXhr).toHaveBeenCalledWith(expect.any(Object), { onProgress }) - }) - - it('should upload file with abort signal', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const uuid = 'file-uuid-123' - const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) - const controller = new AbortController() - - mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) - - await storage.uploadFile(token, workspace, uuid, file, { signal: controller.signal }) - - expect(mockUploadXhr).toHaveBeenCalledWith(expect.any(Object), { signal: controller.signal }) - }) - - it('should handle upload errors', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const uuid = 'file-uuid-123' - const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) - - mockUploadXhr.mockRejectedValueOnce(new Error('Upload failed')) - - await expect(storage.uploadFile(token, workspace, uuid, file)).rejects.toThrow('Upload failed') - }) - - it('should handle different file types', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const uuid = 'image-uuid-123' - const file = new File(['binary data'], 'image.png', { type: 'image/png' }) - - mockUploadXhr.mockResolvedValueOnce({ status: 201, responseText: '' }) - - await storage.uploadFile(token, workspace, uuid, file) - - expect(mockUploadXhr).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'PUT', - headers: expect.objectContaining({ - Authorization: `Bearer ${token}`, - 'Content-Type': 'image/png' - }), - body: file - }), - undefined - ) - }) - - it('should handle empty files', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const uuid = 'empty-uuid-123' - const file = new File([''], 'empty.txt', { type: 'text/plain' }) - - mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) - - await storage.uploadFile(token, workspace, uuid, file) - - expect(mockUploadXhr).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'PUT', - body: file - }), - undefined - ) - }) - }) - - describe('integration scenarios', () => { - it('should handle complete file lifecycle', async () => { - const token = 'test-token' - const workspace = 'test-workspace' - const uuid = 'lifecycle-uuid' - const filename = 'lifecycle.txt' - const file = new File(['test content'], filename, { type: 'text/plain' }) - - // Upload file - mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) - await storage.uploadFile(token, workspace, uuid, file) - - // Get file URL - const url = storage.getFileUrl(workspace, uuid, filename) - expect(url).toBe(`${baseUrl}/api/${workspace}/${uuid}`) - - // Get file meta (should return empty object for HulylakeStorage) - const meta = await storage.getFileMeta(token, workspace, uuid) - expect(meta).toEqual({}) - - // Delete file - mockFetch.mockResolvedValueOnce({ ok: true, status: 200 }) - await storage.deleteFile(token, workspace, uuid) - - expect(mockUploadXhr).toHaveBeenCalledTimes(1) - expect(mockFetch).toHaveBeenCalledTimes(1) - }) - }) -}) diff --git a/foundations/core/packages/storage-client/src/__tests__/integration.test.ts b/foundations/core/packages/storage-client/src/__tests__/integration.test.ts deleted file mode 100644 index 31817c5116..0000000000 --- a/foundations/core/packages/storage-client/src/__tests__/integration.test.ts +++ /dev/null @@ -1,499 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { createFileStorage, FileStorageConfig } from '../client' -import { FileStorage, FileStorageUploadOptions } from '../types' -import * as upload from '../upload' - -// Mock the upload module -jest.mock('../upload') -const mockUploadXhr = jest.mocked(upload.uploadXhr) -const mockUploadMultipart = jest.mocked(upload.uploadMultipart) - -// Mock fetch -const mockFetch = jest.fn() -global.fetch = mockFetch as any - -// Mock XMLHttpRequest -class MockXMLHttpRequest { - public url: string = '' - public method: string = '' - public headers: Record = {} - public body: any = null - public status: number = 200 - public statusText: string = 'OK' - public upload: { onprogress: ((event: ProgressEvent) => void) | null } = { onprogress: null } - public onload: (() => void) | null = null - public onerror: (() => void) | null = null - public onabort: (() => void) | null = null - public ontimeout: (() => void) | null = null - - open (method: string, url: string, async: boolean = true): void { - this.method = method - this.url = url - } - - setRequestHeader (key: string, value: string): void { - this.headers[key] = value - } - - send (body: any): void { - this.body = body - setTimeout(() => { - this.onload?.() - }, 0) - } - - abort (): void { - this.onabort?.() - } -} - -;(global as any).XMLHttpRequest = MockXMLHttpRequest - -beforeAll(() => {}) - -afterAll(() => {}) - -describe('Storage Client Integration Tests', () => { - beforeEach(() => { - jest.clearAllMocks() - mockFetch.mockClear() - mockUploadXhr.mockClear() - mockUploadMultipart.mockClear() - }) - - describe('Front Storage Integration', () => { - let storage: FileStorage - - beforeEach(() => { - const config: FileStorageConfig = { - uploadUrl: 'https://files.example.com' - } - storage = createFileStorage(config) - }) - - it('should complete full file lifecycle with Front storage', async () => { - const token = 'test-token' - const workspace = 'integration-workspace' - const uuid = 'integration-file-uuid' - const filename = 'integration-test.txt' - const file = new File(['Integration test content'], filename, { type: 'text/plain' }) - - // Mock successful upload - mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) - - // Upload file - await storage.uploadFile(token, workspace, uuid, file) - - // Verify upload was called with correct parameters - expect(mockUploadXhr).toHaveBeenCalledWith( - { - url: 'https://files.example.com/integration-workspace', - method: 'POST', - headers: { Authorization: 'Bearer test-token' }, - body: expect.any(FormData) - }, - undefined - ) - - // Generate file URL - const fileUrl = storage.getFileUrl(workspace, uuid, filename) - expect(fileUrl).toBe(`https://files.example.com/${workspace}/${filename}?file=${uuid}&workspace=${workspace}`) - - // Get file metadata (should return empty object for Front storage) - const meta = await storage.getFileMeta(token, workspace, uuid) - expect(meta).toEqual({}) - - // Mock successful delete - mockFetch.mockResolvedValueOnce({ ok: true, status: 200 }) - - // Delete file - await storage.deleteFile(token, workspace, uuid) - - // Delete uses the file ID URL, not the filename URL - const deleteUrl = storage.getFileUrl(workspace, uuid) - expect(mockFetch).toHaveBeenCalledWith(deleteUrl, { - method: 'DELETE', - headers: { Authorization: 'Bearer test-token' } - }) - }) - - it('should handle upload with progress tracking', async () => { - const token = 'test-token' - const workspace = 'progress-workspace' - const uuid = 'progress-uuid' - const file = new File(['Progress test'], 'progress.txt', { type: 'text/plain' }) - const progressCallback = jest.fn() - - mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) - - await storage.uploadFile(token, workspace, uuid, file, { - onProgress: progressCallback - }) - - expect(mockUploadXhr).toHaveBeenCalledWith(expect.any(Object), { onProgress: progressCallback }) - }) - - it('should handle upload with abort signal', async () => { - const token = 'test-token' - const workspace = 'abort-workspace' - const uuid = 'abort-uuid' - const file = new File(['Abort test'], 'abort.txt', { type: 'text/plain' }) - const controller = new AbortController() - - mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) - - await storage.uploadFile(token, workspace, uuid, file, { - signal: controller.signal - }) - - expect(mockUploadXhr).toHaveBeenCalledWith(expect.any(Object), { signal: controller.signal }) - }) - }) - - describe('Datalake Storage Integration', () => { - let storage: FileStorage - - beforeEach(() => { - const config: FileStorageConfig = { - uploadUrl: 'https://files.example.com', - datalakeUrl: 'https://datalake.example.com' - } - storage = createFileStorage(config) - }) - - it('should complete full file lifecycle with small file', async () => { - const token = 'test-token' - const workspace = 'datalake-workspace' - const uuid = 'small-file-uuid' - const filename = 'small-file.txt' - const file = new File(['Small file content'], filename, { type: 'text/plain' }) - Object.defineProperty(file, 'size', { value: 1024 }) // 1KB - - // Mock successful small file upload - mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) - - // Upload small file (should use form-data) - await storage.uploadFile(token, workspace, uuid, file) - - expect(mockUploadXhr).toHaveBeenCalledWith( - { - url: `https://datalake.example.com/upload/form-data/${encodeURIComponent(workspace)}`, - method: 'POST', - headers: { Authorization: 'Bearer test-token' }, - body: expect.any(FormData) - }, - undefined - ) - - // Generate file URL - const fileUrl = storage.getFileUrl(workspace, uuid, filename) - expect(fileUrl).toBe(`https://datalake.example.com/blob/${workspace}/${uuid}/${filename}`) - - // Mock file metadata response - const expectedMeta = { size: 1024, contentType: 'text/plain' } - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve(expectedMeta) - }) - - // Get file metadata - const meta = await storage.getFileMeta(token, workspace, uuid) - expect(meta).toEqual(expectedMeta) - - // Mock successful delete - mockFetch.mockResolvedValueOnce({ ok: true, status: 200 }) - - // Delete file - await storage.deleteFile(token, workspace, uuid) - - expect(mockFetch).toHaveBeenCalledWith(`https://datalake.example.com/blob/${workspace}/${uuid}`, { - method: 'DELETE', - headers: { Authorization: 'Bearer test-token' } - }) - }) - - it('should complete full file lifecycle with large file', async () => { - const token = 'test-token' - const workspace = 'datalake-workspace' - const uuid = 'large-file-uuid' - const filename = 'large-file.txt' - const file = new File(['Large file content'], filename, { type: 'text/plain' }) - Object.defineProperty(file, 'size', { value: 15 * 1024 * 1024 }) // 15MB - - // Mock successful multipart upload - mockUploadMultipart.mockResolvedValueOnce() - - // Upload large file (should use multipart) - await storage.uploadFile(token, workspace, uuid, file) - - expect(mockUploadMultipart).toHaveBeenCalledWith( - { - url: `https://datalake.example.com/upload/multipart/${encodeURIComponent(workspace)}/${encodeURIComponent(uuid)}`, - headers: { Authorization: 'Bearer test-token' }, - body: file - }, - undefined - ) - - expect(mockUploadXhr).not.toHaveBeenCalled() - - // Verify other operations work the same - const fileUrl = storage.getFileUrl(workspace, uuid, filename) - expect(fileUrl).toBe(`https://datalake.example.com/blob/${workspace}/${uuid}/${filename}`) - }) - - it('should handle file size threshold correctly', async () => { - const token = 'test-token' - const workspace = 'threshold-workspace' - - // Test exactly 10MB file (should use form-data) - const boundaryUuid = 'boundary-uuid' - const boundaryFile = new File(['boundary content'], 'boundary.txt', { type: 'text/plain' }) - Object.defineProperty(boundaryFile, 'size', { value: 10 * 1024 * 1024 }) // exactly 10MB - - mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) - await storage.uploadFile(token, workspace, boundaryUuid, boundaryFile) - - expect(mockUploadXhr).toHaveBeenCalled() - expect(mockUploadMultipart).not.toHaveBeenCalled() - - // Reset mocks - mockUploadXhr.mockClear() - mockUploadMultipart.mockClear() - - // Test 10MB + 1 byte file (should use multipart) - const overBoundaryUuid = 'over-boundary-uuid' - const overBoundaryFile = new File(['over boundary content'], 'over-boundary.txt', { type: 'text/plain' }) - Object.defineProperty(overBoundaryFile, 'size', { value: 10 * 1024 * 1024 + 1 }) // 10MB + 1 byte - - mockUploadMultipart.mockResolvedValueOnce() - await storage.uploadFile(token, workspace, overBoundaryUuid, overBoundaryFile) - - expect(mockUploadMultipart).toHaveBeenCalled() - expect(mockUploadXhr).not.toHaveBeenCalled() - }) - }) - - describe('Hulylake Storage Integration', () => { - let storage: FileStorage - - beforeEach(() => { - const config: FileStorageConfig = { - uploadUrl: 'https://files.example.com', - hulylakeUrl: 'https://hulylake.example.com' - } - storage = createFileStorage(config) - }) - - it('should complete full file lifecycle with Hulylake storage', async () => { - const token = 'test-token' - const workspace = 'hulylake-workspace' - const uuid = 'hulylake-file-uuid' - const filename = 'hulylake-test.txt' - const file = new File(['Hulylake test content'], filename, { type: 'text/plain' }) - - // Mock successful upload - mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) - - // Upload file (always uses PUT method) - await storage.uploadFile(token, workspace, uuid, file) - - expect(mockUploadXhr).toHaveBeenCalledWith( - { - url: `https://hulylake.example.com/api/${workspace}/${uuid}`, - method: 'PUT', - headers: { - Authorization: 'Bearer test-token', - 'Content-Type': 'text/plain', - 'Content-Length': file.size.toString() - }, - body: file - }, - undefined - ) - - // Generate file URL - const fileUrl = storage.getFileUrl(workspace, uuid, filename) - expect(fileUrl).toBe(`https://hulylake.example.com/api/${workspace}/${uuid}`) - - // Get file metadata (should return empty object for Hulylake storage) - const meta = await storage.getFileMeta(token, workspace, uuid) - expect(meta).toEqual({}) - - // Mock successful delete - mockFetch.mockResolvedValueOnce({ ok: true, status: 200 }) - - // Delete file - await storage.deleteFile(token, workspace, uuid) - - expect(mockFetch).toHaveBeenCalledWith(fileUrl, { - method: 'DELETE', - headers: { Authorization: 'Bearer test-token' } - }) - }) - }) - - describe('Error Handling Integration', () => { - it('should handle upload failures across all storage types', async () => { - const configs: Array<{ name: string, config: FileStorageConfig }> = [ - { name: 'Front', config: { uploadUrl: 'https://front.example.com' } }, - { - name: 'Datalake', - config: { uploadUrl: 'https://front.example.com', datalakeUrl: 'https://datalake.example.com' } - }, - { - name: 'Hulylake', - config: { uploadUrl: 'https://front.example.com', hulylakeUrl: 'https://hulylake.example.com' } - } - ] - - for (const { name, config } of configs) { - const storage = createFileStorage(config) - const token = 'test-token' - const workspace = `${name.toLowerCase()}-workspace` - const uuid = `${name.toLowerCase()}-uuid` - const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) - - // Mock upload failure - mockUploadXhr.mockRejectedValueOnce(new Error(`${name} upload failed`)) - mockUploadMultipart.mockRejectedValueOnce(new Error(`${name} multipart upload failed`)) - - await expect(storage.uploadFile(token, workspace, uuid, file)).rejects.toThrow( - new RegExp(`${name}.*upload failed`) - ) - - // Reset mocks for next iteration - mockUploadXhr.mockClear() - mockUploadMultipart.mockClear() - } - }) - - it('should handle delete failures across all storage types', async () => { - const configs: Array<{ name: string, config: FileStorageConfig }> = [ - { name: 'Front', config: { uploadUrl: 'https://front.example.com' } }, - { - name: 'Datalake', - config: { uploadUrl: 'https://front.example.com', datalakeUrl: 'https://datalake.example.com' } - }, - { - name: 'Hulylake', - config: { uploadUrl: 'https://front.example.com', hulylakeUrl: 'https://hulylake.example.com' } - } - ] - - for (const { name, config } of configs) { - const storage = createFileStorage(config) - const token = 'test-token' - const workspace = `${name.toLowerCase()}-workspace` - const uuid = `${name.toLowerCase()}-uuid` - - // Mock delete failure - mockFetch.mockResolvedValueOnce({ - ok: false, - status: 500, - statusText: 'Internal Server Error' - }) - - await expect(storage.deleteFile(token, workspace, uuid)).rejects.toThrow('Failed to delete file') - - // Reset mocks for next iteration - mockFetch.mockClear() - } - }) - }) - - describe('Configuration Priority Integration', () => { - it('should respect storage priority in real usage scenarios', async () => { - // Test that Datalake takes priority over Hulylake - const config: FileStorageConfig = { - uploadUrl: 'https://upload.example.com', - datalakeUrl: 'https://datalake.example.com', - hulylakeUrl: 'https://hulylake.example.com' - } - - const storage = createFileStorage(config) - const token = 'test-token' - const workspace = 'priority-workspace' - const uuid = 'priority-uuid' - const file = new File(['priority test'], 'priority.txt', { type: 'text/plain' }) - Object.defineProperty(file, 'size', { value: 1024 }) // Small file to ensure form-data upload - - mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) - - await storage.uploadFile(token, workspace, uuid, file) - - // Should use Datalake form-data endpoint, not Hulylake PUT endpoint - expect(mockUploadXhr).toHaveBeenCalledWith( - expect.objectContaining({ - url: `https://datalake.example.com/upload/form-data/${encodeURIComponent(workspace)}`, - method: 'POST' // Datalake uses POST for form-data, Hulylake uses PUT - }), - undefined - ) - }) - }) - - describe('Upload Options Integration', () => { - it('should pass upload options correctly across all storage types', async () => { - const onProgress = jest.fn() - const controller = new AbortController() - const options: FileStorageUploadOptions = { - onProgress, - signal: controller.signal - } - - const configs: Array<{ name: string, config: FileStorageConfig }> = [ - { name: 'Front', config: { uploadUrl: 'https://front.example.com' } }, - { - name: 'Datalake', - config: { uploadUrl: 'https://front.example.com', datalakeUrl: 'https://datalake.example.com' } - }, - { - name: 'Hulylake', - config: { uploadUrl: 'https://front.example.com', hulylakeUrl: 'https://hulylake.example.com' } - } - ] - - for (const { name, config } of configs) { - const storage = createFileStorage(config) - const token = 'test-token' - const workspace = `${name.toLowerCase()}-options-workspace` - const uuid = `${name.toLowerCase()}-options-uuid` - const file = new File(['options test'], 'options.txt', { type: 'text/plain' }) - - if (name === 'Datalake') { - Object.defineProperty(file, 'size', { value: 1024 }) // Ensure small file for XHR upload - } - - mockUploadXhr.mockResolvedValueOnce({ status: 200, responseText: '' }) - mockUploadMultipart.mockResolvedValueOnce() - - await storage.uploadFile(token, workspace, uuid, file, options) - - if (name === 'Datalake' && file.size <= 10 * 1024 * 1024) { - expect(mockUploadXhr).toHaveBeenCalledWith(expect.any(Object), options) - } else if (name !== 'Datalake') { - expect(mockUploadXhr).toHaveBeenCalledWith(expect.any(Object), options) - } - - // Reset mocks for next iteration - mockUploadXhr.mockClear() - mockUploadMultipart.mockClear() - } - }) - }) -}) diff --git a/foundations/core/packages/storage-client/src/__tests__/upload.test.ts b/foundations/core/packages/storage-client/src/__tests__/upload.test.ts deleted file mode 100644 index 198203c4e0..0000000000 --- a/foundations/core/packages/storage-client/src/__tests__/upload.test.ts +++ /dev/null @@ -1,704 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { uploadXhr, uploadMultipart, XHRUpload, MultipartUpload } from '../upload' - -// Mock XMLHttpRequest -class MockXMLHttpRequest { - public url: string = '' - public method: string = '' - public headers: Record = {} - public body: any = null - public status: number = 200 - public statusText: string = 'OK' - public responseText: string = '' - public upload: { onprogress: ((event: ProgressEvent) => void) | null } = { onprogress: null } - public onload: (() => void) | null = null - public onerror: (() => void) | null = null - public onabort: (() => void) | null = null - public ontimeout: (() => void) | null = null - - open (method: string, url: string, async: boolean = true): void { - this.method = method - this.url = url - } - - setRequestHeader (key: string, value: string): void { - this.headers[key] = value - } - - send (body: any): void { - this.body = body - // Don't auto-call onload - let tests control when it's called - } - - abort (): void { - this.onabort?.() - } - - // Test helpers - simulateProgress (loaded: number, total: number): void { - const event: Partial = { loaded, total, lengthComputable: true } - this.upload.onprogress?.(event as ProgressEvent) - } - - simulateError (): void { - this.onerror?.() - } - - simulateTimeout (): void { - this.ontimeout?.() - } - - simulateSuccess (responseText: string = ''): void { - this.responseText = responseText - this.onload?.() - } -} - -// Mock fetch -const mockFetch = jest.fn() -global.fetch = mockFetch as any - -// Track all XHR instances for multipart tests -const xhrInstances: MockXMLHttpRequest[] = [] - -describe('uploadXhr', () => { - let xhrInstance: MockXMLHttpRequest - - beforeEach(() => { - jest.clearAllMocks() - xhrInstances.length = 0 - xhrInstance = new MockXMLHttpRequest() - ;(global as any).XMLHttpRequest = jest.fn().mockImplementation(() => { - xhrInstances.push(xhrInstance) - return xhrInstance - }) - }) - - afterEach(() => { - jest.restoreAllMocks() - }) - - it('should upload successfully with POST method', async () => { - const upload: XHRUpload = { - url: 'https://example.com/upload', - method: 'POST', - headers: { Authorization: 'Bearer token' }, - body: new FormData() - } - - const uploadPromise = uploadXhr(upload) - - // XHR setup is synchronous - expect(xhrInstance.method).toBe('POST') - expect(xhrInstance.url).toBe('https://example.com/upload') - expect(xhrInstance.headers.Authorization).toBe('Bearer token') - - // Manually trigger onload to resolve the promise with response - xhrInstance.simulateSuccess('{"result": "success"}') - - const result = await uploadPromise - expect(result.status).toBe(200) - expect(result.responseText).toBe('{"result": "success"}') - }) - - it('should upload successfully with PUT method', async () => { - const upload: XHRUpload = { - url: 'https://example.com/upload', - method: 'PUT', - headers: { 'Content-Type': 'application/octet-stream' }, - body: new Blob(['test content']) - } - - const uploadPromise = uploadXhr(upload) - - // XHR setup is synchronous - expect(xhrInstance.method).toBe('PUT') - expect(xhrInstance.headers['Content-Type']).toBe('application/octet-stream') - - // Manually trigger onload to resolve the promise - xhrInstance.simulateSuccess('') - - const result = await uploadPromise - expect(result.status).toBe(200) - expect(result.responseText).toBe('') - }) - - it('should track upload progress', async () => { - const onProgress = jest.fn() - const upload: XHRUpload = { - url: 'https://example.com/upload', - method: 'POST', - headers: {}, - body: new FormData() - } - - const uploadPromise = uploadXhr(upload, { onProgress }) - - // Simulate progress events (synchronous) - xhrInstance.simulateProgress(50, 100) - xhrInstance.simulateProgress(100, 100) - - // Manually trigger onload to resolve the promise - xhrInstance.simulateSuccess('') - - await uploadPromise - - expect(onProgress).toHaveBeenCalledWith({ - loaded: 50, - total: 100, - percentage: 50 - }) - expect(onProgress).toHaveBeenCalledWith({ - loaded: 100, - total: 100, - percentage: 100 - }) - }) - - it('should handle network errors', async () => { - const upload: XHRUpload = { - url: 'https://example.com/upload', - method: 'POST', - headers: {}, - body: new FormData() - } - - const uploadPromise = uploadXhr(upload) - - // Simulate error (synchronous) - xhrInstance.simulateError() - - await expect(uploadPromise).rejects.toThrow('Network error') - }) - - it('should handle HTTP errors', async () => { - xhrInstance.status = 500 - xhrInstance.statusText = 'Internal Server Error' - - const upload: XHRUpload = { - url: 'https://example.com/upload', - method: 'POST', - headers: {}, - body: new FormData() - } - - const uploadPromise = uploadXhr(upload) - - // Manually trigger onload to trigger the status check - xhrInstance.onload?.() - - await expect(uploadPromise).rejects.toThrow('Upload failed with status 500: Internal Server Error') - }) - - it('should handle timeout errors', async () => { - const upload: XHRUpload = { - url: 'https://example.com/upload', - method: 'POST', - headers: {}, - body: new FormData() - } - - const uploadPromise = uploadXhr(upload) - - // Simulate timeout (synchronous) - xhrInstance.simulateTimeout() - - await expect(uploadPromise).rejects.toThrow('Upload timeout') - }) - - it('should handle abort signal', async () => { - const controller = new AbortController() - const upload: XHRUpload = { - url: 'https://example.com/upload', - method: 'POST', - headers: {}, - body: new FormData() - } - - // Abort immediately - controller.abort() - - await expect(uploadXhr(upload, { signal: controller.signal })).rejects.toThrow('Upload aborted') - }) - - it.skip('should handle abort during upload', async () => { - // This test is complex due to async event handling in mocks - // The abort functionality is tested in integration tests - const controller = new AbortController() - const upload: XHRUpload = { - url: 'https://example.com/upload', - method: 'POST', - headers: {}, - body: new FormData() - } - - // Pre-abort the signal - controller.abort() - - // Should reject immediately if already aborted - await expect(uploadXhr(upload, { signal: controller.signal })).rejects.toThrow('Upload aborted') - }) -}) - -describe('uploadMultipart', () => { - beforeEach(() => { - jest.clearAllMocks() - mockFetch.mockClear() - mockFetch.mockReset() - xhrInstances.length = 0 - ;(global as any).XMLHttpRequest = jest.fn().mockImplementation(() => { - const instance = new MockXMLHttpRequest() - xhrInstances.push(instance) - return instance - }) - }) - - it('should upload small file in single chunk', async () => { - const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) - Object.defineProperty(file, 'size', { value: 1024 }) // 1KB - - const upload: MultipartUpload = { - url: 'https://example.com/upload', - headers: { Authorization: 'Bearer token' }, - body: file - } - - // Mock multipart upload create and complete (using fetch) - mockFetch - .mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({ uuid: 'test-uuid', uploadId: 'test-upload-id' }) - }) - .mockResolvedValueOnce({ - ok: true - }) - - const uploadPromise = uploadMultipart(upload) - - // Wait for async initialization to complete (fetch promise resolution) - await new Promise(setImmediate) - - // Should have 1 XHR for the part upload - expect(xhrInstances).toHaveLength(1) - const partXhr = xhrInstances[0] - - expect(partXhr.method).toBe('PUT') - expect(partXhr.url).toContain('/part') - expect(partXhr.url).toContain('uploadId=test-upload-id') - expect(partXhr.url).toContain('partNumber=1') - - // Simulate successful part upload - partXhr.simulateSuccess('{"etag": "test-etag-1"}') - - await uploadPromise - - // Check fetch was called for init and complete - expect(mockFetch).toHaveBeenCalledTimes(2) - - // Check initialization call - expect(mockFetch).toHaveBeenNthCalledWith(1, 'https://example.com/upload', { - method: 'POST', - headers: { - Authorization: 'Bearer token', - 'Content-Type': 'text/plain' - }, - signal: undefined - }) - - // Check completion call - expect(mockFetch).toHaveBeenNthCalledWith(2, expect.any(URL), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: 'Bearer token' - }, - body: JSON.stringify({ parts: [{ partNumber: 1, etag: 'test-etag-1' }] }), - signal: undefined - }) - }) - - it('should upload large file in multiple chunks', async () => { - const file = new File(['test content'.repeat(1000000)], 'test.txt', { type: 'text/plain' }) - Object.defineProperty(file, 'size', { value: 12 * 1024 * 1024 }) // 12MB - - const upload: MultipartUpload = { - url: 'https://example.com/upload', - headers: { Authorization: 'Bearer token' }, - body: file - } - - // Mock multipart upload create and complete - mockFetch - .mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({ uuid: 'test-uuid', uploadId: 'test-upload-id' }) - }) - .mockResolvedValueOnce({ - ok: true - }) - - const uploadPromise = uploadMultipart(upload) - - // Wait for async initialization to complete - await new Promise(setImmediate) - - // Should have at least 1 XHR created for first part - expect(xhrInstances.length).toBeGreaterThan(0) - - // Simulate each part upload sequentially (12MB / 5MB = 3 parts) - for (let i = 0; i < 3; i++) { - // Wait for XHR to be created if not yet - while (xhrInstances[i] === undefined) { - await new Promise(setImmediate) - } - xhrInstances[i].simulateSuccess(`{"etag": "test-etag-${i + 1}"}`) - // Allow upload to process the completion - await new Promise(setImmediate) - } - - await uploadPromise - - // Should be 1 init + 1 complete = 2 fetch calls (parts use XHR) - expect(mockFetch).toHaveBeenCalledTimes(2) - // Should have 3 XHR instances for the 3 parts - expect(xhrInstances).toHaveLength(3) - }) - - it('should track progress during multipart upload', async () => { - const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) - Object.defineProperty(file, 'size', { value: 10 * 1024 * 1024 }) // 10MB - - const onProgress = jest.fn() - const upload: MultipartUpload = { - url: 'https://example.com/upload', - headers: { - Authorization: 'Bearer token', - 'Content-Type': 'text/plain' - }, - body: file - } - - // Mock multipart upload create and complete - mockFetch - .mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({ uuid: 'test-uuid', uploadId: 'test-upload-id' }) - }) - .mockResolvedValueOnce({ - ok: true - }) - - const uploadPromise = uploadMultipart(upload, { onProgress }) - - // Wait for async initialization to complete - await new Promise(setImmediate) - - // Simulate progress on first part (should be 2 parts: 5MB + 5MB) - if (xhrInstances[0] !== undefined) { - xhrInstances[0].simulateProgress(2.5 * 1024 * 1024, 5 * 1024 * 1024) // 2.5MB of 5MB - xhrInstances[0].simulateProgress(5 * 1024 * 1024, 5 * 1024 * 1024) // Complete first part - xhrInstances[0].simulateSuccess('{"etag": "test-etag-1"}') - } - - // Wait for second XHR to be created - await new Promise(setImmediate) - - // Simulate second part - if (xhrInstances[1] !== undefined) { - xhrInstances[1].simulateProgress(2.5 * 1024 * 1024, 5 * 1024 * 1024) // 2.5MB of 5MB (second part) - xhrInstances[1].simulateProgress(5 * 1024 * 1024, 5 * 1024 * 1024) // Complete second part - xhrInstances[1].simulateSuccess('{"etag": "test-etag-2"}') - } - - await uploadPromise - - expect(onProgress).toHaveBeenCalled() - const progressCalls = onProgress.mock.calls - - // Check that progress was reported with proper values - expect(progressCalls.length).toBeGreaterThan(0) - - // First progress should show partial progress of first chunk - const firstCall = progressCalls[0][0] - expect(firstCall).toHaveProperty('loaded') - expect(firstCall).toHaveProperty('total', 10 * 1024 * 1024) - expect(firstCall).toHaveProperty('percentage') - expect(firstCall.loaded).toBeLessThanOrEqual(firstCall.total) - - // Progress should increase over time - const lastCall = progressCalls[progressCalls.length - 1][0] - expect(lastCall.loaded).toBeGreaterThanOrEqual(firstCall.loaded) - }) - - it('should handle initialization failure', async () => { - const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) - - const upload: MultipartUpload = { - url: 'https://example.com/upload', - headers: { - Authorization: 'Bearer token', - 'Content-Type': 'text/plain' - }, - body: file - } - - mockFetch.mockResolvedValueOnce({ - ok: false, - status: 401 - }) - - await expect(uploadMultipart(upload)).rejects.toThrow('Failed to initialize multipart upload') - }) - - it('should handle part upload failure', async () => { - const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) - Object.defineProperty(file, 'size', { value: 6 * 1024 * 1024 }) // 6MB - - const upload: MultipartUpload = { - url: 'https://example.com/upload', - headers: { Authorization: 'Bearer token' }, - body: file - } - - // Mock successful initialization and abort call - mockFetch - .mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({ uuid: 'test-uuid', uploadId: 'test-upload-id' }) - }) - .mockResolvedValueOnce({ - ok: true // abort call - }) - - const uploadPromise = uploadMultipart(upload) - - // Wait for async initialization and XHR to be created - await new Promise(setImmediate) - - // Simulate upload failure - if (xhrInstances[0] !== undefined) { - xhrInstances[0].status = 500 - xhrInstances[0].statusText = 'Internal Server Error' - xhrInstances[0].onload?.() - } - - await expect(uploadPromise).rejects.toThrow('Upload failed with status 500') - }) - - it('should handle completion failure', async () => { - const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) - Object.defineProperty(file, 'size', { value: 1024 }) // 1KB - - const upload: MultipartUpload = { - url: 'https://example.com/upload', - headers: { Authorization: 'Bearer token' }, - body: file - } - - // Mock successful init, failed completion, and abort - mockFetch - .mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({ uuid: 'test-uuid', uploadId: 'test-upload-id' }) - }) - .mockResolvedValueOnce({ - ok: false, - status: 500 - }) - .mockResolvedValueOnce({ - ok: true // abort call - }) - - const uploadPromise = uploadMultipart(upload) - - // Wait for async initialization and XHR to be created - await new Promise(setImmediate) - - if (xhrInstances[0] !== undefined) { - xhrInstances[0].simulateSuccess('{"etag": "test-etag-1"}') - } - - await expect(uploadPromise).rejects.toThrow('Failed to complete multipart upload') - }) - - it('should handle abort signal', async () => { - const controller = new AbortController() - const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) - - const upload: MultipartUpload = { - url: 'https://example.com/upload', - headers: { Authorization: 'Bearer token' }, - body: file - } - - // Mock the initialization call - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({ uuid: 'test-uuid', uploadId: 'test-upload-id' }) - }) - - // Abort immediately - this should be caught before part upload - controller.abort() - - // The implementation checks for abort before each part upload - await expect(uploadMultipart(upload, { signal: controller.signal })).rejects.toThrow('Upload aborted') - }) - - it('should report immediate progress updates during part upload', async () => { - const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) - Object.defineProperty(file, 'size', { value: 5 * 1024 * 1024 }) // 5MB - - const progressUpdates: Array<{ loaded: number, total: number, percentage: number }> = [] - const onProgress = jest.fn((progress) => { - progressUpdates.push({ ...progress }) - }) - - const upload: MultipartUpload = { - url: 'https://example.com/upload', - headers: { Authorization: 'Bearer token' }, - body: file - } - - // Mock multipart upload create and complete - mockFetch - .mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({ uuid: 'test-uuid', uploadId: 'test-upload-id' }) - }) - .mockResolvedValueOnce({ - ok: true - }) - - const uploadPromise = uploadMultipart(upload, { onProgress }) - - // Wait for async initialization to complete - await new Promise(setImmediate) - - // Simulate incremental progress updates (this is the key feature we're testing) - if (xhrInstances[0] !== undefined) { - const chunkSize = 5 * 1024 * 1024 - xhrInstances[0].simulateProgress(1 * 1024 * 1024, chunkSize) // 20% of chunk - xhrInstances[0].simulateProgress(2 * 1024 * 1024, chunkSize) // 40% of chunk - xhrInstances[0].simulateProgress(3 * 1024 * 1024, chunkSize) // 60% of chunk - xhrInstances[0].simulateProgress(4 * 1024 * 1024, chunkSize) // 80% of chunk - xhrInstances[0].simulateProgress(5 * 1024 * 1024, chunkSize) // 100% of chunk - xhrInstances[0].simulateSuccess('{"etag": "test-etag-1"}') - } - - await uploadPromise - - // Verify we got multiple intermediate progress updates - expect(progressUpdates.length).toBeGreaterThanOrEqual(5) - - // Verify progress is monotonically increasing - for (let i = 1; i < progressUpdates.length; i++) { - expect(progressUpdates[i].loaded).toBeGreaterThanOrEqual(progressUpdates[i - 1].loaded) - } - - // Verify percentages are calculated correctly - expect(progressUpdates[0].percentage).toBe(20) - expect(progressUpdates[1].percentage).toBe(40) - expect(progressUpdates[2].percentage).toBe(60) - expect(progressUpdates[3].percentage).toBe(80) - expect(progressUpdates[4].percentage).toBe(100) - - // Verify total is consistent - progressUpdates.forEach((update) => { - expect(update.total).toBe(5 * 1024 * 1024) - }) - }) - - it('should correctly calculate progress across multiple chunks', async () => { - const file = new File(['test content'], 'test.txt', { type: 'text/plain' }) - Object.defineProperty(file, 'size', { value: 10 * 1024 * 1024 }) // 10MB = 2 chunks - - const progressUpdates: Array<{ loaded: number, total: number, percentage: number }> = [] - const onProgress = jest.fn((progress) => { - progressUpdates.push({ ...progress }) - }) - - const upload: MultipartUpload = { - url: 'https://example.com/upload', - headers: { Authorization: 'Bearer token' }, - body: file - } - - // Mock multipart upload create and complete - mockFetch - .mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({ uuid: 'test-uuid', uploadId: 'test-upload-id' }) - }) - .mockResolvedValueOnce({ - ok: true - }) - - const uploadPromise = uploadMultipart(upload, { onProgress }) - - // Wait for async initialization to complete - await new Promise(setImmediate) - - // Simulate first chunk progress (5MB) - if (xhrInstances[0] !== undefined) { - xhrInstances[0].simulateProgress(2.5 * 1024 * 1024, 5 * 1024 * 1024) // 25% overall - xhrInstances[0].simulateProgress(5 * 1024 * 1024, 5 * 1024 * 1024) // 50% overall - xhrInstances[0].simulateSuccess('{"etag": "test-etag-1"}') - } - - // Wait for second XHR to be created - await new Promise(setImmediate) - - // Simulate second chunk progress (5MB) - if (xhrInstances[1] !== undefined) { - xhrInstances[1].simulateProgress(2.5 * 1024 * 1024, 5 * 1024 * 1024) // 75% overall - xhrInstances[1].simulateProgress(5 * 1024 * 1024, 5 * 1024 * 1024) // 100% overall - xhrInstances[1].simulateSuccess('{"etag": "test-etag-2"}') - } - - await uploadPromise - - // Verify we got progress updates - expect(progressUpdates.length).toBeGreaterThan(0) - - // Find progress updates at key milestones (from first chunk) - const progress25 = progressUpdates.find((p) => p.percentage === 25) - const progress50 = progressUpdates.find((p) => p.percentage === 50) - - // At least first chunk progress should be reported - expect(progress25).toBeDefined() - expect(progress50).toBeDefined() - - // Verify the loaded values are correct for milestones that exist - if (progress25 !== undefined) { - expect(progress25.loaded).toBe(2.5 * 1024 * 1024) - expect(progress25.total).toBe(10 * 1024 * 1024) - } - if (progress50 !== undefined) { - expect(progress50.loaded).toBe(5 * 1024 * 1024) - expect(progress50.total).toBe(10 * 1024 * 1024) - } - - // Verify that progress updates span across the upload - const progressValues = progressUpdates.map((p) => p.loaded) - const minProgress = Math.min(...progressValues) - const maxProgress = Math.max(...progressValues) - - // Should have progress from start through at least first chunk - expect(minProgress).toBeGreaterThan(0) - expect(maxProgress).toBeGreaterThanOrEqual(2.5 * 1024 * 1024) - }) -}) diff --git a/foundations/core/packages/storage-client/src/client/datalake.ts b/foundations/core/packages/storage-client/src/client/datalake.ts deleted file mode 100644 index 24c9476d13..0000000000 --- a/foundations/core/packages/storage-client/src/client/datalake.ts +++ /dev/null @@ -1,88 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { concatLink } from '@hcengineering/core' -import { FileStorage, FileStorageUploadOptions } from '../types' -import { uploadMultipart, uploadXhr } from '../upload' - -/** @public */ -export class DatalakeStorage implements FileStorage { - constructor (private readonly baseUrl: string) {} - - getFileUrl (workspace: string, file: string, filename?: string): string { - const path = filename !== undefined ? `/blob/${workspace}/${file}/${filename}` : `/blob/${workspace}/${file}` - return concatLink(this.baseUrl, path) - } - - async getFileMeta (token: string, workspace: string, file: string): Promise> { - const url = concatLink(this.baseUrl, `/meta/${encodeURIComponent(workspace)}/${encodeURIComponent(file)}`) - try { - const response = await fetch(url, { - headers: { - Authorization: `Bearer ${token}` - } - }) - if (response.ok) { - return await response.json() - } - } catch (err: any) {} - return {} - } - - async deleteFile (token: string, workspace: string, file: string): Promise { - const url = this.getFileUrl(workspace, file) - - const response = await fetch(url, { - method: 'DELETE', - headers: { - Authorization: `Bearer ${token}` - } - }) - - if (!response.ok) { - throw new Error('Failed to delete file') - } - } - - async uploadFile ( - token: string, - workspace: string, - uuid: string, - file: File, - options?: FileStorageUploadOptions - ): Promise { - if (file.size <= 10 * 1024 * 1024) { - const formData = new FormData() - formData.append('file', file, uuid) - - await uploadXhr( - { - url: concatLink(this.baseUrl, `/upload/form-data/${encodeURIComponent(workspace)}`), - method: 'POST', - headers: { Authorization: `Bearer ${token}` }, - body: formData - }, - options - ) - } else { - const url = concatLink( - this.baseUrl, - `/upload/multipart/${encodeURIComponent(workspace)}/${encodeURIComponent(uuid)}` - ) - const headers = { Authorization: `Bearer ${token}` } - await uploadMultipart({ url, headers, body: file }, options) - } - } -} diff --git a/foundations/core/packages/storage-client/src/client/front.ts b/foundations/core/packages/storage-client/src/client/front.ts deleted file mode 100644 index 67ade0229c..0000000000 --- a/foundations/core/packages/storage-client/src/client/front.ts +++ /dev/null @@ -1,68 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { concatLink } from '@hcengineering/core' -import { FileStorage, FileStorageUploadOptions } from '../types' -import { uploadXhr } from '../upload' - -/** @public */ -export class FrontStorage implements FileStorage { - constructor (private readonly baseUrl: string) {} - - getFileUrl (workspace: string, file: string, filename?: string): string { - const path = `/${workspace}/${filename ?? file}?file=${file}&workspace=${workspace}` - return concatLink(this.baseUrl, path) - } - - async getFileMeta (token: string, workspace: string, file: string): Promise> { - return {} - } - - async deleteFile (token: string, workspace: string, file: string): Promise { - const url = this.getFileUrl(workspace, file) - - const response = await fetch(url, { - method: 'DELETE', - headers: { - Authorization: `Bearer ${token}` - } - }) - - if (!response.ok) { - throw new Error('Failed to delete file') - } - } - - async uploadFile ( - token: string, - workspace: string, - uuid: string, - file: File, - options?: FileStorageUploadOptions - ): Promise { - const formData = new FormData() - formData.append('file', file, uuid) - - await uploadXhr( - { - url: concatLink(this.baseUrl, `/${workspace}`), - method: 'POST', - headers: { Authorization: `Bearer ${token}` }, - body: formData - }, - options - ) - } -} diff --git a/foundations/core/packages/storage-client/src/client/hulylake.ts b/foundations/core/packages/storage-client/src/client/hulylake.ts deleted file mode 100644 index 397815c015..0000000000 --- a/foundations/core/packages/storage-client/src/client/hulylake.ts +++ /dev/null @@ -1,71 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { concatLink } from '@hcengineering/core' -import { FileStorage, FileStorageUploadOptions } from '../types' -import { uploadXhr } from '../upload' - -/** @public */ -export class HulylakeStorage implements FileStorage { - constructor (private readonly baseUrl: string) {} - - getFileUrl (workspace: string, file: string, filename?: string): string { - const path = `/api/${workspace}/${file}` - return concatLink(this.baseUrl, path) - } - - async getFileMeta (token: string, workspace: string, file: string): Promise> { - return {} - } - - async deleteFile (token: string, workspace: string, file: string): Promise { - const url = this.getFileUrl(workspace, file) - - const response = await fetch(url, { - method: 'DELETE', - headers: { - Authorization: `Bearer ${token}` - } - }) - - if (!response.ok) { - throw new Error('Failed to delete file') - } - } - - async uploadFile ( - token: string, - workspace: string, - uuid: string, - file: File, - options?: FileStorageUploadOptions - ): Promise { - const url = this.getFileUrl(workspace, uuid) - - await uploadXhr( - { - url, - method: 'PUT', - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': file.type, - 'Content-Length': file.size.toString() - }, - body: file - }, - options - ) - } -} diff --git a/foundations/core/packages/storage-client/src/client/index.ts b/foundations/core/packages/storage-client/src/client/index.ts deleted file mode 100644 index 4ee30a2b8f..0000000000 --- a/foundations/core/packages/storage-client/src/client/index.ts +++ /dev/null @@ -1,45 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { DatalakeStorage } from './datalake' -import { FrontStorage } from './front' -import { HulylakeStorage } from './hulylake' - -import { FileStorage } from '../types' - -/** @public */ -export interface FileStorageConfig { - uploadUrl: string - datalakeUrl?: string - hulylakeUrl?: string -} - -/** @public */ -export function createFileStorage (config: FileStorageConfig): FileStorage { - const { uploadUrl, datalakeUrl, hulylakeUrl } = config - - if (datalakeUrl !== undefined && datalakeUrl !== '') { - console.debug('Using Datalake storage') - return new DatalakeStorage(datalakeUrl) - } - - if (hulylakeUrl !== undefined && hulylakeUrl !== '') { - console.debug('Using Hulylake storage') - return new HulylakeStorage(hulylakeUrl) - } - - console.debug('Using Front storage') - return new FrontStorage(uploadUrl) -} diff --git a/foundations/core/packages/storage-client/src/index.ts b/foundations/core/packages/storage-client/src/index.ts deleted file mode 100644 index 0f078ded71..0000000000 --- a/foundations/core/packages/storage-client/src/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export * from './client' -export * from './types' diff --git a/foundations/core/packages/storage-client/src/types.ts b/foundations/core/packages/storage-client/src/types.ts deleted file mode 100644 index bcbe88be8f..0000000000 --- a/foundations/core/packages/storage-client/src/types.ts +++ /dev/null @@ -1,41 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -/** @public */ -export interface FileStorageUploadProgress { - loaded: number - total: number - percentage: number -} - -/** @public */ -export interface FileStorageUploadOptions { - onProgress?: (progress: FileStorageUploadProgress) => void - signal?: AbortSignal -} - -/** @public */ -export interface FileStorage { - getFileUrl: (workspace: string, file: string, filename?: string) => string - getFileMeta: (token: string, workspace: string, file: string) => Promise> - uploadFile: ( - token: string, - workspace: string, - uuid: string, - file: File, - options?: FileStorageUploadOptions - ) => Promise - deleteFile: (token: string, workspace: string, file: string) => Promise -} diff --git a/foundations/core/packages/storage-client/src/upload.ts b/foundations/core/packages/storage-client/src/upload.ts deleted file mode 100644 index 9272c9b6a9..0000000000 --- a/foundations/core/packages/storage-client/src/upload.ts +++ /dev/null @@ -1,258 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { concatLink } from '@hcengineering/core' -import { FileStorageUploadOptions } from './types' - -/** @public */ -export interface XHRUpload { - url: string - method: 'POST' | 'PUT' - headers: Record - body: XMLHttpRequestBodyInit -} - -/** @public */ -export interface XHRUploadResult { - status: number - responseText: string -} - -/** @public */ -export async function uploadXhr (upload: XHRUpload, options?: FileStorageUploadOptions): Promise { - const signal = options?.signal - const onProgress = options?.onProgress - - // Check if already aborted before starting - if (signal?.aborted === true) { - throw new Error('Upload aborted') - } - - const xhr = new XMLHttpRequest() - - const abortHandler = (): void => { - xhr.abort() - } - - if (signal !== undefined) { - signal.addEventListener('abort', abortHandler) - } - - try { - return await new Promise((resolve, reject) => { - xhr.upload.onprogress = (event) => { - if (event.lengthComputable) { - onProgress?.({ - loaded: event.loaded, - total: event.total, - percentage: Math.round((100 * event.loaded) / event.total) - }) - } - } - - xhr.onload = () => { - if (xhr.status >= 200 && xhr.status < 300) { - resolve({ - status: xhr.status, - responseText: xhr.responseText - }) - } else { - reject(new Error(`Upload failed with status ${xhr.status}: ${xhr.statusText}`)) - } - } - - xhr.onerror = () => { - reject(new Error('Network error')) - } - - xhr.onabort = () => { - reject(new Error('Upload aborted')) - } - - xhr.ontimeout = () => { - reject(new Error('Upload timeout')) - } - - xhr.open(upload.method, upload.url, true) - - for (const key in upload.headers) { - xhr.setRequestHeader(key, upload.headers[key]) - } - - xhr.send(upload.body) - }) - } finally { - if (signal !== undefined) { - signal.removeEventListener('abort', abortHandler) - } - } -} - -/** @public */ -export interface MultipartUpload { - url: string - headers: Record - body: File | Blob -} - -/** @public */ -export async function uploadMultipart (upload: MultipartUpload, options?: FileStorageUploadOptions): Promise { - const CHUNK_SIZE = 5 * 1024 * 1024 // 5MB chunks - const { url, headers, body } = upload - const signal = options?.signal - const onProgress = options?.onProgress - - let uploadId: string | undefined - - try { - const { uploadId } = await multipartUploadCreate(url, { ...headers, 'Content-Type': body.type }, signal) - - const parts: Array<{ partNumber: number, etag: string }> = [] - const totalParts = Math.ceil(body.size / CHUNK_SIZE) - let uploaded = 0 - - for (let partNumber = 1; partNumber <= totalParts; partNumber++) { - const start = (partNumber - 1) * CHUNK_SIZE - const end = Math.min(start + CHUNK_SIZE, body.size) - const chunk = body.slice(start, end) - - throwIfAborted(signal) - - const partOptions: FileStorageUploadOptions = { - signal, - onProgress: - onProgress !== undefined - ? (progress) => { - const loaded = uploaded + progress.loaded - onProgress({ - loaded, - total: body.size, - percentage: Math.round((loaded * 100) / body.size) - }) - } - : undefined - } - - const { etag } = await multipartUploadPart(url, headers, uploadId, partNumber, chunk, partOptions) - parts.push({ partNumber, etag }) - - uploaded += chunk.size - } - - throwIfAborted(signal) - - await multipartUploadComplete(url, headers, uploadId, parts, signal) - } catch (err) { - if (uploadId !== undefined) { - await multipartUploadAbort(url, headers, uploadId) - } - - throw err instanceof Error ? err : new Error(String(err)) - } -} - -function throwIfAborted (signal?: AbortSignal): void { - if (signal?.aborted === true) { - throw new Error('Upload aborted') - } -} - -async function multipartUploadCreate ( - baseUrl: string, - headers: Record, - signal?: AbortSignal -): Promise<{ uuid: string, uploadId: string }> { - const response = await fetch(baseUrl, { - signal, - method: 'POST', - headers - }) - - if (!response.ok) { - throw new Error('Failed to initialize multipart upload') - } - - const { uuid, uploadId } = await response.json() - return { uuid, uploadId } -} - -async function multipartUploadComplete ( - baseUrl: string, - headers: Record, - uploadId: string, - parts: Array<{ partNumber: number, etag: string }>, - signal?: AbortSignal -): Promise { - const url = new URL(concatLink(baseUrl, '/complete')) - url.searchParams.set('uploadId', uploadId) - - const response = await fetch(url, { - signal, - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...headers - }, - body: JSON.stringify({ parts }) - }) - - if (!response.ok) { - throw new Error('Failed to complete multipart upload') - } -} - -async function multipartUploadPart ( - baseUrl: string, - headers: Record, - uploadId: string, - partNumber: number, - blob: Blob, - options?: FileStorageUploadOptions -): Promise<{ etag: string }> { - const url = new URL(concatLink(baseUrl, '/part')) - url.searchParams.set('uploadId', uploadId) - url.searchParams.set('partNumber', `${partNumber}`) - - const result = await uploadXhr( - { - url: url.toString(), - method: 'PUT', - headers, - body: blob - }, - options - ) - - try { - const response = JSON.parse(result.responseText) - return { etag: response.etag } - } catch (err) { - throw new Error(`Failed to parse response for part ${partNumber}`) - } -} - -async function multipartUploadAbort (baseUrl: string, headers: Record, uploadId: string): Promise { - const url = new URL(concatLink(baseUrl, '/abort')) - url.searchParams.set('uploadId', uploadId) - - const response = await fetch(url, { - method: 'POST', - headers - }) - - if (!response.ok) { - throw new Error('Failed to reject multipart upload') - } -} diff --git a/foundations/core/packages/storage-client/tsconfig.json b/foundations/core/packages/storage-client/tsconfig.json deleted file mode 100644 index a0f3d087e1..0000000000 --- a/foundations/core/packages/storage-client/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo", - "types": ["node", "jest"] - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/foundations/core/packages/storage/.eslintrc.js b/foundations/core/packages/storage/.eslintrc.js deleted file mode 100644 index ce90fb9646..0000000000 --- a/foundations/core/packages/storage/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/foundations/core/packages/storage/.npmignore b/foundations/core/packages/storage/.npmignore deleted file mode 100644 index e3ec093c38..0000000000 --- a/foundations/core/packages/storage/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!/lib/** -!CHANGELOG.md -/lib/**/__tests__/ diff --git a/foundations/core/packages/storage/CHANGELOG.json b/foundations/core/packages/storage/CHANGELOG.json deleted file mode 100644 index 6e836ef1f5..0000000000 --- a/foundations/core/packages/storage/CHANGELOG.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "name": "@hcengineering/storage", - "entries": [ - { - "version": "0.7.17", - "tag": "@hcengineering/storage_v0.7.17", - "date": "Mon, 27 Oct 2025 13:27:12 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" - } - ] - } - }, - { - "version": "0.7.5", - "tag": "@hcengineering/storage_v0.7.5", - "date": "Tue, 14 Oct 2025 04:58:17 GMT", - "comments": { - "patch": [ - { - "comment": "update deps" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.4` to `0.7.5`" - }, - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`" - } - ] - } - }, - { - "version": "0.7.4", - "tag": "@hcengineering/storage_v0.7.4", - "date": "Sat, 11 Oct 2025 18:20:33 GMT", - "comments": { - "patch": [ - { - "comment": "Update to latest platform rig" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.3` to `0.7.4`" - }, - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`" - } - ] - } - }, - { - "version": "0.7.3", - "tag": "@hcengineering/storage_v0.7.3", - "date": "Wed, 08 Oct 2025 03:40:53 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`" - } - ] - } - } - ] -} diff --git a/foundations/core/packages/storage/CHANGELOG.md b/foundations/core/packages/storage/CHANGELOG.md deleted file mode 100644 index 21264a95a6..0000000000 --- a/foundations/core/packages/storage/CHANGELOG.md +++ /dev/null @@ -1,28 +0,0 @@ -# Change Log - @hcengineering/storage - -This log was last generated on Mon, 27 Oct 2025 13:27:12 GMT and should not be manually modified. - -## 0.7.17 -Mon, 27 Oct 2025 13:27:12 GMT - -_Version update only_ - -## 0.7.5 -Tue, 14 Oct 2025 04:58:17 GMT - -### Patches - -- update deps - -## 0.7.4 -Sat, 11 Oct 2025 18:20:33 GMT - -### Patches - -- Update to latest platform rig - -## 0.7.3 -Wed, 08 Oct 2025 03:40:53 GMT - -_Initial release_ - diff --git a/foundations/core/packages/storage/config/rig.json b/foundations/core/packages/storage/config/rig.json deleted file mode 100644 index 78cc5a1733..0000000000 --- a/foundations/core/packages/storage/config/rig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig", - "rigProfile": "node" -} diff --git a/foundations/core/packages/storage/jest.config.js b/foundations/core/packages/storage/jest.config.js deleted file mode 100644 index 069ea8aa0d..0000000000 --- a/foundations/core/packages/storage/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ['./src'], - coverageReporters: ['text-summary', 'html', 'lcov'] -} diff --git a/foundations/core/packages/storage/package.json b/foundations/core/packages/storage/package.json deleted file mode 100644 index be13fe5431..0000000000 --- a/foundations/core/packages/storage/package.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "name": "@hcengineering/storage", - "version": "0.7.17", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "author": "Anticrm Platform Contributors", - "template": "@hcengineering/node-package", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "test": "jest --passWithNoTests --silent --forceExit --coverage", - "build:watch": "compile", - "format": "format src", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --forceExit --coverage", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "workspace:^0.7.19", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "@typescript-eslint/parser": "^6.21.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.6.2", - "typescript": "^5.9.3", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5", - "@types/node": "^22.18.1", - "eslint-plugin-svelte": "^2.35.1" - }, - "dependencies": { - "@hcengineering/platform": "workspace:^0.7.18", - "@hcengineering/core": "workspace:^0.7.22", - "fast-equals": "^5.2.2" - }, - "files": [ - "lib/**/*", - "!lib/**/__test__/**", - "types/**/*", - "!types/**/__test__/**", - "src/**/*", - "!src/**/__test__/**", - "tsconfig.json" - ], - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.js" - } - }, - "repository": "https://github.com/hcengineering/huly.core", - "publishConfig": { - "access": "public" - } -} diff --git a/foundations/core/packages/storage/src/index.ts b/foundations/core/packages/storage/src/index.ts deleted file mode 100644 index 255680bb64..0000000000 --- a/foundations/core/packages/storage/src/index.ts +++ /dev/null @@ -1,233 +0,0 @@ -// -// Copyright © 2024 Anticrm Platform Contributors. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - type WorkspaceIds, - type Blob, - type MeasureContext, - type StorageIterator, - type WorkspaceDataId -} from '@hcengineering/core' -import { PlatformError, unknownError } from '@hcengineering/platform' -import { type Readable } from 'stream' - -export type ListBlobResult = Omit & { contentType?: string } -export interface UploadedObjectInfo { - etag: string - versionId: string | null -} - -export interface BlobStorageIterator { - next: () => Promise - close: () => Promise -} - -export interface BucketInfo { - name: string - delete: () => Promise - list: () => Promise -} - -export interface StorageAdapter { - initialize: (ctx: MeasureContext, wsIds: WorkspaceIds) => Promise - - close: () => Promise - - exists: (ctx: MeasureContext, wsIds: WorkspaceIds) => Promise - make: (ctx: MeasureContext, wsIds: WorkspaceIds) => Promise - delete: (ctx: MeasureContext, wsIds: WorkspaceIds) => Promise - - listBuckets: (ctx: MeasureContext) => Promise - remove: (ctx: MeasureContext, wsIds: WorkspaceIds, objectNames: string[]) => Promise - listStream: (ctx: MeasureContext, wsIds: WorkspaceIds) => Promise - stat: (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string) => Promise - get: (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string) => Promise - put: ( - ctx: MeasureContext, - wsIds: WorkspaceIds, - objectName: string, - stream: Readable | Buffer | string, - contentType: string, - size?: number - ) => Promise - read: (ctx: MeasureContext, wsIds: WorkspaceIds, name: string) => Promise - partial: ( - ctx: MeasureContext, - wsIds: WorkspaceIds, - objectName: string, - offset: number, - length?: number - ) => Promise - - getUrl: (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string) => Promise -} - -export interface NamedStorageAdapter { - name: string - adapter: StorageAdapter -} - -export interface StorageAdapterEx extends StorageAdapter { - adapters?: NamedStorageAdapter[] - - find: (ctx: MeasureContext, wsIds: WorkspaceIds) => StorageIterator -} - -/** - * Ad dummy storage adapter for tests - */ -export class DummyStorageAdapter implements StorageAdapter, StorageAdapterEx { - defaultAdapter: string = '' - async syncBlobFromStorage (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise { - throw new PlatformError(unknownError('Method not implemented')) - } - - async initialize (ctx: MeasureContext, wsIds: WorkspaceIds): Promise {} - - async close (): Promise {} - - async exists (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { - return false - } - - find (ctx: MeasureContext, wsIds: WorkspaceIds): StorageIterator { - return { - next: async (ctx) => [], - close: async (ctx) => {} - } - } - - async listBuckets (ctx: MeasureContext): Promise { - return [] - } - - async make (ctx: MeasureContext, wsIds: WorkspaceIds): Promise {} - - async delete (ctx: MeasureContext, wsIds: WorkspaceIds): Promise {} - - async remove (ctx: MeasureContext, wsIds: WorkspaceIds, objectNames: string[]): Promise {} - - async list (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { - return [] - } - - async listStream (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { - return { - next: async (): Promise => { - return [] - }, - close: async () => {} - } - } - - async stat (ctx: MeasureContext, wsIds: WorkspaceIds, name: string): Promise { - return undefined - } - - async get (ctx: MeasureContext, wsIds: WorkspaceIds, name: string): Promise { - throw new Error('not implemented') - } - - async partial ( - ctx: MeasureContext, - wsIds: WorkspaceIds, - objectName: string, - offset: number, - length?: number | undefined - ): Promise { - throw new Error('not implemented') - } - - async read (ctx: MeasureContext, wsIds: WorkspaceIds, name: string): Promise { - throw new Error('not implemented') - } - - async put ( - ctx: MeasureContext, - wsIds: WorkspaceIds, - objectName: string, - stream: string | Readable | Buffer, - contentType: string, - size?: number | undefined - ): Promise { - throw new Error('not implemented') - } - - async getUrl (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise { - throw new Error('not implemented') - } -} - -export function createDummyStorageAdapter (): StorageAdapter { - return new DummyStorageAdapter() -} - -export async function removeAllObjects ( - ctx: MeasureContext, - storage: StorageAdapter, - wsIds: WorkspaceIds -): Promise { - ctx.warn('removing all objects from workspace', wsIds) - // We need to list all files and delete them - const iterator = await storage.listStream(ctx, wsIds) - try { - let bulk: string[] = [] - while (true) { - const objs = await iterator.next() - if (objs.length === 0) { - break - } - for (const obj of objs) { - bulk.push(obj._id) - if (bulk.length > 50) { - await storage.remove(ctx, wsIds, bulk) - bulk = [] - } - } - } - if (bulk.length > 0) { - await storage.remove(ctx, wsIds, bulk) - bulk = [] - } - } finally { - await iterator.close() - } -} - -export async function objectsToArray ( - ctx: MeasureContext, - storage: StorageAdapter, - wsIds: WorkspaceIds -): Promise { - // We need to list all files and delete them - const iterator = await storage.listStream(ctx, wsIds) - try { - const bulk: ListBlobResult[] = [] - while (true) { - const obj = await iterator.next() - if (obj.length === 0) { - break - } - bulk.push(...obj) - } - return bulk - } finally { - await iterator.close() - } -} - -export function getDataId (wsIds: WorkspaceIds): WorkspaceDataId { - return wsIds.dataId ?? (wsIds.uuid as unknown as WorkspaceDataId) -} diff --git a/foundations/core/packages/storage/tsconfig.json b/foundations/core/packages/storage/tsconfig.json deleted file mode 100644 index c6a877cf6c..0000000000 --- a/foundations/core/packages/storage/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/node/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/foundations/core/packages/text-core/.eslintrc.js b/foundations/core/packages/text-core/.eslintrc.js deleted file mode 100644 index 72235dc283..0000000000 --- a/foundations/core/packages/text-core/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/foundations/core/packages/text-core/CHANGELOG.json b/foundations/core/packages/text-core/CHANGELOG.json deleted file mode 100644 index e6414f8cc7..0000000000 --- a/foundations/core/packages/text-core/CHANGELOG.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "name": "@hcengineering/text-core", - "entries": [ - { - "version": "0.7.18", - "tag": "@hcengineering/text-core_v0.7.18", - "date": "Mon, 27 Oct 2025 16:46:51 GMT", - "comments": { - "patch": [ - { - "comment": "add support for textColor and textStyle marks" - } - ] - } - }, - { - "version": "0.7.17", - "tag": "@hcengineering/text-core_v0.7.17", - "date": "Mon, 27 Oct 2025 13:27:12 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" - } - ] - } - }, - { - "version": "0.7.5", - "tag": "@hcengineering/text-core_v0.7.5", - "date": "Tue, 14 Oct 2025 04:58:17 GMT", - "comments": { - "patch": [ - { - "comment": "update deps" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`" - } - ] - } - }, - { - "version": "0.7.4", - "tag": "@hcengineering/text-core_v0.7.4", - "date": "Sat, 11 Oct 2025 18:20:33 GMT", - "comments": { - "patch": [ - { - "comment": "Update to latest platform rig" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`" - } - ] - } - }, - { - "version": "0.7.3", - "tag": "@hcengineering/text-core_v0.7.3", - "date": "Wed, 08 Oct 2025 03:40:53 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`" - } - ] - } - } - ] -} diff --git a/foundations/core/packages/text-core/CHANGELOG.md b/foundations/core/packages/text-core/CHANGELOG.md deleted file mode 100644 index 1c5c41a8c8..0000000000 --- a/foundations/core/packages/text-core/CHANGELOG.md +++ /dev/null @@ -1,35 +0,0 @@ -# Change Log - @hcengineering/text-core - -This log was last generated on Mon, 27 Oct 2025 16:46:51 GMT and should not be manually modified. - -## 0.7.18 -Mon, 27 Oct 2025 16:46:51 GMT - -### Patches - -- add support for textColor and textStyle marks - -## 0.7.17 -Mon, 27 Oct 2025 13:27:12 GMT - -_Version update only_ - -## 0.7.5 -Tue, 14 Oct 2025 04:58:17 GMT - -### Patches - -- update deps - -## 0.7.4 -Sat, 11 Oct 2025 18:20:33 GMT - -### Patches - -- Update to latest platform rig - -## 0.7.3 -Wed, 08 Oct 2025 03:40:53 GMT - -_Initial release_ - diff --git a/foundations/core/packages/text-core/config/rig.json b/foundations/core/packages/text-core/config/rig.json deleted file mode 100644 index 0110930f55..0000000000 --- a/foundations/core/packages/text-core/config/rig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig" -} diff --git a/foundations/core/packages/text-core/jest.config.js b/foundations/core/packages/text-core/jest.config.js deleted file mode 100644 index 069ea8aa0d..0000000000 --- a/foundations/core/packages/text-core/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ['./src'], - coverageReporters: ['text-summary', 'html', 'lcov'] -} diff --git a/foundations/core/packages/text-core/package.json b/foundations/core/packages/text-core/package.json deleted file mode 100644 index d9db61d2ee..0000000000 --- a/foundations/core/packages/text-core/package.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "name": "@hcengineering/text-core", - "version": "0.7.18", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "files": [ - "lib/**/*", - "types/**/*", - "src/**/*", - "!src/**/__test__/**", - "tsconfig.json" - ], - "author": "Anticrm Platform Contributors", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "test": "jest --passWithNoTests --silent --coverage", - "build:watch": "compile", - "format": "format src", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --coverage", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "workspace:^0.7.19", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "@typescript-eslint/parser": "^6.21.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.6.2", - "typescript": "^5.9.3", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5", - "@types/markdown-it": "~13.0.0", - "jest-environment-jsdom": "^30.2.0", - "eslint-plugin-svelte": "^2.35.1" - }, - "dependencies": { - "@hcengineering/core": "workspace:^0.7.22", - "fast-equals": "^5.2.2", - "hash-it": "^6.0.0" - }, - "repository": "https://github.com/hcengineering/huly.core", - "publishConfig": { - "access": "public" - }, - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.js" - } - } -} diff --git a/foundations/core/packages/text-core/src/index.ts b/foundations/core/packages/text-core/src/index.ts deleted file mode 100644 index 5e2540fdb2..0000000000 --- a/foundations/core/packages/text-core/src/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -// -// Copyright © 2023 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export * from './markup/dsl' -export * from './markup/model' -export * from './markup/reference' -export * from './markup/traverse' -export * from './markup/utils' diff --git a/foundations/core/packages/text-core/src/markup/__tests__/traverse.test.ts b/foundations/core/packages/text-core/src/markup/__tests__/traverse.test.ts deleted file mode 100644 index cafd4506dd..0000000000 --- a/foundations/core/packages/text-core/src/markup/__tests__/traverse.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { MarkupNode, MarkupNodeType } from '../model' -import { traverseAllMarks, traverseNode, traverseNodeMarks } from '../traverse' - -describe('traverseNode', () => { - it('should call the callback function for each node', () => { - const callback = jest.fn() - const node = { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'Hello, world!' - } - ] - } - - traverseNode(node as MarkupNode, callback) - - expect(callback).toHaveBeenCalledTimes(2) - expect(callback).toHaveBeenCalledWith(node, undefined) - expect(callback).toHaveBeenCalledWith(node.content[0], node) - }) - - it('should stop traversing if the callback returns false', () => { - const callback = jest.fn((node) => { - if (node.type === MarkupNodeType.paragraph) { - return false - } - }) - const node = { - type: MarkupNodeType.paragraph, - content: [ - { - type: MarkupNodeType.text, - text: 'Hello, world!' - } - ] - } - - traverseNode(node, callback) - - expect(callback).toHaveBeenCalledTimes(1) - expect(callback).toHaveBeenCalledWith(node, undefined) - }) -}) - -describe('traverseNodeMarks', () => { - it('should call the callback function for each mark', () => { - const callback = jest.fn() - const node = { - type: 'paragraph', - marks: [{ type: 'bold' }, { type: 'italic' }, { type: 'underline' }] - } - - traverseNodeMarks(node as MarkupNode, callback) - - expect(callback).toHaveBeenCalledTimes(3) - expect(callback).toHaveBeenCalledWith(node.marks[0]) - expect(callback).toHaveBeenCalledWith(node.marks[1]) - expect(callback).toHaveBeenCalledWith(node.marks[2]) - }) - - it('should not call the callback function if marks are not present', () => { - const callback = jest.fn() - const node = { - type: MarkupNodeType.paragraph - } - - traverseNodeMarks(node, callback) - - expect(callback).not.toHaveBeenCalled() - }) -}) - -describe('traverseAllMarks', () => { - it('should traverse all marks and call the callback function', () => { - const callback = jest.fn() - const node = { - type: 'paragraph', - marks: [{ type: 'bold' }], - content: [ - { - type: MarkupNodeType.text, - text: 'Hello, world!', - marks: [{ type: 'italic' }, { type: 'underline' }] - } - ] - } - - traverseAllMarks(node as MarkupNode, callback) - - expect(callback).toHaveBeenCalledTimes(3) - expect(callback).toHaveBeenCalledWith(node, node.marks[0]) - expect(callback).toHaveBeenCalledWith(node.content[0], node.content[0].marks[0]) - expect(callback).toHaveBeenCalledWith(node.content[0], node.content[0].marks[1]) - }) -}) diff --git a/foundations/core/packages/text-core/src/markup/__tests__/utils.test.ts b/foundations/core/packages/text-core/src/markup/__tests__/utils.test.ts deleted file mode 100644 index 740aa1c64f..0000000000 --- a/foundations/core/packages/text-core/src/markup/__tests__/utils.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { hashAttrs, stripHash } from '../utils' - -describe('hashAttrs', () => { - it('should return a hash of length 8', () => { - const attrs = { a: 1 } - const hash = hashAttrs(attrs) - expect(hash.length).toEqual(8) - }) - it('should return the same hash for the same attrs', () => { - const attrs = { a: 1, b: 2 } - const hash1 = hashAttrs(attrs) - const hash2 = hashAttrs(attrs) - expect(hash1).toEqual(hash2) - }) - - it('should return different hashes for different attrs', () => { - const attrs1 = { a: 1, b: 2 } - const attrs2 = { a: 1, b: 3 } - const hash1 = hashAttrs(attrs1) - const hash2 = hashAttrs(attrs2) - expect(hash1).not.toEqual(hash2) - }) -}) - -describe('stripHash', () => { - it('should return the name without the hash', () => { - const name = 'bold--c0decafe' - const result = stripHash(name) - expect(result).toEqual('bold') - }) - - it('should return the original name if no hash is present', () => { - const name = 'bold' - const result = stripHash(name) - expect(result).toEqual(name) - }) - - it('should return the original name if the hash is not 8 characters long', () => { - const name = 'bold--1234567' - const result = stripHash(name) - expect(result).toEqual(name) - }) - - it('should return the original name if the hash is not a valid base64 string', () => { - const name = 'bold--invalid!' - const result = stripHash(name) - expect(result).toEqual(name) - }) -}) diff --git a/foundations/core/packages/text-core/src/markup/dsl.ts b/foundations/core/packages/text-core/src/markup/dsl.ts deleted file mode 100644 index f72a1bb2db..0000000000 --- a/foundations/core/packages/text-core/src/markup/dsl.ts +++ /dev/null @@ -1,81 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { MarkupMark, MarkupMarkType, MarkupNode, MarkupNodeType } from './model' - -// Nodes - -export function nodeDoc (...content: MarkupNode[]): MarkupNode { - return node(MarkupNodeType.doc, ...content) -} - -export function nodeParagraph (...content: MarkupNode[]): MarkupNode { - return node(MarkupNodeType.paragraph, ...content) -} - -export function nodeText (text: string): MarkupNode { - return { type: MarkupNodeType.text, text } -} - -export function nodeImage (attrs: { src: string, alt?: string, width?: number, height?: number }): MarkupNode { - return { type: MarkupNodeType.image, attrs } -} - -export function nodeReference (attrs: { id: string, label: string, objectclass: string }): MarkupNode { - return { type: MarkupNodeType.reference, attrs } -} - -// Marks - -export function markBold (node: MarkupNode): MarkupNode { - return withMarks(node, mark(MarkupMarkType.bold)) -} - -export function markCode (node: MarkupNode): MarkupNode { - return withMarks(node, mark(MarkupMarkType.code)) -} - -export function markItalic (node: MarkupNode): MarkupNode { - return withMarks(node, mark(MarkupMarkType.em)) -} - -export function markStrike (node: MarkupNode): MarkupNode { - return withMarks(node, mark(MarkupMarkType.strike)) -} - -export function markUnderline (node: MarkupNode): MarkupNode { - return withMarks(node, mark(MarkupMarkType.underline)) -} - -export function markLink (attrs: { href: string, title: string }, node: MarkupNode): MarkupNode { - return withMarks(node, mark(MarkupMarkType.link, attrs)) -} - -// Utility - -function node (type: MarkupNodeType, ...content: MarkupNode[]): MarkupNode { - return { type, content } -} - -function mark (type: MarkupMarkType, attrs?: Record): MarkupMark { - return { type, attrs: attrs ?? {} } -} - -function withMarks (node: MarkupNode, ...marks: MarkupMark[]): MarkupNode { - const current = node.marks ?? [] - current.push(...marks) - - return { ...node, marks: current } -} diff --git a/foundations/core/packages/text-core/src/markup/model.ts b/foundations/core/packages/text-core/src/markup/model.ts deleted file mode 100644 index f061ca84cd..0000000000 --- a/foundations/core/packages/text-core/src/markup/model.ts +++ /dev/null @@ -1,96 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -/** @public */ -export enum MarkupNodeType { - doc = 'doc', - paragraph = 'paragraph', - blockquote = 'blockquote', - horizontal_rule = 'horizontalRule', - heading = 'heading', - code_block = 'codeBlock', - text = 'text', - image = 'image', - file = 'file', - reference = 'reference', - emoji = 'emoji', - hard_break = 'hardBreak', - ordered_list = 'orderedList', - bullet_list = 'bulletList', - list_item = 'listItem', - taskList = 'taskList', - taskItem = 'taskItem', - todoList = 'todoList', - todoItem = 'todoItem', - subLink = 'subLink', - table = 'table', - table_row = 'tableRow', - table_cell = 'tableCell', - table_header = 'tableHeader', - mermaid = 'mermaid', - comment = 'comment', - markdown = 'markdown', - embed = 'embed' -} - -/** @public */ -export enum MarkupMarkType { - link = 'link', - em = 'italic', - bold = 'bold', - code = 'code', - strike = 'strike', - underline = 'underline', - textColor = 'textColor', - textStyle = 'textStyle' -} - -/** @public */ -export interface MarkupMark { - type: MarkupMarkType - attrs?: Record // A map of attributes -} - -export type AttrValue = string | number | boolean | null | undefined -export type Attrs = Record - -/** @public */ -export interface MarkupNode { - type: MarkupNodeType - content?: MarkupNode[] // A list of child nodes - marks?: MarkupMark[] - attrs?: Attrs - text?: string -} - -/** @public */ -export function emptyMarkupNode (): MarkupNode { - return { - type: MarkupNodeType.doc, - content: [{ type: MarkupNodeType.paragraph, content: [] }] - } -} - -/** @public */ -export interface LinkMark extends MarkupMark { - href: string - title: string -} - -/** @public */ -export interface ReferenceMarkupNode extends MarkupNode { - type: MarkupNodeType.reference - attrs: { id: string, label: string, objectclass: string } -} diff --git a/foundations/core/packages/text-core/src/markup/reference.ts b/foundations/core/packages/text-core/src/markup/reference.ts deleted file mode 100644 index a9e4c64261..0000000000 --- a/foundations/core/packages/text-core/src/markup/reference.ts +++ /dev/null @@ -1,49 +0,0 @@ -// -// Copyright © 2023 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { Class, Doc, Ref } from '@hcengineering/core' -import { MarkupNode, MarkupNodeType, ReferenceMarkupNode } from '../markup/model' -import { traverseNode } from '../markup/traverse' - -/** - * @public - */ -export interface Reference { - objectId: Ref - objectClass: Ref> - parentNode: MarkupNode | null -} - -/** - * @public - */ -export function extractReferences (content: MarkupNode): Array { - const result: Array = [] - - traverseNode(content, (node, parent) => { - if (node.type === MarkupNodeType.reference) { - const reference = node as ReferenceMarkupNode - const objectId = reference.attrs.id as Ref - const objectClass = reference.attrs.objectclass as Ref> - const e = result.find((e) => e.objectId === objectId && e.objectClass === objectClass) - if (e === undefined) { - result.push({ objectId, objectClass, parentNode: parent ?? node }) - } - } - return true - }) - - return result -} diff --git a/foundations/core/packages/text-core/src/markup/traverse.ts b/foundations/core/packages/text-core/src/markup/traverse.ts deleted file mode 100644 index 08f6dc1e5e..0000000000 --- a/foundations/core/packages/text-core/src/markup/traverse.ts +++ /dev/null @@ -1,57 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { MarkupMark, MarkupNode } from './model' - -export function traverseNode ( - node: MarkupNode, - fn: (el: MarkupNode, parent: MarkupNode | undefined) => boolean | undefined -): void { - _traverseNode(node, undefined, fn) -} - -function _traverseNode ( - node: MarkupNode, - parent: MarkupNode | undefined, - fn: (el: MarkupNode, parent: MarkupNode | undefined) => boolean | undefined -): void { - const result = fn(node, parent) - if (result !== false) { - node.content?.forEach((p) => { - _traverseNode(p, node, fn) - }) - } -} - -export function traverseNodeMarks (node: MarkupNode, f: (el: MarkupMark) => void): void { - node.marks?.forEach((p) => { - f(p) - }) -} - -export function traverseNodeContent (node: MarkupNode, f: (el: MarkupNode) => void): void { - node.content?.forEach((p) => { - f(p) - }) -} - -export function traverseAllMarks (node: MarkupNode, f: (el: MarkupNode, mark: MarkupMark) => void): void { - traverseNode(node, (node) => { - traverseNodeMarks(node, (mark) => { - f(node, mark) - }) - return true - }) -} diff --git a/foundations/core/packages/text-core/src/markup/utils.ts b/foundations/core/packages/text-core/src/markup/utils.ts deleted file mode 100644 index dd5efd7630..0000000000 --- a/foundations/core/packages/text-core/src/markup/utils.ts +++ /dev/null @@ -1,229 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { Markup } from '@hcengineering/core' - -import { deepEqual } from 'fast-equals' -import hashIt from 'hash-it' - -import { nodeDoc, nodeParagraph, nodeText } from './dsl' -import { MarkupMark, MarkupMarkType, MarkupNode, MarkupNodeType, emptyMarkupNode } from './model' -import { traverseAllMarks, traverseNode } from './traverse' - -/** @public */ -export const EmptyMarkup: Markup = jsonToMarkup(emptyMarkupNode()) - -/** @public */ -export function isEmptyMarkup (markup: Markup | undefined): boolean { - if (markup === undefined || markup === null || markup === '') { - return true - } - return isEmptyNode(markupToJSON(markup)) -} - -/** @public */ -export function areEqualMarkups (markup1: Markup, markup2: Markup): boolean { - if (markup1 === markup2) { - return true - } - - const node1 = markupToJSON(markup1) - const node2 = markupToJSON(markup2) - - if (isEmptyNode(node1) && isEmptyNode(node2)) { - return true - } - - return equalNodes(node1, node2) -} - -/** @public */ -export function areEqualJson (json1: MarkupNode, json2: MarkupNode): boolean { - return equalNodes(json1, json2) -} - -function equalNodes (node1: MarkupNode, node2: MarkupNode): boolean { - if (node1.type !== node2.type) return false - - const text1 = node1.text ?? '' - const text2 = node2.text ?? '' - if (text1 !== text2) return false - - if (!equalArrays(node1.content, node2.content, equalNodes)) return false - if (!equalArrays(node1.marks, node2.marks, equalMarks)) return false - if (!equalRecords(node1.attrs, node2.attrs)) return false - - return true -} - -function equalArrays (a: T[] | undefined, b: T[] | undefined, equal: (a: T, b: T) => boolean): boolean { - if (a === b) return true - const arr1 = a ?? [] - const arr2 = b ?? [] - if (arr1.length !== arr2.length) return false - return arr1.every((item1, i) => equal(item1, arr2[i])) -} - -function equalRecords (a: Record | undefined, b: Record | undefined): boolean { - if (a === b) return true - a = Object.fromEntries(Object.entries(a ?? {}).filter(([_, v]) => v != null)) - b = Object.fromEntries(Object.entries(b ?? {}).filter(([_, v]) => v != null)) - return deepEqual(a, b) -} - -export function equalMarks (a: MarkupMark, b: MarkupMark): boolean { - return a.type === b.type && equalRecords(a.attrs, b.attrs) -} - -const emptyNodes = [MarkupNodeType.hard_break] - -const nonEmptyNodes = [ - MarkupNodeType.horizontal_rule, - MarkupNodeType.image, - MarkupNodeType.reference, - MarkupNodeType.emoji, - MarkupNodeType.subLink, - MarkupNodeType.table -] - -/** @public */ -export function isEmptyNode (node: MarkupNode): boolean { - if (emptyNodes.includes(node.type)) return true - if (nonEmptyNodes.includes(node.type)) return false - if (node.text !== undefined && node.text?.trim().length > 0) return false - - const content = node.content ?? [] - return content.every(isEmptyNode) -} - -// Markup - -/** @public */ -export function jsonToMarkup (json: MarkupNode): Markup { - return JSON.stringify(json) -} - -/** @public */ -export function markupToJSON (markup: Markup): MarkupNode { - if (markup == null || markup === '') { - return emptyMarkupNode() - } - - try { - // Ideally Markup should contain only serialized JSON - // But there seem to be some cases when it contains HTML or plain text - // So we need to handle those cases and produce valid MarkupNode - if (markup.startsWith('{')) { - const json = JSON.parse(markup) as MarkupNode - traverseAllMarks(json, (node, mark) => { - mark.type = stripHash(mark.type) as MarkupMarkType - }) - return json - } else { - return nodeDoc(nodeParagraph(nodeText(markup))) - } - } catch (error) { - return emptyMarkupNode() - } -} - -// UTILS - -const ELLIPSIS_CHAR = '…' -const WHITESPACE = ' ' - -/** @public */ -export function stripTags (markup: Markup, textLimit = 0): string { - const parsed = markupToJSON(markup) - - const textParts: string[] = [] - let charCount = 0 - let isHardStop = false - - const pushText = (text: string): void => { - if (textLimit > 0 && charCount + text.length > textLimit) { - const toAddCount = textLimit - charCount - const textPart = text.substring(0, toAddCount) - textParts.push(textPart) - textParts.push(ELLIPSIS_CHAR) - isHardStop = true - } else { - textParts.push(text) - charCount += text.length - } - } - - traverseNode(parsed, (node, parent): boolean => { - if (isHardStop) { - return false - } - - if (node.type === MarkupNodeType.text) { - const text = node.text ?? '' - pushText(text) - return false - } else if ( - node.type === MarkupNodeType.paragraph || - node.type === MarkupNodeType.table || - node.type === MarkupNodeType.doc || - node.type === MarkupNodeType.blockquote - ) { - if (textParts.length > 0 && textParts[textParts.length - 1] !== WHITESPACE) { - textParts.push(WHITESPACE) - charCount++ - } - } else if (node.type === MarkupNodeType.reference) { - const label = `${node.attrs?.label ?? ''}` - pushText(label.length > 0 ? `@${label}` : '') - } - return true - }) - - const result = textParts.join('') - return result -} - -export function markupToText (markup: Markup): string { - const jsonModel = markupToJSON(markup) - const fragments: string[] = [] - - traverseNode(jsonModel, (node) => { - if (node.type === MarkupNodeType.text) { - const text = node.text ?? '' - if (node.text !== undefined && node.text.length > 0) { - fragments.push(text) - } - } else if (node.type === MarkupNodeType.paragraph) { - fragments.push('\n\n') - } - return true - }) - - return fragments.join('').trim() -} - -// see https://github.com/yjs/y-prosemirror/blob/master/src/lib.js#L402 -const hashedMarkNameRegex = /(.*)(--[a-zA-Z0-9+/=]{8})$/ - -/** @public */ -export function stripHash (attrName: string): string { - return hashedMarkNameRegex.exec(attrName)?.[1] ?? attrName -} - -/** @public */ -export function hashAttrs (attrs: any): string { - const hash = hashIt(attrs) - return (hash >>> 0).toString(16).padStart(8, '0') -} diff --git a/foundations/core/packages/text-core/tsconfig.json b/foundations/core/packages/text-core/tsconfig.json deleted file mode 100644 index b5ae22f6e4..0000000000 --- a/foundations/core/packages/text-core/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/foundations/core/packages/text-html/.eslintrc.js b/foundations/core/packages/text-html/.eslintrc.js deleted file mode 100644 index 72235dc283..0000000000 --- a/foundations/core/packages/text-html/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/foundations/core/packages/text-html/CHANGELOG.json b/foundations/core/packages/text-html/CHANGELOG.json deleted file mode 100644 index f3e5322660..0000000000 --- a/foundations/core/packages/text-html/CHANGELOG.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "name": "@hcengineering/text-html", - "entries": [ - { - "version": "0.7.18", - "tag": "@hcengineering/text-html_v0.7.18", - "date": "Mon, 27 Oct 2025 16:46:51 GMT", - "comments": { - "patch": [ - { - "comment": "add support for textColor and textStyle marks" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/text-core\" from `^0.7.17` to `0.7.18`" - } - ] - } - }, - { - "version": "0.7.5", - "tag": "@hcengineering/text-html_v0.7.5", - "date": "Tue, 14 Oct 2025 04:58:17 GMT", - "comments": { - "patch": [ - { - "comment": "update deps" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/text-core\" from `^0.7.4` to `0.7.5`" - } - ] - } - }, - { - "version": "0.7.4", - "tag": "@hcengineering/text-html_v0.7.4", - "date": "Sat, 11 Oct 2025 18:20:33 GMT", - "comments": { - "patch": [ - { - "comment": "Update to latest platform rig" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/text-core\" from `^0.7.3` to `0.7.4`" - } - ] - } - } - ] -} diff --git a/foundations/core/packages/text-html/CHANGELOG.md b/foundations/core/packages/text-html/CHANGELOG.md deleted file mode 100644 index e1465e07a7..0000000000 --- a/foundations/core/packages/text-html/CHANGELOG.md +++ /dev/null @@ -1,25 +0,0 @@ -# Change Log - @hcengineering/text-html - -This log was last generated on Mon, 27 Oct 2025 16:46:51 GMT and should not be manually modified. - -## 0.7.18 -Mon, 27 Oct 2025 16:46:51 GMT - -### Patches - -- add support for textColor and textStyle marks - -## 0.7.5 -Tue, 14 Oct 2025 04:58:17 GMT - -### Patches - -- update deps - -## 0.7.4 -Sat, 11 Oct 2025 18:20:33 GMT - -### Patches - -- Update to latest platform rig - diff --git a/foundations/core/packages/text-html/config/rig.json b/foundations/core/packages/text-html/config/rig.json deleted file mode 100644 index 0110930f55..0000000000 --- a/foundations/core/packages/text-html/config/rig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig" -} diff --git a/foundations/core/packages/text-html/jest.config.js b/foundations/core/packages/text-html/jest.config.js deleted file mode 100644 index 069ea8aa0d..0000000000 --- a/foundations/core/packages/text-html/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ['./src'], - coverageReporters: ['text-summary', 'html', 'lcov'] -} diff --git a/foundations/core/packages/text-html/package.json b/foundations/core/packages/text-html/package.json deleted file mode 100644 index a0921a2bb4..0000000000 --- a/foundations/core/packages/text-html/package.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "name": "@hcengineering/text-html", - "version": "0.7.18", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "files": [ - "lib/**/*", - "types/**/*", - "src/**/*", - "!src/**/__test__/**", - "tsconfig.json" - ], - "author": "Anticrm Platform Contributors", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "test": "jest --passWithNoTests --silent --coverage", - "build:watch": "compile", - "format": "format src", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --coverage", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "workspace:^0.7.19", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "@typescript-eslint/parser": "^6.21.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.6.2", - "typescript": "^5.9.3", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5", - "eslint-plugin-svelte": "^2.35.1" - }, - "dependencies": { - "@hcengineering/text-core": "workspace:^0.7.18", - "htmlparser2": "^9.0.0" - }, - "repository": "https://github.com/hcengineering/huly.core", - "publishConfig": { - "access": "public" - }, - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.js" - } - } -} diff --git a/foundations/core/packages/text-html/src/__tests__/html.test.ts b/foundations/core/packages/text-html/src/__tests__/html.test.ts deleted file mode 100644 index 7df7fb1c60..0000000000 --- a/foundations/core/packages/text-html/src/__tests__/html.test.ts +++ /dev/null @@ -1,581 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { MarkupNode } from '@hcengineering/text-core' -import { htmlToMarkup, markupToHtml } from '..' - -const tests: Array<{ name: string, markup: object, html: string }> = [ - { - name: 'paragraph', - markup: { - type: 'doc', - content: [ - { - type: 'paragraph', - attrs: { - textAlign: null - }, - content: [ - { - type: 'text', - text: 'paragraph 1' - } - ] - } - ] - }, - html: '

paragraph 1

' - }, - { - name: 'text alignment', - markup: { - type: 'doc', - content: [ - { - type: 'heading', - attrs: { - level: 1, - textAlign: 'left' - }, - content: [ - { - type: 'text', - text: 'heading 1' - } - ] - }, - { - type: 'paragraph', - attrs: { - textAlign: 'right' - }, - content: [ - { - type: 'text', - text: 'paragraph 1' - } - ] - } - ] - }, - html: '

heading 1

paragraph 1

' - }, - { - name: 'headings', - markup: { - type: 'doc', - content: [ - { - type: 'heading', - attrs: { - level: 1, - textAlign: null - }, - content: [ - { - type: 'text', - text: 'heading 1' - } - ] - }, - { - type: 'heading', - attrs: { - level: 2, - textAlign: null - }, - content: [ - { - type: 'text', - text: 'heading 2' - } - ] - }, - { - type: 'heading', - attrs: { - level: 3, - textAlign: null - }, - content: [ - { - type: 'text', - text: 'heading 3' - } - ] - }, - { - type: 'heading', - attrs: { - level: 4, - textAlign: null - }, - content: [ - { - type: 'text', - text: 'heading 4' - } - ] - }, - { - type: 'heading', - attrs: { - level: 5, - textAlign: null - }, - content: [ - { - type: 'text', - text: 'heading 5' - } - ] - }, - { - type: 'heading', - attrs: { - level: 6, - textAlign: null - }, - content: [ - { - type: 'text', - text: 'heading 6' - } - ] - } - ] - }, - html: '

heading 1

heading 2

heading 3

heading 4

heading 5
heading 6
' - }, - { - name: 'blockquote', - markup: { - type: 'doc', - content: [ - { - type: 'blockquote', - content: [ - { - type: 'paragraph', - attrs: { - textAlign: null - }, - content: [ - { - type: 'text', - text: 'Lorem ipsum dolor sit amet.' - } - ] - } - ] - } - ] - }, - html: '

Lorem ipsum dolor sit amet.

' - }, - { - name: 'codeblock', - markup: { - type: 'doc', - content: [ - { - type: 'codeBlock', - attrs: { - language: 'typescript' - }, - content: [ - { - type: 'text', - text: 'const x: number = 42;' - } - ] - } - ] - }, - html: '
const x: number = 42;
' - }, - { - name: 'hr', - markup: { - type: 'doc', - content: [ - { - type: 'paragraph', - attrs: { - textAlign: null - }, - content: [ - { - type: 'text', - text: 'paragraph 1' - } - ] - }, - { - type: 'horizontalRule' - }, - { - type: 'paragraph', - attrs: { - textAlign: null - }, - content: [ - { - type: 'text', - text: 'paragraph 2' - } - ] - } - ] - }, - html: '

paragraph 1


paragraph 2

' - }, - { - name: 'br', - markup: { - type: 'doc', - content: [ - { - type: 'paragraph', - attrs: { - textAlign: null - }, - content: [ - { - type: 'text', - text: 'line 1' - }, - { - type: 'hardBreak' - }, - { - type: 'text', - text: 'line 2' - } - ] - } - ] - }, - html: '

line 1
line 2

' - }, - { - name: 'ordered list', - markup: { - type: 'doc', - content: [ - { - type: 'orderedList', - attrs: { - start: 1 - }, - content: [ - { - type: 'listItem', - content: [ - { - type: 'paragraph', - attrs: { - textAlign: null - }, - content: [ - { - type: 'text', - text: 'item 1' - } - ] - } - ] - }, - { - type: 'listItem', - content: [ - { - type: 'paragraph', - attrs: { - textAlign: null - }, - content: [ - { - type: 'text', - text: 'item 2' - } - ] - } - ] - }, - { - type: 'listItem', - content: [ - { - type: 'paragraph', - attrs: { - textAlign: null - }, - content: [ - { - type: 'text', - text: 'item 3' - } - ] - } - ] - } - ] - } - ] - }, - html: '
  1. item 1

  2. item 2

  3. item 3

' - }, - { - name: 'bullet list', - markup: { - type: 'doc', - content: [ - { - type: 'bulletList', - content: [ - { - type: 'listItem', - content: [ - { - type: 'paragraph', - attrs: { - textAlign: null - }, - content: [ - { - type: 'text', - text: 'item 1' - } - ] - } - ] - }, - { - type: 'listItem', - content: [ - { - type: 'paragraph', - attrs: { - textAlign: null - }, - content: [ - { - type: 'text', - text: 'item 2' - } - ] - } - ] - }, - { - type: 'listItem', - content: [ - { - type: 'paragraph', - attrs: { - textAlign: null - }, - content: [ - { - type: 'text', - text: 'item 3' - } - ] - } - ] - } - ] - } - ] - }, - html: '
  • item 1

  • item 2

  • item 3

' - }, - { - name: 'ref', - markup: { - type: 'doc', - content: [ - { - type: 'paragraph', - attrs: { - textAlign: null - }, - content: [ - { - type: 'text', - text: 'hello ' - }, - { - type: 'reference', - attrs: { - id: '64708c79c8f2613474dea38b', - objectclass: 'contact:class:Person', - label: 'John Doe' - } - } - ] - } - ] - }, - html: '

hello @John Doe

' - }, - { - name: 'embed', - markup: { - type: 'doc', - content: [ - { - type: 'paragraph', - attrs: { - textAlign: null - }, - content: [ - { - type: 'text', - text: 'hello ' - }, - { - type: 'embed', - attrs: { - src: 'http://localhost/embed' - } - } - ] - } - ] - }, - html: '

hello http://localhost/embed

' - }, - { - name: 'embed-uri-escape', - markup: { - type: 'doc', - content: [ - { - type: 'paragraph', - attrs: { - textAlign: null - }, - content: [ - { - type: 'text', - text: 'hello ' - }, - { - type: 'embed', - attrs: { - src: 'http://localhost/embed spaces' - } - } - ] - } - ] - }, - html: '

hello http://localhost/embed spaces

' - }, - { - name: 'embed-html-escape', - markup: { - type: 'doc', - content: [ - { - type: 'paragraph', - attrs: { - textAlign: null - }, - content: [ - { - type: 'text', - text: 'hello ' - }, - { - type: 'embed', - attrs: { - src: 'http://localhost/embed' - } - } - ] - } - ] - }, - html: '

hello http://localhost/embed<html>

' - }, - { - name: 'link-with-class', - markup: { - type: 'doc', - content: [ - { - type: 'paragraph', - attrs: { - textAlign: null - }, - content: [ - { - type: 'text', - text: 'https://example.com', - marks: [ - { - type: 'link', - attrs: { - href: 'https://example.com', - target: '_blank', - rel: 'noopener noreferrer', - class: 'cursor-pointer', - title: undefined - } - } - ] - } - ] - } - ] - }, - html: '

https://example.com

' - } -] - -describe('markupToHtml', () => { - describe('convert to html', () => { - tests.forEach(({ name, markup, html }) => { - it(name, () => { - const result = markupToHtml(markup as MarkupNode) - expect(result).toEqual(html) - }) - }) - }) -}) - -describe('htmlToMarkup', () => { - describe('convert to markup', () => { - tests.forEach(({ name, markup, html }) => { - it(name, () => { - const result = htmlToMarkup(html) - expect(result).toEqual(markup) - }) - }) - }) - - describe('convert to markup and back', () => { - tests.forEach(({ name, markup, html }) => { - it(name, () => { - const result = htmlToMarkup(html) - const serialized = markupToHtml(result) - expect(serialized).toEqual(html) - }) - }) - }) -}) diff --git a/foundations/core/packages/text-html/src/index.ts b/foundations/core/packages/text-html/src/index.ts deleted file mode 100644 index 1462f0008a..0000000000 --- a/foundations/core/packages/text-html/src/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { type MarkupNode } from '@hcengineering/text-core' -import { type HtmlParserOptions, HtmlParser } from './parser' -import { type HtmlSerializerOptions, HtmlSerializer } from './serializer' - -export function markupToHtml (markup: MarkupNode, options: HtmlSerializerOptions = {}): string { - const serializer = new HtmlSerializer(options) - return serializer.serialize(markup) -} - -export function htmlToMarkup (html: string, options: HtmlParserOptions = {}): MarkupNode { - const parser = new HtmlParser(options) - return parser.parse(html) -} diff --git a/foundations/core/packages/text-html/src/parser.ts b/foundations/core/packages/text-html/src/parser.ts deleted file mode 100644 index 44a8aa9fe9..0000000000 --- a/foundations/core/packages/text-html/src/parser.ts +++ /dev/null @@ -1,626 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - type AttrValue, - type MarkupMark, - type MarkupNode, - MarkupMarkType, - MarkupNodeType -} from '@hcengineering/text-core' -import { Parser } from 'htmlparser2' - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface HtmlParserOptions {} - -interface HtmlTagHandler { - handleOpenTag: (state: HtmlParseState, tag: string, attributes: Record) => void - handleCloseTag: (state: HtmlParseState, tag: string) => void -} - -interface HtmlNodeRule { - node: MarkupNodeType - getAttrs?: Record | ((attrs: Record) => Record | undefined) - wrapNode?: boolean - wrapContent?: boolean -} - -interface HtmlMarkRule { - mark: MarkupMarkType - getAttrs?: Record | ((attrs: Record) => Record | undefined) -} - -interface HtmlSpecialRule { - handleOpenTag: (state: HtmlParseState, tag: string, attributes: Record) => void - handleCloseTag: (state: HtmlParseState, tag: string) => void -} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -interface HtmlIgnoreRule {} - -interface HtmlStyleRule { - style: string - getAttrs?: (value: string) => Record | undefined -} - -class HtmlParseState { - private readonly stack: MarkupNode[] = [] - private readonly marks: MarkupMark[] = [] - - constructor ( - readonly root: MarkupNode, - readonly handlers: Record - ) { - this.stack.push(root) - } - - top (): MarkupNode | undefined { - return this.stack[this.stack.length - 1] - } - - addText (text: string): void { - const top = this.top() - if (top === undefined || text.length === 0) { - return - } - - // Auto-wrap text in paragraph if we're at the doc level - if (top.type === MarkupNodeType.doc) { - this.openNode(MarkupNodeType.paragraph) - const node: MarkupNode = - this.marks.length > 0 - ? { type: MarkupNodeType.text, text, marks: [...this.marks] } - : { type: MarkupNodeType.text, text } - this.push(node) - this.closeNode(MarkupNodeType.paragraph) - return - } - - const node: MarkupNode = - this.marks.length > 0 - ? { type: MarkupNodeType.text, text, marks: [...this.marks] } - : { type: MarkupNodeType.text, text } - - this.push(node) - } - - openMark (mark: MarkupMarkType, attrs?: Record): void { - this.marks.push(attrs !== undefined ? { type: mark, attrs } : { type: mark }) - } - - closeMark (mark: MarkupMarkType): void { - if (this.marks[this.marks.length - 1]?.type === mark) { - this.marks.pop() - } - } - - openNode (node: MarkupNodeType, attrs?: Record): void { - this.stack.push(attrs !== undefined ? { type: node, attrs } : { type: node }) - } - - closeNode (node: MarkupNodeType): void { - this.marks.splice(0) - const info = this.stack.pop() - if (info !== undefined) { - this.push(info) - } - } - - addNode (node: MarkupNode): void { - this.push(node) - } - - push (node: MarkupNode): void { - const parent = this.top() - if (parent !== undefined) { - const content = parent.content ?? [] - content.push(node) - parent.content = content - } - } -} - -function nodeHandler ({ node, getAttrs, wrapContent, wrapNode }: HtmlNodeRule): HtmlTagHandler { - const wrapStack: boolean[] = [] - - return { - handleOpenTag: (state: HtmlParseState, tag: string, attributes: Record) => { - const attrs = - typeof getAttrs === 'function' - ? getAttrs(attributes) - : typeof getAttrs === 'object' - ? { ...getAttrs } - : undefined - - const shouldWrapNode = wrapNode === true && state.top()?.type !== MarkupNodeType.paragraph - - if (shouldWrapNode) { - state.openNode(MarkupNodeType.paragraph) - } - wrapStack.push(shouldWrapNode) - - state.openNode(node, attrs) - - if (wrapContent === true) { - state.openNode(MarkupNodeType.paragraph) - } - }, - handleCloseTag: (state: HtmlParseState, tag: string) => { - if (wrapContent === true && state.top()?.type === MarkupNodeType.paragraph) { - state.closeNode(MarkupNodeType.paragraph) - } - - state.closeNode(node) - if (wrapStack.pop() === true) { - state.closeNode(MarkupNodeType.paragraph) - } - } - } -} - -function markHandler ({ mark, getAttrs }: HtmlMarkRule): HtmlTagHandler { - return { - handleOpenTag: (state: HtmlParseState, tag: string, attributes: Record) => { - const attrs = - typeof getAttrs === 'function' - ? getAttrs(attributes) - : typeof getAttrs === 'object' - ? { ...getAttrs } - : undefined - state.openMark(mark, attrs) - }, - handleCloseTag: (state: HtmlParseState, tag: string) => { - state.closeMark(mark) - } - } -} - -function ignoreHandler (rule: HtmlIgnoreRule): HtmlTagHandler { - return { - handleOpenTag: () => {}, - handleCloseTag: () => {} - } -} - -function specialHandler (rule: HtmlSpecialRule): HtmlTagHandler { - return { - handleOpenTag: (state: HtmlParseState, tag: string, attributes: Record) => { - rule.handleOpenTag(state, tag, attributes) - }, - handleCloseTag: (state: HtmlParseState, tag: string) => { - rule.handleCloseTag(state, tag) - } - } -} - -const styleRules: HtmlStyleRule[] = [ - { - style: 'text-align', - getAttrs: (value: string) => { - return { textAlign: value ?? null } - } - } -] - -const markRules: Record = { - b: { - mark: MarkupMarkType.bold - }, - em: { - mark: MarkupMarkType.em - }, - i: { - mark: MarkupMarkType.em - }, - s: { - mark: MarkupMarkType.strike - }, - strong: { - mark: MarkupMarkType.bold - }, - u: { - mark: MarkupMarkType.underline - } -} - -const nodeRules: Record = { - h1: { - node: MarkupNodeType.heading, - getAttrs: (attributes: Record) => { - return { - level: 1, - textAlign: attributes.textAlign ?? null - } - } - }, - h2: { - node: MarkupNodeType.heading, - getAttrs: (attributes: Record) => { - return { - level: 2, - textAlign: attributes.textAlign ?? null - } - } - }, - h3: { - node: MarkupNodeType.heading, - getAttrs: (attributes: Record) => { - return { - level: 3, - textAlign: attributes.textAlign ?? null - } - } - }, - h4: { - node: MarkupNodeType.heading, - getAttrs: (attributes: Record) => { - return { - level: 4, - textAlign: attributes.textAlign ?? null - } - } - }, - h5: { - node: MarkupNodeType.heading, - getAttrs: (attributes: Record) => { - return { - level: 5, - textAlign: attributes.textAlign ?? null - } - } - }, - h6: { - node: MarkupNodeType.heading, - getAttrs: (attributes: Record) => { - return { - level: 6, - textAlign: attributes.textAlign ?? null - } - } - }, - blockquote: { - node: MarkupNodeType.blockquote - }, - pre: { - node: MarkupNodeType.code_block - }, - hr: { - node: MarkupNodeType.horizontal_rule - }, - br: { - node: MarkupNodeType.hard_break - }, - ol: { - node: MarkupNodeType.ordered_list, - getAttrs: (attributes: Record) => { - return { - start: parseInt(attributes.starts ?? '1') - } - } - }, - ul: { - node: MarkupNodeType.bullet_list - }, - li: { - node: MarkupNodeType.list_item - }, - img: { - node: MarkupNodeType.image, - wrapNode: true, - getAttrs: (attributes: Record) => { - return { - src: attributes.src, - alt: attributes.alt, - title: attributes.title ?? null, - width: attributes.width !== undefined ? parseInt(attributes.width) : undefined, - height: attributes.height !== undefined ? parseInt(attributes.height) : undefined, - 'file-id': attributes['file-id'] ?? null - } - } - }, - sub: { - node: MarkupNodeType.subLink - }, - table: { - node: MarkupNodeType.table - }, - tr: { - node: MarkupNodeType.table_row - }, - th: { - node: MarkupNodeType.table_header, - getAttrs: (attributes: Record) => { - return { - colspan: attributes.colspan !== undefined ? parseInt(attributes.colspan) : undefined, - rowspan: attributes.rowspan !== undefined ? parseInt(attributes.rowspan) : undefined, - colwidth: attributes.colwidth !== undefined ? parseInt(attributes.colwidth) : undefined - } - }, - wrapContent: true - }, - td: { - node: MarkupNodeType.table_cell, - getAttrs: (attributes: Record) => { - return { - colspan: attributes.colspan !== undefined ? parseInt(attributes.colspan) : undefined, - rowspan: attributes.rowspan !== undefined ? parseInt(attributes.rowspan) : undefined, - colwidth: attributes.colwidth !== undefined ? parseInt(attributes.colwidth) : undefined - } - }, - wrapContent: true - }, - comment: { - node: MarkupNodeType.comment - } -} - -const specialRules: Record = { - p: { - handleOpenTag: (state: HtmlParseState, tag: string, attributes: Record) => { - const top = state.top() - if (top?.type !== MarkupNodeType.paragraph) { - const attrs = { textAlign: attributes.textAlign ?? null } - state.openNode(MarkupNodeType.paragraph, attrs) - } - }, - handleCloseTag: (state: HtmlParseState, tag: string) => { - const top = state.top() - if (top?.type === MarkupNodeType.paragraph) { - state.closeNode(MarkupNodeType.paragraph) - } - } - }, - span: { - handleOpenTag: (state: HtmlParseState, tag: string, attributes: Record) => { - const dataType = attributes['data-type'] - const dataColor = attributes['data-color'] - const style = attributes.style - - if (dataType === 'reference') { - state.openNode(MarkupNodeType.reference, { - id: attributes['data-id'], - objectclass: attributes['data-objectclass'], - label: attributes['data-label'] - }) - } else if (dataColor !== undefined) { - state.openMark(MarkupMarkType.textColor, { - color: dataColor - }) - } else if (style !== undefined) { - const attrs: Record = {} - style.split(';').forEach((part) => { - const [key, value] = part.split(':') - if (key !== undefined && value !== undefined) { - const trimmedKey = key.trim() - const trimmedValue = value.trim() - if (trimmedKey.length > 0 && trimmedValue.length > 0) { - // Convert CSS property names to camelCase (e.g., font-family -> fontFamily) - const camelKey = trimmedKey.replace(/-([a-z])/g, (g) => g[1].toUpperCase()) - attrs[camelKey] = trimmedValue - } - } - }) - - if (Object.keys(attrs).length > 0) { - state.openMark(MarkupMarkType.textStyle, attrs) - } - } - }, - handleCloseTag: (state: HtmlParseState, tag: string) => { - const top = state.top() - if (top?.type === MarkupNodeType.reference) { - delete top.content - state.closeNode(MarkupNodeType.reference) - } else { - state.closeMark(MarkupMarkType.textColor) - state.closeMark(MarkupMarkType.textStyle) - } - } - }, - code: { - handleOpenTag: (state: HtmlParseState, tag: string, attributes: Record) => { - const top = state.top() - if (top?.type === MarkupNodeType.code_block) { - const classes = attributes.class?.split(' ') ?? [] - const language = classes.find((c) => c.startsWith('language-')) - if (language !== undefined) { - top.attrs = { language: language.substring(9) } - } - } else { - state.openMark(MarkupMarkType.code) - } - }, - handleCloseTag: (state: HtmlParseState, tag: string) => { - const top = state.top() - if (top?.type === MarkupNodeType.code_block) { - // do nothing - } else { - state.closeMark(MarkupMarkType.code) - } - } - }, - a: { - handleOpenTag: (state: HtmlParseState, tag: string, attributes: Record) => { - const dataType = attributes['data-type'] - if (dataType === 'embed') { - state.openNode(MarkupNodeType.embed, { - src: decodeURI(attributes.href) - }) - } else { - state.openMark(MarkupMarkType.link, { - rel: attributes.rel, - target: attributes.target, - class: attributes.class, - href: attributes.href, - title: attributes.title - }) - } - }, - handleCloseTag: (state: HtmlParseState, tag: string) => { - const top = state.top() - if (top?.type === MarkupNodeType.embed) { - delete top.content - state.closeNode(MarkupNodeType.embed) - } else { - state.closeMark(MarkupMarkType.link) - } - } - } -} - -const ignoreRules: Record = { - html: {}, - head: {}, - body: {}, - thead: {}, - tbody: {} -} - -export class HtmlParser { - private readonly handlers: Record = {} - - constructor (private readonly options: HtmlParserOptions = {}) { - Object.entries(nodeRules).forEach(([tag, rule]) => { - this.handlers[tag] = nodeHandler(rule) - }) - - Object.entries(markRules).forEach(([tag, rule]) => { - this.handlers[tag] = markHandler(rule) - }) - - Object.entries(specialRules).forEach(([tag, rule]) => { - this.handlers[tag] = specialHandler(rule) - }) - - Object.entries(ignoreRules).forEach(([tag, rule]) => { - this.handlers[tag] = ignoreHandler(rule) - }) - } - - parse (html: string): MarkupNode { - const root: MarkupNode = { type: MarkupNodeType.doc, content: [] } - const state = new HtmlParseState(root, this.handlers) - - let rawPos: number | undefined - let rawDepth: number | undefined - - const parser = new Parser( - { - onopentag (tag: string, attributes: Record) { - if (rawDepth !== undefined) { - rawDepth += 1 - return - } - - if (attributes.style !== undefined) { - const attrs = extractStyleAttrs(attributes, styleRules) - if (attrs !== undefined) { - attributes = { ...attributes, ...attrs } - } - } - - const handler = state.handlers[tag] - if (handler !== undefined) { - handler.handleOpenTag(state, tag, attributes) - } else { - rawPos = parser.startIndex - rawDepth = 0 - } - }, - - ontext (text: string) { - if (rawDepth !== undefined) { - return - } - - if (text.length === 0) { - return - } - - state.addText(text) - }, - - oncomment (text: string) { - if (rawDepth !== undefined) { - return - } - - state.openNode(MarkupNodeType.comment) - state.addText(text) - state.closeNode(MarkupNodeType.comment) - }, - - onclosetag (tag: string) { - if (rawDepth !== undefined) { - rawDepth -= 1 - - if (rawDepth === 0) { - const start = rawPos ?? 0 - const end = parser.endIndex - const text = html.substring(start, end) - state.addText(text) - rawPos = undefined - rawDepth = undefined - } - return - } - - const handler = state.handlers[tag] - if (handler !== undefined) { - handler.handleCloseTag(state, tag) - } else { - // do nothing - } - } - }, - { - decodeEntities: true, - lowerCaseTags: true, - recognizeSelfClosing: true - } - ) - - parser.write(html) - parser.end() - - return state.root - } -} - -function extractStyleAttrs (attrs: Record, rules: HtmlStyleRule[]): Record | undefined { - const style = attrs.style - if (style !== undefined) { - const styles: Record = {} - - // parse style attribute - style.split(';').forEach((stylePart) => { - const [key, value] = stylePart.split(':') - styles[key.trim()] = value?.trim() - }) - - const result = {} - - rules.forEach((rule) => { - if (styles[rule.style] !== undefined) { - const attrs = rule.getAttrs?.(styles[rule.style]) - if (attrs !== undefined) { - Object.assign(result, attrs) - } - } - }) - - return result - } -} diff --git a/foundations/core/packages/text-html/src/serializer.ts b/foundations/core/packages/text-html/src/serializer.ts deleted file mode 100644 index ff427a2e4c..0000000000 --- a/foundations/core/packages/text-html/src/serializer.ts +++ /dev/null @@ -1,373 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - type Attrs, - type AttrValue, - type MarkupMark, - type MarkupNode, - MarkupMarkType, - MarkupNodeType -} from '@hcengineering/text-core' - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface HtmlSerializerOptions {} - -export class HtmlSerializer { - constructor (private readonly options: HtmlSerializerOptions = {}) {} - - serialize (markup: MarkupNode): string { - const builder = new NodeBuilder(true) - addNode(builder, markup) - return builder.toText() - } -} - -class NodeBuilder { - textParts: string[] = [] - - constructor (private readonly addTags: boolean) {} - - addText (text: string): void { - this.textParts.push(text) - } - - openTag ( - tag: string, - attributes: Record = {}, - options?: { newLine?: boolean, selfClosing?: boolean } - ): void { - if (this.addTags) { - this.textParts.push('<') - this.textParts.push(tag) - - for (const [key, value] of Object.entries(attributes)) { - if (value == null) continue - - if (typeof value === 'boolean') { - if (value) { - this.textParts.push(` ${key}`) - } - } else { - this.textParts.push(` ${key}="${escapeHtml(String(value))}"`) - } - } - - this.textParts.push(options?.selfClosing === true ? '/>' : '>') - } else { - if (options?.newLine === true) { - this.textParts.push('\n') - } - } - } - - closeTag (tag: string, options?: { newLine: boolean }): void { - if (this.addTags) { - this.textParts.push(``) - } else if (options?.newLine === true) { - this.textParts.push('\n') - } - } - - toText (): string { - return this.textParts.join('') - } -} - -// Helper function to escape HTML special characters -function escapeHtml (text: string): string { - return text - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') - .replace(/\r/g, ' ') - .replace(/\n/g, ' ') -} - -function addMark (builder: NodeBuilder, mark?: MarkupMark, next?: () => void): void { - if (mark != null) { - const attrs = mark.attrs ?? {} - - if (mark.type === MarkupMarkType.bold) { - builder.openTag('strong') - next?.() - builder.closeTag('strong') - } else if (mark.type === MarkupMarkType.code) { - builder.openTag('code') - next?.() - builder.closeTag('code') - } else if (mark.type === MarkupMarkType.em) { - builder.openTag('em') - next?.() - builder.closeTag('em') - } else if (mark.type === MarkupMarkType.link) { - builder.openTag('a', { - target: attrs.target, - rel: attrs.rel, - class: attrs.class, - href: attrs.href, - title: attrs.title - }) - next?.() - builder.closeTag('a') - } else if (mark.type === MarkupMarkType.strike) { - builder.openTag('s') - next?.() - builder.closeTag('s') - } else if (mark.type === MarkupMarkType.underline) { - builder.openTag('u') - next?.() - builder.closeTag('u') - } else if (mark.type === MarkupMarkType.textColor) { - const color = attrs.color - builder.openTag('span', { - style: color !== undefined ? `color: ${color}` : undefined, - 'data-color': color - }) - next?.() - builder.closeTag('span') - } else if (mark.type === MarkupMarkType.textStyle) { - // Convert camelCase properties back to kebab-case CSS properties - const styleAttrs = Object.entries(attrs) - .map(([key, value]) => { - const kebabKey = key.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`) - return `${kebabKey}: ${value}` - }) - .join('; ') - - builder.openTag('span', { - style: styleAttrs - }) - next?.() - builder.closeTag('span') - } else { - // Handle unknown mark as span with data attribute - builder.openTag('span', { 'data-mark-type': mark.type as string }) - next?.() - builder.closeTag('span') - } - } -} - -function addMarks (builder: NodeBuilder, marks: MarkupMark[], next?: () => void): void { - if (marks.length > 0) { - const mark = marks[0] - const others = marks.slice(1) - - if (others.length > 0) { - addMark(builder, mark, () => { - addMarks(builder, others, next) - }) - } else { - addMark(builder, mark, next) - } - } -} - -function addNodes (builder: NodeBuilder, nodes: MarkupNode[]): void { - nodes.forEach((childNode) => { - addNode(builder, childNode) - }) -} - -function addNodeContent (builder: NodeBuilder, node?: MarkupNode): void { - if (node == null) return - - const attrs = node.attrs ?? {} - const nodes = node.content ?? [] - const style = toStyleAttr(attrs) - - if (node.type === MarkupNodeType.doc) { - addNodes(builder, nodes) - } else if (node.type === MarkupNodeType.paragraph) { - builder.openTag('p', { style }) - addNodes(builder, nodes) - builder.closeTag('p') - } else if (node.type === MarkupNodeType.blockquote) { - builder.openTag('blockquote') - addNodes(builder, nodes) - builder.closeTag('blockquote') - } else if (node.type === MarkupNodeType.horizontal_rule) { - builder.openTag('hr', {}, { selfClosing: true }) - } else if (node.type === MarkupNodeType.heading) { - const level = toNumber(attrs.level) ?? 1 - const tag = `h${level}` - builder.openTag(tag, { style }) - addNodes(builder, nodes) - builder.closeTag(tag) - } else if (node.type === MarkupNodeType.code_block) { - const attrs = node.attrs?.language !== undefined ? { class: `language-${node.attrs.language}` } : {} - builder.openTag('pre') - builder.openTag('code', attrs) - addNodes(builder, nodes) - builder.closeTag('code') - builder.closeTag('pre') - } else if (node.type === MarkupNodeType.text) { - builder.addText(node.text ?? '') - } else if (node.type === MarkupNodeType.image) { - const src = toString(attrs.src) - const alt = toString(attrs.alt) - const width = toString(attrs.width) - const height = toString(attrs.height) - builder.openTag('img', { src, alt, width, height }, { selfClosing: true }) - } else if (node.type === MarkupNodeType.reference) { - const label = toString(attrs.label) - builder.openTag('span', { - 'data-type': 'reference', - 'data-id': attrs.id, - 'data-objectclass': attrs.objectclass, - 'data-label': attrs.label - }) - builder.addText(label !== undefined ? `@${label}` : '') - builder.closeTag('span') - } else if (node.type === MarkupNodeType.hard_break) { - builder.openTag('br', {}, { selfClosing: true }) - } else if (node.type === MarkupNodeType.ordered_list) { - builder.openTag('ol') - addNodes(builder, nodes) - builder.closeTag('ol') - } else if (node.type === MarkupNodeType.bullet_list) { - builder.openTag('ul') - addNodes(builder, nodes) - builder.closeTag('ul') - } else if (node.type === MarkupNodeType.list_item) { - builder.openTag('li') - addNodes(builder, nodes) - builder.closeTag('li') - } else if (node.type === MarkupNodeType.todoList) { - builder.openTag('ul', { 'data-type': MarkupNodeType.todoList }) - addNodes(builder, nodes) - builder.closeTag('ul') - } else if (node.type === MarkupNodeType.todoItem) { - const checked = node.attrs?.checked === true || node.attrs?.checked === 'true' - const disabled = node.attrs?.disabled === true || node.attrs?.disabled === 'true' - - builder.openTag('li', { - 'data-type': MarkupNodeType.todoItem, - 'data-todoid': node.attrs?.todoid, - 'data-userid': node.attrs?.userid, - 'data-checked': checked - }) - - builder.openTag('input', { type: 'checkbox', checked, disabled }, { selfClosing: true }) - - addNodes(builder, nodes) - - builder.closeTag('li') - } else if (node.type === MarkupNodeType.taskList) { - builder.openTag('ul', { 'data-type': MarkupNodeType.taskList }) - addNodes(builder, nodes) - builder.closeTag('ul') - } else if (node.type === MarkupNodeType.taskItem) { - const checked = node.attrs?.checked === true || node.attrs?.checked === 'true' - const disabled = node.attrs?.disabled === true || node.attrs?.disabled === 'true' - - builder.openTag('li', { - 'data-type': MarkupNodeType.taskItem, - 'data-checked': checked - }) - builder.openTag('input', { type: 'checkbox', checked, disabled }, { selfClosing: true }) - - addNodes(builder, nodes) - - builder.closeTag('li') - } else if (node.type === MarkupNodeType.subLink) { - builder.openTag('sub') - addNodes(builder, nodes) - builder.closeTag('sub') - } else if (node.type === MarkupNodeType.table) { - builder.openTag('table') - builder.openTag('tbody') - addNodes(builder, nodes) - builder.closeTag('tbody') - builder.closeTag('table') - } else if (node.type === MarkupNodeType.table_row) { - builder.openTag('tr') - addNodes(builder, nodes) - builder.closeTag('tr') - } else if (node.type === MarkupNodeType.table_cell) { - const colspan = toNumber(attrs.colspan) ?? 1 - const rowspan = toNumber(attrs.rowspan) ?? 1 - const colwidth = toNumber(attrs.colwidth) - builder.openTag('td', { - colspan: colspan !== 1 ? colspan : undefined, - rowspan: rowspan !== 1 ? rowspan : undefined, - colwidth: colwidth !== undefined && colwidth > 0 ? colwidth : undefined - }) - addNodes(builder, nodes) - builder.closeTag('td') - } else if (node.type === MarkupNodeType.table_header) { - const colspan = toNumber(attrs.colspan) ?? 1 - const rowspan = toNumber(attrs.rowspan) ?? 1 - const colwidth = toNumber(attrs.colwidth) - builder.openTag('th', { - colspan: colspan !== 1 ? colspan : undefined, - rowspan: rowspan !== 1 ? rowspan : undefined, - colwidth: colwidth !== undefined && colwidth > 0 ? colwidth : undefined - }) - addNodes(builder, nodes) - builder.closeTag('th') - } else if (node.type === MarkupNodeType.comment) { - builder.addText('') - } else if (node.type === MarkupNodeType.embed) { - const src = toString(attrs.src) ?? '' - builder.openTag('a', { href: encodeURI(src), 'data-type': 'embed' }) - builder.addText(escapeHtml(src)) - builder.closeTag('a') - } else { - // Handle unknown node types as div with data attribute - builder.openTag('div', { 'data-node-type': node.type }) - addNodes(builder, nodes) - builder.closeTag('div') - } -} - -function addNode (builder: NodeBuilder, node: MarkupNode): void { - const marks = node.marks ?? [] - - if (marks.length > 0) { - addMarks(builder, marks, () => { - addNodeContent(builder, node) - }) - } else { - addNodeContent(builder, node) - } -} - -function toString (value: AttrValue | undefined): string | undefined { - return value !== undefined ? `${value}` : undefined -} - -function toNumber (value: AttrValue | undefined): number | undefined { - if (typeof value === 'boolean') { - return value ? 1 : 0 - } - - return value != null ? (typeof value === 'string' ? parseInt(value) : value) : undefined -} - -function toStyleAttr (attrs: Attrs): string | undefined { - const styles: string[] = [] - - if (attrs.textAlign != null) { - styles.push(`text-align: ${attrs.textAlign}`) - } - - return styles.length > 0 ? styles.join('; ') : undefined -} diff --git a/foundations/core/packages/text-html/tsconfig.json b/foundations/core/packages/text-html/tsconfig.json deleted file mode 100644 index b5ae22f6e4..0000000000 --- a/foundations/core/packages/text-html/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/foundations/core/packages/text-markdown/.eslintrc.js b/foundations/core/packages/text-markdown/.eslintrc.js deleted file mode 100644 index 72235dc283..0000000000 --- a/foundations/core/packages/text-markdown/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/foundations/core/packages/text-markdown/CHANGELOG.json b/foundations/core/packages/text-markdown/CHANGELOG.json deleted file mode 100644 index b155968395..0000000000 --- a/foundations/core/packages/text-markdown/CHANGELOG.json +++ /dev/null @@ -1,89 +0,0 @@ -{ - "name": "@hcengineering/text-markdown", - "entries": [ - { - "version": "0.7.20", - "tag": "@hcengineering/text-markdown_v0.7.20", - "date": "Wed, 19 Nov 2025 07:37:47 GMT", - "comments": { - "patch": [ - { - "comment": "Fix NaN error in markdown" - } - ] - } - }, - { - "version": "0.7.19", - "tag": "@hcengineering/text-markdown_v0.7.19", - "date": "Thu, 06 Nov 2025 09:09:58 GMT", - "comments": { - "patch": [ - { - "comment": "Fix normalizeMarkdown errors" - } - ] - } - }, - { - "version": "0.7.18", - "tag": "@hcengineering/text-markdown_v0.7.18", - "date": "Mon, 27 Oct 2025 16:46:51 GMT", - "comments": { - "patch": [ - { - "comment": "add support for textColor and textStyle marks" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/text-core\" from `^0.7.17` to `0.7.18`" - }, - { - "comment": "Updating dependency \"@hcengineering/text-html\" from `^0.7.17` to `0.7.18`" - } - ] - } - }, - { - "version": "0.7.5", - "tag": "@hcengineering/text-markdown_v0.7.5", - "date": "Tue, 14 Oct 2025 04:58:17 GMT", - "comments": { - "patch": [ - { - "comment": "update deps" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/text-core\" from `^0.7.4` to `0.7.5`" - }, - { - "comment": "Updating dependency \"@hcengineering/text-html\" from `^0.7.4` to `0.7.5`" - } - ] - } - }, - { - "version": "0.7.4", - "tag": "@hcengineering/text-markdown_v0.7.4", - "date": "Sat, 11 Oct 2025 18:20:33 GMT", - "comments": { - "patch": [ - { - "comment": "Update to latest platform rig" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/text-core\" from `^0.7.3` to `0.7.4`" - }, - { - "comment": "Updating dependency \"@hcengineering/text-html\" from `^0.7.3` to `0.7.4`" - } - ] - } - } - ] -} diff --git a/foundations/core/packages/text-markdown/CHANGELOG.md b/foundations/core/packages/text-markdown/CHANGELOG.md deleted file mode 100644 index f6f944e867..0000000000 --- a/foundations/core/packages/text-markdown/CHANGELOG.md +++ /dev/null @@ -1,39 +0,0 @@ -# Change Log - @hcengineering/text-markdown - -This log was last generated on Wed, 19 Nov 2025 07:37:47 GMT and should not be manually modified. - -## 0.7.20 -Wed, 19 Nov 2025 07:37:47 GMT - -### Patches - -- Fix NaN error in markdown - -## 0.7.19 -Thu, 06 Nov 2025 09:09:58 GMT - -### Patches - -- Fix normalizeMarkdown errors - -## 0.7.18 -Mon, 27 Oct 2025 16:46:51 GMT - -### Patches - -- add support for textColor and textStyle marks - -## 0.7.5 -Tue, 14 Oct 2025 04:58:17 GMT - -### Patches - -- update deps - -## 0.7.4 -Sat, 11 Oct 2025 18:20:33 GMT - -### Patches - -- Update to latest platform rig - diff --git a/foundations/core/packages/text-markdown/config/rig.json b/foundations/core/packages/text-markdown/config/rig.json deleted file mode 100644 index 0110930f55..0000000000 --- a/foundations/core/packages/text-markdown/config/rig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig" -} diff --git a/foundations/core/packages/text-markdown/jest.config.js b/foundations/core/packages/text-markdown/jest.config.js deleted file mode 100644 index 069ea8aa0d..0000000000 --- a/foundations/core/packages/text-markdown/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ['./src'], - coverageReporters: ['text-summary', 'html', 'lcov'] -} diff --git a/foundations/core/packages/text-markdown/package.json b/foundations/core/packages/text-markdown/package.json deleted file mode 100644 index 34d22bab56..0000000000 --- a/foundations/core/packages/text-markdown/package.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "name": "@hcengineering/text-markdown", - "version": "0.7.20", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "files": [ - "lib/**/*", - "types/**/*", - "src/**/*", - "!src/**/__test__/**", - "tsconfig.json" - ], - "author": "Anticrm Platform Contributors", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "test": "jest --passWithNoTests --silent --coverage", - "build:watch": "compile", - "format": "format src", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --coverage", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "workspace:^0.7.19", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "@typescript-eslint/parser": "^6.21.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.6.2", - "typescript": "^5.9.3", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5", - "@types/markdown-it": "~13.0.0", - "eslint-plugin-svelte": "^2.35.1" - }, - "dependencies": { - "@hcengineering/text-core": "workspace:^0.7.18", - "@hcengineering/text-html": "workspace:^0.7.18", - "markdown-it": "^14.0.0", - "fast-equals": "^5.2.2" - }, - "repository": "https://github.com/hcengineering/huly.core", - "publishConfig": { - "access": "public" - }, - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.js" - } - } -} diff --git a/foundations/core/packages/text-markdown/src/__tests__/markdown.test.ts b/foundations/core/packages/text-markdown/src/__tests__/markdown.test.ts deleted file mode 100644 index 1068e7b2ad..0000000000 --- a/foundations/core/packages/text-markdown/src/__tests__/markdown.test.ts +++ /dev/null @@ -1,1252 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { MarkupNode, MarkupNodeType, MarkupMarkType } from '@hcengineering/text-core' -import { markdownToMarkup, markupToMarkdown } from '..' -import { isMarkdownsEquals, normalizeMarkdown } from '../compare' - -const refUrl: string = 'ref://' -const imageUrl: string = 'http://localhost' -const options = { refUrl, imageUrl } - -declare global { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace jest { - interface Matchers { - toEqualMarkdown: (expected: string) => R - } - } -} - -expect.extend({ - toEqualMarkdown (received: string, expected: string) { - const pass = isMarkdownsEquals(received, expected) - return { - message: () => - pass - ? `Expected markdown strings NOT to be equal:\n Received:\n${received}\n Expected:\n${expected}` - : `Expected markdown strings to be equal:\n Received:\n${received}\n Expected:\n${expected}`, - pass - } - } -}) - -describe('markdownToMarkup', () => { - const tests: Array<{ name: string, markdown: string, markup: object }> = [ - { - name: 'simple text', - markdown: 'Lorem ipsum dolor sit amet.', - markup: { - type: 'doc', - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'Lorem ipsum dolor sit amet.', - marks: [] - } - ] - } - ] - } - }, - { - name: 'text with heading', - markdown: `# Lorem ipsum - -Lorem ipsum dolor sit amet. -`, - markup: { - type: 'doc', - content: [ - { - type: 'heading', - attrs: { level: 1, marker: '#' }, - content: [ - { - type: 'text', - text: 'Lorem ipsum', - marks: [] - } - ] - }, - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'Lorem ipsum dolor sit amet.', - marks: [] - } - ] - } - ] - } - }, - { - name: 'bullet list', - markdown: `# bullet list -- list item 1 -- list item 2 -`, - markup: { - type: 'doc', - content: [ - { - type: 'heading', - attrs: { level: 1, marker: '#' }, - content: [ - { - type: 'text', - text: 'bullet list', - marks: [] - } - ] - }, - { - type: 'bulletList', - attrs: { - bullet: '-' - }, - content: [ - { - type: 'listItem', - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'list item 1', - marks: [] - } - ] - } - ] - }, - { - type: 'listItem', - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'list item 2', - marks: [] - } - ] - } - ] - } - ] - } - ] - } - }, - { - name: 'todos', - markdown: `# TODO -- [ ] todo 1 -- [x] todo 2 - `, - markup: { - type: 'doc', - content: [ - { - type: 'heading', - attrs: { level: 1, marker: '#' }, - content: [ - { - type: 'text', - text: 'TODO', - marks: [] - } - ] - }, - { - type: 'todoList', - attrs: { - bullet: '-' - }, - content: [ - { - type: 'todoItem', - attrs: { checked: false }, - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'todo 1', - marks: [] - } - ] - } - ] - }, - { - type: 'todoItem', - attrs: { checked: true }, - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'todo 2', - marks: [] - } - ] - } - ] - } - ] - } - ] - } - }, - { - name: 'todos followed by list items', - markdown: `# todo and list -- [ ] todo 1 -- [x] todo 2 -- list item 1 -- list item 2 -`, - markup: { - type: 'doc', - content: [ - { - type: 'heading', - attrs: { level: 1, marker: '#' }, - content: [ - { - type: 'text', - text: 'todo and list', - marks: [] - } - ] - }, - { - type: 'todoList', - attrs: { - bullet: '-' - }, - content: [ - { - type: 'todoItem', - attrs: { checked: false }, - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'todo 1', - marks: [] - } - ] - } - ] - }, - { - type: 'todoItem', - attrs: { checked: true }, - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'todo 2', - marks: [] - } - ] - } - ] - } - ] - }, - { - type: 'bulletList', - attrs: { - bullet: '-' - }, - content: [ - { - type: 'listItem', - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'list item 1', - marks: [] - } - ] - } - ] - }, - { - type: 'listItem', - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'list item 2', - marks: [] - } - ] - } - ] - } - ] - } - ] - } - }, - { - name: 'todos followed by list items', - markdown: `# mixed lists -- [ ] todo 1 -- list item 1 -- [x] todo 2 -- list item 2 -`, - markup: { - type: 'doc', - content: [ - { - type: 'heading', - attrs: { level: 1, marker: '#' }, - content: [ - { - type: 'text', - text: 'mixed lists', - marks: [] - } - ] - }, - { - type: 'todoList', - attrs: { - bullet: '-' - }, - content: [ - { - type: 'todoItem', - attrs: { checked: false }, - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'todo 1', - marks: [] - } - ] - } - ] - } - ] - }, - { - type: 'bulletList', - attrs: { - bullet: '-' - }, - content: [ - { - type: 'listItem', - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'list item 1', - marks: [] - } - ] - } - ] - } - ] - }, - { - type: 'todoList', - attrs: { - bullet: '-' - }, - content: [ - { - type: 'todoItem', - attrs: { checked: true }, - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'todo 2', - marks: [] - } - ] - } - ] - } - ] - }, - { - type: 'bulletList', - attrs: { - bullet: '-' - }, - content: [ - { - type: 'listItem', - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'list item 2', - marks: [] - } - ] - } - ] - } - ] - } - ] - } - }, - { - name: 'nested todos', - markdown: `# nested todos -- [ ] todo - - [x] sub todo -`, - markup: { - type: 'doc', - content: [ - { - type: 'heading', - attrs: { level: 1, marker: '#' }, - content: [ - { - type: 'text', - text: 'nested todos', - marks: [] - } - ] - }, - { - type: 'todoList', - attrs: { - bullet: '-' - }, - content: [ - { - type: 'todoItem', - attrs: { checked: false }, - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'todo', - marks: [] - } - ] - }, - { - type: 'todoList', - attrs: { - bullet: '-' - }, - content: [ - { - type: 'todoItem', - attrs: { checked: true }, - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'sub todo', - marks: [] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - }, - { - name: 'nested lists', - markdown: `# nested lists -- [ ] todo - - sub list item - - [x] sub todo -- list item - - [x] sub todo - - sub list item -`, - markup: { - type: 'doc', - content: [ - { - type: 'heading', - attrs: { level: 1, marker: '#' }, - content: [ - { - type: 'text', - text: 'nested lists', - marks: [] - } - ] - }, - { - type: 'todoList', - attrs: { - bullet: '-' - }, - content: [ - { - type: 'todoItem', - attrs: { checked: false }, - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'todo', - marks: [] - } - ] - }, - { - type: 'bulletList', - attrs: { - bullet: '-' - }, - content: [ - { - type: 'listItem', - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'sub list item', - marks: [] - } - ] - } - ] - } - ] - }, - { - type: 'todoList', - attrs: { - bullet: '-' - }, - content: [ - { - type: 'todoItem', - attrs: { checked: true }, - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'sub todo', - marks: [] - } - ] - } - ] - } - ] - } - ] - } - ] - }, - { - type: 'bulletList', - attrs: { - bullet: '-' - }, - content: [ - { - type: 'listItem', - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'list item', - marks: [] - } - ] - }, - { - type: 'todoList', - attrs: { - bullet: '-' - }, - content: [ - { - type: 'todoItem', - attrs: { checked: true }, - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'sub todo', - marks: [] - } - ] - } - ] - } - ] - }, - { - type: 'bulletList', - attrs: { - bullet: '-' - }, - content: [ - { - type: 'listItem', - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'sub list item', - marks: [] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - }, - { - name: 'nested todos', - markdown: `# nested todos -- [ ] todo - - [x] sub todo -`, - markup: { - type: 'doc', - content: [ - { - type: 'heading', - attrs: { level: 1, marker: '#' }, - content: [ - { - type: 'text', - text: 'nested todos', - marks: [] - } - ] - }, - { - type: 'todoList', - attrs: { - bullet: '-' - }, - content: [ - { - type: 'todoItem', - attrs: { checked: false }, - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'todo', - marks: [] - } - ] - }, - { - type: 'todoList', - attrs: { - bullet: '-' - }, - content: [ - { - type: 'todoItem', - attrs: { checked: true }, - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'sub todo', - marks: [] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - }, - { - name: 'mermaid diagram', - markdown: '```mermaid\ngraph TD;\n\tA-->B;\n\tA-->C;\n\tB-->D;\n\tC-->D;\n```', - markup: { - type: 'doc', - content: [ - { - type: 'mermaid', - attrs: { - language: 'mermaid' - }, - content: [ - { - marks: [], - text: 'graph TD;\n\tA-->B;\n\tA-->C;\n\tB-->D;\n\tC-->D;', - type: 'text' - } - ] - } - ] - } - }, - { - name: 'embed', - markdown: 'http://localhost/embed', - markup: { - type: 'doc', - content: [ - { - type: 'paragraph', - content: [ - { - type: 'embed', - attrs: { src: 'http://localhost/embed' }, - content: [] - } - ] - } - ] - } - }, - { - name: 'embed-uri-escape', - markdown: - 'http://localhost/embed spaces', - markup: { - type: 'doc', - content: [ - { - type: 'paragraph', - content: [ - { - type: 'embed', - attrs: { src: 'http://localhost/embed spaces' }, - content: [] - } - ] - } - ] - } - }, - { - name: 'embed-html-escape', - markdown: - 'http://localhost/embed<html>', - markup: { - type: 'doc', - content: [ - { - type: 'paragraph', - content: [ - { - type: 'embed', - attrs: { src: 'http://localhost/embed' }, - content: [] - } - ] - } - ] - } - }, - { - name: 'multiline image alt', - markdown: '![line0\\\n\\\nline1](http://example.com/image.png)', - markup: { - type: 'doc', - content: [ - { - type: 'paragraph', - content: [ - { - type: 'image', - attrs: { src: 'http://example.com/image.png', alt: 'line0\n\nline1' }, - content: [] - } - ] - } - ] - } - }, - { - name: 'image in a table cell', - markdown: - '

Some text

image-alt

', - markup: { - type: 'doc', - content: [ - { - type: 'table', - content: [ - { - type: 'tableRow', - content: [ - { - type: 'tableCell', - attrs: { - colspan: undefined, - rowspan: undefined, - colwidth: undefined - }, - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'Some text' - } - ] - }, - { - type: 'paragraph', - attrs: { - textAlign: null - }, - content: [ - { - type: 'text', - text: ' ' - }, - { - type: 'image', - attrs: { - src: 'files/image_1.png', - alt: 'image-alt', - 'file-id': null, - title: null - } - } - ] - } - ] - } - ] - } - ] - } - ] - } - }, - { - name: 'textColor', - markdown: 'colored', - markup: { - type: MarkupNodeType.doc, - content: [ - { - type: MarkupNodeType.paragraph, - content: [ - { - type: MarkupNodeType.text, - text: 'colored', - marks: [{ type: 'textColor' as MarkupMarkType, attrs: { color: '#abcdef' } }] - } - ] - } - ] - } - } - ] - - describe('to markup', () => { - tests.forEach(({ name, markdown, markup }) => { - it(name, () => { - const parsed = markdownToMarkup(markdown, options) - expect(parsed).toEqual(markup) - }) - }) - }) - - describe('to markdown', () => { - tests.forEach(({ name, markdown, markup }) => { - it(name, () => { - const serialized = markupToMarkdown(markup as MarkupNode, options) - expect(serialized).toEqualMarkdown(markdown) - }) - }) - }) -}) - -describe('markupToMarkdown', () => { - const tests: Array<{ name: string, markdown: string, markup: object }> = [ - { - name: 'links', - markdown: `[Link](https://example.com) - -[Link with spaces]() - -[Link with spaces and braces](>)`, - markup: { - type: 'doc', - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'Link', - marks: [{ type: 'link', attrs: { href: 'https://example.com' } }] - } - ] - }, - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'Link with spaces', - marks: [{ type: 'link', attrs: { href: 'https://example.com/with spaces' } }] - } - ] - }, - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'Link with spaces and braces', - marks: [{ type: 'link', attrs: { href: 'https://example.com/' } }] - } - ] - } - ] - } - }, - { - name: 'textColor', - markdown: 'colored', - markup: { - type: MarkupNodeType.doc, - content: [ - { - type: MarkupNodeType.paragraph, - content: [ - { - type: MarkupNodeType.text, - text: 'colored', - marks: [{ type: 'textColor' as MarkupMarkType, attrs: { color: '#abcdef' } }] - } - ] - } - ] - } - }, - { - name: 'textColor + italic', - markdown: '*styled*', - markup: { - type: MarkupNodeType.doc, - content: [ - { - type: MarkupNodeType.paragraph, - content: [ - { - type: MarkupNodeType.text, - text: 'styled', - marks: [ - { type: 'textColor' as MarkupMarkType, attrs: { color: '#abcdef' } }, - { type: MarkupMarkType.em } - ] - } - ] - } - ] - } - }, - { - name: 'textStyle with font-family', - markdown: 'arial', - markup: { - type: MarkupNodeType.doc, - content: [ - { - type: MarkupNodeType.paragraph, - content: [ - { - type: MarkupNodeType.text, - text: 'arial', - marks: [{ type: MarkupMarkType.textStyle, attrs: { fontFamily: 'Arial' } }] - } - ] - } - ] - } - }, - { - name: 'textStyle with multiple properties', - markdown: 'styled', - markup: { - type: MarkupNodeType.doc, - content: [ - { - type: MarkupNodeType.paragraph, - content: [ - { - type: MarkupNodeType.text, - text: 'styled', - marks: [ - { - type: MarkupMarkType.textStyle, - attrs: { fontFamily: 'Arial', fontSize: '16px', fontWeight: 'bold' } - } - ] - } - ] - } - ] - } - }, - { - name: 'textStyle with color (no data-color)', - markdown: 'red', - markup: { - type: MarkupNodeType.doc, - content: [ - { - type: MarkupNodeType.paragraph, - content: [ - { - type: MarkupNodeType.text, - text: 'red', - marks: [{ type: MarkupMarkType.textStyle, attrs: { color: 'red' } }] - } - ] - } - ] - } - } - ] - - describe('to markdown', () => { - tests.forEach(({ name, markdown, markup }) => { - it(name, () => { - const result = markupToMarkdown(markup as MarkupNode, options) - expect(result).toEqual(markdown) - }) - }) - }) -}) - -describe('markdownToMarkup -> markupToMarkdown', () => { - const tests: Array<{ name: string, markdown: string, alternate?: string }> = [ - { name: 'Italic', markdown: '*Asteriscs* and _Underscores_' }, - { name: 'Bold', markdown: '**Asteriscs** and __Underscores__' }, - { name: 'Bullet list with asteriscs', markdown: 'Asterisks :\r\n* Firstly\r\n* Secondly' }, - { name: 'Bullet list with dashes', markdown: 'Dashes :\r\n- Firstly\r\n- Secondly' }, - { name: 'TODO list with asteriscs', markdown: '* [ ] Take\n* [ ] Do\n\n' }, - { name: 'TODO list with dashes', markdown: '- [x] Take\n- [ ] Do\n\n' }, - { - name: 'Different markers', - markdown: - 'Asterisks bulleted list:\r\n* Asterisks: *Italic* and **Bold*** Underscores: _Italic_ and __Bold__\r\n\r\nDash bulleted list:\r\n- Asterisks: *Italic* and **Bold**\r\n- Underscores: _Italic_ and __Bold__\r\n-' - }, - { name: 'Single line comment', markdown: '' }, - { - name: 'Multsiline comment', - markdown: - '"\r\n\r\nThis PR fix issue #22424\r\n\r\n\r\n"' - }, - { - name: 'Link', - markdown: 'See [link](https://example.com)' - }, - { - name: 'Link with spaces', - markdown: 'See [link]()', - alternate: 'See [link](https://example.com/with%20spaces)' - }, - { - name: 'Link with spaces and braces', - markdown: 'See [link](>)', - alternate: 'See [link](https://example.com/%3Cwith%20spaces%3E)' - }, - { - name: 'Codeblock', - markdown: '```typescript\nconst x: number = 42;\n```' - }, - { - name: 'Image', - markdown: 'image' - }, - { - name: 'Images', - markdown: ` -Screenshot 2025-09-11 at 15 42 40 - -Screenshot 2025-09-11 at 15 43 42 - -Screenshot 2025-09-11 at 15 43 50` - }, - { - name: 'Image with multiline alt', - markdown: '![link0\\\n\\\nline1](http://example.com/image.png)' - }, - { - name: 'Table', - markdown: - '

Header 1

Header 2

Cell 1

Cell 2

Cell 3

Cell 4

' - }, - { - name: 'Complex table', - markdown: - '

Header

Cell 1

Cell 2

Cell 3

' - }, - { - name: 'Sub', - markdown: 'View in Huly TSK-50' - } - // { - // name: 'Malformed', - // markdown: 'try to parse me' - // } - ] - - tests.forEach(({ name, markdown, alternate }) => { - it(name, () => { - const json = markdownToMarkup(markdown, options) - const serialized = markupToMarkdown(json, options) - expect(serialized).toEqualMarkdown(alternate ?? markdown) - }) - }) -}) - -describe('normalizeMarkdown', () => { - it('handles null and undefined inputs', () => { - expect(normalizeMarkdown(null as any)).toBe('') - expect(normalizeMarkdown(undefined as any)).toBe('') - expect(normalizeMarkdown('')).toBe('') - }) - - it('handles non-string inputs', () => { - expect(normalizeMarkdown(123 as any)).toBe('') - expect(normalizeMarkdown({} as any)).toBe('') - expect(normalizeMarkdown([] as any)).toBe('') - }) - - it('normalizes line endings', () => { - expect(normalizeMarkdown('line1\r\nline2\rline3\nline4')).toBe('line1\nline2\nline3\nline4') - }) - - it('removes trailing whitespace and empty lines', () => { - expect(normalizeMarkdown('line1 \n \nline2 \n\n\nline3')).toBe('line1\nline2\nline3') - }) - - it('normalizes HTML tag attributes', () => { - const input = 'test' - const expected = 'test' - expect(normalizeMarkdown(input)).toBe(expected) - }) - - it('handles void elements correctly', () => { - expect(normalizeMarkdown('')).toBe('') - expect(normalizeMarkdown('
')).toBe('
') - expect(normalizeMarkdown('
')).toBe('
') - }) - - it('handles non-void elements correctly', () => { - expect(normalizeMarkdown('
')).toBe('
') - expect(normalizeMarkdown('')).toBe('') - }) - - it('sorts attributes alphabetically', () => { - const input = 'test' - const expected = 'test' - expect(normalizeMarkdown(input)).toBe(expected) - }) - - it('handles attributes without values', () => { - expect(normalizeMarkdown('')).toBe('') - }) - - it('handles mixed content', () => { - const input = 'Text before\r\ntest \n\nText after ' - const expected = 'Text before\ntest\nText after' - expect(normalizeMarkdown(input)).toBe(expected) - }) -}) diff --git a/foundations/core/packages/text-markdown/src/compare.ts b/foundations/core/packages/text-markdown/src/compare.ts deleted file mode 100644 index aef9fdb0a1..0000000000 --- a/foundations/core/packages/text-markdown/src/compare.ts +++ /dev/null @@ -1,126 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -/** - * Calculate Sørensen–Dice coefficient - */ -export function calcSørensenDiceCoefficient (a: string, b: string): number { - if (a == null || b == null) return 0 - - const first = a.replace(/\s+/g, '') - const second = b.replace(/\s+/g, '') - - if (first === second) return 1 // identical or empty - if (first.length < 2 || second.length < 2) return 0 // if either is a 0-letter or 1-letter string - - const firstBigrams = new Map() - for (let i = 0; i < first.length - 1; i++) { - const bigram = first.substring(i, i + 2) - const count = (firstBigrams.get(bigram) ?? 0) + 1 - - firstBigrams.set(bigram, count) - } - - let intersectionSize = 0 - for (let i = 0; i < second.length - 1; i++) { - const bigram = second.substring(i, i + 2) - const count = firstBigrams.get(bigram) ?? 0 - - if (count > 0) { - firstBigrams.set(bigram, count - 1) - intersectionSize++ - } - } - - const denominator = first.length + second.length - 2 - if (denominator <= 0) return 0 - - return (2.0 * intersectionSize) / denominator -} - -/** - * Perform markdown diff/comparison to understand do we have a major differences. - */ -export function isMarkdownsEquals (source1: string, source2: string): boolean { - const normalized1 = normalizeMarkdown(source1) - const normalized2 = normalizeMarkdown(source2) - return normalized1 === normalized2 -} - -export function normalizeMarkdown (source: string): string { - if (source == null || typeof source !== 'string') return '' - - const tagRegex = /<(\w+)([^>]*?)(\/?)>/g - const attrRegex = /(\w+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g - - // Normalize line endings to LF - source = source.replace(/\r\n/g, '\n').replace(/\r/g, '\n') - - // Remove extra blank lines - source = source - .split('\n') - .map((it) => it.trimEnd()) - .filter((it) => it.length > 0) - .join('\n') - - // Normalize HTML tags - source = source.replace(tagRegex, (match, tagName, attributes) => { - const attrs: Record = {} - - let attrMatch = attrRegex.exec(attributes) - while (attrMatch !== null) { - const attrName = attrMatch[1] - const attrValue = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? '' - attrs[attrName] = attrValue - attrMatch = attrRegex.exec(attributes) - } - - // Sort attributes by name for consistent order - const sortedAttrs = Object.keys(attrs) - .sort() - .map((key) => { - const value = attrs[key] - return value !== '' ? `${key}="${value}"` : key - }) - .join(' ') - - // Normalize to self-closing format for void elements - const voidElements = [ - 'img', - 'br', - 'hr', - 'input', - 'meta', - 'area', - 'base', - 'col', - 'embed', - 'link', - 'param', - 'source', - 'track', - 'wbr' - ] - const isVoidElement = voidElements.includes(tagName.toLowerCase()) - - if (sortedAttrs !== '') { - return isVoidElement ? `<${tagName} ${sortedAttrs} />` : `<${tagName} ${sortedAttrs}>` - } else { - return isVoidElement ? `<${tagName} />` : `<${tagName}>` - } - }) - - return source -} diff --git a/foundations/core/packages/text-markdown/src/index.ts b/foundations/core/packages/text-markdown/src/index.ts deleted file mode 100644 index 7618c5548b..0000000000 --- a/foundations/core/packages/text-markdown/src/index.ts +++ /dev/null @@ -1,47 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { MarkupNode } from '@hcengineering/text-core' -import { MarkdownParser } from './parser' -import { MarkdownState, storeMarks, storeNodes } from './serializer' - -export * from './compare' -export * from './parser' -export * from './serializer' - -/** @public */ -export interface MarkdownOptions { - refUrl?: string - imageUrl?: string -} - -/** @public */ -export function markupToMarkdown (markup: MarkupNode, options?: MarkdownOptions): string { - const refUrl = options?.refUrl ?? 'ref://' - const imageUrl = options?.imageUrl ?? 'image://' - - const state = new MarkdownState(storeNodes, storeMarks, { tightLists: true, refUrl, imageUrl }) - state.renderContent(markup) - return state.out -} - -/** @public */ -export function markdownToMarkup (markdown: string, options?: MarkdownOptions): MarkupNode { - const refUrl = options?.refUrl ?? 'ref://' - const imageUrl = options?.imageUrl ?? 'image://' - - const parser = new MarkdownParser({ refUrl, imageUrl }) - return parser.parse(markdown ?? '') -} diff --git a/foundations/core/packages/text-markdown/src/marks.ts b/foundations/core/packages/text-markdown/src/marks.ts deleted file mode 100644 index 66585ddddb..0000000000 --- a/foundations/core/packages/text-markdown/src/marks.ts +++ /dev/null @@ -1,46 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { MarkupMark, MarkupMarkType } from '@hcengineering/text-core' -import { deepEqual } from 'fast-equals' - -export function markAttrs (mark: MarkupMark): Record { - return mark.attrs ?? {} -} - -export function isInSet (mark: MarkupMark, marks: MarkupMark[]): boolean { - return marks.find((m) => markEq(mark, m)) !== undefined -} - -export function addToSet (mark: MarkupMark, marks: MarkupMark[]): MarkupMark[] { - const m = marks.find((m) => markEq(mark, m)) - if (m !== undefined) { - // We already have mark - return marks - } - return [...marks, mark] -} - -export function removeFromSet (markType: MarkupMarkType, marks: MarkupMark[]): MarkupMark[] { - return marks.filter((m) => m.type !== markType) -} - -export function sameSet (a?: MarkupMark[], b?: MarkupMark[]): boolean { - return deepEqual(a, b) -} - -export function markEq (first: MarkupMark, other: MarkupMark): boolean { - return deepEqual(first, other) -} diff --git a/foundations/core/packages/text-markdown/src/node.ts b/foundations/core/packages/text-markdown/src/node.ts deleted file mode 100644 index 45c6f286bc..0000000000 --- a/foundations/core/packages/text-markdown/src/node.ts +++ /dev/null @@ -1,24 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { Attrs, MarkupNode } from '@hcengineering/text-core' - -export function nodeContent (node: MarkupNode): MarkupNode[] { - return node?.content ?? [] -} - -export function nodeAttrs (node: MarkupNode): Attrs { - return node.attrs ?? {} -} diff --git a/foundations/core/packages/text-markdown/src/parser.ts b/foundations/core/packages/text-markdown/src/parser.ts deleted file mode 100644 index 6e71bae4cc..0000000000 --- a/foundations/core/packages/text-markdown/src/parser.ts +++ /dev/null @@ -1,870 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { Attrs, MarkupMark, MarkupMarkType, MarkupNode, MarkupNodeType } from '@hcengineering/text-core' -import { htmlToMarkup } from '@hcengineering/text-html' -import MarkdownIt, { type Token } from 'markdown-it' -import type { RuleCore } from 'markdown-it/lib/parser_core' -import type StateCore from 'markdown-it/lib/rules_core/state_core' - -import { addToSet, removeFromSet, sameSet } from './marks' -import { nodeContent } from './node' - -type SpecRule = T | ((tok: Token, state: MarkdownParseState) => T) - -function readSpec (rule: SpecRule, tok: Token, state: MarkdownParseState): T { - if (typeof rule === 'function') { - return (rule as (tok: Token, state: MarkdownParseState) => T)(tok, state) - } - return rule -} - -interface ParsingBlockRule { - block: SpecRule - getAttrs?: (tok: Token, state: MarkdownParseState) => Attrs - wrapContent?: boolean - noCloseToken?: boolean -} - -interface ParsingNodeRule { - node: MarkupNodeType - getAttrs?: (tok: Token, state: MarkdownParseState) => Attrs -} - -interface ParsingMarkRule { - mark: MarkupMarkType - getAttrs?: (tok: Token, state: MarkdownParseState) => Attrs - noCloseToken?: boolean -} - -interface ParsingSpecialRule { - type: (state: MarkdownParseState, tok: Token) => { type: MarkupMarkType | MarkupNodeType, node: boolean } | undefined - getAttrs?: (tok: Token, state: MarkdownParseState) => Attrs | undefined -} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -interface ParsingIgnoreRule { - // empty -} - -type HandlerRecord = (state: MarkdownParseState, tok: Token) => void -type HandlersRecord = Record - -// **************************************************************** -// Markdown parser -// **************************************************************** -function isText (a: MarkupNode, b: MarkupNode): boolean { - return (a.type === MarkupNodeType.text || a.type === MarkupNodeType.reference) && b.type === MarkupNodeType.text -} -function maybeMerge (a: MarkupNode, b: MarkupNode): MarkupNode | undefined { - if (isText(a, b) && (sameSet(a.marks, b.marks) || (a.text === '' && (a.marks?.length ?? 0) === 0))) { - if (a.text === '' && (a.marks?.length ?? 0) === 0) { - return { ...b } - } - return { ...a, text: (a.text ?? '') + (b.text ?? '') } - } - return undefined -} - -interface StateElement { - type: MarkupNodeType - content: MarkupNode[] - attrs: Attrs -} - -// Object used to track the context of a running parse. -class MarkdownParseState { - stack: StateElement[] - marks: MarkupMark[] - tokenHandlers: Record void> - - constructor ( - tokenHandlers: Record void>, - readonly refUrl: string, - readonly imageUrl: string - ) { - this.stack = [{ type: MarkupNodeType.doc, attrs: {}, content: [] }] - this.marks = [] - this.tokenHandlers = tokenHandlers - } - - top (): StateElement | undefined { - return this.stack[this.stack.length - 1] - } - - push (elt: MarkupNode): void { - if (this.stack.length > 0) { - const tt = this.top() - tt?.content.push(elt) - } - } - - mergeWithLast (nodes: MarkupNode[], node: MarkupNode): boolean { - const last = nodes[nodes.length - 1] - let merged: MarkupNode | undefined - if (last !== undefined && (merged = maybeMerge(last, node)) !== undefined) { - nodes[nodes.length - 1] = merged - return true - } - return false - } - - // Adds the given text to the current position in the document, - // using the current marks as styling. - addText (text?: string): void { - const top = this.top() - if (text === undefined || top === undefined || text.length === 0) { - return - } - - const node: MarkupNode = { - type: MarkupNodeType.text, - text - } - if (this.marks !== undefined) { - node.marks = this.marks - } - - const nodes = top.content - - if (!this.mergeWithLast(nodes, node)) { - nodes.push(node) - } - } - - // Adds the given mark to the set of active marks. - openMark (mark: MarkupMark): void { - this.marks = addToSet(mark, this.marks) - } - - // Removes the given mark from the set of active marks. - closeMark (mark: MarkupMarkType): void { - this.marks = removeFromSet(mark, this.marks) - } - - parseTokens (toks: Token[] | null): void { - const _toks = [...(toks ?? [])] - while (_toks.length > 0) { - const tok = _toks.shift() - if (tok === undefined) { - break - } - - // Merge ... into one html token - if (tok.type === 'html_inline' && tok.content.trim() === '') { - while (_toks.length > 0) { - const _tok = _toks.shift() - if (_tok !== undefined) { - tok.content += _tok.content - if (_tok.type === 'html_inline' && _tok.content.trim() === '') { - break - } - } - } - } - - // Merge ... into one html token - if (tok.type === 'html_inline' && tok.content.trim().startsWith(' 0) { - const _tok = _toks.shift() - if (_tok !== undefined) { - tok.content += _tok.content - if (_tok.type === 'html_inline' && _tok.content.trim() === '') { - break - } - } - } - } - - const handler = this.tokenHandlers[tok.type] - if (handler === undefined) { - throw new Error(`Token type '${String(tok.type)} not supported by Markdown parser`) - } - handler(this, tok) - } - } - - // Add a node at the current position. - addNode (type: MarkupNodeType, attrs: Attrs, content: MarkupNode[] = []): MarkupNode { - const node: MarkupNode = { type, content } - - if (Object.keys(attrs ?? {}).length > 0) { - node.attrs = attrs - } - if (this.marks.length > 0) { - node.marks = this.marks - } - this.push(node) - return node - } - - // Wrap subsequent content in a node of the given type. - openNode (type: MarkupNodeType, attrs: Attrs): void { - this.stack.push({ type, attrs, content: [] }) - } - - // Close and return the node that is currently on top of the stack. - closeNode (): MarkupNode { - if (this.marks.length > 0) this.marks = [] - const info = this.stack.pop() - if (info !== undefined) { - return this.addNode(info.type, info.attrs, info.content) - } - return { type: MarkupNodeType.doc } - } -} - -function attrs ( - spec: ParsingBlockRule | ParsingMarkRule | ParsingNodeRule, - token: Token, - state: MarkdownParseState -): Attrs { - return spec.getAttrs?.(token, state) ?? {} -} - -// Code content is represented as a single token with a `content` -// property in Markdown-it. -function noCloseToken (spec: ParsingBlockRule | ParsingMarkRule, type: string): boolean { - return (spec.noCloseToken ?? false) || ['code_inline', 'code_block', 'fence'].indexOf(type) > 0 -} - -function withoutTrailingNewline (str: string): string { - return str[str.length - 1] === '\n' ? str.slice(0, str.length - 1) : str -} - -function addSpecBlock ( - handlers: HandlersRecord, - spec: ParsingBlockRule, - type: string, - specBlock: SpecRule -): void { - if (noCloseToken(spec, type)) { - handlers[type] = newSimpleBlockHandler(specBlock, spec) - } else { - handlers[type + '_open'] = (state, tok) => { - state.openNode(readSpec(specBlock, tok, state), attrs(spec, tok, state)) - if (spec.wrapContent === true) { - state.openNode(MarkupNodeType.paragraph, {}) - } - } - handlers[type + '_close'] = (state) => { - if (spec.wrapContent === true) { - state.closeNode() - } - state.closeNode() - } - } -} -function newSimpleBlockHandler (specBlock: SpecRule, spec: ParsingBlockRule): HandlerRecord { - return (state, tok) => { - state.openNode(readSpec(specBlock, tok, state), attrs(spec, tok, state)) - state.addText(withoutTrailingNewline(tok.content)) - state.closeNode() - } -} - -function addSpecMark (handlers: HandlersRecord, spec: ParsingMarkRule, type: string, specMark: MarkupMarkType): void { - if (noCloseToken(spec, type)) { - handlers[type] = newSimpleMarkHandler(spec, specMark) - } else { - handlers[type + '_open'] = (state, tok) => { - state.openMark({ type: specMark, attrs: attrs(spec, tok, state) }) - } - handlers[type + '_close'] = (state) => { - state.closeMark(specMark) - } - } -} -function addSpecialRule (handlers: HandlersRecord, spec: ParsingSpecialRule, type: string): void { - handlers[type + '_open'] = (state, tok) => { - const type = spec.type(state, tok) - if (type !== undefined) { - if (type.node) { - state.openNode(type.type as MarkupNodeType, spec.getAttrs?.(tok, state) ?? {}) - } else { - state.openMark({ type: type.type as MarkupMarkType, attrs: spec.getAttrs?.(tok, state) ?? {} }) - } - } - } - handlers[type + '_close'] = (state, tok) => { - const type = spec.type(state, tok) - if (type !== undefined) { - if (type.node) { - state.closeNode() - } else { - state.closeMark(type.type as MarkupMarkType) - } - } - } -} -function addIgnoreRule (handlers: HandlersRecord, spec: ParsingIgnoreRule, type: string): void { - handlers[type + '_open'] = (state, tok) => {} - handlers[type + '_close'] = (state, tok) => {} -} -function newSimpleMarkHandler (spec: ParsingMarkRule, specMark: MarkupMarkType): HandlerRecord { - return (state: MarkdownParseState, tok: Token): void => { - state.openMark({ attrs: attrs(spec, tok, state), type: specMark }) - state.addText(withoutTrailingNewline(tok.content)) - state.closeMark(specMark) - } -} - -function tokenHandlers ( - tokensBlock: Record, - tokensNode: Record, - tokensMark: Record, - specialRules: Record, - ignoreRules: Record, - htmlParser: HtmlParser -): HandlersRecord { - const handlers: HandlersRecord = {} - - Object.entries(tokensBlock).forEach(([type, spec]) => { - addSpecBlock(handlers, spec, type, spec.block) - }) - Object.entries(tokensNode).forEach(([type, spec]) => { - addSpecNode(handlers, type, spec) - }) - Object.entries(tokensMark).forEach(([type, spec]) => { - addSpecMark(handlers, spec, type, spec.mark) - }) - Object.entries(specialRules).forEach(([type, spec]) => { - addSpecialRule(handlers, spec, type) - }) - Object.entries(ignoreRules).forEach(([type, spec]) => { - addIgnoreRule(handlers, spec, type) - }) - - handlers.html_inline = (state: MarkdownParseState, tok: Token) => { - try { - const top = state.top() - if (tok.content.trim() === '' && top?.type === MarkupNodeType.embed) { - top.content = [] - state.closeNode() - return - } - const markup = htmlParser(tok.content) - if (markup.content !== undefined) { - // unwrap content from wrapping paragraph - const shouldUnwrap = - markup.content.length === 1 && - markup.content[0].type === MarkupNodeType.paragraph && - top?.type === MarkupNodeType.paragraph - - const content = nodeContent(shouldUnwrap ? markup.content[0] : markup) - for (const c of content) { - if (c.type === MarkupNodeType.embed) { - state.openNode(MarkupNodeType.embed, c.attrs ?? {}) - continue - } - state.push(c) - } - } - } catch (err: any) { - console.error(err) - state.addText(tok.content) - } - } - handlers.html_block = (state: MarkdownParseState, tok: Token) => { - try { - const model = htmlParser(tok.content) - const content = nodeContent(model) - for (const c of content) { - state.push(c) - } - } catch (err: any) { - console.error(err) - state.addText(tok.content) - } - } - - addTextHandlers(handlers) - - return handlers -} - -function addTextHandlers (handlers: HandlersRecord): void { - handlers.text = (state, tok) => { - state.addText(tok.content) - } - handlers.inline = (state, tok) => { - state.parseTokens(tok.children) - } - handlers.softbreak = (state) => { - state.addText('\n') - } -} - -function addSpecNode (handlers: HandlersRecord, type: string, spec: ParsingNodeRule): void { - handlers[type] = (state: MarkdownParseState, tok: Token) => state.addNode(spec.node, attrs(spec, tok, state)) -} - -function tokAttrGet (token: Token, name: string): string | undefined { - const attr = token.attrGet(name) - if (attr != null) { - return attr - } - // try iterate attrs - for (const [k, v] of token.attrs ?? []) { - if (k === name) { - return v - } - } -} - -function tokToAttrs (token: Token, ...names: string[]): Record { - const result: Record = {} - for (const name of names) { - const attr = token.attrGet(name) - if (attr !== null) { - result[name] = attr - } - } - return result -} - -function todoItemMetaAttrsGet (tok: Token): Record { - const userid = tokAttrGet(tok, 'userid') - const todoid = tokAttrGet(tok, 'todoid') - - const result: Record = {} - - if (userid !== undefined) { - result.userid = userid - } - if (todoid !== undefined) { - result.todoid = todoid - } - - return result -} - -// ::- A configuration of a Markdown parser. Such a parser uses -const tokensBlock: Record = { - blockquote: { block: MarkupNodeType.blockquote }, - paragraph: { block: MarkupNodeType.paragraph }, - list_item: { block: MarkupNodeType.list_item }, - task_item: { block: MarkupNodeType.taskItem, getAttrs: (tok) => ({ 'data-type': 'taskItem' }) }, - bullet_list: { - block: MarkupNodeType.bullet_list, - getAttrs: (tok) => ({ - bullet: tok.markup - }) - }, - todo_list: { - block: MarkupNodeType.todoList, - getAttrs: (tok) => ({ - bullet: tok.markup - }) - }, - todo_item: { - block: MarkupNodeType.todoItem, - getAttrs: (tok) => ({ - checked: tokAttrGet(tok, 'checked') === 'true', - ...todoItemMetaAttrsGet(tok) - }) - }, - ordered_list: { - block: MarkupNodeType.ordered_list, - getAttrs: (tok: Token) => ({ order: tokAttrGet(tok, 'start') ?? '1' }) - }, - task_list: { - block: MarkupNodeType.taskList, - getAttrs: (tok: Token) => ({ order: tokAttrGet(tok, 'start') ?? '1', 'data-type': 'taskList' }) - }, - heading: { - block: MarkupNodeType.heading, - getAttrs: (tok: Token) => ({ level: Number(tok.tag.slice(1)), marker: tok.markup }) - }, - code_block: { - block: (tok) => { - if (tok.info === 'mermaid') { - return MarkupNodeType.mermaid - } - return MarkupNodeType.code_block - }, - getAttrs: (tok: Token) => { - return { language: tok.info ?? '' } - }, - noCloseToken: true - }, - fence: { - block: (tok) => { - if (tok.info === 'mermaid') { - return MarkupNodeType.mermaid - } - return MarkupNodeType.code_block - }, - getAttrs: (tok: Token) => { - return { language: tok.info ?? '' } - }, - noCloseToken: true - }, - sub: { - block: MarkupNodeType.subLink, - getAttrs: (tok: Token) => { - return { language: tok.info ?? '' } - }, - noCloseToken: false - }, - table: { - block: MarkupNodeType.table, - noCloseToken: false - }, - th: { - block: MarkupNodeType.table_header, - getAttrs: (tok: Token) => { - return { - colspan: Number(tok.attrGet('colspan') ?? '1'), - rowspan: Number(tok.attrGet('rowspan') ?? '1') - } - }, - wrapContent: true, - noCloseToken: false - }, - tr: { - block: MarkupNodeType.table_row, - noCloseToken: false - }, - td: { - block: MarkupNodeType.table_cell, - getAttrs: (tok: Token) => { - return { - colspan: Number(tok.attrGet('colspan') ?? '1'), - rowspan: Number(tok.attrGet('rowspan') ?? '1') - } - }, - wrapContent: true, - noCloseToken: false - } -} -const tokensNode: Record = { - hr: { node: MarkupNodeType.horizontal_rule }, - image: { - node: MarkupNodeType.image, - getAttrs: (tok: Token, state) => { - const result = tokToAttrs(tok, 'src', 'title', 'alt', 'data') - result.alt = convertStringLikeToken(tok, result.alt) - if (result.src.startsWith(state.imageUrl)) { - const url = new URL(result.src) - result['data-type'] = 'image' - const file = url.searchParams.get('file') - if (file != null) { - result['file-id'] = file - } - - const width = url.searchParams.get('width') - if (width != null) { - result.width = width - } - - const height = url.searchParams.get('height') - if (height != null) { - result.height = height - } - } - return result - } - }, - hardbreak: { node: MarkupNodeType.hard_break } -} -const tokensMark: Record = { - em: { - mark: MarkupMarkType.em, - getAttrs: (tok: Token, state: MarkdownParseState) => { - return { marker: tok.markup } - } - }, - bold: { - mark: MarkupMarkType.bold, - getAttrs: (tok: Token, state: MarkdownParseState) => { - return { marker: tok.markup } - } - }, - strong: { - mark: MarkupMarkType.bold, - getAttrs: (tok: Token, state: MarkdownParseState) => { - return { marker: tok.markup } - } - }, - s: { mark: MarkupMarkType.strike }, - u: { mark: MarkupMarkType.underline }, - code_inline: { - mark: MarkupMarkType.code, - noCloseToken: true - } -} - -const specialRule: Record = { - link: { - type: (state, tok) => { - const href = tok.attrGet('href') - if ((href?.startsWith(state.refUrl) ?? false) || state.stack[state.stack.length - 1]?.type === 'reference') { - return { type: MarkupNodeType.reference, node: true } - } - return { type: MarkupMarkType.link, node: false, close: true } - }, - getAttrs: (tok: Token, state) => { - const attrs = tokToAttrs(tok, 'href', 'title') - if (attrs.href !== undefined) { - try { - const url = new URL(attrs.href) - if (attrs.href.startsWith(state.refUrl) ?? false) { - return { - label: url.searchParams?.get('label') ?? '', - id: url.searchParams?.get('_id') ?? '', - objectclass: url.searchParams?.get('_class') ?? '' - } - } - } catch (err: any) { - // ignore - } - } - return attrs - } - } -} - -const ignoreRule: Record = { - thead: {}, - tbody: {} -} - -export const isInlineToken = (token?: Token): boolean => token?.type === 'inline' - -export const isParagraphToken = (token?: Token): boolean => token?.type === 'paragraph_open' - -export const isListItemToken = (token?: Token): boolean => token?.type === 'list_item_open' - -export interface TaskListEnv { - tasklists: number -} - -interface TaskListStateCore extends StateCore { - env: TaskListEnv -} - -// The leading whitespace in a list item (token.content) is already trimmed off by markdown-it. -// The regex below checks for '[ ] ' or '[x] ' or '[X] ' at the start of the string token.content, -// where the space is either a normal space or a non-breaking space (character 160 = \u00A0). -const startsWithTodoMarkdown = (token: Token): boolean => /^\[[xX \u00A0]\][ \u00A0]/.test(token.content) -const isCheckedTodoItem = (token: Token): boolean => /^\[[xX]\][ \u00A0]/.test(token.content) - -export type HtmlParser = (html: string) => MarkupNode - -export interface MarkdownParserOptions { - refUrl: string - imageUrl: string - htmlParser?: HtmlParser -} - -export class MarkdownParser { - tokenizer: MarkdownIt - tokenHandlers: Record void> - htmlParser: HtmlParser - - constructor (private readonly options: MarkdownParserOptions) { - this.tokenizer = MarkdownIt('default', { - html: true - }) - this.tokenizer.core.ruler.after('inline', 'task_list', this.listRule) - this.tokenizer.core.ruler.after('inline', 'html_comment', this.htmlCommentRule) - - this.htmlParser = options.htmlParser ?? htmlToMarkup - this.tokenHandlers = tokenHandlers(tokensBlock, tokensNode, tokensMark, specialRule, ignoreRule, this.htmlParser) - } - - parse (text: string): MarkupNode { - const state = new MarkdownParseState(this.tokenHandlers, this.options.refUrl, this.options.imageUrl) - let doc: MarkupNode - - const tokens = this.tokenizer.parse(text, {}) - - state.parseTokens(tokens) - do { - doc = state.closeNode() - } while (state.stack.length > 0) - return doc - } - - htmlCommentRule: RuleCore = (state: StateCore): boolean => { - const tokens = state.tokens - for (let i = 0; i < tokens.length; i++) { - // Prosemirror entirely ignores comments when parsing, so - // here we replaces html comment tag with a custom tag so the comments got parsed as a node - if (tokens[i].type === 'html_block' || tokens[i].type === 'html_inline') { - const content = tokens[i].content.replaceAll('', '') - tokens[i].content = content - } - } - return true - } - - listRule: RuleCore = (state: TaskListStateCore): boolean => { - const tokens = (state as any).tokens - const states: Array<{ closeIdx: number, lastItemIdx: number }> = [] - - // step #1 - convert list items to todo items - for (let open = 0; open < tokens.length; open++) { - if (isTodoListItem(tokens, open)) { - convertTodoItem(tokens, open) - } - } - - // step #2 - convert lists to proper type - // listCloseIdx and itemCloseIdx tracks position of the list and item close tokens - // because we insert items into the list, the variables keep the position from the - // end of the list so we don't have to count inserts - let listCloseIdx = -1 - let itemCloseIdx = -1 - - for (let i = tokens.length - 1; i >= 0; i--) { - if (tokens[i].type === 'bullet_list_close') { - states.push({ closeIdx: listCloseIdx, lastItemIdx: itemCloseIdx }) - listCloseIdx = tokens.length - i - itemCloseIdx = -1 - } else if (tokens[i].type === 'list_item_close' || tokens[i].type === 'todo_item_close') { - // when found item close token of different type, split the list - if (itemCloseIdx === -1) { - itemCloseIdx = tokens.length - i - } else if (tokens[i].type !== tokens[tokens.length - itemCloseIdx].type) { - const bulletListOpen = new state.Token('bullet_list_open', 'ul', 1) - bulletListOpen.markup = tokens[i + 1].markup - tokens.splice(i + 1, 0, bulletListOpen) - tokens.splice(i + 1, 0, new state.Token('bullet_list_close', 'ul', -1)) - convertTodoList(tokens, i + 2, tokens.length - listCloseIdx, tokens.length - itemCloseIdx) - listCloseIdx = tokens.length - i - 1 - itemCloseIdx = tokens.length - i - } - } else if (tokens[i].type === 'bullet_list_open') { - if (itemCloseIdx !== -1) { - convertTodoList(tokens, i, tokens.length - listCloseIdx, tokens.length - itemCloseIdx) - } - - const prevState = states.pop() ?? { closeIdx: -1, lastItemIdx: -1 } - listCloseIdx = prevState.closeIdx - itemCloseIdx = prevState.lastItemIdx - } - } - - return true - } -} - -function convertTodoList (tokens: Token[], open: number, close: number, item: number): void { - if (tokens[open].type !== 'bullet_list_open') { - throw new Error('bullet_list_open token expected') - } - if (tokens[close].type !== 'bullet_list_close') { - throw new Error('bullet_list_close token expected') - } - - if (tokens[item].type === 'todo_item_close') { - tokens[open].type = 'todo_list_open' - tokens[close].type = 'todo_list_close' - } -} - -function convertTodoItem (tokens: Token[], open: number): boolean { - const close = findListItemCloseToken(tokens, open) - if (close !== -1) { - tokens[open].type = 'todo_item_open' - tokens[close].type = 'todo_item_close' - - const inline = tokens[open + 2] - - if (tokens[open].attrs == null) { - tokens[open].attrs = [] - } - - ;(tokens[open].attrs as any).push(['checked', isCheckedTodoItem(inline) ? 'true' : 'false']) - - if (inline.children !== null) { - const newContent = inline.children[0].content.slice(4) - if (newContent.length > 0) { - inline.children[0].content = newContent - } else { - inline.children = inline.children.slice(1) - } - - const metaTok = inline.children.find( - (tok: Token) => tok.type === 'html_inline' && tok.content.startsWith('') - ) - if (metaTok !== undefined) { - const metaValues = metaTok.content.slice(5, -4).split(',') - for (const mv of metaValues) { - if (mv.startsWith('todoid')) { - ;(tokens[open].attrs as any).push(['todoid', mv.slice(7)]) - } - if (mv.startsWith('userid')) { - ;(tokens[open].attrs as any).push(['userid', mv.slice(7)]) - } - } - } - } - - return true - } - - return false -} - -function findListItemCloseToken (tokens: Token[], open: number): number { - if (tokens[open].type !== 'list_item_open') { - throw new Error('list_item_open token expected') - } - - const level = tokens[open].level - for (let close = open + 1; close < tokens.length; close++) { - if (tokens[close].type === 'list_item_close' && tokens[close].level === level) { - return close - } - } - - return -1 -} - -// todo token structure -// tokens[i].type === list_item_open -// tokens[i + 1].type === paragraph -// tokens[i + 2].type === inline -function isTodoListItem (tokens: Token[], pos: number): boolean { - return ( - isListItemToken(tokens[pos]) && - isParagraphToken(tokens[pos + 1]) && - isInlineToken(tokens[pos + 2]) && - startsWithTodoMarkdown(tokens[pos + 2]) - ) -} - -function convertStringLikeToken (tok: Token, attrValue?: string): string { - if (typeof attrValue === 'string' && attrValue !== '') { - return attrValue - } - const children = tok.children ?? [] - let out = '' - for (const child of children) { - switch (child.type) { - case 'text': - out += child.content - break - case 'hardbreak': - out += '\n' - break - } - } - - return out -} diff --git a/foundations/core/packages/text-markdown/src/serializer.ts b/foundations/core/packages/text-markdown/src/serializer.ts deleted file mode 100644 index ed490608ce..0000000000 --- a/foundations/core/packages/text-markdown/src/serializer.ts +++ /dev/null @@ -1,876 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { MarkupMark, MarkupNode, MarkupNodeType } from '@hcengineering/text-core' -import { markupToHtml } from '@hcengineering/text-html' - -import { isInSet, markEq } from './marks' -import { nodeContent, nodeAttrs } from './node' - -type FirstDelim = (i: number, attrs?: Record, parentAttrs?: Record) => string -interface IState { - wrapBlock: (delim: string, firstDelim: string | null, node: MarkupNode, f: () => void) => void - flushClose: (size: number) => void - atBlank: () => void - ensureNewLine: () => void - write: (content: string) => void - closeBlock: (node: any) => void - text: (text: string, escape?: boolean) => void - render: (node: MarkupNode, parent: MarkupNode, index: number) => void - renderContent: (parent: MarkupNode) => void - renderInline: (parent: MarkupNode) => void - renderList: (node: MarkupNode, delim: string, firstDelim: FirstDelim) => void - esc: (str: string, startOfLine?: boolean) => string - htmlEsc: (str: string) => string - quote: (str: string) => string - repeat: (str: string, n: number) => string - markString: (mark: MarkupMark, open: boolean, parent: MarkupNode, index: number) => string - renderHtml: (node: MarkupNode) => string - refUrl: string - imageUrl: string - inAutolink?: boolean - renderAHref?: boolean -} - -type NodeProcessor = (state: IState, node: MarkupNode, parent: MarkupNode, index: number) => void - -interface InlineState { - active: MarkupMark[] - trailing: string - parent: MarkupNode - node?: MarkupNode - marks: MarkupMark[] -} - -// ************************************************************* - -function backticksFor (side: boolean): string { - return side ? '`' : '`' -} - -function isPlainURL (link: MarkupMark, parent: MarkupNode, index: number): boolean { - if (link.attrs?.title !== undefined || !/^\w+:/.test(link.attrs?.href)) return false - const content = parent.content?.[index] - if (content === undefined) { - return false - } - if ( - content.type !== MarkupNodeType.text || - content.text !== link.attrs?.href || - content.marks?.[content.marks.length - 1] !== link - ) { - return false - } - return index === (parent.content?.length ?? 0) - 1 || !isInSet(link, parent.content?.[index + 1]?.marks ?? []) -} - -const formatTodoItem: FirstDelim = (i, attrs, parentAttrs?: Record) => { - const meta = - attrs?.todoid !== undefined && attrs?.userid !== undefined - ? `` - : '' - - const bullet = parentAttrs?.bullet ?? '*' - return `${bullet} [${attrs?.checked === true ? 'x' : ' '}] ${meta}` -} - -// ************************************************************* - -export const storeNodes: Record = { - blockquote: (state, node) => { - state.wrapBlock('> ', null, node, () => { - state.renderContent(node) - }) - }, - codeBlock: (state, node) => { - state.write('```' + `${nodeAttrs(node).language ?? ''}` + '\n') - // TODO: Check for node.textContent - state.renderInline(node) - // state.text(node.text ?? '', false) - state.ensureNewLine() - state.write('```') - state.closeBlock(node) - }, - mermaid: (state, node) => { - state.write('```mermaid\n') - state.renderInline(node) - state.ensureNewLine() - state.write('```') - state.closeBlock(node) - }, - heading: (state, node) => { - const attrs = nodeAttrs(node) - if (attrs.marker === '=' && attrs.level === 1) { - state.renderInline(node) - state.ensureNewLine() - state.write('===\n') - } else if (attrs.marker === '-' && attrs.level === 2) { - state.renderInline(node) - state.ensureNewLine() - state.write('---\n') - } else { - state.write(state.repeat('#', attrs.level !== undefined ? Number(attrs.level) : 1) + ' ') - state.renderInline(node) - } - state.closeBlock(node) - }, - horizontalRule: (state, node) => { - state.write(`${nodeAttrs(node).markup ?? '---'}`) - state.closeBlock(node) - }, - bulletList: (state, node) => { - state.renderList(node, ' ', () => `${nodeAttrs(node).bullet ?? '*'}` + ' ') - }, - taskList: (state, node) => { - state.renderList(node, ' ', () => '* [ ]' + ' ') - }, - todoList: (state, node) => { - state.renderList(node, ' ', formatTodoItem) - }, - orderedList: (state, node) => { - let start = 1 - if (nodeAttrs(node).order !== undefined) { - start = Number(nodeAttrs(node).order) - } - const maxW = String(start + nodeContent(node).length - 1).length - const space = state.repeat(' ', maxW + 2) - state.renderList(node, space, (i: number) => { - const nStr = String(start + i) - return state.repeat(' ', maxW - nStr.length) + nStr + '. ' - }) - }, - listItem: (state, node) => { - state.renderContent(node) - }, - taskItem: (state, node) => { - state.renderContent(node) - }, - todoItem: (state, node) => { - state.renderContent(node) - }, - paragraph: (state, node) => { - state.renderInline(node) - state.closeBlock(node) - }, - subLink: (state, node) => { - state.write('') - state.renderAHref = true - state.renderInline(node) - state.renderAHref = false - state.write('') - }, - - image: (state, node) => { - const attrs = nodeAttrs(node) - if (attrs.token != null && attrs['file-id'] != null) { - // Convert image to token format - state.write( - '![' + - state.esc(`${attrs.alt ?? ''}`) + - '](' + - (state.imageUrl + - `${attrs['file-id']}` + - `?file=${attrs['file-id']}` + - (attrs.width != null ? '&width=' + state.esc(`${attrs.width}`) : '') + - (attrs.height != null ? '&height=' + state.esc(`${attrs.height}`) : '') + - (attrs.token != null ? '&token=' + state.esc(`${attrs.token}`) : '')) + - (attrs.title != null ? ' ' + state.quote(`${attrs.title}`) : '') + - ')' - ) - } else if (attrs['file-id'] != null) { - // Convert image to fileid format - state.write( - '![' + - state.esc(`${attrs.alt ?? ''}`) + - '](' + - (state.imageUrl + - `${attrs['file-id']}` + - (attrs.width != null ? '&width=' + state.esc(`${attrs.width}`) : '') + - (attrs.height != null ? '&height=' + state.esc(`${attrs.height}`) : '')) + - (attrs.title != null ? ' ' + state.quote(`${attrs.title}`) : '') + - ')' - ) - } else { - if (attrs.width != null || attrs.height != null) { - state.write( - '' + state.quote(`${attrs.title}`) + '' : '>') - ) - } else { - state.write( - '![' + - state.esc(`${attrs.alt ?? ''}`) + - '](' + - state.esc(`${attrs.src}`) + - (attrs.title != null ? ' ' + state.quote(`${attrs.title}`) : '') + - ')' - ) - } - } - }, - reference: (state, node) => { - const attrs = nodeAttrs(node) - let url = state.refUrl - if (!url.includes('?')) { - url += '?' - } else { - url += '&' - } - state.write( - '[' + - state.esc(`${attrs.label ?? ''}`) + - '](' + - `${url}${makeQuery({ - _class: attrs.objectclass, - _id: attrs.id, - label: attrs.label - })}` + - (attrs.title !== undefined ? ' ' + state.quote(`${attrs.title}`) : '') + - ')' - ) - }, - markdown: (state, node) => { - state.renderInline(node) - state.closeBlock(node) - }, - comment: (state, node) => { - state.write('') - }, - hardBreak: (state, node, parent, index) => { - const content = nodeContent(parent) - for (let i = index + 1; i < content.length; i++) { - if (content[i].type !== node.type) { - state.write('\\\n') - return - } - } - }, - text: (state, node) => { - // Check if test has reference mark, in this case we need to remove [[]] - state.text(node.text ?? '') - }, - emoji: (state, node) => { - state.text(node.attrs?.emoji as string) - }, - table: (state, node) => { - state.write(state.renderHtml(node)) - state.closeBlock(node) - }, - embed: (state, node) => { - const attrs = nodeAttrs(node) - const embedUrl = attrs.src as string - state.write(``) - // Slashes are escaped to prevent autolink creation - state.write(state.htmlEsc(embedUrl).replace(/\//g, '/')) - state.write('') - } -} - -interface MarkProcessor { - open: ((_state: IState, mark: MarkupMark, parent: MarkupNode, index: number) => string) | string - close: ((_state: IState, mark: MarkupMark, parent: MarkupNode, index: number) => string) | string - mixable: boolean - expelEnclosingWhitespace: boolean - escape: boolean -} - -export const storeMarks: Record = { - em: { - open: '*', - close: '*', - mixable: true, - expelEnclosingWhitespace: true, - escape: true - }, - italic: { - open: '*', - close: '*', - mixable: true, - expelEnclosingWhitespace: true, - escape: true - }, - bold: { - open: '**', - close: '**', - mixable: true, - expelEnclosingWhitespace: true, - escape: true - }, - strong: { - open: '**', - close: '**', - mixable: true, - expelEnclosingWhitespace: true, - escape: true - }, - strike: { - open: '~~', - close: '~~', - mixable: true, - expelEnclosingWhitespace: true, - escape: true - }, - underline: { - open: '', - close: '', - mixable: true, - expelEnclosingWhitespace: true, - escape: true - }, - link: { - open: (state, mark, parent, index) => { - if (state.renderAHref === true) { - return `` - } else { - state.inAutolink = isPlainURL(mark, parent, index) - return state.inAutolink ? '<' : '[' - } - }, - close: (state, mark, parent, index) => { - if (state.renderAHref === true) { - return '' - } else { - const { inAutolink } = state - state.inAutolink = undefined - - const href = (mark.attrs?.href as string) ?? '' - // eslint-disable-next-line - const url = href.replace(/[\(\)"\\<>]/g, '\\$&') - const hasSpaces = url.includes(' ') - - return inAutolink === true - ? '>' - : '](' + - (hasSpaces ? `<${url}>` : url) + - (mark.attrs?.title !== undefined ? ` "${(mark.attrs?.title as string).replace(/"/g, '\\"')}"` : '') + - ')' - } - }, - mixable: false, - expelEnclosingWhitespace: false, - escape: true - }, - code: { - open: (state, mark, parent, index) => { - return backticksFor(false) - }, - close: (state, mark, parent, index) => { - return backticksFor(true) - }, - mixable: false, - expelEnclosingWhitespace: false, - escape: false - }, - textColor: { - open: (state, mark, parent, index) => { - if (mark.attrs?.color === undefined) { - return '' - } - return `` - }, - close: (state, mark, parent, index) => { - if (mark.attrs?.color === undefined) { - return '' - } - return '' - }, - mixable: false, - expelEnclosingWhitespace: false, - escape: false - }, - textStyle: { - open: (state, mark, parent, index) => { - const attrs = mark.attrs ?? {} - if (Object.keys(attrs).length === 0) { - return '' - } - const styleAttrs = Object.entries(attrs) - .map(([key, value]) => { - const kebabKey = key.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`) - return `${kebabKey}: ${value}` - }) - .join('; ') - - return `` - }, - close: (state, mark, parent, index) => { - const attrs = mark.attrs ?? {} - if (Object.keys(attrs).length === 0) { - return '' - } - return '' - }, - mixable: false, - expelEnclosingWhitespace: false, - escape: false - } -} - -export type HtmlWriter = (markup: MarkupNode) => string - -export interface StateOptions { - tightLists: boolean - refUrl: string - imageUrl: string - htmlWriter?: HtmlWriter -} -export class MarkdownState implements IState { - nodes: Record - marks: Record - delim: string - out: string - closed: boolean - closedNode?: MarkupNode - inTightList: boolean - options: StateOptions - refUrl: string - imageUrl: string - htmlWriter: HtmlWriter - - constructor ( - nodes = storeNodes, - marks = storeMarks, - options: StateOptions = { tightLists: true, refUrl: 'ref://', imageUrl: 'http://' } - ) { - this.nodes = nodes - this.marks = marks - this.delim = this.out = '' - this.closed = false - this.inTightList = false - this.refUrl = options.refUrl - this.imageUrl = options.imageUrl - this.htmlWriter = options.htmlWriter ?? markupToHtml - - this.options = options - } - - flushClose (size: number): void { - if (this.closed) { - if (!this.atBlank()) this.out += '\n' - if (size > 1) { - this.addDelim(size) - } - this.closed = false - } - } - - private addDelim (size: number): void { - let delimMin = this.delim - const trim = /\s+$/.exec(delimMin) - if (trim !== null) { - delimMin = delimMin.slice(0, delimMin.length - trim[0].length) - } - for (let i = 1; i < size; i++) { - this.out += delimMin + '\n' - } - } - - renderHtml (node: MarkupNode): string { - return this.htmlWriter(node) - } - - wrapBlock (delim: string, firstDelim: string | null, node: MarkupNode, f: () => void): void { - const old = this.delim - this.write(firstDelim ?? delim) - this.delim += delim - f() - this.delim = old - this.closeBlock(node) - } - - atBlank (): boolean { - return /(^|\n)$/.test(this.out) - } - - // :: () - // Ensure the current content ends with a newline. - ensureNewLine (): void { - if (!this.atBlank()) this.out += '\n' - } - - // :: (?string) - // Prepare the state for writing output (closing closed paragraphs, - // adding delimiters, and so on), and then optionally add content - // (unescaped) to the output. - write (content: string): void { - this.flushClose(2) - if (this.delim !== undefined && this.atBlank()) this.out += this.delim - if (content.length > 0) this.out += content - } - - // :: (Node) - // Close the block for the given node. - closeBlock (node: MarkupNode): void { - this.closedNode = node - this.closed = true - } - - // :: (string, ?bool) - // Add the given text to the document. When escape is not `false`, - // it will be escaped. - text (text: string, escape = false): void { - const lines = text.split('\n') - for (let i = 0; i < lines.length; i++) { - const startOfLine = this.atBlank() || this.closed - this.write('') - this.out += escape ? this.esc(lines[i], startOfLine) : lines[i] - if (i !== lines.length - 1) this.out += '\n' - } - } - - // :: (Node) - // Render the given node as a block. - render (node: MarkupNode, parent: MarkupNode, index: number): void { - if (this.nodes[node.type] === undefined) { - throw new Error('Token type `' + node.type + '` not supported by Markdown renderer') - } - this.nodes[node.type](this, node, parent, index) - } - - // :: (Node) - // Render the contents of `parent` as block nodes. - renderContent (parent: MarkupNode): void { - nodeContent(parent).forEach((node: MarkupNode, i: number) => { - this.render(node, parent, i) - }) - } - - reorderMixableMark (state: InlineState, mark: MarkupMark, i: number, len: number): void { - for (let j = 0; j < state.active.length; j++) { - const other = state.active[j] - if (!this.marks[other.type].mixable || this.checkSwitchMarks(i, j, state, mark, other, len)) { - break - } - } - } - - reorderMixableMarks (state: InlineState, len: number): void { - // Try to reorder 'mixable' marks, such as em and strong, which - // in Markdown may be opened and closed in different order, so - // that order of the marks for the token matches the order in - // active. - - for (let i = 0; i < len; i++) { - const mark = state.marks[i] - const mm = this.marks[mark.type] - if (mm == null) { - break - } - if (!mm.mixable) break - this.reorderMixableMark(state, mark, i, len) - } - } - - private checkSwitchMarks ( - i: number, - j: number, - state: InlineState, - mark: MarkupMark, - other: MarkupMark, - len: number - ): boolean { - if (!markEq(mark, other) || i === j) { - return false - } - this.switchMarks(i, j, state, mark, len) - return true - } - - private switchMarks (i: number, j: number, state: InlineState, mark: MarkupMark, len: number): void { - if (i > j) { - state.marks = state.marks - .slice(0, j) - .concat(mark) - .concat(state.marks.slice(j, i)) - .concat(state.marks.slice(i + 1, len)) - } - if (j > i) { - state.marks = state.marks - .slice(0, i) - .concat(state.marks.slice(i + 1, j)) - .concat(mark) - .concat(state.marks.slice(j, len)) - } - } - - renderNodeInline (state: InlineState, index: number): void { - state.marks = state.node?.marks ?? [] - this.updateHardBreakMarks(state, index) - - const leading = this.adjustLeading(state) - - const inner: MarkupMark | undefined = state.marks.length > 0 ? state.marks[state.marks.length - 1] : undefined - const noEsc = inner !== undefined && !(this.marks[inner.type]?.escape ?? false) - const len = state.marks.length - (noEsc ? 1 : 0) - - this.reorderMixableMarks(state, len) - - // Find the prefix of the mark set that didn't change - this.checkCloseMarks(state, len, index) - - // Output any previously expelled trailing whitespace outside the marks - if (leading !== '') this.text(leading) - - // Open the marks that need to be opened - this.checkOpenMarks(state, len, index, inner, noEsc) - } - - private checkOpenMarks ( - state: InlineState, - len: number, - index: number, - inner: MarkupMark | undefined, - noEsc: boolean - ): void { - if (state.node !== undefined) { - this.updateActiveMarks(state, len, index) - - // Render the node. Special case code marks, since their content - // may not be escaped. - if (this.isNoEscapeRequire(state.node, inner, noEsc, state)) { - this.renderMarkText(inner as MarkupMark, state, index) - } else { - this.render(state.node, state.parent, index) - } - } - } - - private isNoEscapeRequire ( - node: MarkupNode, - inner: MarkupMark | undefined, - noEsc: boolean, - state: InlineState - ): boolean { - return inner !== undefined && noEsc && node.type === MarkupNodeType.text - } - - private renderMarkText (inner: MarkupMark, state: InlineState, index: number): void { - this.text( - this.markString(inner, true, state.parent, index) + - (state.node?.text as string) + - this.markString(inner, false, state.parent, index + 1), - false - ) - } - - private updateActiveMarks (state: InlineState, len: number, index: number): void { - while (state.active.length < len) { - const add = state.marks[state.active.length] - state.active.push(add) - this.text(this.markString(add, true, state.parent, index), false) - } - } - - private checkCloseMarks (state: InlineState, len: number, index: number): void { - let keep = 0 - while (keep < Math.min(state.active.length, len) && markEq(state.marks[keep], state.active[keep])) { - ++keep - } - - // Close the marks that need to be closed - while (keep < state.active.length) { - const mark = state.active.pop() - if (mark !== undefined) { - this.text(this.markString(mark, false, state.parent, index), false) - } - } - } - - private adjustLeading (state: InlineState): string { - let leading = state.trailing - state.trailing = '' - // If whitespace has to be expelled from the node, adjust - // leading and trailing accordingly. - const node = state?.node - if (this.isText(node) && this.isMarksHasExpelEnclosingWhitespace(state)) { - const match = /^(\s*)(.*?)(\s*)$/m.exec(node?.text ?? '') - if (match !== null) { - const [leadMatch, innerMatch, trailMatch] = [match[1], match[2], match[3]] - leading += leadMatch - state.trailing = trailMatch - this.adjustLeadingTextNode(leadMatch, trailMatch, state, innerMatch, node as MarkupNode) - } - } - return leading - } - - private isMarksHasExpelEnclosingWhitespace (state: InlineState): boolean { - return state.marks.some((mark) => this.marks[mark.type]?.expelEnclosingWhitespace) - } - - private adjustLeadingTextNode ( - lead: string, - trail: string, - state: InlineState, - inner: string, - node: MarkupNode - ): void { - if (lead !== '' || trail !== '') { - state.node = inner !== undefined ? { ...node, text: inner } : undefined - if (state.node === undefined) { - state.marks = state.active - } - } - } - - private updateHardBreakMarks (state: InlineState, index: number): void { - if (state.node !== undefined && state.node.type === MarkupNodeType.hard_break) { - state.marks = this.filterHardBreakMarks(state.marks, index, state) - } - } - - private filterHardBreakMarks (marks: MarkupMark[], index: number, state: InlineState): MarkupMark[] { - const content = state.parent.content ?? [] - const next = content[index + 1] - if (!this.isHardbreakText(next)) { - return [] - } - return marks.filter((m) => isInSet(m, next.marks ?? [])) - } - - private isHardbreakText (next?: MarkupNode): boolean { - return ( - next !== undefined && (next.type !== MarkupNodeType.text || (next.text !== undefined && /\S/.test(next.text))) - ) - } - - private isText (node?: MarkupNode): boolean { - return node !== undefined && node.type === MarkupNodeType.text && node.text !== undefined - } - - // :: (Node) - // Render the contents of `parent` as inline content. - renderInline (parent: MarkupNode): void { - const state: InlineState = { active: [], trailing: '', parent, marks: [] } - nodeContent(parent).forEach((nde, index) => { - state.node = nde - this.renderNodeInline(state, index) - }) - state.node = undefined - this.renderNodeInline(state, 0) - } - - // :: (Node, string, (number) → string) - // Render a node's content as a list. `delim` should be the extra - // indentation added to all lines except the first in an item, - // `firstDelim` is a function going from an item index to a - // delimiter for the first line of the item. - renderList (node: MarkupNode, delim: string, firstDelim: FirstDelim): void { - this.flushListClose(node) - - const isTight: boolean = - typeof node.attrs?.tight !== 'undefined' ? node.attrs.tight === 'true' : this.options.tightLists - const prevTight = this.inTightList - this.inTightList = isTight - - nodeContent(node).forEach((child, i) => { - this.renderListItem(node, child, i, isTight, delim, firstDelim) - }) - this.inTightList = prevTight - } - - renderListItem ( - node: MarkupNode, - child: MarkupNode, - i: number, - isTight: boolean, - delim: string, - firstDelim: FirstDelim - ): void { - if (i > 0 && isTight) this.flushClose(1) - this.wrapBlock(delim, firstDelim(i, node.content?.[i].attrs, node.attrs), node, () => { - this.render(child, node, i) - }) - } - - private flushListClose (node: MarkupNode): void { - if (this.closed && this.closedNode?.type === node.type) { - this.flushClose(3) - } else if (this.inTightList) { - this.flushClose(1) - } - } - - // :: (string, ?bool) → string - // Escape the given string so that it can safely appear in Markdown - // content. If `startOfLine` is true, also escape characters that - // has special meaning only at the start of the line. - esc (str: string, startOfLine = false): string { - if (str == null) { - return '' - } - str = str.replace(/[`*\\~\[\]]/g, '\\$&') // eslint-disable-line - if (startOfLine) { - str = str.replace(/^[:#\-*+]/, '\\$&').replace(/^(\d+)\./, '$1\\.') - } - str = str.replace(/\r?\n/g, '\\\n') - return str - } - - htmlEsc (str: string): string { - if (str == null) { - return '' - } - - return str - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') - } - - quote (str: string): string { - const wrap = !(str?.includes('"') ?? false) ? '""' : !(str?.includes("'") ?? false) ? "''" : '()' - return wrap[0] + str + wrap[1] - } - - // :: (string, number) → string - // Repeat the given string `n` times. - repeat (str: string, n: number): string { - let out = '' - for (let i = 0; i < n; i++) out += str - return out - } - - // : (Mark, bool, string?) → string - // Get the markdown string for a given opening or closing mark. - markString (mark: MarkupMark, open: boolean, parent: MarkupNode, index: number): string { - let value = mark.attrs?.marker - if (value === undefined) { - const info = this.marks[mark.type] - if (info == null) { - throw new Error(`No info for mark ${mark.type}`) - } - value = open ? info.open : info.close - } - return typeof value === 'string' ? value : (value(this, mark, parent, index) ?? '') - } -} - -function makeQuery (obj: Record): string { - return Object.keys(obj) - .filter((it) => it[1] != null) - .map(function (k) { - return encodeURIComponent(k) + '=' + encodeURIComponent(obj[k] as string | number | boolean) - }) - .join('&') -} diff --git a/foundations/core/packages/text-markdown/tsconfig.json b/foundations/core/packages/text-markdown/tsconfig.json deleted file mode 100644 index b5ae22f6e4..0000000000 --- a/foundations/core/packages/text-markdown/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/foundations/core/packages/text-ydoc/.eslintrc.js b/foundations/core/packages/text-ydoc/.eslintrc.js deleted file mode 100644 index 72235dc283..0000000000 --- a/foundations/core/packages/text-ydoc/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/foundations/core/packages/text-ydoc/CHANGELOG.json b/foundations/core/packages/text-ydoc/CHANGELOG.json deleted file mode 100644 index 4fd6618ea4..0000000000 --- a/foundations/core/packages/text-ydoc/CHANGELOG.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "name": "@hcengineering/text-ydoc", - "entries": [ - { - "version": "0.7.18", - "tag": "@hcengineering/text-ydoc_v0.7.18", - "date": "Mon, 27 Oct 2025 17:09:21 GMT", - "comments": { - "patch": [ - { - "comment": "add support for textColor and textStyle marks" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/text\" from `^0.7.17` to `0.7.18`" - } - ] - } - }, - { - "version": "0.7.17", - "tag": "@hcengineering/text-ydoc_v0.7.17", - "date": "Mon, 27 Oct 2025 13:27:12 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" - } - ] - } - }, - { - "version": "0.7.5", - "tag": "@hcengineering/text-ydoc_v0.7.5", - "date": "Tue, 14 Oct 2025 04:58:17 GMT", - "comments": { - "patch": [ - { - "comment": "update deps" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`" - }, - { - "comment": "Updating dependency \"@hcengineering/text\" from `^0.7.4` to `0.7.5`" - }, - { - "comment": "Updating dependency \"@hcengineering/text-core\" from `^0.7.4` to `0.7.5`" - } - ] - } - }, - { - "version": "0.7.4", - "tag": "@hcengineering/text-ydoc_v0.7.4", - "date": "Sat, 11 Oct 2025 18:20:33 GMT", - "comments": { - "patch": [ - { - "comment": "Update to latest platform rig" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`" - }, - { - "comment": "Updating dependency \"@hcengineering/text\" from `^0.7.3` to `0.7.4`" - }, - { - "comment": "Updating dependency \"@hcengineering/text-core\" from `^0.7.3` to `0.7.4`" - } - ] - } - }, - { - "version": "0.7.3", - "tag": "@hcengineering/text-ydoc_v0.7.3", - "date": "Wed, 08 Oct 2025 03:40:53 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`" - } - ] - } - } - ] -} diff --git a/foundations/core/packages/text-ydoc/CHANGELOG.md b/foundations/core/packages/text-ydoc/CHANGELOG.md deleted file mode 100644 index 05fe657b1e..0000000000 --- a/foundations/core/packages/text-ydoc/CHANGELOG.md +++ /dev/null @@ -1,35 +0,0 @@ -# Change Log - @hcengineering/text-ydoc - -This log was last generated on Mon, 27 Oct 2025 17:09:21 GMT and should not be manually modified. - -## 0.7.18 -Mon, 27 Oct 2025 17:09:21 GMT - -### Patches - -- add support for textColor and textStyle marks - -## 0.7.17 -Mon, 27 Oct 2025 13:27:12 GMT - -_Version update only_ - -## 0.7.5 -Tue, 14 Oct 2025 04:58:17 GMT - -### Patches - -- update deps - -## 0.7.4 -Sat, 11 Oct 2025 18:20:33 GMT - -### Patches - -- Update to latest platform rig - -## 0.7.3 -Wed, 08 Oct 2025 03:40:53 GMT - -_Initial release_ - diff --git a/foundations/core/packages/text-ydoc/config/rig.json b/foundations/core/packages/text-ydoc/config/rig.json deleted file mode 100644 index 0110930f55..0000000000 --- a/foundations/core/packages/text-ydoc/config/rig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig" -} diff --git a/foundations/core/packages/text-ydoc/jest.config.js b/foundations/core/packages/text-ydoc/jest.config.js deleted file mode 100644 index 069ea8aa0d..0000000000 --- a/foundations/core/packages/text-ydoc/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ['./src'], - coverageReporters: ['text-summary', 'html', 'lcov'] -} diff --git a/foundations/core/packages/text-ydoc/package.json b/foundations/core/packages/text-ydoc/package.json deleted file mode 100644 index c279bce5ac..0000000000 --- a/foundations/core/packages/text-ydoc/package.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "name": "@hcengineering/text-ydoc", - "version": "0.7.18", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "files": [ - "lib/**/*", - "types/**/*", - "src/**/*", - "!src/**/__test__/**", - "tsconfig.json" - ], - "author": "Anticrm Platform Contributors", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "test": "jest --passWithNoTests --silent --coverage", - "build:watch": "compile", - "format": "format src", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --coverage", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "workspace:^0.7.19", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "@typescript-eslint/parser": "^6.21.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.6.2", - "typescript": "^5.9.3", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5", - "jest-environment-jsdom": "^30.2.0", - "fast-equals": "^5.2.2", - "y-prosemirror": "^1.3.7", - "eslint-plugin-svelte": "^2.35.1" - }, - "dependencies": { - "@hcengineering/core": "workspace:^0.7.22", - "@hcengineering/text": "workspace:^0.7.18", - "@hcengineering/text-core": "workspace:^0.7.18", - "yjs": "^13.6.27", - "y-protocols": "^1.0.6" - }, - "repository": "https://github.com/hcengineering/huly.core", - "publishConfig": { - "access": "public" - }, - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.js" - } - } -} diff --git a/foundations/core/packages/text-ydoc/src/__tests__/ydoc.test.ts b/foundations/core/packages/text-ydoc/src/__tests__/ydoc.test.ts deleted file mode 100644 index 43d6f6fab5..0000000000 --- a/foundations/core/packages/text-ydoc/src/__tests__/ydoc.test.ts +++ /dev/null @@ -1,259 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { type Markup, generateId } from '@hcengineering/core' -import { type MarkupNode, jsonToMarkup, jsonToPmNode, markupToJSON } from '@hcengineering/text' -import { prosemirrorToYXmlFragment, yDocToProsemirrorJSON } from 'y-prosemirror' -import { deepEqual } from 'fast-equals' -import { Doc as YDoc } from 'yjs' -import { markupToYDoc, yDocToMarkup } from '../ydoc' - -declare global { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace jest { - interface Matchers { - toEqualMarkup: (expected: string) => R - toEqualYdoc: (expected: YDoc) => R - } - } -} - -expect.extend({ - toEqualMarkup (received: string, expected: string) { - const pass = received === expected || deepEqual(JSON.parse(received), JSON.parse(expected)) - return { - message: () => - pass - ? `Expected markup strings NOT to be equal:\n Expected: ${expected}\n Received: ${received}` - : `Expected markup strings to be equal:\n Expected: ${expected}\n Received: ${received}`, - pass - } - }, - - toEqualYdoc (received: YDoc, expected: YDoc) { - const expectedJSON = expected.toJSON() - const receivedJSON = received.toJSON() - - const pass = deepEqual(expectedJSON, receivedJSON) - return { - message: () => - pass - ? `Expected yjs documents NOT to be equal:\n Expected: ${JSON.stringify(expectedJSON)}\n Received: ${JSON.stringify(receivedJSON)}` - : `Expected yjs documents to be equal:\n Expected: ${JSON.stringify(expectedJSON)}\n Received: ${JSON.stringify(receivedJSON)}`, - pass - } - } -}) - -function referenceMarkupToYDoc (markup: Markup, field: string): YDoc { - const ydoc = new YDoc({ guid: generateId() }) - const fragment = ydoc.getXmlFragment(field) - prosemirrorToYXmlFragment(jsonToPmNode(markupToJSON(markup)), fragment) - return ydoc -} - -function referenceYDocToMarkup (ydoc: YDoc, field: string): Markup { - const json = yDocToProsemirrorJSON(ydoc, field) - return jsonToMarkup(json as MarkupNode) -} - -const markups: Array<{ name: string, markup: Markup, skipYdocCompare?: boolean }> = [ - { - name: 'text', - markup: '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello world"}]}]}' - }, - { - name: 'text with bold mark', - markup: - '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"bold","attrs":{}}],"text":"hello world"}]}]}' - }, - { - name: 'separate paragraphs with bold mark', - markup: - '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"bold","attrs":{}}],"text":"hello"}]},{"type":"paragraph","content":[{"type":"text","marks":[{"type":"bold","attrs":{}}],"text":"world"}]}]}' - }, - { - name: 'mixed text and text with bold mark', - markup: - '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello "},{"type":"text","marks":[{"type":"bold","attrs":{}}],"text":"world"}]}]}' - }, - { - name: 'mixed text with italic and text with bold and italic marks', - markup: - '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"hello "},{"type":"text","marks":[{"type":"bold","attrs":{}},{"type":"italic","attrs":{}}],"text":"world"}]}]}' - }, - { - name: 'text with link and italic marks', - markup: - '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello "},{"type":"text","text":"hello world","marks":[{"type":"link","attrs":{"href":"http://example.com","target":"_blank","rel":"noopener noreferrer","class":"cursor-pointer"}},{"type":"italic","attrs":{}}]}]}]}' - }, - { - name: 'image', - markup: - '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"image","attrs":{"src":"http://example.com/image.jpg","alt":"image"}}]}]}' - }, - { - name: 'table with formatting inside', - markup: - '{"type":"doc","content":[{"type":"table","content":[{"type":"tableRow","content":[{"type":"tableCell","attrs":{"colspan":1,"rowspan":1},"content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"bold","attrs":{}}],"text":"1"}]}]},{"type":"tableCell","attrs":{"colspan":1,"rowspan":1},"content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"2"}]}]}]},{"type":"tableRow","content":[{"type":"tableCell","attrs":{"colspan":1,"rowspan":1},"content":[{"type":"codeBlock","content":[{"type":"text","text":"3"}]}]},{"type":"tableCell","attrs":{"colspan":1,"rowspan":1},"content":[{"type":"paragraph","content":[{"type":"text","text":"4"}]}]}]}]}]}' - }, - { - name: 'non-overlapping marks', - markup: `{ - "type": "doc", - "content": [ - { - "type": "paragraph", - "content": [ - { - "type": "text", - "text": "xy", - "marks": [ - { - "type": "inline-comment", - "attrs": { - "thread": "1" - } - } - ] - }, - { - "type": "text", - "text": "z", - "marks": [ - { - "type": "inline-comment", - "attrs": { - "thread": "2" - } - } - ] - } - ] - } - ] -}`, - skipYdocCompare: true - }, - { - name: 'overlapping marks', - markup: `{ - "type": "doc", - "content": [ - { - "type": "paragraph", - "content": [ - { - "type": "text", - "text": "x", - "marks": [ - { - "type": "inline-comment", - "attrs": { - "thread": "1" - } - } - ] - }, - { - "type": "text", - "text": "y", - "marks": [ - { - "type": "inline-comment", - "attrs": { - "thread": "1" - } - }, - { - "type": "inline-comment", - "attrs": { - "thread": "2" - } - } - ] - }, - { - "type": "text", - "text": "z", - "marks": [ - { - "type": "inline-comment", - "attrs": { - "thread": "2" - } - } - ] - } - ] - } - ] -}`, - skipYdocCompare: true - } -] - -describe('markupToYDoc', () => { - describe.each(markups)('compare with reference', ({ name, markup, skipYdocCompare }) => { - if (skipYdocCompare !== true) { - it(name, () => { - const expected = referenceMarkupToYDoc(markup, 'test') - const actual = markupToYDoc(markup, 'test') - expect(actual).toEqualYdoc(expected) - }) - } - }) - - describe.each(markups)('converts markup to ydoc and back', ({ name, markup }) => { - it(name, () => { - const ydoc = markupToYDoc(markup, 'test') - const actual = yDocToMarkup(ydoc, 'test') - - expect(actual).toEqualMarkup(markup) - }) - }) -}) - -describe('yDocToMarkup', () => { - describe.each(markups)('compare with original', ({ name, markup }) => { - it(name, () => { - const ydoc = referenceMarkupToYDoc(markup, 'test') - const actual = yDocToMarkup(ydoc, 'test') - - expect(actual).toEqualMarkup(markup) - }) - }) - - describe.each(markups)('compare with reference', ({ name, markup }) => { - it(name, () => { - const ydoc = referenceMarkupToYDoc(markup, 'test') - const expected = referenceYDocToMarkup(ydoc, 'test') - const actual = yDocToMarkup(ydoc, 'test') - - expect(actual).toEqualMarkup(expected) - }) - }) - - describe.each(markups)('converts ydoc to markup and back', ({ name, markup, skipYdocCompare }) => { - if (skipYdocCompare !== true) { - it(name, () => { - const expected = referenceMarkupToYDoc(markup, 'test') - const actual = referenceMarkupToYDoc(yDocToMarkup(expected, 'test'), 'test') - - expect(actual).toEqualYdoc(expected) - }) - } - }) -}) diff --git a/foundations/core/packages/text-ydoc/src/index.ts b/foundations/core/packages/text-ydoc/src/index.ts deleted file mode 100644 index d6dbacbc50..0000000000 --- a/foundations/core/packages/text-ydoc/src/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -// -// Copyright © 2023 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export * from './ydoc' diff --git a/foundations/core/packages/text-ydoc/src/ydoc.ts b/foundations/core/packages/text-ydoc/src/ydoc.ts deleted file mode 100644 index f777c7eb4a..0000000000 --- a/foundations/core/packages/text-ydoc/src/ydoc.ts +++ /dev/null @@ -1,142 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { generateId, Markup } from '@hcengineering/core' -import { jsonToMarkup, MarkupMarkType, MarkupNodeType, markupToJSON, type MarkupNode } from '@hcengineering/text' -import { hashAttrs, stripHash } from '@hcengineering/text-core' -import { Doc as YDoc, XmlElement as YXmlElement, XmlFragment as YXmlFragment, XmlText as YXmlText } from 'yjs' - -/** - * Convert Markup to Y.Doc - * - * @public - */ -export function markupToYDoc (markup: Markup, field: string): YDoc { - return jsonToYDoc(markupToJSON(markup), field) -} - -/** - * Convert Markup JSON to Y.Doc - * - * @public - */ -export function jsonToYDoc (json: MarkupNode, field: string): YDoc { - const ydoc = new YDoc({ guid: generateId() }) - const fragment = ydoc.getXmlFragment(field) - - const nodes = json.type === 'doc' ? (json.content ?? []) : [json] - nodes.map((p) => nodeToXmlElement(fragment, p)) - - return ydoc -} - -function nodeToXmlElement (parent: YXmlFragment, node: MarkupNode): YXmlElement | YXmlText { - const elem = node.type === 'text' ? new YXmlText() : new YXmlElement(node.type) - parent.push([elem]) - - if (elem instanceof YXmlElement) { - if (node.content !== undefined && node.content.length > 0) { - node.content.map((p) => nodeToXmlElement(elem, p)) - } - } else if (elem instanceof YXmlText) { - // https://github.com/yjs/y-prosemirror/blob/master/src/plugins/sync-plugin.js#L777 - const attributes: Record = {} - if (node.marks !== undefined) { - const attrCount = new Map() - node.marks.forEach((mark) => { - attrCount.set(mark.type, (attrCount.get(mark.type) ?? 0) + 1) - }) - - node.marks.forEach((mark) => { - const count = attrCount.get(mark.type) ?? 0 - const type = count > 1 ? `${mark.type}--${hashAttrs(mark.attrs)}` : mark.type - attributes[type] = mark.attrs ?? {} - }) - } - - elem.applyDelta([ - { - insert: node.text ?? '', - attributes - } - ]) - } - - if (node.attrs !== undefined) { - Object.entries(node.attrs).forEach(([key, value]) => { - elem.setAttribute(key, value) - }) - } - - return elem -} - -/** - * Convert Y.Doc to Markup - * - * @public - */ -export function yDocToMarkup (ydoc: YDoc, field: string): Markup { - const fragment = ydoc.getXmlFragment(field) - const json = xmlFragmentToNode(fragment) - return jsonToMarkup({ type: MarkupNodeType.doc, content: json }) -} - -function xmlFragmentToNode (fragment: YXmlFragment): MarkupNode[] { - const result: MarkupNode[] = [] - - for (let i = 0; i < fragment.length; i++) { - const item = fragment.get(i) - if (item instanceof YXmlElement) { - const node: MarkupNode = { - type: item.nodeName as MarkupNodeType - } - - // Handle attributes - const attrs = item.getAttributes() - if (Object.keys(attrs).length > 0) { - node.attrs = attrs - } - - // Handle content - if (item.length > 0) { - node.content = xmlFragmentToNode(item) - } - - result.push(node) - } else if (item instanceof YXmlText) { - // Handle text with marks - const delta = item.toDelta() - for (const op of delta) { - const textNode: MarkupNode = { - type: MarkupNodeType.text, - text: op.insert - } - - // Convert attributes to marks - if (op.attributes != null) { - textNode.marks = Object.entries(op.attributes).map(([type, attrs]) => ({ - type: stripHash(type) as MarkupMarkType, - attrs: attrs as Record - })) - } - - result.push(textNode) - } - } - } - - return result -} diff --git a/foundations/core/packages/text-ydoc/tsconfig.json b/foundations/core/packages/text-ydoc/tsconfig.json deleted file mode 100644 index b5ae22f6e4..0000000000 --- a/foundations/core/packages/text-ydoc/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/foundations/core/packages/text/.eslintrc.js b/foundations/core/packages/text/.eslintrc.js deleted file mode 100644 index 72235dc283..0000000000 --- a/foundations/core/packages/text/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/foundations/core/packages/text/CHANGELOG.json b/foundations/core/packages/text/CHANGELOG.json deleted file mode 100644 index 26a36bbca7..0000000000 --- a/foundations/core/packages/text/CHANGELOG.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "name": "@hcengineering/text", - "entries": [ - { - "version": "0.7.18", - "tag": "@hcengineering/text_v0.7.18", - "date": "Mon, 27 Oct 2025 17:09:21 GMT", - "comments": { - "patch": [ - { - "comment": "add support for textColor and textStyle marks" - } - ] - } - }, - { - "version": "0.7.17", - "tag": "@hcengineering/text_v0.7.17", - "date": "Mon, 27 Oct 2025 13:27:12 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" - } - ] - } - }, - { - "version": "0.7.5", - "tag": "@hcengineering/text_v0.7.5", - "date": "Tue, 14 Oct 2025 04:58:17 GMT", - "comments": { - "patch": [ - { - "comment": "update deps" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`" - }, - { - "comment": "Updating dependency \"@hcengineering/text-core\" from `^0.7.4` to `0.7.5`" - } - ] - } - }, - { - "version": "0.7.4", - "tag": "@hcengineering/text_v0.7.4", - "date": "Sat, 11 Oct 2025 18:20:33 GMT", - "comments": { - "patch": [ - { - "comment": "Update to latest platform rig" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`" - }, - { - "comment": "Updating dependency \"@hcengineering/text-core\" from `^0.7.3` to `0.7.4`" - } - ] - } - }, - { - "version": "0.7.3", - "tag": "@hcengineering/text_v0.7.3", - "date": "Wed, 08 Oct 2025 03:40:53 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`" - } - ] - } - } - ] -} diff --git a/foundations/core/packages/text/CHANGELOG.md b/foundations/core/packages/text/CHANGELOG.md deleted file mode 100644 index e0442c313d..0000000000 --- a/foundations/core/packages/text/CHANGELOG.md +++ /dev/null @@ -1,35 +0,0 @@ -# Change Log - @hcengineering/text - -This log was last generated on Mon, 27 Oct 2025 17:09:21 GMT and should not be manually modified. - -## 0.7.18 -Mon, 27 Oct 2025 17:09:21 GMT - -### Patches - -- add support for textColor and textStyle marks - -## 0.7.17 -Mon, 27 Oct 2025 13:27:12 GMT - -_Version update only_ - -## 0.7.5 -Tue, 14 Oct 2025 04:58:17 GMT - -### Patches - -- update deps - -## 0.7.4 -Sat, 11 Oct 2025 18:20:33 GMT - -### Patches - -- Update to latest platform rig - -## 0.7.3 -Wed, 08 Oct 2025 03:40:53 GMT - -_Initial release_ - diff --git a/foundations/core/packages/text/config/rig.json b/foundations/core/packages/text/config/rig.json deleted file mode 100644 index 0110930f55..0000000000 --- a/foundations/core/packages/text/config/rig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig" -} diff --git a/foundations/core/packages/text/jest.config.js b/foundations/core/packages/text/jest.config.js deleted file mode 100644 index 069ea8aa0d..0000000000 --- a/foundations/core/packages/text/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ['./src'], - coverageReporters: ['text-summary', 'html', 'lcov'] -} diff --git a/foundations/core/packages/text/package.json b/foundations/core/packages/text/package.json deleted file mode 100644 index 83adb46dc4..0000000000 --- a/foundations/core/packages/text/package.json +++ /dev/null @@ -1,96 +0,0 @@ -{ - "name": "@hcengineering/text", - "version": "0.7.18", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "files": [ - "lib/**/*", - "types/**/*", - "src/**/*", - "!src/**/__test__/**", - "tsconfig.json" - ], - "author": "Anticrm Platform Contributors", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "test": "jest --passWithNoTests --silent --coverage", - "build:watch": "compile", - "format": "format src", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --coverage", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "workspace:^0.7.19", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "@typescript-eslint/parser": "^6.21.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.6.2", - "typescript": "^5.9.3", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5", - "@types/markdown-it": "~13.0.0", - "eslint-plugin-svelte": "^2.35.1" - }, - "dependencies": { - "@hcengineering/core": "workspace:^0.7.22", - "@hcengineering/text-core": "workspace:^0.7.18", - "@tiptap/core": "^2.11.7", - "@tiptap/html": "^2.11.7", - "@tiptap/pm": "^2.11.7", - "@tiptap/starter-kit": "^2.11.7", - "@tiptap/extension-gapcursor": "^2.11.7", - "@tiptap/extension-heading": "^2.11.7", - "@tiptap/extension-highlight": "^2.11.7", - "@tiptap/extension-history": "^2.11.7", - "@tiptap/extension-link": "^2.11.7", - "@tiptap/extension-mention": "^2.11.7", - "@tiptap/extension-table": "^2.11.7", - "@tiptap/extension-table-cell": "^2.11.7", - "@tiptap/extension-table-header": "^2.11.7", - "@tiptap/extension-table-row": "^2.11.7", - "@tiptap/extension-task-item": "^2.11.7", - "@tiptap/extension-task-list": "^2.11.7", - "@tiptap/extension-bold": "^2.11.7", - "@tiptap/extension-blockquote": "^2.11.7", - "@tiptap/extension-text": "^2.11.7", - "@tiptap/extension-document": "^2.11.7", - "@tiptap/extension-ordered-list": "^2.11.7", - "@tiptap/extension-bullet-list": "^2.11.7", - "@tiptap/extension-list-item": "^2.11.7", - "@tiptap/extension-dropcursor": "^2.11.7", - "@tiptap/extension-hard-break": "^2.11.7", - "@tiptap/extension-horizontal-rule": "^2.11.7", - "@tiptap/extension-italic": "^2.11.7", - "@tiptap/extension-paragraph": "^2.11.7", - "@tiptap/extension-strike": "^2.11.7", - "@tiptap/extension-typography": "^2.11.7", - "@tiptap/extension-code-block": "^2.11.7", - "@tiptap/extension-code": "^2.11.7", - "@tiptap/extension-underline": "^2.11.7", - "@tiptap/suggestion": "^2.11.7", - "prosemirror-codemark": "^0.4.2", - "fast-equals": "^5.2.2", - "@tiptap/extension-text-align": "~2.11.0", - "@tiptap/extension-text-style": "~2.11.0" - }, - "repository": "https://github.com/hcengineering/huly.core", - "publishConfig": { - "access": "public" - }, - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.js" - } - } -} diff --git a/foundations/core/packages/text/src/extensions.ts b/foundations/core/packages/text/src/extensions.ts deleted file mode 100644 index 01e3f270b7..0000000000 --- a/foundations/core/packages/text/src/extensions.ts +++ /dev/null @@ -1,18 +0,0 @@ -// -// Copyright © 2023 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { ServerKit } from './kits/server-kit' - -export const defaultExtensions = [ServerKit] diff --git a/foundations/core/packages/text/src/index.ts b/foundations/core/packages/text/src/index.ts deleted file mode 100644 index ae3ab5ea13..0000000000 --- a/foundations/core/packages/text/src/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -// -// Copyright © 2023 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export * from './extensions' -export * from '@hcengineering/text-core' -export * from './nodes' -export * from './markup/utils' -export * from './marks/code' -export * from './marks/colors' -export * from './marks/noteBase' -export * from './marks/inlineComment' - -export * from './kits/common-kit' -export * from './kits/server-kit' -export * from './kit' - -export * from './tiptapExtensions' diff --git a/foundations/core/packages/text/src/kit.ts b/foundations/core/packages/text/src/kit.ts deleted file mode 100644 index 7600dbf98f..0000000000 --- a/foundations/core/packages/text/src/kit.ts +++ /dev/null @@ -1,89 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { AnyExtension, Extension } from '@tiptap/core' - -export type ExtensionFactory = ( - extension: T, - options?: Partial | boolean -) => ExtensionSpec - -export interface ExtensionSpec { - extension: T - options: Partial | boolean -} - -export type ExtensionSpecOptions = { - [K in keyof T]: T[K] extends ExtensionSpec ? Partial | boolean : never -} - -export function extensionKit ( - name: string, - fn: (e: ExtensionFactory, o: O) => K -): Extension> { - return Extension.create({ - name, - addExtensions () { - const e: ExtensionFactory = (extension, options) => { - // ExtensionFactory -> ExtensionSpec is mostly intented as a wrapper to provide a comfortable typing - return { extension, options: options ?? true } - } - - const extensions: AnyExtension[] = [] - const entries: object = fn(e, this.options) as any - - for (const [key, _data] of Object.entries(entries)) { - const data = _data as ExtensionSpec - if (data?.extension === undefined) continue - - let options = mergeKitOptions(data.options, (this.options as any)[key]) ?? false - - // "false" is indication that the extension should not be loaded at all - if (options === false) continue - - // "true" is indication that the extension should be loaded with whatever options some parent loader provided - if (options === true) { - if (typeof data.options === 'object') { - options = data.options - } else { - options = {} - } - } - - extensions.push(data.extension.configure(options)) - } - - return extensions - } - }) -} - -export function mergeKitOptions> (target: T, source: T): T { - if (typeof target === 'object' && typeof source === 'object') { - const output = { ...target } - Object.keys(source).forEach((key: keyof T) => { - const a = target[key] - const b = source[key] - if (typeof a === 'object' && typeof b === 'object') { - output[key] = mergeKitOptions(a, b) - } else { - output[key] = b ?? a - } - }) - return output - } - - return source ?? target -} diff --git a/foundations/core/packages/text/src/kits/common-kit.ts b/foundations/core/packages/text/src/kits/common-kit.ts deleted file mode 100644 index 4e4639a43e..0000000000 --- a/foundations/core/packages/text/src/kits/common-kit.ts +++ /dev/null @@ -1,135 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - Blockquote, - Bold, - BulletList, - Document, - Dropcursor, - Gapcursor, - Heading, - History, - HorizontalRule, - Italic, - Link, - ListItem, - OrderedList, - Paragraph, - Strike, - Table, - TableCell, - TableHeader, - TableRow, - Text, - TextAlign, - TextStyle, - Typography, - Underline -} from '../tiptapExtensions' - -import { ExtensionFactory, extensionKit } from '../kit' -import { BackgroundColor, TextColor } from '../marks/colors' -import { InlineCommentMark } from '../marks/inlineComment' -import { CodeBlockExtension, codeBlockOptions, CommentNode, MarkdownNode, TodoItemNode, TodoListNode } from '../nodes' - -import { CodeExtension, codeOptions } from '../marks/code' -import { MermaidExtension, mermaidOptions } from '../nodes/mermaid' - -export const CommonKitFactory = (e: ExtensionFactory) => - ({ - text: e(Text), - document: e(Document), - paragraph: e(Paragraph), - - bold: e(Bold), - italic: e(Italic), - strike: e(Strike), - - comment: e(CommentNode), - markdown: e(MarkdownNode, { HTMLAttributes: { class: 'proseCodeBlock' } }), - - inlineComment: e(InlineCommentMark), - - horizontalRule: e(HorizontalRule), - heading: e(Heading), - underline: e(Underline), - blockquote: e(Blockquote, { HTMLAttributes: { class: 'proseBlockQuote' } }), - link: e(Link.extend({ inclusive: false }), { - openOnClick: false, - HTMLAttributes: { class: 'cursor-pointer', rel: 'noopener noreferrer', target: '_blank' } - }), - textAlign: e(TextAlign, { - types: ['heading', 'paragraph'], - alignments: ['left', 'center', 'right'], - defaultAlignment: null - }), - - typography: e(Typography), - - dropcursor: e(Dropcursor), - gapcursor: e(Gapcursor), - history: e(History) - }) as const - -export const CommonKit = extensionKit('common-kit', CommonKitFactory) - -export const TextColorStylingKit = extensionKit( - 'text-color-styling', - (e) => - ({ - textStyle: e(TextStyle), - testColor: e(TextColor), - backgroundColor: e(BackgroundColor, { types: ['tableCell'] }) - }) as const -) - -export const TableKit = extensionKit( - 'table-kit', - (e) => - ({ - table: e(Table, { resizable: false, HTMLAttributes: { class: 'proseTable' } }), - tableRow: e(TableRow), - tableHeader: e(TableHeader), - tableCell: e(TableCell) - }) as const -) - -export const CodeSnippetsKit = extensionKit( - 'code-snippet-kit', - (e) => - ({ - codeBlock: e(CodeBlockExtension, codeBlockOptions), - codeBlockMermaid: e(MermaidExtension, mermaidOptions), - codeInline: e(CodeExtension, codeOptions) - }) as const -) - -export const CommonListKitFactory = (e: ExtensionFactory) => - ({ - listItem: e(ListItem.extend({ group: 'listItems' })), - bulletList: e(BulletList.extend({ content: 'listItems+' })), - orderedList: e(OrderedList.extend({ content: 'listItems+' })) - }) as const - -export const ListKit = extensionKit( - 'list-kit', - (e) => - ({ - ...CommonListKitFactory(e), - todoItem: e(TodoItemNode), - todoList: e(TodoListNode) - }) as const -) diff --git a/foundations/core/packages/text/src/kits/server-kit.ts b/foundations/core/packages/text/src/kits/server-kit.ts deleted file mode 100644 index 3778c22fd0..0000000000 --- a/foundations/core/packages/text/src/kits/server-kit.ts +++ /dev/null @@ -1,55 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { NoteBaseExtension } from '../marks/noteBase' -import { QMSInlineCommentMark } from '../marks/qmsInlineCommentMark' - -import { EmojiNode } from '../nodes/emoji' -import { FileNode } from '../nodes/file' -import { ImageNode } from '../nodes/image' -import { ReferenceNode } from '../nodes/reference' - -import { EmbedNode } from '../nodes/embed' - -import { ExtensionFactory, extensionKit } from '../kit' -import { HardBreak } from '../tiptapExtensions' -import { CodeSnippetsKit, CommonKitFactory, ListKit, TableKit, TextColorStylingKit } from './common-kit' - -export const ServerKitFactory = (e: ExtensionFactory) => - ({ - ...CommonKitFactory(e), - - // ========================================================================================== - // Extensions and kits with separate / extended implementations in the client-side editor kit - // See file://./../../../../plugins/text-editor-resources/src/kits/editor-kit.ts - // ============================================================================= - - lists: e(ListKit), - codeSnippets: e(CodeSnippetsKit), - tables: e(TableKit), - textColorStyling: e(TextColorStylingKit), - - hardBreak: e(HardBreak), - reference: e(ReferenceNode), - file: e(FileNode), - image: e(ImageNode), - embed: e(EmbedNode), - emoji: e(EmojiNode), - - inlineNote: e(NoteBaseExtension), // Semi-deprecated, should be removed in the future - qmsInlineCommentMark: e(QMSInlineCommentMark) // Semi-deprecated, should be removed in the future - }) as const - -export const ServerKit = extensionKit('server-kit', ServerKitFactory) diff --git a/foundations/core/packages/text/src/marks/code.ts b/foundations/core/packages/text/src/marks/code.ts deleted file mode 100644 index 282848acfe..0000000000 --- a/foundations/core/packages/text/src/marks/code.ts +++ /dev/null @@ -1,82 +0,0 @@ -import Code, { CodeOptions } from '@tiptap/extension-code' -import { Plugin, PluginKey } from '@tiptap/pm/state' -import { EditorView } from '@tiptap/pm/view' -import { Slice } from '@tiptap/pm/model' -import codemark from 'prosemirror-codemark' - -export const codeOptions: CodeOptions = { - HTMLAttributes: { - class: 'proseCode' - } -} - -/** - * Note: do not forget to include css import for UI use of this extension. - * import 'prosemirror-codemark/dist/codemark.css' - */ -export const CodeExtension = Code.extend({ - addProseMirrorPlugins () { - return [ - ...codemark({ markType: this.editor.schema.marks.code }), - new Plugin({ - key: new PluginKey('code-consecutive-backticks'), - props: { - // Typing a character inside of two backticks will wrap the character - // in an inline code mark. - handleTextInput: (view: EditorView, from: number, to: number, text: string) => { - const { state } = view - - // Prevent access out of document bounds - if (from === 0 || to === state.doc.nodeSize - 1 || text === '`') { - return false - } - - if ( - from === to && - state.doc.textBetween(from - 1, from) === '`' && - state.doc.textBetween(to, to + 1) === '`' - ) { - const start = from - 1 - const end = to + 1 - view.dispatch( - state.tr - .delete(start, end) - .insertText(text, start) - .addMark(start, start + text.length, state.schema.marks.code.create()) - ) - - return true - } - - return false - }, - - // Pasting a character inside of two backticks will wrap the character - // in an inline code mark. - handlePaste: (view: EditorView, _event: Event, slice: Slice) => { - const { state } = view - const { from, to } = state.selection - - // Prevent access out of document bounds - if (from === 0 || to === state.doc.nodeSize - 1) { - return false - } - - const start = from - 1 - const end = to + 1 - if (from === to && state.doc.textBetween(start, from) === '`' && state.doc.textBetween(to, end) === '`') { - view.dispatch( - state.tr - .replaceRange(start, end, slice) - .addMark(start, start + slice.size, state.schema.marks.code.create()) - ) - return true - } - - return false - } - } - }) - ] - } -}) diff --git a/foundations/core/packages/text/src/marks/colors.ts b/foundations/core/packages/text/src/marks/colors.ts deleted file mode 100644 index a29f83a49f..0000000000 --- a/foundations/core/packages/text/src/marks/colors.ts +++ /dev/null @@ -1,142 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { Extension } from '@tiptap/core' -import '@tiptap/extension-text-style' - -export interface BackgroundColorOptions { - types: string[] -} - -declare module '@tiptap/core' { - interface Commands { - colors: { - setTextColor: (color: string) => ReturnType - unsetTextColor: () => ReturnType - setBackgroundColor: (color: string) => ReturnType - unsetBackgroundColor: () => ReturnType - } - } -} - -export const BackgroundColor = Extension.create({ - name: 'backgroundColor', - - addOptions () { - return { - types: [] - } - }, - - addGlobalAttributes () { - return [ - { - types: this.options.types, - attributes: { - backgroundColor: { - parseHTML: (element) => { - return element.getAttribute('data-background-color') ?? undefined - }, - renderHTML: (attributes) => { - if (typeof attributes.backgroundColor !== 'string') { - return {} - } - - return { - 'data-background-color': attributes.backgroundColor, - style: `background-color: ${attributes.backgroundColor}` - } - } - } - } - } - ] - }, - - addCommands () { - return { - setBackgroundColor: - (backgroundColor: string) => - ({ commands }) => { - return this.options.types - .map((type) => commands.updateAttributes(type, { backgroundColor })) - .every((response) => response) - }, - - unsetBackgroundColor: - () => - ({ commands }) => { - return this.options.types - .map((type) => commands.resetAttributes(type, 'backgroundColor')) - .every((response) => response) - } - } - } -}) - -export interface TextColorOptions { - types: string[] -} - -export const TextColor = Extension.create({ - name: 'textColor', - - addOptions () { - return { - types: ['textStyle'] - } - }, - - addGlobalAttributes () { - return [ - { - types: this.options.types, - attributes: { - color: { - parseHTML: (element) => { - return element.getAttribute('data-color') ?? undefined - }, - renderHTML: (attributes) => { - if (typeof attributes.color !== 'string') { - return {} - } - - return { - 'data-color': attributes.color, - style: `color: ${attributes.color}` - } - } - } - } - } - ] - }, - - addCommands () { - return { - setTextColor: - (color: string) => - ({ chain }) => { - return chain().setMark('textStyle', { color }).run() - }, - - unsetTextColor: - () => - ({ chain }) => { - return chain().unsetMark('textStyle').run() - } - } - } -}) diff --git a/foundations/core/packages/text/src/marks/inlineComment.ts b/foundations/core/packages/text/src/marks/inlineComment.ts deleted file mode 100644 index a2d942fc57..0000000000 --- a/foundations/core/packages/text/src/marks/inlineComment.ts +++ /dev/null @@ -1,87 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { Mark } from '@tiptap/core' -import { Fragment, Node, Slice } from '@tiptap/pm/model' -import { Plugin, PluginKey } from '@tiptap/pm/state' - -export const InlineCommentMark = Mark.create({ - name: 'inline-comment', - excludes: '', - - inclusive: false, - - parseHTML () { - return [ - { - tag: 'span.proseInlineComment[data-inline-comment-thread]' - } - ] - }, - - renderHTML ({ HTMLAttributes, mark }) { - return ['span', { ...HTMLAttributes, class: 'proseInlineComment' }, 0] - }, - - addAttributes () { - const name = 'data-inline-comment-thread-id' - return { - thread: { - default: undefined, - parseHTML: (element) => { - return element.getAttribute(name) - }, - renderHTML: (attributes) => { - return { [name]: attributes.thread } - } - } - } - }, - - addProseMirrorPlugins () { - return [...(this.parent?.() ?? []), InlineCommentPasteFixPlugin()] - } -}) - -function removeMarkFromNode (node: Node, name: string): Node { - if (node.isText) { - return node.mark(node.marks.filter((mark) => mark.type.name !== name)) - } - - if (node.content.size > 0) { - const nodes: Node[] = [] - node.content.forEach((child) => { - nodes.push(removeMarkFromNode(child, name)) - }) - return node.copy(Fragment.fromArray(nodes)) - } - - return node -} - -export function InlineCommentPasteFixPlugin (): Plugin { - return new Plugin({ - key: new PluginKey('inline-comment-paste-fix-plugin'), - props: { - transformPasted: (slice) => { - const nodes: Node[] = [] - slice.content.forEach((node) => { - nodes.push(removeMarkFromNode(node, 'inline-comment')) - }) - return new Slice(Fragment.fromArray(nodes), slice.openStart, slice.openEnd) - } - } - }) -} diff --git a/foundations/core/packages/text/src/marks/noteBase.ts b/foundations/core/packages/text/src/marks/noteBase.ts deleted file mode 100644 index 1114d061ce..0000000000 --- a/foundations/core/packages/text/src/marks/noteBase.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Mark } from '@tiptap/core' -import { getDataAttribute } from '../nodes' - -export const name = 'note' -export enum NoteKind { - Neutral = 'neutral', - Dangerous = 'dangerous', - DangerousLight = 'dangerous-light', - Warning = 'warning', - WarningLight = 'warning-light', - Positive = 'positive', - PositiveLight = 'positive-light', - Primary = 'primary', - PrimaryLight = 'primary-light' -} - -declare module '@tiptap/core' { - export interface Commands { - [name]: { - setNote: (text: string, kind: NoteKind) => ReturnType - unsetNote: () => ReturnType - } - } -} - -export const NoteBaseExtension = Mark.create({ - name, - - parseHTML () { - return [ - { - tag: `span[data-mark="${name}"]` - } - ] - }, - - renderHTML ({ HTMLAttributes, mark }) { - return [ - 'span', - { ...HTMLAttributes, 'data-mark': this.name, class: `theme-text-editor-note-anchor ${mark.attrs.kind}` }, - 0 - ] - }, - - addAttributes () { - return { - title: { - default: null - }, - kind: getDataAttribute('kind', { default: NoteKind.Neutral }) - } - } -}) diff --git a/foundations/core/packages/text/src/marks/qmsInlineCommentMark.ts b/foundations/core/packages/text/src/marks/qmsInlineCommentMark.ts deleted file mode 100644 index 72391e10dc..0000000000 --- a/foundations/core/packages/text/src/marks/qmsInlineCommentMark.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Mark } from '@tiptap/core' - -const NAME = 'node-uuid' - -/** - * @public - */ -export const QMSInlineCommentMark = Mark.create({ - name: NAME, - inline: true, - - parseHTML () { - return [ - { - tag: `span[${NAME}]` - } - ] - }, - - renderHTML ({ HTMLAttributes }) { - return ['span', HTMLAttributes, 0] - } -}) diff --git a/foundations/core/packages/text/src/markup/__tests__/dsl.test.ts b/foundations/core/packages/text/src/markup/__tests__/dsl.test.ts deleted file mode 100644 index 37841dfc8f..0000000000 --- a/foundations/core/packages/text/src/markup/__tests__/dsl.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { - markLink, - markUnderline, - MarkupNodeType, - nodeDoc, - nodeImage, - nodeParagraph, - nodeReference, - nodeText -} from '@hcengineering/text-core' -import { jsonToHTML } from '../utils' - -describe('dsl', () => { - it('returns a MarkupNode for complex doc', () => { - const doc = nodeDoc( - nodeParagraph(nodeText('Hello, '), nodeReference({ id: '123', label: 'World', objectclass: 'world' })), - nodeParagraph( - nodeText('Check out '), - markLink({ href: 'https://example.com', title: 'this link' }, markUnderline(nodeText('this link'))), - nodeText('.') - ) - ) - expect(jsonToHTML(doc)).toEqual( - '

Hello, @World

Check out this link.

' - ) - }) -}) - -describe('nodeDoc', () => { - it('returns a MarkupNode with type "doc"', () => { - const result = nodeDoc() - expect(result.type).toEqual(MarkupNodeType.doc) - }) - - it('returns a MarkupNode with the provided content', () => { - const content = [ - { type: MarkupNodeType.paragraph, content: [{ type: MarkupNodeType.text, text: 'Hello' }] }, - { type: MarkupNodeType.paragraph, content: [{ type: MarkupNodeType.text, text: 'World' }] } - ] - const result = nodeDoc(...content) - expect(result.content).toEqual(content) - }) - - it('returns an empty MarkupNode if no content is provided', () => { - const result = nodeDoc() - expect(result.content).toEqual([]) - }) -}) - -describe('nodeParagraph', () => { - it('returns a MarkupNode with type "paragraph"', () => { - const result = nodeParagraph() - expect(result.type).toEqual(MarkupNodeType.paragraph) - }) - - it('returns a MarkupNode with the provided content', () => { - const content = [{ type: MarkupNodeType.text, text: 'Hello' }] - const result = nodeParagraph(...content) - expect(result.content).toEqual(content) - }) - - it('returns an empty MarkupNode if no content is provided', () => { - const result = nodeParagraph() - expect(result.content).toEqual([]) - }) -}) - -describe('nodeText', () => { - it('returns a MarkupNode with type "text"', () => { - const result = nodeText('Hello') - expect(result.type).toEqual(MarkupNodeType.text) - }) - - it('returns a MarkupNode with the provided text', () => { - const result = nodeText('Hello') - expect(result.text).toEqual('Hello') - }) -}) - -describe('nodeImage', () => { - it('returns a MarkupNode with type "image"', () => { - const attrs = { src: 'image.jpg' } - const result = nodeImage(attrs) - expect(result.type).toEqual(MarkupNodeType.image) - }) - - it('returns a MarkupNode with the provided attributes', () => { - const attrs = { src: 'image.jpg', alt: 'Image', width: 500, height: 300 } - const result = nodeImage(attrs) - expect(result.attrs).toEqual(attrs) - }) -}) - -describe('nodeReference', () => { - it('returns a MarkupNode with type "reference"', () => { - const attrs = { id: '123', label: 'Reference', objectclass: 'class' } - const result = nodeReference(attrs) - expect(result.type).toEqual(MarkupNodeType.reference) - }) - - it('returns a MarkupNode with the provided attributes', () => { - const attrs = { id: '123', label: 'Reference', objectclass: 'class' } - const result = nodeReference(attrs) - expect(result.attrs).toEqual(attrs) - }) -}) diff --git a/foundations/core/packages/text/src/markup/__tests__/utils.test.ts b/foundations/core/packages/text/src/markup/__tests__/utils.test.ts deleted file mode 100644 index 2bff4dc275..0000000000 --- a/foundations/core/packages/text/src/markup/__tests__/utils.test.ts +++ /dev/null @@ -1,416 +0,0 @@ -/** - * @jest-environment jsdom - */ - -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { - areEqualMarkups, - isEmptyMarkup, - isEmptyNode, - jsonToMarkup, - MarkupMarkType, - MarkupNode, - MarkupNodeType, - markupToJSON, - nodeDoc, - nodeParagraph, - nodeText -} from '@hcengineering/text-core' -import { Editor, getSchema } from '@tiptap/core' -import { ServerKit } from '../../kits/server-kit' -import { getMarkup, htmlToJSON, htmlToMarkup, jsonToHTML, jsonToPmNode, jsonToText, pmNodeToJSON } from '../utils' - -// mock tiptap functions -jest.mock('@tiptap/html', () => ({ - generateHTML: jest.fn(() => '

hello

'), - generateJSON: jest.fn(() => ({ - type: 'doc', - content: [{ type: 'paragraph', content: [{ type: 'text', text: 'hello' }] }] - })) -})) - -const extensions = [ServerKit] - -describe('EmptyMarkup', () => { - it('is empty markup', async () => { - const editor = new Editor({ extensions }) - expect(isEmptyMarkup(getMarkup(editor))).toBeTruthy() - }) -}) - -describe('getMarkup', () => { - it('with empty content', async () => { - const editor = new Editor({ extensions }) - expect(getMarkup(editor)).toEqual('{"type":"doc","content":[{"type":"paragraph","attrs":{"textAlign":null}}]}') - }) - it('with some content', async () => { - const editor = new Editor({ extensions, content: '

hello

' }) - expect(getMarkup(editor)).toEqual( - '{"type":"doc","content":[{"type":"paragraph","attrs":{"textAlign":null},"content":[{"type":"text","text":"hello"}]}]}' - ) - }) - it('with empty paragraphs as content', async () => { - const editor = new Editor({ extensions, content: '

' }) - expect(getMarkup(editor)).toEqual( - '{"type":"doc","content":[{"type":"paragraph","attrs":{"textAlign":null}},{"type":"paragraph","attrs":{"textAlign":null}}]}' - ) - }) -}) - -describe('isEmptyMarkup', () => { - it('returns true for undefined content', async () => { - expect(isEmptyMarkup(undefined)).toBeTruthy() - expect(isEmptyMarkup('')).toBeTruthy() - }) - it('returns true for empty content', async () => { - const editor = new Editor({ extensions }) - expect(isEmptyMarkup(getMarkup(editor))).toBeTruthy() - }) - it('returns true for empty paragraphs content', async () => { - const editor = new Editor({ extensions, content: '

' }) - expect(isEmptyMarkup(getMarkup(editor))).toBeTruthy() - }) - it('returns true for empty paragraphs content with spaces', async () => { - const editor = new Editor({ extensions, content: '

' }) - expect(isEmptyMarkup(getMarkup(editor))).toBeTruthy() - }) - it('returns false for not empty content', async () => { - const editor = new Editor({ extensions, content: '

hello

' }) - expect(isEmptyMarkup(getMarkup(editor))).toBeFalsy() - }) - it('returns true for various empty content', async () => { - expect(isEmptyMarkup(jsonToMarkup({ type: MarkupNodeType.doc }))).toBeTruthy() - expect(isEmptyMarkup(jsonToMarkup({ type: MarkupNodeType.doc, content: [] }))).toBeTruthy() - expect( - isEmptyMarkup(jsonToMarkup({ type: MarkupNodeType.doc, content: [{ type: MarkupNodeType.paragraph }] })) - ).toBeTruthy() - expect( - isEmptyMarkup( - jsonToMarkup({ type: MarkupNodeType.doc, content: [{ type: MarkupNodeType.paragraph, content: [] }] }) - ) - ).toBeTruthy() - }) -}) - -describe('areEqualMarkups', () => { - it('returns true for the same content', async () => { - const markup = '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello"}]}]}' - expect(areEqualMarkups(markup, markup)).toBeTruthy() - }) - it('returns true for empty content', async () => { - expect( - areEqualMarkups('{"type":"doc","content":[]}', '{"type":"doc","content":[{"type":"paragraph"}]}') - ).toBeTruthy() - expect( - areEqualMarkups( - '{"type":"doc","content":[{"type":"paragraph"}]}', - '{"type":"doc","content":[{"type":"paragraph","content":[]}]}' - ) - ).toBeTruthy() - }) - it('returns true for same content but empty marks and attrs', async () => { - expect( - areEqualMarkups( - '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello"}]}]}', - '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello","content":[],"marks":[],"attrs": {"color": null}}]}]}' - ) - ).toBeTruthy() - }) - it('returns false for same content but trailing hard breaks', async () => { - expect( - areEqualMarkups( - '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello","marks":[]}]}]}', - '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello"},{"type":"hardBreak"}]}]}' - ) - ).toBeFalsy() - expect( - areEqualMarkups( - '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello"}]}]}', - '{"type":"doc","content":[{"type":"hardBreak"},{"type":"paragraph","content":[{"type":"text","text":"hello"}]},{"type":"hardBreak"}]}' - ) - ).toBeFalsy() - }) - it('returns false for different content', async () => { - expect( - areEqualMarkups( - '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello"}]}]}', - '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"world"}]}]}' - ) - ).toBeFalsy() - }) - it('returns false for different marks', async () => { - expect( - areEqualMarkups( - '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello","marks":[{"type":"bold"}]}]}]}', - '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello","marks":[{"type":"italic"}]}]}]}' - ) - ).toBeFalsy() - }) -}) - -describe('isEmptyNode', () => { - it('returns true for empty doc node', () => { - const node: MarkupNode = { - type: MarkupNodeType.doc, - content: [] - } - expect(isEmptyNode(node)).toBeTruthy() - }) - - it('returns true for empty paragraph node', () => { - const node: MarkupNode = { - type: MarkupNodeType.doc, - content: [ - { - type: MarkupNodeType.paragraph, - content: [] - } - ] - } - expect(isEmptyNode(node)).toBeTruthy() - }) - - it('returns true for empty text node', () => { - const node: MarkupNode = { - type: MarkupNodeType.doc, - content: [ - { - type: MarkupNodeType.paragraph, - content: [ - { - type: MarkupNodeType.text, - text: '' - } - ] - } - ] - } - expect(isEmptyNode(node)).toBeTruthy() - }) - - it('returns false for non-empty text node', () => { - const node: MarkupNode = { - type: MarkupNodeType.paragraph, - content: [ - { - type: MarkupNodeType.text, - text: 'Hello, world!' - } - ] - } - expect(isEmptyNode(node)).toBeFalsy() - }) - - it('returns false for non-empty text node', () => { - const node: MarkupNode = { - type: MarkupNodeType.paragraph, - content: [ - { - type: MarkupNodeType.horizontal_rule - } - ] - } - expect(isEmptyNode(node)).toBeFalsy() - }) - - it('returns false for non-empty node', () => { - const node: MarkupNode = { - type: MarkupNodeType.paragraph, - content: [ - { - type: MarkupNodeType.text, - text: 'Hello, world!' - } - ] - } - expect(isEmptyNode(node)).toBeFalsy() - }) -}) - -describe('markupToJSON', () => { - it('with empty content', async () => { - expect(markupToJSON('')).toEqual({ type: 'doc', content: [{ type: 'paragraph', content: [] }] }) - }) - it('with some content', async () => { - const markup = '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello"}]}]}' - expect(markupToJSON(markup)).toEqual({ - type: 'doc', - content: [{ type: 'paragraph', content: [{ type: 'text', text: 'hello' }] }] - }) - }) -}) - -describe('jsonToMarkup', () => { - it('with some content', async () => { - const json: MarkupNode = { - type: MarkupNodeType.doc, - content: [ - { - type: MarkupNodeType.paragraph, - content: [ - { - type: MarkupNodeType.text, - text: 'hello' - } - ] - } - ] - } - expect(jsonToMarkup(json)).toEqual( - '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello"}]}]}' - ) - }) -}) - -describe('pmNodeToJSON', () => { - it('converts ProseMirrorNode to Markup', () => { - const schema = getSchema(extensions) - const node = schema.node('paragraph', {}, [schema.text('Hello, world!')]) - - const json: MarkupNode = { - type: MarkupNodeType.paragraph, - attrs: { textAlign: null as any }, - content: [nodeText('Hello, world!')] - } - expect(pmNodeToJSON(node)).toEqual(json) - }) -}) - -describe('jsonToPmNode', () => { - it('converts json to ProseMirrorNode', () => { - const markup = '{"type":"paragraph","content":[{"type":"text","text":"Hello, world!"}]}' - const node = jsonToPmNode(markupToJSON(markup)) - - expect(node.type.name).toEqual('paragraph') - expect(node.content.childCount).toEqual(1) - expect(node.content.child(0).type.name).toEqual('text') - expect(node.content.child(0).text).toEqual('Hello, world!') - }) -}) - -describe('htmlToMarkup', () => { - it('converts HTML to Markup', () => { - const html = '

hello

' - const expectedMarkup = '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello"}]}]}' - expect(htmlToMarkup(html)).toEqual(expectedMarkup) - }) -}) - -describe('htmlToJSON', () => { - it('converts HTML to JSON', () => { - const html = '

hello

' - const json = nodeDoc(nodeParagraph(nodeText('hello'))) - expect(htmlToJSON(html)).toEqual(json) - }) -}) - -describe('jsonToHTML', () => { - it('converts JSON to HTML', () => { - const json = nodeDoc(nodeParagraph(nodeText('hello'))) - const html = '

hello

' - expect(jsonToHTML(json)).toEqual(html) - }) -}) - -describe('jsonToText', () => { - it('returns text for text node', () => { - const node: MarkupNode = { - type: MarkupNodeType.paragraph, - content: [ - { - type: MarkupNodeType.text, - text: 'Hello, world!' - } - ] - } - expect(jsonToText(node)).toEqual('Hello, world!') - }) - it('returns concatenated text for block node with multiple children', () => { - const node: MarkupNode = { - type: MarkupNodeType.paragraph, - content: [ - { - type: MarkupNodeType.text, - text: 'Hello ' - }, - { - type: MarkupNodeType.text, - text: 'world!' - } - ] - } - expect(jsonToText(node)).toEqual('Hello world!') - }) - it('returns text for node with link', () => { - const node: MarkupNode = { - type: MarkupNodeType.paragraph, - content: [ - { - type: MarkupNodeType.text, - text: 'Hello! Check out ' - }, - { - type: MarkupNodeType.text, - text: 'this page', - marks: [ - { - type: MarkupMarkType.link, - attrs: { - href: 'http://example.com/' - } - } - ] - }, - { - type: MarkupNodeType.text, - text: '!' - } - ] - } - expect(jsonToText(node)).toEqual('Hello! Check out this page!') - }) - it('returns empty string for block node with no children', () => { - const node: MarkupNode = { - type: MarkupNodeType.paragraph, - content: [] - } - expect(jsonToText(node)).toEqual('') - }) - it('returns error for text node with no text', () => { - const node: MarkupNode = { - type: MarkupNodeType.text, - text: '' - } - expect(() => jsonToText(node)).toThrow('Empty text nodes are not allowed') - }) - it('returns error for block node with empty children', () => { - const node: MarkupNode = { - type: MarkupNodeType.paragraph, - content: [ - { - type: MarkupNodeType.text, - text: '' - }, - { - type: MarkupNodeType.text, - text: '' - } - ] - } - expect(() => jsonToText(node)).toThrow('Empty text nodes are not allowed') - }) -}) diff --git a/foundations/core/packages/text/src/markup/utils.ts b/foundations/core/packages/text/src/markup/utils.ts deleted file mode 100644 index 0746171454..0000000000 --- a/foundations/core/packages/text/src/markup/utils.ts +++ /dev/null @@ -1,80 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { Markup } from '@hcengineering/core' -import { Editor, Extensions, getSchema } from '@tiptap/core' -import { generateHTML, generateJSON } from '@tiptap/html' -import { Node as ProseMirrorNode, Schema } from '@tiptap/pm/model' - -import { MarkupNode, jsonToMarkup } from '@hcengineering/text-core' -import { defaultExtensions } from '../extensions' - -/** @public */ -const defaultSchema = getSchema(defaultExtensions) - -/** @public */ -export function getMarkup (editor?: Editor): Markup { - return jsonToMarkup(editor?.getJSON() as MarkupNode) -} - -// Markup - -/** @public */ -export function jsonToPmNode (json: MarkupNode, schema?: Schema, extensions?: Extensions): ProseMirrorNode { - schema ??= extensions == null ? defaultSchema : getSchema(extensions ?? defaultExtensions) - return ProseMirrorNode.fromJSON(schema, json) -} - -/** @public */ -export function pmNodeToJSON (node: ProseMirrorNode): MarkupNode { - return node.toJSON() -} - -/** @public */ -export function jsonToText (node: MarkupNode, schema?: Schema, extensions?: Extensions): string { - const pmNode = jsonToPmNode(node, schema, extensions) - return pmNode.textBetween(0, pmNode.content.size, '\n', '') -} - -// export function markupToText (markup: Markup, schema?: Schema, extensions?: Extensions): string { -// const pmNode = markupToPmNode(markup, schema, extensions) -// return pmNode.textBetween(0, pmNode.content.size, '\n', '') -// } - -// HTML - -/** @public */ -export function htmlToMarkup (html: string, extensions?: Extensions): Markup { - const json = htmlToJSON(html, extensions) - return jsonToMarkup(json) -} - -// /** @public */ -// export function markupToHTML (markup: Markup, extensions?: Extensions): string { -// const json = markupToJSON(markup) -// return jsonToHTML(json, extensions) -// } - -/** @public */ -export function htmlToJSON (html: string, extensions?: Extensions): MarkupNode { - extensions = extensions ?? defaultExtensions - return generateJSON(html, extensions, { preserveWhitespace: 'full' }) as MarkupNode -} - -/** @public */ -export function jsonToHTML (json: MarkupNode, extensions?: Extensions): string { - extensions = extensions ?? defaultExtensions - return generateHTML(json, extensions) -} diff --git a/foundations/core/packages/text/src/nodes/codeblock.ts b/foundations/core/packages/text/src/nodes/codeblock.ts deleted file mode 100644 index 29e686ee29..0000000000 --- a/foundations/core/packages/text/src/nodes/codeblock.ts +++ /dev/null @@ -1,88 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { textblockTypeInputRule } from '@tiptap/core' -import CodeBlock, { CodeBlockOptions } from '@tiptap/extension-code-block' - -export const codeBlockOptions: CodeBlockOptions = { - defaultLanguage: 'plaintext', - languageClassPrefix: 'language-', - exitOnArrowDown: true, - exitOnTripleEnter: true, - HTMLAttributes: { - class: 'proseCodeBlock' - } -} - -/** - * Matches a code block with backticks. - */ -export const backtickInputRegex = /^```$/ - -/** - * Matches a code block with tildes. - */ -export const tildeInputRegex = /^~~~$/ - -export const CodeBlockExtension = CodeBlock.extend({ - marks: 'inline-comment', - - addAttributes () { - return { - language: { - default: null, - parseHTML: (element) => { - const { languageClassPrefix } = this.options - let fchild = element.firstElementChild - if (fchild == null) { - for (const c of element.childNodes) { - if (c.nodeType === 1) { - // According to https://developer.mozilla.org/en-US/docs/Web/API/Node - fchild = c as Element - } - } - } - const classNames = [...Array.from(fchild?.classList ?? [])] - if (classNames.length === 0 && fchild?.className !== undefined) { - classNames.push(fchild?.className) - } - const languages = classNames - .filter((className) => className.startsWith(languageClassPrefix)) - .map((className) => className.replace(languageClassPrefix, '')) - const language = languages[0] - - if (language == null) { - return null - } - - return language - }, - rendered: false - } - } - }, - addInputRules () { - return [ - textblockTypeInputRule({ - find: backtickInputRegex, - type: this.type - }), - textblockTypeInputRule({ - find: tildeInputRegex, - type: this.type - }) - ] - } -}) diff --git a/foundations/core/packages/text/src/nodes/comment.ts b/foundations/core/packages/text/src/nodes/comment.ts deleted file mode 100644 index 9bec970fa6..0000000000 --- a/foundations/core/packages/text/src/nodes/comment.ts +++ /dev/null @@ -1,39 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { Node } from '@tiptap/core' - -/** - * @public - */ -export const CommentNode = Node.create({ - name: 'comment', - group: 'inline', - inline: true, - content: 'text*', - marks: '_', - - parseHTML () { - return [ - { - tag: 'comment' - } - ] - }, - - renderText () { - return '' - } -}) diff --git a/foundations/core/packages/text/src/nodes/embed.ts b/foundations/core/packages/text/src/nodes/embed.ts deleted file mode 100644 index 401c1e02ca..0000000000 --- a/foundations/core/packages/text/src/nodes/embed.ts +++ /dev/null @@ -1,50 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { mergeAttributes, Node } from '@tiptap/core' - -export const EmbedNode = Node.create({ - name: 'embed', - - addOptions () { - return {} - }, - - inline: false, - group: 'block', - atom: false, - draggable: false, - - addAttributes () { - return { - src: { - default: null - } - } - }, - - parseHTML () { - return [ - { - priority: 60, - tag: `figure[data-type="${this.name}"] iframe[src]` - } - ] - }, - - renderHTML ({ HTMLAttributes }) { - return ['figure', { 'data-type': this.name }, ['iframe', mergeAttributes(HTMLAttributes)]] - } -}) diff --git a/foundations/core/packages/text/src/nodes/emoji.ts b/foundations/core/packages/text/src/nodes/emoji.ts deleted file mode 100644 index 96fb909ae7..0000000000 --- a/foundations/core/packages/text/src/nodes/emoji.ts +++ /dev/null @@ -1,143 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { Node, mergeAttributes } from '@tiptap/core' -import type { Blob, Ref } from '@hcengineering/core' - -declare module '@tiptap/core' { - interface Commands { - emoji: { - insertEmoji: (emoji: string, kind: 'unicode' | 'image', image?: Ref) => ReturnType - } - } -} - -export interface EmojiNodeOptions { - getBlobRef: (fileId: Ref, filename?: string, size?: number) => Promise<{ src: string, srcset: string }> -} - -export const EmojiNode = Node.create({ - name: 'emoji', - group: 'inline', - inline: true, - atom: true, - selectable: false, - - addAttributes () { - return { - emoji: { - default: '' - }, - kind: { - default: 'unicode' - }, - image: { - default: null - } - } - }, - - addCommands () { - return { - insertEmoji: - (emoji: string, kind: 'unicode' | 'image', image?: Ref) => - ({ commands }) => { - if (kind === 'image') emoji = `:${emoji}:` - return commands.insertContent({ - type: this.name, - attrs: { emoji, kind, image } - }) - } - } - }, - - parseHTML () { - return [ - { - tag: `span[data-type="${this.name}"]` - } - ] - }, - - addNodeView () { - return ({ node, HTMLAttributes }) => { - const container = document.createElement('span') - const containerAttributes = mergeAttributes( - { - 'data-type': this.name, - class: 'emoji' - }, - HTMLAttributes - ) - - for (const [k, v] of Object.entries(containerAttributes)) { - if (v !== null) { - container.setAttribute(k, v) - } - } - - if (node.attrs.kind === 'image') { - const imgElement = document.createElement('img') - imgElement.alt = node.attrs.emoji - imgElement.setAttribute('data-type', this.name) - void this.options.getBlobRef(node.attrs.image).then((val) => { - imgElement.src = val.src - imgElement.srcset = val.srcset - }) - container.append(imgElement) - } else { - container.append(node.attrs.emoji) - } - - return { - dom: container - } - } - }, - - renderHTML ({ node, HTMLAttributes }) { - if (node.attrs.kind === 'image') { - return [ - 'span', - mergeAttributes( - { - 'data-type': this.name, - class: 'emoji' - }, - HTMLAttributes - ), - [ - 'img', - mergeAttributes({ - 'data-type': this.name, - src: node.attrs.image, - alt: node.attrs.emoji - }) - ] - ] - } - return [ - 'span', - mergeAttributes( - { - 'data-type': this.name, - class: 'emoji' - }, - HTMLAttributes - ), - node.attrs.emoji - ] - } -}) diff --git a/foundations/core/packages/text/src/nodes/file.ts b/foundations/core/packages/text/src/nodes/file.ts deleted file mode 100644 index 8f88dd3305..0000000000 --- a/foundations/core/packages/text/src/nodes/file.ts +++ /dev/null @@ -1,102 +0,0 @@ -// -// Copyright © 2023 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// -import { Node } from '@tiptap/core' - -/** - * @public - */ -export interface FileOptions { - inline: boolean - HTMLAttributes: Record -} - -/** - * @public - */ -export const FileNode = Node.create({ - name: 'file', - - addOptions () { - return { - inline: true, - HTMLAttributes: {} - } - }, - - inline () { - return this.options.inline - }, - - group () { - return this.options.inline ? 'inline' : 'block' - }, - - draggable: true, - - selectable: true, - - addAttributes () { - return { - 'file-id': { - default: null - }, - 'data-file-name': { - default: null - }, - 'data-file-size': { - default: null - }, - 'data-file-type': { - default: null - }, - 'data-file-href': { - default: null - } - } - }, - - parseHTML () { - return [ - { - tag: `div[data-type="${this.name}"]` - } - ] - }, - - renderHTML ({ node, HTMLAttributes }) { - const nodeAttributes = { - 'data-type': this.name - } - - const fileName = HTMLAttributes['data-file-name'] - const size = HTMLAttributes['data-file-size'] - const fileType = HTMLAttributes['data-file-type'] - const href = HTMLAttributes['data-file-href'] - const linkAttributes = { - class: 'file-name', - href, - type: fileType, - download: fileName, - target: '_blank' - } - - return [ - 'div', - nodeAttributes, - ['div', {}, ['a', linkAttributes, `${fileName} (${fileType})`]], - ['div', {}, `${size}`] - ] - } -}) diff --git a/foundations/core/packages/text/src/nodes/image.ts b/foundations/core/packages/text/src/nodes/image.ts deleted file mode 100644 index ae29f14042..0000000000 --- a/foundations/core/packages/text/src/nodes/image.ts +++ /dev/null @@ -1,147 +0,0 @@ -// -// Copyright © 2023 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// -import type { Blob, Ref } from '@hcengineering/core' -import { Node, mergeAttributes } from '@tiptap/core' -import { getDataAttribute } from './utils' - -/** - * @public - */ -export type ImageAlignment = 'center' | 'left' | 'right' - -export interface ImageAlignmentOptions { - align?: ImageAlignment -} - -export interface ImageSizeOptions { - height?: number | string - width?: number | string -} - -declare module '@tiptap/core' { - export interface Commands { - image: { - /** - * Add an image - */ - setImage: (options: { src: string, alt?: string, title?: string }) => ReturnType - /** - * Set image alignment - */ - setImageAlignment: (options: ImageAlignmentOptions) => ReturnType - /** - * Set image size - */ - setImageSize: (options: ImageSizeOptions) => ReturnType - } - } -} - -/** - * @public - */ -export interface ImageOptions { - inline: boolean - HTMLAttributes: Record - - loadingImgSrc?: string - getBlobRef: (fileId: Ref, filename?: string, size?: number) => Promise<{ src: string, srcset: string }> -} - -/** - * @public - */ -export const ImageNode = Node.create({ - name: 'image', - - addOptions () { - return { - inline: true, - HTMLAttributes: {}, - getBlobRef: async () => ({ src: '', srcset: '' }) - } - }, - - inline () { - return this.options.inline - }, - - group () { - return this.options.inline ? 'inline' : 'block' - }, - - draggable: true, - - selectable: true, - - addAttributes () { - return { - 'file-id': { - default: null - }, - width: { - default: null - }, - height: { - default: null - }, - src: { - default: null - }, - alt: { - default: null - }, - title: { - default: null - }, - align: getDataAttribute('align'), - 'data-file-type': { - default: null - } - } - }, - - parseHTML () { - return [ - { - tag: `img[data-type="${this.name}"]` - }, - { - tag: 'img[src]' - } - ] - }, - - renderHTML ({ node, HTMLAttributes }) { - const divAttributes = { - class: 'text-editor-image-container', - 'data-type': this.name, - 'data-align': node.attrs.align - } - const imgAttributes = mergeAttributes( - { - 'data-type': this.name - }, - this.options.HTMLAttributes, - HTMLAttributes - ) - const fileId = imgAttributes['file-id'] - if (fileId != null) { - imgAttributes.src = `platform://platform/files/workspace/?file=${fileId}` - } - - return ['div', divAttributes, ['img', imgAttributes]] - } -}) diff --git a/foundations/core/packages/text/src/nodes/index.ts b/foundations/core/packages/text/src/nodes/index.ts deleted file mode 100644 index 11bd4eb863..0000000000 --- a/foundations/core/packages/text/src/nodes/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright © 2023 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export * from './image' -export * from './reference' -export * from './emoji' -export * from './todo' -export * from './file' -export * from './codeblock' -export * from './comment' -export * from './markdown' -export * from './embed' -export { getDataAttribute } from './utils' diff --git a/foundations/core/packages/text/src/nodes/markdown.ts b/foundations/core/packages/text/src/nodes/markdown.ts deleted file mode 100644 index dd3e2f2194..0000000000 --- a/foundations/core/packages/text/src/nodes/markdown.ts +++ /dev/null @@ -1,42 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { mergeAttributes, Node } from '@tiptap/core' - -export const MarkdownNode = Node.create({ - name: 'markdown', - group: 'block', - content: 'text*', - marks: '', - code: true, - defining: true, - - parseHTML () { - return [ - { - tag: 'pre[data-type="markdown"]', - preserveWhitespace: 'full' - } - ] - }, - - renderHTML ({ node, HTMLAttributes }) { - return [ - 'pre', - mergeAttributes({ 'data-type': this.name }, this.options.HTMLAttributes, HTMLAttributes), - ['code', {}, 0] - ] - } -}) diff --git a/foundations/core/packages/text/src/nodes/mermaid.ts b/foundations/core/packages/text/src/nodes/mermaid.ts deleted file mode 100644 index 4290c60864..0000000000 --- a/foundations/core/packages/text/src/nodes/mermaid.ts +++ /dev/null @@ -1,45 +0,0 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import CodeBlock, { CodeBlockOptions } from '@tiptap/extension-code-block' -import { codeBlockOptions } from './codeblock' - -export const mermaidOptions: CodeBlockOptions = { - ...codeBlockOptions, - defaultLanguage: 'mermaid' -} - -export const MermaidExtension = CodeBlock.extend({ - name: 'mermaid', - group: 'block', - marks: 'inline-comment', - - parseHTML () { - return [ - { - tag: 'div.mermaid-diagram', - preserveWhitespace: 'full' - } - ] - }, - - addAttributes () { - return { - language: { - default: 'mermaid' - } - } - } -}) diff --git a/foundations/core/packages/text/src/nodes/reference.ts b/foundations/core/packages/text/src/nodes/reference.ts deleted file mode 100644 index 613a51eca1..0000000000 --- a/foundations/core/packages/text/src/nodes/reference.ts +++ /dev/null @@ -1,104 +0,0 @@ -// -// Copyright © 2023 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { Node, mergeAttributes } from '@tiptap/core' -import { getDataAttribute } from './utils' -import { Class, Doc, Ref } from '@hcengineering/core' -import { Attrs } from '@tiptap/pm/model' - -export interface ReferenceNodeProps { - id: Ref - objectclass: Ref> - label: string -} - -export interface ReferenceOptions { - suggestion: { char?: string } - HTMLAttributes: Record -} - -/** - * @public - */ -export const ReferenceNode = Node.create({ - name: 'reference', - group: 'inline', - inline: true, - selectable: true, - - addAttributes () { - return { - id: getDataAttribute('id'), - objectclass: getDataAttribute('objectclass'), - label: getDataAttribute('label') - } - }, - - addOptions () { - return { - suggestion: { char: '@' }, - HTMLAttributes: {} - } - }, - - parseHTML () { - return [ - { - priority: 60, - tag: 'span[data-type="reference"]', - getAttrs - }, - { - priority: 60, - tag: 'a[data-type="reference"]', - getAttrs - } - ] - }, - - renderHTML ({ node, HTMLAttributes }) { - return [ - 'span', - mergeAttributes( - { - 'data-type': this.name, - 'data-id': node.attrs.id, - 'data-objectclass': node.attrs.objectclass, - 'data-label': node.attrs.label, - class: 'antiMention' - }, - this.options.HTMLAttributes, - HTMLAttributes - ), - `${this.options.suggestion.char}${node.attrs.label ?? node.attrs.id}` - ] - } -}) - -function getAttrs (el: HTMLSpanElement): Attrs | false { - const id = el.dataset.id?.trim() - const label = el.dataset.label?.trim() - const objectclass = el.dataset.objectclass?.trim() - - if (id == null || label == null || objectclass == null) { - return false - } - - return { - id, - label, - objectclass - } -} diff --git a/foundations/core/packages/text/src/nodes/todo.ts b/foundations/core/packages/text/src/nodes/todo.ts deleted file mode 100644 index f7d28ba147..0000000000 --- a/foundations/core/packages/text/src/nodes/todo.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { TaskItem } from '@tiptap/extension-task-item' -import { TaskList } from '@tiptap/extension-task-list' - -import { getDataAttribute } from './utils' - -export const TodoItemNode = TaskItem.extend({ - name: 'todoItem', - group: 'listItems', - - addOptions () { - return { - nested: true, - HTMLAttributes: {}, - taskListTypeName: 'todoList' - } - }, - - addAttributes () { - return { - ...this.parent?.(), - todoid: getDataAttribute('todoid', { default: null, keepOnSplit: false }), - userid: getDataAttribute('userid', { default: null, keepOnSplit: false }) - } - } -}) - -export const TodoListNode = TaskList.extend({ - name: 'todoList', - - addOptions () { - return { - itemTypeName: 'todoItem', - HTMLAttributes: {} - } - } -}) diff --git a/foundations/core/packages/text/src/nodes/utils.ts b/foundations/core/packages/text/src/nodes/utils.ts deleted file mode 100644 index 0e051f1520..0000000000 --- a/foundations/core/packages/text/src/nodes/utils.ts +++ /dev/null @@ -1,41 +0,0 @@ -// -// Copyright © 2023 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { Attribute } from '@tiptap/core' - -/** - * @public - */ -export function getDataAttribute ( - name: string, - options?: Partial> -): Partial { - const dataName = `data-${name}` - - return { - default: null, - parseHTML: (element) => element.getAttribute(dataName), - renderHTML: (attributes) => { - if (attributes[name] == null) { - return null - } - - return { - [dataName]: attributes[name] - } - }, - ...(options ?? {}) - } -} diff --git a/foundations/core/packages/text/src/tiptapExtensions.ts b/foundations/core/packages/text/src/tiptapExtensions.ts deleted file mode 100644 index ff889ad401..0000000000 --- a/foundations/core/packages/text/src/tiptapExtensions.ts +++ /dev/null @@ -1,43 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export { TextStyle, type TextStyleOptions } from '@tiptap/extension-text-style' -export { Blockquote, type BlockquoteOptions } from '@tiptap/extension-blockquote' -export { Bold, type BoldOptions } from '@tiptap/extension-bold' -export { Document } from '@tiptap/extension-document' -export { Dropcursor, type DropcursorOptions } from '@tiptap/extension-dropcursor' -export { Gapcursor } from '@tiptap/extension-gapcursor' -export { HardBreak, type HardBreakOptions } from '@tiptap/extension-hard-break' -export { Heading, type HeadingOptions } from '@tiptap/extension-heading' -export { History, type HistoryOptions } from '@tiptap/extension-history' -export { HorizontalRule, type HorizontalRuleOptions } from '@tiptap/extension-horizontal-rule' -export { Italic, type ItalicOptions } from '@tiptap/extension-italic' -export { Paragraph, type ParagraphOptions } from '@tiptap/extension-paragraph' -export { Strike, type StrikeOptions } from '@tiptap/extension-strike' -export { Text } from '@tiptap/extension-text' -export { Link, type LinkOptions } from '@tiptap/extension-link' -export { Typography, type TypographyOptions } from '@tiptap/extension-typography' -export { Underline, type UnderlineOptions } from '@tiptap/extension-underline' -export { BulletList, type BulletListOptions } from '@tiptap/extension-bullet-list' -export { ListItem, type ListItemOptions } from '@tiptap/extension-list-item' -export { OrderedList, type OrderedListOptions } from '@tiptap/extension-ordered-list' -export { TextAlign, type TextAlignOptions } from '@tiptap/extension-text-align' -export { TaskList, type TaskListOptions } from '@tiptap/extension-task-list' - -export { Table, type TableOptions } from '@tiptap/extension-table' -export { TableCell, type TableCellOptions } from '@tiptap/extension-table-cell' -export { TableHeader, type TableHeaderOptions } from '@tiptap/extension-table-header' -export { TableRow, type TableRowOptions } from '@tiptap/extension-table-row' -export { TaskItem, type TaskItemOptions } from '@tiptap/extension-task-item' diff --git a/foundations/core/packages/text/tsconfig.json b/foundations/core/packages/text/tsconfig.json deleted file mode 100644 index b5ae22f6e4..0000000000 --- a/foundations/core/packages/text/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/foundations/core/packages/token/.eslintrc.js b/foundations/core/packages/token/.eslintrc.js deleted file mode 100644 index 72235dc283..0000000000 --- a/foundations/core/packages/token/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json' - } -} diff --git a/foundations/core/packages/token/.npmignore b/foundations/core/packages/token/.npmignore deleted file mode 100644 index e3ec093c38..0000000000 --- a/foundations/core/packages/token/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!/lib/** -!CHANGELOG.md -/lib/**/__tests__/ diff --git a/foundations/core/packages/token/CHANGELOG.json b/foundations/core/packages/token/CHANGELOG.json deleted file mode 100644 index a4d312f15d..0000000000 --- a/foundations/core/packages/token/CHANGELOG.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "name": "@hcengineering/server-token", - "entries": [ - { - "version": "0.7.17", - "tag": "@hcengineering/server-token_v0.7.17", - "date": "Mon, 27 Oct 2025 13:27:12 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`" - } - ] - } - }, - { - "version": "0.7.5", - "tag": "@hcengineering/server-token_v0.7.5", - "date": "Tue, 14 Oct 2025 04:58:17 GMT", - "comments": { - "patch": [ - { - "comment": "update deps" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`" - }, - { - "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.4` to `0.7.5`" - } - ] - } - }, - { - "version": "0.7.4", - "tag": "@hcengineering/server-token_v0.7.4", - "date": "Sat, 11 Oct 2025 18:20:33 GMT", - "comments": { - "patch": [ - { - "comment": "Update to latest platform rig" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`" - }, - { - "comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.3` to `0.7.4`" - } - ] - } - }, - { - "version": "0.7.3", - "tag": "@hcengineering/server-token_v0.7.3", - "date": "Wed, 08 Oct 2025 03:40:53 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`" - } - ] - } - } - ] -} diff --git a/foundations/core/packages/token/CHANGELOG.md b/foundations/core/packages/token/CHANGELOG.md deleted file mode 100644 index 39e2bc934e..0000000000 --- a/foundations/core/packages/token/CHANGELOG.md +++ /dev/null @@ -1,28 +0,0 @@ -# Change Log - @hcengineering/server-token - -This log was last generated on Mon, 27 Oct 2025 13:27:12 GMT and should not be manually modified. - -## 0.7.17 -Mon, 27 Oct 2025 13:27:12 GMT - -_Version update only_ - -## 0.7.5 -Tue, 14 Oct 2025 04:58:17 GMT - -### Patches - -- update deps - -## 0.7.4 -Sat, 11 Oct 2025 18:20:33 GMT - -### Patches - -- Update to latest platform rig - -## 0.7.3 -Wed, 08 Oct 2025 03:40:53 GMT - -_Initial release_ - diff --git a/foundations/core/packages/token/config/rig.json b/foundations/core/packages/token/config/rig.json deleted file mode 100644 index 0110930f55..0000000000 --- a/foundations/core/packages/token/config/rig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@hcengineering/platform-rig" -} diff --git a/foundations/core/packages/token/jest.config.js b/foundations/core/packages/token/jest.config.js deleted file mode 100644 index 069ea8aa0d..0000000000 --- a/foundations/core/packages/token/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - roots: ['./src'], - coverageReporters: ['text-summary', 'html', 'lcov'] -} diff --git a/foundations/core/packages/token/package.json b/foundations/core/packages/token/package.json deleted file mode 100644 index f5683085a0..0000000000 --- a/foundations/core/packages/token/package.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "name": "@hcengineering/server-token", - "version": "0.7.17", - "main": "lib/index.js", - "svelte": "src/index.ts", - "types": "types/index.d.ts", - "files": [ - "lib/**/*", - "types/**/*", - "src/**/*", - "!src/**/__test__/**", - "tsconfig.json" - ], - "author": "Anticrm Platform Contributors", - "license": "EPL-2.0", - "scripts": { - "build": "compile", - "build:watch": "compile", - "format": "format src", - "test": "jest --passWithNoTests --silent --coverage", - "_phase:build": "compile transpile src", - "_phase:test": "jest --passWithNoTests --silent --coverage", - "_phase:format": "format src", - "_phase:validate": "compile validate" - }, - "devDependencies": { - "@hcengineering/platform-rig": "workspace:^0.7.19", - "@types/node": "^22.18.1", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-n": "^15.4.0", - "eslint": "^8.54.0", - "@typescript-eslint/parser": "^6.21.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "prettier": "^3.6.2", - "typescript": "^5.9.3", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "@types/jest": "^29.5.5", - "@types/uuid": "^8.3.1", - "eslint-plugin-svelte": "^2.35.1" - }, - "dependencies": { - "@hcengineering/core": "workspace:^0.7.22", - "@hcengineering/platform": "workspace:^0.7.18", - "jwt-simple": "^0.5.6", - "uuid": "^8.3.2" - }, - "repository": "https://github.com/hcengineering/huly.core", - "publishConfig": { - "access": "public" - }, - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.js" - } - } -} diff --git a/foundations/core/packages/token/src/__tests__/token.test.ts b/foundations/core/packages/token/src/__tests__/token.test.ts deleted file mode 100644 index fc54e62991..0000000000 --- a/foundations/core/packages/token/src/__tests__/token.test.ts +++ /dev/null @@ -1,116 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { setMetadata } from '@hcengineering/platform' -import type { PersonUuid, WorkspaceUuid } from '@hcengineering/core' -import { decodeToken, generateToken } from '../token' -import plugin from '../plugin' - -export function decodeTokenPayload (token: string): any { - try { - return JSON.parse(atob(token.split('.')[1])) - } catch (err: any) { - console.error(err) - return {} - } -} - -describe('generateToken', () => { - beforeEach(() => { - setMetadata(plugin.metadata.Secret, undefined) - setMetadata(plugin.metadata.Service, undefined) - }) - - it('throws TokenError for invalid account uuid', () => { - expect(() => { - generateToken('invalid-uuid' as PersonUuid, '' as WorkspaceUuid, {}, 'secret') - }).toThrow('Invalid account uuid: "invalid-uuid"') - }) - - it('throws TokenError for invalid workspace uuid', () => { - expect(() => { - generateToken('123e4567-e89b-12d3-a456-426614174000' as PersonUuid, 'invalid-uuid' as WorkspaceUuid, {}, 'secret') - }).toThrow('Invalid workspace uuid: "invalid-uuid"') - }) - - it('generates token without extra and workspace', () => { - const token = generateToken('123e4567-e89b-12d3-a456-426614174000' as PersonUuid, undefined, undefined, 'secret') - const decodedPayload = decodeTokenPayload(token) - expect(decodedPayload).toEqual({ - account: '123e4567-e89b-12d3-a456-426614174000', - workspace: undefined - }) - }) - - it('should generate token with only required fields', () => { - const token = generateToken( - '123e4567-e89b-12d3-a456-426614174000' as PersonUuid, - '123e4567-e89b-12d3-a456-426614174001' as WorkspaceUuid, - undefined, - 'secret' - ) - const decodedPayload = decodeTokenPayload(token) - expect(decodedPayload).toEqual({ - account: '123e4567-e89b-12d3-a456-426614174000', - workspace: '123e4567-e89b-12d3-a456-426614174001' - }) - }) - - it('should generate token with extra fields', () => { - const extra = { service: 'test' } - const token = generateToken( - '123e4567-e89b-12d3-a456-426614174000' as PersonUuid, - '123e4567-e89b-12d3-a456-426614174001' as WorkspaceUuid, - extra, - 'secret' - ) - const decodedPayload = decodeTokenPayload(token) - expect(decodedPayload).toEqual({ - extra, - account: '123e4567-e89b-12d3-a456-426614174000', - workspace: '123e4567-e89b-12d3-a456-426614174001' - }) - }) - - it('should generate token with default secret', () => { - const token = generateToken( - '123e4567-e89b-12d3-a456-426614174000' as PersonUuid, - '123e4567-e89b-12d3-a456-426614174001' as WorkspaceUuid, - undefined, - 'test' - ) - const decodedPayload = decodeTokenPayload(token) - expect(decodedPayload).toEqual({ - account: '123e4567-e89b-12d3-a456-426614174000', - workspace: '123e4567-e89b-12d3-a456-426614174001' - }) - }) - - it('should generate token with default service in extra', () => { - setMetadata(plugin.metadata.Service, 'test') - const token = generateToken( - '123e4567-e89b-12d3-a456-426614174000' as PersonUuid, - '123e4567-e89b-12d3-a456-426614174001' as WorkspaceUuid, - undefined, - 'secret' - ) - const decodedPayload = decodeToken(token, false, 'test') - expect(decodedPayload).toEqual({ - extra: { service: 'test' }, - account: '123e4567-e89b-12d3-a456-426614174000', - workspace: '123e4567-e89b-12d3-a456-426614174001' - }) - }) -}) diff --git a/foundations/core/packages/token/src/index.ts b/foundations/core/packages/token/src/index.ts deleted file mode 100644 index dd6f940800..0000000000 --- a/foundations/core/packages/token/src/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -// -// Copyright © 2020, 2021 Anticrm Platform Contributors. -// Copyright © 2021 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -export { default } from './plugin' -export * from './token' diff --git a/foundations/core/packages/token/src/plugin.ts b/foundations/core/packages/token/src/plugin.ts deleted file mode 100644 index 67a8f57976..0000000000 --- a/foundations/core/packages/token/src/plugin.ts +++ /dev/null @@ -1,34 +0,0 @@ -// -// Copyright © 2022 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import type { Metadata, Plugin } from '@hcengineering/platform' -import { plugin } from '@hcengineering/platform' - -/** - * @public - */ -export const serverTokenId = 'server-token' as Plugin - -/** - * @public - */ -const serverToken = plugin(serverTokenId, { - metadata: { - Secret: '' as Metadata, - Service: '' as Metadata - } -}) - -export default serverToken diff --git a/foundations/core/packages/token/src/token.ts b/foundations/core/packages/token/src/token.ts deleted file mode 100644 index 7a6ae0eabf..0000000000 --- a/foundations/core/packages/token/src/token.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { AccountRole, AccountUuid, MeasureContext, PersonUuid, WorkspaceUuid } from '@hcengineering/core' -import { getMetadata } from '@hcengineering/platform' -import { decode, encode } from 'jwt-simple' -import { validate } from 'uuid' -import serverPlugin from './plugin' - -/** - * @public - */ -export interface Token { - account: AccountUuid - workspace: WorkspaceUuid - extra?: Record - grant?: PermissionsGrant - - sub?: AccountUuid // Subject - exp?: number // Expiration, seconds since epoch - nbf?: number // Not valid before, seconds since epoch -} - -// Permissions grant provides the token presenter access to a specific workspace -export interface PermissionsGrant { - workspace: WorkspaceUuid - role: AccountRole - - // Ideally we shouldn't need this but for now it's the only way to check - // if some granted permissions are valid - the ones which can only be verified in the workspace - grantedBy?: AccountUuid - - firstName?: string - lastName?: string - - spaces?: string[] - - extra?: Record -} - -/** - * @public - */ -export class TokenError extends Error { - constructor (message: string) { - super(message) - this.name = 'TokenError' - } -} - -const getSecret = (): string => { - return getMetadata(serverPlugin.metadata.Secret) ?? 'secret' -} - -/** - * @public - */ -export function generateToken ( - accountUuid: PersonUuid, - workspaceUuid?: WorkspaceUuid, - extra?: Record, - secret?: string, - options?: { - grant?: PermissionsGrant - nbf?: number - exp?: number - sub?: PersonUuid - } -): string { - if (!validate(accountUuid)) { - throw new TokenError(`Invalid account uuid: "${accountUuid}"`) - } - if (workspaceUuid !== undefined && !validate(workspaceUuid)) { - throw new TokenError(`Invalid workspace uuid: "${workspaceUuid}"`) - } - const { grant, nbf, exp, sub } = options ?? {} - if (grant?.workspace !== undefined && !validate(grant?.workspace)) { - throw new TokenError(`Invalid grant workspace uuid: "${grant?.workspace}"`) - } - - if (grant != null && sub == null && (nbf == null || exp == null)) { - throw new TokenError('nbf and exp are required when sub is not provided') - } - - const service = getMetadata(serverPlugin.metadata.Service) - if (service !== undefined) { - extra = { service, ...extra } - } - - const sanitizedGrant: PermissionsGrant | undefined = - grant !== undefined - ? { - workspace: grant.workspace, - role: grant.role, - grantedBy: grant.grantedBy, - firstName: grant.firstName, - lastName: grant.lastName, - spaces: grant.spaces, - extra: grant.extra - } - : undefined - - return encode( - { - ...(extra !== undefined ? { extra } : {}), - account: accountUuid, - workspace: workspaceUuid, - grant: sanitizedGrant, - sub, - exp, - nbf - }, - secret ?? getSecret() - ) -} - -/** - * @public - */ -export function decodeToken (token: string, verify: boolean = true, secret?: string): Token { - try { - return decode(token, secret ?? getSecret(), !verify) - } catch (err: any) { - throw new TokenError(err.message) - } -} - -/** - * @public - */ -export function decodeTokenVerbose (ctx: MeasureContext, token: string): Token { - try { - return decodeToken(token) - } catch (err: any) { - try { - const decode = decodeToken(token, false) - ctx.warn('Failed to verify token', { ...decode }) - } catch (err2: any) { - // Nothing to do - } - throw new TokenError(err.message) - } -} diff --git a/foundations/core/packages/token/tsconfig.json b/foundations/core/packages/token/tsconfig.json deleted file mode 100644 index b5ae22f6e4..0000000000 --- a/foundations/core/packages/token/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", - - "compilerOptions": { - "rootDir": "./src", - "outDir": "./lib", - "declarationDir": "./types", - "tsBuildInfoFile": ".build/build.tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file diff --git a/foundations/core/rush.json b/foundations/core/rush.json deleted file mode 100644 index a357322de8..0000000000 --- a/foundations/core/rush.json +++ /dev/null @@ -1,469 +0,0 @@ -/** - * This is the main configuration file for Rush. - * For full documentation, please see https://rushjs.io - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush.schema.json", - - /** - * (Required) This specifies the version of the Rush engine to be used in this repo. - * Rush's "version selector" feature ensures that the globally installed tool will - * behave like this release, regardless of which version is installed globally. - * - * The common/scripts/install-run-rush.js automation script also uses this version. - * - * NOTE: If you upgrade to a new major version of Rush, you should replace the "v5" - * path segment in the "$schema" field for all your Rush config files. This will ensure - * correct error-underlining and tab-completion for editors such as VS Code. - */ - "rushVersion": "5.158.1", - - /** - * The next field selects which package manager should be installed and determines its version. - * Rush installs its own local copy of the package manager to ensure that your build process - * is fully isolated from whatever tools are present in the local environment. - * - * Specify one of: "pnpmVersion", "npmVersion", or "yarnVersion". See the Rush documentation - * for details about these alternatives. - */ - "pnpmVersion": "10.15.1", - - // "npmVersion": "6.14.15", - // "yarnVersion": "1.9.4", - - /** - * Older releases of the Node.js engine may be missing features required by your system. - * Other releases may have bugs. In particular, the "latest" version will not be a - * Long Term Support (LTS) version and is likely to have regressions. - * - * Specify a SemVer range to ensure developers use a Node.js version that is appropriate - * for your repo. - * - * LTS schedule: https://nodejs.org/en/about/releases/ - * LTS versions: https://nodejs.org/en/download/releases/ - */ - "nodeSupportedVersionRange": ">=18.20.3 <19.0.0 || >=20.14.0 <25.0.0", - - /** - * If the version check above fails, Rush will display a message showing the current - * node version and the supported version range. You can use this setting to provide - * additional instructions that will display below the warning, if there's a specific - * tool or script you'd like the user to use to get in line with the expected version. - */ - // "nodeSupportedVersionInstructions": "Run 'nvs use' to switch to the expected node version.", - - /** - * Odd-numbered major versions of Node.js are experimental. Even-numbered releases - * spend six months in a stabilization period before the first Long Term Support (LTS) version. - * For example, 8.9.0 was the first LTS version of Node.js 8. Pre-LTS versions are not recommended - * for production usage because they frequently have bugs. They may cause Rush itself - * to malfunction. - * - * Rush normally prints a warning if it detects a pre-LTS Node.js version. If you are testing - * pre-LTS versions in preparation for supporting the first LTS version, you can use this setting - * to disable Rush's warning. - */ - // "suppressNodeLtsWarning": false, - - /** - * Rush normally prints a warning if it detects that the current version is not one published to the - * public npmjs.org registry. If you need to block calls to the npm registry, you can use this setting to disable - * Rush's check. - */ - // "suppressRushIsPublicVersionCheck": false, - - /** - * Large monorepos can become intimidating for newcomers if project folder paths don't follow - * a consistent and recognizable pattern. When the system allows nested folder trees, - * we've found that teams will often use subfolders to create islands that isolate - * their work from others ("shipping the org"). This hinders collaboration and code sharing. - * - * The Rush developers recommend a "category folder" model, where buildable project folders - * must always be exactly two levels below the repo root. The parent folder acts as the category. - * This provides a basic facility for grouping related projects (e.g. "apps", "libraries", - * "tools", "prototypes") while still encouraging teams to organize their projects into - * a unified taxonomy. Limiting to 2 levels seems very restrictive at first, but if you have - * 20 categories and 20 projects in each category, this scheme can easily accommodate hundreds - * of projects. In practice, you will find that the folder hierarchy needs to be rebalanced - * occasionally, but if that's painful, it's a warning sign that your development style may - * discourage refactoring. Reorganizing the categories should be an enlightening discussion - * that brings people together, and maybe also identifies poor coding practices (e.g. file - * references that reach into other project's folders without using Node.js module resolution). - * - * The defaults are projectFolderMinDepth=1 and projectFolderMaxDepth=2. - * - * To remove these restrictions, you could set projectFolderMinDepth=1 - * and set projectFolderMaxDepth to a large number. - */ - // "projectFolderMinDepth": 2, - // "projectFolderMaxDepth": 2, - - /** - * Today the npmjs.com registry enforces fairly strict naming rules for packages, but in the early - * days there was no standard and hardly any enforcement. A few large legacy projects are still using - * nonstandard package names, and private registries sometimes allow it. Set "allowMostlyStandardPackageNames" - * to true to relax Rush's enforcement of package names. This allows upper case letters and in the future may - * relax other rules, however we want to minimize these exceptions. Many popular tools use certain punctuation - * characters as delimiters, based on the assumption that they will never appear in a package name; thus if we relax - * the rules too much it is likely to cause very confusing malfunctions. - * - * The default value is false. - */ - // "allowMostlyStandardPackageNames": true, - - /** - * This feature helps you to review and approve new packages before they are introduced - * to your monorepo. For example, you may be concerned about licensing, code quality, - * performance, or simply accumulating too many libraries with overlapping functionality. - * The approvals are tracked in two config files "browser-approved-packages.json" - * and "nonbrowser-approved-packages.json". See the Rush documentation for details. - */ - // "approvedPackagesPolicy": { - // /** - // * The review categories allow you to say for example "This library is approved for usage - // * in prototypes, but not in production code." - // * - // * Each project can be associated with one review category, by assigning the "reviewCategory" field - // * in the "projects" section of rush.json. The approval is then recorded in the files - // * "common/config/rush/browser-approved-packages.json" and "nonbrowser-approved-packages.json" - // * which are automatically generated during "rush update". - // * - // * Designate categories with whatever granularity is appropriate for your review process, - // * or you could just have a single category called "default". - // */ - // "reviewCategories": [ - // // Some example categories: - // "production", // projects that ship to production - // "tools", // non-shipping projects that are part of the developer toolchain - // "prototypes" // experiments that should mostly be ignored by the review process - // ], - // - // /** - // * A list of NPM package scopes that will be excluded from review. - // * We recommend to exclude TypeScript typings (the "@types" scope), because - // * if the underlying package was already approved, this would imply that the typings - // * are also approved. - // */ - // // "ignoredNpmScopes": ["@types"] - // }, - - /** - * If you use Git as your version control system, this section has some additional - * optional features you can use. - */ - "gitPolicy": { - /** - * Work at a big company? Tired of finding Git commits at work with unprofessional Git - * emails such as "beer-lover@my-college.edu"? Rush can validate people's Git email address - * before they get started. - * - * Define a list of regular expressions describing allowable e-mail patterns for Git commits. - * They are case-insensitive anchored JavaScript RegExps. Example: ".*@example\.com" - * - * IMPORTANT: Because these are regular expressions encoded as JSON string literals, - * RegExp escapes need two backslashes, and ordinary periods should be "\\.". - */ - // "allowedEmailRegExps": [ - // "[^@]+@users\\.noreply\\.github\\.com", - // "rush-bot@example\\.org" - // ], - /** - * When Rush reports that the address is malformed, the notice can include an example - * of a recommended email. Make sure it conforms to one of the allowedEmailRegExps - * expressions. - */ - // "sampleEmail": "example@users.noreply.github.com", - /** - * The commit message to use when committing changes during 'rush publish'. - * - * For example, if you want to prevent these commits from triggering a CI build, - * you might configure your system's trigger to look for a special string such as "[skip-ci]" - * in the commit message, and then customize Rush's message to contain that string. - */ - // "versionBumpCommitMessage": "Bump versions [skip ci]", - /** - * The commit message to use when committing changes during 'rush version'. - * - * For example, if you want to prevent these commits from triggering a CI build, - * you might configure your system's trigger to look for a special string such as "[skip-ci]" - * in the commit message, and then customize Rush's message to contain that string. - */ - // "changeLogUpdateCommitMessage": "Update changelogs [skip ci]", - /** - * The commit message to use when committing changefiles during 'rush change --commit' - * - * If no commit message is set it will default to 'Rush change' - */ - // "changefilesCommitMessage": "Rush change" - }, - - "repository": { - /** - * The URL of this Git repository, used by "rush change" to determine the base branch for your PR. - * - * The "rush change" command needs to determine which files are affected by your PR diff. - * If you merged or cherry-picked commits from the main branch into your PR branch, those commits - * should be excluded from this diff (since they belong to some other PR). In order to do that, - * Rush needs to know where to find the base branch for your PR. This information cannot be - * determined from Git alone, since the "pull request" feature is not a Git concept. Ideally - * Rush would use a vendor-specific protocol to query the information from GitHub, Azure DevOps, etc. - * But to keep things simple, "rush change" simply assumes that your PR is against the "main" branch - * of the Git remote indicated by the repository.url setting in rush.json. If you are working in - * a GitHub "fork" of the real repo, this setting will be different from the repository URL of your - * your PR branch, and in this situation "rush change" will also automatically invoke "git fetch" - * to retrieve the latest activity for the remote main branch. - */ - "url": "https://github.com/hcengineering/huly.core", - /** - * The default branch name. This tells "rush change" which remote branch to compare against. - * The default value is "main" - */ - "defaultBranch": "main", - /** - * The default remote. This tells "rush change" which remote to compare against if the remote URL is - * not set or if a remote matching the provided remote URL is not found. - */ - "defaultRemote": "origin" - }, - - /** - * Event hooks are customized script actions that Rush executes when specific events occur - */ - "eventHooks": { - /** - * A list of shell commands to run before "rush install" or "rush update" starts installation - */ - "preRushInstall": [ - // "common/scripts/pre-rush-install.js" - ], - - /** - * A list of shell commands to run after "rush install" or "rush update" finishes installation - */ - "postRushInstall": [], - - /** - * A list of shell commands to run before "rush build" or "rush rebuild" starts building - */ - "preRushBuild": [], - - /** - * A list of shell commands to run after "rush build" or "rush rebuild" finishes building - */ - "postRushBuild": [], - - /** - * A list of shell commands to run before the "rushx" command starts - */ - "preRushx": [], - - /** - * A list of shell commands to run after the "rushx" command finishes - */ - "postRushx": [] - }, - - /** - * Installation variants allow you to maintain a parallel set of configuration files that can be - * used to build the entire monorepo with an alternate set of dependencies. For example, suppose - * you upgrade all your projects to use a new release of an important framework, but during a transition period - * you intend to maintain compatibility with the old release. In this situation, you probably want your - * CI validation to build the entire repo twice: once with the old release, and once with the new release. - * - * Rush "installation variants" correspond to sets of config files located under this folder: - * - * common/config/rush/variants/ - * - * The variant folder can contain an alternate common-versions.json file. Its "preferredVersions" field can be used - * to select older versions of dependencies (within a loose SemVer range specified in your package.json files). - * To install a variant, run "rush install --variant ". - * - * For more details and instructions, see this article: https://rushjs.io/pages/advanced/installation_variants/ - */ - "variants": [ - // { - // /** - // * The folder name for this variant. - // */ - // "variantName": "old-sdk", - // - // /** - // * An informative description - // */ - // "description": "Build this repo using the previous release of the SDK" - // } - ], - - /** - * Rush can collect anonymous telemetry about everyday developer activity such as - * success/failure of installs, builds, and other operations. You can use this to identify - * problems with your toolchain or Rush itself. THIS TELEMETRY IS NOT SHARED WITH MICROSOFT. - * It is written into JSON files in the common/temp folder. It's up to you to write scripts - * that read these JSON files and do something with them. These scripts are typically registered - * in the "eventHooks" section. - */ - // "telemetryEnabled": false, - - /** - * Allows creation of hotfix changes. This feature is experimental so it is disabled by default. - * If this is set, 'rush change' only allows a 'hotfix' change type to be specified. This change type - * will be used when publishing subsequent changes from the monorepo. - */ - // "hotfixChangeEnabled": false, - - /** - * This is an optional, but recommended, list of allowed tags that can be applied to Rush projects - * using the "tags" setting in this file. This list is useful for preventing mistakes such as misspelling, - * and it also provides a centralized place to document your tags. If "allowedProjectTags" list is - * not specified, then any valid tag is allowed. A tag name must be one or more words - * separated by hyphens or slashes, where a word may contain lowercase ASCII letters, digits, - * ".", and "@" characters. - */ - // "allowedProjectTags": [ "tools", "frontend-team", "1.0.0-release" ], - - /** - * (Required) This is the inventory of projects to be managed by Rush. - * - * Rush does not automatically scan for projects using wildcards, for a few reasons: - * 1. Depth-first scans are expensive, particularly when tools need to repeatedly collect the list. - * 2. On a caching CI machine, scans can accidentally pick up files left behind from a previous build. - * 3. It's useful to have a centralized inventory of all projects and their important metadata. - */ - "projects": [ - { - "packageName": "@hcengineering/scripts", - "projectFolder": "common/scripts", - "shouldPublish": false - }, - { - "packageName": "@hcengineering/account-client", - "projectFolder": "packages/account-client", - "shouldPublish": true - }, - { - "packageName": "@hcengineering/analytics", - "projectFolder": "packages/analytics", - "shouldPublish": true - }, - { - "packageName": "@hcengineering/analytics-service", - "projectFolder": "packages/analytics-service", - "shouldPublish": true - }, - { - "packageName": "@hcengineering/api-client", - "projectFolder": "packages/api-client", - "shouldPublish": true - }, - { - "packageName": "@hcengineering/collaborator-client", - "projectFolder": "packages/collaborator-client", - "shouldPublish": true - }, - { - "packageName": "@hcengineering/core", - "projectFolder": "packages/core", - "shouldPublish": true - }, - { - "packageName": "@hcengineering/model", - "projectFolder": "packages/model", - "shouldPublish": true - }, - { - "packageName": "@hcengineering/platform", - "projectFolder": "packages/platform", - "shouldPublish": true - }, - { - "packageName": "@hcengineering/query", - "projectFolder": "packages/query", - "shouldPublish": true - }, - { - "packageName": "@hcengineering/rank", - "projectFolder": "packages/rank", - "shouldPublish": true - }, - { - "packageName": "@hcengineering/retry", - "projectFolder": "packages/retry", - "shouldPublish": true - }, - { - "packageName": "@hcengineering/storage", - "projectFolder": "packages/storage", - "shouldPublish": true - }, - { - "packageName": "@hcengineering/text", - "projectFolder": "packages/text", - "shouldPublish": true - }, - { - "packageName": "@hcengineering/text-core", - "projectFolder": "packages/text-core", - "shouldPublish": true - }, - { - "packageName": "@hcengineering/text-html", - "projectFolder": "packages/text-html", - "shouldPublish": true - }, - { - "packageName": "@hcengineering/text-markdown", - "projectFolder": "packages/text-markdown", - "shouldPublish": true - }, - { - "packageName": "@hcengineering/text-ydoc", - "projectFolder": "packages/text-ydoc", - "shouldPublish": true - }, - { - "packageName": "@hcengineering/client", - "projectFolder": "packages/client", - "shouldPublish": true - }, - { - "packageName": "@hcengineering/client-resources", - "projectFolder": "packages/client-resources", - "shouldPublish": true - }, - { - "packageName": "@hcengineering/rpc", - "projectFolder": "packages/rpc", - "shouldPublish": true - }, - { - "packageName": "@hcengineering/server-token", - "projectFolder": "packages/token", - "shouldPublish": true - }, - { - "packageName": "@hcengineering/hulylake-client", - "projectFolder": "packages/hulylake-client", - "shouldPublish": true - }, - { - "packageName": "@hcengineering/storage-client", - "projectFolder": "packages/storage-client", - "shouldPublish": true - }, - { - "packageName": "@hcengineering/measurements", - "projectFolder": "packages/measurements", - "shouldPublish": true - }, - { - "packageName": "@hcengineering/measurements-otlp", - "projectFolder": "packages/measurements-otlp", - "shouldPublish": true - }, - { - "packageName": "@hcengineering/postgres-base", - "projectFolder": "packages/postgres-base", - "shouldPublish": true - } - ] -} diff --git a/foundations/hulylake/.dockerignore b/foundations/hulylake/.dockerignore deleted file mode 100644 index e75469be2c..0000000000 --- a/foundations/hulylake/.dockerignore +++ /dev/null @@ -1,4 +0,0 @@ -target/ -.git/ -.jj/ -.cargo/ \ No newline at end of file diff --git a/foundations/hulylake/.github/workflows/build.yaml b/foundations/hulylake/.github/workflows/build.yaml deleted file mode 100644 index 70ca5b8bd0..0000000000 --- a/foundations/hulylake/.github/workflows/build.yaml +++ /dev/null @@ -1,31 +0,0 @@ -name: Build and Push - -on: - workflow_dispatch: - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Log to registry - uses: docker/login-action@v3 - with: - username: ${{ vars.DOCKER_USER }} - password: ${{ secrets.DOCKER_ACCESS_TOKEN }} - - - run: echo VERSION=$(grep '^version =' server/Cargo.toml | cut -d '"' -f 2) >> $GITHUB_ENV - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build and Push - uses: docker/build-push-action@v6 - with: - file: Dockerfile - push: true - tags: "${{ vars.DOCKER_USER }}/service_hulylake:${{ env.VERSION }},${{ vars.DOCKER_USER }}/service_hulylake:latest" - platforms: linux/amd64,linux/arm64 diff --git a/foundations/hulylake/.gitignore b/foundations/hulylake/.gitignore deleted file mode 100644 index 9bf4926a2a..0000000000 --- a/foundations/hulylake/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/target -.cargo -.vscode \ No newline at end of file diff --git a/foundations/hulylake/Cargo.lock b/foundations/hulylake/Cargo.lock deleted file mode 100644 index 1daeff2723..0000000000 --- a/foundations/hulylake/Cargo.lock +++ /dev/null @@ -1,6649 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "actix-codec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" -dependencies = [ - "bitflags 2.9.3", - "bytes", - "futures-core", - "futures-sink", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "actix-cors" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daa239b93927be1ff123eebada5a3ff23e89f0124ccb8609234e5103d5a5ae6d" -dependencies = [ - "actix-utils", - "actix-web", - "derive_more", - "futures-util", - "log", - "once_cell", - "smallvec", -] - -[[package]] -name = "actix-http" -version = "3.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44cceded2fb55f3c4b67068fa64962e2ca59614edc5b03167de9ff82ae803da0" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-tls", - "actix-utils", - "base64 0.22.1", - "bitflags 2.9.3", - "brotli", - "bytes", - "bytestring", - "derive_more", - "encoding_rs", - "flate2", - "foldhash", - "futures-core", - "h2 0.3.27", - "http 0.2.12", - "httparse", - "httpdate", - "itoa", - "language-tags", - "local-channel", - "mime", - "percent-encoding", - "pin-project-lite", - "rand 0.9.2", - "sha1", - "smallvec", - "tokio", - "tokio-util", - "tracing", - "zstd", -] - -[[package]] -name = "actix-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" -dependencies = [ - "quote", - "syn 2.0.106", -] - -[[package]] -name = "actix-router" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" -dependencies = [ - "bytestring", - "cfg-if", - "http 0.2.12", - "regex", - "regex-lite", - "serde", - "tracing", -] - -[[package]] -name = "actix-rt" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63" -dependencies = [ - "futures-core", - "tokio", -] - -[[package]] -name = "actix-server" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "futures-util", - "mio", - "socket2 0.5.10", - "tokio", - "tracing", -] - -[[package]] -name = "actix-service" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" -dependencies = [ - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "actix-tls" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac453898d866cdbecdbc2334fe1738c747b4eba14a677261f2b768ba05329389" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "impl-more", - "pin-project-lite", - "tokio", - "tokio-rustls 0.23.4", - "tokio-util", - "tracing", - "webpki-roots 0.22.6", -] - -[[package]] -name = "actix-utils" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" -dependencies = [ - "local-waker", - "pin-project-lite", -] - -[[package]] -name = "actix-web" -version = "4.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a597b77b5c6d6a1e1097fddde329a83665e25c5437c696a3a9a4aa514a614dea" -dependencies = [ - "actix-codec", - "actix-http", - "actix-macros", - "actix-router", - "actix-rt", - "actix-server", - "actix-service", - "actix-tls", - "actix-utils", - "actix-web-codegen", - "bytes", - "bytestring", - "cfg-if", - "cookie 0.16.2", - "derive_more", - "encoding_rs", - "foldhash", - "futures-core", - "futures-util", - "impl-more", - "itoa", - "language-tags", - "log", - "mime", - "once_cell", - "pin-project-lite", - "regex", - "regex-lite", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "socket2 0.5.10", - "time 0.3.42", - "tracing", - "url", -] - -[[package]] -name = "actix-web-codegen" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" -dependencies = [ - "actix-router", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "ahash" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" -dependencies = [ - "getrandom 0.2.16", - "once_cell", - "version_check", -] - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "ansi-to-tui" -version = "7.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67555e1f1ece39d737e28c8a017721287753af3f93225e4a445b29ccb0f5912c" -dependencies = [ - "nom", - "ratatui", - "simdutf8", - "smallvec", - "thiserror 1.0.69", -] - -[[package]] -name = "anstream" -version = "0.6.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" - -[[package]] -name = "anstyle-parse" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" -dependencies = [ - "windows-sys 0.60.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.60.2", -] - -[[package]] -name = "anyhow" -version = "1.0.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" -dependencies = [ - "backtrace", -] - -[[package]] -name = "arraydeque" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" - -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "async-compression" -version = "0.4.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "977eb15ea9efd848bb8a4a1a2500347ed7f0bf794edf0dc3ddcf439f43d36b23" -dependencies = [ - "compression-codecs", - "compression-core", - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "async-tungstenite" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee88b4c88ac8c9ea446ad43498955750a4bbe64c4392f21ccfe5d952865e318f" -dependencies = [ - "atomic-waker", - "futures-core", - "futures-io", - "futures-task", - "futures-util", - "log", - "pin-project-lite", - "tungstenite", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "aws-config" -version = "1.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bc1b40fb26027769f16960d2f4a6bc20c4bb755d403e552c8c1a73af433c246" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-sdk-sso", - "aws-sdk-ssooidc", - "aws-sdk-sts", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "hex", - "http 1.3.1", - "ring 0.17.14", - "time 0.3.42", - "tokio", - "tracing", - "url", - "zeroize", -] - -[[package]] -name = "aws-credential-types" -version = "1.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d025db5d9f52cbc413b167136afb3d8aeea708c0d8884783cf6253be5e22f6f2" -dependencies = [ - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "zeroize", -] - -[[package]] -name = "aws-lc-rs" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" -dependencies = [ - "bindgen", - "cc", - "cmake", - "dunce", - "fs_extra", -] - -[[package]] -name = "aws-runtime" -version = "1.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c034a1bc1d70e16e7f4e4caf7e9f7693e4c9c24cd91cf17c2a0b21abaebc7c8b" -dependencies = [ - "aws-credential-types", - "aws-sigv4", - "aws-smithy-async", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "http 0.2.12", - "http-body 0.4.6", - "percent-encoding", - "pin-project-lite", - "tracing", - "uuid", -] - -[[package]] -name = "aws-sdk-s3" -version = "1.104.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c488cd6abb0ec9811c401894191932e941c5f91dc226043edacd0afa1634bc" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-sigv4", - "aws-smithy-async", - "aws-smithy-checksums", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-smithy-xml", - "aws-types", - "bytes", - "fastrand", - "hex", - "hmac", - "http 0.2.12", - "http 1.3.1", - "http-body 0.4.6", - "lru 0.12.5", - "percent-encoding", - "regex-lite", - "sha2", - "tracing", - "url", -] - -[[package]] -name = "aws-sdk-sso" -version = "1.83.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cd43af212d2a1c4dedff6f044d7e1961e5d9e7cfe773d70f31d9842413886" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "http 0.2.12", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-ssooidc" -version = "1.84.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20ec4a95bd48e0db7a424356a161f8d87bd6a4f0af37204775f0da03d9e39fc3" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "http 0.2.12", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-sts" -version = "1.85.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "410309ad0df4606bc721aff0d89c3407682845453247213a0ccc5ff8801ee107" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-query", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-smithy-xml", - "aws-types", - "fastrand", - "http 0.2.12", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sigv4" -version = "1.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084c34162187d39e3740cb635acd73c4e3a551a36146ad6fe8883c929c9f876c" -dependencies = [ - "aws-credential-types", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "crypto-bigint 0.5.5", - "form_urlencoded", - "hex", - "hmac", - "http 0.2.12", - "http 1.3.1", - "p256", - "percent-encoding", - "ring 0.17.14", - "sha2", - "subtle", - "time 0.3.42", - "tracing", - "zeroize", -] - -[[package]] -name = "aws-smithy-async" -version = "1.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e190749ea56f8c42bf15dd76c65e14f8f765233e6df9b0506d9d934ebef867c" -dependencies = [ - "futures-util", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "aws-smithy-checksums" -version = "0.63.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56d2df0314b8e307995a3b86d44565dfe9de41f876901a7d71886c756a25979f" -dependencies = [ - "aws-smithy-http", - "aws-smithy-types", - "bytes", - "crc-fast", - "hex", - "http 0.2.12", - "http-body 0.4.6", - "md-5", - "pin-project-lite", - "sha1", - "sha2", - "tracing", -] - -[[package]] -name = "aws-smithy-eventstream" -version = "0.60.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "182b03393e8c677347fb5705a04a9392695d47d20ef0a2f8cfe28c8e6b9b9778" -dependencies = [ - "aws-smithy-types", - "bytes", - "crc32fast", -] - -[[package]] -name = "aws-smithy-http" -version = "0.62.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c4dacf2d38996cf729f55e7a762b30918229917eca115de45dfa8dfb97796c9" -dependencies = [ - "aws-smithy-eventstream", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "bytes-utils", - "futures-core", - "http 0.2.12", - "http 1.3.1", - "http-body 0.4.6", - "percent-encoding", - "pin-project-lite", - "pin-utils", - "tracing", -] - -[[package]] -name = "aws-smithy-http-client" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147e8eea63a40315d704b97bf9bc9b8c1402ae94f89d5ad6f7550d963309da1b" -dependencies = [ - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "h2 0.3.27", - "h2 0.4.12", - "http 0.2.12", - "http 1.3.1", - "http-body 0.4.6", - "hyper 0.14.32", - "hyper 1.7.0", - "hyper-rustls 0.24.2", - "hyper-rustls 0.27.7", - "hyper-util", - "pin-project-lite", - "rustls 0.21.12", - "rustls 0.23.31", - "rustls-native-certs 0.8.1", - "rustls-pki-types", - "tokio", - "tokio-rustls 0.26.2", - "tower", - "tracing", -] - -[[package]] -name = "aws-smithy-json" -version = "0.61.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaa31b350998e703e9826b2104dd6f63be0508666e1aba88137af060e8944047" -dependencies = [ - "aws-smithy-types", -] - -[[package]] -name = "aws-smithy-observability" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9364d5989ac4dd918e5cc4c4bdcc61c9be17dcd2586ea7f69e348fc7c6cab393" -dependencies = [ - "aws-smithy-runtime-api", -] - -[[package]] -name = "aws-smithy-query" -version = "0.60.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2fbd61ceb3fe8a1cb7352e42689cec5335833cd9f94103a61e98f9bb61c64bb" -dependencies = [ - "aws-smithy-types", - "urlencoding", -] - -[[package]] -name = "aws-smithy-runtime" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3946acbe1ead1301ba6862e712c7903ca9bb230bdf1fbd1b5ac54158ef2ab1f" -dependencies = [ - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-http-client", - "aws-smithy-observability", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "fastrand", - "http 0.2.12", - "http 1.3.1", - "http-body 0.4.6", - "http-body 1.0.1", - "pin-project-lite", - "pin-utils", - "tokio", - "tracing", -] - -[[package]] -name = "aws-smithy-runtime-api" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07f5e0fc8a6b3f2303f331b94504bbf754d85488f402d6f1dd7a6080f99afe56" -dependencies = [ - "aws-smithy-async", - "aws-smithy-types", - "bytes", - "http 0.2.12", - "http 1.3.1", - "pin-project-lite", - "tokio", - "tracing", - "zeroize", -] - -[[package]] -name = "aws-smithy-types" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d498595448e43de7f4296b7b7a18a8a02c61ec9349128c80a368f7c3b4ab11a8" -dependencies = [ - "base64-simd", - "bytes", - "bytes-utils", - "futures-core", - "http 0.2.12", - "http 1.3.1", - "http-body 0.4.6", - "http-body 1.0.1", - "http-body-util", - "itoa", - "num-integer", - "pin-project-lite", - "pin-utils", - "ryu", - "serde", - "time 0.3.42", - "tokio", - "tokio-util", -] - -[[package]] -name = "aws-smithy-xml" -version = "0.60.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db87b96cb1b16c024980f133968d52882ca0daaee3a086c6decc500f6c99728" -dependencies = [ - "xmlparser", -] - -[[package]] -name = "aws-types" -version = "1.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b069d19bf01e46298eaedd7c6f283fe565a59263e53eebec945f3e6398f42390" -dependencies = [ - "aws-credential-types", - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "rustc_version", - "tracing", -] - -[[package]] -name = "backon" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "592277618714fbcecda9a02ba7a8781f319d26532a88553bbacc77ba5d2b3a8d" -dependencies = [ - "fastrand", - "gloo-timers", - "tokio", -] - -[[package]] -name = "backtrace" -version = "0.3.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] - -[[package]] -name = "base16ct" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64-simd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" -dependencies = [ - "outref", - "vsimd", -] - -[[package]] -name = "base64ct" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" - -[[package]] -name = "bb8" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212d8b8e1a22743d9241575c6ba822cf9c8fef34771c86ab7e477a4fbfd254e5" -dependencies = [ - "futures-util", - "parking_lot 0.12.4", - "tokio", -] - -[[package]] -name = "bb8-postgres" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e570e6557cd0f88d28d32afa76644873271a70dc22656df565b2021c4036aa9c" -dependencies = [ - "bb8", - "tokio", - "tokio-postgres", -] - -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - -[[package]] -name = "bindgen" -version = "0.69.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" -dependencies = [ - "bitflags 2.9.3", - "cexpr", - "clang-sys", - "itertools 0.12.1", - "lazy_static", - "lazycell", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex", - "syn 2.0.106", - "which", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" -dependencies = [ - "serde", -] - -[[package]] -name = "blake3" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" -dependencies = [ - "arrayref", - "arrayvec", - "cc", - "cfg-if", - "constant_time_eq", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "brotli" -version = "8.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[package]] -name = "bumpalo" -version = "3.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" - -[[package]] -name = "bytecount" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" - -[[package]] -name = "bytes-utils" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" -dependencies = [ - "bytes", - "either", -] - -[[package]] -name = "bytestring" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f" -dependencies = [ - "bytes", -] - -[[package]] -name = "cassowary" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" - -[[package]] -name = "castaway" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" -dependencies = [ - "rustversion", -] - -[[package]] -name = "cc" -version = "1.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" -dependencies = [ - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - -[[package]] -name = "cfg-if" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "chrono" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link 0.2.0", -] - -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "clap" -version = "4.5.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.5.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.5.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "clap_lex" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" - -[[package]] -name = "cmake" -version = "0.1.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" -dependencies = [ - "cc", -] - -[[package]] -name = "color-eyre" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" -dependencies = [ - "backtrace", - "color-spantrace", - "eyre", - "indenter", - "once_cell", - "owo-colors", - "tracing-error", -] - -[[package]] -name = "color-spantrace" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427" -dependencies = [ - "once_cell", - "owo-colors", - "tracing-core", - "tracing-error", -] - -[[package]] -name = "colorchoice" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" - -[[package]] -name = "compact_str" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" -dependencies = [ - "castaway", - "cfg-if", - "itoa", - "rustversion", - "ryu", - "static_assertions", -] - -[[package]] -name = "compression-codecs" -version = "0.4.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "485abf41ac0c8047c07c87c72c8fb3eb5197f6e9d7ded615dfd1a00ae00a0f64" -dependencies = [ - "brotli", - "compression-core", - "flate2", - "memchr", - "zstd", - "zstd-safe", -] - -[[package]] -name = "compression-core" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" - -[[package]] -name = "config" -version = "0.15.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0faa974509d38b33ff89282db9c3295707ccf031727c0de9772038ec526852ba" -dependencies = [ - "async-trait", - "convert_case", - "json5", - "pathdiff", - "ron", - "rust-ini", - "serde", - "serde-untagged", - "serde_json", - "toml 0.9.5", - "winnow", - "yaml-rust2", -] - -[[package]] -name = "console" -version = "0.15.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" -dependencies = [ - "encode_unicode", - "libc", - "once_cell", - "unicode-width 0.2.0", - "windows-sys 0.59.0", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "const-random" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" -dependencies = [ - "const-random-macro", -] - -[[package]] -name = "const-random-macro" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" -dependencies = [ - "getrandom 0.2.16", - "once_cell", - "tiny-keccak", -] - -[[package]] -name = "constant_time_eq" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" - -[[package]] -name = "convert_case" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "cookie" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" -dependencies = [ - "percent-encoding", - "time 0.3.42", - "version_check", -] - -[[package]] -name = "cookie" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" -dependencies = [ - "percent-encoding", - "time 0.3.42", - "version_check", -] - -[[package]] -name = "cookie_store" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9" -dependencies = [ - "cookie 0.18.1", - "document-features", - "idna", - "log", - "publicsuffix", - "serde", - "serde_derive", - "serde_json", - "time 0.3.42", - "url", -] - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - -[[package]] -name = "crc-fast" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bf62af4cc77d8fe1c22dde4e721d87f2f54056139d8c412e1366b740305f56f" -dependencies = [ - "crc", - "digest", - "libc", - "rand 0.9.2", - "regex", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crossterm" -version = "0.28.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" -dependencies = [ - "bitflags 2.9.3", - "crossterm_winapi", - "futures-core", - "mio", - "parking_lot 0.12.4", - "rustix 0.38.44", - "signal-hook", - "signal-hook-mio", - "winapi", -] - -[[package]] -name = "crossterm_winapi" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" -dependencies = [ - "winapi", -] - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "crypto-bigint" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.106", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "dashmap" -version = "6.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" -dependencies = [ - "cfg-if", - "crossbeam-utils", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core 0.9.11", -] - -[[package]] -name = "data-encoding" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" - -[[package]] -name = "der" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" -dependencies = [ - "const-oid", - "zeroize", -] - -[[package]] -name = "deranged" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" -dependencies = [ - "powerfmt", - "serde", -] - -[[package]] -name = "derive_builder" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" -dependencies = [ - "derive_builder_macro", -] - -[[package]] -name = "derive_builder_core" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "derive_builder_macro" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" -dependencies = [ - "derive_builder_core", - "syn 2.0.106", -] - -[[package]] -name = "derive_more" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", - "unicode-xid", -] - -[[package]] -name = "diff" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", - "subtle", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "dlv-list" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" -dependencies = [ - "const-random", -] - -[[package]] -name = "document-features" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" -dependencies = [ - "litrs", -] - -[[package]] -name = "dotenv" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "ecdsa" -version = "0.14.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" -dependencies = [ - "der", - "elliptic-curve", - "rfc6979", - "signature", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "elliptic-curve" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" -dependencies = [ - "base16ct", - "crypto-bigint 0.4.9", - "der", - "digest", - "ff", - "generic-array", - "group", - "pkcs8", - "rand_core 0.6.4", - "sec1", - "subtle", - "zeroize", -] - -[[package]] -name = "encode_unicode" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "erased-serde" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7" -dependencies = [ - "serde", - "typeid", -] - -[[package]] -name = "errno" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] - -[[package]] -name = "eyre" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" -dependencies = [ - "indenter", - "once_cell", -] - -[[package]] -name = "fallible-iterator" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "ff" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "flate2" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "font8x8" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875488b8711a968268c7cf5d139578713097ca4635a76044e8fe8eedf831d07e" - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - -[[package]] -name = "fuchsia-cprng" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-timer" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi", - "wasi 0.14.3+wasi-0.2.4", - "wasm-bindgen", -] - -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "gloo-timers" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "governor" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444405bbb1a762387aa22dd569429533b54a1d8759d35d3b64cb39b0293eaa19" -dependencies = [ - "cfg-if", - "dashmap", - "futures-sink", - "futures-timer", - "futures-util", - "getrandom 0.3.3", - "hashbrown 0.15.5", - "nonzero_ext", - "parking_lot 0.12.4", - "portable-atomic", - "quanta", - "rand 0.9.2", - "smallvec", - "spinning_top", - "web-time", -] - -[[package]] -name = "group" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" -dependencies = [ - "ff", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "h2" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap 2.11.0", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "h2" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http 1.3.1", - "indexmap 2.11.0", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash", -] - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", -] - -[[package]] -name = "hashlink" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" -dependencies = [ - "hashbrown 0.15.5", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "home" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http 1.3.1", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http 1.3.1", - "http-body 1.0.1", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hulylake" -version = "0.1.18" -dependencies = [ - "actix-cors", - "actix-web", - "anyhow", - "async-stream", - "aws-config", - "aws-sdk-s3", - "bb8", - "bb8-postgres", - "blake3", - "bytes", - "chrono", - "config", - "futures", - "futures-util", - "hulyrs", - "json-patch", - "jsonptr", - "ksuid", - "lockable", - "md5", - "mime", - "opentelemetry", - "opentelemetry-appender-tracing", - "refinery", - "secrecy", - "serde", - "serde_json", - "size", - "strum 0.27.2", - "thiserror 2.0.16", - "tokio", - "tokio-postgres", - "tokio-stream", - "tracing", - "tracing-actix-web", - "tracing-opentelemetry", - "tracing-subscriber", - "uuid", -] - -[[package]] -name = "hulyrs" -version = "0.1.0" -source = "git+https://github.com/hcengineering/hulyrs.git#24b211c4d1b013e2ec1e2daae470168a1841dacf" -dependencies = [ - "actix-web", - "bytes", - "chrono", - "config", - "derive_builder", - "futures", - "governor", - "itoa", - "jsonwebtoken", - "num-traits", - "opentelemetry", - "opentelemetry-otlp", - "opentelemetry-stdout", - "opentelemetry_sdk", - "rand 0.9.2", - "reqwest", - "reqwest-middleware", - "reqwest-ratelimit", - "reqwest-retry", - "reqwest-websocket", - "ryu", - "secrecy", - "serde", - "serde_json", - "serde_with", - "strum 0.27.2", - "thiserror 2.0.16", - "tokio", - "tokio-stream", - "tokio_with_wasm", - "tracing", - "url", - "uuid", - "wasmtimer", -] - -[[package]] -name = "humantime" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" - -[[package]] -name = "humantime-serde" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a3db5ea5923d99402c94e9feb261dc5ee9b4efa158b0315f788cf549cc200c" -dependencies = [ - "humantime", - "serde", -] - -[[package]] -name = "hyper" -version = "0.14.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2 0.3.27", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.5.10", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "h2 0.4.12", - "http 1.3.1", - "http-body 1.0.1", - "httparse", - "itoa", - "pin-project-lite", - "pin-utils", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" -dependencies = [ - "futures-util", - "http 0.2.12", - "hyper 0.14.32", - "log", - "rustls 0.21.12", - "rustls-native-certs 0.6.3", - "tokio", - "tokio-rustls 0.24.1", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http 1.3.1", - "hyper 1.7.0", - "hyper-util", - "rustls 0.23.31", - "rustls-native-certs 0.8.1", - "rustls-pki-types", - "tokio", - "tokio-rustls 0.26.2", - "tower-service", - "webpki-roots 1.0.2", -] - -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper 1.7.0", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "hyper 1.7.0", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2 0.6.0", - "system-configuration", - "tokio", - "tower-service", - "tracing", - "windows-registry", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" - -[[package]] -name = "icu_properties" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "potential_utf", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" - -[[package]] -name = "icu_provider" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" -dependencies = [ - "displaydoc", - "icu_locale_core", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "impl-more" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" - -[[package]] -name = "indenter" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - -[[package]] -name = "indexmap" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" -dependencies = [ - "equivalent", - "hashbrown 0.15.5", - "serde", -] - -[[package]] -name = "indoc" -version = "2.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" - -[[package]] -name = "instability" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a" -dependencies = [ - "darling", - "indoc", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "io-uring" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = [ - "bitflags 2.9.3", - "cfg-if", - "libc", -] - -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" - -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.3", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "json-patch" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "159294d661a039f7644cea7e4d844e6b25aaf71c1ffe9d73a96d768c24b0faf4" -dependencies = [ - "jsonptr", - "serde", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] -name = "json5" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" -dependencies = [ - "pest", - "pest_derive", - "serde", -] - -[[package]] -name = "jsonptr" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "jsonwebtoken" -version = "9.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" -dependencies = [ - "base64 0.22.1", - "js-sys", - "pem", - "ring 0.17.14", - "serde", - "serde_json", - "simple_asn1", -] - -[[package]] -name = "ksuid" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42729258604bc3c02a3502ca8cb070d044fef73d75c657bec0fe5d51bfac8d24" -dependencies = [ - "byteorder", - "rand 0.3.23", - "resize-slice", - "time 0.1.45", -] - -[[package]] -name = "language-tags" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - -[[package]] -name = "libc" -version = "0.2.175" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" - -[[package]] -name = "libloading" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" -dependencies = [ - "cfg-if", - "windows-targets 0.53.3", -] - -[[package]] -name = "libredox" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" -dependencies = [ - "bitflags 2.9.3", - "libc", - "redox_syscall 0.5.17", -] - -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - -[[package]] -name = "linux-raw-sys" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" - -[[package]] -name = "litemap" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" - -[[package]] -name = "litrs" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" - -[[package]] -name = "local-channel" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" -dependencies = [ - "futures-core", - "futures-sink", - "local-waker", -] - -[[package]] -name = "local-waker" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" - -[[package]] -name = "lock_api" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "lockable" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0f35193d57711b7b4730a3b888a5033347fb7be1ee9a64a755fa4ee013ef80" -dependencies = [ - "derive_more", - "futures", - "itertools 0.14.0", - "lru 0.14.0", - "tokio", -] - -[[package]] -name = "log" -version = "0.4.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" - -[[package]] -name = "lru" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999beba7b6e8345721bd280141ed958096a2e4abdf74f67ff4ce49b4b54e47a" -dependencies = [ - "hashbrown 0.12.3", -] - -[[package]] -name = "lru" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" -dependencies = [ - "hashbrown 0.15.5", -] - -[[package]] -name = "lru" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f8cc7106155f10bdf99a6f379688f543ad6596a415375b36a59a054ceda1198" -dependencies = [ - "hashbrown 0.15.5", -] - -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest", -] - -[[package]] -name = "md5" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" - -[[package]] -name = "memchr" -version = "2.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" - -[[package]] -name = "memoize" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8d1d5792299bab3f8b5d88d1b7a7cb50ad7ef039a8c4d45a6b84880a6526276" -dependencies = [ - "lazy_static", - "lru 0.7.8", - "memoize-inner", -] - -[[package]] -name = "memoize-inner" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dd8f89255d8ff313afabed9a3c83ef0993cc056679dfd001f5111a026f876f7" -dependencies = [ - "lazy_static", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", -] - -[[package]] -name = "mio" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" -dependencies = [ - "libc", - "log", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", -] - -[[package]] -name = "mutually_exclusive_features" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94e1e6445d314f972ff7395df2de295fe51b71821694f0b0e1e79c4f12c8577" - -[[package]] -name = "native-tls" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework 2.11.1", - "security-framework-sys", - "tempfile", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "nonzero_ext" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" - -[[package]] -name = "nu-ansi-term" -version = "0.50.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" -dependencies = [ - "windows-sys 0.52.0", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" - -[[package]] -name = "onig" -version = "6.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" -dependencies = [ - "bitflags 2.9.3", - "libc", - "once_cell", - "onig_sys", -] - -[[package]] -name = "onig_sys" -version = "69.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" -dependencies = [ - "cc", - "pkg-config", -] - -[[package]] -name = "openssl" -version = "0.10.73" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" -dependencies = [ - "bitflags 2.9.3", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - -[[package]] -name = "openssl-sys" -version = "0.9.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "opentelemetry" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaf416e4cb72756655126f7dd7bb0af49c674f4c1b9903e80c009e0c37e552e6" -dependencies = [ - "futures-core", - "futures-sink", - "js-sys", - "pin-project-lite", - "thiserror 2.0.16", - "tracing", -] - -[[package]] -name = "opentelemetry-appender-tracing" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e68f63eca5fad47e570e00e893094fc17be959c80c79a7d6ec1abdd5ae6ffc16" -dependencies = [ - "opentelemetry", - "tracing", - "tracing-core", - "tracing-subscriber", -] - -[[package]] -name = "opentelemetry-http" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f6639e842a97dbea8886e3439710ae463120091e2e064518ba8e716e6ac36d" -dependencies = [ - "async-trait", - "bytes", - "http 1.3.1", - "opentelemetry", - "reqwest", -] - -[[package]] -name = "opentelemetry-otlp" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbee664a43e07615731afc539ca60c6d9f1a9425e25ca09c57bc36c87c55852b" -dependencies = [ - "http 1.3.1", - "opentelemetry", - "opentelemetry-http", - "opentelemetry-proto", - "opentelemetry_sdk", - "prost", - "reqwest", - "thiserror 2.0.16", - "tracing", -] - -[[package]] -name = "opentelemetry-proto" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e046fd7660710fe5a05e8748e70d9058dc15c94ba914e7c4faa7c728f0e8ddc" -dependencies = [ - "opentelemetry", - "opentelemetry_sdk", - "prost", - "tonic", -] - -[[package]] -name = "opentelemetry-stdout" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447191061af41c3943e082ea359ab8b64ff27d6d34d30d327df309ddef1eef6f" -dependencies = [ - "chrono", - "opentelemetry", - "opentelemetry_sdk", -] - -[[package]] -name = "opentelemetry_sdk" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f644aa9e5e31d11896e024305d7e3c98a88884d9f8919dbf37a9991bc47a4b" -dependencies = [ - "futures-channel", - "futures-executor", - "futures-util", - "opentelemetry", - "percent-encoding", - "rand 0.9.2", - "serde_json", - "thiserror 2.0.16", -] - -[[package]] -name = "ordered-multimap" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" -dependencies = [ - "dlv-list", - "hashbrown 0.14.5", -] - -[[package]] -name = "outref" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" - -[[package]] -name = "owo-colors" -version = "4.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" - -[[package]] -name = "p256" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" -dependencies = [ - "ecdsa", - "elliptic-curve", - "sha2", -] - -[[package]] -name = "papergrid" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b915f831b85d984193fdc3d3611505871dc139b2534530fa01c1a6a6707b6723" -dependencies = [ - "bytecount", - "fnv", - "unicode-width 0.2.0", -] - -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core 0.8.6", -] - -[[package]] -name = "parking_lot" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" -dependencies = [ - "lock_api", - "parking_lot_core 0.9.11", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" -dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall 0.2.16", - "smallvec", - "winapi", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.5.17", - "smallvec", - "windows-targets 0.52.6", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pathdiff" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" - -[[package]] -name = "pem" -version = "3.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" -dependencies = [ - "base64 0.22.1", - "serde", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pest" -version = "2.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" -dependencies = [ - "memchr", - "thiserror 2.0.16", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "pest_meta" -version = "2.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" -dependencies = [ - "pest", - "sha2", -] - -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_shared", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pin-project" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkcs8" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "plist" -version = "1.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" -dependencies = [ - "base64 0.22.1", - "indexmap 2.11.0", - "quick-xml", - "serde", - "time 0.3.42", -] - -[[package]] -name = "portable-atomic" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" - -[[package]] -name = "postgres-protocol" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ff0abab4a9b844b93ef7b81f1efc0a366062aaef2cd702c76256b5dc075c54" -dependencies = [ - "base64 0.22.1", - "byteorder", - "bytes", - "fallible-iterator", - "hmac", - "md-5", - "memchr", - "rand 0.9.2", - "sha2", - "stringprep", -] - -[[package]] -name = "postgres-types" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613283563cd90e1dfc3518d548caee47e0e725455ed619881f5cf21f36de4b48" -dependencies = [ - "bytes", - "chrono", - "fallible-iterator", - "postgres-protocol", - "serde", - "serde_json", - "uuid", -] - -[[package]] -name = "potential_utf" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "pretty_assertions" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" -dependencies = [ - "diff", - "yansi", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.106", -] - -[[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "proc-macro-error2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" -dependencies = [ - "proc-macro-error-attr2", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "proc-macro2" -version = "1.0.101" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "prost" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-derive" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" -dependencies = [ - "anyhow", - "itertools 0.14.0", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "psl-types" -version = "2.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" - -[[package]] -name = "publicsuffix" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" -dependencies = [ - "idna", - "psl-types", -] - -[[package]] -name = "quanta" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" -dependencies = [ - "crossbeam-utils", - "libc", - "once_cell", - "raw-cpuid", - "wasi 0.11.1+wasi-snapshot-preview1", - "web-sys", - "winapi", -] - -[[package]] -name = "quick-xml" -version = "0.38.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" -dependencies = [ - "memchr", -] - -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash 2.1.1", - "rustls 0.23.31", - "socket2 0.6.0", - "thiserror 2.0.16", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" -dependencies = [ - "bytes", - "getrandom 0.3.3", - "lru-slab", - "rand 0.9.2", - "ring 0.17.14", - "rustc-hash 2.1.1", - "rustls 0.23.31", - "rustls-pki-types", - "slab", - "thiserror 2.0.16", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2 0.6.0", - "tracing", - "windows-sys 0.60.2", -] - -[[package]] -name = "quote" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "rand" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" -dependencies = [ - "libc", - "rand 0.4.6", -] - -[[package]] -name = "rand" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" -dependencies = [ - "fuchsia-cprng", - "libc", - "rand_core 0.3.1", - "rdrand", - "winapi", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" -dependencies = [ - "rand_core 0.4.2", -] - -[[package]] -name = "rand_core" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.16", -] - -[[package]] -name = "rand_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.3", -] - -[[package]] -name = "ratatui" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" -dependencies = [ - "bitflags 2.9.3", - "cassowary", - "compact_str", - "crossterm", - "indoc", - "instability", - "itertools 0.13.0", - "lru 0.12.5", - "paste", - "strum 0.26.3", - "unicode-segmentation", - "unicode-truncate", - "unicode-width 0.2.0", -] - -[[package]] -name = "raw-cpuid" -version = "11.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6df7ab838ed27997ba19a4664507e6f82b41fe6e20be42929332156e5e85146" -dependencies = [ - "bitflags 2.9.3", -] - -[[package]] -name = "rdrand" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" -dependencies = [ - "rand_core 0.3.1", -] - -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.5.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" -dependencies = [ - "bitflags 2.9.3", -] - -[[package]] -name = "ref-cast" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "refinery" -version = "0.8.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba5d693abf62492c37268512ff35b77655d2e957ca53dab85bf993fe9172d15" -dependencies = [ - "refinery-core", - "refinery-macros", -] - -[[package]] -name = "refinery-core" -version = "0.8.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a83581f18c1a4c3a6ebd7a174bdc665f17f618d79f7edccb6a0ac67e660b319" -dependencies = [ - "async-trait", - "cfg-if", - "log", - "regex", - "serde", - "siphasher", - "thiserror 1.0.69", - "time 0.3.42", - "tokio", - "tokio-postgres", - "toml 0.8.23", - "url", - "walkdir", -] - -[[package]] -name = "refinery-macros" -version = "0.8.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c225407d8e52ef8cf094393781ecda9a99d6544ec28d90a6915751de259264" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "refinery-core", - "regex", - "syn 2.0.106", -] - -[[package]] -name = "regex" -version = "1.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-lite" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943f41321c63ef1c92fd763bfe054d2668f7f225a5c29f0105903dc2fc04ba30" - -[[package]] -name = "regex-syntax" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" - -[[package]] -name = "reqwest" -version = "0.12.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" -dependencies = [ - "async-compression", - "base64 0.22.1", - "bytes", - "cookie 0.18.1", - "cookie_store", - "encoding_rs", - "futures-channel", - "futures-core", - "futures-util", - "h2 0.4.12", - "http 1.3.1", - "http-body 1.0.1", - "http-body-util", - "hyper 1.7.0", - "hyper-rustls 0.27.7", - "hyper-tls", - "hyper-util", - "js-sys", - "log", - "mime", - "native-tls", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls 0.23.31", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-native-tls", - "tokio-rustls 0.26.2", - "tokio-util", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", - "webpki-roots 1.0.2", -] - -[[package]] -name = "reqwest-middleware" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57f17d28a6e6acfe1733fe24bcd30774d13bffa4b8a22535b4c8c98423088d4e" -dependencies = [ - "anyhow", - "async-trait", - "http 1.3.1", - "reqwest", - "serde", - "thiserror 1.0.69", - "tower-service", -] - -[[package]] -name = "reqwest-ratelimit" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b8fff0d8036f23dcad6c27605ca3baa8ae3867438d0a8b34072f40f6c8bf628" -dependencies = [ - "async-trait", - "http 1.3.1", - "reqwest", - "reqwest-middleware", -] - -[[package]] -name = "reqwest-retry" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c73e4195a6bfbcb174b790d9b3407ab90646976c55de58a6515da25d851178" -dependencies = [ - "anyhow", - "async-trait", - "futures", - "getrandom 0.2.16", - "http 1.3.1", - "hyper 1.7.0", - "parking_lot 0.11.2", - "reqwest", - "reqwest-middleware", - "retry-policies", - "thiserror 1.0.69", - "tokio", - "tracing", - "wasm-timer", -] - -[[package]] -name = "reqwest-websocket" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd5f79b25f7f17a62cc9337108974431a66ae5a723ac0d9fe78ac1cce2027720" -dependencies = [ - "async-tungstenite", - "bytes", - "futures-util", - "reqwest", - "serde", - "serde_json", - "thiserror 2.0.16", - "tokio", - "tokio-util", - "tracing", - "tungstenite", - "web-sys", -] - -[[package]] -name = "resize-slice" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a3cb2f74a9891e76958b9e0ccd269a25b466c3ae3bb3efd71db157248308c4a" -dependencies = [ - "uninitialized", -] - -[[package]] -name = "retry-policies" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5875471e6cab2871bc150ecb8c727db5113c9338cc3354dc5ee3425b6aa40a1c" -dependencies = [ - "rand 0.8.5", -] - -[[package]] -name = "rfc6979" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" -dependencies = [ - "crypto-bigint 0.4.9", - "hmac", - "zeroize", -] - -[[package]] -name = "ring" -version = "0.16.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin", - "untrusted 0.7.1", - "web-sys", - "winapi", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.16", - "libc", - "untrusted 0.9.0", - "windows-sys 0.52.0", -] - -[[package]] -name = "ron" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" -dependencies = [ - "base64 0.21.7", - "bitflags 2.9.3", - "serde", - "serde_derive", -] - -[[package]] -name = "rust-ini" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" -dependencies = [ - "cfg-if", - "ordered-multimap", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.9.3", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustix" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" -dependencies = [ - "bitflags 2.9.3", - "errno", - "libc", - "linux-raw-sys 0.9.4", - "windows-sys 0.60.2", -] - -[[package]] -name = "rustls" -version = "0.20.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" -dependencies = [ - "log", - "ring 0.16.20", - "sct", - "webpki", -] - -[[package]] -name = "rustls" -version = "0.21.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" -dependencies = [ - "log", - "ring 0.17.14", - "rustls-webpki 0.101.7", - "sct", -] - -[[package]] -name = "rustls" -version = "0.23.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" -dependencies = [ - "aws-lc-rs", - "once_cell", - "ring 0.17.14", - "rustls-pki-types", - "rustls-webpki 0.103.4", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-native-certs" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" -dependencies = [ - "openssl-probe", - "rustls-pemfile", - "schannel", - "security-framework 2.11.1", -] - -[[package]] -name = "rustls-native-certs" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework 3.3.0", -] - -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64 0.21.7", -] - -[[package]] -name = "rustls-pki-types" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.101.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring 0.17.14", - "untrusted 0.9.0", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" -dependencies = [ - "aws-lc-rs", - "ring 0.17.14", - "rustls-pki-types", - "untrusted 0.9.0", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schannel" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "schemars" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring 0.17.14", - "untrusted 0.9.0", -] - -[[package]] -name = "sec1" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" -dependencies = [ - "base16ct", - "der", - "generic-array", - "pkcs8", - "subtle", - "zeroize", -] - -[[package]] -name = "secrecy" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" -dependencies = [ - "serde", - "zeroize", -] - -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.9.3", - "core-foundation 0.9.4", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" -dependencies = [ - "bitflags 2.9.3", - "core-foundation 0.10.1", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "semver" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" - -[[package]] -name = "serde" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde-untagged" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34836a629bcbc6f1afdf0907a744870039b1e14c0561cb26094fa683b158eff3" -dependencies = [ - "erased-serde", - "serde", - "typeid", -] - -[[package]] -name = "serde_derive" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "serde_json" -version = "1.0.143" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_spanned" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_with" -version = "3.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" -dependencies = [ - "base64 0.22.1", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.11.0", - "schemars 0.9.0", - "schemars 1.0.4", - "serde", - "serde_derive", - "serde_json", - "serde_with_macros", - "time 0.3.42", -] - -[[package]] -name = "serde_with_macros" -version = "3.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" -dependencies = [ - "libc", - "signal-hook-registry", -] - -[[package]] -name = "signal-hook-mio" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" -dependencies = [ - "libc", - "mio", - "signal-hook", -] - -[[package]] -name = "signal-hook-registry" -version = "1.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" -dependencies = [ - "libc", -] - -[[package]] -name = "signature" -version = "1.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" -dependencies = [ - "digest", - "rand_core 0.6.4", -] - -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - -[[package]] -name = "simple_asn1" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" -dependencies = [ - "num-bigint", - "num-traits", - "thiserror 2.0.16", - "time 0.3.42", -] - -[[package]] -name = "siphasher" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" - -[[package]] -name = "size" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b6709c7b6754dca1311b3c73e79fcce40dd414c782c66d88e8823030093b02b" -dependencies = [ - "serde", -] - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "smawk" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" - -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "socket2" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - -[[package]] -name = "spinning_top" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" -dependencies = [ - "lock_api", -] - -[[package]] -name = "spki" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "stringprep" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" -dependencies = [ - "unicode-bidi", - "unicode-normalization", - "unicode-properties", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -dependencies = [ - "strum_macros 0.26.4", -] - -[[package]] -name = "strum" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" -dependencies = [ - "strum_macros 0.27.2", -] - -[[package]] -name = "strum_macros" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.106", -] - -[[package]] -name = "strum_macros" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "syntect" -version = "5.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1" -dependencies = [ - "bincode", - "bitflags 1.3.2", - "flate2", - "fnv", - "once_cell", - "onig", - "plist", - "regex-syntax", - "serde", - "serde_derive", - "serde_json", - "thiserror 1.0.69", - "walkdir", - "yaml-rust", -] - -[[package]] -name = "system-configuration" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" -dependencies = [ - "bitflags 2.9.3", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "tabled" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121d8171ee5687a4978d1b244f7d99c43e7385a272185a2f1e1fa4dc0979d444" -dependencies = [ - "papergrid", - "tabled_derive", -] - -[[package]] -name = "tabled_derive" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d9946811baad81710ec921809e2af67ad77719418673b2a3794932d57b7538" -dependencies = [ - "heck", - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "tanu" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa0a72002d2dd371f8e042c01cc132e90960960a9a032182f71a4f2bea9bbe66" -dependencies = [ - "anyhow", - "async-trait", - "clap", - "color-eyre", - "console", - "eyre", - "futures", - "itertools 0.13.0", - "log", - "num_cpus", - "once_cell", - "pretty_assertions", - "serde", - "strum 0.26.3", - "tanu-core", - "tanu-derive", - "tanu-tui", - "thiserror 1.0.69", - "tokio", - "toml 0.8.23", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "tanu-core" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ca865e4ede20d65cdcea4979a3a4670d2372b0b618bcfa950c25c69ba59fadd" -dependencies = [ - "anyhow", - "async-trait", - "backon", - "chrono", - "console", - "dotenv", - "eyre", - "futures", - "http 1.3.1", - "humantime-serde", - "indexmap 2.11.0", - "itertools 0.13.0", - "once_cell", - "pretty_assertions", - "reqwest", - "serde", - "serde_json", - "strum 0.26.3", - "tabled", - "tanu-derive", - "thiserror 2.0.16", - "tokio", - "toml 0.8.23", - "tracing", - "tracing-subscriber", - "url", -] - -[[package]] -name = "tanu-derive" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d4fa711d3072ea6af7088742ea448ab9fcfed315df126f878cba114d208fd39" -dependencies = [ - "eyre", - "itertools 0.13.0", - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.106", - "walkdir", -] - -[[package]] -name = "tanu-tui" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46ca2135608d3d5690f181a42293de7251e0b4385493d20c666d5e9fe66be79f" -dependencies = [ - "ansi-to-tui", - "async-trait", - "crossterm", - "dotenv", - "eyre", - "futures", - "http 1.3.1", - "itertools 0.13.0", - "log", - "memoize", - "once_cell", - "ratatui", - "serde_json", - "strum 0.26.3", - "syntect", - "tanu-core", - "textwrap", - "throbber-widgets-tui", - "tokio", - "tracing", - "tracing-log", - "tracing-subscriber", - "tui-big-text", - "tui-logger", -] - -[[package]] -name = "tempfile" -version = "3.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" -dependencies = [ - "fastrand", - "getrandom 0.3.3", - "once_cell", - "rustix 1.0.8", - "windows-sys 0.60.2", -] - -[[package]] -name = "tests" -version = "0.1.0" -dependencies = [ - "futures", - "hex", - "hulyrs", - "rand 0.9.2", - "reqwest", - "secrecy", - "serde_json", - "tanu", - "tokio", - "uuid", -] - -[[package]] -name = "textwrap" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" -dependencies = [ - "smawk", - "unicode-linebreak", - "unicode-width 0.2.0", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" -dependencies = [ - "thiserror-impl 2.0.16", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "throbber-widgets-tui" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d36b5738d666a2b4c91b7c24998a8588db724b3107258343ebf8824bf55b06d" -dependencies = [ - "rand 0.8.5", - "ratatui", -] - -[[package]] -name = "time" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - -[[package]] -name = "time" -version = "0.3.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca967379f9d8eb8058d86ed467d81d03e81acd45757e4ca341c24affbe8e8e3" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9108bb380861b07264b950ded55a44a14a4adc68b9f5efd85aafc3aa4d40a68" - -[[package]] -name = "time-macros" -version = "0.2.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7182799245a7264ce590b349d90338f1c1affad93d2639aed5f8f69c090b334c" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - -[[package]] -name = "tinystr" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.47.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" -dependencies = [ - "backtrace", - "bytes", - "io-uring", - "libc", - "mio", - "parking_lot 0.12.4", - "pin-project-lite", - "signal-hook-registry", - "slab", - "socket2 0.6.0", - "tokio-macros", - "windows-sys 0.59.0", -] - -[[package]] -name = "tokio-macros" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - -[[package]] -name = "tokio-postgres" -version = "0.7.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c95d533c83082bb6490e0189acaa0bbeef9084e60471b696ca6988cd0541fb0" -dependencies = [ - "async-trait", - "byteorder", - "bytes", - "fallible-iterator", - "futures-channel", - "futures-util", - "log", - "parking_lot 0.12.4", - "percent-encoding", - "phf", - "pin-project-lite", - "postgres-protocol", - "postgres-types", - "rand 0.9.2", - "socket2 0.5.10", - "tokio", - "tokio-util", - "whoami", -] - -[[package]] -name = "tokio-rustls" -version = "0.23.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" -dependencies = [ - "rustls 0.20.9", - "tokio", - "webpki", -] - -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls 0.21.12", - "tokio", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" -dependencies = [ - "rustls 0.23.31", - "tokio", -] - -[[package]] -name = "tokio-stream" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", - "tokio-util", -] - -[[package]] -name = "tokio-util" -version = "0.7.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" -dependencies = [ - "bytes", - "futures-core", - "futures-io", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio_with_wasm" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dfba9b946459940fb564dcf576631074cdfb0bfe4c962acd4c31f0dca7897e6" -dependencies = [ - "js-sys", - "tokio", - "tokio_with_wasm_proc", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "tokio_with_wasm_proc" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e04c1865c281139e5ccf633cb9f76ffdaabeebfe53b703984cf82878e2aabb" -dependencies = [ - "quote", - "syn 2.0.106", -] - -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_edit", -] - -[[package]] -name = "toml" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" -dependencies = [ - "serde", - "serde_spanned 1.0.0", - "toml_datetime 0.7.0", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_datetime" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap 2.11.0", - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_write", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" -dependencies = [ - "winnow", -] - -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - -[[package]] -name = "tonic" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e581ba15a835f4d9ea06c55ab1bd4dce26fc53752c69a04aac00703bfb49ba9" -dependencies = [ - "async-trait", - "base64 0.22.1", - "bytes", - "http 1.3.1", - "http-body 1.0.1", - "http-body-util", - "percent-encoding", - "pin-project", - "prost", - "tokio-stream", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" -dependencies = [ - "bitflags 2.9.3", - "bytes", - "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "iri-string", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-actix-web" -version = "0.7.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5360edd490ec8dee9fedfc6a9fd83ac2f01b3e1996e3261b9ad18a61971fe064" -dependencies = [ - "actix-web", - "mutually_exclusive_features", - "pin-project", - "tracing", - "uuid", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "tracing-core" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-error" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" -dependencies = [ - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-opentelemetry" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddcf5959f39507d0d04d6413119c04f33b623f4f951ebcbdddddfad2d0623a9c" -dependencies = [ - "js-sys", - "once_cell", - "opentelemetry", - "opentelemetry_sdk", - "smallvec", - "tracing", - "tracing-core", - "tracing-log", - "tracing-subscriber", - "web-time", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "tui-big-text" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97cefa9f1425ab6146db2961241cec86845d11105b5dd6bb504294b0cdd21af" -dependencies = [ - "derive_builder", - "font8x8", - "itertools 0.14.0", - "ratatui", -] - -[[package]] -name = "tui-logger" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f3fb54e48fa37d5081603f7804c730734b381235ebd876cb1e66f853a7d2533" -dependencies = [ - "chrono", - "fxhash", - "lazy_static", - "log", - "parking_lot 0.12.4", - "ratatui", - "tracing", - "tracing-subscriber", - "unicode-segmentation", -] - -[[package]] -name = "tungstenite" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d" -dependencies = [ - "bytes", - "data-encoding", - "http 1.3.1", - "httparse", - "log", - "rand 0.9.2", - "sha1", - "thiserror 2.0.16", - "utf-8", -] - -[[package]] -name = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - -[[package]] -name = "typenum" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" - -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - -[[package]] -name = "unicode-bidi" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" - -[[package]] -name = "unicode-ident" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" - -[[package]] -name = "unicode-linebreak" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" - -[[package]] -name = "unicode-normalization" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-properties" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" - -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "unicode-truncate" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" -dependencies = [ - "itertools 0.13.0", - "unicode-segmentation", - "unicode-width 0.1.14", -] - -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - -[[package]] -name = "unicode-width" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "uninitialized" -version = "0.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74c1aa4511c38276c548406f0b1f5f8b793f000cfb51e18f278a102abd057e81" - -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "uuid" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" -dependencies = [ - "getrandom 0.3.3", - "js-sys", - "serde", - "wasm-bindgen", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "vsimd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasi" -version = "0.14.3+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" - -[[package]] -name = "wasm-bindgen" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.106", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" -dependencies = [ - "cfg-if", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-streams" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "wasm-timer" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" -dependencies = [ - "futures", - "js-sys", - "parking_lot 0.11.2", - "pin-utils", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "wasmtimer" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8d49b5d6c64e8558d9b1b065014426f35c18de636895d24893dbbd329743446" -dependencies = [ - "futures", - "js-sys", - "parking_lot 0.12.4", - "pin-utils", - "slab", - "wasm-bindgen", -] - -[[package]] -name = "web-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" -dependencies = [ - "ring 0.17.14", - "untrusted 0.9.0", -] - -[[package]] -name = "webpki-roots" -version = "0.22.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" -dependencies = [ - "webpki", -] - -[[package]] -name = "webpki-roots" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.44", -] - -[[package]] -name = "whoami" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" -dependencies = [ - "libredox", - "wasite", - "web-sys", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" -dependencies = [ - "windows-sys 0.60.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.1.3", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "windows-interface" -version = "0.59.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-link" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" - -[[package]] -name = "windows-registry" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" -dependencies = [ - "windows-link 0.1.3", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.3", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" -dependencies = [ - "windows-link 0.1.3", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" - -[[package]] -name = "winnow" -version = "0.7.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814" - -[[package]] -name = "writeable" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" - -[[package]] -name = "xmlparser" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" - -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] - -[[package]] -name = "yaml-rust2" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ce2a4ff45552406d02501cea6c18d8a7e50228e7736a872951fe2fe75c91be7" -dependencies = [ - "arraydeque", - "encoding_rs", - "hashlink", -] - -[[package]] -name = "yansi" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" - -[[package]] -name = "yoke" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" - -[[package]] -name = "zerotrie" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.15+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/foundations/hulylake/Cargo.toml b/foundations/hulylake/Cargo.toml deleted file mode 100644 index 71b23b6b49..0000000000 --- a/foundations/hulylake/Cargo.toml +++ /dev/null @@ -1,3 +0,0 @@ -[workspace] -resolver = "3" -members = ["server", "tests"] diff --git a/foundations/hulylake/Dockerfile b/foundations/hulylake/Dockerfile deleted file mode 100644 index 0227824801..0000000000 --- a/foundations/hulylake/Dockerfile +++ /dev/null @@ -1,39 +0,0 @@ -FROM --platform=$BUILDPLATFORM rust:1.88 AS builder -ARG TARGETPLATFORM - -WORKDIR /tmp/build - -COPY . . - -RUN echo build for $TARGETPLATFORM -RUN \ - if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ - apt-get update && apt-get install -y cmake \ - && rm -rf /var/lib/apt/lists/* ; \ - cargo build --release --target=x86_64-unknown-linux-gnu --package hulylake; \ - elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \ - apt-get update && apt-get install -y \ - gcc-aarch64-linux-gnu \ - g++-aarch64-linux-gnu \ - libc6-dev-arm64-cross \ - cmake \ - && rm -rf /var/lib/apt/lists/* ; \ - rustup target add aarch64-unknown-linux-gnu ; \ - export CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc ; \ - export CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++ ; \ - export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc ; \ - cargo tree --target=aarch64-unknown-linux-gnu ; \ - cargo build --release --target=aarch64-unknown-linux-gnu --package hulylake ; \ - else \ - echo "Unexpected target platform: $TARGETPLATFORM" && exit 1 ; \ - fi - -RUN cargo test - -FROM debian:12-slim - -ARG TARGET -COPY --from=builder /tmp/build/target/*/release/hulylake /usr/local/bin/hulylake -RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* - -ENTRYPOINT ["/usr/local/bin/hulylake"] diff --git a/foundations/hulylake/Justfile b/foundations/hulylake/Justfile deleted file mode 100644 index 8648ef0693..0000000000 --- a/foundations/hulylake/Justfile +++ /dev/null @@ -1,2 +0,0 @@ -image platform='linux/amd64': - cd server && docker buildx build --tag=hardcoreeng/hulylake:latest --platform={{platform}} --load . \ No newline at end of file diff --git a/foundations/hulylake/server/Cargo.toml b/foundations/hulylake/server/Cargo.toml deleted file mode 100644 index ef9fbaa554..0000000000 --- a/foundations/hulylake/server/Cargo.toml +++ /dev/null @@ -1,49 +0,0 @@ -[package] -name = "hulylake" -version = "0.1.18" -edition = "2024" -rust-version = "1.88.0" - -[dependencies] -tokio = { version = "1", features = ["full"] } -tracing = "0.1.41" -tracing-subscriber = "0.3.19" -anyhow = "1.0.99" -config = "0.15.14" -serde = "1.0.219" -actix-web = "4.11.0" -actix-cors = "0.7.1" -refinery = { version = "0.8.16", features = ["tokio-postgres"] } -tokio-postgres = { version = "0.7.13", features = [ - "with-uuid-1", - "with-serde_json-1", - "with-chrono-0_4", -] } -bb8 = "0.9.0" -bb8-postgres = { version = "0.9.0", features = ["with-uuid-1"] } -md5 = "0.8.0" -size = { version = "0.5.0", features = ["serde"] } -uuid = { version = "1.18", features = ["v4", "serde"] } -serde_json = "1.0" -hulyrs = { git = "https://github.com/hcengineering/hulyrs.git", features = ["actix", "otel"] } -secrecy = "0.10.3" -tracing-actix-web = "0.7.19" -aws-config = { version = "1.8.5" } -aws-sdk-s3 = "1.103.0" -futures-util = "0.3.31" -tokio-stream = "0.1.17" -thiserror = "2.0.16" -bytes = "1.10.1" -ksuid = "0.2.0" -tracing-opentelemetry = "0.31.0" -mime = "0.3.17" -async-stream = "0.3.6" -blake3 = "1.8.2" -futures = "0.3.31" -strum = { version = "0.27.2", features = ["derive"] } -json-patch = "4.0.0" -jsonptr = "0.7.1" -chrono = { version = "0.4.42", features = ["now"] } -opentelemetry = "0.30.0" -opentelemetry-appender-tracing = "0.30.1" -lockable = "0.2.0" diff --git a/foundations/hulylake/server/Justfile b/foundations/hulylake/server/Justfile deleted file mode 100644 index 15818dcee4..0000000000 --- a/foundations/hulylake/server/Justfile +++ /dev/null @@ -1,11 +0,0 @@ -workspace := "4cd5a9d5-7c74-47b1-ac93-265f0bcc73af" -key := "abcd" -headers := '-H "Content-Type:" -H "huly-header-x-test: test" -H "huly-meta-x-test: test"' -token := `cat _hidden/token.txt` - - -put: - curl -X PUT -i -H "Authorization: Bearer {{token}}" -H @_hidden/headers.txt --data-binary @_hidden/data_large.bin http://localhost:8096/api/{{workspace}}/{{key}} - -get: - curl -X GET -s -D - -H "Authorization: Bearer {{token}}" -H @_hidden/headers.txt -o _hidden/data_output.bin http://localhost:8096/api/{{workspace}}/{{key}} diff --git a/foundations/hulylake/server/etc/migrations/V1__initial.sql b/foundations/hulylake/server/etc/migrations/V1__initial.sql deleted file mode 100644 index cd4f8c1f73..0000000000 --- a/foundations/hulylake/server/etc/migrations/V1__initial.sql +++ /dev/null @@ -1,18 +0,0 @@ -create table blob( - key text not null, - hash text not null -); - -create unique index blob_key on blob(key); -create unique index blob_hash on blob(hash); - - -create table object( - workspace uuid not null, - key text not null, - part int4 not null, - data jsonb not null, - inline bytea, - - primary key (workspace, key, part) -) \ No newline at end of file diff --git a/foundations/hulylake/server/src/blob.rs b/foundations/hulylake/server/src/blob.rs deleted file mode 100644 index 772377dd8e..0000000000 --- a/foundations/hulylake/server/src/blob.rs +++ /dev/null @@ -1,198 +0,0 @@ -use std::error::Error as StdError; - -use aws_sdk_s3::primitives::ByteStream; -use blake3::Hasher; -use bytes::{Bytes, BytesMut}; -use futures::stream::StreamExt; -use futures_util::Stream; -use size::Size; -use tokio_postgres::error::SqlState; -use tracing::*; - -use crate::handlers::ApiError; -use crate::postgres::DbError; -use crate::recovery; -use crate::s3::S3Client; -use crate::{ - config::CONFIG, - postgres::{self, Pool}, -}; - -#[derive(Debug)] -pub struct Blob { - pub s3_key: String, - pub hash: String, - pub length: usize, - pub inline: Option, - pub parts_count: Option, - pub deduplicated: bool, -} - -fn random_key() -> String { - ksuid::Ksuid::generate().to_base62() -} - -#[instrument(level = "debug", skip_all, fields(s3_bucket))] -pub async fn upload( - s3: &S3Client, - pool: &Pool, - length: Size, - mut source: S, -) -> Result -where - S: Stream> + Unpin, - E: StdError + Send + Sync + 'static, -{ - let span = Span::current(); - - let s3_bucket = &CONFIG.s3_bucket; - span.record("s3_bucket", &s3_bucket); - - let blob = if length < CONFIG.multipart_threshold { - let mut hash = Hasher::new(); - - let mut buffer = BytesMut::new(); - - // read in all chunks, but not more that length - while let Some(Ok(chunk)) = source.next().await { - buffer.extend_from_slice(&chunk); - - if buffer.len() > length.bytes() as usize { - return Err(actix_web::error::ErrorPayloadTooLarge("payload too large").into()); - } - } - - if buffer.len() != length.bytes() as usize { - return Err(actix_web::error::ErrorBadRequest("payload size mismatch").into()); - } - - let buffer = buffer.freeze(); - - let hash = hash.update(&buffer).finalize().to_hex().to_string(); - let length = buffer.len(); - - let inline = Some(buffer.clone()); - - let (s3_key, deduplicated) = - if let Some(s3_key_found) = postgres::find_blob_by_hash(&pool, &hash).await? { - span.record("s3_key", &s3_key_found); - debug!(s3_key_found, "blob deduplicated"); - (s3_key_found, true) - } else { - let s3_key = random_key(); - span.record("s3_key", &s3_key); - - s3.put_object() - .bucket(s3_bucket) - .key(&s3_key) - .body(ByteStream::from(buffer)) - .send() - .await?; - - match make_blob(pool, &s3_key, &hash).await? { - Some(s3_key_found) => { - debug!(s3_key_found, "blob deduplicated"); - - // delete uploaded - s3.delete_object() - .bucket(s3_bucket) - .key(&s3_key) - .send() - .await?; - - (s3_key_found, true) - } - None => { - debug!("blob created"); - (s3_key, false) - } - } - }; - - Blob { - hash, - s3_key, - length, - inline, - parts_count: None, - deduplicated, - } - } else { - let s3_key = random_key(); - span.record("s3_key", &s3_key); - - let upload = crate::s3::multipart_upload(&s3, &s3_bucket, &s3_key, source).await?; - - let hash = upload.hash.to_hex().to_string(); - - let (s3_key, deduplicated) = match postgres::find_blob_by_hash(&pool, &hash).await? { - Some(s3_key_found) => { - debug!(s3_key_found, "blob deduplicated"); - (s3_key_found, true) - } - None => { - match make_blob(pool, &s3_key, &hash).await? { - Some(s3_key_found) => { - debug!(s3_key_found, "blob deduplicated"); - - // delete uploaded - s3.delete_object() - .bucket(s3_bucket) - .key(&s3_key) - .send() - .await?; - - (s3_key_found, true) - } - None => { - debug!("blob created"); - (s3_key, false) - } - } - } - }; - - Blob { - hash, - s3_key, - length: upload.length, - inline: None, - parts_count: Some(upload.parts_count), - deduplicated, - } - }; - - if !blob.deduplicated { - recovery::set_blob(&s3, &blob.s3_key, &blob.hash).await?; - } - - Ok(blob) -} - -async fn make_blob(pool: &Pool, s3_key: &String, hash: &str) -> Result, DbError> { - let mut retries = 3; - - loop { - match postgres::insert_blob(&pool, &s3_key, &hash).await { - Ok(_) => break Ok(None), - Err(e) => { - if matches!(e, DbError::Db(ref db_err) if db_err.code() == Some(&SqlState::UNIQUE_VIOLATION)) - { - debug!("concurrent upload detected"); - - if let Some(s3_key_found) = postgres::find_blob_by_hash(&pool, &hash).await? { - break Ok(Some(s3_key_found)); - } - - if retries > 0 { - retries -= 1; - tokio::time::sleep(std::time::Duration::from_millis(50)).await; - continue; - } - } - - break Err(e); - } - }; - } -} diff --git a/foundations/hulylake/server/src/compact.rs b/foundations/hulylake/server/src/compact.rs deleted file mode 100644 index ec4bb5c461..0000000000 --- a/foundations/hulylake/server/src/compact.rs +++ /dev/null @@ -1,187 +0,0 @@ -use std::collections::HashSet; -use std::sync::Arc; - -use size::Size; -use tokio::sync::{RwLock, mpsc}; -use tracing::*; -use uuid::Uuid; - -use crate::config::CONFIG; -use crate::handlers::{ApiError, PartData}; -use crate::merge; -use crate::mutex::KeyMutex; -use crate::postgres::{ObjectPart, Pool}; -use crate::s3::S3Client; -use crate::{blob, postgres, recovery}; - -#[derive(Debug, Clone, Hash, Eq, PartialEq)] -pub struct CompactTask { - pub workspace: Uuid, - pub key: String, -} - -pub struct CompactWorker { - ingest_tx: mpsc::Sender, - ingest_handle: tokio::task::JoinHandle<()>, - compact_handle: tokio::task::JoinHandle<()>, -} - -impl CompactWorker { - pub fn new(s3: Arc, pool: Pool, lock: KeyMutex, buffer_size: usize) -> Self { - let (ingest_tx, ingest_rx) = mpsc::channel(buffer_size); - let (compact_tx, compact_rx) = mpsc::channel(buffer_size); - - let pending_tasks = Arc::new(RwLock::new(HashSet::new())); - let pending_tasks_ingest = pending_tasks.clone(); - let pending_tasks_compact = pending_tasks.clone(); - - let ingest_handle = tokio::spawn(async move { - debug!(buffer_size, "started ingest worker"); - Self::run_ingest_worker(ingest_rx, compact_tx, pending_tasks_ingest).await - }); - - let compact_handle = tokio::spawn(async move { - debug!(buffer_size, "started compact worker"); - Self::run_compact_worker( - compact_rx, - s3.clone(), - pool, - lock.clone(), - pending_tasks_compact, - ) - .await; - }); - - Self { - ingest_tx, - ingest_handle, - compact_handle, - } - } - - async fn run_ingest_worker( - mut ingest_rx: mpsc::Receiver, - compact_tx: mpsc::Sender, - pending_tasks: Arc>>, - ) { - loop { - while let Some(task) = ingest_rx.recv().await { - let is_new = pending_tasks.write().await.insert(task.clone()); - if !is_new { - continue; - } - - if let Err(err) = compact_tx.send(task.clone()).await { - error!(%err, "failed to send compact task"); - pending_tasks.write().await.remove(&task); - } - } - } - } - - async fn run_compact_worker( - mut rx: mpsc::Receiver, - s3: Arc, - pool: Pool, - lock: KeyMutex, - pending_tasks: Arc>>, - ) { - loop { - while let Some(task) = rx.recv().await { - let CompactTask { workspace, key } = task.clone(); - - let _guard = lock.lock(workspace, key).await; - - pending_tasks.write().await.remove(&task); - - let res = compact(s3.clone(), pool.clone(), task.clone()).await; - match res { - Ok(_) => debug!(workspace = %task.workspace, key = %task.key, "blob compacted"), - Err(err) => error!(%err, "failed to compact"), - } - } - } - } - - pub async fn try_send(&self, parts: &Vec>) -> bool { - if parts.len() >= CONFIG.compact_parts_limit { - let task = CompactTask { - workspace: parts[0].data.workspace, - key: parts[0].data.key.clone(), - }; - self.send(task).await - } else { - false - } - } - - pub async fn send(&self, task: CompactTask) -> bool { - let res = self.ingest_tx.send(task).await; - match res { - Ok(_) => true, - Err(err) => { - warn!(%err, "failed to schedule compact"); - false - } - } - } - - pub async fn stop(&self) { - self.ingest_handle.abort(); - self.compact_handle.abort(); - } -} - -#[instrument(level = "debug", skip_all, fields(workspace, huly_key))] -async fn compact(s3: Arc, pool: Pool, task: CompactTask) -> anyhow::Result<(), ApiError> { - let pool = pool.clone(); - - let workspace = task.workspace; - let key = task.key; - - Span::current() - .record("workspace", workspace.to_string()) - .record("huly_key", &key); - - let parts = postgres::find_parts(&pool, task.workspace, &key).await?; - let first = &parts.first().unwrap().data; - let last = &parts.last().unwrap().data; - - let stream = merge::stream(s3.clone(), parts.to_vec()).await?; - - let uploaded = blob::upload( - &s3, - &pool, - Size::from_bytes(stream.content_length), - stream.stream, - ) - .await?; - - let inline = uploaded.inline.and_then(|inline| { - if inline.len() < CONFIG.inline_threshold.bytes() as usize { - Some(inline) - } else { - None - } - }); - - let part_data = PartData { - workspace, - key: key.to_owned(), - part: 0, - blob: uploaded.s3_key, - size: uploaded.length, - etag: last.etag.clone(), - date: last.date.clone(), - - headers: first.headers.clone(), - meta: first.meta.clone(), - merge_strategy: first.merge_strategy, - }; - let obj_parts = vec![&part_data]; - - postgres::set_part(&pool, workspace, &key, inline, &part_data).await?; - recovery::set_object(&s3, workspace, &key, obj_parts, None).await?; - - Ok(()) -} diff --git a/foundations/hulylake/server/src/conditional.rs b/foundations/hulylake/server/src/conditional.rs deleted file mode 100644 index 0f73e446e4..0000000000 --- a/foundations/hulylake/server/src/conditional.rs +++ /dev/null @@ -1,167 +0,0 @@ -use actix_web::HttpRequest; -use actix_web::http::header::EntityTag; -use actix_web::http::header::Header; -use actix_web::http::header::IfMatch; -use actix_web::http::header::IfNoneMatch; - -#[derive(thiserror::Error, Debug, PartialEq, Eq)] -pub enum ConditionalError { - #[error("Invalid header")] - ParseError, -} - -#[derive(Debug, PartialEq, Eq)] -pub enum ConditionalMatch { - IfMatch(String), - IfNoneMatch(String), -} - -pub fn any_match( - req: &HttpRequest, - etag: Option, -) -> Result, ConditionalError> { - match IfMatch::parse(req) { - Ok(IfMatch::Any) => Ok(Some(etag.is_some())), - Ok(IfMatch::Items(items)) => Ok({ - if items.is_empty() { - None - } else { - Some(etag.map_or(false, |e| items.contains(&e))) - } - }), - Err(_) => Err(ConditionalError::ParseError), - } -} - -pub fn none_match( - req: &HttpRequest, - etag: Option, -) -> Result, ConditionalError> { - match IfNoneMatch::parse(req) { - Ok(IfNoneMatch::Any) => Ok(Some(etag.is_none())), - Ok(IfNoneMatch::Items(items)) => Ok({ - if items.is_empty() { - None - } else { - Some(etag.map_or(true, |e| !items.contains(&e))) - } - }), - Err(_) => Err(ConditionalError::ParseError), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use actix_web::test::TestRequest; - - #[test] - fn test_any_match_none_some() { - assert_eq!( - any_match( - &TestRequest::default().to_http_request(), - Some(EntityTag::new_strong("foo".to_owned())) - ), - Ok(None) - ); - } - - #[test] - fn test_any_match_any_some() { - assert_eq!( - any_match( - &TestRequest::default() - .insert_header(("If-Match", "*")) - .to_http_request(), - Some(EntityTag::new_strong("foo".to_owned())) - ), - Ok(Some(true)) - ); - } - - #[test] - fn test_any_match_some_some() { - assert_eq!( - any_match( - &TestRequest::default() - .insert_header(("If-Match", "\"foo\" ,\"bar\"")) - .to_http_request(), - Some(EntityTag::new_strong("foo".to_owned())) - ), - Ok(Some(true)) - ); - } - - #[test] - fn test_none_match_none_none() { - assert_eq!( - none_match(&TestRequest::default().to_http_request(), None), - Ok(None) - ); - } - - #[test] - fn test_none_match_any_some() { - assert_eq!( - none_match( - &TestRequest::default() - .insert_header(("If-None-Match", "*")) - .to_http_request(), - Some(EntityTag::new_strong("foo".to_owned())) - ), - Ok(Some(false)) - ); - } - - #[test] - fn test_none_match_any_none() { - assert_eq!( - none_match( - &TestRequest::default() - .insert_header(("If-None-Match", "*")) - .to_http_request(), - None - ), - Ok(Some(true)) - ); - } - - #[test] - fn test_none_match_some_some() { - assert_eq!( - none_match( - &TestRequest::default() - .insert_header(("If-None-Match", "\"foo\"")) - .to_http_request(), - Some(EntityTag::new_strong("foo".to_owned())) - ), - Ok(Some(false)) - ); - } - - #[test] - fn test_none_match_some_none() { - assert_eq!( - none_match( - &TestRequest::default() - .insert_header(("If-None-Match", "\"foo\"")) - .to_http_request(), - None - ), - Ok(Some(true)) - ); - } - - #[test] - fn test_none_match_some_unknown() { - assert_eq!( - none_match( - &TestRequest::default() - .insert_header(("If-None-Match", "\"foo\"")) - .to_http_request(), - Some(EntityTag::new_strong("bar".to_owned())) - ), - Ok(Some(true)) - ); - } -} diff --git a/foundations/hulylake/server/src/config.rs b/foundations/hulylake/server/src/config.rs deleted file mode 100644 index 1eee9f3886..0000000000 --- a/foundations/hulylake/server/src/config.rs +++ /dev/null @@ -1,101 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -use std::{path::Path, sync::LazyLock}; - -use config::FileFormat; -use secrecy::SecretString; -use serde::Deserialize; -use size::Size; - -#[derive(Deserialize, Debug)] -pub struct Config { - pub bind_port: u16, - pub bind_host: String, - - pub token_secret: SecretString, - - pub db_connection: String, - pub db_scheme: String, - - pub s3_bucket: String, - - // use multipart upload if blob size is greater than this - pub multipart_threshold: Size, - - // store blobs inline if size is less than this - pub inline_threshold: Size, - - pub cache_control: String, - - pub compact_parts_limit: usize, - pub compact_buffer_size: usize, -} - -pub mod hulyrs { - use std::sync::LazyLock; - - pub static CONFIG: LazyLock = LazyLock::new(|| match hulyrs::Config::auto() { - Ok(config) => config, - Err(error) => { - eprintln!("configuration error: {}", error); - std::process::exit(1); - } - }); -} - -pub static CONFIG: LazyLock = LazyLock::new(|| { - const DEFAULTS: &str = r#" - bind_port = 8096 - bind_host = "0.0.0.0" - - token_secret = "secret" - - db_connection = "postgresql://root@huly.local:26257/defaultdb?sslmode=disable" - db_scheme = "hulylake" - - s3_bucket = "hulylake" - - multipart_threshold = "4MB" - inline_threshold = "100KB" - - cache_control = "public, no-cache" - - compact_parts_limit = 100 - compact_buffer_size = 1000 - "#; - - let mut builder = - config::Config::builder().add_source(config::File::from_str(DEFAULTS, FileFormat::Toml)); - - let path = Path::new("etc/config.toml"); - - if path.exists() { - builder = builder.add_source(config::File::with_name(path.as_os_str().to_str().unwrap())); - } - - let settings = builder - .add_source(config::Environment::with_prefix("HULY")) - .build() - .and_then(|c| c.try_deserialize::()); - - match settings { - Ok(settings) => settings, - Err(error) => { - eprintln!("configuration error: {}", error); - std::process::exit(1); - } - } -}); diff --git a/foundations/hulylake/server/src/handlers.rs b/foundations/hulylake/server/src/handlers.rs deleted file mode 100644 index 207a58116e..0000000000 --- a/foundations/hulylake/server/src/handlers.rs +++ /dev/null @@ -1,772 +0,0 @@ -use std::{collections::HashMap, fmt::Display, io, str::FromStr, time::SystemTime}; - -use actix_web::{ - HttpRequest, HttpResponse, - body::SizedStream, - dev::ServiceRequest, - http::{ - self, StatusCode, - header::{self, ContentLength, ContentType, EntityTag, HttpDate, Range}, - }, - web::{Data, Header, Path, Payload}, -}; -use aws_sdk_s3::error::SdkError; -use chrono::{DateTime, Utc}; -use futures::{StreamExt, stream}; -use serde::{Deserialize, Serialize}; -use size::Size; -use tracing::*; -use uuid::Uuid; - -use crate::s3::S3Client; -use crate::{ - blob, - conditional::{ConditionalMatch, any_match, none_match}, - merge, - postgres::ObjectPart, -}; -use crate::{compact::CompactWorker, conditional}; -use crate::{ - config::CONFIG, - postgres::{self, Pool}, -}; -use crate::{merge::MergeStrategy, recovery}; - -#[derive(Deserialize, Debug)] -pub struct ObjectPath { - pub workspace: Uuid, - pub key: String, -} - -#[derive(thiserror::Error, Debug)] -pub enum ApiError { - #[error("S3 Error: {0}")] - S3(String), - - #[error(transparent)] - Db(#[from] postgres::DbError), - - #[error(transparent)] - ActixError(#[from] actix_web::error::Error), - - #[error(transparent)] - ActixParseError(#[from] actix_web::error::ParseError), - - #[error(transparent)] - ConditionalError(#[from] conditional::ConditionalError), - - #[error("Precondition Failed")] - PreconditionFailed, - - #[error(transparent)] - Other(#[from] anyhow::Error), -} - -pub type HandlerResult = Result; - -impl actix_web::error::ResponseError for ApiError { - fn error_response(&self) -> HttpResponse { - match self { - ApiError::ActixError(error) => error.error_response(), - - ApiError::ConditionalError(_) => HttpResponse::BadRequest().body("Bad Request"), - - ApiError::PreconditionFailed => { - HttpResponse::PreconditionFailed().body("Precondition Failed") - } - - _ => { - tracing::error!(error=?self, "Internal error in http handler"); - HttpResponse::InternalServerError().body("Internal Server Error") - } - } - } -} - -impl From for ApiError { - fn from(error: recovery::RecoveryError) -> Self { - match error { - recovery::RecoveryError::S3(err) => ApiError::S3(err), - recovery::RecoveryError::PreconditionFailed => ApiError::PreconditionFailed, - recovery::RecoveryError::Other(err) => ApiError::Other(err), - } - } -} - -impl From> - for ApiError -{ - fn from(error: SdkError) -> Self { - error.raw_response(); - - ApiError::S3(format!("{} {:#?}", error, error.raw_response())) - } -} - -fn random_etag() -> String { - ksuid::Ksuid::generate().to_base62() -} - -#[derive(Debug, Clone)] -pub struct Headers { - pub content_length: Size, - pub content_type: Option, - pub huly_headers: Vec<(String, String)>, - pub meta: Vec<(String, String)>, -} - -async fn extract_headers(request: &mut ServiceRequest) -> HandlerResult<(Headers, MergeStrategy)> { - let content_length = request - .extract::>() - .await - .map(|header| Size::from_bytes(*header.0)) - .map_err(|_| actix_web::error::ErrorBadRequest("invalid content length"))?; - - let content_type = request - .extract::>() - .await - .map(|header| header.0.to_string()) - .ok(); - - let merge_strategy = request - .headers() - .get("Huly-Merge-Strategy") - .and_then(|v| v.to_str().ok()) - .map(|v| { - MergeStrategy::from_str(v).map_err(|_| { - actix_web::error::ErrorBadRequest(format!("invalid merge strategy: {v}")) - }) - }) - .transpose()? - .unwrap_or_default(); - - let mut huly_headers = Vec::new(); - for (key, value) in request.headers().iter() { - if let Some(header) = key.as_str().strip_prefix("huly-header-") { - if let Ok(value) = value.to_str() { - huly_headers.push((header.to_owned(), value.to_owned())); - } - } - } - if let Some(content_type) = &content_type { - huly_headers.push(( - http::header::CONTENT_TYPE.as_str().to_owned(), - content_type.to_owned(), - )); - } - - let mut meta = Vec::new(); - for (key, value) in request.headers().iter() { - if let Some(header) = key.as_str().strip_prefix("huly-meta-") { - if let Ok(value) = value.to_str() { - meta.push((header.to_owned(), value.to_owned())); - } - } - } - - meta.push(( - "merge-strategy".to_owned(), - serde_json::to_string(&merge_strategy).unwrap(), - )); - - Ok(( - Headers { - content_length, - content_type, - huly_headers, - meta, - }, - merge_strategy, - )) -} - -async fn extract_range_header(request: &mut ServiceRequest) -> Option { - request - .extract::>() - .await - .map(|header| header.0.to_string()) - .ok() -} -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct PartData { - pub workspace: Uuid, - pub key: String, - pub part: u32, - pub size: usize, - pub blob: String, - pub etag: String, - - #[serde(default)] - pub date: DateTime, - - #[serde(skip_serializing_if = "Option::is_none")] - pub headers: Option>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub meta: Option>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub merge_strategy: Option, -} - -#[instrument(level = "debug", skip_all, fields(workspace, huly_key))] -pub async fn put(request: HttpRequest, payload: Payload) -> HandlerResult { - let span = Span::current(); - debug!("put request"); - - let mut request = ServiceRequest::from_request(request); - let path = request.extract::>().await?.into_inner(); - let (headers, merge_strategy) = extract_headers(&mut request).await?; - - merge::validate_put_request(merge_strategy, &headers)?; - - span.record("workspace", path.workspace.to_string()); - span.record("huly_key", &path.key); - - let pool = request.app_data::>().unwrap().to_owned(); - let s3 = request.app_data::>().unwrap().to_owned(); - - let parts = postgres::find_parts::(&pool, path.workspace, &path.key).await?; - - let conditionals = validate_put_conditionals(request.request(), &parts)?; - - let uploaded = blob::upload(&s3, &pool, headers.content_length, payload).await?; - - merge::validate_put_body(merge_strategy, &uploaded)?; - - let part_data = PartData { - workspace: path.workspace, - key: path.key, - part: 0, - blob: uploaded.s3_key, - size: uploaded.length, - etag: random_etag(), - date: chrono::Utc::now(), - - headers: Some(headers.huly_headers.clone().into_iter().collect()), - meta: Some(headers.meta.into_iter().collect()), - merge_strategy: Some(merge_strategy), - }; - - let inline = uploaded.inline.and_then(|inline| { - if inline.len() < CONFIG.inline_threshold.bytes() as usize { - Some(inline) - } else { - None - } - }); - - let obj_parts = vec![&part_data]; - - recovery::set_object(&s3, path.workspace, &part_data.key, obj_parts, conditionals).await?; - - postgres::set_part(&pool, path.workspace, &part_data.key, inline, &part_data).await?; - - let mut response = HttpResponse::Created(); - response.insert_header((header::ETAG, part_data.etag)); - - if uploaded.deduplicated { - response.insert_header(("Huly-Deduplicated", "true")); - } else { - if let Some(parts_count) = uploaded.parts_count { - response.insert_header(("Huly-S3-Parts-Count", parts_count.to_string())); - } - } - - for (key, value) in headers.huly_headers { - response.insert_header((key.as_str(), value.to_owned())); - } - - Ok(response.finish()) -} - -#[instrument(level = "debug", skip_all, fields(workspace, huly_key))] -pub async fn patch(request: HttpRequest, payload: Payload) -> HandlerResult { - let span = Span::current(); - - let mut request = ServiceRequest::from_request(request); - - let path = request.extract::>().await?.into_inner(); - let (headers, _) = extract_headers(&mut request).await?; - - span.record("workspace", path.workspace.to_string()); - span.record("huly_key", &path.key); - - let pool = request.app_data::>().unwrap().to_owned(); - let s3 = request.app_data::>().unwrap().to_owned(); - - let parts = postgres::find_parts::(&pool, path.workspace, &path.key).await?; - - let mut response = if !parts.is_empty() { - let conditionals = validate_patch_conditionals(request.request(), &parts)?; - - let merge_strategy = objectpart_strategy(&parts).unwrap(); - - merge::validate_patch_request(merge_strategy, &headers)?; - - let uploaded = blob::upload(&s3, &pool, headers.content_length, payload).await?; - - merge::validate_patch_body(merge_strategy, &uploaded)?; - - let part = parts - .iter() - .map(|p| p.data.part) - .reduce(u32::max) - .map(|m| m + 1) - .unwrap_or(0); - - let part_data = PartData { - workspace: path.workspace, - key: path.key, - part, - blob: uploaded.s3_key, - size: uploaded.length, - etag: random_etag(), - date: chrono::Utc::now(), - - // defined in the first part - headers: None, - meta: None, - merge_strategy: None, - }; - - let obj_parts = parts - .iter() - .map(|p| &p.data) - .chain(std::iter::once(&part_data)) - .collect::>(); - - recovery::set_object(&s3, path.workspace, &part_data.key, obj_parts, conditionals).await?; - - postgres::append_part( - &pool, - path.workspace, - &part_data.key, - part_data.part, - uploaded.inline, - &part_data, - ) - .await?; - - let mut response = HttpResponse::Created(); - - if uploaded.deduplicated { - response.insert_header(("Huly-Deduplicated", "true")); - } else { - if let Some(parts_count) = uploaded.parts_count { - response.insert_header(("Huly-S3-Parts-Count", parts_count.to_string())); - } - } - - response - } else { - HttpResponse::NotFound() - }; - - Ok(response.finish()) -} - -#[instrument(level = "debug", skip_all, fields(workspace, huly_key))] -pub async fn get(request: HttpRequest) -> HandlerResult { - let span = Span::current(); - - let mut request = ServiceRequest::from_request(request); - - let path = request.extract::>().await?.into_inner(); - - span.record("workspace", path.workspace.to_string()); - span.record("huly_key", &path.key); - - let pool = request.app_data::>().unwrap().to_owned(); - - let parts = postgres::find_parts::(&pool, path.workspace, &path.key).await?; - - let response = if !parts.is_empty() { - let etag = objectpart_etag(&parts).unwrap(); - let date = objectpart_date(&parts).unwrap(); - - let range = extract_range_header(&mut request).await; - - match none_match(request.request(), Some(etag.clone()))? { - Some(false) => HttpResponse::NotModified() - .insert_header((header::ETAG, etag)) - .insert_header((header::LAST_MODIFIED, HttpDate::from(date))) - .insert_header((header::CACHE_CONTROL, CONFIG.cache_control.clone())) - .finish(), - _ => { - let mut response = HttpResponse::Ok(); - - let s3 = request - .app_data::>() - .unwrap() - .to_owned() - .into_inner(); - - let headers = parts[0].data.headers.as_ref(); - if let Some(headers) = headers { - for (header, value) in headers.iter() { - response.insert_header((header.as_str(), value.to_owned())); - } - } - - let accept_ranges = objectpart_accept_ranges(&parts); - if let Some(accept_ranges) = accept_ranges { - response.insert_header((header::ACCEPT_RANGES, accept_ranges)); - } - - response.insert_header((header::ETAG, etag)); - response.insert_header((header::LAST_MODIFIED, HttpDate::from(date))); - response.insert_header((header::CACHE_CONTROL, CONFIG.cache_control.clone())); - - match range { - Some(range) => { - let partial = merge::partial(s3, parts, range).await?; - - if partial.partial { - response.status(StatusCode::PARTIAL_CONTENT); - } - - if let Some(content_range) = partial.content_range { - response.insert_header((header::CONTENT_RANGE, content_range)); - } - - response.body(SizedStream::new(partial.content_length, partial.stream)) - } - None => { - let compact = request.app_data::>().unwrap(); - compact.try_send(&parts).await; - - let stream = merge::stream(s3.clone(), parts).await?; - response.body(SizedStream::new(stream.content_length, stream.stream)) - } - } - } - } - } else { - HttpResponse::NotFound().finish() - }; - - Ok(response) -} - -#[instrument(level = "debug", skip_all, fields(workspace, huly_key))] -pub async fn head(request: HttpRequest) -> HandlerResult { - let span = Span::current(); - - let mut request = ServiceRequest::from_request(request); - - let path = request.extract::>().await?.into_inner(); - - span.record("workspace", path.workspace.to_string()); - span.record("huly_key", &path.key); - - let pool = request.app_data::>().unwrap().to_owned(); - - let parts = postgres::find_parts::(&pool, path.workspace, &path.key).await?; - - let response = if !parts.is_empty() { - let etag = objectpart_etag(&parts).unwrap(); - let date = objectpart_date(&parts).unwrap(); - - match none_match(request.request(), Some(etag.clone()))? { - Some(false) => HttpResponse::NotModified() - .insert_header((header::ETAG, etag)) - .insert_header((header::LAST_MODIFIED, HttpDate::from(date))) - .insert_header((header::CACHE_CONTROL, CONFIG.cache_control.clone())) - .finish(), - _ => { - let mut response = HttpResponse::Ok(); - - let headers = parts[0].data.headers.as_ref(); - if let Some(headers) = headers { - for (header, value) in headers.iter() { - response.insert_header((header.as_str(), value.to_owned())); - } - } - - let accept_ranges = objectpart_accept_ranges(&parts); - if let Some(accept_ranges) = accept_ranges { - response.insert_header((header::ACCEPT_RANGES, accept_ranges)); - } - - response.insert_header((header::ETAG, etag)); - response.insert_header((header::LAST_MODIFIED, HttpDate::from(date))); - response.insert_header((header::CACHE_CONTROL, CONFIG.cache_control.clone())); - - // see https://github.com/actix/examples/blob/master/forms/multipart-s3/src/main.rs#L67-L79 - let content_length = merge::content_length(parts); - match content_length { - Some(content_length) => response.body(SizedStream::new( - content_length as u64, - stream::empty::>().boxed_local(), - )), - None => response.finish(), - } - } - } - } else { - HttpResponse::NotFound().finish() - }; - - Ok(response) -} - -pub async fn delete(_path: Path) -> HandlerResult { - unimplemented!("delete is not implemented") -} - -fn objectpart_etag(parts: &Vec>) -> Option { - parts - .last() - .map(|p| EntityTag::new_strong(p.data.etag.to_owned())) -} - -fn objectpart_date(parts: &Vec>) -> Option { - parts.last().map(|p| p.data.date).map(|d| d.into()) -} - -fn objectpart_strategy(parts: &Vec>) -> Option { - parts.first().map(|p| p.data.merge_strategy.unwrap()) -} - -fn objectpart_accept_ranges(parts: &Vec>) -> Option<&str> { - let strategy = objectpart_strategy(parts)?; - match strategy { - MergeStrategy::JsonPatch => None, - _ => { - if parts.len() == 1 { - Some("bytes") - } else { - None - } - } - } -} - -fn validate_patch_conditionals( - req: &HttpRequest, - parts: &Vec>, -) -> Result, ApiError> { - let etag = objectpart_etag(parts); - - match any_match(req, etag)? { - Some(false) => Err(ApiError::PreconditionFailed), - Some(true) => { - let parts_data = parts.iter().map(|p| &p.data).collect::>(); - let parts_etag = recovery::object_etag(parts_data)?; - - Ok(Some(ConditionalMatch::IfMatch(parts_etag))) - } - None => Ok(None), - } -} - -fn validate_put_conditionals( - req: &HttpRequest, - parts: &Vec>, -) -> Result, ApiError> { - let etag = objectpart_etag(parts); - - match any_match(req, etag.clone())? { - Some(true) => { - let parts_data = parts.iter().map(|p| &p.data).collect::>(); - let parts_etag = recovery::object_etag(parts_data)?; - - Ok(Some(ConditionalMatch::IfMatch(parts_etag))) - } - Some(false) => Err(ApiError::PreconditionFailed), - None => match none_match(req, etag.clone())? { - Some(true) => Ok(Some(ConditionalMatch::IfNoneMatch("*".to_owned()))), - Some(false) => Err(ApiError::PreconditionFailed), - None => Ok(None), - }, - } -} - -#[cfg(test)] -mod tests { - use super::*; - use actix_web::test::TestRequest; - - fn object_part(etag: &str) -> ObjectPart { - ObjectPart { - inline: None, - data: PartData { - workspace: Uuid::new_v4(), - key: "test".to_string(), - part: 0, - size: 0, - blob: "test".to_string(), - etag: etag.to_owned(), - date: Utc::now(), - headers: None, - meta: None, - merge_strategy: None, - }, - } - } - - #[test] - fn test_objectpart_etag() { - let parts = vec![object_part("foo"), object_part("bar")]; - - let etag = objectpart_etag(&parts); - assert_eq!(etag, Some(EntityTag::new_strong("bar".to_owned()))); - } - - #[test] - fn test_validate_patch_conditionals_none_none() { - let req = TestRequest::default().to_http_request(); - let parts = vec![]; - - let res = validate_patch_conditionals(&req, &parts); - assert!(res.is_ok()); - assert_eq!(res.unwrap(), None); - } - - #[test] - fn test_validate_patch_conditionals_none_some() { - let req = TestRequest::default().to_http_request(); - let parts = vec![object_part("foo")]; - - let res = validate_patch_conditionals(&req, &parts); - assert!(res.is_ok()); - assert_eq!(res.unwrap(), None); - } - - #[test] - fn test_validate_patch_conditionals_some_some_match() { - let req = TestRequest::default() - .insert_header((header::IF_MATCH, "\"foo\"")) - .to_http_request(); - let parts = vec![object_part("foo")]; - let etag = recovery::object_etag(vec![&parts[0].data]).unwrap(); - - let res = validate_patch_conditionals(&req, &parts); - assert!(res.is_ok()); - assert_eq!(res.unwrap(), Some(ConditionalMatch::IfMatch(etag))); - } - - #[test] - fn test_validate_patch_conditionals_some_some_not_match() { - let req = TestRequest::default() - .insert_header((header::IF_MATCH, "\"bar\"")) - .to_http_request(); - let parts = vec![object_part("foo")]; - - let res = validate_patch_conditionals(&req, &parts); - assert!(res.is_err()); - assert_eq!( - res.unwrap_err().to_string(), - ApiError::PreconditionFailed.to_string() - ); - } - - #[test] - fn test_validate_put_conditionals_none_none() { - let req = TestRequest::default().to_http_request(); - let parts = vec![]; - - let res = validate_put_conditionals(&req, &parts); - assert!(res.is_ok()); - assert_eq!(res.unwrap(), None); - } - - #[test] - fn test_validate_put_conditionals_none_some() { - let req = TestRequest::default().to_http_request(); - let parts = vec![object_part("foo")]; - - let res = validate_put_conditionals(&req, &parts); - assert!(res.is_ok()); - assert_eq!(res.unwrap(), None); - } - - #[test] - fn test_validate_put_conditionals_some_some_match() { - let req = TestRequest::default() - .insert_header((header::IF_MATCH, "\"foo\"")) - .to_http_request(); - let parts = vec![object_part("foo")]; - let etag = recovery::object_etag(vec![&parts[0].data]).unwrap(); - - let res = validate_put_conditionals(&req, &parts); - assert!(res.is_ok()); - assert_eq!(res.unwrap(), Some(ConditionalMatch::IfMatch(etag))); - } - - #[test] - fn test_validate_put_conditionals_some_some_not_match() { - let req = TestRequest::default() - .insert_header((header::IF_MATCH, "\"bar\"")) - .to_http_request(); - let parts = vec![object_part("foo")]; - - let res = validate_put_conditionals(&req, &parts); - assert!(res.is_err()); - assert_eq!( - res.unwrap_err().to_string(), - ApiError::PreconditionFailed.to_string() - ); - } - - #[test] - fn test_validate_put_conditionals_if_match_if_none_match() { - let req = TestRequest::default() - .insert_header((header::IF_MATCH, "\"bar\"")) - .insert_header((header::IF_NONE_MATCH, "*")) - .to_http_request(); - let parts = vec![object_part("foo")]; - - let res = validate_put_conditionals(&req, &parts); - assert!(res.is_err()); - assert_eq!( - res.unwrap_err().to_string(), - ApiError::PreconditionFailed.to_string() - ); - } - - #[test] - fn test_validate_put_conditionals_if_none_match_match() { - let req = TestRequest::default() - .insert_header((header::IF_NONE_MATCH, "\"bar\"")) - .to_http_request(); - let parts = vec![object_part("foo")]; - - let res = validate_put_conditionals(&req, &parts); - assert!(res.is_ok()); - assert_eq!( - res.unwrap(), - Some(ConditionalMatch::IfNoneMatch("*".to_owned())) - ); - } - - #[test] - fn test_validate_put_conditionals_if_none_match_not_match() { - let req = TestRequest::default() - .insert_header((header::IF_NONE_MATCH, "\"foo\"")) - .to_http_request(); - let parts = vec![object_part("foo")]; - - let res = validate_put_conditionals(&req, &parts); - assert_eq!( - res.unwrap_err().to_string(), - ApiError::PreconditionFailed.to_string() - ); - } - - #[test] - fn test_validate_put_conditionals_if_none_match_err() { - let req = TestRequest::default() - .insert_header((header::IF_NONE_MATCH, "*")) - .to_http_request(); - let parts = vec![object_part("foo")]; - - let res = validate_put_conditionals(&req, &parts); - assert!(res.is_err()); - assert_eq!( - res.unwrap_err().to_string(), - ApiError::PreconditionFailed.to_string() - ); - } -} diff --git a/foundations/hulylake/server/src/main.rs b/foundations/hulylake/server/src/main.rs deleted file mode 100644 index 64659e5129..0000000000 --- a/foundations/hulylake/server/src/main.rs +++ /dev/null @@ -1,210 +0,0 @@ -use std::{net::SocketAddr, sync::Arc}; - -use actix_cors::Cors; -use actix_web::{ - App, Error, HttpMessage, HttpServer, - body::MessageBody, - dev::{ServiceRequest, ServiceResponse}, - middleware::{Next, from_fn}, - web::{self, Data, Path}, -}; -use tracing::*; -use tracing_actix_web::TracingLogger; -use uuid::Uuid; - -use hulyrs::services::jwt::actix::ServiceRequestExt; -use hulyrs::services::otel; - -mod blob; -mod compact; -mod conditional; -mod config; -mod handlers; -mod merge; -mod mutex; -mod patch; -mod postgres; -mod recovery; -mod s3; - -use config::CONFIG; - -use crate::mutex::KeyMutex; - -fn initialize_tracing() { - use opentelemetry::trace::TracerProvider; - use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge; - use tracing::Level; - use tracing_opentelemetry::OpenTelemetryLayer; - use tracing_subscriber::{filter::targets::Targets, prelude::*}; - - let otel_config = otel::OtelConfig { - mode: config::hulyrs::CONFIG.otel_mode.clone(), - service_name: env!("CARGO_PKG_NAME").to_string(), - service_version: env!("CARGO_PKG_VERSION").to_string(), - }; - - otel::init(&otel_config); - - let filter = Targets::default() - .with_target(env!("CARGO_BIN_NAME"), config::hulyrs::CONFIG.log) - .with_target("actix", Level::WARN); - let format = tracing_subscriber::fmt::layer().compact(); - - match &config::hulyrs::CONFIG.otel_mode { - otel::OtelMode::Off => { - tracing_subscriber::registry() - .with(filter) - .with(format) - .init(); - } - - _ => { - tracing_subscriber::registry() - .with(filter) - .with(format) - .with(otel::tracer_provider(&otel_config).map(|provider| { - let filter = Targets::default() - .with_default(Level::DEBUG) - .with_target(env!("CARGO_PKG_NAME"), config::hulyrs::CONFIG.log); - - OpenTelemetryLayer::new(provider.tracer("hulylake")).with_filter(filter) - })) - .with(otel::logger_provider(&otel_config).as_ref().map(|logger| { - let filter = Targets::default() - .with_default(Level::DEBUG) - .with_target(env!("CARGO_PKG_NAME"), Level::DEBUG); - - OpenTelemetryTracingBridge::new(logger).with_filter(filter) - })) - .init(); - } - } -} - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - initialize_tracing(); - - tracing::info!( - "{}/{} started", - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_VERSION") - ); - - tracing::debug!( - db_connection = &CONFIG.db_connection, - db_scheme = &CONFIG.db_scheme, - s3_bucket = &CONFIG.s3_bucket, - "configuration" - ); - - let lock = mutex::KeyMutex::new(); - let postgres = postgres::pool().await?; - let s3 = s3::client().await; - - match s3.head_bucket().bucket(&CONFIG.s3_bucket).send().await { - Ok(_) => info!(bucket = &CONFIG.s3_bucket, "s3 bucket exists and available"), - Err(_) => { - s3.create_bucket().bucket(&CONFIG.s3_bucket).send().await?; - info!(bucket = &CONFIG.s3_bucket, "s3 bucket created"); - } - } - - let bind_to = SocketAddr::new(CONFIG.bind_host.as_str().parse()?, CONFIG.bind_port); - - #[allow(dead_code)] - async fn auth( - mut request: ServiceRequest, - next: Next, - ) -> Result, Error> { - let claims = request - .extract_claims(&CONFIG.token_secret) - .map_err(|error| { - let method = request.method(); - let path = request.path(); - warn!(%method, path, %error, "Unauthorized request"); - error - })?; - - let workspace = Uuid::parse_str(&request.extract::>().await?); - - if claims.is_system() || Ok(claims.workspace.clone()) == workspace.clone().map(Some) { - request.extensions_mut().insert(claims); - next.call(request).await - } else { - warn!( - expected = ?claims.workspace, - actual = ?workspace, - "Unauthorized request, workspace mismatch" - ); - Err(actix_web::error::ErrorUnauthorized("Unauthorized").into()) - } - } - - async fn mutex( - mut request: ServiceRequest, - next: Next, - ) -> Result, Error> { - let path = request - .extract::>() - .await? - .into_inner(); - - let mutex = request.app_data::>().unwrap().to_owned(); - - let _guard = mutex.lock(path.workspace, path.key).await; - - next.call(request).await - } - - let compactor = compact::CompactWorker::new( - Arc::new(s3.clone()), - postgres.clone(), - lock.clone(), - CONFIG.compact_buffer_size, - ); - let compactor_data = Data::new(compactor); - let compactor_handle = compactor_data.clone(); - - let server = HttpServer::new(move || { - let cors = Cors::default() - .allow_any_origin() - .allow_any_method() - .allow_any_header() - .supports_credentials() - .max_age(3600); - - const KEY_PATH: &str = "/{key:.*}"; - - App::new() - .app_data(Data::new(postgres.clone())) - .app_data(Data::new(s3.clone())) - .app_data(Data::new(lock.clone())) - .app_data(compactor_data.clone()) - .wrap(TracingLogger::default()) - .wrap(cors) - .service( - web::scope("/api/{workspace}") - .wrap(from_fn(auth)) - .route(KEY_PATH, web::head().to(handlers::head)) - .route(KEY_PATH, web::get().to(handlers::get)) - .route(KEY_PATH, web::put().to(handlers::put).wrap(from_fn(mutex))) - .route( - KEY_PATH, - web::patch().to(handlers::patch).wrap(from_fn(mutex)), - ) - .route(KEY_PATH, web::delete().to(handlers::delete)), - ) - .route("/status", web::get().to(async || "ok")) - }) - .bind(bind_to)? - .run(); - - info!("http listener on {}", bind_to); - - server.await?; - compactor_handle.stop().await; - - Ok(()) -} diff --git a/foundations/hulylake/server/src/merge.rs b/foundations/hulylake/server/src/merge.rs deleted file mode 100644 index 335ec23290..0000000000 --- a/foundations/hulylake/server/src/merge.rs +++ /dev/null @@ -1,430 +0,0 @@ -use std::{io::Error as IoError, pin::Pin, sync::Arc}; - -use actix_web::error::ErrorBadRequest; -use async_stream::stream; -use bytes::Bytes; -use futures_util::Stream; -use serde::{Deserialize, Serialize}; -use serde_json::{Value, from_slice}; -use tracing::*; - -use crate::handlers::PartData; -use crate::handlers::{HandlerResult, Headers}; -use crate::patch; -use crate::postgres::ObjectPart; -use crate::s3::S3Client; -use crate::{blob::Blob, config::CONFIG}; - -#[derive( - Clone, Copy, Debug, Serialize, Deserialize, Default, strum::EnumString, strum::Display, -)] -#[serde(rename_all = "lowercase")] -#[strum(serialize_all = "lowercase")] -pub enum MergeStrategy { - JsonPatch, - - #[default] - Concatenate, -} - -pub fn validate_put_request(merge_strategy: MergeStrategy, headers: &Headers) -> HandlerResult<()> { - match merge_strategy { - MergeStrategy::JsonPatch - if headers.content_type != Some("application/json".to_string()) - || headers.content_length > CONFIG.inline_threshold => - { - Err(ErrorBadRequest("invalid content type and length").into()) - } - - _ => Ok(()), - } -} - -pub fn validate_put_body(merge_strategy: MergeStrategy, blob: &Blob) -> HandlerResult<()> { - match merge_strategy { - MergeStrategy::JsonPatch => match blob.inline.as_ref() { - Some(inline) => { - from_slice::(inline).map_err(|e| ErrorBadRequest(e.to_string()))?; - Ok(()) - } - _ => Err(ErrorBadRequest("missing inline body").into()), - }, - - _ => Ok(()), - } -} - -pub fn validate_patch_request( - merge_strategy: MergeStrategy, - headers: &Headers, -) -> HandlerResult<()> { - match merge_strategy { - MergeStrategy::JsonPatch - if headers.content_type != Some("application/json-patch+json".to_string()) - || headers.content_length > CONFIG.inline_threshold => - { - Err(ErrorBadRequest("invalid content type and length").into()) - } - - _ => Ok(()), - } -} - -pub fn validate_patch_body(merge_strategy: MergeStrategy, blob: &Blob) -> HandlerResult<()> { - match merge_strategy { - MergeStrategy::JsonPatch => match blob.inline.as_ref() { - Some(inline) => { - from_slice::>(inline) - .map_err(|e| ErrorBadRequest(e.to_string()))?; - Ok(()) - } - _ => Err(ErrorBadRequest("missing inline body").into()), - }, - - _ => Ok(()), - } -} - -pub struct PartialResponse { - pub partial: bool, - pub content_range: Option, - pub content_length: u64, - pub stream: Pin>>>, -} - -#[instrument(level = "debug", skip_all)] -pub async fn partial( - s3: Arc, - parts: Vec>, - range: String, -) -> anyhow::Result { - let part = parts.first().unwrap(); - - let mut response = s3 - .get_object() - .bucket(&CONFIG.s3_bucket) - .key(&part.data.blob) - .range(range) - .send() - .await?; - - let content_range = response.content_range().map(|s| s.to_string()); - let content_length = response.content_length().map_or(0, |c| c as u64); - - let stream = stream! { - while let Some(chunk) = response.body.next().await { - yield Ok(Bytes::from(chunk?)); - }; - }; - - Ok(PartialResponse { - partial: part.data.size != content_length as usize, - content_range, - content_length, - stream: Box::pin(stream), - }) -} - -pub struct StreamResponse { - pub content_length: u64, - pub stream: Pin> + Send>>, -} - -#[instrument(level = "debug", skip_all)] -pub async fn stream( - s3: Arc, - parts: Vec>, -) -> anyhow::Result { - let first = parts.first().unwrap(); - let merge_strategy = first.data.merge_strategy.unwrap(); - - match merge_strategy { - MergeStrategy::Concatenate => { - let mut content_length = 0; - - for part in parts.iter() { - content_length += part.data.size; - } - - let stream = stream! { - for parts in parts { - match parts.inline { - Some(inline) => { - yield Ok(Bytes::from(inline)); - }, - None => { - match s3.get_object().bucket(&CONFIG.s3_bucket).key(parts.data.blob).send().await { - Ok(mut response) => { - while let Some(bytes) = response.body.next().await { - yield Ok(bytes?); - } - }, - - Err(error) => { - yield Err(IoError::new(std::io::ErrorKind::Other, error)); - break; - } - } - } - } - } - }; - - Ok(StreamResponse { - content_length: content_length as u64, - stream: Box::pin(stream), - }) - } - - MergeStrategy::JsonPatch => { - let mut acc = None; - - for part in parts { - let part_data = part_data(&s3, part).await?; - - if let Some(acc) = &mut acc { - let ops = serde_json::from_slice::>(&part_data); - match ops { - Ok(ops) => { - if let Err(error) = patch::apply(acc, &ops) { - error!("json patch error: {error}"); - } - } - Err(error) => { - error!("json patch deserialization error: {error}"); - } - } - } else { - acc = Some(serde_json::from_slice::(&part_data)?); - } - } - - let bytes = serde_json::to_vec(&acc.unwrap())?; - let content_length = bytes.len() as u64; - - let stream = stream! { - yield Result::::Ok(Bytes::from(bytes)); - }; - - Ok(StreamResponse { - content_length, - stream: Box::pin(stream), - }) - } - } -} - -pub fn content_length(parts: Vec>) -> Option { - let first = parts.first().unwrap(); - let merge_strategy = first.data.merge_strategy.unwrap(); - - match merge_strategy { - MergeStrategy::Concatenate => { - let mut content_length = 0; - - for part in parts { - content_length += part.data.size; - } - - Some(content_length) - } - - MergeStrategy::JsonPatch => None, - } -} - -async fn part_data(s3: &S3Client, part: ObjectPart) -> anyhow::Result> { - match part.inline { - Some(inline) => Ok(inline), - - None => { - let response = s3 - .get_object() - .bucket(&CONFIG.s3_bucket) - .key(&part.data.blob) - .send() - .await?; - - let body = response.body.collect().await?; - let bytes = body.into_bytes().to_vec(); - - Ok(bytes) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use size::Size; - - #[test] - fn test_validate_put_request() { - let test_cases: Vec<(MergeStrategy, &str, Size, HandlerResult<()>)> = vec![ - ( - MergeStrategy::JsonPatch, - "application/json", - Size::from_bytes(100), - Ok(()), - ), - ( - MergeStrategy::JsonPatch, - "application/json", - Size::from_kb(10), - Ok(()), - ), - ( - MergeStrategy::JsonPatch, - "application/json", - Size::from_mb(1), - Err(ErrorBadRequest("invalid content type and length").into()), - ), - ( - MergeStrategy::JsonPatch, - "text/plain", - Size::from_kb(10), - Err(ErrorBadRequest("invalid content type and length").into()), - ), - ( - MergeStrategy::Concatenate, - "text/plain", - Size::from_mb(1), - Ok(()), - ), - ]; - - for (merge_strategy, content_type, content_length, expected) in test_cases { - let headers = Headers { - content_length, - content_type: Some(content_type.to_string()), - huly_headers: Vec::new(), - meta: Vec::new(), - }; - let res = validate_put_request(merge_strategy, &headers); - match expected { - Ok(_) => assert!(res.is_ok(), "Expected Ok, got Err: {:?}", res.err()), - Err(e) => assert_eq!(res.unwrap_err().to_string(), e.to_string()), - } - } - } - - #[test] - fn test_validate_put_body() { - let test_cases: Vec<(MergeStrategy, Option, HandlerResult<()>)> = vec![ - ( - MergeStrategy::JsonPatch, - Some(Bytes::from( - r#"{ "op": "add", "path": "/foo", "value": "bar" }"#, - )), - Ok(()), - ), - ( - MergeStrategy::JsonPatch, - None, - Err(ErrorBadRequest("missing inline body").into()), - ), - (MergeStrategy::Concatenate, None, Ok(())), - ]; - - for (merge_strategy, body, expected) in test_cases { - let blob = Blob { - hash: "hash".to_string(), - s3_key: "key".to_string(), - length: body.as_ref().map(|b| b.len()).unwrap_or(0), - inline: body, - parts_count: None, - deduplicated: false, - }; - let res = validate_put_body(merge_strategy, &blob); - match expected { - Ok(_) => assert!(res.is_ok(), "Expected Ok, got Err: {:?}", res.err()), - Err(e) => assert_eq!(res.unwrap_err().to_string(), e.to_string()), - } - } - } - - #[test] - fn test_validate_patch_request() { - let test_cases: Vec<(MergeStrategy, &str, Size, HandlerResult<()>)> = vec![ - ( - MergeStrategy::JsonPatch, - "application/json-patch+json", - Size::from_bytes(100), - Ok(()), - ), - ( - MergeStrategy::JsonPatch, - "application/json-patch+json", - Size::from_kb(10), - Ok(()), - ), - ( - MergeStrategy::JsonPatch, - "application/json-patch+json", - Size::from_mb(1), - Err(ErrorBadRequest("invalid content type and length").into()), - ), - ( - MergeStrategy::JsonPatch, - "application/json", - Size::from_kb(10), - Err(ErrorBadRequest("invalid content type and length").into()), - ), - ( - MergeStrategy::Concatenate, - "text/plain", - Size::from_mb(1), - Ok(()), - ), - ]; - - for (merge_strategy, content_type, content_length, expected) in test_cases { - let headers = Headers { - content_length, - content_type: Some(content_type.to_string()), - huly_headers: Vec::new(), - meta: Vec::new(), - }; - let res = validate_patch_request(merge_strategy, &headers); - match expected { - Ok(_) => assert!(res.is_ok(), "Expected Ok, got Err: {:?}", res.err()), - Err(e) => assert_eq!(res.unwrap_err().to_string(), e.to_string()), - } - } - } - - #[test] - fn test_validate_patch_body() { - let test_cases: Vec<(MergeStrategy, Option, HandlerResult<()>)> = vec![ - ( - MergeStrategy::JsonPatch, - Some(Bytes::from( - r#"[{ "op": "add", "path": "/foo", "value": "bar" }]"#, - )), - Ok(()), - ), - ( - MergeStrategy::JsonPatch, - None, - Err(ErrorBadRequest("missing inline body").into()), - ), - (MergeStrategy::Concatenate, None, Ok(())), - ]; - - for (merge_strategy, body, expected) in test_cases { - let blob = Blob { - hash: "hash".to_string(), - s3_key: "key".to_string(), - length: body.as_ref().map(|b| b.len()).unwrap_or(0), - inline: body, - parts_count: None, - deduplicated: false, - }; - let res = validate_patch_body(merge_strategy, &blob); - match expected { - Ok(_) => assert!(res.is_ok(), "Expected Ok, got Err: {:?}", res.err()), - Err(e) => assert_eq!(res.unwrap_err().to_string(), e.to_string()), - } - } - } -} diff --git a/foundations/hulylake/server/src/mutex.rs b/foundations/hulylake/server/src/mutex.rs deleted file mode 100644 index 261910dea5..0000000000 --- a/foundations/hulylake/server/src/mutex.rs +++ /dev/null @@ -1,23 +0,0 @@ -use std::sync::Arc; - -use lockable::LockPool; -use uuid::Uuid; - -#[derive(Clone)] -pub struct KeyMutex { - lock_pool: Arc>, -} - -impl KeyMutex { - pub fn new() -> Self { - let lock_pool = Arc::new(LockPool::::new()); - - KeyMutex { lock_pool } - } - - pub async fn lock(&self, workspace: Uuid, key: String) -> impl Drop + '_ { - self.lock_pool - .async_lock(format!("{}:{}", workspace, key)) - .await - } -} diff --git a/foundations/hulylake/server/src/patch.rs b/foundations/hulylake/server/src/patch.rs deleted file mode 100644 index a431ff2e2f..0000000000 --- a/foundations/hulylake/server/src/patch.rs +++ /dev/null @@ -1,654 +0,0 @@ -use json_patch::PatchOperation as StandardPatchOperation; -use jsonptr::{Pointer, PointerBuf}; -use serde::{Deserialize, Serialize}; -use serde_json::{Number, Value, json}; -use thiserror::Error; -use tracing::*; -/// 'add' operation - increments a numeric value -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct AddOperationExt { - /// JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location - /// within the target document where the operation is performed. - pub path: PointerBuf, - /// Value to add to the target location. - pub value: Value, - // When enabled, ensures that the operation does not overwrite existing fields - #[serde(default)] - pub safe: bool, -} - -/// 'inc' operation - increments a numeric value -#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] -pub struct IncOperationExt { - // JSON Pointer to the target location (must point to a numeric value) - pub path: PointerBuf, - // Amount to increment by (can be negative for decrement) - // Should be a number - pub value: Value, - // When enabled, ensures that the operation does not create new fields - #[serde(default)] - pub safe: bool, -} - -/// 'remove' operation - increments a numeric value -#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] -pub struct RemoveOperationExt { - /// JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location - /// within the target document where the operation is performed. - pub path: PointerBuf, - // When enabled, ensures that the operation does not create new fields - #[serde(default)] - pub safe: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(tag = "hop")] -#[serde(rename_all = "lowercase")] -pub enum HulyPatchOperation { - /// 'add' operation - Add(AddOperationExt), - /// 'inc' operation - Inc(IncOperationExt), - /// 'remove' operation - Remove(RemoveOperationExt), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum PatchOperation { - Huly(HulyPatchOperation), - Standard(StandardPatchOperation), -} - -impl<'de> serde::Deserialize<'de> for PatchOperation { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let value = Value::deserialize(deserializer)?; - - let op = serde_json::from_value::(value.clone()); - if let Ok(op) = op { - Ok(Self::Standard(op)) - } else { - let standard_error = op.err().unwrap(); - serde_json::from_value::(value) - .map_err(|huly_error| { - serde::de::Error::custom(format!( - "Failed to deserialize as StandardPatchOperation: {}. Also failed to deserialize as HulyPatchOperation: {}", - standard_error, huly_error - )) - }) - .map(Self::Huly) - } - } -} - -#[derive(Debug, Error, PartialEq, Eq)] -pub enum HulyPatchError { - #[error("invalid number")] - InvalidNumber, - #[error("patch error: {0}")] - PatchError(String), -} - -impl From for HulyPatchError { - fn from(err: json_patch::PatchError) -> Self { - HulyPatchError::PatchError(err.to_string()) - } -} - -pub fn apply(doc: &mut Value, patches: &[PatchOperation]) -> Result<(), HulyPatchError> { - for patch in patches { - if let Some(op) = match patch { - PatchOperation::Huly(huly_op) => match huly_op { - HulyPatchOperation::Add(op) => add(doc, &op.path, &op.value, op.safe), - HulyPatchOperation::Inc(op) => inc(doc, &op.path, &op.value, op.safe), - HulyPatchOperation::Remove(op) => remove(doc, &op.path, op.safe), - }, - PatchOperation::Standard(standard_op) => Ok(Some(standard_op.clone())), - }? { - if let Err(e) = json_patch::patch(doc, &[op.clone()]) { - error!("Failed to apply patch {:?}: {}", patch, e); - return Err(e.into()); - } - } - } - Ok(()) -} - -fn add( - doc: &Value, - path: &Pointer, - value: &Value, - safe: bool, -) -> Result, HulyPatchError> { - let target = doc.pointer(path.as_str()); - - Ok(if safe && target.is_some() { - None - } else { - Some(StandardPatchOperation::Add(json_patch::AddOperation { - path: path.to_owned(), - value: value.to_owned(), - })) - }) -} - -fn inc( - doc: &Value, - path: &Pointer, - value: &Value, - safe: bool, -) -> Result, HulyPatchError> { - let target = doc.pointer(path.as_str()); - - match (target, value.as_number()) { - (None, _) if safe => Ok(None), - - (None, Some(value)) => { - let op = StandardPatchOperation::Add(json_patch::AddOperation { - path: path.to_owned(), - value: serde_json::Value::Number(value.to_owned()), - }); - - Ok(Some(op)) - } - - (Some(_), None) => Err(HulyPatchError::InvalidNumber), - - (Some(serde_json::Value::Number(old_value)), Some(increment)) => { - let new_value = add_json_numbers(old_value, increment)?; - - let op = StandardPatchOperation::Replace(json_patch::ReplaceOperation { - path: path.to_owned(), - value: json!(new_value), - }); - - Ok(Some(op)) - } - - (Some(_), Some(_)) => Err(HulyPatchError::InvalidNumber), - (None, None) => Err(HulyPatchError::InvalidNumber), - } -} - -fn remove( - doc: &Value, - path: &Pointer, - safe: bool, -) -> Result, HulyPatchError> { - let target = doc.pointer(path.as_str()); - - Ok(if safe && target.is_none() { - None - } else { - Some(StandardPatchOperation::Remove( - json_patch::RemoveOperation { - path: path.to_owned(), - }, - )) - }) -} - -fn add_json_numbers(a: &Number, b: &Number) -> Result { - a.as_i64() - .and_then(|a| b.as_i64().map(|b| Number::from(a + b))) - .or_else(|| { - a.as_f64() - .and_then(|a| b.as_f64().map(|b| a + b)) - .and_then(Number::from_f64) - }) - .ok_or(HulyPatchError::InvalidNumber) -} - -#[cfg(test)] -mod tests { - use serde_json::json; - - use super::*; - - fn test_add( - doc: Value, - path: PointerBuf, - value: Value, - safe: bool, - expected: Result, HulyPatchError>, - ) { - let mut doc = doc.clone(); - let res = add(&mut doc, &path, &value, safe); - - match expected { - Ok(op) => { - assert!(res.is_ok()); - assert_eq!(op, res.unwrap()); - } - Err(e) => { - assert!(res.is_err()); - assert_eq!(Some(e), res.err()); - } - } - } - - fn test_inc( - doc: Value, - path: PointerBuf, - value: Value, - safe: bool, - expected: Result, HulyPatchError>, - ) { - let mut doc = doc.clone(); - let res = inc(&mut doc, &path, &value, safe); - - match expected { - Ok(op) => { - assert!(res.is_ok()); - assert_eq!(op, res.unwrap()); - } - Err(e) => { - assert!(res.is_err()); - assert_eq!(Some(e), res.err()); - } - } - } - - fn test_remove( - doc: Value, - path: PointerBuf, - safe: bool, - expected: Result, HulyPatchError>, - ) { - let mut doc = doc.clone(); - let res = remove(&mut doc, &path, safe); - - match expected { - Ok(op) => { - assert!(res.is_ok()); - assert_eq!(op, res.unwrap()); - } - Err(e) => { - assert!(res.is_err()); - assert_eq!(Some(e), res.err()); - } - } - } - - #[test] - fn test_add_non_existing_object_field() { - test_add( - json!({}), - PointerBuf::from_tokens(["a"]), - json!(1), - false, - Ok(Some(StandardPatchOperation::Add( - json_patch::AddOperation { - path: PointerBuf::from_tokens(["a"]), - value: json!(1), - }, - ))), - ); - } - - #[test] - fn test_add_non_existing_object_field_safe() { - test_add( - json!({}), - PointerBuf::from_tokens(["a"]), - json!(1), - true, - Ok(Some(StandardPatchOperation::Add( - json_patch::AddOperation { - path: PointerBuf::from_tokens(["a"]), - value: json!(1), - }, - ))), - ); - } - - #[test] - fn test_add_existing_object_field() { - test_add( - json!({ "a": 1 }), - PointerBuf::from_tokens(["a"]), - json!(2), - false, - Ok(Some(StandardPatchOperation::Add( - json_patch::AddOperation { - path: PointerBuf::from_tokens(["a"]), - value: json!(2), - }, - ))), - ); - } - - #[test] - fn test_add_existing_object_field_safe() { - test_add( - json!({ "a": 1 }), - PointerBuf::from_tokens(["a"]), - json!(2), - true, - Ok(None), - ); - } - - #[test] - fn test_add_invalid_path() { - test_add( - json!({ "a": 1 }), - PointerBuf::from_tokens(["a", "b"]), - json!(2), - false, - Ok(Some(StandardPatchOperation::Add( - json_patch::AddOperation { - path: PointerBuf::from_tokens(["a", "b"]), - value: json!(2), - }, - ))), - ); - } - - #[test] - fn test_add_invalid_path_safe() { - test_add( - json!({ "a": 1 }), - PointerBuf::from_tokens(["a", "b"]), - json!(2), - true, - Ok(Some(StandardPatchOperation::Add( - json_patch::AddOperation { - path: PointerBuf::from_tokens(["a", "b"]), - value: json!(2), - }, - ))), - ); - } - - #[test] - fn test_inc_existing_object_field() { - test_inc( - json!({ "a": 1 }), - PointerBuf::from_tokens(["a"]), - json!(1), - false, - Ok(Some(StandardPatchOperation::Replace( - json_patch::ReplaceOperation { - path: PointerBuf::from_tokens(["a"]), - value: json!(2), - }, - ))), - ); - } - - #[test] - fn test_inc_non_existing_object_field() { - test_inc( - json!({}), - PointerBuf::from_tokens(["a"]), - json!(1), - false, - Ok(Some(StandardPatchOperation::Add( - json_patch::AddOperation { - path: PointerBuf::from_tokens(["a"]), - value: json!(1), - }, - ))), - ); - } - - #[test] - fn test_inc_non_existing_object_field_safe() { - test_inc( - json!({}), - PointerBuf::from_tokens(["a"]), - json!(1), - true, - Ok(None), - ); - } - - #[test] - fn test_inc_existing_array_item() { - test_inc( - json!({ "a": [0, 1, 2] }), - PointerBuf::from_tokens(["a", "0"]), - json!(1), - false, - Ok(Some(StandardPatchOperation::Replace( - json_patch::ReplaceOperation { - path: PointerBuf::from_tokens(["a", "0"]), - value: json!(1), - }, - ))), - ); - } - - #[test] - fn test_inc_non_existing_array_item() { - test_inc( - json!({ "a": [0, 1, 2] }), - PointerBuf::from_tokens(["a", "3"]), - json!(3), - false, - Ok(Some(StandardPatchOperation::Add( - json_patch::AddOperation { - path: PointerBuf::from_tokens(["a", "3"]), - value: json!(3), - }, - ))), - ); - } - - #[test] - fn test_inc_non_existing_array_item_safe() { - test_inc( - json!({ "a": [0, 1, 2] }), - PointerBuf::from_tokens(["a", "3"]), - json!(3), - true, - Ok(None), - ); - } - - #[test] - fn test_inc_non_number_field() { - test_inc( - json!({ "a": "b" }), - PointerBuf::from_tokens(["a"]), - json!(1), - false, - Err(HulyPatchError::InvalidNumber), - ); - } - - #[test] - fn test_inc_by_non_number() { - test_inc( - json!({ "a": "1" }), - PointerBuf::from_tokens(["a"]), - json!("one"), - false, - Err(HulyPatchError::InvalidNumber), - ); - } - - #[test] - fn test_remove_existing_object_field() { - test_remove( - json!({ "a": 1 }), - PointerBuf::from_tokens(["a"]), - false, - Ok(Some(StandardPatchOperation::Remove( - json_patch::RemoveOperation { - path: PointerBuf::from_tokens(["a"]), - }, - ))), - ); - } - - #[test] - fn test_remove_non_existing_object_field() { - test_remove( - json!({}), - PointerBuf::from_tokens(["a"]), - false, - Ok(Some(StandardPatchOperation::Remove( - json_patch::RemoveOperation { - path: PointerBuf::from_tokens(["a"]), - }, - ))), - ); - } - - #[test] - fn test_remove_non_existing_object_field_safe() { - test_remove(json!({}), PointerBuf::from_tokens(["a"]), true, Ok(None)); - } - - #[test] - fn test_add_json_numbers_i_i() { - let res = add_json_numbers(&Number::from(1), &Number::from(1)); - assert_eq!(res, Ok(Number::from(2))); - } - - #[test] - fn test_add_json_numbers_i_f() { - let res = add_json_numbers(&Number::from(1), &Number::from_f64(1.0).unwrap()); - assert_eq!(res, Ok(Number::from_f64(2.0).unwrap())); - } - - #[test] - fn test_add_json_numbers_f_i() { - let res = add_json_numbers(&Number::from_f64(1.0).unwrap(), &Number::from(1)); - assert_eq!(res, Ok(Number::from_f64(2.0).unwrap())); - } - - #[test] - fn test_add_json_numbers_f_f() { - let res = add_json_numbers( - &Number::from_f64(1.0).unwrap(), - &Number::from_f64(1.0).unwrap(), - ); - assert_eq!(res, Ok(Number::from_f64(2.0).unwrap())); - } - - #[test] - fn test_patch() { - let mut doc = json!({}); - - let patches = vec![ - PatchOperation::Huly(HulyPatchOperation::Add(AddOperationExt { - path: PointerBuf::from_tokens(["a"]), - value: json!([]), - safe: false, - })), - PatchOperation::Huly(HulyPatchOperation::Inc(IncOperationExt { - path: PointerBuf::from_tokens(["a", "0"]), - value: json!(1), - safe: true, - })), - PatchOperation::Huly(HulyPatchOperation::Add(AddOperationExt { - path: PointerBuf::from_tokens(["a", "0"]), - value: json!(0), - safe: false, - })), - PatchOperation::Huly(HulyPatchOperation::Inc(IncOperationExt { - path: PointerBuf::from_tokens(["a", "0"]), - value: json!(2), - safe: true, - })), - ]; - - let res = apply(&mut doc, &patches); - assert!(res.is_ok()); - assert_eq!( - doc, - json!({ - "a": [2] - }) - ); - } - - #[test] - fn test_patch_err() { - let mut doc = json!({}); - - let patches = vec![ - PatchOperation::Standard(StandardPatchOperation::Add(json_patch::AddOperation { - path: PointerBuf::from_tokens(["a"]), - value: json!([]), - })), - PatchOperation::Standard(StandardPatchOperation::Add(json_patch::AddOperation { - path: PointerBuf::from_tokens(["a", "b"]), - value: json!(1), - })), - ]; - - let res = apply(&mut doc, &patches); - assert_eq!( - res, - Err(HulyPatchError::PatchError(String::from( - "operation '/0' failed at path '/a/b': path is invalid" - ))) - ); - } - - #[test] - fn test_deserialize_add_safe() { - let patch = r#"{ "hop": "add", "path": "/a", "value": 1, "safe": true }"#; - let res = serde_json::from_str::(patch); - assert!(res.is_ok()); - assert_eq!( - res.unwrap(), - PatchOperation::Huly(HulyPatchOperation::Add(AddOperationExt { - path: PointerBuf::from_tokens(["a"]), - value: json!(1), - safe: true, - })) - ); - } - - #[test] - fn test_deserialize_add_unsafe() { - let patch = r#"{ "hop": "add", "path": "/a", "value": 1 }"#; - let res = serde_json::from_str::(patch); - assert!(res.is_ok()); - assert_eq!( - res.unwrap(), - PatchOperation::Huly(HulyPatchOperation::Add(AddOperationExt { - path: PointerBuf::from_tokens(["a"]), - value: json!(1), - safe: false, - })) - ); - } - - #[test] - fn test_deserialize_inc_safe() { - let patch = r#"{ "hop": "inc", "path": "/a", "value": 1, "safe": true }"#; - let res = serde_json::from_str::(patch); - assert!(res.is_ok()); - assert_eq!( - res.unwrap(), - PatchOperation::Huly(HulyPatchOperation::Inc(IncOperationExt { - path: PointerBuf::from_tokens(["a"]), - value: json!(1), - safe: true, - })) - ); - } - - #[test] - fn test_deserialize_inc_unsafe() { - let patch = r#"{ "hop": "inc", "path": "/a", "value": 1 }"#; - let res = serde_json::from_str::(patch); - assert!(res.is_ok()); - assert_eq!( - res.unwrap(), - PatchOperation::Huly(HulyPatchOperation::Inc(IncOperationExt { - path: PointerBuf::from_tokens(["a"]), - value: json!(1), - safe: false, - })) - ); - } -} diff --git a/foundations/hulylake/server/src/postgres.rs b/foundations/hulylake/server/src/postgres.rs deleted file mode 100644 index bd52e0b769..0000000000 --- a/foundations/hulylake/server/src/postgres.rs +++ /dev/null @@ -1,220 +0,0 @@ -use std::pin::Pin; - -use bb8_postgres::PostgresConnectionManager; -use bytes::Bytes; -use serde::de::DeserializeOwned; -use tokio_postgres::NoTls; -use tokio_postgres::{self as pg}; -use tracing::*; - -use crate::config::CONFIG; - -pub type Pool = bb8::Pool>; - -#[derive(thiserror::Error, Debug)] -pub enum DbError { - #[error(transparent)] - Pool(#[from] bb8::RunError), - - #[error(transparent)] - Db(#[from] tokio_postgres::Error), - - #[error(transparent)] - Json(#[from] serde_json::Error), - - #[error(transparent)] - Refinery(#[from] refinery::Error), - - #[error(transparent)] - Other(#[from] anyhow::Error), -} - -pub async fn pool() -> anyhow::Result { - let manager = bb8_postgres::PostgresConnectionManager::new_from_stringlike( - &CONFIG.db_connection, - tokio_postgres::NoTls, - )?; - - #[derive(Debug)] - struct ConnectionCustomizer; - - impl bb8::CustomizeConnection for ConnectionCustomizer { - fn on_acquire<'a>( - &'a self, - client: &'a mut pg::Client, - ) -> Pin> + Send + 'a>> { - Box::pin(async { - client - .execute("set search_path to $1", &[&CONFIG.db_scheme]) - .await - .unwrap(); - Ok(()) - }) - } - } - - let pool = bb8::Pool::builder() - .max_size(15) - .connection_customizer(Box::new(ConnectionCustomizer)) - .build(manager) - .await?; - - { - let mut connection = pool.dedicated_connection().await?; - - // query params cannot be bound in ddl statements - connection - .execute( - &format!("create schema if not exists {}", CONFIG.db_scheme), - &[], - ) - .await?; - - refinery::embed_migrations!("etc/migrations"); - - let report = migrations::runner() - .set_migration_table_name("migrations") - .run_async(&mut connection) - .await?; - - for m in report.applied_migrations().iter() { - info!(migration = m.to_string(), "apply migration"); - } - } - - Ok(pool) -} - -#[instrument(level = "debug", skip_all)] -async fn get_connection( - pool: &Pool, -) -> Result>, DbError> { - Ok(pool.get().await?) -} - -#[instrument(level = "debug", skip_all)] -pub async fn find_blob_by_hash(pool: &Pool, hash: &str) -> anyhow::Result, DbError> { - let connection = get_connection(pool).await?; - - let blob = connection - .query("select key from blob where hash = $1", &[&hash]) - .await?; - - Ok(match blob.as_slice() { - [found] => Some(found.get::<_, String>("key")), - [] => None, - - _ => panic!(), - }) -} - -#[instrument(level = "debug", skip_all)] -pub async fn insert_blob(pool: &Pool, key: &str, hash: &str) -> anyhow::Result<(), DbError> { - let connection = get_connection(pool).await?; - - connection - .execute( - "insert into blob (key, hash) values ($1, $2)", - &[&key, &hash], - ) - .await?; - - Ok(()) -} - -#[derive(Debug, Clone)] -pub struct ObjectPart { - pub inline: Option>, - pub data: T, -} - -#[instrument(level = "debug", skip_all)] -pub async fn find_parts( - pool: &Pool, - workspace: uuid::Uuid, - key: &str, -) -> anyhow::Result>, DbError> { - let connection = get_connection(pool).await?; - - let rows = connection - .query( - "select part, data, inline from object where workspace = $1 and key = $2 order by part", - &[&workspace, &key], - ) - .await?; - - let mut parts = Vec::with_capacity(rows.len()); - - for row in rows { - let data = row.get::<_, serde_json::Value>("data"); - let inline = row.get::<_, Option>>("inline"); - - let data = serde_json::from_value(data)?; - parts.push(ObjectPart { inline, data }) - } - - Ok(parts) -} - -#[instrument(level = "debug", skip_all)] -pub async fn append_part( - pool: &Pool, - workspace: uuid::Uuid, - key: &str, - part: u32, - inline: Option, - data: &D, -) -> anyhow::Result<(), DbError> { - let connection = get_connection(pool).await?; - - let data = serde_json::to_value(data)?; - let inline = inline.map(|b| b.to_vec()); - - connection - .execute( - "insert into object (workspace, key, part, inline, data) values ($1, $2, $3, $4, $5)", - &[&workspace, &key, &(part as i32), &inline, &data], - ) - .await?; - - Ok(()) -} - -#[instrument(level = "debug", skip_all)] -pub async fn set_part( - pool: &Pool, - workspace: uuid::Uuid, - key: &str, - inline: Option, - data: &D, -) -> anyhow::Result<(), DbError> { - let mut connection = get_connection(pool).await?; - - let transaction = connection.transaction().await?; - - transaction - .execute( - "delete from object where workspace = $1 and key = $2", - &[&workspace, &key], - ) - .await?; - - let data = serde_json::to_value(data)?; - let inline = inline.map(|b| b.to_vec()); - - transaction - .execute( - r#" - insert into object (workspace, key, part, inline, data) values ($1, $2, 0, $3, $4) - on conflict (workspace, key, part) do update set - inline = $3, - data = $4 - "#, - &[&workspace, &key, &inline, &data], - ) - .await?; - - transaction.commit().await?; - - Ok(()) -} diff --git a/foundations/hulylake/server/src/recovery.rs b/foundations/hulylake/server/src/recovery.rs deleted file mode 100644 index cf3a399157..0000000000 --- a/foundations/hulylake/server/src/recovery.rs +++ /dev/null @@ -1,91 +0,0 @@ -use aws_sdk_s3::error::SdkError; -use aws_sdk_s3::operation::put_object::PutObjectError; -use bytes::Bytes; - -use crate::conditional::ConditionalMatch; -use crate::config::CONFIG; -use crate::{handlers::PartData, s3::S3Client}; - -#[derive(thiserror::Error, Debug)] -pub enum RecoveryError { - #[error("S3 Error: {0}")] - S3(String), - - #[error("Precondition Failed")] - PreconditionFailed, - - #[error(transparent)] - Other(#[from] anyhow::Error), -} - -impl From for RecoveryError { - fn from(err: serde_json::Error) -> Self { - RecoveryError::Other(err.into()) - } -} - -impl From> for RecoveryError { - fn from(err: SdkError) -> Self { - match err { - SdkError::ServiceError(service_err) if service_err.raw().status().as_u16() == 412 => { - RecoveryError::PreconditionFailed - } - _ => RecoveryError::S3(err.to_string()), - } - } -} - -pub fn object_etag(parts: Vec<&PartData>) -> anyhow::Result { - let body = Bytes::from(serde_json::to_string(&parts)?); - let digest = md5::compute(body); - Ok(format!("{:x}", digest)) -} - -#[tracing::instrument(level = "debug", skip_all)] -pub async fn set_object( - s3: &S3Client, - workspace: uuid::Uuid, - key: &str, - parts: Vec<&PartData>, - conditions: Option, -) -> Result<(), RecoveryError> { - let s3_bucket = &CONFIG.s3_bucket; - - let key = format!("blob/{}/{}", workspace, key); - let body = Bytes::from(serde_json::to_string(&parts)?); - - let mut cmd = s3 - .put_object() - .bucket(s3_bucket) - .key(key) - .body(body.into()) - .content_type("application/json"); - - cmd = match conditions { - Some(ConditionalMatch::IfMatch(etag)) => cmd.if_match(etag), - Some(ConditionalMatch::IfNoneMatch(etag)) => cmd.if_none_match(etag), - None => cmd, - }; - - cmd.send().await?; - - Ok(()) -} - -#[tracing::instrument(level = "debug", skip_all)] -pub async fn set_blob(s3: &S3Client, key: &str, hash: &str) -> Result<(), RecoveryError> { - let s3_bucket = &CONFIG.s3_bucket; - - let key = format!("hash/{}", key); - let body = Bytes::from(hash.to_string()); - - s3.put_object() - .bucket(s3_bucket) - .key(key) - .body(body.into()) - .content_type("text/plain") - .send() - .await?; - - Ok(()) -} diff --git a/foundations/hulylake/server/src/s3.rs b/foundations/hulylake/server/src/s3.rs deleted file mode 100644 index 8d19a01120..0000000000 --- a/foundations/hulylake/server/src/s3.rs +++ /dev/null @@ -1,177 +0,0 @@ -use std::error::Error as StdError; - -use anyhow::Result; -use aws_config::BehaviorVersion; -use aws_sdk_s3::{ - Config, - types::{CompletedMultipartUpload, CompletedPart}, -}; -use blake3::{Hash, Hasher}; -use bytes::{Bytes, BytesMut}; -use futures::stream::StreamExt; -use futures_util::Stream; -use tracing::*; - -pub type S3Client = aws_sdk_s3::Client; - -pub async fn client() -> S3Client { - let ref sdk_config = aws_config::defaults(BehaviorVersion::latest()) - .load() - .await - .into_builder() - .build(); - - let s3_config = Config::from(sdk_config) - .to_builder() - .force_path_style(true) - .build(); - - S3Client::from_conf(s3_config) -} - -pub struct Upload { - pub hash: Hash, - pub length: usize, - pub parts_count: usize, -} - -async fn multipart_upload_stream( - s3: &S3Client, - bucket: &str, - key: &str, - upload_id: &str, - mut source: S, -) -> Result<(CompletedMultipartUpload, Upload)> -where - S: Stream> + Unpin, - E: StdError + Send + Sync + 'static, -{ - debug!("upload start"); - - let upload_part = async |number, buffer: Bytes| -> Result { - let upload = s3 - .upload_part() - .bucket(bucket) - .key(key) - .upload_id(upload_id) - .body(buffer.into()) - .part_number(number) - .send() - .await?; - - let part = CompletedPart::builder() - .e_tag(upload.e_tag.unwrap()) - .part_number(number) - .build(); - - Ok(part) - }; - - let mut buffer = BytesMut::with_capacity(1024 * 1024 * 6); - let mut complete = CompletedMultipartUpload::builder(); - let mut part_number = 1; - let mut hash = Hasher::new(); - let mut total_in = 0; - let mut length = 0; - - while let Some(part) = source.next().await { - let part = part.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; - - hash.update(&part); - - total_in += part.len(); - - buffer.extend_from_slice(&part); - - // each part must be at least 5MB - if buffer.len() > 1024 * 1024 * 5 { - trace!(length = buffer.len(), part_number, "upload part"); - - length += buffer.len(); - - let uploaded = upload_part(part_number, buffer.freeze()).await?; - - complete = complete.parts(uploaded); - - buffer = BytesMut::new(); - part_number += 1; - } - } - - // the last part - if buffer.len() > 0 { - length += buffer.len(); - - trace!(length = buffer.len(), part_number, "upload part"); - let uploaded = upload_part(part_number, buffer.freeze()).await?; - complete = complete.parts(uploaded); - } - - assert_eq!(total_in, length); - - let hash = hash.finalize(); - - let complete = complete.build(); - let parts_count = complete.parts().len(); - - Ok(( - complete, - Upload { - hash, - length, - parts_count, - }, - )) -} - -#[tracing::instrument(level = "debug", skip_all)] -pub async fn multipart_upload( - s3: &S3Client, - bucket: &str, - key: &str, - source: S, -) -> Result -where - S: Stream> + Unpin, - E: StdError + Send + Sync + 'static, -{ - let span = Span::current(); - - let create_multipart = s3 - .create_multipart_upload() - .bucket(bucket) - .key(key) - .send() - .await?; - - let upload_id = create_multipart.upload_id().unwrap(); - - span.record("upload", &upload_id[upload_id.len().saturating_sub(16)..]); - - match multipart_upload_stream(s3, bucket, key, upload_id, source).await { - Ok((complete, upload)) => { - s3.complete_multipart_upload() - .bucket(bucket) - .key(key) - .multipart_upload(complete) - .upload_id(upload_id) - .send() - .await?; - - debug!(hash = %upload.hash, length = upload.length, "upload complete"); - - Ok(upload) - } - Err(error) => { - s3.abort_multipart_upload() - .bucket(bucket) - .key(key) - .upload_id(upload_id) - .send() - .await?; - - error!(%error, "upload error"); - Err(error) - } - } -} diff --git a/foundations/hulylake/tests/Cargo.toml b/foundations/hulylake/tests/Cargo.toml deleted file mode 100644 index 0c4bd1c291..0000000000 --- a/foundations/hulylake/tests/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "tests" -version = "0.1.0" -edition = "2024" - -[dependencies] -tanu = "0.9.0" -tokio = { version = "1.47.1", features = ["full"] } -uuid = { version = "1.18.0" } -hulyrs = { git = "https://github.com/hcengineering/hulyrs.git", features = [ - "actix", -] } -secrecy = "0.10.3" -reqwest = { version = "0.12.23", default-features = false, features = [ - "json", - "rustls-tls", - "stream", -] } -rand = "0.9.2" -hex = "0.4.3" -serde_json = "1.0.143" -futures = "0.3.31" diff --git a/foundations/hulylake/tests/src/auth.rs b/foundations/hulylake/tests/src/auth.rs deleted file mode 100644 index 17fe163cf1..0000000000 --- a/foundations/hulylake/tests/src/auth.rs +++ /dev/null @@ -1,41 +0,0 @@ -use secrecy::ExposeSecret; -use tanu::{ - check, check_eq, check_ne, eyre, - http::{Client, Method, StatusCode}, -}; - -use crate::config::CONFIG; -use crate::util::*; - -// #[tanu::test("HEAD", Method::HEAD)] -#[tanu::test("GET", Method::GET)] -#[tanu::test("PUT", Method::PUT)] -#[tanu::test("POST", Method::POST)] -#[tanu::test("DELETE", Method::DELETE)] -async fn auth_without_token_unauthorized(_: &str, method: Method) -> eyre::Result<()> { - let http = Client::new(); - - let res = http.request(&method, &path(&random_key())).send().await?; - check!(!res.status().is_success(), "method: {method}"); - check_eq!(StatusCode::UNAUTHORIZED, res.status(), "method: {method}"); - - Ok(()) -} - -// #[tanu::test("HEAD", Method::HEAD)] -#[tanu::test("GET", Method::GET)] -#[tanu::test("PUT", Method::PUT)] -#[tanu::test("POST", Method::POST)] -// #[tanu::test("DELETE", Method::DELETE)] -async fn auth_with_token(_: &str, method: Method) -> eyre::Result<()> { - let http = Client::new(); - - let res = http - .request(&method, &path(&random_key())) - .bearer_auth(CONFIG.token_valid.expose_secret()) - .send() - .await?; - check_ne!(StatusCode::UNAUTHORIZED, res.status(), "method: {method}"); - - Ok(()) -} diff --git a/foundations/hulylake/tests/src/compact.rs b/foundations/hulylake/tests/src/compact.rs deleted file mode 100644 index f482d14c93..0000000000 --- a/foundations/hulylake/tests/src/compact.rs +++ /dev/null @@ -1,202 +0,0 @@ -use serde_json::{self as json, Value, json}; -use tanu::{check, eyre, http::Client}; - -use crate::util::*; - -#[tanu::test(50)] -#[tanu::test(100)] -#[tanu::test(200)] -#[tanu::test(500)] -pub async fn compact_json(count: usize) -> eyre::Result<()> { - let key = random_key(); - - let http = Client::new(); - - let initial = json!({ - "a": 0 - }); - - // create new blob - let res = http - .key_put(&key) - .body(json::to_string(&initial)?) - .header("huly-merge-strategy", "jsonpatch") - .header("content-type", "application/json") - .send() - .await?; - - check!(res.status().is_success(), "{:#?}", res); - - for i in 0..count { - let patch = json!([ - { "op": "replace", "path": "/a", "value": i + 1}, - ]); - - let body: String = json::to_string(&patch)?; - let res = http - .key_patch(&key) - .body(body) - .header("content-type", "application/json-patch+json") - .send() - .await?; - - check!(res.status().is_success(), "{:#?}", res); - } - - let res = http.key_get(&key).send().await?; - check!(res.status().is_success(), "{:#?}", res); - let json = res.json::().await?; - assert_eq!(json, json!({ "a": count })); - - tokio::time::sleep(std::time::Duration::from_millis(1000)).await; - - let res = http.key_get(&key).send().await?; - check!(res.status().is_success(), "{:#?}", res); - let json = res.json::().await?; - assert_eq!(json, json!({ "a": count })); - - Ok(()) -} - -#[tanu::test(50, 10)] -#[tanu::test(100, 10)] -#[tanu::test(100, 50)] -#[tanu::test(100, 100)] -pub async fn compact_text(count: usize, size: usize) -> eyre::Result<()> { - let key = random_key(); - let patch = random_text(1024 * size); - - let http = Client::new(); - - let initial = json!({ - "a": 0 - }); - - // create new blob - let res = http - .key_put(&key) - .body(json::to_string(&initial)?) - .header("content-type", "text/plain") - .send() - .await?; - - check!(res.status().is_success(), "{:#?}", res); - - for _ in 0..count { - let body: String = json::to_string(&patch)?; - let res = http - .key_patch(&key) - .body(body) - .header("content-type", "text/plain") - .send() - .await?; - - check!(res.status().is_success(), "{:#?}", res); - } - - let res = http.key_get(&key).send().await?; - check!(res.status().is_success(), "{:#?}", res); - - tokio::time::sleep(std::time::Duration::from_millis(1000)).await; - - let res = http.key_get(&key).send().await?; - check!(res.status().is_success(), "{:#?}", res); - - Ok(()) -} - -#[tanu::test(50, 10)] -#[tanu::test(100, 10)] -#[tanu::test(100, 50)] -#[tanu::test(100, 100)] -pub async fn compact_get_text(count: usize, size: usize) -> eyre::Result<()> { - let key = random_key(); - let patch = random_text(1024 * size); - - let http = Client::new(); - - let initial = json!({ - "a": 0 - }); - - // create new blob - let res = http - .key_put(&key) - .body(json::to_string(&initial)?) - .header("content-type", "text/plain") - .send() - .await?; - - check!(res.status().is_success(), "{:#?}", res); - - for _ in 0..count { - let body: String = json::to_string(&patch)?; - let res = http - .key_patch(&key) - .body(body) - .header("content-type", "text/plain") - .send() - .await?; - - check!(res.status().is_success(), "{:#?}", res); - - let res = http.key_get(&key).send().await?; - check!(res.status().is_success(), "{:#?}", res); - } - - tokio::time::sleep(std::time::Duration::from_millis(1000)).await; - - let res = http.key_get(&key).send().await?; - check!(res.status().is_success(), "{:#?}", res); - - Ok(()) -} - -#[tanu::test(50)] -#[tanu::test(100)] -pub async fn compact_get_json(count: usize) -> eyre::Result<()> { - let key = random_key(); - let text = random_text(1024 * 10); - let http = Client::new(); - - let initial = json!({ - "a": [] - }); - - // create new blob - let res = http - .key_put(&key) - .body(json::to_string(&initial)?) - .header("huly-merge-strategy", "jsonpatch") - .header("content-type", "application/json") - .send() - .await?; - - check!(res.status().is_success(), "{:#?}", res); - - for i in 0..count { - let patch = json!([ - { "op": "add", "path": format!("/a/{}", i), "value": text}, - ]); - - let body: String = json::to_string(&patch)?; - let res = http - .key_patch(&key) - .body(body) - .header("content-type", "application/json-patch+json") - .send() - .await?; - - check!(res.status().is_success(), "{:#?}", res); - - let res = http.key_get(&key).send().await?; - check!(res.status().is_success(), "{:#?}", res); - } - - tokio::time::sleep(std::time::Duration::from_millis(1000)).await; - - let res = http.key_get(&key).send().await?; - check!(res.status().is_success(), "{:#?}", res); - - Ok(()) -} diff --git a/foundations/hulylake/tests/src/config.rs b/foundations/hulylake/tests/src/config.rs deleted file mode 100644 index fb9305dd12..0000000000 --- a/foundations/hulylake/tests/src/config.rs +++ /dev/null @@ -1,44 +0,0 @@ -use std::sync::LazyLock; - -use hulyrs::services::jwt::ClaimsBuilder; -use secrecy::SecretString; -use uuid::{Uuid, uuid}; - -pub struct Config { - pub base_url: String, - pub workspace: Uuid, - pub token_valid: SecretString, - pub token_invalid: SecretString, -} - -pub static CONFIG: LazyLock = LazyLock::new(|| { - let config = tanu::get_config(); - - let secret = SecretString::from("secret"); - - let workspace = uuid!("dc06ad17-58af-4166-beca-48e40c990d51"); - let workspace_invalid = uuid!("2f52775a-57a1-4fc1-aa90-d6cde40d4b5a"); - let account = uuid!("34cee70d-52c4-47a1-a33a-7fec6e3e7ada"); - - let claims_valid = ClaimsBuilder::default() - .workspace(workspace) - .account(account) - .build() - .unwrap(); - - let claims_invalid = ClaimsBuilder::default() - .workspace(workspace_invalid) - .account(account) - .build() - .unwrap(); - - let token_valid = claims_valid.encode(&secret).unwrap(); - let token_invalid = claims_invalid.encode(&secret).unwrap(); - - Config { - base_url: config.get_str("base_url").unwrap().to_string(), - workspace, - token_valid, - token_invalid, - } -}); diff --git a/foundations/hulylake/tests/src/get.rs b/foundations/hulylake/tests/src/get.rs deleted file mode 100644 index dde207b920..0000000000 --- a/foundations/hulylake/tests/src/get.rs +++ /dev/null @@ -1,156 +0,0 @@ -use tanu::{ - check, check_eq, eyre, - http::{self, Client}, -}; - -use crate::util::*; - -#[tanu::test] -pub async fn get_unknown() -> eyre::Result<()> { - let http = Client::new(); - - let res = http.key_get(&random_key()).send().await?; - check!(!res.status().is_success()); - check_eq!(http::StatusCode::NOT_FOUND, res.status()); - - Ok(()) -} - -#[tanu::test] -pub async fn get_known() -> eyre::Result<()> { - let key = random_key(); - let text = random_text(1024); - - let http = Client::new(); - - let res = http.key_put(&key).body(text.clone()).send().await?; - check!(res.status().is_success()); - - let res = http.key_get(&key).send().await?; - check!(res.status().is_success()); - check_eq!( - Some(text.len().to_string().as_str()), - res.header("content-length"), - ); - check!(res.header("etag").is_some()); - check_eq!(text, res.text().await?); - - Ok(()) -} - -#[tanu::test] -pub async fn get_conditional() -> eyre::Result<()> { - let key = random_key(); - let text = random_text(1024); - - let http = Client::new(); - - let res = http.key_put(&key).body(text.clone()).send().await?; - check!(res.status().is_success()); - - let res = http.key_get(&key).send().await?; - check!(res.status().is_success()); - let etag = res.header("etag").expect("ETag not found"); - - // Test without If-None-Match (normal GET) - let res = http.key_get(&key).send().await?; - check!(res.status().is_success()); - check_eq!(text, res.text().await?); - - // Test with If-None-Match: * - let res = http - .key_get(&key) - .header("If-None-Match", "*") - .send() - .await?; - check_eq!(res.status(), http::StatusCode::NOT_MODIFIED); - - // Test with correct ETag - let res = http - .key_get(&key) - .header("If-None-Match", etag) - .send() - .await?; - check_eq!(res.status(), http::StatusCode::NOT_MODIFIED); - - // Test with incorrect ETag - let res = http - .key_get(&key) - .header("If-None-Match", "\"invalid-etag\"") - .send() - .await?; - check!(res.status().is_success()); - check_eq!(text, res.text().await?); - - Ok(()) -} - -#[tanu::test] -pub async fn get_partial() -> eyre::Result<()> { - let key = random_key(); - let text = random_text(1024 * 1024 * 5); - - let http = Client::new(); - - let res = http.key_put(&key).body(text.clone()).send().await?; - check!(res.status().is_success()); - - let res = http.key_get(&key).send().await?; - check!(res.status().is_success()); - check_eq!(res.header("accept-ranges"), Some("bytes")); - - let res = http - .key_get(&key) - .header("range", "bytes=0-127") - .send() - .await?; - check_eq!(res.status(), http::StatusCode::PARTIAL_CONTENT); - check_eq!(res.header("content-length"), Some("128")); - check_eq!(res.header("content-range"), Some("bytes 0-127/5242880")); - - let res = http - .key_get(&key) - .header("range", "bytes=0-5242879") - .send() - .await?; - check_eq!(res.status(), http::StatusCode::OK); - check_eq!(res.header("content-length"), Some("5242880")); - check_eq!(res.header("content-range"), Some("bytes 0-5242879/5242880")); - - Ok(()) -} - -#[tanu::test] -pub async fn get_partial_inline() -> eyre::Result<()> { - let key = random_key(); - let text = random_text(1024); - - let http = Client::new(); - - let res = http.key_put(&key).body(text.clone()).send().await?; - check!(res.status().is_success()); - - let res = http.key_get(&key).send().await?; - check!(res.status().is_success()); - check_eq!(res.header("accept-ranges"), Some("bytes")); - - let res = http - .key_get(&key) - .header("range", "bytes=0-31") - .send() - .await?; - check_eq!(res.status(), http::StatusCode::PARTIAL_CONTENT); - check_eq!(res.header("content-length"), Some("32")); - check_eq!(res.header("content-range"), Some("bytes 0-31/1024")); - - let res = http - .key_get(&key) - .header("range", "bytes=0-1023") - .send() - .await?; - check_eq!(res.status(), http::StatusCode::OK); - check_eq!(res.header("content-length"), Some("1024")); - check_eq!(res.header("content-range"), Some("bytes 0-1023/1024")); - - Ok(()) -} diff --git a/foundations/hulylake/tests/src/head.rs b/foundations/hulylake/tests/src/head.rs deleted file mode 100644 index 1cdd808e84..0000000000 --- a/foundations/hulylake/tests/src/head.rs +++ /dev/null @@ -1,111 +0,0 @@ -use serde_json::{self as json, json}; -use tanu::{ - check, check_eq, eyre, - http::{self, Client}, -}; - -use crate::util::*; - -#[tanu::test] -pub async fn head_unknown() -> eyre::Result<()> { - let http = Client::new(); - - let res = http.key_head(&random_key()).send().await?; - check!(!res.status().is_success()); - check_eq!(http::StatusCode::NOT_FOUND, res.status()); - - Ok(()) -} - -#[tanu::test] -pub async fn head_known() -> eyre::Result<()> { - let key = random_key(); - let text = random_text(1024); - - let http = Client::new(); - - let res = http.key_put(&key).body(text.clone()).send().await?; - check!(res.status().is_success()); - - let res = http.key_head(&key).send().await?; - check!(res.status().is_success()); - check_eq!( - Some(text.len().to_string().as_str()), - res.header("content-length"), - ); - check!(res.header("etag").is_some()); - check_eq!("", res.text().await?); - - Ok(()) -} - -#[tanu::test] -pub async fn head_known_with_jsonpatch() -> eyre::Result<()> { - let key = random_key(); - let payload = json!({ "foo": "bar" }); - - let http = Client::new(); - - // create new blob - let res = http - .key_put(&key) - .body(json::to_string(&payload)?) - .header("huly-merge-strategy", "jsonpatch") - .header("content-type", "application/json") - .send() - .await?; - check!(res.status().is_success()); - - let res = http.key_head(&key).send().await?; - check!(res.status().is_success()); - // despite the fact we are not setting content-length - // actix forces it to be returned as 0 - check_eq!(Some("0"), res.header("content-length")); - - Ok(()) -} - -#[tanu::test] -pub async fn head_conditional() -> eyre::Result<()> { - let key = random_key(); - let text = random_text(1024); - - let http = Client::new(); - - let res = http.key_put(&key).body(text.clone()).send().await?; - check!(res.status().is_success()); - - let res = http.key_head(&key).send().await?; - check!(res.status().is_success()); - let etag = res.header("etag").expect("ETag not found"); - - // Test without If-None-Match (normal GET) - let res = http.key_head(&key).send().await?; - check!(res.status().is_success()); - - // Test with If-None-Match: * - let res = http - .key_head(&key) - .header("If-None-Match", "*") - .send() - .await?; - check_eq!(res.status(), http::StatusCode::NOT_MODIFIED); - - // Test with correct ETag - let res = http - .key_head(&key) - .header("If-None-Match", etag) - .send() - .await?; - check_eq!(res.status(), http::StatusCode::NOT_MODIFIED); - - // Test with incorrect ETag - let res = http - .key_head(&key) - .header("If-None-Match", "\"invalid-etag\"") - .send() - .await?; - check!(res.status().is_success()); - - Ok(()) -} diff --git a/foundations/hulylake/tests/src/main.rs b/foundations/hulylake/tests/src/main.rs deleted file mode 100644 index c6fe12ba78..0000000000 --- a/foundations/hulylake/tests/src/main.rs +++ /dev/null @@ -1,20 +0,0 @@ -mod auth; -mod compact; -mod config; -mod get; -mod head; -mod patch; -mod put; -mod sanity; -mod util; - -use tanu::eyre; - -#[tanu::main] -#[tokio::main] -async fn main() -> eyre::Result<()> { - let runner = run(); - let app = tanu::App::new(); - app.run(runner).await?; - Ok(()) -} diff --git a/foundations/hulylake/tests/src/patch.rs b/foundations/hulylake/tests/src/patch.rs deleted file mode 100644 index 9d4f5c1c8d..0000000000 --- a/foundations/hulylake/tests/src/patch.rs +++ /dev/null @@ -1,393 +0,0 @@ -use futures::future::join_all; -use hulyrs::StatusCode; -use serde_json::{self as json, Value, json}; -use tanu::{check, check_eq, check_ne, eyre, http::Client}; - -use crate::util::*; - -#[tanu::test((10, 10))] -pub async fn put_and_patch_content((initial, patch): (usize, usize)) -> eyre::Result<()> { - let key = random_key(); - let initial = random_text(1024 * initial); - let patch = random_text(1024 * patch); - - let http = Client::new(); - - // patch non-existing blob - let res = http.key_patch(&key).body(patch.clone()).send().await?; - check_eq!(res.status(), StatusCode::NOT_FOUND, "{:#?}", res); - - // create new blob - let res = http.key_put(&key).body(initial.clone()).send().await?; - check!(res.status().is_success(), "{:#?}", res); - - // check content - let res = http.key_get(&key).send().await?; - check!(res.status().is_success(), "{:#?}", res); - check_eq!(initial, res.text().await?); - - // patch - let res = http.key_patch(&key).body(patch.clone()).send().await?; - check!(res.status().is_success(), "{:#?}", res); - - // check content - let res = http.key_get(&key).send().await?; - check!(res.status().is_success(), "{:#?}", res); - check_eq!(res.text().await?, format!("{}{}", initial, patch)); - - Ok(()) -} - -use super::put::Body; - -#[tanu::test(1, None, Body::Text("{}"), StatusCode::BAD_REQUEST)] -#[tanu::test(2, Some("application/json"), Body::Text("{}"), StatusCode::BAD_REQUEST)] -#[tanu::test( - 3, - Some("application/json-patch+json"), - Body::Text("{"), - StatusCode::BAD_REQUEST -)] -#[tanu::test( - 4, - Some("application/json-patch+json"), - Body::Text("{}"), - StatusCode::BAD_REQUEST -)] -pub async fn put_and_patch_json_patch( - _: usize, - content_type: Option<&str>, - body: Body, - status: StatusCode, -) -> eyre::Result<()> { - let key = random_key(); - - let http = Client::new(); - - // create new blob - let res = http - .key_put(&key) - .body("{}") - .header("huly-merge-strategy", "jsonpatch") - .header("content-type", "application/json") - .send() - .await?; - - check!(res.status().is_success(), "{:#?}", res); - - let mut req = http.key_patch(&key); - - match body { - Body::Random(size) => { - req = req.body(random_body(size)); - } - Body::Text(text) => { - req = req.body(text.to_string()); - } - } - - if let Some(content_type) = content_type { - req = req.header("content-type", content_type); - } - - let res = req.send().await?; - - check!(res.status() == status); - - Ok(()) -} - -#[tanu::test] -async fn get_json_patch() -> eyre::Result<()> { - let key = random_key(); - - let http = Client::new(); - - let initial = json!({ - "a": 1, - "b": 2, - "c": 3 - }); - - // create new blob - let res = http - .key_put(&key) - .body(json::to_string(&initial)?) - .header("huly-merge-strategy", "jsonpatch") - .header("content-type", "application/json") - .send() - .await?; - - check!(res.status().is_success(), "{:#?}", res); - - let patch = json!([ - { "op": "add", "path": "/a", "value": 4 }, - { "op": "replace", "path": "/b", "value": 5 }, - { "op": "remove", "path": "/c" } - ]); - - let res = http - .key_patch(&key) - .body(json::to_string(&patch)?) - .header("content-type", "application/json-patch+json") - .send() - .await?; - - check!(res.status().is_success(), "{:#?}", res); - - let res = http.key_get(&key).send().await?; - - let json = res.json::().await?; - - assert_eq!(json, json!({ "a": 4, "b": 5 })); - - Ok(()) -} - -#[tanu::test] -async fn get_json_patch_unsafe() -> eyre::Result<()> { - let key = random_key(); - - let http = Client::new(); - - let initial = json!({ - "a": { - "b": 1 - } - }); - - // create new blob - let res = http - .key_put(&key) - .body(json::to_string(&initial)?) - .header("huly-merge-strategy", "jsonpatch") - .header("content-type", "application/json") - .send() - .await?; - - check!(res.status().is_success(), "{:#?}", res); - - let patch = json!([ - { "hop": "add", "path": "/a/b", "value": 0, "safe": false }, - { "hop": "inc", "path": "/a/c", "value": 1, "safe": false } - ]); - - let res = http - .key_patch(&key) - .body(json::to_string(&patch)?) - .header("content-type", "application/json-patch+json") - .send() - .await?; - - check!(res.status().is_success(), "{:#?}", res); - - let res = http.key_get(&key).send().await?; - - let json = res.json::().await?; - - assert_eq!(json, json!({ "a": { "b": 0, "c": 1 }})); - - Ok(()) -} - -#[tanu::test] -async fn get_json_patch_safe() -> eyre::Result<()> { - let key = random_key(); - - let http = Client::new(); - - let initial = json!({ - "a": { - "b": 1 - } - }); - - // create new blob - let res = http - .key_put(&key) - .body(json::to_string(&initial)?) - .header("huly-merge-strategy", "jsonpatch") - .header("content-type", "application/json") - .send() - .await?; - - check!(res.status().is_success(), "{:#?}", res); - - let patch = json!([ - { "hop": "add", "path": "/a/b", "value": 0, "safe": true }, - { "hop": "inc", "path": "/a/c", "value": 1, "safe": true } - ]); - - let res = http - .key_patch(&key) - .body(json::to_string(&patch)?) - .header("content-type", "application/json-patch+json") - .send() - .await?; - - check!(res.status().is_success(), "{:#?}", res); - - let res = http.key_get(&key).send().await?; - - let json = res.json::().await?; - - assert_eq!(json, json!({ "a": { "b": 1 }})); - - Ok(()) -} - -#[derive(PartialEq, Eq)] -pub enum IfMatch { - ETag, - Some(&'static str), - None, -} - -#[tanu::test(1, IfMatch::None, StatusCode::CREATED)] -#[tanu::test(2, IfMatch::ETag, StatusCode::CREATED)] -#[tanu::test(3, IfMatch::Some("*"), StatusCode::CREATED)] -#[tanu::test(4, IfMatch::Some("\"unknown\""), StatusCode::PRECONDITION_FAILED)] -pub async fn put_and_patch_conditional( - _: usize, - if_match: IfMatch, - status: StatusCode, -) -> eyre::Result<()> { - let key = random_key(); - let initial = random_text(1024); - let patch = random_text(1024); - - let http = Client::new(); - - // create new blob - let res = http.key_put(&key).body(initial.clone()).send().await?; - check!(res.status().is_success()); - - // check content - let res = http.key_get(&key).send().await?; - check!(res.status().is_success()); - let etag = res.header("etag").expect("ETag not found"); - - let mut req = http.key_patch(&key).body(patch.clone()); - - req = match if_match { - IfMatch::ETag => req.header("If-Match", etag), - IfMatch::Some(etag) => req.header("If-Match", etag), - IfMatch::None => req, - }; - - let res = req.send().await?; - check_eq!(res.status(), status); - - let res = http.key_get(&key).send().await?; - check!(res.status().is_success()); - - if status.is_success() { - check_ne!(res.header("etag"), Some(etag)); - } else { - check_eq!(res.header("etag"), Some(etag)); - } - - Ok(()) -} - -#[tanu::test] -pub async fn put_and_patch_json_err() -> eyre::Result<()> { - let key = random_key(); - - let http = Client::new(); - - let initial = json!({ - "a": 1 - }); - - // create new blob - let res = http - .key_put(&key) - .body(json::to_string(&initial)?) - .header("huly-merge-strategy", "jsonpatch") - .header("content-type", "application/json") - .send() - .await?; - - check!(res.status().is_success(), "{:#?}", res); - - let patch = json!([ - { "hop": "add", "path": "/a/b/c", "value": 0, "safe": false }, - ]); - - let res = http - .key_patch(&key) - .body(json::to_string(&patch)?) - .header("content-type", "application/json-patch+json") - .send() - .await?; - - check!(res.status().is_success(), "{:#?}", res); - let res = http.key_get(&key).send().await?; - check!(res.status().is_success(), "{:#?}", res); - - let json = res.json::().await?; - assert_eq!(json, json!({ "a": 1 })); - - Ok(()) -} - -#[tanu::test(1)] -#[tanu::test(2)] -#[tanu::test(5)] -#[tanu::test(10)] -#[tanu::test(50)] -pub async fn put_and_patch_concurrent(count: usize) -> eyre::Result<()> { - let key = random_key(); - - let http = Client::new(); - - let initial = json!({ - "a": 0 - }); - - // create new blob - let res = http - .key_put(&key) - .body(json::to_string(&initial)?) - .header("huly-merge-strategy", "jsonpatch") - .header("content-type", "application/json") - .send() - .await?; - - check!(res.status().is_success(), "{:#?}", res); - - let patch = json!([ - { "hop": "inc", "path": "/a", "value": 1 }, - ]); - - let body: String = json::to_string(&patch)?; - - let futures: Vec<_> = (0..count) - .map(|_| { - let key = key.clone(); - let body = body.clone(); - let http = http.clone(); - - async move { - http.key_patch(&key) - .body(body) - .header("content-type", "application/json-patch+json") - .send() - .await - } - }) - .collect(); - - let results = join_all(futures).await; - for result in results.iter() { - assert!(result.is_ok()) - } - - let res = http.key_get(&key).send().await?; - check!(res.status().is_success(), "{:#?}", res); - let json = res.json::().await?; - assert_eq!(json, json!({ "a": count })); - - Ok(()) -} diff --git a/foundations/hulylake/tests/src/put.rs b/foundations/hulylake/tests/src/put.rs deleted file mode 100644 index e0597cdb34..0000000000 --- a/foundations/hulylake/tests/src/put.rs +++ /dev/null @@ -1,414 +0,0 @@ -use hulyrs::StatusCode; -use tanu::{ - check, check_eq, eyre, - http::{self, Client}, -}; - -use crate::util::*; - -#[tanu::test] -pub async fn put_new_deduplicated_no_multipart() -> eyre::Result<()> { - let text = random_text(1024); - - let http = Client::new(); - - let res = http - .key_put(&random_key()) - .body(text.clone()) - .send() - .await?; - check!(res.status().is_success()); - check_eq!(None, res.header("huly-deduplicated")); - - let res = http - .key_put(&random_key()) - .body(text.clone()) - .send() - .await?; - check!(res.status().is_success()); - check_eq!(Some("true"), res.header("huly-deduplicated")); - - Ok(()) -} - -#[tanu::test(1)] -#[tanu::test(2)] -#[tanu::test(10)] -#[tanu::test(100)] -#[tanu::test(1024)] -#[tanu::test(1024 * 10)] -pub async fn put_new_sized_no_multipart(size: usize) -> eyre::Result<()> { - let key = random_key(); - let text = random_text(size); - - let http = Client::new(); - - // create new blob - let res = http - .key_put(&key) - .body(text.clone()) - .header(http::header::CONTENT_TYPE, "text/plain") - .send() - .await?; - - check!(res.status().is_success()); - check!(res.headers().get(http::header::ETAG).is_some()); - check_eq!(None, res.header("huly-parts-count")); - - // check content - let res = http.key_get(&key).send().await?; - check!(res.status().is_success()); - check_eq!( - "text/plain", - res.headers().get(http::header::CONTENT_TYPE).unwrap() - ); - check_eq!( - Some(size.to_string().as_str()), - res.header("content-length") - ); - check_eq!(text, res.text().await?); - - Ok(()) -} - -#[tanu::test] -pub async fn put_new_sized_multipart() -> eyre::Result<()> { - let text = random_text(1024 * 1024 * 5); // above multipart threshold - let http = Client::new(); - - let key1 = random_key(); - - let res = http.key_put(&key1).body(text.clone()).send().await?; - check!(res.status().is_success()); - - check!(res.status().is_success()); - check_eq!(Some("1"), res.header("huly-s3-parts-count")); - - let res = http.key_get(&key1).send().await?; - check!(res.status().is_success()); - check_eq!( - Some(text.len().to_string().as_str()), - res.header("content-length") - ); - check_eq!(text, res.text().await?); - - let key2 = random_key(); - - let res = http.key_put(&key2).body(text.clone()).send().await?; - check!(res.status().is_success()); - check_eq!(None, res.header("huly-s3-parts-count")); - check_eq!(Some("true"), res.header("huly-deduplicated")); - - let res = http.key_get(&key2).send().await?; - check!(res.status().is_success()); - check_eq!( - Some(text.len().to_string().as_str()), - res.header("content-length") - ); - check_eq!(text, res.text().await?); - - Ok(()) -} - -#[tanu::test] -pub async fn put_existing() -> eyre::Result<()> { - let key = random_key(); - let text1 = random_text(1024); - let text2 = random_text(1024 * 10); - - let http = Client::new(); - - // create new blob - let res = http.key_put(&key).body(text1.clone()).send().await?; - check!(res.status().is_success()); - - // update content - let res = http.key_put(&key).body(text2.clone()).send().await?; - check!(res.status().is_success()); - check!(res.headers().get(http::header::ETAG).is_some()); - - // check content - let res = http.key_get(&key).send().await?; - check!(res.status().is_success()); - check_eq!(text2, res.text().await?); - - Ok(()) -} - -#[tanu::test(1)] -#[tanu::test(1024)] -#[tanu::test(10 * 1024)] -#[tanu::test(1 * 1024 * 1024)] -#[tanu::test(10 * 1024 * 1024)] -pub async fn put_with_length(length: usize) -> eyre::Result<()> { - let key = random_key(); - let body = random_body(length); - - let http = Client::new(); - - // create new blob - let res = http.key_put(&key).body(body).send().await?; - check!(res.status().is_success()); - - // check content - let res = http.key_get(&key).send().await?; - check!(res.status().is_success()); - check_eq!( - length, - res.headers() - .get(http::header::CONTENT_LENGTH) - .unwrap() - .to_str()? - .parse::()?, - "Invalid content length" - ); - - Ok(()) -} - -#[tanu::test] -pub async fn put_with_zero_length() -> eyre::Result<()> { - let key = random_key(); - let body = random_body(0); - - let http = Client::new(); - - // create new blob - let res = http.key_put(&key).body(body).send().await?; - check!(!res.status().is_success()); - check_eq!(http::StatusCode::BAD_REQUEST, res.status()); - - Ok(()) -} - -#[tanu::test("huly-header-header", "header", "value1")] -#[tanu::test("huly-header-HeAdEr", "header", "value2")] -#[tanu::test("HULY-HEADER-HEADER", "header", "value3")] -#[tanu::test("HULY-HEADER-HEADER", "HEADER", "value4")] -pub async fn put_with_header_case( - req_header: &str, - res_header: &str, - value: &str, -) -> eyre::Result<()> { - let key = random_key(); - let body = random_body(1024 * 10); - - let http = Client::new(); - - // create new blob - let res = http - .key_put(&key) - .body(body) - .header(req_header, value) - .send() - .await?; - check!(res.status().is_success()); - - // check header is returned - let res = http.key_get(&key).send().await?; - check!(res.status().is_success()); - check_eq!(Some(value), res.header(res_header)); - - Ok(()) -} - -#[tanu::test] -pub async fn put_with_headers() -> eyre::Result<()> { - let key = random_key(); - let body = random_body(1024 * 10); - - let http = Client::new(); - - // create new blob - let res = http - .key_put(&key) - .body(body) - .header("huly-header-header1", "foo") - .header("huly-header-header2", "bar") - .header("huly-meta-meta1", "baz") - .header("content-type", "application/json") - .send() - .await?; - check!(res.status().is_success()); - - // check headers are returned - let res = http.key_get(&key).send().await?; - check!(res.status().is_success()); - check_eq!(Some("foo"), res.header("header1")); - check_eq!(Some("bar"), res.header("header2")); - check_eq!(Some("application/json"), res.header("content-type")); - check_eq!(None, res.header("meta1")); - - Ok(()) -} - -#[tanu::test] -pub async fn put_with_meta() -> eyre::Result<()> { - let key = random_key(); - let body = random_body(1024 * 10); - - let http = Client::new(); - - // create new blob - let res = http - .key_put(&key) - .body(body) - .header("huly-meta-meta1", "foo") - .header("huly-meta-meta2", "bar") - .send() - .await?; - check!(res.status().is_success()); - - // check meta is private - let res = http.key_get(&key).send().await?; - check!(res.status().is_success()); - check!(res.headers().get("meta1").is_none()); - check!(res.headers().get("meta2").is_none()); - - Ok(()) -} - -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum Body { - Random(usize), - Text(&'static str), -} - -#[tanu::test(1, None, None, Body::Text("{}"), StatusCode::CREATED)] -#[tanu::test(2, Some("invalid"), None, Body::Text("{}"), StatusCode::BAD_REQUEST)] -#[tanu::test(3, Some("jsonpatch"), None, Body::Text("{}"), StatusCode::BAD_REQUEST)] -#[tanu::test( - 4, - Some("jsonpatch"), - Some("application/xml"), - Body::Text("{}"), - StatusCode::BAD_REQUEST -)] -#[tanu::test( - 5, - Some("jsonpatch"), - Some("application/json"), - Body::Text("{"), - StatusCode::BAD_REQUEST -)] -#[tanu::test( - 6, - Some("jsonpatch"), - Some("application/json"), - Body::Random(1024 * 101), - StatusCode::BAD_REQUEST -)] -#[tanu::test( - 7, - Some("jsonpatch"), - Some("application/json"), - Body::Text("{}"), - StatusCode::CREATED -)] - -pub async fn put_merge_patch( - _: usize, - strategy: Option<&str>, - content_type: Option<&str>, - body: Body, - status: StatusCode, -) -> eyre::Result<()> { - let http = Client::new(); - let key = random_key(); - - let mut req = http.key_put(&key); - - match body { - Body::Random(size) => { - req = req.body(random_body(size)); - } - Body::Text(text) => { - req = req.body(text.to_string()); - } - } - - if let Some(strategy) = strategy { - req = req.header("huly-merge-strategy", strategy); - } - - if let Some(content_type) = content_type { - req = req.header("content-type", content_type); - } - - let res = req.send().await?; - - check!(res.status() == status); - - Ok(()) -} - -#[derive(PartialEq, Eq)] -pub enum Condition { - ETag, - IfMatch(&'static str), - IfNoneMatch(&'static str), -} - -#[tanu::test(1, Condition::ETag, StatusCode::CREATED)] -#[tanu::test(2, Condition::IfMatch("*"), StatusCode::PRECONDITION_FAILED)] -#[tanu::test(3, Condition::IfNoneMatch("*"), StatusCode::CREATED)] -#[tanu::test(4, Condition::IfNoneMatch("\"unknown\""), StatusCode::CREATED)] -pub async fn put_conditional_create( - _: usize, - condition: Condition, - status: StatusCode, -) -> eyre::Result<()> { - let key = random_key(); - let body = random_text(1024); - - let http = Client::new(); - - let mut req = http.key_put(&key).body(body.clone()); - - req = match condition { - Condition::ETag => req, - Condition::IfMatch(etag) => req.header("If-Match", etag), - Condition::IfNoneMatch(etag) => req.header("If-None-Match", etag), - }; - - let res = req.send().await?; - check_eq!(res.status(), status); - - Ok(()) -} - -#[tanu::test(1, Condition::ETag, StatusCode::CREATED)] -#[tanu::test(2, Condition::IfMatch("*"), StatusCode::CREATED)] -#[tanu::test(3, Condition::IfNoneMatch("*"), StatusCode::PRECONDITION_FAILED)] -#[tanu::test( - 4, - Condition::IfNoneMatch("\"unknown\""), - StatusCode::PRECONDITION_FAILED -)] -pub async fn put_conditional_update( - _: usize, - condition: Condition, - status: StatusCode, -) -> eyre::Result<()> { - let key = random_key(); - let body = random_text(1024); - - let http = Client::new(); - - let res = http.key_put(&key).body(body.clone()).send().await?; - check!(res.status().is_success()); - let etag = res.header("etag").expect("ETag not found"); - - let mut req = http.key_put(&key).body(body.clone()); - - req = match condition { - Condition::ETag => req.header("If-Match", etag), - Condition::IfMatch(etag) => req.header("If-Match", etag), - Condition::IfNoneMatch(etag) => req.header("If-None-Match", etag), - }; - - let res = req.send().await?; - check_eq!(res.status(), status); - - Ok(()) -} diff --git a/foundations/hulylake/tests/src/sanity.rs b/foundations/hulylake/tests/src/sanity.rs deleted file mode 100644 index 92a2fd257a..0000000000 --- a/foundations/hulylake/tests/src/sanity.rs +++ /dev/null @@ -1,14 +0,0 @@ -use crate::config::CONFIG; -use tanu::{check, check_eq, eyre, http::Client}; - -#[tanu::test] -async fn status_is_ok() -> eyre::Result<()> { - let http = Client::new(); - let res = http - .get(format!("{}/status", CONFIG.base_url)) - .send() - .await?; - check!(res.status().is_success()); - check_eq!("ok", res.text().await?); - Ok(()) -} diff --git a/foundations/hulylake/tests/src/util.rs b/foundations/hulylake/tests/src/util.rs deleted file mode 100644 index f318812894..0000000000 --- a/foundations/hulylake/tests/src/util.rs +++ /dev/null @@ -1,98 +0,0 @@ -use crate::config::CONFIG; -use rand::{Rng, RngCore}; -use reqwest::Body; -use secrecy::ExposeSecret; -use tanu::http::{Client, Method, RequestBuilder}; - -pub trait ClientExt { - fn key_head(&self, key: &str) -> RequestBuilder; - fn key_get(&self, key: &str) -> RequestBuilder; - fn key_put(&self, key: &str) -> RequestBuilder; - fn key_patch(&self, key: &str) -> RequestBuilder; - fn key_delete(&self, key: &str) -> RequestBuilder; - fn request(&self, method: &Method, path: &str) -> RequestBuilder; -} - -pub fn path(key: &str) -> String { - format!("{}/api/{}/{key}", CONFIG.base_url, CONFIG.workspace) -} - -impl ClientExt for Client { - fn request(&self, method: &Method, path: &str) -> RequestBuilder { - match *method { - Method::HEAD => self.head(path), - Method::GET => self.get(path), - Method::PUT => self.put(path), - Method::POST => self.post(path), - Method::DELETE => self.delete(path), - _ => panic!("unsupported method"), - } - } - - fn key_head(&self, key: &str) -> RequestBuilder { - self.head(path(key)) - .bearer_auth(CONFIG.token_valid.expose_secret()) - } - - fn key_get(&self, key: &str) -> RequestBuilder { - self.get(path(key)) - .bearer_auth(CONFIG.token_valid.expose_secret()) - } - - fn key_put(&self, key: &str) -> RequestBuilder { - self.put(path(key)) - .bearer_auth(CONFIG.token_valid.expose_secret()) - } - - fn key_patch(&self, key: &str) -> RequestBuilder { - self.patch(path(key)) - .bearer_auth(CONFIG.token_valid.expose_secret()) - } - - fn key_delete(&self, key: &str) -> RequestBuilder { - self.get(path(key)) - .bearer_auth(CONFIG.token_valid.expose_secret()) - } -} - -pub trait ResponseExt { - fn header(&self, key: &str) -> Option<&str>; -} - -impl ResponseExt for tanu::http::Response { - fn header(&self, key: &str) -> Option<&str> { - self.headers().get(key).and_then(|v| v.to_str().ok()) - } -} - -pub fn random_text(length: usize) -> String { - let mut rng = rand::rng(); - let charset: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 "; - - let text: String = (0..length) - .map(|_| { - let idx = rng.random_range(0..charset.len()); - charset[idx] as char - }) - .collect(); - - text -} - -pub fn random_body(size: usize) -> Body { - let mut rng = rand::rng(); - let mut bytes = vec![0u8; size]; - - rng.fill_bytes(&mut bytes); - - Body::from(bytes) -} - -pub fn random_key() -> String { - let mut rng = rand::rng(); - let mut bytes = vec![0u8; 16]; - - rng.fill_bytes(&mut bytes); - - hex::encode(bytes) -} diff --git a/foundations/hulylake/tests/tanu.toml b/foundations/hulylake/tests/tanu.toml deleted file mode 100644 index e9bdb5694a..0000000000 --- a/foundations/hulylake/tests/tanu.toml +++ /dev/null @@ -1,3 +0,0 @@ -[[projects]] -name = "dev" -base_url = "http://localhost:8096" diff --git a/foundations/hulypulse/.github/workflows/build.yml b/foundations/hulypulse/.github/workflows/build.yml deleted file mode 100644 index 387987e13c..0000000000 --- a/foundations/hulypulse/.github/workflows/build.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Hulypulse - -on: - workflow_dispatch: - push: - tags: - - 'v*.*.*' - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Log to registry - uses: docker/login-action@v3 - with: - username: ${{ vars.DOCKER_USER }} - password: ${{ secrets.DOCKER_ACCESS_TOKEN }} - - - run: echo VERSION=$(grep '^version =' Cargo.toml | cut -d '"' -f 2) >> $GITHUB_ENV - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build and Push - uses: docker/build-push-action@v6 - with: - file: Dockerfile - push: true - tags: "${{ vars.DOCKER_USER }}/service_hulypulse:${{ env.VERSION }},${{ vars.DOCKER_USER }}/service_hulypulse:latest" - platforms: linux/amd64,linux/arm64 diff --git a/foundations/hulypulse/.gitignore b/foundations/hulypulse/.gitignore deleted file mode 100644 index 543c428394..0000000000 --- a/foundations/hulypulse/.gitignore +++ /dev/null @@ -1,19 +0,0 @@ -/off -/target -/scripts/other -/scripts/off -/scripts/typing-test.sh -/scripts/TEST_LLEOTOKEN.html -Justfile -commit.sh -/src/GO.sh -/src/GOT.sh -GO.sh -TEST.sh -TEST_WS.sh -DROP_DB.sh -TODO.txt -DOCKER.sh -/lleo -/client -/scripts diff --git a/foundations/hulypulse/Cargo.lock b/foundations/hulypulse/Cargo.lock deleted file mode 100644 index 1d45a58ccd..0000000000 --- a/foundations/hulypulse/Cargo.lock +++ /dev/null @@ -1,4307 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "actix" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de7fa236829ba0841304542f7614c42b80fca007455315c45c785ccfa873a85b" -dependencies = [ - "actix-macros", - "actix-rt", - "actix_derive", - "bitflags 2.9.4", - "bytes", - "crossbeam-channel", - "futures-core", - "futures-sink", - "futures-task", - "futures-util", - "log", - "once_cell", - "parking_lot 0.12.4", - "pin-project-lite", - "smallvec", - "tokio", - "tokio-util", -] - -[[package]] -name = "actix-codec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" -dependencies = [ - "bitflags 2.9.4", - "bytes", - "futures-core", - "futures-sink", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "actix-cors" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daa239b93927be1ff123eebada5a3ff23e89f0124ccb8609234e5103d5a5ae6d" -dependencies = [ - "actix-utils", - "actix-web", - "derive_more", - "futures-util", - "log", - "once_cell", - "smallvec", -] - -[[package]] -name = "actix-http" -version = "3.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44cceded2fb55f3c4b67068fa64962e2ca59614edc5b03167de9ff82ae803da0" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-tls", - "actix-utils", - "base64 0.22.1", - "bitflags 2.9.4", - "brotli", - "bytes", - "bytestring", - "derive_more", - "encoding_rs", - "flate2", - "foldhash", - "futures-core", - "h2", - "http 0.2.12", - "httparse", - "httpdate", - "itoa", - "language-tags", - "local-channel", - "mime", - "percent-encoding", - "pin-project-lite", - "rand 0.9.2", - "sha1", - "smallvec", - "tokio", - "tokio-util", - "tracing", - "zstd", -] - -[[package]] -name = "actix-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" -dependencies = [ - "quote", - "syn", -] - -[[package]] -name = "actix-router" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" -dependencies = [ - "bytestring", - "cfg-if", - "http 0.2.12", - "regex", - "regex-lite", - "serde", - "tracing", -] - -[[package]] -name = "actix-rt" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63" -dependencies = [ - "futures-core", - "tokio", -] - -[[package]] -name = "actix-server" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "futures-util", - "mio", - "socket2 0.5.10", - "tokio", - "tracing", -] - -[[package]] -name = "actix-service" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" -dependencies = [ - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "actix-tls" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac453898d866cdbecdbc2334fe1738c747b4eba14a677261f2b768ba05329389" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "impl-more", - "pin-project-lite", - "tokio", - "tokio-rustls 0.23.4", - "tokio-util", - "tracing", - "webpki-roots 0.22.6", -] - -[[package]] -name = "actix-utils" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" -dependencies = [ - "local-waker", - "pin-project-lite", -] - -[[package]] -name = "actix-web" -version = "4.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a597b77b5c6d6a1e1097fddde329a83665e25c5437c696a3a9a4aa514a614dea" -dependencies = [ - "actix-codec", - "actix-http", - "actix-macros", - "actix-router", - "actix-rt", - "actix-server", - "actix-service", - "actix-tls", - "actix-utils", - "actix-web-codegen", - "bytes", - "bytestring", - "cfg-if", - "cookie", - "derive_more", - "encoding_rs", - "foldhash", - "futures-core", - "futures-util", - "impl-more", - "itoa", - "language-tags", - "log", - "mime", - "once_cell", - "pin-project-lite", - "regex", - "regex-lite", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "socket2 0.5.10", - "time", - "tracing", - "url", -] - -[[package]] -name = "actix-web-actors" -version = "4.3.1+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98c5300b38fd004fe7d2a964f9a90813fdbe8a81fed500587e78b1b71c6f980" -dependencies = [ - "actix", - "actix-codec", - "actix-http", - "actix-web", - "bytes", - "bytestring", - "futures-core", - "pin-project-lite", - "tokio", - "tokio-util", -] - -[[package]] -name = "actix-web-codegen" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" -dependencies = [ - "actix-router", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "actix-ws" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3a1fb4f9f2794b0aadaf2ba5f14a6f034c7e86957b458c506a8cb75953f2d99" -dependencies = [ - "actix-codec", - "actix-http", - "actix-web", - "bytestring", - "futures-core", - "tokio", -] - -[[package]] -name = "actix_derive" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6ac1e58cded18cb28ddc17143c4dea5345b3ad575e14f32f66e4054a56eb271" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "getrandom 0.3.3", - "once_cell", - "serde", - "version_check", - "zerocopy", -] - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anyhow" -version = "1.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" - -[[package]] -name = "arraydeque" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "async-tungstenite" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee88b4c88ac8c9ea446ad43498955750a4bbe64c4392f21ccfe5d952865e318f" -dependencies = [ - "atomic-waker", - "futures-core", - "futures-io", - "futures-task", - "futures-util", - "log", - "pin-project-lite", - "tungstenite 0.27.0", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link", -] - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" -dependencies = [ - "serde", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "borrow-or-share" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eeab4423108c5d7c744f4d234de88d18d636100093ae04caf4825134b9c3a32" - -[[package]] -name = "brotli" -version = "8.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[package]] -name = "bstr" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "bumpalo" -version = "3.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" - -[[package]] -name = "bytecount" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" - -[[package]] -name = "bytestring" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289" -dependencies = [ - "bytes", -] - -[[package]] -name = "cc" -version = "1.2.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "chrono" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "chrono-tz" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" -dependencies = [ - "chrono", - "phf", -] - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "futures-core", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", -] - -[[package]] -name = "config" -version = "0.15.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e549344080374f9b32ed41bf3b6b57885ff6a289367b3dbc10eea8acc1918" -dependencies = [ - "async-trait", - "convert_case", - "json5", - "pathdiff", - "ron", - "rust-ini", - "serde-untagged", - "serde_core", - "serde_json", - "toml", - "winnow", - "yaml-rust2", -] - -[[package]] -name = "const-random" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" -dependencies = [ - "const-random-macro", -] - -[[package]] -name = "const-random-macro" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" -dependencies = [ - "getrandom 0.2.16", - "once_cell", - "tiny-keccak", -] - -[[package]] -name = "convert_case" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "cookie" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" -dependencies = [ - "percent-encoding", - "time", - "version_check", -] - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core 0.20.11", - "darling_macro 0.20.11", -] - -[[package]] -name = "darling" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" -dependencies = [ - "darling_core 0.21.3", - "darling_macro 0.21.3", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", -] - -[[package]] -name = "darling_core" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core 0.20.11", - "quote", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" -dependencies = [ - "darling_core 0.21.3", - "quote", - "syn", -] - -[[package]] -name = "dashmap" -version = "6.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" -dependencies = [ - "cfg-if", - "crossbeam-utils", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core 0.9.11", -] - -[[package]] -name = "data-encoding" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" - -[[package]] -name = "deranged" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" -dependencies = [ - "powerfmt", - "serde_core", -] - -[[package]] -name = "derive_builder" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" -dependencies = [ - "derive_builder_macro", -] - -[[package]] -name = "derive_builder_core" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" -dependencies = [ - "darling 0.20.11", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "derive_builder_macro" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" -dependencies = [ - "derive_builder_core", - "syn", -] - -[[package]] -name = "derive_more" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "unicode-xid", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "dlv-list" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" -dependencies = [ - "const-random", -] - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "email_address" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" -dependencies = [ - "serde", -] - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "erased-serde" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b" -dependencies = [ - "serde", - "serde_core", - "typeid", -] - -[[package]] -name = "fancy-regex" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" -dependencies = [ - "bit-set", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" - -[[package]] -name = "flate2" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "fluent-uri" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5" -dependencies = [ - "borrow-or-share", - "ref-cast", - "serde", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fraction" -version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f158e3ff0a1b334408dc9fb811cd99b446986f4d8b741bb08f9df1604085ae7" -dependencies = [ - "lazy_static", - "num", -] - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-timer" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi", - "wasi 0.14.7+wasi-0.2.4", - "wasm-bindgen", -] - -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - -[[package]] -name = "globset" -version = "0.4.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" -dependencies = [ - "aho-corasick", - "bstr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "governor" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444405bbb1a762387aa22dd569429533b54a1d8759d35d3b64cb39b0293eaa19" -dependencies = [ - "cfg-if", - "dashmap", - "futures-sink", - "futures-timer", - "futures-util", - "getrandom 0.3.3", - "hashbrown 0.15.5", - "nonzero_ext", - "parking_lot 0.12.4", - "portable-atomic", - "quanta", - "rand 0.9.2", - "smallvec", - "spinning_top", - "web-time", -] - -[[package]] -name = "h2" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap 2.11.4", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" - -[[package]] -name = "hashlink" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" -dependencies = [ - "hashbrown 0.15.5", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http 1.3.1", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http 1.3.1", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hulypulse" -version = "0.2.1" -dependencies = [ - "actix", - "actix-cors", - "actix-web", - "actix-web-actors", - "actix-ws", - "anyhow", - "config", - "futures-util", - "hex", - "hulyrs", - "md5", - "redis", - "regorus", - "secrecy", - "serde", - "serde_json", - "serde_with", - "size", - "strum", - "strum_macros", - "tokio", - "tokio-stream", - "tokio-tungstenite", - "tracing", - "tracing-subscriber", - "url", - "uuid", -] - -[[package]] -name = "hulyrs" -version = "0.1.0" -source = "git+https://github.com/hcengineering/hulyrs.git#3684385152b5022a2ef2c44a4e99c561e965de57" -dependencies = [ - "actix-web", - "bytes", - "chrono", - "config", - "derive_builder", - "futures", - "governor", - "itoa", - "jsonwebtoken", - "num-traits", - "rand 0.9.2", - "reqwest", - "reqwest-middleware", - "reqwest-ratelimit", - "reqwest-retry", - "reqwest-websocket", - "ryu", - "secrecy", - "serde", - "serde_json", - "serde_with", - "strum", - "thiserror 2.0.17", - "tokio", - "tokio-stream", - "tokio_with_wasm", - "tracing", - "url", - "uuid", - "wasmtimer", -] - -[[package]] -name = "hyper" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "http 1.3.1", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "pin-utils", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http 1.3.1", - "hyper", - "hyper-util", - "rustls 0.23.32", - "rustls-pki-types", - "tokio", - "tokio-rustls 0.26.4", - "tower-service", - "webpki-roots 1.0.2", -] - -[[package]] -name = "hyper-util" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http 1.3.1", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2 0.6.0", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" - -[[package]] -name = "icu_properties" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "potential_utf", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" - -[[package]] -name = "icu_provider" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" -dependencies = [ - "displaydoc", - "icu_locale_core", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "impl-more" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - -[[package]] -name = "indexmap" -version = "2.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" -dependencies = [ - "equivalent", - "hashbrown 0.16.0", - "serde", - "serde_core", -] - -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "io-uring" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "libc", -] - -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.3", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "json5" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" -dependencies = [ - "pest", - "pest_derive", - "serde", -] - -[[package]] -name = "jsonschema" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1b46a0365a611fbf1d2143104dcf910aada96fafd295bab16c60b802bf6fa1d" -dependencies = [ - "ahash", - "base64 0.22.1", - "bytecount", - "email_address", - "fancy-regex", - "fraction", - "idna", - "itoa", - "num-cmp", - "num-traits", - "once_cell", - "percent-encoding", - "referencing", - "regex", - "regex-syntax", - "serde", - "serde_json", - "uuid-simd", -] - -[[package]] -name = "jsonwebtoken" -version = "9.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" -dependencies = [ - "base64 0.22.1", - "js-sys", - "pem", - "ring 0.17.14", - "serde", - "serde_json", - "simple_asn1", -] - -[[package]] -name = "language-tags" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libc" -version = "0.2.176" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" - -[[package]] -name = "litemap" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" - -[[package]] -name = "local-channel" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" -dependencies = [ - "futures-core", - "futures-sink", - "local-waker", -] - -[[package]] -name = "local-waker" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" - -[[package]] -name = "lock_api" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" - -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - -[[package]] -name = "md5" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" - -[[package]] -name = "memchr" -version = "2.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", -] - -[[package]] -name = "mio" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" -dependencies = [ - "libc", - "log", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", -] - -[[package]] -name = "msvc_spectre_libs" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29e871a9861f3664f18b7e04e9301d4edd55090c2dadb4b1c602e26ab32b1f5b" -dependencies = [ - "cc", -] - -[[package]] -name = "nonzero_ext" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" - -[[package]] -name = "nu-ansi-term" -version = "0.50.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" -dependencies = [ - "windows-sys 0.52.0", -] - -[[package]] -name = "num" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" -dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-cmp" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" - -[[package]] -name = "num-complex" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - -[[package]] -name = "ordered-multimap" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" -dependencies = [ - "dlv-list", - "hashbrown 0.14.5", -] - -[[package]] -name = "outref" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" - -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core 0.8.6", -] - -[[package]] -name = "parking_lot" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" -dependencies = [ - "lock_api", - "parking_lot_core 0.9.11", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" -dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall 0.2.16", - "smallvec", - "winapi", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.5.17", - "smallvec", - "windows-targets 0.52.6", -] - -[[package]] -name = "pathdiff" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" - -[[package]] -name = "pem" -version = "3.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" -dependencies = [ - "base64 0.22.1", - "serde", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pest" -version = "2.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8" -dependencies = [ - "memchr", - "thiserror 2.0.17", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc58706f770acb1dbd0973e6530a3cff4746fb721207feb3a8a6064cd0b6c663" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d4f36811dfe07f7b8573462465d5cb8965fffc2e71ae377a33aecf14c2c9a2f" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pest_meta" -version = "2.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42919b05089acbd0a5dcd5405fb304d17d1053847b81163d09c4ad18ce8e8420" -dependencies = [ - "pest", - "sha2", -] - -[[package]] -name = "phf" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" -dependencies = [ - "phf_shared", -] - -[[package]] -name = "phf_shared" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "portable-atomic" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" - -[[package]] -name = "potential_utf" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "proc-macro2" -version = "1.0.101" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quanta" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" -dependencies = [ - "crossbeam-utils", - "libc", - "once_cell", - "raw-cpuid", - "wasi 0.11.1+wasi-snapshot-preview1", - "web-sys", - "winapi", -] - -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls 0.23.32", - "socket2 0.6.0", - "thiserror 2.0.17", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" -dependencies = [ - "bytes", - "getrandom 0.3.3", - "lru-slab", - "rand 0.9.2", - "ring 0.17.14", - "rustc-hash", - "rustls 0.23.32", - "rustls-pki-types", - "slab", - "thiserror 2.0.17", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2 0.6.0", - "tracing", - "windows-sys 0.60.2", -] - -[[package]] -name = "quote" -version = "1.0.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.16", -] - -[[package]] -name = "rand_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.3", -] - -[[package]] -name = "raw-cpuid" -version = "11.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" -dependencies = [ - "bitflags 2.9.4", -] - -[[package]] -name = "redis" -version = "0.32.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd3650deebc68526b304898b192fa4102a4ef0b9ada24da096559cb60e0eef8" -dependencies = [ - "bytes", - "cfg-if", - "combine", - "futures-util", - "itoa", - "num-bigint", - "percent-encoding", - "pin-project-lite", - "rand 0.9.2", - "ryu", - "sha1_smol", - "socket2 0.6.0", - "tokio", - "tokio-util", - "url", -] - -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.5.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" -dependencies = [ - "bitflags 2.9.4", -] - -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "referencing" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8eff4fa778b5c2a57e85c5f2fe3a709c52f0e60d23146e2151cbef5893f420e" -dependencies = [ - "ahash", - "fluent-uri", - "once_cell", - "parking_lot 0.12.4", - "percent-encoding", - "serde_json", -] - -[[package]] -name = "regex" -version = "1.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-lite" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943f41321c63ef1c92fd763bfe054d2668f7f225a5c29f0105903dc2fc04ba30" - -[[package]] -name = "regex-syntax" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" - -[[package]] -name = "regorus" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee058fce2fefa4eb9364b0a514296c70235f0f29bb92ec2c0d24766b837f08aa" -dependencies = [ - "anyhow", - "chrono", - "chrono-tz", - "data-encoding", - "globset", - "jsonschema", - "lazy_static", - "msvc_spectre_libs", - "rand 0.9.2", - "regex", - "scientific", - "semver", - "serde", - "serde_json", - "serde_yaml", - "thiserror 2.0.17", - "url", - "uuid", -] - -[[package]] -name = "reqwest" -version = "0.12.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-core", - "http 1.3.1", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls 0.23.32", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls 0.26.4", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots 1.0.2", -] - -[[package]] -name = "reqwest-middleware" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57f17d28a6e6acfe1733fe24bcd30774d13bffa4b8a22535b4c8c98423088d4e" -dependencies = [ - "anyhow", - "async-trait", - "http 1.3.1", - "reqwest", - "serde", - "thiserror 1.0.69", - "tower-service", -] - -[[package]] -name = "reqwest-ratelimit" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b8fff0d8036f23dcad6c27605ca3baa8ae3867438d0a8b34072f40f6c8bf628" -dependencies = [ - "async-trait", - "http 1.3.1", - "reqwest", - "reqwest-middleware", -] - -[[package]] -name = "reqwest-retry" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c73e4195a6bfbcb174b790d9b3407ab90646976c55de58a6515da25d851178" -dependencies = [ - "anyhow", - "async-trait", - "futures", - "getrandom 0.2.16", - "http 1.3.1", - "hyper", - "parking_lot 0.11.2", - "reqwest", - "reqwest-middleware", - "retry-policies", - "thiserror 1.0.69", - "tokio", - "tracing", - "wasm-timer", -] - -[[package]] -name = "reqwest-websocket" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd5f79b25f7f17a62cc9337108974431a66ae5a723ac0d9fe78ac1cce2027720" -dependencies = [ - "async-tungstenite", - "bytes", - "futures-util", - "reqwest", - "serde", - "serde_json", - "thiserror 2.0.17", - "tokio", - "tokio-util", - "tracing", - "tungstenite 0.27.0", - "web-sys", -] - -[[package]] -name = "retry-policies" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5875471e6cab2871bc150ecb8c727db5113c9338cc3354dc5ee3425b6aa40a1c" -dependencies = [ - "rand 0.8.5", -] - -[[package]] -name = "ring" -version = "0.16.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin", - "untrusted 0.7.1", - "web-sys", - "winapi", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.16", - "libc", - "untrusted 0.9.0", - "windows-sys 0.52.0", -] - -[[package]] -name = "ron" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" -dependencies = [ - "base64 0.21.7", - "bitflags 2.9.4", - "serde", - "serde_derive", -] - -[[package]] -name = "rust-ini" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" -dependencies = [ - "cfg-if", - "ordered-multimap", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustls" -version = "0.20.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" -dependencies = [ - "log", - "ring 0.16.20", - "sct", - "webpki", -] - -[[package]] -name = "rustls" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" -dependencies = [ - "log", - "ring 0.17.14", - "rustls-pki-types", - "rustls-webpki 0.102.8", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls" -version = "0.23.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" -dependencies = [ - "once_cell", - "ring 0.17.14", - "rustls-pki-types", - "rustls-webpki 0.103.7", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-native-certs" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" -dependencies = [ - "openssl-probe", - "rustls-pemfile", - "rustls-pki-types", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "rustls-pki-types" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.102.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" -dependencies = [ - "ring 0.17.14", - "rustls-pki-types", - "untrusted 0.9.0", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" -dependencies = [ - "ring 0.17.14", - "rustls-pki-types", - "untrusted 0.9.0", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "schannel" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" -dependencies = [ - "windows-sys 0.61.1", -] - -[[package]] -name = "schemars" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "scientific" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38a4b339a8de779ecb098a772ecbba2ace74e23ed959a5b4f30631d8bf1799a8" -dependencies = [ - "scientific-macro", -] - -[[package]] -name = "scientific-macro" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ee4885492bb655bfa05d039cd9163eb8fe9f79ddebf00ca23a1637510c2fd2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring 0.17.14", - "untrusted 0.9.0", -] - -[[package]] -name = "secrecy" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" -dependencies = [ - "serde", - "zeroize", -] - -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.9.4", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde-untagged" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" -dependencies = [ - "erased-serde", - "serde", - "serde_core", - "typeid", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.145" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", - "serde_core", -] - -[[package]] -name = "serde_spanned" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_with" -version = "3.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e" -dependencies = [ - "base64 0.22.1", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.11.4", - "schemars 0.9.0", - "schemars 1.0.4", - "serde", - "serde_derive", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e" -dependencies = [ - "darling 0.21.3", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap 2.11.4", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha1_smol" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook-registry" -version = "1.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" -dependencies = [ - "libc", -] - -[[package]] -name = "simple_asn1" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" -dependencies = [ - "num-bigint", - "num-traits", - "thiserror 2.0.17", - "time", -] - -[[package]] -name = "siphasher" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" - -[[package]] -name = "size" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b6709c7b6754dca1311b3c73e79fcce40dd414c782c66d88e8823030093b02b" -dependencies = [ - "serde", -] - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "socket2" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - -[[package]] -name = "spinning_top" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" -dependencies = [ - "lock_api", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "strum" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "2.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" -dependencies = [ - "thiserror-impl 2.0.17", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "time" -version = "0.3.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" - -[[package]] -name = "time-macros" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - -[[package]] -name = "tinystr" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.47.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" -dependencies = [ - "backtrace", - "bytes", - "io-uring", - "libc", - "mio", - "parking_lot 0.12.4", - "pin-project-lite", - "signal-hook-registry", - "slab", - "socket2 0.6.0", - "tokio-macros", - "windows-sys 0.59.0", -] - -[[package]] -name = "tokio-macros" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tokio-rustls" -version = "0.23.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" -dependencies = [ - "rustls 0.20.9", - "tokio", - "webpki", -] - -[[package]] -name = "tokio-rustls" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" -dependencies = [ - "rustls 0.22.4", - "rustls-pki-types", - "tokio", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls 0.23.32", - "tokio", -] - -[[package]] -name = "tokio-stream" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", - "tokio-util", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" -dependencies = [ - "futures-util", - "log", - "rustls 0.22.4", - "rustls-native-certs", - "rustls-pki-types", - "tokio", - "tokio-rustls 0.25.0", - "tungstenite 0.21.0", -] - -[[package]] -name = "tokio-util" -version = "0.7.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" -dependencies = [ - "bytes", - "futures-core", - "futures-io", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio_with_wasm" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dfba9b946459940fb564dcf576631074cdfb0bfe4c962acd4c31f0dca7897e6" -dependencies = [ - "js-sys", - "tokio", - "tokio_with_wasm_proc", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "tokio_with_wasm_proc" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e04c1865c281139e5ccf633cb9f76ffdaabeebfe53b703984cf82878e2aabb" -dependencies = [ - "quote", - "syn", -] - -[[package]] -name = "toml" -version = "0.9.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0" -dependencies = [ - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_datetime" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_parser" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" -dependencies = [ - "winnow", -] - -[[package]] -name = "tower" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" -dependencies = [ - "bitflags 2.9.4", - "bytes", - "futures-util", - "http 1.3.1", - "http-body", - "iri-string", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" -dependencies = [ - "nu-ansi-term", - "sharded-slab", - "smallvec", - "thread_local", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "tungstenite" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" -dependencies = [ - "byteorder", - "bytes", - "data-encoding", - "http 1.3.1", - "httparse", - "log", - "rand 0.8.5", - "rustls 0.22.4", - "rustls-pki-types", - "sha1", - "thiserror 1.0.69", - "url", - "utf-8", -] - -[[package]] -name = "tungstenite" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d" -dependencies = [ - "bytes", - "data-encoding", - "http 1.3.1", - "httparse", - "log", - "rand 0.9.2", - "sha1", - "thiserror 2.0.17", - "utf-8", -] - -[[package]] -name = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - -[[package]] -name = "unicode-ident" -version = "1.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" - -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "uuid" -version = "1.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" -dependencies = [ - "getrandom 0.3.3", - "js-sys", - "rand 0.9.2", - "serde", - "wasm-bindgen", -] - -[[package]] -name = "uuid-simd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" -dependencies = [ - "outref", - "uuid", - "vsimd", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "vsimd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" -dependencies = [ - "wasip2", -] - -[[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" -dependencies = [ - "cfg-if", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-timer" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" -dependencies = [ - "futures", - "js-sys", - "parking_lot 0.11.2", - "pin-utils", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "wasmtimer" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c598d6b99ea013e35844697fc4670d08339d5cda15588f193c6beedd12f644b" -dependencies = [ - "futures", - "js-sys", - "parking_lot 0.12.4", - "pin-utils", - "slab", - "wasm-bindgen", -] - -[[package]] -name = "web-sys" -version = "0.3.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" -dependencies = [ - "ring 0.17.14", - "untrusted 0.9.0", -] - -[[package]] -name = "webpki-roots" -version = "0.22.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" -dependencies = [ - "webpki", -] - -[[package]] -name = "webpki-roots" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-core" -version = "0.62.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-link" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" - -[[package]] -name = "windows-result" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.4", -] - -[[package]] -name = "windows-sys" -version = "0.61.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" - -[[package]] -name = "winnow" -version = "0.7.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" - -[[package]] -name = "writeable" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" - -[[package]] -name = "yaml-rust2" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9" -dependencies = [ - "arraydeque", - "encoding_rs", - "hashlink", -] - -[[package]] -name = "yoke" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" - -[[package]] -name = "zerotrie" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/foundations/hulypulse/Cargo.toml b/foundations/hulypulse/Cargo.toml deleted file mode 100644 index 611fb59db9..0000000000 --- a/foundations/hulypulse/Cargo.toml +++ /dev/null @@ -1,48 +0,0 @@ -[package] -name = "hulypulse" -version = "0.2.1" -edition = "2024" - -[dependencies] -tokio = { version = "1", features = ["full", "macros", "rt-multi-thread"] } -tracing = "0.1.41" -tracing-subscriber = "0.3.19" -anyhow = "1.0.97" -config = "0.15.4" -serde = { version = "1.0.219", features = ["derive"] } -actix = "0.13.5" -actix-web = "4.10.2" -actix-cors = "0.7.1" -actix-web-actors = "4.2.0" -redis = { version = "=0.32.5", features = ["aio", "tokio-comp", "sentinel"] } -md5 = "0.8.0" -serde_with = "3" -url = "2" -size = { version = "0.5.0", features = ["serde"] } -uuid = { version = "1.7", features = ["v4", "serde"] } -hex = "0.4.3" -serde_json = "1.0" -hulyrs = { git = "https://github.com/hcengineering/hulyrs.git", features = [ - "actix", -] } -secrecy = "0.10.3" -tokio-stream = "0.1" -strum = { version = "0.27.2", features = ["derive"] } -strum_macros = "0.27.2" -regorus = "0.5.0" -actix-ws = "0.3.0" -futures-util = "0.3" - -[[bin]] -name = "hulypulse" -path = "src/main.rs" - -[dev-dependencies] -tokio-tungstenite = { version = "0.21", default-features = false, features = [ - "rustls-tls-native-roots", - "connect", -] } -tokio = { version = "1", features = ["macros", "rt-multi-thread"] } -futures-util = "0.3" -serde_json = "1" -uuid = "1" diff --git a/foundations/hulypulse/Dockerfile b/foundations/hulypulse/Dockerfile deleted file mode 100644 index 7d80417bf7..0000000000 --- a/foundations/hulypulse/Dockerfile +++ /dev/null @@ -1,35 +0,0 @@ -FROM --platform=$BUILDPLATFORM rust:1.88 AS builder -ARG TARGETPLATFORM - -WORKDIR /tmp/build - -COPY . . - -RUN \ - if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ - cargo build --release --target=x86_64-unknown-linux-gnu ; \ - elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \ - apt-get update && apt-get install -y \ - gcc-aarch64-linux-gnu \ - g++-aarch64-linux-gnu \ - libc6-dev-arm64-cross \ - && rm -rf /var/lib/apt/lists/* ; \ -\ - rustup target add aarch64-unknown-linux-gnu ; \ -\ - export CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc ; \ - export CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++ ; \ - export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc ; \ -\ - cargo build --release --target=aarch64-unknown-linux-gnu ; \ - else \ - echo "Unexpected target platform: $TARGETPLATFORM" && exit 1 ; \ - fi - -FROM debian:12-slim - -ARG TARGET -COPY --from=builder /tmp/build/target/*/release/hulypulse /usr/local/bin/hulypulse -RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* - -ENTRYPOINT ["/usr/local/bin/hulypulse"] diff --git a/foundations/hulypulse/LICENSE b/foundations/hulypulse/LICENSE deleted file mode 100644 index e48e096345..0000000000 --- a/foundations/hulypulse/LICENSE +++ /dev/null @@ -1,277 +0,0 @@ -Eclipse Public License - v 2.0 - - THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE - PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION - OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. - -1. DEFINITIONS - -"Contribution" means: - - a) in the case of the initial Contributor, the initial content - Distributed under this Agreement, and - - b) in the case of each subsequent Contributor: - i) changes to the Program, and - ii) additions to the Program; - where such changes and/or additions to the Program originate from - and are Distributed by that particular Contributor. A Contribution - "originates" from a Contributor if it was added to the Program by - such Contributor itself or anyone acting on such Contributor's behalf. - Contributions do not include changes or additions to the Program that - are not Modified Works. - -"Contributor" means any person or entity that Distributes the Program. - -"Licensed Patents" mean patent claims licensable by a Contributor which -are necessarily infringed by the use or sale of its Contribution alone -or when combined with the Program. - -"Program" means the Contributions Distributed in accordance with this -Agreement. - -"Recipient" means anyone who receives the Program under this Agreement -or any Secondary License (as applicable), including Contributors. - -"Derivative Works" shall mean any work, whether in Source Code or other -form, that is based on (or derived from) the Program and for which the -editorial revisions, annotations, elaborations, or other modifications -represent, as a whole, an original work of authorship. - -"Modified Works" shall mean any work in Source Code or other form that -results from an addition to, deletion from, or modification of the -contents of the Program, including, for purposes of clarity any new file -in Source Code form that contains any contents of the Program. Modified -Works shall not include works that contain only declarations, -interfaces, types, classes, structures, or files of the Program solely -in each case in order to link to, bind by name, or subclass the Program -or Modified Works thereof. - -"Distribute" means the acts of a) distributing or b) making available -in any manner that enables the transfer of a copy. - -"Source Code" means the form of a Program preferred for making -modifications, including but not limited to software source code, -documentation source, and configuration files. - -"Secondary License" means either the GNU General Public License, -Version 2.0, or any later versions of that license, including any -exceptions or additional permissions as identified by the initial -Contributor. - -2. GRANT OF RIGHTS - - a) Subject to the terms of this Agreement, each Contributor hereby - grants Recipient a non-exclusive, worldwide, royalty-free copyright - license to reproduce, prepare Derivative Works of, publicly display, - publicly perform, Distribute and sublicense the Contribution of such - Contributor, if any, and such Derivative Works. - - b) Subject to the terms of this Agreement, each Contributor hereby - grants Recipient a non-exclusive, worldwide, royalty-free patent - license under Licensed Patents to make, use, sell, offer to sell, - import and otherwise transfer the Contribution of such Contributor, - if any, in Source Code or other form. This patent license shall - apply to the combination of the Contribution and the Program if, at - the time the Contribution is added by the Contributor, such addition - of the Contribution causes such combination to be covered by the - Licensed Patents. The patent license shall not apply to any other - combinations which include the Contribution. No hardware per se is - licensed hereunder. - - c) Recipient understands that although each Contributor grants the - licenses to its Contributions set forth herein, no assurances are - provided by any Contributor that the Program does not infringe the - patent or other intellectual property rights of any other entity. - Each Contributor disclaims any liability to Recipient for claims - brought by any other entity based on infringement of intellectual - property rights or otherwise. As a condition to exercising the - rights and licenses granted hereunder, each Recipient hereby - assumes sole responsibility to secure any other intellectual - property rights needed, if any. For example, if a third party - patent license is required to allow Recipient to Distribute the - Program, it is Recipient's responsibility to acquire that license - before distributing the Program. - - d) Each Contributor represents that to its knowledge it has - sufficient copyright rights in its Contribution, if any, to grant - the copyright license set forth in this Agreement. - - e) Notwithstanding the terms of any Secondary License, no - Contributor makes additional grants to any Recipient (other than - those set forth in this Agreement) as a result of such Recipient's - receipt of the Program under the terms of a Secondary License - (if permitted under the terms of Section 3). - -3. REQUIREMENTS - -3.1 If a Contributor Distributes the Program in any form, then: - - a) the Program must also be made available as Source Code, in - accordance with section 3.2, and the Contributor must accompany - the Program with a statement that the Source Code for the Program - is available under this Agreement, and informs Recipients how to - obtain it in a reasonable manner on or through a medium customarily - used for software exchange; and - - b) the Contributor may Distribute the Program under a license - different than this Agreement, provided that such license: - i) effectively disclaims on behalf of all other Contributors all - warranties and conditions, express and implied, including - warranties or conditions of title and non-infringement, and - implied warranties or conditions of merchantability and fitness - for a particular purpose; - - ii) effectively excludes on behalf of all other Contributors all - liability for damages, including direct, indirect, special, - incidental and consequential damages, such as lost profits; - - iii) does not attempt to limit or alter the recipients' rights - in the Source Code under section 3.2; and - - iv) requires any subsequent distribution of the Program by any - party to be under a license that satisfies the requirements - of this section 3. - -3.2 When the Program is Distributed as Source Code: - - a) it must be made available under this Agreement, or if the - Program (i) is combined with other material in a separate file or - files made available under a Secondary License, and (ii) the initial - Contributor attached to the Source Code the notice described in - Exhibit A of this Agreement, then the Program may be made available - under the terms of such Secondary Licenses, and - - b) a copy of this Agreement must be included with each copy of - the Program. - -3.3 Contributors may not remove or alter any copyright, patent, -trademark, attribution notices, disclaimers of warranty, or limitations -of liability ("notices") contained within the Program from any copy of -the Program which they Distribute, provided that Contributors may add -their own appropriate notices. - -4. COMMERCIAL DISTRIBUTION - -Commercial distributors of software may accept certain responsibilities -with respect to end users, business partners and the like. While this -license is intended to facilitate the commercial use of the Program, -the Contributor who includes the Program in a commercial product -offering should do so in a manner which does not create potential -liability for other Contributors. Therefore, if a Contributor includes -the Program in a commercial product offering, such Contributor -("Commercial Contributor") hereby agrees to defend and indemnify every -other Contributor ("Indemnified Contributor") against any losses, -damages and costs (collectively "Losses") arising from claims, lawsuits -and other legal actions brought by a third party against the Indemnified -Contributor to the extent caused by the acts or omissions of such -Commercial Contributor in connection with its distribution of the Program -in a commercial product offering. The obligations in this section do not -apply to any claims or Losses relating to any actual or alleged -intellectual property infringement. In order to qualify, an Indemnified -Contributor must: a) promptly notify the Commercial Contributor in -writing of such claim, and b) allow the Commercial Contributor to control, -and cooperate with the Commercial Contributor in, the defense and any -related settlement negotiations. The Indemnified Contributor may -participate in any such claim at its own expense. - -For example, a Contributor might include the Program in a commercial -product offering, Product X. That Contributor is then a Commercial -Contributor. If that Commercial Contributor then makes performance -claims, or offers warranties related to Product X, those performance -claims and warranties are such Commercial Contributor's responsibility -alone. Under this section, the Commercial Contributor would have to -defend claims against the other Contributors related to those performance -claims and warranties, and if a court requires any other Contributor to -pay any damages as a result, the Commercial Contributor must pay -those damages. - -5. NO WARRANTY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT -PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" -BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR -IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF -TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR -PURPOSE. Each Recipient is solely responsible for determining the -appropriateness of using and distributing the Program and assumes all -risks associated with its exercise of rights under this Agreement, -including but not limited to the risks and costs of program errors, -compliance with applicable laws, damage to or loss of data, programs -or equipment, and unavailability or interruption of operations. - -6. DISCLAIMER OF LIABILITY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT -PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS -SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST -PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE -EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - -7. GENERAL - -If any provision of this Agreement is invalid or unenforceable under -applicable law, it shall not affect the validity or enforceability of -the remainder of the terms of this Agreement, and without further -action by the parties hereto, such provision shall be reformed to the -minimum extent necessary to make such provision valid and enforceable. - -If Recipient institutes patent litigation against any entity -(including a cross-claim or counterclaim in a lawsuit) alleging that the -Program itself (excluding combinations of the Program with other software -or hardware) infringes such Recipient's patent(s), then such Recipient's -rights granted under Section 2(b) shall terminate as of the date such -litigation is filed. - -All Recipient's rights under this Agreement shall terminate if it -fails to comply with any of the material terms or conditions of this -Agreement and does not cure such failure in a reasonable period of -time after becoming aware of such noncompliance. If all Recipient's -rights under this Agreement terminate, Recipient agrees to cease use -and distribution of the Program as soon as reasonably practicable. -However, Recipient's obligations under this Agreement and any licenses -granted by Recipient relating to the Program shall continue and survive. - -Everyone is permitted to copy and distribute copies of this Agreement, -but in order to avoid inconsistency the Agreement is copyrighted and -may only be modified in the following manner. The Agreement Steward -reserves the right to publish new versions (including revisions) of -this Agreement from time to time. No one other than the Agreement -Steward has the right to modify this Agreement. The Eclipse Foundation -is the initial Agreement Steward. The Eclipse Foundation may assign the -responsibility to serve as the Agreement Steward to a suitable separate -entity. Each new version of the Agreement will be given a distinguishing -version number. The Program (including Contributions) may always be -Distributed subject to the version of the Agreement under which it was -received. In addition, after a new version of the Agreement is published, -Contributor may elect to Distribute the Program (including its -Contributions) under the new version. - -Except as expressly stated in Sections 2(a) and 2(b) above, Recipient -receives no rights or licenses to the intellectual property of any -Contributor under this Agreement, whether expressly, by implication, -estoppel or otherwise. All rights in the Program not expressly granted -under this Agreement are reserved. Nothing in this Agreement is intended -to be enforceable by any entity that is not a Contributor or Recipient. -No third-party beneficiary rights are created under this Agreement. - -Exhibit A - Form of Secondary Licenses Notice - -"This Source Code may also be made available under the following -Secondary Licenses when the conditions for such availability set forth -in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), -version(s), and exceptions or additional permissions here}." - - Simply including a copy of this Agreement, including this Exhibit A - is not sufficient to license the Source Code under Secondary Licenses. - - If it is not possible or desirable to put the notice in a particular - file, then You may include the notice in a location (such as a LICENSE - file in a relevant directory) where a recipient would be likely to - look for such a notice. - - You may add additional accurate notices of copyright ownership. diff --git a/foundations/hulypulse/README.md b/foundations/hulypulse/README.md deleted file mode 100644 index 3b4a447a9f..0000000000 --- a/foundations/hulypulse/README.md +++ /dev/null @@ -1,243 +0,0 @@ -# Hulypulse - -Hulypulse is a service that enables clients to share information on a “whiteboard”. Clients connected to the same “whiteboard” see data provided by other clients to the whiteboard. - -The service is exposed as REST and WebSocket API. - -**Usage scenarios:** - -- user presence in a document -- user is “typing” event -- user cursor position in editor or drawing board -- service posts a process status - -## Key - -Key is a string that consists of one or multiple segments separated by ‘/’. Example: foo/bar/baz. -Key may not end with ‘/’ -Segment may not contain special characters (‘*’, ‘?’, ‘[’, ‘]’,‘\’,‘\x00..\xF1’,‘\x7F’,‘"’,‘'’) -Segment may not be empty -Key segment may be private (prefixed with ‘$’) - - Query - -May not contain special characters (‘*’, ‘?’, ‘[’, ‘]’,‘\’,‘\x00..\xF1’,‘\x7F’,‘"’,‘'’) -It is possible to use prefix, for listings / subscriptions (prefix ends with segment separator ‘/’) - -- GET/SUBSCRIBE/.. a/b → single key -- GET/SUBSCRIBE/.. a/b/c/ → multiple - - If multiple - -select all keys starting with prefix -skip keys, containing private segments to the right from the prefix - - example - -- 1. /a/b/$c/$d, 2. /a/b/c, 3. /a/b/$c, 4. /a/b/$c/$d/e -- / → [2] -- /a/b/ → [2] -- /a/b/$c/ → [3] -- /a/b/$c/$d/ → [4] -- /a/b/$c/$d → [1] - - -## Data -“Data” is an arbitrary JSON document. -Size of data is limited to some reasonable size - -## HTTP API - -```GET /status``` - server status and websockets count -- Answer: `{"status":"OK","websockets":2}` - - -```PUT /{workspace}/{key}``` - Save key -- Input - - Body - data - - Content-Type: application/json - - Content-Length: optional - - Headers: TTL or absolute expiration time - - `HULY-TTL` — autodelete in N seconds - - or `HULY-EXPIRE-AT` — autodelete in UnixTime - - default max_ttl = 3600 (settings in config/default.toml) - - Conditional Headers: - - `If-Match: *` — update only if the key exists - - `If-Match: ` — update only if current value's MD5 matches - - `If-None-Match: *` — insert only if the key does not exist -- Output - - Status: - - `201` if inserted with `If-None-Match: *` - - `204` on successful insert or update - - `412` if the condition is not met - - `400` if headers are invalid - - Body: `DONE` - -```DELETE /{workspace}/{key}``` - Delete key -- Output - - Status: `204 No content`, no body - - `404 Not Found` if nothing to do - -```GET /{workspace}/{key}``` - Read one key -- Output - - Status 200 - - Content-type: application/json - - Header: `Etag: ` - - Body: - - workspace (copy of input) - - key (copy of input) - - data (copy of input) - - expiresAt / TTL (copy of input, optional) - - etag - -```GET /{workspace}/{key}/``` - Read array of keys -- Output - - Status 200 - - Content-type: application/json - - Body (array): - - [{"key","data","ttl","etag"}, ...] - -## WebSocket API - -**Client to Server** - -```PUT``` - - type: "put" - - correlation id (optional) - - key: - - “workspace/foo/bar“ - shared key - - “workspace/foo/bar/$/secret“ - secret key - - data - ** time control (optional) ** - - `TTL` — autodelete in N seconds - - `ExpireAt` — autodelete in UnixTime - - or default max_ttl = 3600 (settings in config/default.toml) - ** Conditional (optional) ** - - `ifMatch: *` — update only if the key exists - - `ifMatch: ` — update only if current value's MD5 matches - - `ifNoneMatch: *` — insert only if the key does not exist - -- Answer: `{"action":"put","correlation":"abc123","result":"OK"}` - - -```GET``` - - type: "get" - - correlation id (optional) - - key: - - “workspace/foo/bar“ - one shared key - - “workspace/foo/bar/$/secret“ - one secret key - -- Answer: `{"action":"get","result":{"data":"hello","etag":"5d41402abc4b2a76b9719d911017c592","ttl":3599,"key":"00000000-0000-0000-0000-000000000001/foo/bar"}}` - - -```LIST``` - - type: "list" - - correlation id (optional) - - key: - - “workspace/foo/bar/“ - keys from public space - - “workspace/foo/bar/$/secret/“ - keys from secret space - -- Answer: `{"action":"list","result":[{"data":"hello 1","etag":"df0649bc4f1be901c85b6183091c1d83","ttl":3570,"key":"00000000-0000-0000-0000-000000000001/foo/bar1"},{"data":"hello 2","etag":"bb21ec8394b75795622f61613a777a8b","ttl":3555,"key":"00000000-0000-0000-0000-000000000001/foo/bar2"}]}` - - -```DELETE``` - - type: "delete" - - correlation id (optional) - - key: “workspace/foo/bar“ - ** Conditional (optional) ** - - `ifMatch: ` — delete only if current value's MD5 matches - - `ifMatch: *` — return error if key does not exist - -- Answer: `{"action":"delete","result":"OK"}` - - -```SUBSCRIBE``` - type: "sub" - key: - - “workspace/foo/bar“ - subscribe one shared key - - “workspace/foo/bar/“ - subscribe all keys started with - - “workspace/foo/bar/$/my_secret“ - subscribe one secret key - - “workspace/foo/bar/$/my_secret/“ - subscribe all keys started with secret - -- Answer: `{"action":"sub","result":"OK"}` - - -```UNSUBSCRIBE``` - - type: "unsub" - - key: - - “workspace/foo/bar“ - unsubscribe subscribed key - - “*“ - unsubscribe all - -- Answer: `{"action":"unsub","result":"OK"}` - -```MY SUBSCRIBES``` - - type: "sublist" - -- Answer: `{"action":"list","result":["00000000-0000-0000-0000-000000000001/foo/bar1","00000000-0000-0000-0000-000000000001/foo/bar2"]}` - -```INFO``` - - type: "info" - -- Answer: `{"db_mode":"memory","memory_info":"1231 keys, 80345 bytes","status":"OK","websockets":164}` - - -** Server to Client ** subscribed events: - - - `{"message":"Set","key":"00000000-0000-0000-0000-000000000001/foo/bar","value":"hello"}` - - - `{"message":"Expired","key":"00000000-0000-0000-0000-000000000001/foo/bar"}` - - - `{"message":"Del","key":"00000000-0000-0000-0000-000000000001/foo/bar"}` - -## Special options in config/default.toml - - ```memory_mode = true``` Use native memory storage instead Redis - - ```no_authorization = true``` Don't check authorization - - ```max_size = 100``` Max value size in bytes - - -## Running - -Pre-build docker images is available at: hardcoreeng/service_hulypulse:{tag}. - -You can use the following command to run the image locally: -```bash -docker run -p 8095:8095 -it --rm hardcoreeng/service_hulypulse:{tag}" -``` - -If you want to run the service as a part of local huly development environment use the following command: -```bash - export HULY_REDIS_URLS="redis://huly.local:6379" - docker run --rm -it --network dev_default -p 8095:8095 hardcoreeng/service_hulypulse:{tag} -``` -This will run Hulypulse in the same network as the rest of huly services, and set the redis connection string to the one matching the local dev redis instance. - -You can then access hulypulse at http://localhost:8095. - - -## Authetication -Hulypulse uses bearer JWT token authetication. At the moment, it will accept any token signed by the hulypulse secret. The secret is set in the environment variable HULY_TOKEN_SECRET variable. - -## Configuration -The following environment variables are used to configure hulypulse: - - ```HULY_BIND_HOST```: host to bind the server to (default: 0.0.0.0) - - ```HULY_BIND_PORT```: port to bind the server to (default: 8094) - - ```HULY_TOKEN_SECRET```: secret used to sign JWT tokens (default: secret) - - ```HULY_REDIS_URLS```: redis connection string (default: redis://huly.local:6379) - - ```HULY_REDIS_PASSWORD```: redis password (default: "<invalid>") - - ```HULY_REDIS_MODE```: redis mode "direct" or "sentinel" (default: "direct") - - ```HULY_REDIS_SERVICE```: redis service (default: "mymaster") - - ```HULY_MAX_TTL```: maximum storage time (default: 3600) - - TODO: ```HULY_PAYLOAD_SIZE_LIMIT```: maximum size of the payload (default: 2Mb) - -## Todo (in no particular order) -- [ ] Optional value encryption -- [ ] Support for open telemetry -- [ ] Concurrency control for database migration (several instances of hulypulse are updated at the same time) -- [ ] TLS support -- [ ] Liveness/readiness probe endpoint - -## Contributing -Contributions are welcome! Please open an issue or a pull request if you have any suggestions or improvements. - -## License -This project is licensed under EPL-2.0 diff --git a/foundations/hulypulse/client/off/client.ts b/foundations/hulypulse/client/off/client.ts deleted file mode 100644 index 5908dfe483..0000000000 --- a/foundations/hulypulse/client/off/client.ts +++ /dev/null @@ -1,271 +0,0 @@ -// -// Copyright © 2024-2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -// import { WebSocket } from 'ws'; // для Node <20 обязательно - -// Unknown: -// import { type Ref, concatLink } from '@hcengineering/core' -// import { getMetadata } from '@hcengineering/platform' - -// import { getCurrentEmployee, type Person } from '@hcengineering/contact' -// import presence from '@hcengineering/presence' -// import presentation from '@hcengineering/presentation' -// import { type Unsubscriber, get } from 'svelte/store' - -// import { myPresence, myData, isAnybodyInMyRoom, onPersonUpdate, onPersonLeave, onPersonData } from './store' -// import type { RoomPresence, MyDataItem } from './types' - -// interface PresenceMessage { -// id: Ref -// type: 'update' | 'remove' -// presence?: RoomPresence[] -// lastUpdate?: number -// } - -// interface DataMessage { -// type: 'data' -// sender: Ref -// topic: string -// data: any -// } - -// type IncomingMessage = PresenceMessage | DataMessage - - - -export class HulypulseClient implements Disposable { - private ws: WebSocket | null = null - private closed = false - private reconnectTimeout: number | undefined - private pingTimeout: number | undefined - private pingInterval: number | undefined - private readonly RECONNECT_INTERVAL = 1000 - private readonly PING_INTERVAL = 30 * 1000 - private readonly PING_TIMEOUT = 5 * 60 * 1000 - private readonly myDataThrottleInterval = 100 - // private readonly url: string | URL = 'ws://localhost:8095' - - // private presence: RoomPresence[] - private readonly myDataTimestamps = new Map() - // // private readonly myPresenceUnsub: Unsubscriber - // private readonly myDataUnsub: Unsubscriber - - constructor (private readonly url: string | URL) { - // this.presence = get(myPresence) - // this.myPresenceUnsub = myPresence.subscribe((presence) => { - // this.handlePresenceChanged(presence) - // }) - // this.myDataUnsub = myData.subscribe((data) => { - // this.handleMyDataChanged(data, false) - // }) - - this.connect() - } - - // Close the connection - close (): void { - console.log('Closing connection') - this.closed = true - clearTimeout(this.reconnectTimeout) - this.stopPing() - - // this.myPresenceUnsub() - // this.myDataUnsub() - - if (this.ws !== null) { - this.ws.close() - this.ws = null - } - } - - // Open the connection and reconnect if it fails - private connect (): void { - console.log('Connecting to WebSocket: ', this.url) - try { - const ws = new WebSocket(this.url) - console.log('WebSocket created: ', ws) - this.ws = ws - - ws.onopen = () => { - console.log('WebSocket.onopen') - if (this.ws !== ws) { - return - } - - this.handleConnect() - } - - ws.onclose = (event: CloseEvent) => { - console.log('WebSocket.onclose') - if (this.ws !== ws) { - ws.close() - return - } - - this.reconnect() - } - - ws.onmessage = (event: MessageEvent) => { - console.log('WebSocket.onmessage: ', event.data) - if (this.closed || this.ws !== ws) { - return - } - - this.handleMessage(event.data) - } - - ws.onerror = (event: Event) => { - console.log('client websocket error', event) - if (this.ws !== ws) { - return - } - } - } catch (err: any) { - console.error('WebSocket error', err) - this.reconnect() - } - } - - private startPing (): void { - console.log('Starting ping') - clearInterval(this.pingInterval) - this.pingInterval = window.setInterval(() => { - if (this.ws !== null && this.ws.readyState === WebSocket.OPEN) { - this.ws.send('ping') - } - clearTimeout(this.pingTimeout) - this.pingTimeout = window.setTimeout(() => { - if (this.ws !== null) { - console.log('no response from server') - clearInterval(this.pingInterval) - this.ws.close(1000) - } - }, this.PING_TIMEOUT) - }, this.PING_INTERVAL) - } - - private stopPing (): void { - console.log('Stopping ping') - clearInterval(this.pingInterval) - this.pingInterval = undefined - - clearTimeout(this.pingTimeout) - this.pingTimeout = undefined - } - - private reconnect (): void { - console.log('Reconnecting...') - clearTimeout(this.reconnectTimeout) - this.stopPing() - - if (!this.closed) { - this.reconnectTimeout = window.setTimeout(() => { - this.connect() - }, this.RECONNECT_INTERVAL) - } - } - - private handleConnect (): void { -// this.sendPresence(getCurrentEmployee(), this.presence) - this.startPing() -// this.handleMyDataChanged(get(myData), true) - } - - private handleMessage (data: string): void { - if (data === 'pong') { - clearTimeout(this.pingTimeout) - return - } - - try { - const message = JSON.parse(data); // as IncomingMessage - console.log('Received message', message); - // const message = JSON.parse(data) as IncomingMessage - // if (message.type === 'update' && message.presence !== undefined) { - // onPersonUpdate(message.id, message.presence ?? []) - // } else if (message.type === 'remove') { - // onPersonLeave(message.id) - // } else if (message.type === 'data') { - // onPersonData(message.sender, message.topic, message.data) - // } else { - // console.warn('Unknown message type', message) - // } - } catch (err: any) { - console.error('Error parsing message', err, data) - } - } - - // private handlePresenceChanged (presence: RoomPresence[]): void { - // this.presence = presence - // this.sendPresence(getCurrentEmployee(), this.presence) - // this.handleMyDataChanged(get(myData), true) - // } - - // private sendPresence (person: Ref, presence: RoomPresence[]): void { - // if (!this.closed && this.ws !== null && this.ws.readyState === WebSocket.OPEN) { - // const message: PresenceMessage = { id: person, type: 'update', presence } - // this.ws.send(JSON.stringify(message)) - // } - // } - - // private handleMyDataChanged (data: Map, forceSend: boolean): void { - // if (!isAnybodyInMyRoom()) { - // return - // } - // if (!this.closed && this.ws !== null && this.ws.readyState === WebSocket.OPEN) { - // for (const [topic, value] of data) { - // const lastSend = this.myDataTimestamps.get(topic) ?? 0 - // if (value.lastUpdated >= lastSend + this.myDataThrottleInterval || forceSend) { - // this.myDataTimestamps.set(topic, value.lastUpdated) - // const message: DataMessage = { - // sender: getCurrentEmployee(), - // type: 'data', - // topic, - // data: value.data - // } - // this.ws.send(JSON.stringify(message)) - // } - // } - // } - // } - - [Symbol.dispose] (): void { - this.close() - } -} - -export function connect (): HulypulseClient | undefined { - // const wsUuid = getMetadata(presentation.metadata.WorkspaceUuid) - // if (wsUuid === undefined) { - // console.warn('Workspace uuid is not defined') - // return undefined - // } - - // const token = getMetadata(presentation.metadata.Token) - - // const presenceUrl = getMetadata(presence.metadata.PresenceUrl) - // if (presenceUrl === undefined || presenceUrl === '') { - // console.warn('Presence URL is not defined') - // return undefined - // } - - // const url = new URL(concatLink(presenceUrl, wsUuid)) - // if (token !== undefined) { - // url.searchParams.set('token', token) - // } - - // return new HulypulseClient(url) - return new HulypulseClient("ws://localhost:8095") -} \ No newline at end of file diff --git a/foundations/hulypulse/policy.repo b/foundations/hulypulse/policy.repo deleted file mode 100644 index 0888019a93..0000000000 --- a/foundations/hulypulse/policy.repo +++ /dev/null @@ -1,7 +0,0 @@ -default permit = true - -#permit if { -# input.command == "Get" -# contains(input.key, "/typing/") -# input.claim.workspace == "00000000-0000-0000-0000-000000000001" -#} diff --git a/foundations/hulypulse/scripts/TEST.html b/foundations/hulypulse/scripts/TEST.html deleted file mode 100644 index b7f47d8964..0000000000 --- a/foundations/hulypulse/scripts/TEST.html +++ /dev/null @@ -1,147 +0,0 @@ - - - - - WebSocket JSON Tester - - - - -

WebSocket JSON Tester

- - - -
- - - -

- - - - - - - - - - - - - - - - - - - - - - -

Waiting for server response...
- - - - - diff --git a/foundations/hulypulse/scripts/TEST00.sh b/foundations/hulypulse/scripts/TEST00.sh deleted file mode 100755 index b050c29890..0000000000 --- a/foundations/hulypulse/scripts/TEST00.sh +++ /dev/null @@ -1,151 +0,0 @@ -#!/bin/bash - -clear -source ./pulse_lib.sh - -TOKEN=$(./token.sh claims.json) - -get "00000000-0000-0000-0000-000000000001/foo/bar1" -put "00000000-0000-0000-0000-000000000001/foo/bar1" "rediska" -get "00000000-0000-0000-0000-000000000001/foo/bar1" - -exit - -#echo ${TOKEN} -#exit -ZP="00000000-0000-0000-0000-000000000001/TESTS" - - - -put "00000000-0000-0000-0000-000000000001/TESTS/val1" "value" "HULY-TTL: 1" -put "00000000-0000-0000-0000-000000000001/TESTS/val2" "value" "HULY-TTL: 12" -put "00000000-0000-0000-0000-000000000001/TESTS/val3" "value" "HULY-TTL: 1" - -get "00000000-0000-0000-0000-000000000001/TESTS/" -sleep 2 -# get "00000000-0000-0000-0000-000000000001/TESTS/val2" -get "00000000-0000-0000-0000-000000000001/TESTS/" - - - -exit - - - - - - -put "00000000-0000-0000-0000-000000000001/TESTS" "Value" - -#exit - delete "00000000-0000-0000-0000-000000000001/TESTS" -put "00000000-0000-0000-0000-000000000001/TESTS" "Value" - delete "00000000-0000-0000-0000-000000000001/TESTS" "If-Match: *" -put "00000000-0000-0000-0000-000000000001/TESTS" "Value" - delete "00000000-0000-0000-0000-000000000001/TESTS" "If-Match: dd358c74cb9cb897424838fbcb69c933" - -#exit - - put "00000000-0000-0000-0000-000000000001/TESTS" "Value" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/1" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/2" "Value_2" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/2/$/secret" "Value_secret" "HULY-TTL: 2" - get "00000000-0000-0000-0000-000000000001/TESTS/" - -#exit - - delete "0000000/TESTS" - delete ${ZP} - put ${ZP} "Value_1" "HULY-TTL: 2" - delete ${ZP} - -echo "--------- authorization_test ----------" -TOKEN="" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" -TOKEN=$(./token.sh claims_system.json) - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" -TOKEN=$(./token.sh claims_wrong_ws.json) - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" -TOKEN=$(./token.sh claims.json) - put "00000000-0000-0000-0000-000000000002/TESTS" "Value_1" "HULY-TTL: 2" - - - -echo "--------- if-match ----------" - - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/1" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/2" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/3$" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/3/secret$/4" "Value_1" "HULY-TTL: 2" - get "00000000-0000-0000-0000-000000000001/TESTS" - get "00000000-0000-0000-0000-000000000001/TESTS/" - get "00000000-0000-0000-0000-000000000001/TESTS/3/secret$/" - - -echo "--------- Deprecated symbols ----------" - - put "00000000-0000-0000-0000-000000000001/'TESTS" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TES?TS" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS*" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/" "Value_1" "HULY-TTL: 2" - -echo "--------- if-match ----------" - - delete ${ZP} - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 1" "If-Match: *" - get ${ZP} - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_2" "HULY-TTL: 1" - get ${ZP} - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_3" "HULY-TTL: 1" "If-Match: dd358c74cb9cb897424838fbcb69c933" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_4" "HULY-TTL: 1" "If-Match: *" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_5" "HULY-TTL: 1" "If-Match: c7bcabf6b98a220f2f4888a18d01568d" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_6" "HULY-TTL: 1" "If-None-Match: *" - -echo "-- Expected OK: 201 Created (key was not exist)" - - put ${ZP} "enother text" "If-None-Match" "*" - - put ${ZP} "some text" - echo "-- Expected Error: 412 Precondition Failed (key was exist)" - put ${ZP} "enother text" "If-None-Match" "*" - -echo "================> UPDATE PUT If-Match" - - get ${ZP} - - echo "-- Expected OK: 204 No Content (right hash)" - put ${ZP} "some text" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" - get ${ZP} - - echo "-- Expected OK: 204 No Content (hash still right)" - put ${ZP} "enother version" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" - - - - - - -put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 3" -echo "sleep 1 sec" -sleep 1 -get "00000000-0000-0000-0000-000000000001/TESTS" -echo "sleep 3 sec" -sleep 2 -get "00000000-0000-0000-0000-000000000001/TESTS" - -echo "--------- delete ----------" -put "00000000-0000-0000-0000-000000000001/TESTS" "Value_2" "HULY-TTL: 3" -get "00000000-0000-0000-0000-000000000001/TESTS" -delete "00000000-0000-0000-0000-000000000001/TESTS" -get "00000000-0000-0000-0000-000000000001/TESTS" - -echo "--------- prefix ----------" -put "00000000-0000-0000-0000-000000000001/TESTS1" "Value_1" "HULY-TTL: 3" -put "00000000-0000-0000-0000-000000000001/TESTS2" "Value_1" "HULY-TTL: 3" -put "00000000-0000-0000-0000-000000000001/HREST2" "Value_1" "HULY-TTL: 3" -get "00000000-0000-0000-0000-000000000001?prefix=TES" -sleep 1 -get "00000000-0000-0000-0000-000000000001?prefix=" - -exit diff --git a/foundations/hulypulse/scripts/TEST_HTTP_API.sh b/foundations/hulypulse/scripts/TEST_HTTP_API.sh deleted file mode 100755 index 1b8b9816dd..0000000000 --- a/foundations/hulypulse/scripts/TEST_HTTP_API.sh +++ /dev/null @@ -1,144 +0,0 @@ -#!/bin/bash - -clear -source ./pulse_lib.sh - -TOKEN=$(./token.sh claims.json) -#echo ${TOKEN} -#exit -ZP="00000000-0000-0000-0000-000000000001/TESTS" - - - -put "00000000-0000-0000-0000-000000000001/TESTS/val1" "value" "HULY-TTL: 1" -put "00000000-0000-0000-0000-000000000001/TESTS/val2" "value" "HULY-TTL: 12" -put "00000000-0000-0000-0000-000000000001/TESTS/val3" "value" "HULY-TTL: 1" - -get "00000000-0000-0000-0000-000000000001/TESTS/" -sleep 2 -# get "00000000-0000-0000-0000-000000000001/TESTS/val2" -get "00000000-0000-0000-0000-000000000001/TESTS/" - - - -#exit - - - - - - -put "00000000-0000-0000-0000-000000000001/TESTS" "Value" - -#exit - delete "00000000-0000-0000-0000-000000000001/TESTS" -put "00000000-0000-0000-0000-000000000001/TESTS" "Value" - delete "00000000-0000-0000-0000-000000000001/TESTS" "If-Match: *" -put "00000000-0000-0000-0000-000000000001/TESTS" "Value" - delete "00000000-0000-0000-0000-000000000001/TESTS" "If-Match: dd358c74cb9cb897424838fbcb69c933" - -#exit - - put "00000000-0000-0000-0000-000000000001/TESTS" "Value" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/1" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/2" "Value_2" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/2/$/secret" "Value_secret" "HULY-TTL: 2" - get "00000000-0000-0000-0000-000000000001/TESTS/" - -#exit - - delete "0000000/TESTS" - delete ${ZP} - put ${ZP} "Value_1" "HULY-TTL: 2" - delete ${ZP} - -echo "--------- authorization_test ----------" -TOKEN="" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" -TOKEN=$(./token.sh claims_system.json) - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" -TOKEN=$(./token.sh claims_wrong_ws.json) - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" -TOKEN=$(./token.sh claims.json) - put "00000000-0000-0000-0000-000000000002/TESTS" "Value_1" "HULY-TTL: 2" - - - -echo "--------- if-match ----------" - - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/1" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/2" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/3$" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/3/secret$/4" "Value_1" "HULY-TTL: 2" - get "00000000-0000-0000-0000-000000000001/TESTS" - get "00000000-0000-0000-0000-000000000001/TESTS/" - get "00000000-0000-0000-0000-000000000001/TESTS/3/secret$/" - - -echo "--------- Deprecated symbols ----------" - - put "00000000-0000-0000-0000-000000000001/'TESTS" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TES?TS" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS*" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/" "Value_1" "HULY-TTL: 2" - -echo "--------- if-match ----------" - - delete ${ZP} - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 1" "If-Match: *" - get ${ZP} - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_2" "HULY-TTL: 1" - get ${ZP} - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_3" "HULY-TTL: 1" "If-Match: dd358c74cb9cb897424838fbcb69c933" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_4" "HULY-TTL: 1" "If-Match: *" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_5" "HULY-TTL: 1" "If-Match: c7bcabf6b98a220f2f4888a18d01568d" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_6" "HULY-TTL: 1" "If-None-Match: *" - -echo "-- Expected OK: 201 Created (key was not exist)" - - put ${ZP} "enother text" "If-None-Match" "*" - - put ${ZP} "some text" - echo "-- Expected Error: 412 Precondition Failed (key was exist)" - put ${ZP} "enother text" "If-None-Match" "*" - -echo "================> UPDATE PUT If-Match" - - get ${ZP} - - echo "-- Expected OK: 204 No Content (right hash)" - put ${ZP} "some text" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" - get ${ZP} - - echo "-- Expected OK: 204 No Content (hash still right)" - put ${ZP} "enother version" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" - - - - - - -put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 3" -echo "sleep 1 sec" -sleep 1 -get "00000000-0000-0000-0000-000000000001/TESTS" -echo "sleep 3 sec" -sleep 2 -get "00000000-0000-0000-0000-000000000001/TESTS" - -echo "--------- delete ----------" -put "00000000-0000-0000-0000-000000000001/TESTS" "Value_2" "HULY-TTL: 3" -get "00000000-0000-0000-0000-000000000001/TESTS" -delete "00000000-0000-0000-0000-000000000001/TESTS" -get "00000000-0000-0000-0000-000000000001/TESTS" - -echo "--------- prefix ----------" -put "00000000-0000-0000-0000-000000000001/TESTS1" "Value_1" "HULY-TTL: 3" -put "00000000-0000-0000-0000-000000000001/TESTS2" "Value_1" "HULY-TTL: 3" -put "00000000-0000-0000-0000-000000000001/HREST2" "Value_1" "HULY-TTL: 3" -get "00000000-0000-0000-0000-000000000001?prefix=TES" -sleep 1 -get "00000000-0000-0000-0000-000000000001?prefix=" - -exit diff --git a/foundations/hulypulse/scripts/TEST_HTTP_API_repo.sh b/foundations/hulypulse/scripts/TEST_HTTP_API_repo.sh deleted file mode 100755 index 7ae9608387..0000000000 --- a/foundations/hulypulse/scripts/TEST_HTTP_API_repo.sh +++ /dev/null @@ -1,152 +0,0 @@ -#!/bin/bash - -clear -source ./pulse_lib.sh - -TOKEN=$(./token.sh claims.json) - -put "00000000-0000-0000-0000-000000000001/TESTS/val1" "value" "HULY-TTL: 1" -get "00000000-0000-0000-0000-000000000001/TESTS/val1" -#list "00000000-0000-0000-0000-000000000001/TESTS/" -get "00000000-0000-0000-0000-000000000002/TESTS/val1" - -exit - -#echo ${TOKEN} - -ZP="00000000-0000-0000-0000-000000000001/TESTS" - - - -put "00000000-0000-0000-0000-000000000001/TESTS/val1" "value" "HULY-TTL: 1" -put "00000000-0000-0000-0000-000000000001/TESTS/val2" "value" "HULY-TTL: 12" -put "00000000-0000-0000-0000-000000000001/TESTS/val3" "value" "HULY-TTL: 1" - -get "00000000-0000-0000-0000-000000000001/TESTS/" -sleep 2 -# get "00000000-0000-0000-0000-000000000001/TESTS/val2" -get "00000000-0000-0000-0000-000000000001/TESTS/" - - - -#exit - - - - - - -put "00000000-0000-0000-0000-000000000001/TESTS" "Value" - -#exit - delete "00000000-0000-0000-0000-000000000001/TESTS" -put "00000000-0000-0000-0000-000000000001/TESTS" "Value" - delete "00000000-0000-0000-0000-000000000001/TESTS" "If-Match: *" -put "00000000-0000-0000-0000-000000000001/TESTS" "Value" - delete "00000000-0000-0000-0000-000000000001/TESTS" "If-Match: dd358c74cb9cb897424838fbcb69c933" - -#exit - - put "00000000-0000-0000-0000-000000000001/TESTS" "Value" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/1" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/2" "Value_2" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/2/$/secret" "Value_secret" "HULY-TTL: 2" - get "00000000-0000-0000-0000-000000000001/TESTS/" - -#exit - - delete "0000000/TESTS" - delete ${ZP} - put ${ZP} "Value_1" "HULY-TTL: 2" - delete ${ZP} - -echo "--------- authorization_test ----------" -TOKEN="" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" -TOKEN=$(./token.sh claims_system.json) - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" -TOKEN=$(./token.sh claims_wrong_ws.json) - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" -TOKEN=$(./token.sh claims.json) - put "00000000-0000-0000-0000-000000000002/TESTS" "Value_1" "HULY-TTL: 2" - - - -echo "--------- if-match ----------" - - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/1" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/2" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/3$" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/3/secret$/4" "Value_1" "HULY-TTL: 2" - get "00000000-0000-0000-0000-000000000001/TESTS" - get "00000000-0000-0000-0000-000000000001/TESTS/" - get "00000000-0000-0000-0000-000000000001/TESTS/3/secret$/" - - -echo "--------- Deprecated symbols ----------" - - put "00000000-0000-0000-0000-000000000001/'TESTS" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TES?TS" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS*" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/" "Value_1" "HULY-TTL: 2" - -echo "--------- if-match ----------" - - delete ${ZP} - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 1" "If-Match: *" - get ${ZP} - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_2" "HULY-TTL: 1" - get ${ZP} - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_3" "HULY-TTL: 1" "If-Match: dd358c74cb9cb897424838fbcb69c933" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_4" "HULY-TTL: 1" "If-Match: *" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_5" "HULY-TTL: 1" "If-Match: c7bcabf6b98a220f2f4888a18d01568d" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_6" "HULY-TTL: 1" "If-None-Match: *" - -echo "-- Expected OK: 201 Created (key was not exist)" - - put ${ZP} "enother text" "If-None-Match" "*" - - put ${ZP} "some text" - echo "-- Expected Error: 412 Precondition Failed (key was exist)" - put ${ZP} "enother text" "If-None-Match" "*" - -echo "================> UPDATE PUT If-Match" - - get ${ZP} - - echo "-- Expected OK: 204 No Content (right hash)" - put ${ZP} "some text" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" - get ${ZP} - - echo "-- Expected OK: 204 No Content (hash still right)" - put ${ZP} "enother version" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" - - - - - - -put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 3" -echo "sleep 1 sec" -sleep 1 -get "00000000-0000-0000-0000-000000000001/TESTS" -echo "sleep 3 sec" -sleep 2 -get "00000000-0000-0000-0000-000000000001/TESTS" - -echo "--------- delete ----------" -put "00000000-0000-0000-0000-000000000001/TESTS" "Value_2" "HULY-TTL: 3" -get "00000000-0000-0000-0000-000000000001/TESTS" -delete "00000000-0000-0000-0000-000000000001/TESTS" -get "00000000-0000-0000-0000-000000000001/TESTS" - -echo "--------- prefix ----------" -put "00000000-0000-0000-0000-000000000001/TESTS1" "Value_1" "HULY-TTL: 3" -put "00000000-0000-0000-0000-000000000001/TESTS2" "Value_1" "HULY-TTL: 3" -put "00000000-0000-0000-0000-000000000001/HREST2" "Value_1" "HULY-TTL: 3" -get "00000000-0000-0000-0000-000000000001?prefix=TES" -sleep 1 -get "00000000-0000-0000-0000-000000000001?prefix=" - -exit diff --git a/foundations/hulypulse/scripts/TEST_WS_API.sh b/foundations/hulypulse/scripts/TEST_WS_API.sh deleted file mode 100755 index 2d111e134c..0000000000 --- a/foundations/hulypulse/scripts/TEST_WS_API.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/bash - -clear -#source ./pulse_lib.sh - -websocat ws://127.0.0.1:8095/ws/testworkspace - -exit - - -let ws = new WebSocket("ws://localhost:8095/ws/testworkspace"); -ws.onmessage = e => console.log("Message from server:", e.data); -ws.onopen = () => ws.send("Hello from browser!"); - - - - - - - - - - - - - - - - - - - -TOKEN=$(./token.sh claims.json) -ZP="00000000-0000-0000-0000-000000000001/TESTS" -# /AnyKey" - -# put ${ZP} "one text" - -# put "00000000-0000-0000-0000-000000000001/TESTS" "text 1" "If-None-Match: *" "Blooooooooo: blya" - -#exit - -#put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 3" -#echo "sleep 1 sec" -#sleep 1 -#get "00000000-0000-0000-0000-000000000001/TESTS" -#echo "sleep 3 sec" -#sleep 2 -#get "00000000-0000-0000-0000-000000000001/TESTS" - -put "00000000-0000-0000-0000-000000000001/TESTS1" "Value_1" "HULY-TTL: 3" -put "00000000-0000-0000-0000-000000000001/TESTS2" "Value_1" "HULY-TTL: 3" -put "00000000-0000-0000-0000-000000000001/HREST2" "Value_1" "HULY-TTL: 3" -get "00000000-0000-0000-0000-000000000001?prefix=TES" -sleep 1 -get "00000000-0000-0000-0000-000000000001?prefix=" - -exit diff --git a/foundations/hulypulse/scripts/TEST_lleo.html b/foundations/hulypulse/scripts/TEST_lleo.html deleted file mode 100644 index 3a9608a5e1..0000000000 --- a/foundations/hulypulse/scripts/TEST_lleo.html +++ /dev/null @@ -1,129 +0,0 @@ - - - - - WebSocket JSON Tester - - - - -

WebSocket JSON Tester

- - - -
- - - -

- - - - - - - - - -

Waiting for server response...
- - - - - diff --git a/foundations/hulypulse/scripts/TEST_no_auth.html b/foundations/hulypulse/scripts/TEST_no_auth.html deleted file mode 100644 index 20e2e14c30..0000000000 --- a/foundations/hulypulse/scripts/TEST_no_auth.html +++ /dev/null @@ -1,149 +0,0 @@ - - - - - WebSocket JSON Tester - - - - -

WebSocket JSON Tester

- - - -
- - - -

- - - - - - - - - - - - - - - - - - - - - - -

Waiting for server response...
- - - - - \ No newline at end of file diff --git a/foundations/hulypulse/scripts/claims.json b/foundations/hulypulse/scripts/claims.json deleted file mode 100644 index af4a64c94b..0000000000 --- a/foundations/hulypulse/scripts/claims.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extra": { - "service": "account" - }, - "account": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", - "workspace": "00000000-0000-0000-0000-000000000001" -} diff --git a/foundations/hulypulse/scripts/claims2.json b/foundations/hulypulse/scripts/claims2.json deleted file mode 100644 index ed17b895b8..0000000000 --- a/foundations/hulypulse/scripts/claims2.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extra": { - "service": "account" - }, - "account": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", - "workspace": "00000000-0000-0000-0000-000000000002" -} diff --git a/foundations/hulypulse/scripts/claims_exp.json b/foundations/hulypulse/scripts/claims_exp.json deleted file mode 100644 index dcc90fe4b8..0000000000 --- a/foundations/hulypulse/scripts/claims_exp.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extra": { - "service": "account" - }, - "account": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", - "workspace": "00000000-0000-0000-0000-000000000001", - "exp": 1924236800 -} diff --git a/foundations/hulypulse/scripts/claims_system.json b/foundations/hulypulse/scripts/claims_system.json deleted file mode 100644 index a6f400009e..0000000000 --- a/foundations/hulypulse/scripts/claims_system.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extra": { - "service": "account" - }, - "account": "1749089e-22e6-48de-af4e-165e18fbd2f9", - "workspace": "00000000-0000-0000-0000-000000000001" -} diff --git a/foundations/hulypulse/scripts/claims_wrong_ws.json b/foundations/hulypulse/scripts/claims_wrong_ws.json deleted file mode 100644 index 8bd456b086..0000000000 --- a/foundations/hulypulse/scripts/claims_wrong_ws.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extra": { - "service": "account" - }, - "account": "lleo", - "workspace": "00000000-0000-0000-0000-000000000002" -} diff --git a/foundations/hulypulse/scripts/lleo_TEST_HTTP_API.sh b/foundations/hulypulse/scripts/lleo_TEST_HTTP_API.sh deleted file mode 100755 index a30faf5608..0000000000 --- a/foundations/hulypulse/scripts/lleo_TEST_HTTP_API.sh +++ /dev/null @@ -1,149 +0,0 @@ -#!/bin/bash - -clear -source ./pulse_lib_lleo.sh - -#TOKEN=$(./token.sh claims.json) -#echo ${TOKEN} - - - -put "dnevnik/onlline/admin" "oki" "HULY-TTL: 3" - - - -exit -ZP="00000000-0000-0000-0000-000000000001/TESTS" - -put "00000000-0000-0000-0000-000000000001/TESTS/val1" "value" "HULY-TTL: 3" -put "00000000-0000-0000-0000-000000000001/TESTS/val2" "value" "HULY-TTL: 12" -put "00000000-0000-0000-0000-000000000001/TESTS/val3" "value" "HULY-TTL: 3" - -get "00000000-0000-0000-0000-000000000001/TESTS/" -sleep 4 -# get "00000000-0000-0000-0000-000000000001/TESTS/val2" -get "00000000-0000-0000-0000-000000000001/TESTS/" - - - -exit - - - - - - -put "00000000-0000-0000-0000-000000000001/TESTS" "Value" - -#exit - delete "00000000-0000-0000-0000-000000000001/TESTS" -put "00000000-0000-0000-0000-000000000001/TESTS" "Value" - delete "00000000-0000-0000-0000-000000000001/TESTS" "If-Match: *" -put "00000000-0000-0000-0000-000000000001/TESTS" "Value" - delete "00000000-0000-0000-0000-000000000001/TESTS" "If-Match: dd358c74cb9cb897424838fbcb69c933" - -#exit - - put "00000000-0000-0000-0000-000000000001/TESTS" "Value" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/1" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/2" "Value_2" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/2/$/secret" "Value_secret" "HULY-TTL: 2" - get "00000000-0000-0000-0000-000000000001/TESTS/" - -#exit - - delete "0000000/TESTS" - delete ${ZP} - put ${ZP} "Value_1" "HULY-TTL: 2" - delete ${ZP} - -echo "--------- authorization_test ----------" -TOKEN="" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" -TOKEN=$(./token.sh claims_system.json) - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" -TOKEN=$(./token.sh claims_wrong_ws.json) - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" -TOKEN=$(./token.sh claims.json) - put "00000000-0000-0000-0000-000000000002/TESTS" "Value_1" "HULY-TTL: 2" - - - -echo "--------- if-match ----------" - - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/1" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/2" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/3$" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/3/secret$/4" "Value_1" "HULY-TTL: 2" - get "00000000-0000-0000-0000-000000000001/TESTS" - get "00000000-0000-0000-0000-000000000001/TESTS/" - get "00000000-0000-0000-0000-000000000001/TESTS/3/secret$/" - - -echo "--------- Deprecated symbols ----------" - - put "00000000-0000-0000-0000-000000000001/'TESTS" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TES?TS" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS*" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/" "Value_1" "HULY-TTL: 2" - -echo "--------- if-match ----------" - - delete ${ZP} - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 1" "If-Match: *" - get ${ZP} - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_2" "HULY-TTL: 1" - get ${ZP} - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_3" "HULY-TTL: 1" "If-Match: dd358c74cb9cb897424838fbcb69c933" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_4" "HULY-TTL: 1" "If-Match: *" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_5" "HULY-TTL: 1" "If-Match: c7bcabf6b98a220f2f4888a18d01568d" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_6" "HULY-TTL: 1" "If-None-Match: *" - -echo "-- Expected OK: 201 Created (key was not exist)" - - put ${ZP} "enother text" "If-None-Match" "*" - - put ${ZP} "some text" - echo "-- Expected Error: 412 Precondition Failed (key was exist)" - put ${ZP} "enother text" "If-None-Match" "*" - -echo "================> UPDATE PUT If-Match" - - get ${ZP} - - echo "-- Expected OK: 204 No Content (right hash)" - put ${ZP} "some text" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" - get ${ZP} - - echo "-- Expected OK: 204 No Content (hash still right)" - put ${ZP} "enother version" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" - - - - - - -put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 3" -echo "sleep 1 sec" -sleep 1 -get "00000000-0000-0000-0000-000000000001/TESTS" -echo "sleep 3 sec" -sleep 2 -get "00000000-0000-0000-0000-000000000001/TESTS" - -echo "--------- delete ----------" -put "00000000-0000-0000-0000-000000000001/TESTS" "Value_2" "HULY-TTL: 3" -get "00000000-0000-0000-0000-000000000001/TESTS" -delete "00000000-0000-0000-0000-000000000001/TESTS" -get "00000000-0000-0000-0000-000000000001/TESTS" - -echo "--------- prefix ----------" -put "00000000-0000-0000-0000-000000000001/TESTS1" "Value_1" "HULY-TTL: 3" -put "00000000-0000-0000-0000-000000000001/TESTS2" "Value_1" "HULY-TTL: 3" -put "00000000-0000-0000-0000-000000000001/HREST2" "Value_1" "HULY-TTL: 3" -get "00000000-0000-0000-0000-000000000001?prefix=TES" -sleep 1 -get "00000000-0000-0000-0000-000000000001?prefix=" - -exit diff --git a/foundations/hulypulse/scripts/pulse_lib.sh b/foundations/hulypulse/scripts/pulse_lib.sh deleted file mode 100755 index 19f565c8f8..0000000000 --- a/foundations/hulypulse/scripts/pulse_lib.sh +++ /dev/null @@ -1,77 +0,0 @@ -#!/bin/bash - -clear - -URL="http://localhost:8099/api" - -R='\033[0;31m' # Color red -G='\033[0;32m' # Color green -W='\033[0;33m' # Color ? -S='\033[0;34m' # Color Blue -F='\033[0;35m' # Color Fiolet -L='\033[0;36m' # Color LightBlue -N='\033[0m' # No Color -GRAY='\033[90m' # bright black - -api() { - local tmpfile - tmpfile=$1 - local status - status=$(head -n 1 "$tmpfile") - local status_code - status_code=$(echo "$status" | awk '{print $2}') - local etag - etag=$(grep -i "^ETag:" "${tmpfile}") - local body - body=$(awk 'found { print; next } NF == 0 { found = 1 }' "$tmpfile") - case "$status_code" in - 2*) echo -en "${G}${status}${N}" ;; - 3*) echo -en "${F}${status}${N}" ;; - 4*) echo -en "${R}${status}${N}" ;; - 5*) echo -en "${R}${status}${N}" ;; - *) echo -en "${GRAY}${status}${N}" ;; - esac - if [ -n "$etag" ]; then echo -n -e " ${F}${etag}${N}" ; fi - - body=$(echo "$body" | sed 's/{/\\n{/g') - - if [ -n "$body" ]; then echo -e "\n ${GRAY}[${body}]${N}" ; else echo -e " ${L}(no body)${N}" ; fi - rm -f "$tmpfile" -} - -get() { - echo -n -e "📥 ${L}GET ${W}$1${N} > " - local tmpfile - tmpfile=$(mktemp) - curl -i -s -X GET "$URL/$1" -H "Authorization: Bearer ${TOKEN}" | tr -d '\r' > "$tmpfile" - api ${tmpfile} -} - -put() { # If-None-Match If-Match - local match - local match_prn -# if [ -n "$3" ]; then match=(-H "$3: $4") ; else match=() ; fi -# if [ -n "$3" ]; then match_prn=" ${F}$3:$4${N}" ; else match_prn="" ; fi -# echo -n -e "📥 ${L}PUT ${W}$1${N}${match_prn} > " - - if [ -n "$3" ]; then match1=(-H "$3") ; else match1=() ; fi - if [ -n "$3" ]; then match1_prn=" ${F}$3${N}" ; else match1_prn="" ; fi - if [ -n "$4" ]; then match2=(-H "$4") ; else match2=() ; fi - if [ -n "$4" ]; then match2_prn=" ${F}$4${N}" ; else match2_prn="" ; fi - echo -n -e "📥 ${L}PUT ${W}$1${N}${match1_prn}${match2_prn} > " - - local tmpfile - tmpfile=$(mktemp) -# curl -v -i -s -X PUT "$URL/$1" -H "Authorization: Bearer ${TOKEN}" "${match1[@]}" "${match2[@]}" -H "Content-Type: application/json" -d "$2" | tr -d '\r' > "$tmpfile" - curl -i -s -X PUT "$URL/$1" -H "Authorization: Bearer ${TOKEN}" "${match1[@]}" "${match2[@]}" -H "Content-Type: application/json" -d "$2" | tr -d '\r' > "$tmpfile" - api ${tmpfile} -} - -delete() { - echo -n -e "📥 ${L}DELETE ${W}$1${N} > " - local tmpfile - tmpfile=$(mktemp) - curl -i -s -X DELETE "$URL/$1" -H "Authorization: Bearer ${TOKEN}" | tr -d '\r' > "$tmpfile" -# curl -v -i -s -X DELETE "$URL/$1" -H "Authorization: Bearer ${TOKEN}" | tr -d '\r' > "$tmpfile" - api ${tmpfile} -} diff --git a/foundations/hulypulse/scripts/pulse_lib_huly.sh b/foundations/hulypulse/scripts/pulse_lib_huly.sh deleted file mode 100755 index a500cf371f..0000000000 --- a/foundations/hulypulse/scripts/pulse_lib_huly.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/bin/bash - -clear - -# URL="http://localhost:8095/api" -URL="http://huly.local:8099/api" - -R='\033[0;31m' # Color red -G='\033[0;32m' # Color green -W='\033[0;33m' # Color ? -S='\033[0;34m' # Color Blue -F='\033[0;35m' # Color Fiolet -L='\033[0;36m' # Color LightBlue -N='\033[0m' # No Color -GRAY='\033[90m' # bright black - -api() { - local tmpfile - tmpfile=$1 - local status - status=$(head -n 1 "$tmpfile") - local status_code - status_code=$(echo "$status" | awk '{print $2}') - local etag - etag=$(grep -i "^ETag:" "${tmpfile}") - local body - body=$(awk 'found { print; next } NF == 0 { found = 1 }' "$tmpfile") - case "$status_code" in - 2*) echo -en "${G}${status}${N}" ;; - 3*) echo -en "${F}${status}${N}" ;; - 4*) echo -en "${R}${status}${N}" ;; - 5*) echo -en "${R}${status}${N}" ;; - *) echo -en "${GRAY}${status}${N}" ;; - esac - if [ -n "$etag" ]; then echo -n -e " ${F}${etag}${N}" ; fi - - body=$(echo "$body" | sed 's/{/\\n{/g') - - if [ -n "$body" ]; then echo -e "\n ${GRAY}[${body}]${N}" ; else echo -e " ${L}(no body)${N}" ; fi - rm -f "$tmpfile" -} - -get() { - echo -n -e "📥 ${L}GET ${W}$1${N} > " - local tmpfile - tmpfile=$(mktemp) - curl -i -s -X GET "$URL/$1" -H "Authorization: Bearer ${TOKEN}" | tr -d '\r' > "$tmpfile" - api ${tmpfile} -} - -put() { # If-None-Match If-Match - local match - local match_prn -# if [ -n "$3" ]; then match=(-H "$3: $4") ; else match=() ; fi -# if [ -n "$3" ]; then match_prn=" ${F}$3:$4${N}" ; else match_prn="" ; fi -# echo -n -e "📥 ${L}PUT ${W}$1${N}${match_prn} > " - - if [ -n "$3" ]; then match1=(-H "$3") ; else match1=() ; fi - if [ -n "$3" ]; then match1_prn=" ${F}$3${N}" ; else match1_prn="" ; fi - if [ -n "$4" ]; then match2=(-H "$4") ; else match2=() ; fi - if [ -n "$4" ]; then match2_prn=" ${F}$4${N}" ; else match2_prn="" ; fi - echo -n -e "📥 ${L}PUT ${W}$1${N}${match1_prn}${match2_prn} > " - - local tmpfile - tmpfile=$(mktemp) -# curl -v -i -s -X PUT "$URL/$1" -H "Authorization: Bearer ${TOKEN}" "${match1[@]}" "${match2[@]}" -H "Content-Type: application/json" -d "$2" | tr -d '\r' > "$tmpfile" - curl -i -s -X PUT "$URL/$1" -H "Authorization: Bearer ${TOKEN}" "${match1[@]}" "${match2[@]}" -H "Content-Type: application/json" -d "$2" | tr -d '\r' > "$tmpfile" - api ${tmpfile} -} - -delete() { - echo -n -e "📥 ${L}DELETE ${W}$1${N} > " - local tmpfile - tmpfile=$(mktemp) - curl -i -s -X DELETE "$URL/$1" -H "Authorization: Bearer ${TOKEN}" | tr -d '\r' > "$tmpfile" -# curl -v -i -s -X DELETE "$URL/$1" -H "Authorization: Bearer ${TOKEN}" | tr -d '\r' > "$tmpfile" - api ${tmpfile} -} diff --git a/foundations/hulypulse/scripts/pulse_lib_lleo.sh b/foundations/hulypulse/scripts/pulse_lib_lleo.sh deleted file mode 100755 index 4cf8832fca..0000000000 --- a/foundations/hulypulse/scripts/pulse_lib_lleo.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/bin/bash - -clear - -# URL="http://localhost:8095/api" -URL="https://hulypulse_mem.lleo.me/api" - -R='\033[0;31m' # Color red -G='\033[0;32m' # Color green -W='\033[0;33m' # Color ? -S='\033[0;34m' # Color Blue -F='\033[0;35m' # Color Fiolet -L='\033[0;36m' # Color LightBlue -N='\033[0m' # No Color -GRAY='\033[90m' # bright black - -api() { - local tmpfile - tmpfile=$1 - local status - status=$(head -n 1 "$tmpfile") - local status_code - status_code=$(echo "$status" | awk '{print $2}') - local etag - etag=$(grep -i "^ETag:" "${tmpfile}") - local body - body=$(awk 'found { print; next } NF == 0 { found = 1 }' "$tmpfile") - case "$status_code" in - 2*) echo -en "${G}${status}${N}" ;; - 3*) echo -en "${F}${status}${N}" ;; - 4*) echo -en "${R}${status}${N}" ;; - 5*) echo -en "${R}${status}${N}" ;; - *) echo -en "${GRAY}${status}${N}" ;; - esac - if [ -n "$etag" ]; then echo -n -e " ${F}${etag}${N}" ; fi - - body=$(echo "$body" | sed 's/{/\\n{/g') - - if [ -n "$body" ]; then echo -e "\n ${GRAY}[${body}]${N}" ; else echo -e " ${L}(no body)${N}" ; fi - rm -f "$tmpfile" -} - -get() { - echo -n -e "📥 ${L}GET ${W}$1${N} > " - local tmpfile - tmpfile=$(mktemp) - curl -i -s -X GET "$URL/$1" -H "Authorization: Bearer ${TOKEN}" | tr -d '\r' > "$tmpfile" - api ${tmpfile} -} - -put() { # If-None-Match If-Match - local match - local match_prn -# if [ -n "$3" ]; then match=(-H "$3: $4") ; else match=() ; fi -# if [ -n "$3" ]; then match_prn=" ${F}$3:$4${N}" ; else match_prn="" ; fi -# echo -n -e "📥 ${L}PUT ${W}$1${N}${match_prn} > " - - if [ -n "$3" ]; then match1=(-H "$3") ; else match1=() ; fi - if [ -n "$3" ]; then match1_prn=" ${F}$3${N}" ; else match1_prn="" ; fi - if [ -n "$4" ]; then match2=(-H "$4") ; else match2=() ; fi - if [ -n "$4" ]; then match2_prn=" ${F}$4${N}" ; else match2_prn="" ; fi - echo -n -e "📥 ${L}PUT ${W}$1${N}${match1_prn}${match2_prn} > " - - local tmpfile - tmpfile=$(mktemp) -# curl -v -i -s -X PUT "$URL/$1" "${match1[@]}" "${match2[@]}" -H "Content-Type: application/json" -d "$2" | tr -d '\r' > "$tmpfile" -# curl -v -i -s -X PUT "$URL/$1" -H "Authorization: Bearer ${TOKEN}" "${match1[@]}" "${match2[@]}" -H "Content-Type: application/json" -d "$2" | tr -d '\r' > "$tmpfile" - curl -i -s -X PUT "$URL/$1" -H "Authorization: Bearer ${TOKEN}" "${match1[@]}" "${match2[@]}" -H "Content-Type: application/json" -d "$2" | tr -d '\r' > "$tmpfile" - api ${tmpfile} -} - -delete() { - echo -n -e "📥 ${L}DELETE ${W}$1${N} > " - local tmpfile - tmpfile=$(mktemp) - curl -i -s -X DELETE "$URL/$1" -H "Authorization: Bearer ${TOKEN}" | tr -d '\r' > "$tmpfile" -# curl -v -i -s -X DELETE "$URL/$1" -H "Authorization: Bearer ${TOKEN}" | tr -d '\r' > "$tmpfile" - api ${tmpfile} -} diff --git a/foundations/hulypulse/scripts/test_pulse.sh b/foundations/hulypulse/scripts/test_pulse.sh deleted file mode 100755 index 808173ffe9..0000000000 --- a/foundations/hulypulse/scripts/test_pulse.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/bin/bash - -clear -source ./pulse_lib.sh - -TOKEN=$(./token.sh claims.json) -ZP="00000000-0000-0000-0000-000000000001/TESTS/AnyKey" - -echo "================> LIST" - put "00000000-0000-0000-0000-000000000001/Huome2/MyKey1" "value1" - put "00000000-0000-0000-0000-000000000001/Huome2/MyKey2" "value2" - get "00000000-0000-0000-0000-000000000001/Huome2" - delete "00000000-0000-0000-0000-000000000001/Huome2/MyKey1" - delete "00000000-0000-0000-0000-000000000001/Huome2/MyKey2" - -echo "================> WRONG UUID" - get "WrongUUID/TESTS/AnyKey" - -echo "================> INSERT If-None-Match" - - echo "-- Expected Error: 400 Bad Request (If-None-Match may be only *)" - put ${ZP} "enother text" "If-None-Match" "552e21cd4cd9918678e3c1a0df491bc3" - - delete ${ZP} - - echo "-- Expected OK: 201 Created (key was not exist)" - put ${ZP} "enother text" "If-None-Match" "*" - - put ${ZP} "some text" - echo "-- Expected Error: 412 Precondition Failed (key was exist)" - put ${ZP} "enother text" "If-None-Match" "*" - -echo "================> UPDATE PUT If-Match" - - get ${ZP} - - echo "-- Expected OK: 204 No Content (right hash)" - put ${ZP} "some text" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" - get ${ZP} - - echo "-- Expected OK: 204 No Content (hash still right)" - put ${ZP} "enother version" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" - get ${ZP} - - echo "-- Expected OK: 204 No Content (any hash)" - put ${ZP} "enother version2" "If-Match" "*" - get ${ZP} - - echo "-- Expected Error: 412 Precondition Failed (wrong hash)" - put ${ZP} "enother version3" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" - - delete ${ZP} - - echo "-- Expected Error: 412 Precondition Failed (any hash not found)" - put ${ZP} "enother version2" "If-Match" "*" - -echo "================> UPSERT (Expected OK)" - put ${ZP} "my value" - get ${ZP} - put ${ZP} "my new value" - get ${ZP} - -exit diff --git a/foundations/hulypulse/scripts/test_pulse_system.sh b/foundations/hulypulse/scripts/test_pulse_system.sh deleted file mode 100755 index be2c78fa0d..0000000000 --- a/foundations/hulypulse/scripts/test_pulse_system.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash - -clear -source ./pulse_lib.sh - -TOKEN_OK=$(./token.sh claims.json) -TOKEN_SYSTEM=$(./token.sh claims_system.json) -TOKEN_WRONG=$(./token.sh claims_wrong_ws.json) -ZP="00000000-0000-0000-0000-000000000001/TESTS/JWT_tests" - -echo "================> SYSTEM change - OK" - TOKEN=${TOKEN_SYSTEM} - # delete ${ZP} - put ${ZP} "system value" - -echo "================> USER read/change - OK" - TOKEN=${TOKEN_OK} - get ${ZP} - put ${ZP} "user value" - -echo "================> WRONG USER read/change - ERROR" - TOKEN=${TOKEN_WRONG} - get ${ZP} - put ${ZP} "wrong user value" - -echo "================> SYSTEM read/change - OK" - TOKEN=${TOKEN_SYSTEM} - get ${ZP} - put ${ZP} "system value 2" - -echo "================> USER read - OK" - TOKEN=${TOKEN_OK} - get ${ZP} - -exit diff --git a/foundations/hulypulse/scripts/token.sh b/foundations/hulypulse/scripts/token.sh deleted file mode 100755 index 78ae2ef971..0000000000 --- a/foundations/hulypulse/scripts/token.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -CONFIG_PATH="../src/config/default.toml" -SECRET=$(grep '^token_secret' "$CONFIG_PATH" | sed -E 's/.*=\s*"(.*)"/\1/') # " - -if [ -z "$SECRET" ]; then - echo "❌No token_secret in $CONFIG_PATH" - exit 1 -fi - -claims=$1 # "claims.json" - -#TOKEN=$(echo -n "${SECRET}" | jwt -alg HS256 -key - -sign claims.json) -TOKEN=$(echo -n "${SECRET}" | jwt -alg HS256 -key - -sign ${claims}) - -echo "$TOKEN" diff --git a/foundations/hulypulse/scripts/typing-test.sh b/foundations/hulypulse/scripts/typing-test.sh deleted file mode 100755 index 4c7990b504..0000000000 --- a/foundations/hulypulse/scripts/typing-test.sh +++ /dev/null @@ -1,151 +0,0 @@ -#!/bin/bash - -clear -source ./pulse_lib_huly.sh - -#TOKEN=$(./token.sh claims.json) -TOKEN="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHRyYSI6e30sImFjY291bnQiOiI1NjBlNDRiYS1jM2ZhLTRmMzUtYjQxYi00NWMzY2FhYWZiZTAiLCJ3b3Jrc3BhY2UiOiI4NTk5ZWViZS0xZDEwLTRhNDYtYTIxZS04OWNkMzI4YjRmZmEifQ.rTmKG5ulwTONs6KPfmBOLnY6BaXfwP1kma_Pvay-pz8" -echo ${TOKEN} - -#exit -#ZP="00000000-0000-0000-0000-000000000001/TESTS" - -#put "8599eebe-1d10-4a46-a21e-89cd328b4ffa/typing/chunter:space:General/68874fd619a81293751d001e" "{\"personId\":\"68874fd619a81293751d001e\",\"objectId\":\"chunter:space:General\"}" "HULY-TTL: 120" - -put "8599eebe-1d10-4a46-a21e-89cd328b4ffa/typing/68e259323d9a9ae45c7dd0ea/68874fd619a81293751d001e" "{\"personId\":\"68874fd619a81293751d001e\",\"objectId\":\"68e259323d9a9ae45c7dd0ea\"}" "HULY-TTL: 15" -put "8599eebe-1d10-4a46-a21e-89cd328b4ffa/typing/68e259323d9a9ae45c7dd0ea/68e2585a62753bede49ee803" "{\"personId\":\"68e2585a62753bede49ee803\",\"objectId\":\"68e259323d9a9ae45c7dd0ea\"}" "HULY-TTL: 15" -# "8599eebe-1d10-4a46-a21e-89cd328b4ffa/typing/68e259323d9a9ae45c7dd0ea/" - -#put "00000000-0000-0000-0000-000000000001/TESTS/val1" "value" "HULY-TTL: 1" -#put "00000000-0000-0000-0000-000000000001/TESTS/val2" "value" "HULY-TTL: 120" -#put "00000000-0000-0000-0000-000000000001/TESTS/val3" "value" "HULY-TTL: 1" -#get "00000000-0000-0000-0000-000000000001/TESTS/" -#sleep 2 -# get "00000000-0000-0000-0000-000000000001/TESTS/val2" -#get "00000000-0000-0000-0000-000000000001/TESTS/" - - - -exit - -http://huly.local:8099/status - - - - - - -put "00000000-0000-0000-0000-000000000001/TESTS" "Value" - -#exit - delete "00000000-0000-0000-0000-000000000001/TESTS" -put "00000000-0000-0000-0000-000000000001/TESTS" "Value" - delete "00000000-0000-0000-0000-000000000001/TESTS" "If-Match: *" -put "00000000-0000-0000-0000-000000000001/TESTS" "Value" - delete "00000000-0000-0000-0000-000000000001/TESTS" "If-Match: dd358c74cb9cb897424838fbcb69c933" - -#exit - - put "00000000-0000-0000-0000-000000000001/TESTS" "Value" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/1" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/2" "Value_2" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/2/$/secret" "Value_secret" "HULY-TTL: 2" - get "00000000-0000-0000-0000-000000000001/TESTS/" - -#exit - - delete "0000000/TESTS" - delete ${ZP} - put ${ZP} "Value_1" "HULY-TTL: 2" - delete ${ZP} - -echo "--------- authorization_test ----------" -TOKEN="" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" -TOKEN=$(./token.sh claims_system.json) - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" -TOKEN=$(./token.sh claims_wrong_ws.json) - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" -TOKEN=$(./token.sh claims.json) - put "00000000-0000-0000-0000-000000000002/TESTS" "Value_1" "HULY-TTL: 2" - - - -echo "--------- if-match ----------" - - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/1" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/2" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/3$" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/3/secret$/4" "Value_1" "HULY-TTL: 2" - get "00000000-0000-0000-0000-000000000001/TESTS" - get "00000000-0000-0000-0000-000000000001/TESTS/" - get "00000000-0000-0000-0000-000000000001/TESTS/3/secret$/" - - -echo "--------- Deprecated symbols ----------" - - put "00000000-0000-0000-0000-000000000001/'TESTS" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TES?TS" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS*" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/" "Value_1" "HULY-TTL: 2" - -echo "--------- if-match ----------" - - delete ${ZP} - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 1" "If-Match: *" - get ${ZP} - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_2" "HULY-TTL: 1" - get ${ZP} - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_3" "HULY-TTL: 1" "If-Match: dd358c74cb9cb897424838fbcb69c933" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_4" "HULY-TTL: 1" "If-Match: *" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_5" "HULY-TTL: 1" "If-Match: c7bcabf6b98a220f2f4888a18d01568d" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_6" "HULY-TTL: 1" "If-None-Match: *" - -echo "-- Expected OK: 201 Created (key was not exist)" - - put ${ZP} "enother text" "If-None-Match" "*" - - put ${ZP} "some text" - echo "-- Expected Error: 412 Precondition Failed (key was exist)" - put ${ZP} "enother text" "If-None-Match" "*" - -echo "================> UPDATE PUT If-Match" - - get ${ZP} - - echo "-- Expected OK: 204 No Content (right hash)" - put ${ZP} "some text" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" - get ${ZP} - - echo "-- Expected OK: 204 No Content (hash still right)" - put ${ZP} "enother version" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" - - - - - - -put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 3" -echo "sleep 1 sec" -sleep 1 -get "00000000-0000-0000-0000-000000000001/TESTS" -echo "sleep 3 sec" -sleep 2 -get "00000000-0000-0000-0000-000000000001/TESTS" - -echo "--------- delete ----------" -put "00000000-0000-0000-0000-000000000001/TESTS" "Value_2" "HULY-TTL: 3" -get "00000000-0000-0000-0000-000000000001/TESTS" -delete "00000000-0000-0000-0000-000000000001/TESTS" -get "00000000-0000-0000-0000-000000000001/TESTS" - -echo "--------- prefix ----------" -put "00000000-0000-0000-0000-000000000001/TESTS1" "Value_1" "HULY-TTL: 3" -put "00000000-0000-0000-0000-000000000001/TESTS2" "Value_1" "HULY-TTL: 3" -put "00000000-0000-0000-0000-000000000001/HREST2" "Value_1" "HULY-TTL: 3" -get "00000000-0000-0000-0000-000000000001?prefix=TES" -sleep 1 -get "00000000-0000-0000-0000-000000000001?prefix=" - -exit diff --git a/foundations/hulypulse/src/GOT.sh b/foundations/hulypulse/src/GOT.sh deleted file mode 100755 index 9497816776..0000000000 --- a/foundations/hulypulse/src/GOT.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -clear - -redis-cli set lleo value -redis-cli del lleo -redis-cli set ttlkey 1 EX 2 -# подожди ~2 сек → должно прийти expired diff --git a/foundations/hulypulse/src/config.rs b/foundations/hulypulse/src/config.rs deleted file mode 100644 index ab204576f0..0000000000 --- a/foundations/hulypulse/src/config.rs +++ /dev/null @@ -1,90 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -use std::{path::Path, sync::LazyLock}; - -use secrecy::SecretString; -use serde::Deserialize; -use serde_with::formats::CommaSeparator; -use serde_with::{StringWithSeparator, serde_as}; -use url::Url; - -use config::FileFormat; - -#[derive(Deserialize, Debug, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum RedisMode { - Sentinel, - Direct, -} - -#[derive(Deserialize, Debug, PartialEq, strum::Display)] -#[serde(rename_all = "lowercase")] -pub enum BackendType { - Memory, - Redis, -} - -#[serde_as] -#[derive(Deserialize, Debug)] -pub struct Config { - pub bind_port: u16, - pub bind_host: String, - - pub token_secret: SecretString, - - #[serde_as(as = "StringWithSeparator::")] - pub redis_urls: Vec, - pub redis_password: String, - pub redis_mode: RedisMode, - pub redis_service: String, - - pub max_ttl: usize, - pub max_size: Option, - - pub backend: BackendType, - pub no_authorization: bool, - - pub heartbeat_timeout: u64, - pub ping_timeout: u64, - - pub policy_file: Option, -} - -pub static CONFIG: LazyLock = LazyLock::new(|| { - const DEFAULTS: &str = std::include_str!("config/default.toml"); - - let mut builder = - config::Config::builder().add_source(config::File::from_str(DEFAULTS, FileFormat::Toml)); - - let path = Path::new("etc/config.toml"); - - if path.exists() { - builder = builder.add_source(config::File::with_name(path.as_os_str().to_str().unwrap())); - } - - let settings = builder - .add_source(config::Environment::with_prefix("HULY")) - .build() - .and_then(|c| c.try_deserialize::()); - - match settings { - Ok(settings) => settings, - Err(error) => { - eprintln!("configuration error: {}", error); - std::process::exit(1); - } - } -}); diff --git a/foundations/hulypulse/src/config/default.toml b/foundations/hulypulse/src/config/default.toml deleted file mode 100644 index 5e8d1228ec..0000000000 --- a/foundations/hulypulse/src/config/default.toml +++ /dev/null @@ -1,21 +0,0 @@ -bind_port = 8099 -bind_host = "0.0.0.0" - -token_secret = "secret" - -redis_urls = "redis://huly.local:6379" -redis_password = "" -redis_mode = "direct" -redis_service = "mymaster" - -max_ttl = 3600 -backend = "redis" -no_authorization = false - -heartbeat_timeout = 90 -ping_timeout = 30 - -# optional settings -# max_size = 100 - -# permit_file = "/home/user/hulipulse/permit.rego" diff --git a/foundations/hulypulse/src/db.rs b/foundations/hulypulse/src/db.rs deleted file mode 100644 index aadac7c1ab..0000000000 --- a/foundations/hulypulse/src/db.rs +++ /dev/null @@ -1,128 +0,0 @@ -use std::sync::Arc; - -use crate::{ - hub_service::{HubState, RedisEvent, RedisEventAction, broadcast_event}, - memory::{MemoryBackend, memory_delete, memory_info, memory_list, memory_read, memory_save}, - redis::{ - RedisArray, SaveMode, Ttl, redis_delete, redis_info, redis_list, redis_read, redis_save, - }, -}; -use ::redis::aio::MultiplexedConnection; -use tokio::sync::RwLock; - -#[derive(Clone)] -pub struct Db { - inner: DbInner, - hub: Arc>, -} - -#[derive(Clone)] -enum DbInner { - Memory(MemoryBackend), - Redis(MultiplexedConnection), -} - -impl Db { - pub fn new_memory(m: MemoryBackend, hub: Arc>) -> Self { - Self { - inner: DbInner::Memory(m), - hub, - } - } - pub fn new_redis(c: MultiplexedConnection, hub: Arc>) -> Self { - Self { - inner: DbInner::Redis(c), - hub, - } - } - - pub async fn info(&self) -> redis::RedisResult { - // String { - // let res = - match &self.inner { - DbInner::Memory(m) => memory_info(m).await, - DbInner::Redis(conn) => { - let mut c = conn.clone(); - redis_info(&mut c).await - } - } - // }; - // res.unwrap_or_else(|_| "error".to_string()) - } - - pub async fn list(&self, key: &str) -> redis::RedisResult> { - match &self.inner { - DbInner::Memory(m) => memory_list(m, key).await, - DbInner::Redis(conn) => { - let mut c = conn.clone(); - redis_list(&mut c, key).await - } - } - } - - pub async fn read(&self, key: &str) -> redis::RedisResult> { - match &self.inner { - DbInner::Memory(m) => memory_read(m, key).await, - DbInner::Redis(conn) => { - let mut c = conn.clone(); - redis_read(&mut c, key).await - } - } - } - - pub async fn save>( - &self, - key: &str, - value: V, - ttl: Option, - mode: Option, - ) -> redis::RedisResult<()> { - match &self.inner { - DbInner::Memory(m) => { - memory_save(m, key, value.as_ref(), ttl, mode).await?; - // Send events - let value_str = std::str::from_utf8(value.as_ref()) - .ok() - .map(|s| s.to_string()); - broadcast_event( - &self.hub, - RedisEvent { - message: RedisEventAction::Set, - key: key.to_string(), - }, - value_str, - ) - .await; - Ok(()) - } - DbInner::Redis(conn) => { - let mut c = conn.clone(); - redis_save(&mut c, key, value.as_ref(), ttl, mode).await - } - } - } - - pub async fn delete(&self, key: &str, mode: Option) -> redis::RedisResult { - match &self.inner { - DbInner::Memory(m) => { - let deleted = memory_delete(m, key, mode).await?; - if deleted { - broadcast_event( - &self.hub, - RedisEvent { - message: RedisEventAction::Del, - key: key.to_string(), - }, - None, - ) - .await; - } - Ok(deleted) - } - DbInner::Redis(conn) => { - let mut c = conn.clone(); - redis_delete(&mut c, key, mode).await - } - } - } -} diff --git a/foundations/hulypulse/src/handlers_http.rs b/foundations/hulypulse/src/handlers_http.rs deleted file mode 100644 index 4de72250e4..0000000000 --- a/foundations/hulypulse/src/handlers_http.rs +++ /dev/null @@ -1,267 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -use serde::Deserialize; -use std::str::FromStr; -use tracing::*; - -use actix_web::{ - Error, HttpRequest, HttpResponse, - error::ParseError, - http::header::{self, HeaderName, HeaderValue, IfMatch, IfNoneMatch, TryIntoHeaderValue}, - web, -}; - -use crate::{ - config::CONFIG, - db::Db, - redis::{SaveMode, Ttl}, - workspace_owner::test_rego_http, -}; - -pub fn map_redis_error(err: impl std::fmt::Display) -> Error { - let msg = err.to_string(); - - if let Some(detail) = msg.split(" - ExtensionError: ").nth(1) { - if let Some((code, text)) = detail.split_once(": ") { - let text = format!("{} {}", code, text); - return match code { - "400" => actix_web::error::ErrorBadRequest(text), - "404" => actix_web::error::ErrorNotFound(text), - "412" => actix_web::error::ErrorPreconditionFailed(text), - "500" => actix_web::error::ErrorInternalServerError(text), - _ => actix_web::error::ErrorInternalServerError("unexpected error"), - }; - } - } - actix_web::error::ErrorInternalServerError("internal error") -} - -#[derive(Deserialize, Debug)] -pub struct PathParams { - key: String, - workspace: String, -} - -pub struct TtlSecsHeader(Option); -pub struct TtlExpiresAtHeader(Option); - -/// list -pub async fn list( - req: HttpRequest, - path: web::Path, - db: web::Data, -) -> Result { - let params = path.into_inner(); - let key = format!("{}/{}", ¶ms.workspace, ¶ms.key); - trace!(key, "list request"); - - if !CONFIG.no_authorization && !test_rego_http(req, "List", &key) { - return Err(actix_web::error::ErrorForbidden("forbidden")); - } - - let entries = db.list(&key).await.map_err(map_redis_error)?; - Ok(HttpResponse::Ok().json(entries)) -} - -/// get -pub async fn get( - req: HttpRequest, - path: web::Path, - db: web::Data, -) -> Result { - let params = path.into_inner(); - let key = format!("{}/{}", ¶ms.workspace, ¶ms.key); - trace!(key, "get request"); - - if !CONFIG.no_authorization && !test_rego_http(req, "Get", &key) { - return Err(actix_web::error::ErrorForbidden("forbidden")); - } - - let entry_opt = db.read(&key).await.map_err(map_redis_error)?; - let resp = match entry_opt { - Some(entry) => HttpResponse::Ok() - .insert_header((header::ETAG, entry.etag.clone())) - .json(entry), - None => HttpResponse::NotFound().body("empty"), - }; - Ok(resp) -} - -/// put -pub async fn put( - req: HttpRequest, - path: web::Path, - body: web::Bytes, - db: web::Data, - (secs, expires_at): ( - Result, ParseError>, - Result, ParseError>, - ), - (if_match, if_none_match): ( - web::Header, - web::Header, - ), -) -> Result { - let params = path.into_inner(); - let key = format!("{}/{}", ¶ms.workspace, ¶ms.key); - trace!(key, "put request"); - - if !CONFIG.no_authorization && !test_rego_http(req, "Put", &key) { - return Err(actix_web::error::ErrorForbidden("forbidden")); - } - - // TTL logic - let ttl = match (secs?.into_inner().0, expires_at?.into_inner().0) { - (None, None) => None, - (Some(secs), None) => Some(Ttl::Sec(secs)), - (None, Some(timestamp)) => Some(Ttl::At(timestamp)), - _ => { - return Err(actix_web::error::ErrorBadRequest("Multiple ttl specified")); - } - }; - - // MODE logic - let mode = match (if_match.into_inner(), if_none_match.into_inner()) { - (IfMatch::Items(items), IfNoneMatch::Items(nitems)) - if items.is_empty() && nitems.is_empty() => - { - SaveMode::Upsert - } - (IfMatch::Any, IfNoneMatch::Items(nitems)) if nitems.is_empty() => SaveMode::Update, - (IfMatch::Items(etags), IfNoneMatch::Items(nitems)) - if etags.len() == 1 && nitems.is_empty() => - { - SaveMode::Equal(etags[0].tag().to_string()) - } - (IfMatch::Items(items), IfNoneMatch::Any) if items.is_empty() => SaveMode::Insert, - _ => { - return Err(actix_web::error::ErrorBadRequest( - "Unsupported combination of If-Match and If-None-Match", - )); - } - }; - - db.save(&key, &body[..], ttl, Some(mode)) - .await - .map_err(map_redis_error)?; - Ok(HttpResponse::Ok().body("DONE")) -} - -/// delete -pub async fn delete( - req: HttpRequest, - path: web::Path, - db: web::Data, - if_match: web::Header, -) -> Result { - let params = path.into_inner(); - let key = format!("{}/{}", ¶ms.workspace, ¶ms.key); - trace!(key, "delete request"); - - if !CONFIG.no_authorization && !test_rego_http(req, "Delete", &key) { - return Err(actix_web::error::ErrorForbidden("forbidden")); - } - - // MODE logic - let mode = match if_match.into_inner() { - IfMatch::Any => SaveMode::Update, - IfMatch::Items(etags) => { - if etags.len() == 1 { - SaveMode::Equal(etags[0].tag().to_string()) - } else if etags.is_empty() { - SaveMode::Upsert - } else { - return Err(actix_web::error::ErrorBadRequest( - "Multiple If-Match are not supported", - )); - } - } - }; - - let deleted = db.delete(&key, Some(mode)).await.map_err(map_redis_error)?; - let response = match deleted { - true => HttpResponse::NoContent().finish(), - false => HttpResponse::NotFound().body("not found"), - }; - - Ok(response) -} - -impl TryIntoHeaderValue for TtlSecsHeader { - type Error = std::convert::Infallible; - - fn try_into_value(self) -> Result { - Ok(self - .0 - .map(HeaderValue::from) - .unwrap_or(HeaderValue::from_static(""))) - } -} - -impl header::Header for TtlSecsHeader { - fn name() -> HeaderName { - HeaderName::from_static("huly-ttl") - } - - fn parse(msg: &M) -> Result { - let mut values = msg.headers().get_all(Self::name()); - let val = if let Some(value) = values.next() { - Some( - usize::from_str(value.to_str().map_err(|_| ParseError::Header)?.trim()) - .map_err(|_| ParseError::Header)?, - ) - } else { - None - }; - if values.next().is_some() { - return Err(ParseError::TooLarge); - } - Ok(Self(val)) - } -} - -impl TryIntoHeaderValue for TtlExpiresAtHeader { - type Error = std::convert::Infallible; - - fn try_into_value(self) -> Result { - Ok(self - .0 - .map(HeaderValue::from) - .unwrap_or(HeaderValue::from_static(""))) - } -} - -impl header::Header for TtlExpiresAtHeader { - fn name() -> HeaderName { - HeaderName::from_static("huly-expire-at") - } - - fn parse(msg: &M) -> Result { - let mut values = msg.headers().get_all(Self::name()); - let val = if let Some(value) = values.next() { - Some( - u64::from_str(value.to_str().map_err(|_| ParseError::Header)?.trim()) - .map_err(|_| ParseError::Header)?, - ) - } else { - None - }; - if values.next().is_some() { - return Err(ParseError::TooLarge); - } - Ok(Self(val)) - } -} diff --git a/foundations/hulypulse/src/handlers_ws.rs b/foundations/hulypulse/src/handlers_ws.rs deleted file mode 100644 index 5aa291c93c..0000000000 --- a/foundations/hulypulse/src/handlers_ws.rs +++ /dev/null @@ -1,392 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -use actix_ws; -use futures_util::StreamExt; -// use tracing::info; -use actix_web::{Error, HttpMessage, HttpRequest, HttpResponse, web}; -use serde::Deserialize; -use serde_json::{Value, json}; -use std::sync::Arc; -use tokio::sync::RwLock; - -use crate::{ - config::CONFIG, - db::Db, - hub_service::{HubState, SessionId, new_session_id}, - redis::{SaveMode, Ttl}, - workspace_owner::check_workspace_core, - workspace_owner::test_rego_claims, -}; - -use strum_macros::AsRefStr; - -#[derive(Deserialize, Debug, AsRefStr)] -#[serde(rename_all = "lowercase", tag = "type")] -pub enum WsCommand { - Put { - correlation: String, - key: String, - data: String, - - #[serde(rename = "expiresAt")] - #[serde(default)] - expires_at: Option, - - #[serde(rename = "TTL")] - #[serde(default)] - ttl: Option, - - #[serde(rename = "ifMatch")] - #[serde(default)] - if_match: Option, - - #[serde(rename = "ifNoneMatch")] - #[serde(default)] - if_none_match: Option, - }, - - Delete { - correlation: String, - key: String, - - #[serde(rename = "ifMatch")] - #[serde(default)] - if_match: Option, - }, - - Get { - correlation: String, - key: String, - }, - - List { - correlation: String, - key: String, - }, - - Sub { - correlation: String, - key: String, - }, - - Unsub { - correlation: String, - key: String, - }, - - Sublist { - correlation: String, - }, - - Info { - correlation: String, - }, -} - -use hulyrs::services::jwt::Claims; - -async fn wrong_workspace( - claims: &Option, - key: &str, - correlation: &String, - session: &mut actix_ws::Session, -) -> bool { - if let Err(e) = check_workspace_core(claims.clone(), key) { - result_err(e, correlation, session).await; - return true; - } - false -} - -async fn result>( - result: T, - correlation: &String, - session: &mut actix_ws::Session, -) { - let _ = session - .text(json!({ "correlation": correlation, "result": result.into()}).to_string()) - .await; -} - -async fn result_err(err: impl Into, correlation: &String, session: &mut actix_ws::Session) { - let _ = session - .text(json!({ "correlation": correlation, "error": err.into()}).to_string()) - .await; -} - -async fn handle_command( - ws: &mut actix_ws::Session, - cmd: WsCommand, - db: &Db, - hub_state: &Arc>, - claims: Option, - session_id: SessionId, -) { - match cmd { - // INFO - WsCommand::Info { correlation } => { - tracing::info!("INFO"); - match db.info().await { - Ok(info) => result(info, &correlation, ws).await, - Err(e) => result_err(e.to_string(), &correlation, ws).await, - } - } - - // PUT - WsCommand::Put { - key, - data, - expires_at, - ttl, - if_match, - if_none_match, - correlation, - } => { - tracing::info!("PUT {} = {}", &key, &data); - if wrong_workspace(&claims, &key, &correlation, ws).await { - return; - } - - // TTL logic - let real_ttl = if let Some(secs) = ttl { - Some(Ttl::Sec(secs as usize)) - } else if let Some(timestamp) = expires_at { - Some(Ttl::At(timestamp)) - } else { - None - }; - - // SaveMode logic - let mut mode = Some(SaveMode::Upsert); - if let Some(s) = if_match { - if s == "*" { - mode = Some(SaveMode::Update); - } else { - mode = Some(SaveMode::Equal(s)); - } - } else if let Some(s) = if_none_match { - if s == "*" { - mode = Some(SaveMode::Insert); - } else { - result_err("ifNoneMatch must contain only '*'", &correlation, ws).await; - return; - } - } - - match db.save(&key, &data, real_ttl, mode).await { - Ok(_) => result("OK", &correlation, ws).await, - Err(e) => result_err(e.to_string(), &correlation, ws).await, - } - } - - WsCommand::Delete { - key, - correlation, - if_match, - } => { - tracing::info!("DELETE {}", &key); // correlation:{:?} , &correlation - if wrong_workspace(&claims, &key, &correlation, ws).await { - return; - } - - // MODE logic - let mut mode = Some(SaveMode::Upsert); - if let Some(s) = if_match { - if s == "*" { - // `If-Match: *` - return error if not exist - mode = Some(SaveMode::Update); - } else { - // `If-Match: ` - delete only if current - mode = Some(SaveMode::Equal(s)); - } - } - - // Delete - match db.delete(&key, mode).await { - Ok(true) => result("OK", &correlation, ws).await, - Ok(false) => result_err("not found", &correlation, ws).await, - Err(e) => result_err(e.to_string(), &correlation, ws).await, - } - } - - WsCommand::Get { key, correlation } => { - tracing::info!("GET {}", &key); - if wrong_workspace(&claims, &key, &correlation, ws).await { - return; - } - - match db.read(&key).await { - Ok(Some(data)) => match serde_json::to_value(&data) { - Ok(v) => result(v, &correlation, ws).await, - Err(e) => result_err(e.to_string(), &correlation, ws).await, - }, - Ok(None) => result_err("not found", &correlation, ws).await, - Err(e) => result_err(e.to_string(), &correlation, ws).await, - } - } - - WsCommand::List { key, correlation } => { - tracing::info!("LIST {:?}", &key); - if wrong_workspace(&claims, &key, &correlation, ws).await { - return; - } - match db.list(&key).await { - Ok(data) => { - let values: Vec = data.into_iter().map(|item| json!(item)).collect(); - result(values, &correlation, ws).await; - } - Err(e) => result_err(e.to_string(), &correlation, ws).await, - } - } - - WsCommand::Sub { key, correlation } => { - tracing::info!("SUB {}", &key); - if wrong_workspace(&claims, &key, &correlation, ws).await { - return; - } - hub_state.write().await.subscribe(session_id, key); - result("OK", &correlation, ws).await; - } - - WsCommand::Unsub { key, correlation } => { - tracing::info!("UNSUB {}", &key); - if key == "*" { - hub_state.write().await.unsubscribe_all(session_id); - result("OK", &correlation, ws).await; - } else { - if wrong_workspace(&claims, &key, &correlation, ws).await { - return; - } - hub_state.write().await.unsubscribe(session_id, key); - result("OK", &correlation, ws).await; - } - } - - WsCommand::Sublist { correlation } => { - tracing::info!("SUBLIST"); - // w/o Check workspace! - let keys = hub_state.read().await.subscribe_list(session_id); - result(keys, &correlation, ws).await; - } // End of commands - } -} -// } - -pub async fn handler( - req: HttpRequest, - payload: web::Payload, - db: web::Data, - hub_state: web::Data>>, -) -> Result { - let claims = if !CONFIG.no_authorization { - Some( - req.extensions() - .get::() - .expect("Missing claims") - .to_owned(), - ) - } else { - None - }; - - let (response, mut session, mut msg_stream) = actix_ws::handle(&req, payload)?; - - let session_id = new_session_id(); - - hub_state.write().await.connect(session_id, session.clone()); - tracing::info!("WebSocket connected: {}", session_id); - - actix_web::rt::spawn(async move { - while let Some(Ok(msg)) = msg_stream.next().await { - tracing::debug!("WebSocket message: {:?}", msg); - - // renew heartbeat to unixtime (all messages is activity, including "ping") - hub_state.write().await.renew_heartbeat(session_id); - - match msg { - actix_ws::Message::Ping(bytes) => { - session.pong(&bytes).await.ok(); - continue; - } - - actix_ws::Message::Pong(_) => { - continue; - } - - actix_ws::Message::Text(text) if text == "ping" => { - let _ = session.text("pong").await; - continue; - } - actix_ws::Message::Text(text) if text == "pong" => { - continue; - } - - actix_ws::Message::Text(text) => match serde_json::from_str::(&text) { - Ok(cmd) => { - if !CONFIG.no_authorization { - let key = match &cmd { - WsCommand::Put { key, .. } - | WsCommand::Delete { key, .. } - | WsCommand::Get { key, .. } - | WsCommand::List { key, .. } - | WsCommand::Sub { key, .. } - | WsCommand::Unsub { key, .. } => key.as_str(), - _ => "", - }; - - if let Some(ref claim) = claims { - if !test_rego_claims(claim, cmd.as_ref(), key) { - let _ = session.text("Unauthorized: Rego policy").await; - break; - } - } - } - - handle_command( - &mut session, - cmd, - &db, - &hub_state, - claims.clone(), - session_id, - ) - .await; - } - - Err(err) => { - let _ = session.text(format!("Invalid JSON: {}", err)).await; - } - }, - - actix_ws::Message::Close(reason) => { - if let Err(e) = session.close(reason).await { - tracing::warn!("WS close error: {:?}", e); - } - break; - } - - _ => { - tracing::info!("Unhandled WS message: {:?}", msg); - } - } - } - - hub_state.write().await.disconnect(session_id); - tracing::info!("WebSocket disconnected: {}", session_id); - }); - - Ok(response) -} diff --git a/foundations/hulypulse/src/hub_service.rs b/foundations/hulypulse/src/hub_service.rs deleted file mode 100644 index 38082a8566..0000000000 --- a/foundations/hulypulse/src/hub_service.rs +++ /dev/null @@ -1,250 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -use crate::config::CONFIG; -use redis::aio::MultiplexedConnection; -use serde::Serialize; -use std::collections::{HashMap, HashSet}; -use std::sync::Arc; -use std::sync::atomic::{AtomicU64, Ordering}; -use tokio::sync::RwLock; - -fn subscription_matches(sub_key: &str, key: &str) -> bool { - if sub_key == key { - return true; - } - if sub_key.ends_with('/') && key.starts_with(sub_key) { - let rest = &key[sub_key.len()..]; - return !rest.contains('$'); - } - false -} - -#[derive(Clone, Serialize, Debug)] -pub struct ServerMessage { - #[serde(flatten)] - pub event: RedisEvent, - #[serde(skip_serializing_if = "Option::is_none")] - pub value: Option, -} - -pub type SessionId = u64; -static NEXT_ID: AtomicU64 = AtomicU64::new(1); -pub fn new_session_id() -> SessionId { - NEXT_ID.fetch_add(1, Ordering::SeqCst) -} - -#[derive(Debug, Clone, Serialize)] -pub enum RedisEventAction { - Set, - Del, - Unlink, - Expired, - Other(String), -} - -#[derive(Debug, Clone, Serialize)] -pub struct RedisEvent { - // pub db: u32, - pub message: RedisEventAction, - pub key: String, -} - -#[derive(Default)] // Debug, -pub struct HubState { - sessions: HashMap, - subs: HashMap>, - heartbeats: HashMap, - serverping: HashMap, -} - -impl HubState { - pub fn renew_heartbeat(&mut self, session_id: SessionId) { - if self.sessions.contains_key(&session_id) { - let now = std::time::Instant::now(); - self.heartbeats.insert(session_id, now); - self.serverping.insert(session_id, now); - } - } - - pub fn connect(&mut self, session_id: SessionId, session: actix_ws::Session) { - self.sessions.insert(session_id, session); - self.heartbeats - .insert(session_id, std::time::Instant::now()); - self.serverping - .insert(session_id, std::time::Instant::now()); - } - - pub fn disconnect(&mut self, session_id: SessionId) { - self.sessions.remove(&session_id); - self.heartbeats.remove(&session_id); - self.serverping.remove(&session_id); - self.subs.retain(|_, ids| { - ids.remove(&session_id); - !ids.is_empty() - }); - } - - pub fn subscribe(&mut self, session_id: SessionId, key: String) { - self.subs.entry(key).or_default().insert(session_id); - } - - pub fn unsubscribe(&mut self, session_id: SessionId, key: String) { - if let Some(set) = self.subs.get_mut(&key) { - set.remove(&session_id); - if set.is_empty() { - self.subs.remove(&key); - } - } - } - pub fn unsubscribe_all(&mut self, session_id: SessionId) { - self.subs.retain(|_, ids| { - ids.remove(&session_id); - !ids.is_empty() - }); - } - - pub fn subscribe_list(&self, session_id: SessionId) -> Vec { - self.subs - .iter() - .filter_map(|(key, ids)| { - if ids.contains(&session_id) { - Some(key.clone()) - } else { - None - } - }) - .collect() - } - - pub fn count(&self) -> usize { - self.sessions.len() - } - - pub fn recipients_for_key(&self, key: &str) -> Vec { - let mut out = Vec::new(); - for (sub_key, set) in &self.subs { - if subscription_matches(sub_key, key) { - for sid in set { - if let Some(r) = self.sessions.get(sid) { - out.push(r.clone()); - } - } - } - } - out - } -} - -// Send messages about new db events -pub async fn broadcast_event( - hub_state: &Arc>, - ev: RedisEvent, - value: Option, -) { - // Collect - let recipients: Vec = { hub_state.read().await.recipients_for_key(&ev.key) }; - if recipients.is_empty() { - return; - } - - // Send - let payload = ServerMessage { event: ev, value }; - for mut rcpt in recipients { - // let _ = rcpt.do_send(payload.clone()); - let json = serde_json::to_string(&payload).unwrap(); - let _ = rcpt.text(json).await; - } -} - -pub async fn push_event( - hub_state: &Arc>, - redis: &mut MultiplexedConnection, - ev: RedisEvent, -) { - // Value only for Set - let mut value: Option = None; - if matches!(ev.message, RedisEventAction::Set) { - match ::redis::cmd("GET") - .arg(&ev.key) - .query_async::>(redis) - .await - { - Ok(v) => value = v, - Err(e) => tracing::warn!("redis GET {} failed: {}", &ev.key, e), - } - } - - broadcast_event(hub_state, ev, value).await; -} - -pub fn check_heartbeat(hub_state: Arc>) { - tokio::spawn(async move { - let mut ticker = tokio::time::interval(std::time::Duration::from_secs(2)); - loop { - ticker.tick().await; - - let now = std::time::Instant::now(); - let timelimit = now - std::time::Duration::from_secs(CONFIG.heartbeat_timeout); - let pinglimit = now - std::time::Duration::from_secs(CONFIG.ping_timeout); - - let hub = hub_state.read().await; - - let expired: Vec = hub - .heartbeats - .iter() - .filter_map(|(&sid, &last_beat)| { - if last_beat < timelimit { - hub.sessions.get(&sid).cloned() - } else { - None - } - }) - .collect(); - - let to_ping: Vec = hub - .serverping - .iter() - .filter_map(|(&sid, &last_ping)| { - if last_ping < pinglimit { - Some(sid) - } else { - None - } - }) - .collect(); - - drop(hub); - - if !expired.is_empty() { - for addr in &expired { - // addr.do_send(crate::handlers_ws::ForceDisconnect); - let _ = addr.clone().close(None).await; - } - } - - if !to_ping.is_empty() { - let mut hub = hub_state.write().await; - for sid in &to_ping { - if let Some(session) = hub.sessions.get_mut(sid) { - let _ = session.ping(&[]).await; - } - hub.serverping.insert(*sid, now); - } - drop(hub); - } - } - }); -} diff --git a/foundations/hulypulse/src/main.rs b/foundations/hulypulse/src/main.rs deleted file mode 100644 index 203c6280c1..0000000000 --- a/foundations/hulypulse/src/main.rs +++ /dev/null @@ -1,204 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -use actix_cors::Cors; -use actix_web::{ - App, Error, HttpMessage, HttpResponse, HttpServer, - body::MessageBody, - dev::{ServiceRequest, ServiceResponse}, - middleware::{self, Next}, - web::{self, Path, Query}, -}; - -use hulyrs::services::jwt::{Claims, actix::ServiceRequestExt}; -use secrecy::ExposeSecret; -use serde_json::json; -use tracing::*; -use uuid::Uuid; - -mod config; -mod handlers_http; -mod handlers_ws; -mod redis; -mod workspace_owner; - -mod hub_service; -use hub_service::HubState; - -use config::CONFIG; - -mod db; -mod memory; - -use crate::memory::MemoryBackend; -use crate::{db::Db, hub_service::check_heartbeat}; - -fn initialize_tracing(level: tracing::Level) { - use tracing_subscriber::{filter::targets::Targets, prelude::*}; - - let filter = Targets::default() - .with_target(env!("CARGO_BIN_NAME"), level) - .with_target("actix", tracing::Level::WARN); - let format = tracing_subscriber::fmt::layer().compact(); - - tracing_subscriber::registry() - .with(filter) - .with(format) - .init(); -} - -async fn extract_claims( - mut request: ServiceRequest, - next: Next, -) -> Result, Error> { - #[derive(serde::Deserialize)] - struct QueryString { - token: Option, - } - - if !CONFIG.no_authorization { - let query = request.extract::>().await?.into_inner(); - - let claims = if let Some(token) = query.token { - Claims::from_token(token, CONFIG.token_secret.expose_secret()).unwrap() - } else { - request.extract_claims(&CONFIG.token_secret)? - }; - request.extensions_mut().insert(claims); - } - - next.call(request).await -} - -async fn check_workspace( - mut request: ServiceRequest, - next: Next, -) -> Result, Error> { - if !CONFIG.no_authorization { - let workspace = Uuid::parse_str(&request.extract::>().await?); - let claims = request.extensions().get::().cloned().unwrap(); - - if claims.is_system() || Ok(claims.workspace.clone()) == workspace.clone().map(Some) { - next.call(request).await - } else { - warn!( - expected = ?claims.workspace, - actual = ?workspace, - "Unauthorized request, workspace mismatch" - ); - Err(actix_web::error::ErrorUnauthorized("Unauthorized").into()) - } - } else { - next.call(request).await - } -} - -#[actix_web::main] -async fn main() -> anyhow::Result<()> { - initialize_tracing(tracing::Level::TRACE); - - tracing::info!("{}/{}", env!("CARGO_BIN_NAME"), env!("CARGO_PKG_VERSION")); - - // starting HubService - let hub_state = Arc::new(RwLock::new(HubState::default())); - - // starting heartbeat checker - check_heartbeat(hub_state.clone()); - - let db_backend = match CONFIG.backend { - config::BackendType::Memory => { - let memory = MemoryBackend::new(); - memory.spawn_ticker(hub_state.clone()); - tracing::info!("Memory mode enabled"); - Db::new_memory(memory, hub_state.clone()) - } - - config::BackendType::Redis => { - let redis_client = redis::client().await?; - let redis_connection = redis_client.get_multiplexed_async_connection().await?; - tokio::spawn(crate::redis::receiver(redis_client, hub_state.clone())); - tracing::info!("Redis mode enabled"); - Db::new_redis(redis_connection, hub_state.clone()) - } - }; - - let socket = std::net::SocketAddr::new(CONFIG.bind_host.as_str().parse()?, CONFIG.bind_port); - - let url = format!("http://{}:{}", &CONFIG.bind_host, &CONFIG.bind_port); - tracing::info!("Server running at {}", &url); - tracing::info!("HTTP API: {}/api", &url); - tracing::info!("WebSocket API: {}/ws", &url); - tracing::info!("Status: {}/status", &url); - - use std::sync::Arc; - use tokio::sync::RwLock; - - let server = HttpServer::new(move || { - let cors = Cors::default() - .allow_any_origin() - .allow_any_method() - .allow_any_header() - .supports_credentials() - .max_age(3600); - - App::new() - .app_data(web::Data::new(db_backend.clone())) - .app_data(web::Data::new(hub_state.clone())) - .wrap(middleware::Logger::default()) - .wrap(cors) - .service( - web::scope("/api/{workspace}") - .wrap(middleware::from_fn(check_workspace)) - .wrap(middleware::from_fn(extract_claims)) - .route("/{key:.+/}", web::get().to(handlers_http::list)) - .route("/{key:.+}", web::get().to(handlers_http::get)) - .route("/{key:.+}", web::put().to(handlers_http::put)) - .route("/{key:.+}", web::delete().to(handlers_http::delete)), - ) - .route( - "/ws", - web::get() - .to(handlers_ws::handler) - .wrap(middleware::from_fn(extract_claims)), - ) // WebSocket - .route( - "/status", - web::get().to({ - move |hub_state: web::Data>>, db_backend: web::Data| { - let hub_state = hub_state.clone(); - async move { - let info = db_backend - .info() - .await - .unwrap_or_else(|_| "error".to_string()); - let count = hub_state.read().await.count(); - Ok::<_, actix_web::Error>(HttpResponse::Ok().json(json!({ - "memory_info": info, - "backend": CONFIG.backend.to_string().to_lowercase(), - "websockets": count, - "status": "OK", - }))) - } - } - }), - ) - }) - .bind(socket)? - .run(); - - server.await?; - - Ok(()) -} diff --git a/foundations/hulypulse/src/memory.rs b/foundations/hulypulse/src/memory.rs deleted file mode 100644 index 4774f36980..0000000000 --- a/foundations/hulypulse/src/memory.rs +++ /dev/null @@ -1,344 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -use crate::{ - config::CONFIG, - hub_service::{HubState, RedisEvent, RedisEventAction, broadcast_event}, - redis::{RedisArray, SaveMode, Ttl, deprecated_symbol_error, error}, -}; -use std::{ - collections::HashMap, - sync::Arc, - time::{SystemTime, UNIX_EPOCH}, -}; -use tokio::{ - sync::RwLock, - time::{self, Duration}, -}; - -#[derive(Debug, Clone)] -struct Entry { - data: String, - ttl: u8, -} - -#[derive(Clone, Default)] -pub struct MemoryBackend { - inner: Arc>>, - tick: Arc>, // counter -} - -impl MemoryBackend { - pub fn new() -> Self { - Self { - inner: Arc::new(RwLock::new(HashMap::new())), - tick: Arc::new(RwLock::new(0)), - } - } - - pub fn spawn_ticker(&self, hub_state: Arc>) { - let inner = self.inner.clone(); - let tick = self.tick.clone(); - - tokio::spawn(async move { - let mut ticker = time::interval(Duration::from_secs(1)); - loop { - ticker.tick().await; - - let current_tick = { - let mut t = tick.write().await; - *t = t.wrapping_add(1); - *t - }; - - let expired_keys: Vec = { - let map = inner.read().await; - map.iter() - .filter(|(_, v)| v.ttl == current_tick) - .map(|(k, _)| k.clone()) - .collect() - }; - - { - let mut map = inner.write().await; - for k in &expired_keys { - map.remove(k); - } - } - - for k in expired_keys { - broadcast_event( - &hub_state, - RedisEvent { - message: RedisEventAction::Expired, - key: k, - }, - None, - ) - .await; - } - } - }); - } -} - -/// memory_list(&backend, "prefix/") → Vec -pub async fn memory_list( - backend: &MemoryBackend, - key_prefix: &str, -) -> redis::RedisResult> { - deprecated_symbol_error(key_prefix)?; - if !key_prefix.ends_with('/') { - return error(412, "Key must end with slash"); - } - - let map = backend.inner.read().await; - let current_tick = *backend.tick.read().await; - - let mut results = Vec::new(); - for (k, v) in map.iter() { - if !k.starts_with(key_prefix) { - continue; - } - - if k.strip_prefix(key_prefix) - .map_or(false, |s| s.contains('$')) - { - continue; - } - - if v.ttl == 0 { - continue; - } - - let expires = v.ttl.wrapping_sub(current_tick); - - results.push(RedisArray { - key: k.clone(), - data: v.data.clone(), - ttl: expires as u64, - etag: hex::encode(md5::compute(&v.data).0), - }); - } - - Ok(results) -} - -/// memory_info(&backend) -pub async fn memory_info(backend: &MemoryBackend) -> redis::RedisResult { - let map = backend.inner.read().await; - let keys = map.len(); - let memory: usize = map.values().map(|v| v.data.len()).sum(); - Ok(format!("{} keys, {} bytes", keys, memory)) -} - -/// memory_read(&backend, "key") -pub async fn memory_read( - backend: &MemoryBackend, - key: &str, -) -> redis::RedisResult> { - deprecated_symbol_error(key)?; - if key.ends_with('/') { - return error(412, "Key must not end with a slash"); - } - - let map = backend.inner.read().await; - - match map.get(key) { - None => Ok(None), - Some(entry) => { - let data = entry.data.clone(); - let current_tick = *backend.tick.read().await; - let expires = entry.ttl.wrapping_sub(current_tick); - - Ok(Some(RedisArray { - key: key.to_string(), - data: data.clone(), - ttl: expires as u64, - etag: hex::encode(md5::compute(&data).0), - })) - } - } -} - -/// TTL in sec -fn compute_ttl_u8(ttl: Option) -> redis::RedisResult { - let sec_usize = match ttl { - Some(Ttl::Sec(secs)) => secs, - Some(Ttl::At(timestamp)) => { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - if timestamp <= now { - return error(400, "TTL timestamp exceeds MAX_TTL limit"); - } - (timestamp - now) as usize - } - None => CONFIG.max_ttl, - }; - - if sec_usize == 0 { - return error(400, "TTL must be > 0"); - } - - if sec_usize > CONFIG.max_ttl || sec_usize > 255 { - return error(412, "TTL exceeds MAX_TTL or 255 sec"); - } - - let capped = sec_usize.min(u8::MAX as usize); - Ok(capped as u8) -} - -/// memory_save(&backend, "key", value, ttl, mode) -pub async fn memory_save>( - backend: &MemoryBackend, - key: &str, - bytes_value: V, - ttl: Option, - mode: Option, -) -> redis::RedisResult<()> { - // u8 - String - let value = match std::str::from_utf8(bytes_value.as_ref()) { - Ok(s) => s.to_string(), - Err(_) => return error(400, "Value must be valid UTF-8"), - }; - - // If max_size != 0 and value size > max_size, return error - let max_size = CONFIG.max_size.unwrap_or(0); - if max_size != 0 && value.len() > max_size { - return error( - 400, - format!("Value in memory mode must be less than {} bytes", max_size), - ); - } - - deprecated_symbol_error(key)?; - if key.ends_with('/') { - return error(412, "Key must not end with a slash"); - } - - let ttl_u8 = compute_ttl_u8(ttl)?; - let current_tick = *backend.tick.read().await; - let expire_tick = current_tick.wrapping_add(ttl_u8); - - let val = value.to_string(); - - let mut map = backend.inner.write().await; - - let mode = mode.unwrap_or(SaveMode::Upsert); - - match mode { - SaveMode::Upsert => { - map.insert( - key.to_string(), - Entry { - data: val, - ttl: expire_tick, - }, - ); - } - SaveMode::Insert => { - if map.contains_key(key) { - return error(412, "Insert: key already exists"); - } - map.insert( - key.to_string(), - Entry { - data: val, - ttl: expire_tick, - }, - ); - } - SaveMode::Update => { - let Some(existing) = map.get_mut(key) else { - return error(404, "Update: key does not exist"); - }; - *existing = Entry { - data: val, - ttl: expire_tick, - }; - } - SaveMode::Equal(ref expected_md5) => { - let Some(existing) = map.get_mut(key) else { - return error(404, "Equal: key does not exist"); - }; - let actual_md5 = hex::encode(md5::compute(&existing.data).0); - if &actual_md5 != expected_md5 { - return error( - 412, - format!( - "md5 mismatch, current: {}, expected: {}", - actual_md5, expected_md5 - ), - ); - } - *existing = Entry { - data: val, - ttl: expire_tick, - }; - } - } - - Ok(()) -} - -/// memory_delete(&backend, "key", mode) -pub async fn memory_delete( - backend: &MemoryBackend, - key: &str, - mode: Option, -) -> redis::RedisResult { - deprecated_symbol_error(key)?; - if key.ends_with('/') { - return error(412, "Key must not end with a slash"); - } - - let mut map = backend.inner.write().await; - let mode = mode.unwrap_or(SaveMode::Upsert); - - match mode { - SaveMode::Insert => { - return error(412, "Insert mode is not supported for delete"); - } - SaveMode::Update | SaveMode::Upsert => { - let existed = map.remove(key).is_some(); - Ok(existed) - } - SaveMode::Equal(ref expected_md5) => { - match map.get(key) { - None => return error(404, "Equal: key does not exist"), - Some(existing) => { - let actual_md5 = hex::encode(md5::compute(&existing.data).0); - if &actual_md5 != expected_md5 { - return error( - 412, - format!( - "md5 mismatch, current: {}, expected: {}", - actual_md5, expected_md5 - ), - ); - } - } - } - let existed = map.remove(key).is_some(); - if !existed { - // WHF?! - return error(404, "Delete: key does not exist"); - } - Ok(true) - } - } -} diff --git a/foundations/hulypulse/src/pulse-status.sh b/foundations/hulypulse/src/pulse-status.sh deleted file mode 100755 index cadc091b2d..0000000000 --- a/foundations/hulypulse/src/pulse-status.sh +++ /dev/null @@ -1 +0,0 @@ -curl -i https://pulse.hc.engineering/status \ No newline at end of file diff --git a/foundations/hulypulse/src/redis.rs b/foundations/hulypulse/src/redis.rs deleted file mode 100644 index cb230e247e..0000000000 --- a/foundations/hulypulse/src/redis.rs +++ /dev/null @@ -1,533 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -use std::{ - sync::Arc, - time::{SystemTime, UNIX_EPOCH}, -}; - -use ::redis::Msg; -use tokio::sync::RwLock; -use tokio_stream::StreamExt; -use tracing::*; - -use crate::{ - config::{CONFIG, RedisMode}, - hub_service::{HubState, RedisEvent, RedisEventAction, push_event}, -}; - -#[derive(serde::Serialize)] -pub enum Ttl { - Sec(usize), // EX - At(u64), // EXAT (timestamp in seconds) -} - -#[derive(Debug)] -pub enum SaveMode { - Upsert, // default: set or overwrite - Insert, // only if not exists (NX) - Update, // only if exists (XX) - Equal(String), // only if md5 matches provided -} - -use redis::{ - Client, ConnectionInfo, ProtocolVersion, RedisConnectionInfo, RedisResult, ToRedisArgs, - aio::MultiplexedConnection, -}; -use serde::Serialize; - -static MAX_LOOP_COUNT: usize = 1000; // to avoid infinite loops - -#[derive(Debug, Serialize)] -pub struct RedisArray { - pub key: String, - pub data: String, - pub ttl: u64, // sec to expire TTL - pub etag: String, // md5 hash (data) -} - -/// return Error -pub fn error(code: u16, msg: impl Into) -> redis::RedisResult { - let msg = msg.into(); - let full = format!("{}: {}", code, msg); - Err(redis::RedisError::from(( - redis::ErrorKind::ExtensionError, - "", - full, - ))) -} - -/// Check for redis-deprecated symbols -pub fn deprecated_symbol(s: &str) -> bool { - s.chars().any(|c| { - matches!( - c, - '*' | '?' | '[' | ']' | '\\' | '\0'..='\x1F' | '\x7F' | '"' | '\'' - ) - }) -} - -pub fn deprecated_symbol_error(s: &str) -> redis::RedisResult<()> { - if deprecated_symbol(s) { - error(412, "Deprecated symbol in key") - } else { - Ok(()) - } -} - -/// redis_info(&connection) -pub async fn redis_info(conn: &mut MultiplexedConnection) -> redis::RedisResult { - let info: String = redis::cmd("INFO").query_async(conn).await?; - - let mut redis_keys: Option = None; - let mut redis_bytes: Option = None; - - for line in info.lines() { - if line.starts_with("db0:") { - // parsing: db0:keys=152,expires=10,avg_ttl=456789 - if let Some(keys_part) = line.split(',').find(|s| s.starts_with("keys=")) { - if let Some(val) = keys_part.strip_prefix("keys=") { - redis_keys = val.parse::().ok(); - } - } - } - if line.starts_with("used_memory:") { - if let Some(val) = line.strip_prefix("used_memory:") { - redis_bytes = val.parse::().ok(); - } - } - } - - Ok(format!( - "{} keys, {} bytes", - redis_keys.unwrap_or(0), - redis_bytes.unwrap_or(0) - )) -} - -/// redis_list(&connection,prefix) -pub async fn redis_list( - conn: &mut MultiplexedConnection, - key: &str, -) -> redis::RedisResult> { - deprecated_symbol_error(key)?; - if !key.ends_with('/') { - return error(412, "Key must end with slash"); - } - let pattern = format!("{key}*"); - - let mut cursor = 0u64; - let mut results = Vec::new(); - - loop { - let mut cmd = redis::cmd("SCAN"); - cmd.arg(cursor); - cmd.arg("MATCH").arg(&pattern); - // cmd.arg("COUNT").arg(100); // Optionally adjust batch size - - let (next_cursor, keys): (u64, Vec) = cmd.query_async(conn).await?; - - for k in keys { - // Check for $-security path - if k.strip_prefix(key).map_or(false, |s| s.contains('$')) { - continue; - } - - // Get value - let value: Option = redis::cmd("GET").arg(&k).query_async(conn).await?; - let Some(value) = value else { - continue; - }; // Old and deleted - - // Get TTL - let ttl: i64 = redis::cmd("TTL").arg(&k).query_async(conn).await?; - if ttl >= 0 { - results.push(RedisArray { - key: k, - data: value.clone(), - ttl: ttl as u64, - etag: hex::encode(md5::compute(&value).0), - }); - } - } - - if next_cursor == 0 { - break; - } - cursor = next_cursor; - } - - Ok(results) -} - -/// redis_read(&connection,key) -pub async fn redis_read( - conn: &mut MultiplexedConnection, - key: &str, -) -> redis::RedisResult> { - deprecated_symbol_error(key)?; - - if key.ends_with('/') { - return error(412, "Key must not end with a slash"); - } - - let data: Option = redis::cmd("GET").arg(key).query_async(conn).await?; - - let Some(data) = data else { - return Ok(None); - }; - - let ttl: i64 = redis::cmd("TTL").arg(key).query_async(conn).await?; - match ttl { - -1 => return error(500, "TTL not set"), - -2 => return error(500, "Key not found"), - x if x < 0 => return error(500, "Unknown TTL error"), - _ => {} // ttl >= 0, ок - } - - Ok(Some(RedisArray { - key: key.to_string(), - data: data.clone(), - ttl: ttl as u64, - etag: hex::encode(md5::compute(&data).0), - })) -} - -/// redis_save(&connection,key,value,[ttl?],[mode?]) -pub async fn redis_save( - conn: &mut MultiplexedConnection, - key: &str, - value: T, - ttl: Option, - mode: Option, -) -> RedisResult<()> { - deprecated_symbol_error(&key)?; - - if key.ends_with('/') { - return error(412, "Key must not end with a slash"); - } - - // If max_size != 0 and value size > max_size, return error - let max_size = CONFIG.max_size.unwrap_or(0); - if max_size != 0 && value.to_redis_args().iter().map(|a| a.len()).sum::() > max_size { - return error( - 400, - format!("Value in memory mode must be less than {} bytes", max_size), - ); - } - - // TTL logic - let sec = match ttl { - Some(Ttl::Sec(secs)) => secs, - Some(Ttl::At(timestamp)) => { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - if timestamp <= now { - return error(400, "TTL timestamp exceeds MAX_TTL limit"); - } - (timestamp - now) as usize - } - None => CONFIG.max_ttl, - }; - if sec == 0 { - return error(400, "TTL must be > 0"); - } - if sec > CONFIG.max_ttl { - return error(412, "TTL exceeds MAX_TTL"); - } - - let mode = mode.unwrap_or(SaveMode::Upsert); - - match mode { - SaveMode::Upsert | SaveMode::Insert | SaveMode::Update => { - let mut cmd = redis::cmd("SET"); - cmd.arg(key).arg(value).arg("EX").arg(sec); - match mode { - SaveMode::Insert => { - cmd.arg("NX"); - } // if NOT Exist - SaveMode::Update => { - cmd.arg("XX"); - } // if Exist - _ => {} - }; - let result: Option = cmd.query_async(conn).await?; - if result.is_none() { - return match mode { - SaveMode::Insert => error(412, "Insert: key already exists"), - SaveMode::Update => error(404, "Update: key does not exist"), - _ => error(500, "Unexpected Redis SET failure"), - }; - } - Ok(()) - } - - SaveMode::Equal(ref expected_md5) => { - let mut loop_count = 0; - loop { - let _: () = redis::cmd("WATCH").arg(key).query_async(conn).await?; - - let current: Option = redis::cmd("GET").arg(key).query_async(conn).await?; - let existing = match current { - None => { - let _: () = redis::cmd("UNWATCH").query_async(conn).await?; - return error(404, "Equal: key does not exist"); - } - Some(v) => v, - }; - // check md5 - let actual_md5 = hex::encode(md5::compute(&existing).0); - if &actual_md5 != expected_md5 { - let _: () = redis::cmd("UNWATCH").query_async(conn).await?; - return error( - 412, - format!( - "md5 mismatch, current: {}, expected: {}", - actual_md5, expected_md5 - ), - ); - } - - // MULTI/EXEC - let mut pipe = redis::pipe(); - pipe.atomic() - .cmd("SET") - .arg(key) - .arg(value.to_redis_args()) - .arg("EX") - .arg(sec); - - let result: Option = pipe.query_async(conn).await?; - if result.is_some() { - break; - } - // None -> key was changed -> repeat loop - loop_count += 1; - if loop_count > MAX_LOOP_COUNT { - let _: () = redis::cmd("UNWATCH").query_async(conn).await?; - return error(500, "Something wrong: too many retries on Equal mode"); - } - } - - Ok(()) - } - } -} - -/// redis_delete(&connection,key) -pub async fn redis_delete( - conn: &mut MultiplexedConnection, - key: &str, - mode: Option, -) -> RedisResult { - deprecated_symbol_error(key)?; - - if key.ends_with('/') { - return error(412, "Key must not end with a slash"); - } - - let mode = mode.unwrap_or(SaveMode::Upsert); - - match mode { - SaveMode::Update | SaveMode::Upsert => { - let deleted: i32 = redis::cmd("DEL").arg(key).query_async(conn).await?; - return Ok(deleted > 0); - } - - SaveMode::Equal(ref expected_md5) => { - let mut loop_count = 0; - loop { - let _: () = redis::cmd("WATCH").arg(key).query_async(conn).await?; - - let current: Option = redis::cmd("GET").arg(key).query_async(conn).await?; - let existing = match current { - None => { - let _: () = redis::cmd("UNWATCH").query_async(conn).await?; - return error(404, "Equal: key does not exist"); - } - Some(val) => val, - }; - - // check md5 - let actual_md5 = hex::encode(md5::compute(&existing).0); - if &actual_md5 != expected_md5 { - let _: () = redis::cmd("UNWATCH").query_async(conn).await?; - return error( - 412, - format!( - "md5 mismatch, current: {}, expected: {}", - actual_md5, expected_md5 - ), - ); - } - - let mut pipe = redis::pipe(); - pipe.atomic().cmd("DEL").arg(key); - - let deleted: Option = pipe.query_async(conn).await?; - if let Some(n) = deleted { - return Ok(n > 0); - } - // None -> key was changed -> repeat loop - loop_count += 1; - if loop_count > MAX_LOOP_COUNT { - let _: () = redis::cmd("UNWATCH").query_async(conn).await?; - return error(500, "Something wrong: too many retries on Equal mode"); - } - } - } - - SaveMode::Insert => { - return error(412, "Insert mode is not supported for delete"); - } - } -} - -impl TryFrom for RedisEvent { - type Error = anyhow::Error; - - fn try_from(msg: Msg) -> Result { - let channel = match msg.get_channel::() { - Ok(c) => c, - Err(e) => { - anyhow::bail!("[redis_events] bad channel: {e}"); - } - }; - let payload = match msg.get_payload::() { - Ok(p) => p, - Err(e) => { - anyhow::bail!("[redis_events] bad payload: {e}"); - } - }; - - // parsing: "__keyevent@0__:set" → event="set", db=0; payload = key - let event = channel.rsplit(':').next().unwrap_or(""); - let message = match event { - "set" => RedisEventAction::Set, - "del" => RedisEventAction::Del, - "unlink" => RedisEventAction::Unlink, - "expired" => RedisEventAction::Expired, - other => RedisEventAction::Other(other.to_string()), - }; - - Ok(RedisEvent { - // db, - key: payload.clone(), - message, - }) - } -} - -pub async fn receiver( - redis_client: Client, - hub_state: Arc>, -) -> anyhow::Result<()> { - let mut redis = redis_client.get_multiplexed_async_connection().await?; - let mut pubsub = redis_client.get_async_pubsub().await?; - - let _: String = ::redis::cmd("CONFIG") - .arg("SET") - .arg("notify-keyspace-events") - .arg("E$gx") - .query_async(&mut redis) - .await?; - - for pattern in [ - "__keyevent@*__:set", - "__keyevent@*__:del", - "__keyevent@*__:unlink", - "__keyevent@*__:expired", - ] { - pubsub.psubscribe(pattern).await?; - } - - let mut messages = pubsub.on_message(); - - while let Some(message) = messages.next().await { - match RedisEvent::try_from(message) { - Ok(ev) => { - push_event(&hub_state, &mut redis, ev).await; - } - Err(e) => { - warn!("invalid redis message: {e}"); - } - } - } - - Ok(()) -} - -/// redis_connect() -pub async fn client() -> anyhow::Result { - let default_port = match CONFIG.redis_mode { - RedisMode::Sentinel => 6379, - RedisMode::Direct => 6380, - }; - - let urls = CONFIG - .redis_urls - .iter() - .map(|url| { - redis::ConnectionAddr::Tcp( - url.host().unwrap().to_string(), - url.port().unwrap_or(default_port), - ) - }) - .collect::>(); - - if CONFIG.redis_mode == RedisMode::Sentinel { - use redis::sentinel::{SentinelClientBuilder, SentinelServerType}; - - debug!(urls=?urls, service=CONFIG.redis_service, "sentinel configuration"); - - let mut sentinel = SentinelClientBuilder::new( - urls, - CONFIG.redis_service.to_owned(), - SentinelServerType::Master, - ) - .unwrap() - .set_client_to_redis_protocol(ProtocolVersion::RESP3) - .set_client_to_redis_db(11) - .set_client_to_redis_password(CONFIG.redis_password.clone()) - .set_client_to_sentinel_password(CONFIG.redis_password.clone()) - .build()?; - - let client = sentinel.async_get_client().await?; - - Ok(client) - } else { - let single = urls - .first() - .ok_or_else(|| anyhow::anyhow!("No redis URL provided"))?; - - let redis_connection_info = RedisConnectionInfo { - db: 0, - username: None, - password: Some(CONFIG.redis_password.clone()), - protocol: ProtocolVersion::RESP3, - }; - - let connection_info = ConnectionInfo { - addr: single.clone(), - redis: redis_connection_info, - }; - - let client = Client::open(connection_info)?; - - Ok(client) - } -} diff --git a/foundations/hulypulse/src/workspace_owner.rs b/foundations/hulypulse/src/workspace_owner.rs deleted file mode 100644 index d477abfef6..0000000000 --- a/foundations/hulypulse/src/workspace_owner.rs +++ /dev/null @@ -1,105 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -use actix_web::{HttpMessage, HttpRequest}; -use hulyrs::services::jwt::Claims; -use serde_json::json; -use std::{fs, path::Path, sync::LazyLock}; -use uuid::Uuid; - -use crate::{config::CONFIG, redis::deprecated_symbol}; - -// common checker -pub fn check_workspace_core(claims_opt: Option, key: &str) -> Result<(), &'static str> { - if deprecated_symbol(key) { - return Err("Invalid key: deprecated symbols"); - } - - if CONFIG.no_authorization { - return Ok(()); - } - - let claims = claims_opt.ok_or("Missing authorization")?; - - if claims.is_system() { - return Ok(()); - } - - let jwt_workspace = claims - .workspace - .as_ref() - .ok_or("Missing workspace in token")?; - let path_ws = key - .split('/') - .next() - .ok_or("Invalid key: missing workspace")?; - if path_ws.is_empty() { - return Err("Invalid key: missing workspace"); - } - - let path_ws_uuid = Uuid::parse_str(path_ws).map_err(|_| "Invalid workspace UUID in key")?; - if jwt_workspace != &path_ws_uuid { - return Err("Workspace mismatch"); - } - - Ok(()) -} - -pub fn test_rego_claims(claim: &Claims, command: &str, key: &str) -> bool { - let data = serde_json::to_value(&claim).unwrap_or_default(); - let mut rego = REGORUS_ENGINE.clone(); - - rego.set_input(regorus::Value::from(json!({ - "command": command, - "claim": data, - "key": key, - }))); - let result = rego.eval_rule(String::from("data.main.permit")).unwrap(); - - result == regorus::Value::Bool(true) -} - -pub fn test_rego_http(req: HttpRequest, command: &str, key: &str) -> bool { - let claims = req - .extensions() - .get::() - .expect("Missing claims") - .to_owned(); - test_rego_claims(&claims, command, key) -} - -pub static POLICY_TEXT: LazyLock = LazyLock::new(|| { - let Some(policy_file) = CONFIG.policy_file.as_ref() else { - return "package main\n\ndefault permit = true\n".to_string(); - }; - let path = Path::new(policy_file); - if !path.exists() { - panic!("Policy file not found: {}", path.display()); - } - let policy_text = match fs::read_to_string(path) { - Ok(text) => format!("package main\n\n{}", text), - Err(e) => { - panic!("Failed to read policy file {}: {}", path.display(), e); - } - }; - policy_text -}); - -pub static REGORUS_ENGINE: LazyLock = LazyLock::new(|| { - let mut e = regorus::Engine::new(); - e.add_policy("policy.rego".to_string(), POLICY_TEXT.to_string()) - .expect("can't add policy"); - e -}); diff --git a/foundations/hulypulse/tests/rest_api.rs b/foundations/hulypulse/tests/rest_api.rs deleted file mode 100644 index 6a9f86fa70..0000000000 --- a/foundations/hulypulse/tests/rest_api.rs +++ /dev/null @@ -1,330 +0,0 @@ -use core::panic; -use reqwest::StatusCode; -use serde_json::Value; -use std::env; - -#[derive(Debug)] -struct ApiResponse { - code: u16, - #[allow(dead_code)] - headers: reqwest::header::HeaderMap, - etag: String, - text: String, - json: Value, -} - -fn server_url() -> String { - env::var("TEST_SERVER_URL").unwrap_or_else(|_| "http://127.0.0.1/api".to_string()) -} - -#[allow(dead_code)] -fn etag(data: &str) -> String { - format!("{:x}", md5::compute(data)) -} - -#[allow(dead_code)] -async fn status(base: &str, client: &reqwest::Client) -> () { - let status_url = format!("{}/status", &base); - let resp = client.get(&status_url).send().await.unwrap(); - - assert_eq!(resp.status(), StatusCode::OK); - let text = resp.text().await.unwrap(); - let json: Value = serde_json::from_str(&text).unwrap(); - - assert_eq!(json["backend"], "memory"); - assert_eq!(json["status"], "OK"); - assert!(json.get("memory_info").is_some()); - assert!(json.get("websockets").is_some()); - // println!("Status: {}", text); -} - -#[allow(dead_code)] -async fn get_not(base: &str, client: &reqwest::Client, workspace: &str, key: &str) -> () { - let r = engine("get", base, client, workspace, key, "", &[]).await; - assert!(r.code == 404); -} -#[allow(dead_code)] -async fn get(base: &str, client: &reqwest::Client, workspace: &str, key: &str) -> () { - let r = engine("get", base, client, workspace, key, "", &[]).await; - assert!(r.code == 200); -} -#[allow(dead_code)] -async fn get_data(base: &str, client: &reqwest::Client, workspace: &str, key: &str) -> String { - let r = engine("get", base, client, workspace, key, "", &[]).await; - assert!(r.code == 200); - r.json["data"].as_str().unwrap_or("").to_string() -} -#[allow(dead_code)] -async fn get_ttl(base: &str, client: &reqwest::Client, workspace: &str, key: &str) -> u64 { - let r = engine("get", base, client, workspace, key, "", &[]).await; - assert!(r.code == 200); - r.json["expires_at"].as_u64().unwrap_or(0) -} -#[allow(dead_code)] -async fn get_etag(base: &str, client: &reqwest::Client, workspace: &str, key: &str) -> String { - let r = engine("get", base, client, workspace, key, "", &[]).await; - assert!(r.code == 200); - r.etag -} -#[allow(dead_code)] -async fn put( - base: &str, - client: &reqwest::Client, - workspace: &str, - key: &str, - data: &str, - headers: &[&str], -) -> () { - let r = engine("put", base, client, workspace, key, data, headers).await; - // assert!(r.code == 412); - assert!(r.code == 200); - assert!(r.text == "DONE"); -} - -async fn engine( - method: &str, - base: &str, - client: &reqwest::Client, - workspace: &str, - key: &str, - data: &str, - headers: &[&str], -) -> ApiResponse { - let url = format!("{}/api/{}/{}", base, workspace, key); - let req = client; - let mut req = match method { - "get" => req.get(&url), - "put" => req.put(&url).body(data.to_string()), - "delete" => req.delete(&url), - "list" => req.get(&url), - _ => panic!("Unknown method: {}", method), - }; - - if method == "put" { - req = req.body(data.to_string()); - } - - for h in headers { - if let Some((mut k, v)) = h.split_once(':') { - k = k.trim(); - if k != "IF-MATCH" && k != "IF-NON-MATCH" && k != "HULY-TTL" { - panic!("Only `IF-MATCH`, `IF-NON-MATCH`, `HULY-TTL` headers allowed"); - } - req = req.header(k, v.trim()); - } else { - panic!("Unknown format for header: {}", h); - } - } - - let resp = req.send().await.unwrap(); - - let code = resp.status(); - let headers = resp.headers().clone(); - let text = resp.text().await.unwrap(); - ApiResponse { - code: code.as_u16(), - etag: headers - .get("ETag") - .and_then(|h| h.to_str().ok()) - .unwrap_or("") - .to_string(), - text: text.clone(), - json: serde_json::from_str(&text).unwrap_or_default(), - headers: headers.clone(), - } -} - -#[allow(dead_code)] -async fn delete_any(base: &str, client: &reqwest::Client, workspace: &str, key: &str) -> () { - engine("delete", base, client, workspace, key, "", &[]).await; -} -#[allow(dead_code)] -async fn delete( - base: &str, - client: &reqwest::Client, - workspace: &str, - key: &str, - headers: &[&str], -) -> () { - let r = engine("delete", base, client, workspace, key, "", headers).await; - assert_eq!(r.code, StatusCode::NO_CONTENT); // 204 - assert!(r.text.is_empty()); -} -#[allow(dead_code)] -async fn delete_not( - base: &str, - client: &reqwest::Client, - workspace: &str, - key: &str, - headers: &[&str], -) -> () { - let r = engine("delete", base, client, workspace, key, "", headers).await; - assert!( - (r.code == StatusCode::NOT_FOUND && r.text == "not found") - || (!headers.is_empty() - && r.code == StatusCode::PRECONDITION_FAILED - && (r.text.contains("412 md5 mismatch") - || r.text.contains("404 Equal: key does not exist"))) - ); - // println!("CODE: {}", r.code); - // println!("TEXT: {}", r.text); -} - -#[tokio::test] -async fn put_and_get() { - let base = server_url(); - // println!("Use url: {}", &base); - - let client = reqwest::Client::builder() - .danger_accept_invalid_certs(true) // только на билдере! - .build() - .unwrap(); - - // Check status - status(&base, &client).await; - - let workspace = "00000000-0000-0000-0000-000000000001"; - let key = "TESTS/key1"; - let data = "Value_1"; - - // test put/get/delete - delete_any(&base, &client, workspace, key).await; - get_not(&base, &client, workspace, key).await; - put(&base, &client, workspace, key, data, &[]).await; - get(&base, &client, workspace, key).await; - delete(&base, &client, workspace, key, &[]).await; - delete_not(&base, &client, workspace, key, &[]).await; - - // test If-Match - delete_any(&base, &client, workspace, key).await; - // delete only tag - put(&base, &client, workspace, key, data, &["HULY-TTL: 2"]).await; - delete_not( - &base, - &client, - workspace, - key, - &["IF-MATCH: 11111111111111111111111111111111"], - ) - .await; - get(&base, &client, workspace, key).await; - delete( - &base, - &client, - workspace, - key, - &["IF-MATCH: c7bcabf6b98a220f2f4888a18d01568d"], - ) - .await; - get_not(&base, &client, workspace, key).await; - - // update with tag - put(&base, &client, workspace, key, data, &["HULY-TTL: 2"]).await; - // replace with match - put( - &base, - &client, - workspace, - key, - "Another Data", - &["IF-MATCH: c7bcabf6b98a220f2f4888a18d01568d"], - ) - .await; - assert!(get_data(&base, &client, workspace, key).await == "Another Data".to_string()); - // replace with wrong mismatch - let r = engine( - "put", - &base, - &client, - &workspace, - &key, - "Next Another Data", - &["IF-MATCH: c7bcabf6b98a220f2f4888a18d01568d"], - ) - .await; - assert!(r.code == StatusCode::PRECONDITION_FAILED && r.text.contains("412 md5 mismatch")); - // match: * - assert!(get_data(&base, &client, workspace, key).await == "Another Data".to_string()); - put( - &base, - &client, - workspace, - key, - "Another Data 2", - &["IF-MATCH: *"], - ) - .await; - assert!(get_data(&base, &client, workspace, key).await == "Another Data 2".to_string()); - // unknown key matched - let fake_key = format!("{}{}", &key, "xxx"); - let r = engine( - "put", - &base, - &client, - &workspace, - &fake_key, - "Next Another Data", - &["IF-MATCH: c7bcabf6b98a220f2f4888a18d01568d"], - ) - .await; - assert!(r.code == StatusCode::NOT_FOUND && r.text.contains("404 Equal: key does not exist")); - // if not match - let data3 = "Another Data 3"; - put( - &base, - &client, - &workspace, - &key, - &data3, - &["IF-NON-MATCH: c7bcabf6b98a220f2f4888a18d01568d"], - ) - .await; - assert!(get_data(&base, &client, &workspace, &key).await == data3.to_string()); - - // DELETE if match - put(&base, &client, &workspace, &key, &data, &[]).await; - delete( - &base, - &client, - &workspace, - &key, - &["IF-MATCH: c7bcabf6b98a220f2f4888a18d01568d"], - ) - .await; - - put(&base, &client, &workspace, &key, &data, &[]).await; - delete(&base, &client, &workspace, &key, &["IF-MATCH: *"]).await; - get_not(&base, &client, &workspace, &key).await; - - put(&base, &client, &workspace, &key, &data, &[]).await; - delete(&base, &client, &workspace, &key, &["IF-NON-MATCH: *"]).await; // TODO !!! - get_not(&base, &client, &workspace, &key).await; - - put(&base, &client, &workspace, &key, &data, &[]).await; - let tag = get_etag(&base, &client, &workspace, &key).await; - assert!(tag == "c7bcabf6b98a220f2f4888a18d01568d".to_string()); - delete( - &base, - &client, - &workspace, - &key, - &["IF-NON-MATCH: c7bcabf6b98a220f2f4888a18d01568d"], - ) - .await; - get_not(&base, &client, &workspace, &key).await; - - // let r = engine("put", &base, &client, &workspace, &key, "Next Another Data", &["HULY-TTL: 2","IF-NON-MATCH: c7bcabf6b98a220f2f4888a18d01568d"]).await; - // println!("ALL: {:?}", r); - - // test TTL - delete_any(&base, &client, workspace, key).await; - put(&base, &client, workspace, key, data, &["HULY-TTL: 7"]).await; - let ttl = get_ttl(&base, &client, workspace, key).await; - assert!(ttl == 7 || ttl == 6, "TTL should be 7 (ok, may by 6)"); - put(&base, &client, workspace, key, data, &["HULY-TTL: 1"]).await; - let ttl = get_ttl(&base, &client, workspace, key).await; - assert!(ttl == 1 || ttl == 0, "TTL should be 1 (ok, may by 0)"); - // wait for 1.05 seconds - tokio::time::sleep(tokio::time::Duration::from_millis(1050)).await; - get_not(&base, &client, workspace, key).await; -} diff --git a/foundations/hulypulse/tests/ws.rs b/foundations/hulypulse/tests/ws.rs deleted file mode 100644 index 68f85595a2..0000000000 --- a/foundations/hulypulse/tests/ws.rs +++ /dev/null @@ -1,57 +0,0 @@ -use futures_util::{SinkExt, StreamExt}; -use serde_json::{Value, json}; -use tokio_tungstenite::{WebSocketStream, connect_async, tungstenite::Message}; - -#[tokio::test] -async fn websocket_echo_id() { - let workspace = "00000000-0000-0000-0000-000000000001"; - let key = "TESTS/key1"; - - let base = - std::env::var("TEST_SERVER_URL").unwrap_or_else(|_| "ws://127.0.0.1:8099/ws".to_string()); - - let (ws_stream, _) = connect_async(&base).await.expect("Can't connect WebSocket"); - let (mut write, mut read) = ws_stream.split(); - - let mut req_id = 1; - - let msg = json!({ - "correlation": req_id.to_string(), - "type": "info" - }); - let r = send(&mut write, &mut read, &msg).await; - assert_eq!(r["correlation"], req_id.to_string(), "ID should match"); - - req_id += 1; - let msg = json!({ - "correlation": req_id.to_string(), - "type": "sub", - "workspace": workspace, - "key": key - }); - let r = send(&mut write, &mut read, &msg).await; - // println!("Answer: {:?}", r); - assert_eq!(r["correlation"], req_id.to_string(), "ID should match"); - assert_eq!(r["result"], "OK", "Subscription should be ok"); -} - -async fn send( - write: &mut futures_util::stream::SplitSink, Message>, - read: &mut futures_util::stream::SplitStream>, - data: &Value, -) -> Value -where - S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin, -{ - // Sending - let msg = Message::Text(data.to_string()); - write.send(msg).await.unwrap(); - - // Waiting for response - while let Some(Ok(Message::Text(resp))) = read.next().await { - let json_resp: Value = serde_json::from_str(&resp).unwrap(); - return json_resp; - } - - panic!("No answer from server"); -} diff --git a/foundations/net/.gitattributes b/foundations/net/.gitattributes deleted file mode 100644 index 79a85db5c2..0000000000 --- a/foundations/net/.gitattributes +++ /dev/null @@ -1,14 +0,0 @@ -# Don't allow people to merge changes to these generated files, because the result -# may be invalid. You need to run "rush update" again. -pnpm-lock.yaml merge=text -shrinkwrap.yaml merge=binary -npm-shrinkwrap.json merge=binary -yarn.lock merge=binary - -# Rush's JSON config files use JavaScript-style code comments. The rule below prevents pedantic -# syntax highlighters such as GitHub's from highlighting these comments as errors. Your text editor -# may also require a special configuration to allow comments in JSON. -# -# For more information, see this issue: https://github.com/microsoft/rushstack/issues/1088 -# -*.json linguist-language=JSON-with-Comments diff --git a/foundations/net/.github/workflows/ci.yml b/foundations/net/.github/workflows/ci.yml deleted file mode 100644 index ce9256364c..0000000000 --- a/foundations/net/.github/workflows/ci.yml +++ /dev/null @@ -1,93 +0,0 @@ -name: CI - -on: - push: - branches: ['main'] - tags: - - 'v0.7.*' - - 's0.7.*' - pull_request: - branches: ['main'] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 2 - - uses: actions/setup-node@v3 - with: - node-version: 22 - - name: Verify Change Logs - run: node common/scripts/install-run-rush.js change --verify - - name: Rush Install - run: node common/scripts/install-run-rush.js install - - name: Rush validate - run: node common/scripts/install-run-rush.js validate --verbose - - publish: - runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/v0.7.') || startsWith(github.ref, 'refs/tags/s0.7.') - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 2 - - uses: actions/setup-node@v3 - with: - node-version: 22 - - name: Install dependencies - run: node common/scripts/install-run-rush.js install - - name: Rush validate - run: node common/scripts/install-run-rush.js validate --verbose - - name: Publish packages - env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - run: node common/scripts/install-run-rush.js publish --include-all --publish - - docker: - runs-on: ubuntu-latest - timeout-minutes: 60 - steps: - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Configure docker - uses: docker/setup-docker-action@v4 - with: - daemon-config: | - { - "features": { - "containerd-snapshotter": true - } - } - - uses: actions/checkout@v3 - with: - fetch-depth: 2 - - uses: actions/setup-node@v3 - with: - node-version: 22 - - name: Verify Change Logs - run: node common/scripts/install-run-rush.js change --verify - - name: Rush Install - run: node common/scripts/install-run-rush.js install - - name: Rush validate - run: node common/scripts/install-run-rush.js validate --verbose - - - name: Docker build - run: | - node common/scripts/install-run-rush.js docker:build -v - docker builder prune -a -f - env: - DOCKER_CLI_HINTS: false - DOCKER_EXTRA: --platform=linux/amd64,linux/arm64 - - name: Login to Docker Hub - if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || startsWith(github.ref, 'refs/tags/s') }} - uses: docker/login-action@v3 - with: - username: hardcoreeng - password: ${{ secrets.DOCKER_ACCESS_TOKEN }} - - name: Docker push tag - if: ${{ startsWith(github.ref, 'refs/tags/v') || startsWith(github.ref, 'refs/tags/s') }} - run: | - echo Pushing release of tag ${{ github.ref }} - node common/scripts/install-run-rush.js docker:push -v diff --git a/foundations/net/.gitignore b/foundations/net/.gitignore deleted file mode 100644 index a175778200..0000000000 --- a/foundations/net/.gitignore +++ /dev/null @@ -1,115 +0,0 @@ -.heft/ -lib/ -_api-extractor-temp/ -temp/ -.idea -pods/workspace/init/ -pods/workspace/init-scripts/ - -# Logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -*./rush-logs -*tests/sanity/screenshots - -# Runtime data -*.pid -*.seed -*.pid.lock - -# VS Code settings -.vscode/settings.json - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -# .env - -# next.js build output -.next - -# OS X temporary files -.DS_Store - -# Rush temporary files -common/deploy/ -common/temp/ -common/autoinstallers/*/.npmrc -**/.rush/temp/ -bundle.js -bundle/*.js -dist -.build -typings -types -.validate -tsconfig.tsbuildinfo -ingest-attachment-*.zip -tsdoc-metadata.json -pods/front/dist -*.cpuprofile -*.pyc -metrics.txt -dev/tool/report*.csv -tests/db_dump -.build -.format -tools/apm/apm.js -deploy -metrics.txt -services/github/pod-github/src/github.graphql -.build -.format -dev/tool/report.csv -bundle/* -bundle.js.map -tests/profiles -**/bundle/model.json -.wrangler -dump -**/logs/** -dev/tool/history.json -.aider* -/combined_dependencies -.tmp -ws-tests/docker-compose.override.yml diff --git a/foundations/net/.prettierrc b/foundations/net/.prettierrc deleted file mode 100644 index 00d5897747..0000000000 --- a/foundations/net/.prettierrc +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "trailingComma": "none", - "tabWidth": 2, - "semi": false, - "singleQuote": true, - "printWidth": 120, - "useTabs": false, - "bracketSpacing": true, - "proseWrap": "preserve", - "plugins": [], - "overrides": [ - { - "files": "*.svelte", - "options": { - "parser": "svelte" - } - } - ] -} diff --git a/foundations/net/.vscode/extensions.json b/foundations/net/.vscode/extensions.json deleted file mode 100644 index b8552f9c56..0000000000 --- a/foundations/net/.vscode/extensions.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "recommendations": [ - "dbaeumer.vscode-eslint", - "svelte.svelte-vscode", - "esbenp.prettier-vscode", - "firsttris.vscode-jest-runner" - ] -} diff --git a/foundations/net/.vscode/launch.json b/foundations/net/.vscode/launch.json deleted file mode 100644 index aae12dfc9d..0000000000 --- a/foundations/net/.vscode/launch.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "address": "127.0.0.1", - "localRoot": "${workspaceFolder}", - "name": "Attach to Remote", - "port": 9229, - "request": "attach", - "sourceMaps": true, - "skipFiles": ["/**"], - "type": "node" - }, - { - "name": "Debug Network", - "type": "node", - "request": "launch", - "args": ["src/index.ts"], - "env": { - "PORT": "3737", - "DEVELOPMENT": "true" - }, - "runtimeArgs": ["--nolazy", "-r", "ts-node/register"], - "runtimeVersion": "22", - "showAsyncStacks": true, - "outputCapture": "std", - "sourceMaps": true, - "cwd": "${workspaceRoot}/pods/network-pod", - "protocol": "inspector" - }, - { - "name": "Debug Tool", - "type": "node", - "request": "launch", - "args": ["src/index.ts", "bench-agent"], - "env": { - "NETWORK_HOST": "localhost:37371" - }, - "runtimeArgs": ["--nolazy", "-r", "ts-node/register"], - "runtimeVersion": "22", - "showAsyncStacks": true, - "outputCapture": "std", - "sourceMaps": true, - "cwd": "${workspaceRoot}/pods/network-tool", - "protocol": "inspector" - } - ] -} diff --git a/foundations/net/CHANGELOG.md b/foundations/net/CHANGELOG.md deleted file mode 100644 index ed1f0cc7aa..0000000000 --- a/foundations/net/CHANGELOG.md +++ /dev/null @@ -1,44 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] - -### Added - -- Comprehensive documentation and examples -- GitHub community health files (CONTRIBUTING.md, SECURITY.md) -- Issue and PR templates - -## [0.7.9] - 2025-10-01 - -### Added - -- Initial public release -- Core network implementation with distributed architecture -- ZeroMQ-based RPC communication layer -- Client libraries for network interaction -- Server implementation with multi-client support -- High availability support with stateless containers -- Automatic failover and health monitoring -- Multi-tenant container management -- Comprehensive test suite -- Docker deployment support -- Full documentation and examples - -### Features - -- Distributed load balancing across multiple agents -- Container lifecycle management with reference counting -- Event broadcasting capabilities -- Request/response communication patterns -- Automatic reconnection and retry logic -- Configurable timeouts for different environments -- Label-based container discovery -- Orphaned container detection and cleanup - -[Unreleased]: https://github.com/hcengineering/huly.net/compare/v0.7.9...HEAD -[0.7.9]: https://github.com/hcengineering/huly.net/releases/tag/v0.7.9 diff --git a/foundations/net/CONTRIBUTING.md b/foundations/net/CONTRIBUTING.md deleted file mode 100644 index 737e169e6f..0000000000 --- a/foundations/net/CONTRIBUTING.md +++ /dev/null @@ -1,325 +0,0 @@ -# Contributing to Huly Virtual Network - -First off, thank you for considering contributing to Huly Virtual Network! It's people like you that make this project such a great tool. - -## Table of Contents - -- [Code of Conduct](#code-of-conduct) -- [How Can I Contribute?](#how-can-i-contribute) -- [Development Setup](#development-setup) -- [Pull Request Process](#pull-request-process) -- [Coding Standards](#coding-standards) -- [Testing Guidelines](#testing-guidelines) -- [Commit Message Guidelines](#commit-message-guidelines) - -## Code of Conduct - -This project and everyone participating in it is governed by our commitment to providing a welcoming and inspiring community for all. Please be respectful and constructive in your interactions. - -## How Can I Contribute? - -### Reporting Bugs - -Before creating bug reports, please check the existing issues to avoid duplicates. When you create a bug report, include as many details as possible: - -- **Use a clear and descriptive title** -- **Describe the exact steps to reproduce the problem** -- **Provide specific examples** (code snippets, test cases) -- **Describe the behavior you observed** and what you expected -- **Include logs and error messages** -- **Specify your environment** (Node.js version, OS, etc.) - -### Suggesting Enhancements - -Enhancement suggestions are tracked as GitHub issues. When creating an enhancement suggestion: - -- **Use a clear and descriptive title** -- **Provide a detailed description** of the suggested enhancement -- **Explain why this enhancement would be useful** -- **List any alternatives you've considered** - -### Pull Requests - -We actively welcome your pull requests: - -1. Fork the repo and create your branch from `main` -2. If you've added code that should be tested, add tests -3. If you've changed APIs, update the documentation -4. Ensure the test suite passes -5. Make sure your code follows the existing style -6. Issue your pull request! - -## Development Setup - -### Prerequisites - -- **Node.js**: 22.0.0 or higher -- **PNPM**: 10.15.1 or higher (installed automatically via Rush) -- **ZeroMQ**: Native dependencies (libzmq) - -### Initial Setup - -```bash -# Clone your fork -git clone https://github.com/YOUR_USERNAME/huly.net.git -cd huly.net - -# Install dependencies -node common/scripts/install-run-rush.js install - -# Build all packages -node common/scripts/install-run-rush.js build -``` - -### Project Structure - -``` -huly.net/ -├── packages/ -│ ├── core/ # Core network implementation -│ ├── backrpc/ # ZeroMQ RPC layer -│ ├── client/ # Client libraries -│ └── server/ # Server implementation -├── pods/ -│ └── network-pod/ # Docker deployment -├── tests/ # Integration tests -├── examples/ # Example code -└── docs/ # Documentation -``` - -### Development Workflow - -```bash -# Run tests -node common/scripts/install-run-rush.js test - -# Run tests for a specific package -cd packages/core && npm test - -# Build with watch mode (during development) -node common/scripts/install-run-rush.js build:watch - -# Format code -node common/scripts/install-run-rush.js format - -# Validate TypeScript -node common/scripts/install-run-rush.js validate -``` - -## Pull Request Process - -1. **Update Documentation**: Ensure any new features or changes are documented -2. **Add Tests**: Include tests for new functionality -3. **Update CHANGELOG**: Add your changes to the appropriate package CHANGELOG.md -4. **Pass CI**: Ensure all tests pass in CI -5. **Request Review**: Tag relevant maintainers for review -6. **Sign Commits**: Use `git commit -s` to sign off on your commits - -### PR Title Format - -Use descriptive PR titles that follow this format: - -``` -[Package] Brief description of changes - -Examples: -[core] Add support for custom container timeouts -[client] Fix reconnection logic for dropped connections -[docs] Update production deployment guide -``` - -## Coding Standards - -### TypeScript Style - -- **Use TypeScript strict mode**: All code must pass strict type checking -- **Prefer interfaces over types** for object shapes -- **Use async/await** over raw Promises -- **Document public APIs** with JSDoc comments -- **Use descriptive variable names**: No single-letter variables except in loops - -### Code Organization - -```typescript -// 1. Imports (grouped: external, internal, types) -import { EventEmitter } from 'events' -import { NetworkImpl } from '../network' -import type { Container, ContainerUuid } from '../types' - -// 2. Types and interfaces -interface MyOptions { - timeout: number -} - -// 3. Class implementation -export class MyClass { - // Private fields first - private readonly config: MyOptions - - // Constructor - constructor(options: MyOptions) { - this.config = options - } - - // Public methods - async doSomething(): Promise { - // Implementation - } - - // Private methods - private helper(): void { - // Implementation - } -} -``` - -### Error Handling - -- **Always handle errors explicitly**: No silent failures -- **Use typed errors**: Create custom error classes when needed -- **Provide context**: Include relevant information in error messages - -```typescript -// Good -try { - await operation() -} catch (error: any) { - throw new Error(`Failed to perform operation: ${error.message}`) -} - -// Bad -try { - await operation() -} catch (error) { - // Silent failure -} -``` - -## Testing Guidelines - -### Test Structure - -```typescript -describe('ComponentName', () => { - describe('methodName', () => { - it('should behave correctly under normal conditions', async () => { - // Arrange - const component = new ComponentName() - - // Act - const result = await component.methodName() - - // Assert - expect(result).toBe(expected) - }) - - it('should handle error conditions', async () => { - // Test error cases - }) - }) -}) -``` - -### Test Coverage - -- **Aim for 80%+ coverage**: All new code should have tests -- **Test edge cases**: Don't just test the happy path -- **Test error conditions**: Verify error handling works correctly -- **Integration tests**: Add tests that verify component interaction - -### Running Tests - -```bash -# Run all tests -node common/scripts/install-run-rush.js test - -# Run tests for a specific package -cd packages/core -npm test - -# Run tests in watch mode -npm test -- --watch - -# Generate coverage report -npm test -- --coverage -``` - -## Commit Message Guidelines - -We follow conventional commits for clear git history: - -### Format - -``` -(): - - - -