remove to prepare for subtree

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov
2025-11-26 22:56:11 +05:00
parent 1ab6638012
commit 6ea92dd409
1351 changed files with 0 additions and 228527 deletions
-14
View File
@@ -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
-34
View File
@@ -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
-140
View File
@@ -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
View File
-11
View File
@@ -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"
}
-1
View File
@@ -1 +0,0 @@
0.1.195
-10
View File
@@ -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.
@@ -1,10 +0,0 @@
{
"changes": [
{
"packageName": "@hcengineering/communication-rest-client",
"comment": "update deps",
"type": "patch"
}
],
"packageName": "@hcengineering/communication-rest-client"
}
@@ -1,10 +0,0 @@
{
"changes": [
{
"packageName": "@hcengineering/communication-sdk-types",
"comment": "update deps",
"type": "patch"
}
],
"packageName": "@hcengineering/communication-sdk-types"
}
@@ -1,10 +0,0 @@
{
"changes": [
{
"packageName": "@hcengineering/communication-server",
"comment": "update deps",
"type": "patch"
}
],
"packageName": "@hcengineering/communication-server"
}
@@ -1,10 +0,0 @@
{
"changes": [
{
"packageName": "@hcengineering/communication-types",
"comment": "update deps",
"type": "patch"
}
],
"packageName": "@hcengineering/communication-types"
}
@@ -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/<name>/.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
@@ -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
@@ -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;
}
@@ -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": ""
}
}
}
@@ -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
}
}
@@ -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"
}
@@ -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"
// }
// ]
// }
]
}
@@ -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"
// ]
}
}
@@ -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"
// }
]
}
@@ -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:<name>:ipc` and run them instead
* of `_phase:<name>` 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
}
@@ -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/<name>/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 }
}
}
File diff suppressed because it is too large Load Diff
@@ -1,4 +0,0 @@
// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush.
{
"preferredVersionsHash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f"
}
@@ -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"
// }
]
}
@@ -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": []
}
@@ -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
// }
]
@@ -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
@@ -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
@@ -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} <command> [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
@@ -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
@@ -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 <package>@<version> <command> [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
@@ -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
}
@@ -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)
})
@@ -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
}
@@ -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"
@@ -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 ==="
-78
View File
@@ -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
}
}
}
}
}
}
@@ -1,7 +0,0 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}
@@ -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`"
}
]
}
}
]
}
@@ -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
@@ -1,4 +0,0 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig"
}
@@ -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"
}
}
}
@@ -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)
}
@@ -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<void> {
if (lq != null) {
await lq.refresh()
}
}
export function closeLiveQueries (): void {
if (lq != null) {
lq.close()
}
}
@@ -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<P extends Record<string, any>, 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<MessageQueryParams, PagedQueryCallback<Message>, MessageQueryOptions> {
override createQuery (
params: MessageQueryParams,
callback: PagedQueryCallback<Message>,
options?: MessageQueryOptions
): { unsubscribe: (isUpdate: boolean) => void } {
return getLiveQueries().queryMessages(params, callback, options)
}
}
export class NotificationsQuery extends BaseQuery<NotificationQueryParams, PagedQueryCallback<Notification>> {
override createQuery (
params: NotificationQueryParams,
callback: PagedQueryCallback<Notification>
): {
unsubscribe: (isUpdate: boolean) => void
} {
return getLiveQueries().queryNotifications(params, callback)
}
}
export class NotificationContextsQuery extends BaseQuery<
FindNotificationContextParams,
PagedQueryCallback<NotificationContext>,
NotificationContextQueryOptions
> {
override createQuery (
params: FindNotificationContextParams,
callback: PagedQueryCallback<NotificationContext>,
options?: NotificationContextQueryOptions
): {
unsubscribe: (isUpdate: boolean) => void
} {
return getLiveQueries().queryNotificationContexts(params, callback, options)
}
}
export class LabelsQuery extends BaseQuery<FindLabelsParams, QueryCallback<Label>> {
override createQuery (
params: FindLabelsParams,
callback: QueryCallback<Label>
): {
unsubscribe: (isUpdate: boolean) => void
} {
return getLiveQueries().queryLabels(params, callback)
}
}
export class CollaboratorsQuery extends BaseQuery<FindCollaboratorsParams, QueryCallback<Collaborator>> {
override createQuery (
params: FindCollaboratorsParams,
callback: QueryCallback<Collaborator>
): {
unsubscribe: (isUpdate: boolean) => void
} {
return getLiveQueries().queryCollaborators(params, callback)
}
}
@@ -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"]
}
@@ -1,7 +0,0 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}
@@ -1,63 +0,0 @@
{
"name": "@hcengineering/communication-cockroach",
"entries": [
{
"version": "0.7.11",
"tag": "@hcengineering/communication-cockroach_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.9",
"tag": "@hcengineering/communication-cockroach_v0.7.9",
"date": "Sat, 25 Oct 2025 22:20:35 GMT",
"comments": {
"patch": [
{
"comment": "Fix peer create"
}
],
"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-cockroach_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`"
},
{
"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`"
}
]
}
}
]
}
@@ -1,23 +0,0 @@
# Change Log - @hcengineering/communication-cockroach
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.9
Sat, 25 Oct 2025 22:20:35 GMT
### Patches
- Fix peer create
## 0.7.4
Tue, 14 Oct 2025 10:12:38 GMT
### Patches
- update deps
@@ -1,4 +0,0 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig"
}
@@ -1,61 +0,0 @@
{
"name": "@hcengineering/communication-cockroach",
"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",
"@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",
"@types/node": "^22.18.1",
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"@types/uuid": "^8.3.1"
},
"dependencies": {
"@hcengineering/communication-types": "workspace:^0.7.12",
"@hcengineering/communication-sdk-types": "workspace:^0.7.12",
"@hcengineering/communication-shared": "workspace:^0.7.11",
"postgres": "^3.4.7",
"uuid": "^8.3.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"
}
}
}
@@ -1,316 +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 FindCollaboratorsParams,
type AccountUuid,
type CardID,
type Collaborator,
type ContextID,
type FindNotificationContextParams,
type FindNotificationsParams,
type MessageID,
type Notification,
type NotificationContext,
type SocialID,
type WorkspaceUuid,
type NotificationID,
type Label,
type FindLabelsParams,
type LabelID,
type CardType,
NotificationType,
type NotificationContent,
WithTotal,
PeerKind,
PeerExtra,
FindPeersParams,
Peer,
FindThreadMetaParams,
MessageMeta, ThreadMeta, BlobID,
FindMessagesMetaParams
} from '@hcengineering/communication-types'
import type {
CollaboratorQuery,
CollaboratorUpdate,
DbAdapter,
LabelQuery,
LabelUpdate,
NotificationContextQuery,
NotificationContextUpdate,
NotificationQuery,
NotificationUpdate, ThreadMetaQuery, ThreadMetaUpdate
} from '@hcengineering/communication-sdk-types'
import { MessagesDb } from './db/message'
import { NotificationsDb } from './db/notification'
import { connect } from './connection'
import { type Logger, type Options } from './types'
import { formatName } from './utils'
import { initSchema } from './init'
import { LabelsDb } from './db/label'
import { SqlClient } from './client'
import { PeersDb } from './db/peer'
export class CockroachAdapter implements DbAdapter {
private readonly message: MessagesDb
private readonly notification: NotificationsDb
private readonly label: LabelsDb
private readonly peer: PeersDb
constructor (
private readonly sql: SqlClient,
private readonly workspace: WorkspaceUuid,
private readonly logger?: Logger,
private readonly options?: Options
) {
this.message = new MessagesDb(this.sql, this.workspace, logger, options)
this.notification = new NotificationsDb(this.sql, this.workspace, logger, options)
this.label = new LabelsDb(this.sql, this.workspace, logger, options)
this.peer = new PeersDb(this.sql, this.workspace, logger, options)
}
// MessageMeta
async createMessageMeta (
cardId: CardID,
id: MessageID,
creator: SocialID,
created: Date,
blobID: BlobID
): Promise<boolean> {
return await this.message.createMessageMeta(cardId, id, creator, created, blobID)
}
async removeMessageMeta (cardId: CardID, messageId: MessageID): Promise<void> {
await this.message.removeMessageMeta(cardId, messageId)
}
async findMessagesMeta (params: FindMessagesMetaParams): Promise<MessageMeta[]> {
return await this.message.findMessagesMeta(params)
}
// ThreadsIndex
async attachThreadMeta (
cardId: CardID,
messageId: MessageID,
threadId: CardID,
threadType: CardType
): Promise<void> {
await this.message.attachThreadMeta(cardId, messageId, threadId, threadType)
}
async updateThreadMeta (query: ThreadMetaQuery, update: ThreadMetaUpdate): Promise<void> {
await this.message.updateThreadMeta(query, update)
}
async removeThreadMeta (query: ThreadMetaQuery): Promise<void> {
await this.message.removeThreadMeta(query)
}
async findThreadMeta (params: FindThreadMetaParams): Promise<ThreadMeta[]> {
return await this.message.findThreadMeta(params)
}
// Collaborators
async addCollaborators (
card: CardID,
cardType: CardType,
collaborators: AccountUuid[],
date: Date
): Promise<AccountUuid[]> {
return await this.notification.addCollaborators(card, cardType, collaborators, date)
}
async removeCollaborators (query: CollaboratorQuery): Promise<void> {
await this.notification.removeCollaborators(query)
}
async updateCollaborators (query: CollaboratorQuery, update: CollaboratorUpdate): Promise<void> {
await this.notification.updateCollaborators(query, update)
}
getCollaboratorsCursor (card: CardID, date: Date, size?: number): AsyncIterable<Collaborator[]> {
return this.notification.getCollaboratorsCursor(card, date, size)
}
async findCollaborators (params: FindCollaboratorsParams): Promise<Collaborator[]> {
return await this.notification.findCollaborators(params)
}
// Notifications
async createNotification (
contextId: ContextID,
messageId: MessageID,
blobId: BlobID,
type: NotificationType,
read: boolean,
content: NotificationContent,
creator: SocialID,
created: Date
): Promise<NotificationID> {
return await this.notification.createNotification(
contextId,
messageId,
blobId,
type,
read,
content,
creator,
created
)
}
async updateNotification (query: NotificationQuery, update: NotificationUpdate): Promise<number> {
return await this.notification.updateNotification(query, update)
}
async removeNotifications (
query: NotificationQuery
): Promise<NotificationID[]> {
return await this.notification.removeNotifications(query)
}
async findNotifications (params: FindNotificationsParams): Promise<WithTotal<Notification>> {
return await this.notification.findNotifications(params)
}
// NotificationContext
async createNotificationContext (
account: AccountUuid,
card: CardID,
lastUpdate: Date,
lastView: Date,
lastNotify: Date
): Promise<ContextID> {
return await this.notification.createContext(account, card, lastUpdate, lastView, lastNotify)
}
async updateContext (query: NotificationContextQuery, update: NotificationContextUpdate): Promise<void> {
await this.notification.updateContext(query, update)
}
async removeContext (query: NotificationContextQuery): Promise<ContextID | undefined> {
return await this.notification.removeContext(query)
}
async findNotificationContexts (params: FindNotificationContextParams): Promise<NotificationContext[]> {
return await this.notification.findContexts(params)
}
// Labels
createLabel (cardId: CardID, cardType: CardType, labelId: LabelID, account: AccountUuid, created: Date): Promise<void> {
return this.label.createLabel(labelId, cardId, cardType, account, created)
}
removeLabels (query: LabelQuery): Promise<void> {
return this.label.removeLabels(query)
}
updateLabels (query: LabelQuery, updates: LabelUpdate): Promise<void> {
return this.label.updateLabels(query, updates)
}
findLabels (params: FindLabelsParams): Promise<Label[]> {
return this.label.findLabels(params)
}
// Peers
async createPeer (
workspaceId: WorkspaceUuid,
cardId: CardID,
kind: PeerKind,
value: string,
extra: PeerExtra,
date: Date,
options?: { newValue?: boolean }
): Promise<void> {
await this.peer.createPeer(workspaceId, cardId, kind, value, extra, date, options)
}
async removePeer (workspaceId: WorkspaceUuid,
cardId: CardID,
kind: PeerKind,
value: string): Promise<void> {
await this.peer.removePeer(workspaceId, cardId, kind, value)
}
findPeers (params: FindPeersParams): Promise<Peer[]> {
return this.peer.findPeers(params)
}
async getAccountsByPersonIds (ids: string[]): Promise<AccountUuid[]> {
if (ids.length === 0) return []
const sql = `SELECT data ->> 'personUuid' AS "personUuid"
FROM public.contact
WHERE "workspaceId" = $1::uuid
AND _id = ANY($2::text[])`
const result = await this.sql.execute(sql, [this.workspace, ids])
return result?.map((it) => it.personUuid as AccountUuid).filter((it) => it != null) ?? []
}
async getNameByAccount (id: AccountUuid): Promise<string | undefined> {
const sql = `SELECT data ->> 'name' AS name
FROM public.contact
WHERE "workspaceId" = $1::uuid
AND data ->> 'personUuid' = $2
LIMIT 1`
const result = await this.sql.execute(sql, [this.workspace, id])
const name: string | undefined = result[0]?.name
return name != null ? formatName(name) : undefined
}
async getCardTitle (_id: CardID): Promise<string | undefined> {
const sql = `SELECT data ->> 'title' AS title
FROM public.card
WHERE "workspaceId" = $1::uuid
AND "_id" = $2::text
LIMIT 1`
const result = await this.sql.execute(sql, [this.workspace, _id])
return result[0]?.title
}
async getCardSpaceMembers (cardId: CardID): Promise<AccountUuid[]> {
const sql = `SELECT s.members
FROM public.space AS s
JOIN public.card AS c ON c."workspaceId" = s."workspaceId" AND c.space = s._id
WHERE c."workspaceId" = $1::uuid AND c."_id" = $2::text
LIMIT 1`
const result = await this.sql.execute(sql, [this.workspace, cardId])
return result[0]?.members ?? []
}
close (): void {
this.sql.close()
}
}
export async function createDbAdapter (
connectionString: string,
workspace: WorkspaceUuid,
logger?: Logger,
options?: Options
): Promise<DbAdapter> {
const connection = connect(connectionString)
const sql = await connection.getClient()
await initSchema(sql)
const client = new SqlClient(connection, sql)
return new CockroachAdapter(client, workspace, logger, options)
}
@@ -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 postgres from 'postgres'
import type { PostgresClientReference } from './connection'
import type { SqlParams, SqlResult, SqlRow } from './types'
import { convertArrayParams } from './utils'
export class SqlClient {
constructor (
private readonly db: PostgresClientReference,
private readonly sql: postgres.Sql
) {}
getRawClient (): postgres.Sql {
return this.sql
}
async execute<T extends SqlRow>(query: string, params?: SqlParams, client?: postgres.TransactionSql): Promise<SqlResult<T>> {
const convertedParams = convertArrayParams(params)
return await (client ?? this.sql).unsafe<T[]>(query, convertedParams) as SqlResult<T>
}
cursor<T = SqlRow>(query: string, params?: SqlParams, size?: number): AsyncIterable<NonNullable<T[][number]>[]> {
const convertedParams = convertArrayParams(params)
return this.sql.unsafe<T[]>(query, convertedParams).cursor(size)
}
close (): void {
this.db.close()
}
}
@@ -1,123 +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.
//
// Full copy from @hcengineering/postgres
import postgres from 'postgres'
import { v4 as uuid } from 'uuid'
const connections = new Map<string, PostgresClientReferenceImpl>()
const clientRefs = new Map<string, ClientRef>()
export interface PostgresClientReference {
getClient: () => Promise<postgres.Sql>
close: () => void
}
class PostgresClientReferenceImpl {
count: number
client: postgres.Sql | Promise<postgres.Sql>
constructor (
client: postgres.Sql | Promise<postgres.Sql>,
readonly onclose: () => void
) {
this.count = 0
this.client = client
}
async getClient (): Promise<postgres.Sql> {
if (this.client instanceof Promise) {
this.client = await this.client
}
return this.client
}
close (force: boolean = false): void {
this.count--
if (this.count === 0 || force) {
if (force) {
this.count = 0
}
void (async () => {
this.onclose()
const cl = await this.client
await cl.end()
console.log('Closed postgres connection')
})()
}
}
addRef (): void {
this.count++
console.log('Add postgres connection', this.count)
}
}
export class ClientRef implements PostgresClientReference {
id = uuid()
constructor (readonly client: PostgresClientReferenceImpl) {
clientRefs.set(this.id, this)
}
closed = false
async getClient (): Promise<postgres.Sql> {
if (!this.closed) {
return await this.client.getClient()
} else {
throw new Error('DB client-query is already closed')
}
}
close (): void {
// Do not allow double close of connection client-query
if (!this.closed) {
clientRefs.delete(this.id)
this.closed = true
this.client.close()
}
}
}
export function connect (connectionString: string, database?: string): 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, {
database,
max: 10,
min: 2,
fetch_types: true,
debug: false,
notice: false,
onnotice () {},
onparameter () {},
...extraOptions,
prepare: true,
connection: {
application_name: 'communication'
}
})
existing = new PostgresClientReferenceImpl(sql, () => {
connections.delete(key)
})
connections.set(key, existing)
}
// Add reference and return once closable
existing.addRef()
return new ClientRef(existing)
}
@@ -1,319 +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 ParameterOrJSON } from 'postgres'
import { SortingOrder, WorkspaceUuid } from '@hcengineering/communication-types'
import { Domain } from '@hcengineering/communication-sdk-types'
import { SqlRow, type Logger, type Options, type SqlResult } from '../types'
import { SqlClient } from '../client'
import {
DbModelBatchUpdate,
DbModelColumn,
DbModelColumnType,
DbModelFilter,
DbModelUpdate,
DomainDbModel,
schemas
} from '../schema'
export class BaseDb {
constructor (
readonly client: SqlClient,
readonly workspace: WorkspaceUuid,
readonly logger?: Logger,
readonly options?: Options
) {}
protected getRowClient (): postgres.Sql {
return this.client.getRawClient()
}
protected getInsertSql<D extends Domain, M extends DomainDbModel[D]>(
domain: D,
model: M,
returnColumns: { column: DbModelColumn<D>, cast: string }[] = [],
options?: {
conflictColumns?: DbModelColumn<D>[]
conflictAction?: string
}
): { sql: string, values: any[] } {
const schema = schemas[domain]
const columns = (Object.keys(model) as DbModelColumn<D>[]).filter(
(c): c is DbModelColumn<D> => model[c] !== undefined
)
const values = columns.map((c) => model[c])
const placeholders = columns.map((c, i) => {
const sqlType = (schema as any)[c]
return `$${i + 1}::${sqlType}`
})
const columnsString = columns.map((k) => k).join(', ')
const placeholdersString = placeholders.join(', ')
let sql = `INSERT INTO ${domain} (${columnsString}) VALUES (${placeholdersString})`
if (options?.conflictColumns != null && options.conflictColumns.length > 0) {
const cols = options.conflictColumns.join(', ')
const action = options.conflictAction ?? 'DO NOTHING'
sql += ` ON CONFLICT (${cols}) ${action}`
}
if (returnColumns.length > 0) {
sql += ` RETURNING ${returnColumns.map((c) => `${c.column}::${c.cast}`).join(', ')}`
}
return { sql, values }
}
protected getBatchInsertSql<D extends Domain, M extends DomainDbModel[D]>(
domain: D,
models: M[],
returnColumns: { column: DbModelColumn<D>, cast: string }[] = [],
options?: {
conflictColumns?: DbModelColumn<D>[]
conflictAction?: string
}
): { sql: string, values: any[] } {
if (models.length === 0) throw new Error('models must not be empty')
const columns = Object.keys(models[0]) as Array<keyof M>
const schema = schemas[domain]
const values: any[] = []
const placeholders = models.map((model, i) => {
const rowPlaceholders = columns.map((k, j) => {
values.push(model[k])
return `$${i * columns.length + j + 1}::${(schema as any)[k]}`
})
return `(${rowPlaceholders.join(', ')})`
})
let sql = `INSERT INTO ${domain} (${columns.join(', ')}) VALUES ${placeholders.join(', ')}`.trim()
if (options?.conflictColumns != null && options.conflictColumns.length > 0) {
const cols = options.conflictColumns.join(', ')
const action = options.conflictAction ?? 'DO NOTHING'
sql += ` ON CONFLICT (${cols}) ${action}`
}
if (returnColumns.length > 0) {
sql += ` RETURNING ${returnColumns.map((c) => `${c.column}::${c.cast}`).join(', ')}`
}
return { sql, values }
}
protected getDeleteSql<D extends Domain>(domain: D, filter: DbModelFilter<D>): { sql: string, values: any[] } {
if (filter.length === 0) {
throw new Error('getDeleteSql requires at least one filter')
}
const schema = schemas[domain]
const placeholders: string[] = []
const values: any[] = []
filter.forEach((f, i) => {
if (!Array.isArray(f.value)) {
const idx = values.push(f.value)
placeholders.push(`${f.column} = $${idx}::${(schema as any)[f.column]}`)
} else {
const idx = values.push(f.value)
placeholders.push(`${f.column} = ANY($${idx}::${(schema as any)[f.column]}[])`)
}
})
const sql = `DELETE FROM ${domain} WHERE ${placeholders.join(' AND ')}`.trim()
return { sql, values }
}
protected getUpdateSql<D extends Domain>(
domain: D,
filter: DbModelFilter<D>,
updates: DbModelUpdate<D>
): { sql: string, values: any[] } {
if (filter.length === 0) {
throw new Error('Filter must not be empty')
}
if (updates.length === 0) {
throw new Error('Updates must not be empty')
}
const schema = schemas[domain]
const values: any[] = []
const whereClauses: string[] = []
const setClauses: string[] = []
for (const { column, value } of filter) {
const idx = values.push(value)
const cast = (schema as any)[column]
if (Array.isArray(value)) {
whereClauses.push(`${column} = ANY($${idx}::${cast}[])`)
} else {
whereClauses.push(`${column} = $${idx}::${cast}`)
}
}
updates
.filter((u) => u.innerKey == null)
.forEach((u) => {
const idx = values.push(u.value)
const cast = (schema as any)[u.column]
setClauses.push(`${u.column} = $${idx}::${cast}`)
})
const jsonGroups: Record<string, Array<{ key: string, value: any }>> = {}
updates
.filter((u) => u.innerKey != null)
.forEach((u) => {
const col = u.column
jsonGroups[col] = jsonGroups[col] ?? []
jsonGroups[col].push({ key: u.innerKey as string, value: u.value })
})
for (const [col, items] of Object.entries(jsonGroups)) {
const parts = items.map((item) => {
const idx = values.push(item.value)
return `'${item.key}', $${idx}`
})
setClauses.push(`${col} = ${col} || jsonb_build_object(${parts.join(', ')})`)
}
const sql = `
UPDATE ${domain} AS u
SET ${setClauses.join(', ')}
WHERE ${whereClauses.join(' AND ')}
`.trim()
return { sql, values }
}
protected getBatchUpdateSql<D extends Domain>(
domain: D,
keyColumn: DbModelColumn<D>,
filter: DbModelFilter<D>,
updates: DbModelBatchUpdate<D>
): { sql: string, values: any[] } {
if (filter.length === 0) throw new Error('Batch update requires at least one filter')
if (updates.length === 0) throw new Error('Batch update requires at least one update')
const schema = schemas[domain]
const values: any[] = []
const whereClauses: string[] = []
const setClauses: string[] = []
for (const { column, value } of filter) {
const idx = values.push(value)
const cast = (schema as any)[column]
whereClauses.push(
Array.isArray(value) ? `u.${column} = ANY($${idx}::${cast}[])` : `u.${column} = $${idx}::${cast}`
)
}
const rowsByKey = new Map<DbModelColumnType<D>, typeof updates>()
for (const u of updates) {
if (!rowsByKey.has(u.key)) {
rowsByKey.set(u.key, [])
}
;(rowsByKey.get(u.key) ?? []).push(u)
}
const allCols = Array.from(rowsByKey.values())
.flat()
.map((u) => u.column)
.filter((c, i, a) => a.indexOf(c) === i)
const tuples = Array.from(rowsByKey.entries()).map(([key, ups]) => {
const rowVals = [
key,
...allCols.map((col) => {
const f = ups.find((u) => u.column === col)
if (f == null) return null
return f.innerKey != null ? { [f.innerKey]: f.value } : f.value
})
]
const placeholders = rowVals.map((v, j) => {
const idx = values.push(v)
const colName = j === 0 ? keyColumn : allCols[j - 1]
const cast = (schema as any)[colName]
return `$${idx}::${cast}`
})
return `(${placeholders.join(',')})`
})
for (const col of allCols) {
if (filter.some((f) => f.column === col) || col === keyColumn) continue
setClauses.push(`${col} = v.${col}`)
}
for (const col of allCols) {
if ((schema as any)[col] === 'jsonb') {
const idx = setClauses.findIndex((s) => s.startsWith(`${col} = v.${col}`))
if (idx >= 0) setClauses[idx] = `${col} = u.${col} || v.${col}`
}
}
const sql = `
UPDATE ${domain} AS u
SET ${setClauses.join(', ')}
FROM (
VALUES
${tuples.join(',\n ')}
) AS v(${[keyColumn, ...allCols].join(', ')})
WHERE ${whereClauses.join(' AND ')}
AND u.${keyColumn} = v.${keyColumn}
`.trim()
return { sql, values }
}
protected buildOrderBy (order: SortingOrder | null | undefined, column: string): string {
return order != null ? `ORDER BY ${column} ${order === SortingOrder.Ascending ? 'ASC' : 'DESC'}` : ''
}
protected buildLimit (limit: number | null | undefined): string {
return limit != null ? `LIMIT ${limit}` : ''
}
protected async execute<T extends SqlRow>(
sql: string,
params?: ParameterOrJSON<any>[],
name?: string,
client?: postgres.TransactionSql
): Promise<SqlResult<T>> {
if (this.options?.withLogs === true && this.logger !== undefined) {
return await this.executeWithLogs(name, this.logger, sql, params, client)
}
return await this.client.execute(sql, params, client)
}
private async executeWithLogs<T extends SqlRow>(
name: string | undefined,
logger: Logger,
sql: string,
params?: ParameterOrJSON<any>[],
client?: postgres.TransactionSql
): Promise<SqlResult<T>> {
if (name === undefined) {
return await this.client.execute<T>(sql, params, client)
}
const start = performance.now()
try {
return await this.client.execute<T>(sql, params, client)
} finally {
const time = performance.now() - start
logger.info(name, { time })
}
}
}
@@ -1,190 +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 CardID,
type CardType,
type FindLabelsParams,
type LabelID,
type Label
} from '@hcengineering/communication-types'
import { Domain, LabelQuery, LabelUpdate } from '@hcengineering/communication-sdk-types'
import { BaseDb } from './base'
import { toLabel } from './mapping'
import { DbModel, DbModelFilter, DbModelUpdate } from '../schema'
export class LabelsDb extends BaseDb {
async createLabel (
label: LabelID,
card: CardID,
cardType: CardType,
account: AccountUuid,
created: Date
): Promise<void> {
const db: DbModel<Domain.Label> = {
workspace_id: this.workspace,
label_id: label,
card_id: card,
card_type: cardType,
account,
created
}
const { sql, values } = this.getInsertSql(Domain.Label, db, [], {
conflictColumns: ['workspace_id', 'label_id', 'card_id', 'account'],
conflictAction: 'DO NOTHING'
})
await this.execute(sql, values, 'insert label')
}
async removeLabels (query: LabelQuery): Promise<void> {
const filter: DbModelFilter<Domain.Label> = []
if (query.labelId != null) {
filter.push({
column: 'label_id',
value: query.labelId
})
}
if (query.cardId != null) {
filter.push({
column: 'card_id',
value: query.cardId
})
}
if (query.account != null) {
filter.push({
column: 'account',
value: query.account
})
}
if (filter.length === 0) return
filter.unshift({
column: 'workspace_id',
value: this.workspace
})
const { sql, values } = this.getDeleteSql(Domain.Label, filter)
await this.execute(sql, values, 'remove labels')
}
async updateLabels (query: LabelQuery, update: LabelUpdate): Promise<void> {
const dbUpdate: DbModelUpdate<Domain.Label> = []
const filter: DbModelFilter<Domain.Label> = [
{
column: 'workspace_id',
value: this.workspace
}
]
if (query.cardId != null) {
filter.push({
column: 'card_id',
value: query.cardId
})
}
if (query.labelId != null) {
filter.push({
column: 'label_id',
value: query.labelId
})
}
if (query.account != null) {
filter.push({
column: 'account',
value: query.account
})
}
if (update.cardType != null) {
dbUpdate.push({
column: 'card_type',
value: update.cardType
})
}
if (dbUpdate.length === 0) return
const { sql, values } = this.getUpdateSql(Domain.Label, filter, dbUpdate)
await this.execute(sql, values, 'update labels')
}
async findLabels (params: FindLabelsParams): Promise<Label[]> {
const select = `SELECT *
FROM ${Domain.Label} l`
const { where, values } = this.buildWhere(params)
const limit = this.buildLimit(params.limit)
const orderBy = this.buildOrderBy(params.order, 'l.created')
const sql = [select, where, orderBy, limit].join(' ')
const result = await this.execute(sql, values, 'find labels')
return result.map((it: any) => toLabel(it))
}
buildWhere (params: FindLabelsParams, startIndex: number = 0, prefix = 'l.'): { where: string, values: any[] } {
const where: string[] = []
const values: any[] = []
let index = startIndex + 1
where.push(`${prefix}workspace_id = $${index++}::uuid`)
values.push(this.workspace)
if (params.labelId != null) {
const labels = Array.isArray(params.labelId) ? params.labelId : [params.labelId]
if (labels.length === 1) {
where.push(`${prefix}label_id = $${index++}::varchar`)
values.push(labels[0])
} else {
where.push(`${prefix}label_id = ANY($${index++}::varchar[])`)
values.push(labels)
}
}
if (params.cardId != null) {
where.push(`${prefix}card_id = $${index++}::varchar`)
values.push(params.cardId)
}
if (params.cardType != null) {
const types = Array.isArray(params.cardType) ? params.cardType : [params.cardType]
if (types.length === 1) {
where.push(`${prefix}card_type = $${index++}::varchar`)
values.push(types[0])
} else {
where.push(`${prefix}card_type = ANY($${index++}::varchar[])`)
values.push(types)
}
}
if (params.account != null) {
where.push(`${prefix}account = $${index++}::uuid`)
values.push(params.account)
}
return { where: `WHERE ${where.join(' AND ')}`, values }
}
}
@@ -1,146 +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,
type Collaborator,
type ContextID,
type MessageID,
type Notification,
type NotificationContext,
type NotificationID,
type Label,
type AccountUuid,
Peer,
WorkspaceUuid,
PeerExtra,
MessageMeta,
ThreadMeta,
BlobID
} from '@hcengineering/communication-types'
import { Domain } from '@hcengineering/communication-sdk-types'
import { DbModel } from '../schema'
interface RawNotification extends DbModel<Domain.Notification> {
account: AccountUuid
}
type RawContext = DbModel<Domain.NotificationContext> & { id: ContextID, total?: number } & {
notifications?: RawNotification[]
}
export function toMessageMeta (raw: DbModel<Domain.MessageIndex>): MessageMeta {
return {
id: String(raw.message_id) as MessageID,
cardId: raw.card_id,
created: new Date(raw.created),
creator: raw.creator,
blobId: String(raw.blob_id) as BlobID
}
}
export function toThreadMeta (raw: DbModel<Domain.ThreadIndex>): ThreadMeta {
return {
cardId: raw.card_id,
messageId: String(raw.message_id) as MessageID,
threadId: raw.thread_id,
threadType: raw.thread_type
}
}
export function toNotificationContext (raw: RawContext): NotificationContext {
const lastView = new Date(raw.last_view)
return {
id: String(raw.id) as ContextID,
cardId: raw.card_id,
account: raw.account,
lastView,
lastUpdate: new Date(raw.last_update),
lastNotify: raw.last_notify != null ? new Date(raw.last_notify) : undefined,
notifications: (raw.notifications ?? [])
.filter((it) => it.id != null)
.map((it) => toNotificationRaw(raw.id, raw.card_id, { ...it, account: raw.account })),
totalNotifications: Number(raw.total ?? 0)
}
}
function toNotificationRaw (id: ContextID, card: CardID, raw: RawNotification): Notification {
const created = new Date(raw.created)
return {
id: String(raw.id) as NotificationID,
cardId: card,
account: raw.account,
type: raw.type,
read: Boolean(raw.read),
messageId: String(raw.message_id) as MessageID,
creator: raw.creator,
created,
contextId: String(id) as ContextID,
content: raw.content,
blobId: raw.blob_id ?? undefined
}
}
export function toNotification (raw: RawNotification & { card_id: CardID }): Notification {
return toNotificationRaw(raw.context_id, raw.card_id, raw)
}
export function toCollaborator (raw: DbModel<Domain.Collaborator>): Collaborator {
return {
account: raw.account,
cardType: raw.card_type,
cardId: raw.card_id
}
}
export function toLabel (raw: DbModel<Domain.Label>): Label {
return {
labelId: raw.label_id,
cardId: raw.card_id,
cardType: raw.card_type,
account: raw.account,
created: new Date(raw.created)
}
}
export function toPeer (
raw: DbModel<Domain.Peer> & { members?: { workspace_id: WorkspaceUuid, card_id: CardID, extra?: PeerExtra }[] }
): Peer {
const peer: Peer = {
workspaceId: raw.workspace_id,
cardId: raw.card_id,
kind: raw.kind,
value: raw.value,
extra: raw.extra,
created: new Date(raw.created)
}
if (peer.kind === 'card') {
return {
...peer,
kind: 'card',
members:
raw.members?.map((it) => ({
workspaceId: it.workspace_id,
cardId: it.card_id,
extra: it.extra ?? {}
})) ?? []
}
}
return peer
}
@@ -1,237 +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 {
BlobID,
type CardID,
type CardType,
FindMessagesMetaParams,
FindThreadMetaParams,
type MessageID,
MessageMeta,
type SocialID,
type ThreadMeta
} from '@hcengineering/communication-types'
import { Domain, ThreadMetaUpdate, ThreadMetaQuery } from '@hcengineering/communication-sdk-types'
import { BaseDb } from './base'
import { DbModel, DbModelFilter, schemas } from '../schema'
import { toMessageMeta, toThreadMeta } from './mapping'
export class MessagesDb extends BaseDb {
// Message Index
public async createMessageMeta (
cardId: CardID,
messageId: MessageID,
creator: SocialID,
created: Date,
blobId: BlobID
): Promise<boolean> {
const model: DbModel<Domain.MessageIndex> = {
workspace_id: this.workspace,
card_id: cardId,
message_id: messageId,
created,
creator,
blob_id: blobId
}
const insertSql = this.getInsertSql(Domain.MessageIndex, model, [], {
conflictColumns: ['workspace_id', 'card_id', 'message_id'],
conflictAction: 'DO NOTHING'
})
const result = await this.execute(insertSql.sql, insertSql.values, 'insert message meta')
return result.count !== 0
}
async removeMessageMeta (cardId: CardID, messageId: MessageID): Promise<void> {
const filter: DbModelFilter<Domain.MessageIndex> = [
{
column: 'workspace_id',
value: this.workspace
},
{
column: 'card_id',
value: cardId
},
{
column: 'message_id',
value: messageId
}
]
const { sql, values } = this.getDeleteSql(Domain.MessageIndex, filter)
await this.execute(sql, values, 'remove message meta')
}
public async findMessagesMeta (params: FindMessagesMetaParams): Promise<MessageMeta[]> {
const select = `SELECT *
FROM ${Domain.MessageIndex} mi
`
const limit = this.buildLimit(params.limit)
const orderBy = this.buildOrderBy(params.order, 'mi.created')
const { where, values } = this.buildMessageMetaWhere(params)
const sql = [select, where, orderBy, limit].join(' ')
const result = await this.execute(sql, values, 'find message meta')
return result.map((it: any) => toMessageMeta(it))
}
private buildMessageMetaWhere (params: FindMessagesMetaParams): { where: string, values: any[] } {
const where: string[] = []
const values: any[] = []
const schema = schemas[Domain.MessageIndex]
let index = 1
where.push(`mi.workspace_id = $${index++}::${schema.workspace_id}`)
values.push(this.workspace)
if (params.cardId != null) {
where.push(`mi.card_id = $${index++}::${schema.card_id}`)
values.push(params.cardId)
}
if (params.id != null) {
where.push(`mi.message_id = $${index++}::${schema.message_id}`)
values.push(params.id)
}
if (params.creator != null) {
where.push(`mi.creator = $${index++}::${schema.creator}`)
values.push(params.creator)
}
return { where: `WHERE ${where.join(' AND ')}`, values }
}
// Thread Index
async attachThreadMeta (cardId: CardID, messageId: MessageID, threadId: CardID, threadType: CardType): Promise<void> {
const db: DbModel<Domain.ThreadIndex> = {
workspace_id: this.workspace,
card_id: cardId,
message_id: messageId,
thread_id: threadId,
thread_type: threadType,
replies_count: 0,
last_reply: new Date()
}
const { sql, values } = this.getInsertSql(Domain.ThreadIndex, db)
await this.execute(sql, values, 'insert thread')
}
async updateThreadMeta (query: ThreadMetaQuery, update: ThreadMetaUpdate): Promise<void> {
const set: string[] = []
const values: any[] = []
let index = 1
if (update.threadType != null) {
set.push(`thread_type = $${index++}::varchar`)
values.push(update.threadType)
}
if (set.length === 0) return
const updateSql = `UPDATE ${Domain.ThreadIndex}`
const setSql = 'SET ' + set.join(', ')
let where = `WHERE workspace_id = $${index++}::uuid`
values.push(this.workspace)
if (query.cardId != null) {
where += ` AND card_id = $${index++}::varchar`
values.push(query.cardId)
}
if (query.messageId != null) {
where += ` AND message_id = $${index++}::varchar`
values.push(query.messageId)
}
if (query.threadId != null) {
where += ` AND thread_id = $${index++}::varchar`
values.push(query.threadId)
}
const sql = [updateSql, setSql, where].join(' ')
await this.execute(sql, values, 'update thread')
}
async removeThreadMeta (query: ThreadMetaQuery): Promise<void> {
const filter: DbModelFilter<Domain.ThreadIndex> = [
{
column: 'workspace_id',
value: this.workspace
}
]
if (query.cardId != null) filter.push({ column: 'card_id', value: query.cardId })
if (query.messageId != null) filter.push({ column: 'message_id', value: query.messageId })
if (query.threadId != null) filter.push({ column: 'thread_id', value: query.threadId })
const { sql, values } = this.getDeleteSql(Domain.ThreadIndex, filter)
await this.execute(sql, values, 'remove threads')
}
async findThreadMeta (params: FindThreadMetaParams): Promise<ThreadMeta[]> {
const { where, values } = this.buildThreadMetaWhere(params)
const select = `
SELECT *
FROM ${Domain.ThreadIndex} t
`
const limit = this.buildLimit(params.limit)
const orderBy = this.buildOrderBy(params.order, 't.date')
const sql = [select, where, orderBy, limit].join(' ')
const result = await this.execute(sql, values, 'find threads')
return result.map((it: any) => toThreadMeta(it))
}
private buildThreadMetaWhere (
params: FindThreadMetaParams,
startIndex: number = 0,
prefix: string = 't.'
): { where: string, values: any[] } {
const where: string[] = []
const values: any[] = []
let index = startIndex + 1
where.push(`${prefix}workspace_id = $${index++}::uuid`)
values.push(this.workspace)
if (params.cardId != null) {
where.push(`${prefix}card_id = $${index++}::varchar`)
values.push(params.cardId)
}
if (params.messageId != null) {
where.push(`${prefix}message_id = $${index++}::varchar`)
values.push(params.messageId)
}
if (params.threadId != null) {
where.push(`${prefix}thread_id = $${index++}::varchar`)
values.push(params.threadId)
}
return { where: `WHERE ${where.join(' AND ')}`, values }
}
}
@@ -1,654 +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 CardID,
type Collaborator,
type ContextID,
type FindCollaboratorsParams,
type FindNotificationContextParams,
type FindNotificationsParams,
type MessageID,
type Notification,
type NotificationContext,
SortingOrder,
type NotificationID,
type CardType,
type NotificationType,
type NotificationContent,
WithTotal,
BlobID,
SocialID
} from '@hcengineering/communication-types'
import { withTotal } from '@hcengineering/communication-shared'
import {
CollaboratorQuery,
CollaboratorUpdate,
Domain,
NotificationContextQuery,
NotificationContextUpdate,
NotificationQuery,
NotificationUpdate
} from '@hcengineering/communication-sdk-types'
import { BaseDb } from './base'
import { getCondition } from './utils'
import { toCollaborator, toNotification, toNotificationContext } from './mapping'
import { DbModel, DbModelFilter, DbModelUpdate } from '../schema'
export class NotificationsDb extends BaseDb {
async addCollaborators (
card: CardID,
cardType: CardType,
collaborators: AccountUuid[],
date: Date
): Promise<AccountUuid[]> {
if (collaborators.length === 0) return []
const models: DbModel<Domain.Collaborator>[] = collaborators.map((account, index) => ({
workspace_id: this.workspace,
card_id: card,
account,
date,
card_type: cardType
}))
const { sql, values } = this.getBatchInsertSql(Domain.Collaborator, models, [{ column: 'account', cast: 'text' }], {
conflictColumns: ['workspace_id', 'card_id', 'account'],
conflictAction: 'DO NOTHING'
})
const result = await this.execute(sql, values, 'insert collaborators')
return result.map((it: any) => it.account)
}
async removeCollaborators (query: CollaboratorQuery): Promise<void> {
if (query.cardId == null) return
const filter: DbModelFilter<Domain.Collaborator> = [
{ column: 'workspace_id', value: this.workspace },
{ column: 'card_id', value: query.cardId }
]
if (query.account != null) {
filter.push({ column: 'account', value: query.account ?? [] })
}
const { sql, values } = this.getDeleteSql(Domain.Collaborator, filter)
await this.execute(sql, values, 'remove collaborator')
}
getCollaboratorsCursor (
card: CardID,
date: Date,
size?: number
): AsyncIterable<NonNullable<Collaborator[][number]>[]> {
const sql = `
SELECT *
FROM ${Domain.Collaborator}
WHERE workspace_id = $1::uuid
AND card_id = $2::varchar
AND date <= $3::timestamptz
ORDER BY date ASC `
return this.client.cursor<Collaborator>(sql, [this.workspace, card, date], size)
}
async createNotification (
contextId: ContextID,
messageId: MessageID,
blobId: BlobID,
type: NotificationType,
read: boolean,
content: NotificationContent,
creator: SocialID,
created: Date
): Promise<NotificationID> {
const db: Omit<DbModel<Domain.Notification>, 'id'> = {
context_id: contextId,
type,
message_id: messageId,
blob_id: blobId,
read,
created,
content,
creator
}
const { sql, values } = this.getInsertSql(Domain.Notification, db as DbModel<Domain.Notification>, [
{ column: 'id', cast: 'text' }
])
const result = await this.execute(sql, values, 'insert notification')
return result[0].id as NotificationID
}
async updateNotification (query: NotificationQuery, update: NotificationUpdate): Promise<number> {
const where: string[] = [
'nc.workspace_id = $1::uuid',
'nc.id = $2::int8',
'nc.account = $3::uuid',
'nc.id = n.context_id'
]
const values: any[] = [this.workspace]
let index = values.length + 1
if (query.contextId != null) {
where.push(`n.context_id = $${index++}::int8`)
values.push(query.contextId)
}
if (query.account != null) {
where.push(`nc.account = $${index++}::uuid`)
values.push(query.account)
}
if (query.id != null) {
if (Array.isArray(query.id)) {
if (query.id.length === 1) {
where.push(`n.id = $${index++}::int8`)
values.push(query.id[0])
} else {
where.push(`n.id = ANY($${index++}::int8[])`)
values.push(query.id)
}
} else {
where.push(`n.id = $${index++}::int8`)
values.push(query.id)
}
}
if (query.type != null) {
where.push(`n.type = $${index++}::varchar`)
values.push(query.type)
}
if (query.untilDate != null) {
const createdCondition = getCondition('n', 'created', index, { lessOrEqual: query.untilDate }, 'timestamptz')
if (createdCondition != null) {
where.push(createdCondition.where)
values.push(...createdCondition.values)
index = createdCondition.index
}
}
const whereClause = `WHERE ${where.join(' AND ')} AND read <>${index}::boolean`
const sql = `
UPDATE ${Domain.Notification} n
SET read = $${index}::boolean
FROM ${Domain.NotificationContext} nc
${whereClause}
`
const result = await this.execute(sql, [...values, update.read], 'update notification')
return result?.count ?? 0
}
async removeNotifications (query: NotificationQuery): Promise<NotificationID[]> {
const ids = query.id == null || Array.isArray(query.id) ? query.id : [query.id]
const where: string[] = ['nc.workspace_id = $1::uuid']
const values: any[] = [this.workspace]
let index = values.length + 1
if (query.contextId != null) {
where.push(`n.context_id = $${index++}::int8`)
values.push(query.contextId)
}
if (query.account != null) {
where.push(`nc.account = $${index++}::uuid`)
values.push(query.account)
}
if (query.type != null) {
where.push(`n.type = $${index++}::varchar`)
values.push(query.type)
}
if (ids != null && ids.length > 0) {
if (ids.length === 1) {
where.push(`n.id = $${index++}::int8`)
values.push(ids[0])
} else {
where.push(`n.id = ANY($${index++}::int8[])`)
values.push(ids)
}
}
if (values.length <= 1) return []
const sql = `DELETE FROM ${Domain.Notification} n
USING ${Domain.NotificationContext} nc
WHERE ${where.join(' AND ')}
RETURNING n.id::text`
const result = await this.execute(sql, values, 'remove notifications')
return result.map((row: any) => row.id)
}
async createContext (
account: AccountUuid,
card: CardID,
lastUpdate: Date,
lastView: Date,
lastNotify: Date
): Promise<ContextID> {
const db: Omit<DbModel<Domain.NotificationContext>, 'id'> = {
workspace_id: this.workspace,
card_id: card,
account,
last_view: lastView,
last_update: lastUpdate,
last_notify: lastNotify
}
const { sql, values } = this.getInsertSql(Domain.NotificationContext, db as DbModel<Domain.NotificationContext>, [
{ column: 'id', cast: 'text' }
])
const result = await this.execute(sql, values, 'insert notification context')
return result[0].id as ContextID
}
async removeContext (query: NotificationContextQuery): Promise<ContextID | undefined> {
const sql = `DELETE
FROM ${Domain.NotificationContext}
WHERE workspace_id = $1::uuid
AND id = $2::int8
AND account = $3::uuid
RETURNING id::text`
const result = await this.execute(sql, [this.workspace, query.id, query.account], 'remove notification context')
return result[0]?.id as ContextID | undefined
}
async updateContext (query: NotificationContextQuery, update: NotificationContextUpdate): Promise<void> {
const dbUpdate: DbModelUpdate<Domain.NotificationContext> = []
if (update.lastView != null) {
dbUpdate.push({
column: 'last_view',
value: update.lastView
})
}
if (update.lastUpdate != null) {
dbUpdate.push({
column: 'last_update',
value: update.lastUpdate
})
}
if (update.lastNotify != null) {
dbUpdate.push({
column: 'last_notify',
value: update.lastNotify
})
}
if (dbUpdate.length === 0) return
const filter: DbModelFilter<Domain.NotificationContext> = [
{
column: 'workspace_id',
value: this.workspace
}
]
if (query.id != null) {
filter.push({ column: 'id', value: query.id })
}
if (query.account != null) {
filter.push({ column: 'account', value: query.account })
}
const { sql, values } = this.getUpdateSql(Domain.NotificationContext, filter, dbUpdate)
await this.execute(sql, values, 'update notification context')
}
async findContexts (params: FindNotificationContextParams): Promise<NotificationContext[]> {
const withNotification = params.notifications != null
const withTotal = params.notifications?.total === true
const notificationsLimit = params.notifications?.limit
const notificationOrder = params.notifications?.order === SortingOrder.Ascending ? 'ASC' : 'DESC'
const { where, values } = this.buildContextWhere(params)
const orderBy = this.buildOrderBy(params.order, 'nc.last_notify')
const limit = this.buildLimit(params.limit)
if (!withNotification) {
const sql = `
SELECT nc.id::text,
nc.card_id,
nc.account,
nc.last_view,
nc.last_update,
nc.last_notify
FROM ${Domain.NotificationContext} nc
${where}
${orderBy}
${limit};
`
const rows = await this.execute(sql, values, 'find contexts (no notifications)')
return rows.map((it: any) => toNotificationContext(it))
}
const { where: notificationWhere, values: notificationValues } = this.buildNotificationWhere(
{ read: params.notifications?.read, type: params.notifications?.type },
values.length,
true
)
values.push(...notificationValues)
const contextsCte = `
WITH ctx AS (
SELECT id, card_id, account, last_view, last_update, last_notify, workspace_id
FROM ${Domain.NotificationContext} nc
${where}
${orderBy}
${limit}
)
`
const notificationsCte = `
, last_notifs AS (
SELECT *
FROM (
SELECT n.*,
ROW_NUMBER() OVER (PARTITION BY n.context_id ORDER BY n.created ${notificationOrder}) AS rn
FROM ${Domain.Notification} n
WHERE n.context_id IN (SELECT id FROM ctx)
${notificationWhere.length > 0 ? `AND (${notificationWhere.replace(/^WHERE/i, '')})` : ''}
) t
WHERE rn <= ${notificationsLimit}
)
`
const msgKeysCte = `
, message_keys AS (
SELECT DISTINCT
c.workspace_id,
c.card_id,
n.message_id
FROM last_notifs n
JOIN ctx c ON c.id = n.context_id
WHERE n.message_id IS NOT NULL
)
`
const statsCte = withTotal
? `
, stats AS (
SELECT context_id, COUNT(*) AS total
FROM ${Domain.Notification} n
WHERE n.context_id IN (SELECT id FROM ctx)
${notificationWhere.length > 0 ? `AND (${notificationWhere.replace(/^WHERE/i, '')})` : ''}
GROUP BY context_id
)`
: ''
const sql = `
${contextsCte}
${notificationsCte}
${msgKeysCte}
${statsCte}
SELECT
nc.id::text,
nc.card_id,
nc.account,
nc.last_view,
nc.last_update,
${withTotal ? ' MAX(s.total) AS total,' : ''}
nc.last_notify,
COALESCE(
JSON_AGG(
JSONB_BUILD_OBJECT(
'id', n.id::text,
'read', n.read,
'type', n.type,
'content', n.content,
'created', n.created,
'blob_id', n.blob_id,
'message_id', n.message_id::text,
'creator', n.creator
)
ORDER BY n.created ${notificationOrder}
), '[]'::jsonb
) AS notifications
FROM ctx nc
${withTotal ? 'LEFT JOIN stats s ON s.context_id = nc.id' : ''}
LEFT JOIN last_notifs n
ON n.context_id = nc.id
GROUP BY
nc.id, nc.card_id, nc.account, nc.last_view, nc.last_update, nc.last_notify
${orderBy}
`.trim()
const rows = await this.execute(sql, values, 'find contexts (cte)')
return rows.map((it: any) => toNotificationContext(it))
}
async findNotifications (params: FindNotificationsParams): Promise<WithTotal<Notification>> {
let select =
'SELECT n.id, n.created, n.read, n.message_id, n.blob_id, n.type, n.content, n.context_id, n.creator, nc.card_id, nc.account, nc.last_view '
select += ` FROM ${Domain.Notification} n
JOIN ${Domain.NotificationContext} nc ON n.context_id = nc.id`
const { where, values } = this.buildNotificationWhere(params)
const orderBy = this.buildOrderBy(params.order, 'n.created')
const limit = this.buildLimit(params.limit)
const sql = [select, where, orderBy, limit].join(' ')
const result = await this.execute(sql, values, 'find notifications')
let total: number | undefined
if (params.total === true) {
const totalSql = this.buildNotificationsTotalSql(params)
const result = await this.execute(totalSql.sql, totalSql.values, 'find notifications total')
total = result[0]?.total != null ? Number(result[0]?.total ?? 0) : undefined
}
return withTotal(
result.map((it: any) => toNotification(it)),
total
)
}
private buildNotificationsTotalSql (params: FindNotificationsParams): { sql: string, values: any[] } {
const select = `
SELECT COUNT(*) AS total
FROM ${Domain.Notification} n
JOIN ${Domain.NotificationContext} nc ON n.context_id = nc.id`
const { where, values } = this.buildNotificationWhere(params)
const sql = [select, where].join(' ')
return { sql, values }
}
async updateCollaborators (query: CollaboratorQuery, update: CollaboratorUpdate): Promise<void> {
const dbUpdate: Partial<DbModel<Domain.Collaborator>> = {
account: update.account,
card_id: update.cardId,
card_type: update.cardType
}
const entries = Object.entries(dbUpdate).filter(([_, value]) => value != null)
if (entries.length === 0) return
entries.unshift(['workspace_id', this.workspace])
const setClauses = entries.map(([key], index) => `${key} = $${index + 1}`)
const setValues = entries.map(([_, value]) => value)
const { where, values: whereValues } = this.buildCollaboratorsWhere(query, setValues.length, '')
const sql = `UPDATE ${Domain.Collaborator}
SET ${setClauses.join(', ')}
${where}`
await this.execute(sql, [...setValues, ...whereValues], 'update collaborators')
}
async findCollaborators (params: FindCollaboratorsParams): Promise<Collaborator[]> {
const { where, values } = this.buildCollaboratorsWhere(params)
const select = `
SELECT *
FROM ${Domain.Collaborator} c
`
const limit = params.limit != null ? ` LIMIT ${params.limit}` : ''
const orderBy =
params.order != null ? `ORDER BY c.date ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}` : ''
const sql = [select, where, orderBy, limit].join(' ')
const result = await this.execute(sql, values, 'find collaborators')
return result.map((it: any) => toCollaborator(it))
}
private buildCollaboratorsWhere (
params: FindCollaboratorsParams,
startIndex: number = 0,
prefix: string = 'c.'
): { where: string, values: any[] } {
const where: string[] = []
const values: any[] = []
let index = startIndex + 1
where.push(`${prefix}workspace_id = $${index++}::uuid`)
values.push(this.workspace)
where.push(`${prefix}card_id = $${index++}::varchar`)
values.push(params.cardId)
if (params.account != null) {
const accounts = Array.isArray(params.account) ? params.account : [params.account]
if (accounts.length === 1) {
where.push(`${prefix}account = $${index++}::uuid`)
values.push(accounts[0])
} else if (accounts.length > 1) {
where.push(`${prefix}account = ANY($${index++}::uuid[])`)
values.push(accounts)
}
}
return { where: `WHERE ${where.join(' AND ')}`, values }
}
private buildContextWhere (params: FindNotificationContextParams): {
where: string
values: any[]
index: number
} {
const where: string[] = ['nc.workspace_id = $1::uuid']
const values: any[] = [this.workspace]
let index = 2
if (params.id != null) {
where.push(`nc.id = $${index++}::int8`)
values.push(params.id)
}
if (params.cardId != null) {
const cards = Array.isArray(params.cardId) ? params.cardId : [params.cardId]
if (cards.length === 1) {
where.push(`nc.card_id = $${index++}::varchar`)
values.push(cards[0])
} else {
where.push(`nc.card_id = ANY($${index++}::varchar[])`)
values.push(cards)
}
}
if (params.account != null) {
const accounts = Array.isArray(params.account) ? params.account : [params.account]
if (accounts.length === 1) {
where.push(`nc.account = $${index++}::uuid`)
values.push(accounts[0])
} else if (accounts.length > 1) {
where.push(`nc.account IN (${accounts.map((it) => `$${index++}::uuid`).join(', ')})`)
values.push(...accounts)
}
}
const lastUpdateCondition = getCondition('nc', 'last_notify', index, params.lastNotify, 'timestamptz')
if (lastUpdateCondition != null) {
where.push(lastUpdateCondition.where)
values.push(...lastUpdateCondition.values)
index = lastUpdateCondition.index
}
return { where: `WHERE ${where.join(' AND ')}`, values, index }
}
private buildNotificationWhere (
params: FindNotificationsParams,
initialIndex?: number,
skipWorkspace?: boolean
): {
where: string
values: any[]
} {
const where: string[] = skipWorkspace === true ? [] : ['nc.workspace_id = $1::uuid']
const values: any[] = skipWorkspace === true ? [] : [this.workspace]
let index = (initialIndex ?? 0) + values.length + 1
if (params.contextId != null) {
where.push(`n.context_id = $${index++}::int8`)
values.push(params.contextId)
}
if (params.account != null) {
const accounts = Array.isArray(params.account) ? params.account : [params.account]
if (accounts.length === 1) {
where.push(`nc.account = $${index++}::uuid`)
values.push(accounts[0])
} else if (accounts.length > 1) {
where.push(`nc.account = ANY ($${index++}::uuid[])`)
values.push(accounts)
}
}
if (params.cardId != null) {
where.push(`nc.card_id = $${index++}::varchar`)
values.push(params.cardId)
}
if (params.messageId != null) {
where.push(`n.message_id = $${index++}::varchar`)
values.push(params.messageId)
}
if (params.type != null) {
where.push(`n.type = $${index++}::varchar`)
values.push(params.type)
}
const createdCondition = getCondition('n', 'created', index, params.created, 'timestamptz')
if (createdCondition != null) {
where.push(createdCondition.where)
values.push(...createdCondition.values)
index = createdCondition.index
}
if (params.read === true) {
where.push('n.read = true')
}
if (params.read === false) {
where.push('n.read = false')
}
return { where: where.length > 0 ? `WHERE ${where.join(' AND ')}` : '', values }
}
}
@@ -1,150 +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,
type CardID,
PeerKind,
PeerExtra,
FindPeersParams,
SortingOrder,
Peer
} from '@hcengineering/communication-types'
import { Domain } from '@hcengineering/communication-sdk-types'
import { BaseDb } from './base'
import { DbModel, DbModelFilter } from '../schema'
import { toPeer } from './mapping'
export class PeersDb extends BaseDb {
async createPeer (
workspaceId: WorkspaceUuid,
cardId: CardID,
kind: PeerKind,
value: string,
extra: PeerExtra,
date: Date,
options?: { newValue?: boolean }
): Promise<void> {
const db: DbModel<Domain.Peer> = {
workspace_id: workspaceId,
card_id: cardId,
kind,
value,
extra,
created: date
}
if (options?.newValue === true) {
const { sql, values } = this.getInsertSql(Domain.Peer, db, [], {
conflictColumns: ['workspace_id', 'kind', 'value'],
conflictAction: 'DO NOTHING'
})
const result = await this.execute(sql, values, 'insert peer')
const count = result?.count ?? 0
if (count === 0) {
throw Error('Peer value already exists')
}
} else {
const { sql, values } = this.getInsertSql(Domain.Peer, db, [])
await this.execute(sql, values, 'insert peer')
}
}
async removePeer (workspaceId: WorkspaceUuid, cardId: CardID, kind: PeerKind, value: string): Promise<void> {
const filter: DbModelFilter<Domain.Peer> = [
{
column: 'workspace_id',
value: workspaceId
},
{
column: 'card_id',
value: cardId
},
{
column: 'kind',
value: kind
},
{
column: 'value',
value
}
]
if (filter.length === 0) return
const { sql, values } = this.getDeleteSql(Domain.Peer, filter)
await this.execute(sql, values, 'remove peer')
}
async findPeers (params: FindPeersParams): Promise<Peer[]> {
const select = `SELECT p.*, COALESCE(members.members, '[]') AS members
FROM ${Domain.Peer} p`
const { where, values } = this.buildWhere(params)
const limit = params.limit != null ? `LIMIT ${params.limit}` : ''
const orderBy =
params.order != null ? `ORDER BY p.created ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}` : ''
const join = `LEFT JOIN LATERAL (
SELECT json_agg(
json_build_object(
'workspace_id', p2.workspace_id,
'card_id', p2.card_id,
'extra', p2.extra
)
) AS members
FROM ${Domain.Peer} AS p2
WHERE p2.value = p.value
AND p2.kind = 'card'
AND NOT (p2.workspace_id = p.workspace_id AND p2.card_id = p.card_id)
) members ON true`
const sql = [select, join, where, orderBy, limit].join(' ')
const result = await this.execute(sql, values, 'find peers')
return result.map((it: any) => toPeer(it))
}
buildWhere (params: FindPeersParams, startIndex: number = 0, prefix = 'p.'): { where: string, values: any[] } {
const where: string[] = []
const values: any[] = []
let index = startIndex + 1
if (params.workspaceId != null) {
where.push(`${prefix}workspace_id = $${index++}::uuid`)
values.push(params.workspaceId)
}
if (params.cardId != null) {
where.push(`${prefix}card_id = $${index++}::varchar`)
values.push(params.cardId)
}
if (params.kind != null) {
where.push(`${prefix}kind = $${index++}::varchar`)
values.push(params.kind)
}
if (params.value != null) {
where.push(`${prefix}value = $${index++}::varchar`)
values.push(params.value)
}
return { where: where.length > 0 ? `WHERE ${where.join(' AND ')}` : '', values }
}
}
@@ -1,59 +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 function getCondition (
table: string,
dbField: string,
startIndex: number,
param: any,
type: string
): { where: string, values: any[], index: number } | undefined {
const conditions: string[] = []
const values: any[] = []
let index = startIndex
if (param !== null && typeof param === 'object') {
if (param.less !== undefined) {
conditions.push(`${table}.${dbField} < $${index}::${type}`)
values.push(param.less)
index++
}
if (param.lessOrEqual !== undefined) {
conditions.push(`${table}.${dbField} <= $${index}::${type}`)
values.push(param.lessOrEqual)
index++
}
if (param.greater !== undefined) {
conditions.push(`${table}.${dbField} > $${index}::${type}`)
values.push(param.greater)
index++
}
if (param.greaterOrEqual !== undefined) {
conditions.push(`${table}.${dbField} >= $${index}::${type}`)
values.push(param.greaterOrEqual)
index++
}
}
if (param != null && conditions.length === 0) {
conditions.push(`${table}.${dbField} = $${index}::${type}`)
values.push(param)
index++
}
if (conditions.length === 0) return undefined
return { where: conditions.join(' AND '), values, index }
}
@@ -1,16 +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 './adapter'
@@ -1,662 +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 postgres from 'postgres'
import { Domain } from '@hcengineering/communication-sdk-types'
/* eslint-disable @typescript-eslint/naming-convention */
const migrationsDomain = 'communication._migrations'
let isSchemaInitialized = false
let initPromise: Promise<void> | null = null
export function isInitialized (): boolean {
return isSchemaInitialized
}
export async function initSchema (sql: postgres.Sql): Promise<void> {
if (isInitialized()) return
if (initPromise == null) {
initPromise = (async () => {
const maxAttempts = 3
const retryDelay = 3000
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
await init(sql)
isSchemaInitialized = true
return
} catch (err) {
if (attempt === maxAttempts) {
throw err
}
console.warn(`InitSchema attempt ${attempt} failed, retrying in ${retryDelay}ms…`, err)
await delay(retryDelay)
}
}
})()
.catch((err) => {
throw err
})
.finally(() => {
initPromise = null
})
}
await initPromise
}
function delay (ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
}
async function init (sql: postgres.Sql): Promise<void> {
if (isSchemaInitialized) return
const start = performance.now()
console.log('🗃️ Initializing schema...')
await sql.unsafe('CREATE SCHEMA IF NOT EXISTS communication;')
await sql.unsafe(`CREATE TABLE IF NOT EXISTS ${migrationsDomain}
(
name VARCHAR(255) NOT NULL,
created_on TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (name)
)`)
const appliedMigrations = await sql.unsafe(`SELECT name
FROM ${migrationsDomain}`)
const appliedNames = appliedMigrations.map((it) => it.name)
const migrations = getMigrations()
for (const [name, sqlString] of migrations) {
if (appliedNames.includes(name)) continue
try {
await sql.unsafe(sqlString)
await sql.unsafe(
`INSERT INTO ${migrationsDomain}(name)
VALUES ($1::varchar);`,
[name]
)
console.log(`✅ Migration ${name} applied`)
} catch (err) {
console.error(`❌ Failed on ${name}:`, err)
throw err
}
}
isSchemaInitialized = true
const end = performance.now()
const resTime = (end - start) / 1000
console.log(`🎉 All migrations complete in ${resTime.toFixed(2)} sec`)
}
function getMigrations (): [string, string][] {
return [
migrationV1_1(),
migrationV1_2(),
migrationV2_1(),
migrationV2_2(),
migrationV2_3(),
migrationV2_4(),
migrationV2_5(),
migrationV2_6(),
migrationV2_7(),
migrationV4_1(),
migrationV5_1(),
migrationV5_2(),
migrationV6_1(),
migrationV6_2(),
migrationV6_3(),
migrationV6_4(),
migrationV6_5(),
migrationV6_6(),
migrationV6_7(),
migrationV6_8(),
migrationV7_1(),
migrationV7_2(),
migrationV7_3(),
migrationV8_1(),
migrationV8_2(),
migrationV8_3(),
migrationV9_1(),
migrationV10_1(),
migrationV10_2(),
migrationV10_3(),
migrationV10_4(),
// migrationV10_5(),
migrationV10_6(),
migrationV10_7(),
migrationV10_8(),
migrationV10_9(),
migrationV10_10(),
migrationV10_11(),
migrationV10_12(),
migrationV10_13(),
migrationV10_14(),
migrationV10_15(),
migrationV10_16()
]
}
function migrationV1_1 (): [string, string] {
const sql = `
DROP SCHEMA IF EXISTS communication CASCADE;
CREATE SCHEMA IF NOT EXISTS communication;
CREATE TABLE IF NOT EXISTS ${migrationsDomain}
(
name VARCHAR(255) NOT NULL,
created_on TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (name)
)
`
return ['recreate_schema-v1_1', sql]
}
function migrationV1_2 (): [string, string] {
const sql = `
CREATE TABLE IF NOT EXISTS communication.messages_groups
(
workspace_id UUID NOT NULL,
card_id VARCHAR(255) NOT NULL,
blob_id UUID NOT NULL,
from_date TIMESTAMPTZ NOT NULL,
to_date TIMESTAMPTZ NOT NULL,
count INT NOT NULL,
PRIMARY KEY (workspace_id, card_id, blob_id)
);
CREATE INDEX IF NOT EXISTS idx_messages_groups_workspace_card ON communication.messages_groups (workspace_id, card_id);
CREATE TABLE IF NOT EXISTS communication.thread
(
workspace_id UUID NOT NULL,
card_id VARCHAR(255) NOT NULL,
message_id INT8 NOT NULL,
thread_id VARCHAR(255) NOT NULL,
thread_type VARCHAR(255) NOT NULL,
replies_count INT NOT NULL,
last_reply TIMESTAMPTZ NOT NULL,
message_created TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (workspace_id, thread_id),
CONSTRAINT thread_unique_constraint UNIQUE (workspace_id, card_id, message_id)
);
CREATE INDEX IF NOT EXISTS idx_thread_workspace_thread_message ON communication.thread (workspace_id, thread_id);
CREATE INDEX IF NOT EXISTS idx_thread_workspace_card_message ON communication.thread (workspace_id, card_id, message_id);
CREATE TABLE IF NOT EXISTS communication.notification_context
(
id INT8 NOT NULL DEFAULT unique_rowid(),
workspace_id UUID NOT NULL,
card_id VARCHAR(255) NOT NULL,
account UUID NOT NULL,
last_view TIMESTAMPTZ NOT NULL DEFAULT now(),
last_update TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (id),
UNIQUE (workspace_id, card_id, account)
);
CREATE TABLE IF NOT EXISTS communication.notifications
(
id INT8 NOT NULL DEFAULT unique_rowid(),
context_id INT8 NOT NULL,
read BOOLEAN NOT NULL DEFAULT false,
message_id INT8 NOT NULL,
created TIMESTAMPTZ NOT NULL,
content JSONB NOT NULL DEFAULT '{}',
PRIMARY KEY (id),
FOREIGN KEY (context_id) REFERENCES communication.notification_context (id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS notification_context_idx ON communication.notifications (context_id);
CREATE TABLE IF NOT EXISTS communication.collaborators
(
workspace_id UUID NOT NULL,
card_id VARCHAR(255) NOT NULL,
account UUID NOT NULL,
date TIMESTAMPTZ NOT NULL DEFAULT now(),
card_type VARCHAR(255) NOT NULL,
PRIMARY KEY (workspace_id, card_id, account)
);
CREATE TABLE IF NOT EXISTS communication.label
(
workspace_id UUID NOT NULL,
label_id VARCHAR(255) NOT NULL,
card_id VARCHAR(255) NOT NULL,
card_type VARCHAR(255) NOT NULL,
account UUID NOT NULL,
created TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (workspace_id, card_id, label_id, account)
);
`
return ['reinit_tables-v1_2', sql]
}
function migrationV2_1 (): [string, string] {
const sql = `
ALTER TABLE communication.notifications
ADD COLUMN IF NOT EXISTS type VARCHAR(255) NOT NULL DEFAULT 'message';
`
return ['add_type_column_to_notifications-v2_1', sql]
}
function migrationV2_2 (): [string, string] {
const sql = `
ALTER TABLE communication.notifications
ADD COLUMN IF NOT EXISTS read BOOLEAN NOT NULL DEFAULT false;
`
return ['add_read_column_to_notifications-v2_2', sql]
}
function migrationV2_3 (): [string, string] {
const sql = 'UPDATE communication.notifications SET read = true; '
return ['set_read_to_true-v2_3', sql]
}
function migrationV2_4 (): [string, string] {
const sql = `
ALTER TABLE communication.notifications
ADD COLUMN IF NOT EXISTS message_created TIMESTAMPTZ;
`
return ['add_message_created_column_to_notifications-v2_4', sql]
}
function migrationV2_5 (): [string, string] {
const sql = `
UPDATE communication.notifications
SET message_created = created;
ALTER TABLE communication.notifications
ALTER COLUMN message_created SET NOT NULL;
`
return ['init_and_set_not_null_message_created_notifications-v2_4', sql]
}
function migrationV2_6 (): [string, string] {
const sql = `
ALTER TABLE communication.notification_context
ADD COLUMN IF NOT EXISTS last_notify TIMESTAMPTZ;
`
return ['add_last_notify_column_to_notification_context-v2_6', sql]
}
function migrationV2_7 (): [string, string] {
const sql = `
UPDATE communication.notification_context
SET last_notify = last_update;`
return ['set_last_notify_to_last_update-v2_7', sql]
}
function migrationV4_1 (): [string, string] {
const sql = `
CREATE INDEX IF NOT EXISTS notifications_context_id_read_created_desc_idx ON communication.notifications (context_id, read, created DESC);
`
return ['add_index_notifications_context_id_read_created_desc-v4_1', sql]
}
function migrationV5_1 (): [string, string] {
const sql = `
ALTER TABLE communication.thread
DROP COLUMN IF EXISTS message_created;
`
return ['remove_unused-columns-v5_1', sql]
}
function migrationV5_2 (): [string, string] {
const sql = `
CREATE TABLE IF NOT EXISTS communication.message_created
(
workspace_id UUID NOT NULL,
card_id VARCHAR(255) NOT NULL,
message_id INT8 NOT NULL,
created TIMESTAMPTZ NOT NULL,
PRIMARY KEY (workspace_id, card_id, message_id)
);
`
return ['init_message_created_table-v5_2', sql]
}
function migrationV6_1 (): [string, string] {
const sql = `
ALTER TABLE communication.thread
ADD COLUMN IF NOT EXISTS message_id_str VARCHAR(22);
ALTER TABLE communication.notifications
ADD COLUMN IF NOT EXISTS message_id_str VARCHAR(22);
ALTER TABLE communication.message_created
ADD COLUMN IF NOT EXISTS message_id_str VARCHAR(22);
`
return ['add_message_id_str_columns-v6_1', sql]
}
function migrationV6_2 (): [string, string] {
const sql = `
UPDATE communication.thread
SET message_id_str = message_id::text;
UPDATE communication.notifications
SET message_id_str = message_id::text;
UPDATE communication.message_created
SET message_id_str = message_id::text;
`
return ['copy_int8_ids_to_str_columns-v6_2', sql]
}
function migrationV6_3 (): [string, string] {
const sql = `
DROP INDEX IF EXISTS communication.thread_unique_constraint CASCADE;
DROP INDEX IF EXISTS communication.idx_thread_workspace_card_message;
DROP INDEX IF EXISTS communication.notifications_context_id_read_created_desc_idx;
DROP INDEX IF EXISTS communication.notifications_type_storing_rec_idx;
`
return ['drop_constraints_and_indexes_for_rename-v6_3', sql]
}
function migrationV6_4 (): [string, string] {
const sql = `
ALTER TABLE communication.thread
RENAME COLUMN message_id TO message_id_old;
ALTER TABLE communication.thread
RENAME COLUMN message_id_str TO message_id;
ALTER TABLE communication.notifications
RENAME COLUMN message_id TO message_id_old;
ALTER TABLE communication.notifications
RENAME COLUMN message_id_str TO message_id;
ALTER TABLE communication.message_created
RENAME COLUMN message_id TO message_id_old;
ALTER TABLE communication.message_created
RENAME COLUMN message_id_str TO message_id;
`
return ['rename_message_id_columns-v6_4', sql]
}
function migrationV6_5 (): [string, string] {
const sql = `
ALTER TABLE communication.message_created
ALTER COLUMN message_id SET NOT NULL;
ALTER TABLE communication.thread
ALTER COLUMN message_id SET NOT NULL;
ALTER TABLE communication.notifications
ALTER COLUMN message_id SET NOT NULL;
`
return ['make_message_id_not_null-v6_5', sql]
}
function migrationV6_6 (): [string, string] {
const sql = `
ALTER TABLE communication.message_created
ALTER PRIMARY KEY USING COLUMNS (workspace_id, card_id, message_id);
`
return ['recrate_primary_keys-v6_6', sql]
}
function migrationV6_7 (): [string, string] {
const sql = `
ALTER TABLE communication.thread
ADD CONSTRAINT thread_unique_constraint UNIQUE (workspace_id, card_id, message_id);
CREATE INDEX IF NOT EXISTS idx_thread_workspace_card_message
ON communication.thread (workspace_id, card_id, message_id);
CREATE INDEX IF NOT EXISTS notifications_context_id_read_created_desc_idx
ON communication.notifications (context_id, read, created DESC);
`
return ['recreate_constraints_and_indexes-v6_7', sql]
}
function migrationV6_8 (): [string, string] {
const sql = `
ALTER TABLE communication.thread
DROP COLUMN IF EXISTS message_id_old;
ALTER TABLE communication.notifications
DROP COLUMN IF EXISTS message_id_old;
ALTER TABLE communication.message_created
DROP COLUMN IF EXISTS message_id_old;
`
return ['drop_old_message_id_columns-v6_8', sql]
}
function migrationV7_1 (): [string, string] {
const sql = `
ALTER TABLE communication.notifications
ADD COLUMN IF NOT EXISTS blob_id UUID;
`
return ['add_blobId_to_notifications-v7_1', sql]
}
function migrationV7_2 (): [string, string] {
const sql = `
UPDATE communication.notifications AS n
SET blob_id = mg.blob_id
FROM communication.notification_context AS nc
JOIN communication.messages_groups AS mg
ON mg.workspace_id = nc.workspace_id
AND mg.card_id = nc.card_id
WHERE n.context_id = nc.id
AND n.message_created BETWEEN mg.from_date AND mg.to_date
AND n.blob_id IS NULL;
`
return ['fill_blobId_on_notifications-v7_2', sql]
}
function migrationV7_3 (): [string, string] {
const sql = `
UPDATE communication.notification_context
SET last_notify = last_update
WHERE last_notify IS NULL;
ALTER TABLE communication.notification_context
ALTER COLUMN last_notify SET NOT NULL;
`
return ['make_last_notify_not_null-v7_3', sql]
}
function migrationV8_1 (): [string, string] {
const sql = `
ALTER TABLE communication.messages_groups
RENAME TO messages_group;
ALTER TABLE communication.notifications
RENAME TO notification;
ALTER TABLE communication.collaborators
RENAME TO collaborator;
`
return ['rename_tables-v8_1', sql]
}
function migrationV8_2 (): [string, string] {
const sql = `
CREATE TABLE IF NOT EXISTS communication.attachment
(
workspace_id UUID NOT NULL,
card_id VARCHAR NOT NULL,
message_id VARCHAR NOT NULL,
id UUID NOT NULL,
type TEXT NOT NULL,
params JSONB NOT NULL,
creator VARCHAR NOT NULL,
created TIMESTAMPTZ NOT NULL,
modified TIMESTAMPTZ,
PRIMARY KEY (workspace_id, card_id, message_id, id)
);
`
return ['create_attachment_table-v8_2', sql]
}
function migrationV8_3 (): [string, string] {
const sql = `
CREATE INDEX IF NOT EXISTS attachment_workspace_card_message_idx ON communication.attachment (workspace_id, card_id, message_id)
`
return ['add_attachment_indexes-v8_3', sql]
}
function migrationV9_1 (): [string, string] {
const sql = `
CREATE TABLE IF NOT EXISTS ${Domain.Peer}
(
workspace_id UUID NOT NULL,
card_id VARCHAR(255) NOT NULL,
kind TEXT NOT NULL,
value TEXT NOT NULL,
extra JSONB NOT NULL DEFAULT '{}',
created TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (workspace_id, card_id, kind, value)
);
CREATE INDEX IF NOT EXISTS peer_workspace_card_kind ON ${Domain.Peer} (workspace_id, card_id, kind);
CREATE INDEX IF NOT EXISTS peer_kind_value ON ${Domain.Peer} (kind, value);`
return ['init_peer_tables-v9_1', sql]
}
function migrationV10_1 (): [string, string] {
const sql = `
ALTER TABLE communication.message_created
RENAME TO ${Domain.MessageIndex};
ALTER TABLE communication.thread
RENAME TO ${Domain.ThreadIndex};
`
return ['rename_tables-v10_1', sql]
}
function migrationV10_2 (): [string, string] {
const sql = `
ALTER TABLE ${Domain.MessageIndex}
ADD COLUMN IF NOT EXISTS creator VARCHAR(255);
`
return ['add_creator_to_message_index-v10_2', sql]
}
function migrationV10_3 (): [string, string] {
const sql = `
UPDATE ${Domain.MessageIndex}
SET creator = '';
`
return ['set_creator_to_empty-string-v10_3', sql]
}
function migrationV10_4 (): [string, string] {
const sql = `
ALTER TABLE ${Domain.MessageIndex}
ALTER COLUMN creator SET NOT NULL;
`
return ['make_creator_not_null-v10_4', sql]
}
// function migrationV10_5 (): [string, string] {
// const sql = `
// ALTER TABLE ${Domain.ThreadIndex}
// DROP COLUMN IF EXISTS replies_count;
// ALTER TABLE ${Domain.ThreadIndex}
// DROP COLUMN IF EXISTS last_reply;
// `
// return ['drop_thread_index_columns-v10_5', sql]
// }
function migrationV10_6 (): [string, string] {
const sql = `
ALTER TABLE ${Domain.MessageIndex}
ADD COLUMN IF NOT EXISTS blob_id UUID;
`
return ['add_blob_id-v10_6', sql]
}
function migrationV10_7 (): [string, string] {
const sql = `
UPDATE ${Domain.MessageIndex}
SET blob_id = '00000000-0000-0000-0000-000000000000';
`
return ['set_blob_id-v10_7', sql]
}
function migrationV10_8 (): [string, string] {
const sql = `
ALTER TABLE ${Domain.MessageIndex}
ALTER COLUMN blob_id SET NOT NULL;
`
return ['make_column_blob_id_not_null-v10_8', sql]
}
function migrationV10_9 (): [string, string] {
const sql = `
ALTER TABLE ${Domain.Notification}
DROP COLUMN IF EXISTS message_created;
`
return ['notification_drop_message_created-v10_9', sql]
}
function migrationV10_10 (): [string, string] {
const sql = `
UPDATE ${Domain.Notification}
SET blob_id = '00000000-0000-0000-0000-000000000000'
WHERE blob_id IS NULL;
`
return ['set_blob_id-v10_10', sql]
}
function migrationV10_11 (): [string, string] {
const sql = `
ALTER TABLE ${Domain.Notification}
ALTER COLUMN blob_id SET NOT NULL;
`
return ['make_column_blob_id_not_null-v10_11', sql]
}
function migrationV10_12 (): [string, string] {
const sql = `
ALTER TABLE ${Domain.Notification}
ADD COLUMN IF NOT EXISTS creator VARCHAR(255);
`
return ['make_column_creator_not_null-v10_12', sql]
}
function migrationV10_13 (): [string, string] {
const sql = `
UPDATE ${Domain.Notification}
SET creator = '';
`
return ['set_creator_to_empty-string-v10_13', sql]
}
function migrationV10_14 (): [string, string] {
const sql = `
ALTER TABLE ${Domain.Notification}
ALTER COLUMN creator SET NOT NULL;
`
return ['make_column_creator_not_null-v10_14', sql]
}
function migrationV10_15 (): [string, string] {
const sql = `
DROP INDEX IF EXISTS communication.thread_unique_constraint CASCADE;
`
return ['drop_thread_unique_constraint-v10_15', sql]
}
function migrationV10_16 (): [string, string] {
const sql = `
ALTER TABLE ${Domain.ThreadIndex}
ADD COLUMN IF NOT EXISTS replies_count INT;
ALTER TABLE ${Domain.ThreadIndex}
ADD COLUMN IF NOT EXISTS last_reply TIMESTAMPTZ;
`
return ['add_thread_index_columns-v10_16', sql]
}
@@ -1,193 +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 BlobID,
type CardID,
type ContextID,
type MessageID,
type SocialID,
type WorkspaceUuid,
type NotificationID,
type LabelID,
type CardType,
NotificationContent,
NotificationType,
PeerKind, PeerExtra
} from '@hcengineering/communication-types'
import { Domain } from '@hcengineering/communication-sdk-types'
export const schemas = {
[Domain.MessageIndex]: {
workspace_id: 'uuid',
card_id: 'varchar',
message_id: 'varchar',
created: 'timestamptz',
creator: 'varchar',
blob_id: 'uuid'
},
[Domain.ThreadIndex]: {
workspace_id: 'uuid',
card_id: 'varchar',
message_id: 'varchar',
thread_id: 'varchar',
thread_type: 'varchar',
replies_count: 'int',
last_reply: 'timestamptz'
},
[Domain.Notification]: {
id: 'int8',
context_id: 'int8',
message_id: 'varchar',
creator: 'varchar',
blob_id: 'uuid',
created: 'timestamptz',
content: 'jsonb',
type: 'varchar',
read: 'bool'
},
[Domain.Collaborator]: {
workspace_id: 'uuid',
card_id: 'varchar',
account: 'uuid',
date: 'timestamptz',
card_type: 'varchar'
},
[Domain.Label]: {
workspace_id: 'uuid',
card_id: 'varchar',
card_type: 'varchar',
label_id: 'varchar',
account: 'uuid',
created: 'timestamptz'
},
[Domain.NotificationContext]: {
workspace_id: 'uuid',
card_id: 'varchar',
id: 'int8',
account: 'uuid',
last_view: 'timestamptz',
last_update: 'timestamptz',
last_notify: 'timestamptz'
},
[Domain.Peer]: {
workspace_id: 'uuid',
card_id: 'varchar',
kind: 'varchar',
value: 'varchar',
extra: 'jsonb',
created: 'timestamptz'
}
} as const
export interface DomainDbModel {
[Domain.MessageIndex]: MessageIndexDbModel
[Domain.ThreadIndex]: ThreadDbModel
[Domain.Notification]: NotificationDbModel
[Domain.NotificationContext]: ContextDbModel
[Domain.Collaborator]: CollaboratorDbModel
[Domain.Label]: LabelDbModel
[Domain.Peer]: PeerDbModel
}
export type DbModel<D extends keyof DomainDbModel> = DomainDbModel[D]
export type DbModelColumn<D extends Domain> = keyof DomainDbModel[D] & string
export type DbModelColumnType<D extends Domain> = DomainDbModel[D][DbModelColumn<D>]
export interface DbModelFilterRow<D extends Domain> { column: DbModelColumn<D>, value: DbModelColumnType<D> | DbModelColumnType<D>[] }
export type DbModelFilter<D extends Domain> = Array<DbModelFilterRow<D>>
export type DbModelUpdate<D extends Domain> = Array<{
column: DbModelColumn<D>
innerKey?: string
value: any
}>
export type DbModelBatchUpdate<D extends Domain> = Array<{
key: DbModelColumnType<D>
column: DbModelColumn<D>
innerKey?: string
value: any
}>
interface MessageIndexDbModel {
workspace_id: WorkspaceUuid
card_id: CardID
message_id: MessageID
created: Date
creator: SocialID
blob_id: BlobID
}
interface ThreadDbModel {
workspace_id: WorkspaceUuid
card_id: CardID
message_id: MessageID
thread_id: CardID
thread_type: CardType
replies_count: number
last_reply: Date
}
interface NotificationDbModel {
id: NotificationID
type: NotificationType
read: boolean
message_id: MessageID
creator: SocialID
blob_id: BlobID
context_id: ContextID
created: Date
content: NotificationContent
}
interface ContextDbModel {
id: ContextID
workspace_id: WorkspaceUuid
card_id: CardID
account: AccountUuid
last_update: Date
last_view: Date
last_notify: Date
}
interface CollaboratorDbModel {
workspace_id: WorkspaceUuid
card_id: CardID
card_type: CardType
account: AccountUuid
date: Date
}
interface LabelDbModel {
workspace_id: WorkspaceUuid
label_id: LabelID
card_id: CardID
card_type: CardType
account: AccountUuid
created: Date
}
interface PeerDbModel {
workspace_id: WorkspaceUuid
card_id: CardID
kind: PeerKind
value: string
extra: PeerExtra
created: Date
}
@@ -1,30 +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 ParameterOrJSON, type Row } from 'postgres'
export type SqlRow = Row & Iterable<Row>
export type SqlParams = ParameterOrJSON<any>[]
export type SqlResult<T extends SqlRow = SqlRow> = T[] & { count: number }
export interface Logger {
info: (message: string, data?: Record<string, any>) => void
warn: (message: string, data?: Record<string, any>) => void
error: (message: string, data?: Record<string, any>) => void
}
export interface Options {
withLogs?: boolean
}
@@ -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 function injectVars (sql: string, values: any[]): string {
return sql.replaceAll(/(\$\d+)/g, (_, idx) => {
return escape(values[parseInt(idx.substring(1)) - 1])
})
}
function escape (value: any): string {
if (value === null || value === undefined) {
return 'NULL'
}
if (Array.isArray(value)) {
return 'ARRAY[' + value.map(escape).join(',') + ']'
}
if (value instanceof Date) {
return `'${value.toISOString()}'`
}
switch (typeof value) {
case 'number':
if (isNaN(value) || !isFinite(value)) {
throw new Error('Invalid number value')
}
return value.toString()
case 'boolean':
return value ? 'TRUE' : 'FALSE'
case 'string':
return `'${value.replace(/'/g, "''")}'`
case 'bigint':
return value.toString()
case 'object': {
const json = JSON.stringify(value).replace(/'/g, "''")
return `'${json}'`
}
default:
throw new Error(`Unsupported value type: ${typeof value}`)
}
}
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(',')}}`
})
}
const SEP = ','
function getFirstName (name: string): string {
return name !== undefined ? name.substring(name.indexOf(SEP) + 1) : ''
}
function getLastName (name: string): string {
return name !== undefined ? name.substring(0, name.indexOf(SEP)) : ''
}
export function formatName (name: string): string {
return getFirstName(name) + ' ' + getLastName(name)
}
@@ -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"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "lib", "dist", "types", "bundle"]
}
@@ -1,7 +0,0 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}
@@ -1,61 +0,0 @@
{
"name": "@hcengineering/communication-query",
"entries": [
{
"version": "0.7.11",
"tag": "@hcengineering/communication-query_v0.7.11",
"date": "Mon, 27 Oct 2025 16:28:25 GMT",
"comments": {
"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.8",
"tag": "@hcengineering/communication-query_v0.7.8",
"date": "Sat, 25 Oct 2025 22:20:35 GMT",
"comments": {
"dependency": [
{
"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-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-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`"
}
]
}
}
]
}
@@ -1,22 +0,0 @@
# Change Log - @hcengineering/communication-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
@@ -1,4 +0,0 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig"
}
@@ -1,61 +0,0 @@
{
"name": "@hcengineering/communication-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",
"@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",
"@types/uuid": "^8.3.1"
},
"dependencies": {
"@hcengineering/communication-sdk-types": "workspace:^0.7.12",
"@hcengineering/communication-shared": "workspace:^0.7.11",
"@hcengineering/communication-types": "workspace:^0.7.12",
"@hcengineering/hulylake-client": "workspace:^0.7.17",
"fast-equals": "^5.2.2",
"uuid": "^8.3.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"
}
}
}
@@ -1,185 +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, Collaborator, FindCollaboratorsParams } from '@hcengineering/communication-types'
import {
AddCollaboratorsEvent,
CardEventType,
type Event,
type EventResult,
type FindClient,
NotificationEventType,
type QueryCallback,
RemoveCardEvent,
RemoveCollaboratorsEvent
} from '@hcengineering/communication-sdk-types'
import { type HulylakeWorkspaceClient } from '@hcengineering/hulylake-client'
import { QueryResult } from '../result'
import { QueryOptions, type Query, type QueryId } from '../types'
export class CollaboratorsQuery implements Query<Collaborator, FindCollaboratorsParams> {
private result: Promise<QueryResult<Collaborator>> | QueryResult<Collaborator>
private isCardRemoved = false
constructor (
private readonly client: FindClient,
private readonly hulylake: HulylakeWorkspaceClient,
public readonly id: QueryId,
public readonly params: FindCollaboratorsParams,
public readonly options: QueryOptions | undefined,
private callback?: QueryCallback<Collaborator>,
initialResult?: QueryResult<Collaborator>
) {
if (initialResult !== undefined) {
this.result = initialResult
void this.notify()
} else {
this.result = this.initResult()
void this.result.then(() => {
void this.notify()
})
}
}
async onEvent (event: Event): Promise<void> {
if (this.isCardRemoved) return
switch (event.type) {
case NotificationEventType.AddCollaborators:
await this.onCollaboratorsAdded(event)
break
case NotificationEventType.RemoveCollaborators:
await this.onCollaboratorsRemoved(event)
break
case CardEventType.RemoveCard:
await this.onCardRemoved(event)
break
}
}
async onCollaboratorsAdded (event: AddCollaboratorsEvent): Promise<void> {
if (event.cardId !== this.params.cardId || event.collaborators.length === 0) return
if (this.result instanceof Promise) this.result = await this.result
let updated = false
for (const account of event.collaborators) {
if (this.params.limit != null && this.result.length >= this.params.limit) break
const existing = this.result.get(account)
if (existing != null) continue
const match = this.match(account)
if (!match) continue
updated = true
this.result.push({
account,
cardId: event.cardId,
cardType: event.cardType
})
}
if (updated) {
void this.notify()
}
}
async onCollaboratorsRemoved (event: RemoveCollaboratorsEvent): Promise<void> {
if (event.cardId !== this.params.cardId || event.collaborators.length === 0) return
if (this.result instanceof Promise) this.result = await this.result
const prevLength = this.result.length
let deleted = false
for (const account of event.collaborators) {
const d = this.result.delete(account)
deleted = deleted || d !== undefined
}
if (!deleted) return
if (this.params.limit != null && this.result.length < this.params.limit && prevLength >= this.params.limit) {
const collaborators = await this.find(this.params)
this.result = new QueryResult(collaborators, (c) => c.account)
}
void this.notify()
}
async onCardRemoved (event: RemoveCardEvent): Promise<void> {
if (this.params.cardId !== event.cardId) return
if (this.result instanceof Promise) this.result = await this.result
this.isCardRemoved = true
this.result.deleteAll()
void this.notify()
}
async onRequest (event: Event, promise: Promise<EventResult>): Promise<void> {}
private async initResult (): Promise<QueryResult<Collaborator>> {
try {
const res = await this.find(this.params)
return new QueryResult(res, (c) => c.account)
} catch (error) {
console.error('Failed to initialize query:', error)
return new QueryResult([] as Collaborator[], (c) => c.account)
}
}
async unsubscribe (): Promise<void> {}
removeCallback (): void {
this.callback = () => {}
}
setCallback (callback: QueryCallback<Collaborator>): void {
this.callback = callback
void this.notify()
}
copyResult (): QueryResult<Collaborator> | undefined {
if (this.result instanceof Promise) {
return undefined
}
return this.result.copy()
}
private async find (params: FindCollaboratorsParams): Promise<Collaborator[]> {
if (this.isCardRemoved) return []
return await this.client.findCollaborators(params, this.id)
}
private async notify (): Promise<void> {
if (this.callback == null) return
if (this.result instanceof Promise) this.result = await this.result
const result = this.result.getResult()
this.callback(result)
}
private match (account: AccountUuid): boolean {
if (this.params.account != null) {
const accounts = Array.isArray(this.params.account) ? this.params.account : [this.params.account]
if (!accounts.includes(account)) {
return false
}
}
return true
}
public async refresh (): Promise<void> {
this.result = new QueryResult([] as Collaborator[], (c) => c.account)
this.result = this.initResult()
void this.result.then(() => {
void this.notify()
})
}
}
@@ -1,4 +0,0 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig"
}
@@ -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 './lq'
export type { MessageQueryParams, NotificationQueryParams, MessageQueryOptions, QueryOptions, NotificationContextQueryOptions } from './types'
@@ -1,261 +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 { FindLabelsParams, Label } from '@hcengineering/communication-types'
import {
CardEventType,
CreateLabelEvent,
type Event,
type EventResult,
type FindClient,
LabelEventType,
type QueryCallback,
RemoveCardEvent,
RemoveLabelEvent,
UpdateCardTypeEvent
} from '@hcengineering/communication-sdk-types'
import { type HulylakeWorkspaceClient } from '@hcengineering/hulylake-client'
import { QueryResult } from '../result'
import { type Query, type QueryId, QueryOptions } from '../types'
function getId (label: Label): string {
return `${label.labelId}:${label.cardId}:${label.account}`
}
export class LabelsQuery implements Query<Label, FindLabelsParams> {
private result: Promise<QueryResult<Label>> | QueryResult<Label>
private isCardRemoved = false
constructor (
private readonly client: FindClient,
private readonly hulylake: HulylakeWorkspaceClient,
public readonly id: QueryId,
public readonly params: FindLabelsParams,
public readonly options: QueryOptions | undefined,
private callback?: QueryCallback<Label>,
initialResult?: QueryResult<Label>
) {
if (initialResult !== undefined) {
this.result = initialResult
void this.notify()
} else {
this.result = this.initResult()
void this.result.then(() => {
void this.notify()
})
}
}
async onEvent (event: Event): Promise<void> {
if (this.isCardRemoved) return
switch (event.type) {
case LabelEventType.CreateLabel:
await this.onLabelCreated(event)
break
case LabelEventType.RemoveLabel:
await this.onLabelRemoved(event)
break
case CardEventType.UpdateCardType:
await this.onCardTypeUpdated(event)
break
case CardEventType.RemoveCard:
await this.onCardRemoved(event)
break
}
}
async onLabelCreated (event: CreateLabelEvent): Promise<void> {
if (this.result instanceof Promise) this.result = await this.result
if (this.params.limit != null && this.result.length >= this.params.limit) return
const label: Label = {
labelId: event.labelId,
cardId: event.cardId,
cardType: event.cardType,
account: event.account,
created: event.date ?? new Date()
}
const match = this.match(label)
if (!match) return
const existing = this.result.get(getId(label))
if (existing != null) return
this.result.push(label)
void this.notify()
}
async onLabelRemoved (event: RemoveLabelEvent): Promise<void> {
if (this.result instanceof Promise) this.result = await this.result
const existing = this.result
.getResult()
.find((it) => it.account === event.account && it.cardId === event.cardId && it.labelId === event.labelId)
if (existing === undefined) return
const prevLength = this.result.length
this.result.delete(getId(existing))
if (this.params.limit != null && this.result.length < this.params.limit && prevLength >= this.params.limit) {
const labels = await this.find(this.params)
this.result = new QueryResult(labels, getId)
}
void this.notify()
}
async onCardTypeUpdated (event: UpdateCardTypeEvent): Promise<void> {
if (this.result instanceof Promise) this.result = await this.result
const result = this.result.getResult()
const currentLength = this.result.length
let updated = false
if (this.params.cardType != null) {
const cardTypes = Array.isArray(this.params.cardType) ? this.params.cardType : [this.params.cardType]
if (cardTypes.includes(event.cardType)) {
const labels = await this.find(this.params)
this.result = new QueryResult(labels, getId)
void this.notify()
return
}
}
for (const label of result) {
if (label.cardId === event.cardId) {
const updatedLabel: Label = { ...label, cardType: event.cardType }
const matched = this.match(updatedLabel)
if (matched) {
this.result.update(updatedLabel)
} else {
this.result.delete(getId(label))
}
updated = true
}
}
if (updated) {
const newLength = this.result.length
if (this.params.limit != null && newLength < currentLength && newLength >= this.params.limit) {
const labels = await this.find(this.params)
this.result = new QueryResult(labels, getId)
}
void this.notify()
}
}
async onCardRemoved (event: RemoveCardEvent): Promise<void> {
if (this.result instanceof Promise) this.result = await this.result
if (this.params.cardId === event.cardId) {
this.isCardRemoved = true
this.result.deleteAll()
void this.notify()
return
}
const result = this.result.getResult()
const prevLength = this.result.length
let deleted = false
for (const label of result) {
if (label.cardId === event.cardId) {
this.result.delete(label.labelId)
deleted = true
}
}
if (deleted) {
if (this.params.limit != null && this.result.length < this.params.limit && prevLength >= this.params.limit) {
const labels = await this.find(this.params)
this.result = new QueryResult(labels, getId)
}
void this.notify()
}
}
async onRequest (event: Event, promise: Promise<EventResult>): Promise<void> {}
private async initResult (): Promise<QueryResult<Label>> {
try {
const res = await this.find(this.params)
return new QueryResult(res, getId)
} catch (error) {
console.error('Failed to initialize query:', error)
return new QueryResult([] as Label[], getId)
}
}
async unsubscribe (): Promise<void> {}
removeCallback (): void {
this.callback = () => {}
}
setCallback (callback: QueryCallback<Label>): void {
this.callback = callback
void this.notify()
}
copyResult (): QueryResult<Label> | undefined {
if (this.result instanceof Promise) {
return undefined
}
return this.result.copy()
}
private async find (params: FindLabelsParams): Promise<Label[]> {
if (this.isCardRemoved) return []
return await this.client.findLabels(params, this.id)
}
private async notify (): Promise<void> {
if (this.callback == null) return
if (this.result instanceof Promise) this.result = await this.result
const result = this.result.getResult()
this.callback(result)
}
private match (label: Label): boolean {
if (this.params.account != null && this.params.account !== label.account) {
return false
}
if (this.params.cardId != null && this.params.cardId !== label.cardId) {
return false
}
if (this.params.labelId != null) {
const labels = Array.isArray(this.params.labelId) ? this.params.labelId : [this.params.labelId]
if (!labels.includes(label.labelId)) {
return false
}
}
if (this.params.cardType != null) {
const types = Array.isArray(this.params.cardType) ? this.params.cardType : [this.params.cardType]
if (!types.includes(label.cardType)) {
return false
}
}
return true
}
async refresh (): Promise<void> {
this.result = new QueryResult([] as Label[], getId)
this.result = this.initResult()
void this.result.then(() => {
void this.notify()
})
}
}
@@ -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.
//
import {
type FindNotificationContextParams,
type FindNotificationsParams,
type Message,
type Notification,
type NotificationContext,
type Label,
type FindLabelsParams,
FindCollaboratorsParams,
Collaborator
} from '@hcengineering/communication-types'
import { deepEqual } from 'fast-equals'
import type {
QueryCallback,
Event,
EventResult,
PagedQueryCallback,
FindClient
} from '@hcengineering/communication-sdk-types'
import { type HulylakeWorkspaceClient } from '@hcengineering/hulylake-client'
import type { FindParams, QueryId, AnyQuery, MessageQueryParams, QueryOptions, MessageQueryOptions, NotificationContextQueryOptions } from './types'
import { MessagesQuery } from './messages/query'
import { NotificationQuery } from './notifications/query'
import { NotificationContextsQuery } from './notification-contexts/query'
import { LabelsQuery } from './label/query'
import { CollaboratorsQuery } from './collaborators/query'
interface CreateQueryResult {
unsubscribe: (force: boolean) => void
}
const maxQueriesCache = 100
export class LiveQueries {
private readonly queries = new Map<QueryId, AnyQuery>()
private readonly unsubscribed = new Map<QueryId, number>()
private counter: number = 0
private eventQueue: Promise<void> = Promise.resolve()
constructor (
private readonly client: FindClient,
private readonly hulylake: HulylakeWorkspaceClient
) {
this.client.onEvent = (event) => {
this.eventQueue = this.eventQueue
.then(() => this.onEvent(event))
.catch(err => {
console.error('Error handling event:', err)
})
}
this.client.onRequest = (event, promise) => {
void this.onRequest(event, promise)
}
}
async onEvent (event: Event): Promise<void> {
await Promise.all(Array.from(this.queries.values()).map(q => q.onEvent(event)))
}
async onRequest (event: Event, promise: Promise<EventResult>): Promise<void> {
for (const q of this.queries.values()) {
void q.onRequest(event, promise)
}
}
queryMessages (params: MessageQueryParams, callback: PagedQueryCallback<Message>, options?: MessageQueryOptions): CreateQueryResult {
return this.createAndStoreQuery<Message, MessageQueryParams, MessagesQuery>(
params,
callback,
options,
MessagesQuery,
(params) => this.findMessagesQuery(params, options)
)
}
queryNotifications (params: FindNotificationsParams, callback: PagedQueryCallback<Notification>): CreateQueryResult {
return this.createAndStoreQuery<Notification, FindNotificationsParams, NotificationQuery>(
params,
callback,
undefined,
NotificationQuery,
(params) => this.findNotificationQuery(params)
)
}
queryNotificationContexts (params: FindNotificationContextParams, callback: any, options?: NotificationContextQueryOptions): CreateQueryResult {
return this.createAndStoreQuery<NotificationContext, FindNotificationContextParams, NotificationContextsQuery>(
params,
callback,
options,
NotificationContextsQuery,
(params) => this.findNotificationContextsQuery(params)
)
}
queryLabels (params: FindLabelsParams, callback: any): CreateQueryResult {
return this.createAndStoreQuery<Label, FindLabelsParams, LabelsQuery>(params, callback, undefined, LabelsQuery, (params) =>
this.findLabelsQuery(params)
)
}
queryCollaborators (params: FindCollaboratorsParams, callback: any): CreateQueryResult {
return this.createAndStoreQuery<Collaborator, FindCollaboratorsParams, CollaboratorsQuery>(params, callback, undefined, CollaboratorsQuery, (params) =>
this.findCollaboratorsQuery(params)
)
}
private createAndStoreQuery<T, P extends FindParams, Q extends AnyQuery>(
params: P,
callback: QueryCallback<T> | PagedQueryCallback<T>,
options: QueryOptions | undefined,
QueryClass: new (...args: any[]) => Q,
finder: (params: P) => Q | undefined
): CreateQueryResult {
const query = this.findOrCreateQuery<P, Q>(params, callback, options, QueryClass, finder)
this.queries.set(query.id, query)
return {
unsubscribe: (force) => {
this.unsubscribeQuery(query, force)
}
}
}
private findOrCreateQuery<P, Q extends AnyQuery>(
params: P,
callback: any,
options: QueryOptions | undefined,
QueryClass: new (...args: any[]) => Q,
finder: (params: P) => Q | undefined
): Q {
const id = ++this.counter
const exists = finder(params)
if (exists !== undefined) {
if (this.unsubscribed.has(exists.id)) {
this.unsubscribed.delete(exists.id)
exists.setCallback(callback)
return exists
} else {
const result = exists.copyResult()
return new QueryClass(this.client, this.hulylake, id, params, options, callback, result)
}
}
return new QueryClass(this.client, this.hulylake, id, params, options, callback, undefined)
}
private findQuery<T extends AnyQuery>(params: FindParams, QueryClass: new (...args: any[]) => T, options?: QueryOptions): T | undefined {
for (const query of this.queries.values()) {
if (query instanceof QueryClass && this.compareParams(params, query.params) && this.compareOptions(options, query.options)) {
return query
}
}
}
private findMessagesQuery (params: MessageQueryParams, options?: MessageQueryOptions): MessagesQuery | undefined {
return this.findQuery(params, MessagesQuery, options)
}
private findNotificationQuery (params: FindNotificationsParams): NotificationQuery | undefined {
return this.findQuery(params, NotificationQuery)
}
private findNotificationContextsQuery (params: FindNotificationContextParams): NotificationContextsQuery | undefined {
return this.findQuery(params, NotificationContextsQuery)
}
private findLabelsQuery (params: FindLabelsParams): LabelsQuery | undefined {
return this.findQuery(params, LabelsQuery)
}
private findCollaboratorsQuery (params: FindCollaboratorsParams): CollaboratorsQuery | undefined {
return this.findQuery(params, CollaboratorsQuery)
}
private compareParams (q1: FindParams, q2: FindParams): boolean {
return Object.keys(q1).length === Object.keys(q2).length && deepEqual(q1, q2)
}
private compareOptions (q1: QueryOptions | undefined, q2: QueryOptions | undefined): boolean {
return Object.keys(q1 ?? {}).length === Object.keys(q2 ?? {}).length && deepEqual(q1, q2)
}
private removeOldQueries (): void {
const unsubscribed: [QueryId, number][] = Array.from(this.unsubscribed).sort((a, b) => a[1] - b[1])
for (let i = 0; i < this.unsubscribed.size / 2; i++) {
const u: [QueryId, number] | undefined = unsubscribed.shift()
if (u === undefined) return
this.unsubscribe(u[0])
}
}
private unsubscribe (id: QueryId): void {
const query = this.queries.get(id)
if (query == null) return
void query.unsubscribe()
this.queries.delete(id)
this.unsubscribed.delete(id)
}
private unsubscribeQuery (query: AnyQuery, force: boolean = false): void {
this.unsubscribed.set(query.id, Date.now())
query.removeCallback()
if (this.unsubscribed.size > maxQueriesCache) {
this.removeOldQueries()
}
if (force) {
this.unsubscribe(query.id)
}
}
close (): void {
this.queries.clear()
this.unsubscribed.clear()
}
async refresh (): Promise<void> {
for (const [id, query] of this.queries.entries()) {
if (this.unsubscribed.has(id)) {
this.unsubscribe(id)
continue
}
try {
await query.refresh()
} catch (e) {
console.error('Failed to refresh live query', e, query.id, query.params)
}
}
}
}
@@ -1,902 +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 {
BlobID,
FindMessagesParams,
type Message,
type MessageID,
type MessagesGroup,
MessagesGroupsDoc,
SortingOrder
} from '@hcengineering/communication-types'
import {
CardEventType,
type CreateMessageEvent,
type CreateMessageResult,
type Event,
type EventResult,
type FindClient,
MessageEventType,
type PagedQueryCallback,
PatchEvent,
RemoveCardEvent,
TranslateMessageEvent
} from '@hcengineering/communication-sdk-types'
import { MessageProcessor } from '@hcengineering/communication-shared'
import { v4 as uuid } from 'uuid'
import { type HulylakeWorkspaceClient } from '@hcengineering/hulylake-client'
import { QueryResult } from '../result'
import {
defaultQueryParams,
Direction,
MessageQueryOptions,
type MessageQueryParams,
type OneMessageQueryParams,
type PagedQuery,
type QueryId
} from '../types'
import { WindowImpl } from '../window'
import { loadMessages, loadTranslatedMessages } from '../utils'
export class MessagesQuery implements PagedQuery<Message, MessageQueryParams> {
private result: Promise<QueryResult<Message>> | QueryResult<Message>
private groups: MessagesGroup[] = []
private areGroupsLoaded = false
private groupsPromise: Promise<MessagesGroup[]> | undefined
private initialized = false
nexLoadedPagesCount = 0
prevLoadedPagesCount = 0
private readonly next = {
done: false,
buffer: [] as Message[],
lastGroupIndex: -1
}
private readonly prev = {
done: false,
buffer: [] as Message[],
lastGroupIndex: -1
}
private readonly tmpMessages = new Map<string, MessageID>()
private isCardRemoved = false
private readonly translateBlobs: BlobID[] = []
private translatePromise: Promise<void> | undefined = undefined
constructor (
private readonly client: FindClient,
private readonly hulylake: HulylakeWorkspaceClient,
public readonly id: QueryId,
public readonly params: MessageQueryParams,
public readonly options: MessageQueryOptions | undefined,
private callback: PagedQueryCallback<Message> | undefined,
initialResult: QueryResult<Message> | undefined
) {
const limit = params.limit ?? defaultQueryParams.limit
this.params = {
...params,
limit,
order: params.order ?? defaultQueryParams.order
}
void this.subscribe()
if (initialResult !== undefined) {
const messages = initialResult.getResult()
const count = messages.length
if (count < limit) {
this.result = initialResult
} else {
if (this.params.order === SortingOrder.Ascending) {
this.result = new QueryResult(messages.slice(0, limit), (x) => x.id)
this.result.setHead(this.params.from == null)
this.result.setTail(false)
} else {
this.result = new QueryResult(messages.slice(0, limit), (x) => x.id)
this.result.setHead(false)
this.result.setTail(this.params.from == null)
}
}
this.initialized = true
void this.notify()
} else {
this.result = new QueryResult([] as Message[], (x) => x.id)
if (this.isInitLoadingForward()) {
this.result.setHead(this.params.from == null)
void this.requestLoadNextPage()
} else {
this.result.setTail(this.params.from == null)
void this.requestLoadPrevPage()
}
}
}
private findGroupIndex (groups: MessagesGroup[], from: Date): number {
const match = groups.findIndex(
(it) => it.fromDate.getTime() <= from.getTime() && it.toDate.getTime() >= from.getTime()
)
if (match !== -1) return match
return groups.findIndex((it) => it.fromDate.getTime() >= from.getTime())
}
private async loadGroups (): Promise<void> {
if (this.areGroupsLoaded) return
if (this.groupsPromise instanceof Promise) {
await this.groupsPromise
this.groupsPromise = undefined
return
}
const promise = (async (): Promise<MessagesGroup[]> => {
try {
const res = await this.hulylake.getJson<MessagesGroupsDoc>(`${this.params.cardId}/messages/groups`, {
maxRetries: 3,
isRetryable: () => true,
delayStrategy: {
getDelay: () => 500
}
})
let groups = Object.values(res?.body ?? {})
.map((it) => ({
cardId: this.params.cardId,
count: it.count,
blobId: it.blobId,
fromDate: new Date(it.fromDate),
toDate: new Date(it.toDate)
}))
.sort((a, b) => a.fromDate.getTime() - b.fromDate.getTime())
if (this.isOneMessageQuery(this.params)) {
const blobId =
this.params.blobId ??
(await this.client.findMessagesMeta({ cardId: this.params.cardId, id: this.params.id }))[0]?.blobId
if (blobId != null) {
groups = groups.filter((it) => it.blobId === blobId)
} else {
groups = []
}
}
this.prev.lastGroupIndex = groups.length
const from = this.params.from
const fromGroupIndex = from != null ? this.findGroupIndex(groups, from) : null
if (fromGroupIndex != null) {
if (this.params.order === SortingOrder.Ascending) {
this.next.lastGroupIndex = Math.max(-1, fromGroupIndex - 1)
this.prev.lastGroupIndex = Math.max(-1, fromGroupIndex)
} else {
this.next.lastGroupIndex = Math.max(-1, fromGroupIndex)
this.prev.lastGroupIndex = Math.max(-1, fromGroupIndex + 1)
}
if (fromGroupIndex === 0) {
if (this.params.order === SortingOrder.Ascending) {
this.prev.done = true
}
}
if (fromGroupIndex === -1) {
this.next.done = true
this.prev.lastGroupIndex = groups.length
}
}
this.areGroupsLoaded = true
return groups
} catch (e) {
console.error(e)
}
return []
})()
this.groupsPromise = promise
this.groups = await promise
}
async onEvent (event: Event): Promise<void> {
if (this.isCardRemoved) return
switch (event.type) {
case MessageEventType.CreateMessage: {
await this.onMessageCreatedEvent(event)
break
}
case MessageEventType.TranslateMessage:
await this.onMessageTranslatedEvent(event)
break
case MessageEventType.UpdatePatch:
case MessageEventType.RemovePatch:
case MessageEventType.ThreadPatch:
case MessageEventType.BlobPatch:
case MessageEventType.AttachmentPatch:
case MessageEventType.ReactionPatch: {
await this.onPatchEvent(event)
break
}
case CardEventType.RemoveCard:
await this.onCardRemoved(event)
break
}
}
async onCardRemoved (event: RemoveCardEvent): Promise<void> {
if (this.result instanceof Promise) this.result = await this.result
if (this.params.cardId === event.cardId) {
this.isCardRemoved = true
this.result.deleteAll()
this.result.setHead(true)
this.result.setTail(true)
this.groups = []
void this.notify()
return
}
if (this.options?.threads === true) {
const result = this.result.getResult()
let isUpdated = false
for (const message of result) {
if (message.threads.length > 0 && message.threads.some((it) => it.threadId === event.cardId)) {
this.result.update({
...message,
threads: message.threads.filter((it) => it.threadId !== event.cardId)
})
isUpdated = true
}
}
if (isUpdated) {
void this.notify()
}
}
}
async onRequest (event: Event, promise: Promise<EventResult>): Promise<void> {
if (this.isCardRemoved) return
switch (event.type) {
case MessageEventType.CreateMessage: {
await this.onCreateMessageRequest(event, promise as Promise<CreateMessageResult>)
break
}
}
}
async onCreateMessageRequest (event: CreateMessageEvent, promise: Promise<CreateMessageResult>): Promise<void> {
if (this.params.cardId !== event.cardId) return
if (this.options?.autoExpand !== true) return
const eventId = event._id
if (eventId == null) return
const tmpId = event.messageId ?? (uuid() as MessageID)
const tmpMessage = MessageProcessor.create(event, tmpId)
let resultId: MessageID | undefined
if (!this.match(tmpMessage)) return
promise
.then(async (result) => {
if (tmpId === result.messageId) return
this.tmpMessages.delete(eventId)
resultId = result.messageId
if (this.result instanceof Promise) this.result = await this.result
if (this.result.get(resultId) != null) {
if (this.result.delete(tmpId) != null) {
await this.notify()
}
} else {
const updatedMessage = { ...tmpMessage, id: resultId }
this.result.delete(tmpId)
this.insertMessage(this.result, updatedMessage)
this.resort(this.result)
void this.notify()
}
})
.catch(async () => {
if (this.result instanceof Promise) this.result = await this.result
this.tmpMessages.delete(eventId)
if (this.result.delete(tmpId) != null) {
void this.notify()
}
})
if (this.result instanceof Promise) this.result = await this.result
if (resultId === undefined && this.result.isTail()) {
this.tmpMessages.set(eventId, tmpId)
this.insertMessage(this.result, tmpMessage)
this.resort(this.result)
void this.notify()
}
}
private insertMessage (result: QueryResult<Message>, message: Message): void {
if (this.params.order === SortingOrder.Ascending) {
result.push(message)
} else {
result.unshift(message)
}
}
async subscribe (): Promise<void> {
await this.client.subscribeCard(this.params.cardId, this.id)
}
async unsubscribe (): Promise<void> {
await this.client.unsubscribeCard(this.params.cardId, this.id)
}
async requestLoadNextPage (notify = true): Promise<{ isDone: boolean }> {
if (this.isCardRemoved) return { isDone: true }
if (this.result instanceof Promise) this.result = await this.result
if (this.result.isTail()) return { isDone: true }
const pagePromise = this.loadPage(Direction.Forward, this.result)
this.nexLoadedPagesCount++
this.result = pagePromise
const r = await pagePromise
if (notify) {
await this.notify()
}
void this.translate()
return { isDone: r.isTail() }
}
async requestLoadPrevPage (notify = true): Promise<{ isDone: boolean }> {
if (this.isCardRemoved) return { isDone: true }
if (this.result instanceof Promise) this.result = await this.result
if (this.result.isHead()) return { isDone: true }
const pagePromise = this.loadPage(Direction.Backward, this.result)
this.prevLoadedPagesCount++
this.result = pagePromise
const r = await pagePromise
if (notify) {
await this.notify()
}
void this.translate()
return { isDone: r.isHead() }
}
async translate (): Promise<void> {
if (this.translateBlobs.length === 0) return
if (this.translatePromise instanceof Promise) await this.translatePromise
const lang = this.options?.language
if (lang == null) return
const promise = async (): Promise<void> => {
while (this.translateBlobs.length > 0) {
const [blob] = this.translateBlobs.splice(0, 1)
const translates = await loadTranslatedMessages(this.hulylake, this.params.cardId, blob, lang)
if (translates.length === 0) continue
if (this.result instanceof Promise) this.result = await this.result
for (const translate of translates) {
const msg = this.result.get(translate.id)
if (msg == null) continue
this.result.update({
...msg,
translates: Object.assign(msg.translates ?? {}, { [lang]: translate.content })
})
}
void this.notify()
}
}
this.translatePromise = promise()
await promise()
}
removeCallback (): void {
this.callback = () => {}
}
setCallback (callback: PagedQueryCallback<Message>): void {
this.callback = callback
void this.notify()
}
copyResult (): QueryResult<Message> | undefined {
if (this.result instanceof Promise) {
return undefined
}
return this.result.copy()
}
private isInitLoadingForward (): boolean {
const { order } = this.params
if (this.isOneMessageQuery(this.params)) {
return true
}
return order === SortingOrder.Ascending
}
private async loadPage (direction: Direction, result: QueryResult<Message>): Promise<QueryResult<Message>> {
if (!this.areGroupsLoaded) {
await this.loadGroups()
}
const limit = this.getLimit()
const messages =
direction === Direction.Forward ? await this.loadNextMessages(result) : await this.loadPrevMessages(result)
if (!result.isHead() && direction === Direction.Backward) {
result.setHead(messages.length < limit)
}
if (!result.isTail() && direction === Direction.Forward) {
result.setTail(messages.length < limit)
}
if (this.next.done && this.next.buffer.length === 0) {
result.setTail(true)
}
if (this.prev.done && this.prev.buffer.length === 0) {
result.setHead(true)
}
if (this.params.order === SortingOrder.Ascending && direction === Direction.Backward) {
result.prepend(messages.reverse())
} else if (this.params.order === SortingOrder.Descending && direction === Direction.Forward) {
result.prepend(messages.reverse())
} else {
result.append(messages)
}
return result
}
private getLimit (): number {
return this.params.limit ?? defaultQueryParams.limit
}
// Load next
private async loadNextMessages (result: QueryResult<Message>): Promise<Message[]> {
const limit = this.getLimit()
const messages: Message[] = this.next.buffer.splice(0, limit)
if (messages.length >= limit) return messages
while (!this.next.done) {
await this.fillMessagesBuffer(Direction.Forward, result)
const restLimit = limit - messages.length
if (restLimit <= 0) break
const fromBuffer = this.next.buffer.splice(0, restLimit)
messages.push(...fromBuffer)
if (messages.length >= limit) return messages
}
return messages
}
// Load prev
private async loadPrevMessages (result: QueryResult<Message>): Promise<Message[]> {
const limit = this.getLimit()
const messages: Message[] = this.prev.buffer.splice(-limit).reverse()
if (messages.length >= limit) return messages
while (!this.prev.done) {
await this.fillMessagesBuffer(Direction.Backward, result)
const restLimit = limit - messages.length
if (restLimit <= 0) break
const fromBuffer = this.prev.buffer.splice(-restLimit).reverse()
messages.push(...fromBuffer)
if (messages.length >= limit) return messages
}
return messages
}
private getNextGroupsToLoad (last: Message | undefined): MessagesGroup[] {
const limit = this.getLimit()
const groupsToLoad: MessagesGroup[] = []
let count = 0
let i = this.next.lastGroupIndex + 1
if (i >= this.groups.length) {
this.next.done = true
return groupsToLoad
}
for (; i < this.groups.length; i++) {
if (count >= limit) break
const group = this.groups[i]
if (last != null && group.toDate.getTime() < last.created.getTime()) continue
groupsToLoad.push(group)
count += group.count
}
this.next.lastGroupIndex = i - 1
this.next.done = this.next.lastGroupIndex >= this.groups.length - 1
return groupsToLoad
}
private getPrevGroupsToLoad (first: Message | undefined): MessagesGroup[] {
const limit = this.getLimit()
let count = 0
const groupsToLoad: MessagesGroup[] = []
let i = this.prev.lastGroupIndex - 1
if (i < 0) {
this.prev.done = true
return groupsToLoad
}
for (; i >= 0; i--) {
if (count >= limit) break
const group = this.groups[i]
if (first != null && group.fromDate.getTime() > first.created.getTime()) continue
groupsToLoad.push(group)
count += group.count
}
this.prev.lastGroupIndex = i + 1
this.prev.done = this.prev.lastGroupIndex <= 0
return groupsToLoad
}
private async fillMessagesBuffer (direction: Direction, result: QueryResult<Message>): Promise<void> {
const groups =
direction === Direction.Forward
? this.getNextGroupsToLoad(result.getLast())
: this.getPrevGroupsToLoad(result.getFirst()).reverse()
const messages = await Promise.all(groups.map((group) => this.loadMessagesFromBlob(group.blobId)))
const matched = this.matchMessages(
messages.flat(),
direction,
this.params.order === SortingOrder.Ascending ? result.getFirst() : result.getLast()
)
if (this.options?.language != null) {
groups.forEach((group) => {
this.translateBlobs.push(group.blobId)
})
}
if (direction === Direction.Forward) {
this.next.buffer.push(...matched.next)
this.prev.buffer.push(...matched.prev)
} else {
this.prev.buffer.unshift(...matched.prev)
this.next.buffer.push(...matched.next)
}
}
private matchMessages (
messages: Message[],
direction: Direction,
currentFirst?: Message
): {
prev: Message[]
next: Message[]
} {
const params = this.params
const _messages: Message[] = this.isOneMessageQuery(params)
? [messages.find((it) => it.id === params.id)].filter((it): it is Message => it != null)
: messages
let prev: Message[] = []
let next: Message[] = []
const from = this.initialized ? undefined : this.params.from
const lastMessage = _messages[_messages.length - 1]
if (from instanceof Date) {
for (const message of _messages) {
const isNext = params.order === SortingOrder.Ascending ? message.created >= from : message.created > from
if (isNext) {
next.push(message)
} else {
prev.push(message)
}
}
} else if (
direction === Direction.Backward &&
(currentFirst == null || lastMessage.created < currentFirst.created)
) {
prev = _messages
} else {
next = _messages
}
return { next, prev }
}
private async loadMessagesFromBlob (blobId: BlobID): Promise<Message[]> {
const params: FindMessagesParams = this.isOneMessageQuery(this.params)
? { cardId: this.params.cardId, id: this.params.id, order: SortingOrder.Ascending }
: { cardId: this.params.cardId, order: SortingOrder.Ascending }
return await loadMessages(this.hulylake, this.params.cardId, blobId, params, this.options)
}
private isOneMessageQuery (params: MessageQueryParams): params is OneMessageQueryParams {
return 'id' in this.params && this.params.id != null
}
private async notify (): Promise<void> {
this.initialized = true
if (this.callback == null) return
if (this.result instanceof Promise) this.result = await this.result
const result = this.result.getResult()
this.callback(new WindowImpl(result, this.result.getTotal(), this.result.isTail(), this.result.isHead(), this))
}
private match (message: Message): boolean {
if (this.params.cardId !== message.cardId) {
return false
}
if (this.isOneMessageQuery(this.params) && this.params.id !== message.id) {
return false
}
return true
}
private resort (result: QueryResult<Message>): void {
result.sort((a, b) =>
this.params.order === SortingOrder.Ascending
? a.created.getTime() - b.created.getTime()
: b.created.getTime() - a.created.getTime()
)
}
private async onMessageCreatedEvent (event: CreateMessageEvent): Promise<void> {
if (this.params.cardId !== event.cardId || event.messageId == null) return
if (this.result instanceof Promise) this.result = await this.result
const limit = this.getLimit()
const count = this.result.length
const message = MessageProcessor.create(event)
const exists = this.result.get(message.id)
if (exists !== undefined) return
if (!this.match(message)) return
const autoExpand = this.options?.autoExpand ?? false
const last = this.params.order === SortingOrder.Ascending ? this.result.getLast() : this.result.getFirst()
const fromTail = message.created.getTime() >= (last?.created.getTime() ?? 0)
if (!autoExpand) {
this.result.unshift(message)
this.resort(this.result)
if (count >= limit && this.params.order === SortingOrder.Descending) {
this.result.pop()
} else if (count >= limit && this.params.order === SortingOrder.Ascending) {
this.result.shift()
}
await this.notify()
return
}
const eventId = event._id
if (eventId != null) {
const tmp = this.tmpMessages.get(eventId)
if (tmp != null) this.result.delete(tmp)
this.tmpMessages.delete(eventId)
}
if (this.result.isTail() && fromTail) {
if (this.params.order === SortingOrder.Ascending) {
this.result.push(message)
} else {
this.result.unshift(message)
}
this.resort(this.result)
await this.notify()
} else if (!fromTail) {
this.result.push(message)
this.resort(this.result)
if (!this.result.isTail() && this.params.order === SortingOrder.Descending) {
this.result.pop()
} else if (!this.result.isTail() && this.params.order === SortingOrder.Ascending) {
this.result.shift()
}
await this.notify()
}
}
private async onMessageTranslatedEvent (event: TranslateMessageEvent): Promise<void> {
if (this.options?.language == null) return
if (this.params.cardId !== event.cardId || event.language !== this.options.language) return
if (this.result instanceof Promise) this.result = await this.result
const { messageId } = event
const message = this.result.get(messageId)
if (message != null) {
const updatedMessage = {
...message,
translates: Object.assign(message.translates ?? {}, { [event.language]: event.content })
}
this.result.update(updatedMessage)
await this.notify()
} else {
this.next.buffer = this.next.buffer
.map((it) => {
if (it.id === event.messageId) {
return {
...it,
translates: Object.assign(it.translates ?? {}, { [event.language]: event.content })
}
}
return it
})
.filter((it): it is Message => it != null)
this.prev.buffer = this.next.buffer
.map((it) => {
if (it.id === event.messageId) {
return {
...it,
translates: Object.assign(it.translates ?? {}, { [event.language]: event.content })
}
}
return it
})
.filter((it): it is Message => it != null)
}
}
private async onPatchEvent (event: PatchEvent): Promise<void> {
if (this.params.cardId !== event.cardId) return
const allowedPatchEvents = this.allowedPatchEvents()
if (!allowedPatchEvents.includes(event.type)) return
if (this.result instanceof Promise) this.result = await this.result
const { messageId } = event
const message = this.result.get(messageId)
const count = this.result.length
const limit = this.getLimit()
if (message !== undefined) {
const updatedMessage = MessageProcessor.applyPatch(message, event)
if (updatedMessage != null) {
this.result.update(updatedMessage)
} else {
this.result.delete(messageId)
if (this.options?.autoExpand !== true && count === limit) {
await this.refresh()
return
}
}
await this.notify()
} else {
this.next.buffer = this.next.buffer
.map((it) => {
if (it.id === event.messageId) {
return MessageProcessor.applyPatch(it, event)
}
return it
})
.filter((it): it is Message => it != null)
this.prev.buffer = this.next.buffer
.map((it) => {
if (it.id === event.messageId) {
return MessageProcessor.applyPatch(it, event)
}
return it
})
.filter((it): it is Message => it != null)
}
}
private allowedPatchEvents (): MessageEventType[] {
const result = [MessageEventType.UpdatePatch, MessageEventType.RemovePatch]
if (this.options?.reactions === true) {
result.push(MessageEventType.ReactionPatch)
}
if (this.options?.attachments === true) {
result.push(MessageEventType.AttachmentPatch)
result.push(MessageEventType.BlobPatch)
}
if (this.options?.threads === true) {
result.push(MessageEventType.ThreadPatch)
}
return result
}
async refresh (): Promise<void> {
const nextPagesCount = this.nexLoadedPagesCount
const prevPagesCount = this.prevLoadedPagesCount
this.areGroupsLoaded = false
this.groups = []
this.groupsPromise = undefined
this.nexLoadedPagesCount = 0
this.prevLoadedPagesCount = 0
this.next.done = false
this.next.buffer = []
this.prev.lastGroupIndex = -1
this.prev.done = false
this.prev.buffer = []
this.prev.lastGroupIndex = -1
this.tmpMessages.clear()
this.result = new QueryResult([] as Message[], (x) => x.id)
this.initialized = false
void this.subscribe()
if (this.isInitLoadingForward()) {
this.result.setHead(this.params.from == null)
} else {
this.result.setTail(this.params.from == null)
}
for (let i = 0; i < nextPagesCount; i++) {
const { isDone } = await this.requestLoadNextPage(false)
this.initialized = true
if (!isDone) break
}
for (let i = 0; i < prevPagesCount; i++) {
const { isDone } = await this.requestLoadPrevPage(false)
this.initialized = true
if (!isDone) break
}
await this.notify()
}
}
@@ -1,793 +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 {
BlobID,
CardID,
type FindNotificationContextParams,
FindNotificationsParams,
Message,
MessageID,
MessagesDoc,
type NotificationContext,
SortingOrder
} from '@hcengineering/communication-types'
import {
CardEventType,
CreateMessageEvent,
CreateNotificationContextEvent,
CreateNotificationEvent,
type Event,
type FindClient,
MessageEventType,
NotificationEventType,
type PagedQueryCallback,
PatchEvent,
RemoveCardEvent,
RemoveNotificationContextEvent,
RemoveNotificationsEvent,
UpdateNotificationContextEvent,
UpdateNotificationEvent
} from '@hcengineering/communication-sdk-types'
import {
MessageProcessor,
NotificationContextProcessor,
NotificationProcessor
} from '@hcengineering/communication-shared'
import { type HulylakeWorkspaceClient } from '@hcengineering/hulylake-client'
import { defaultQueryParams, NotificationContextQueryOptions, type PagedQuery, type QueryId } from '../types'
import { QueryResult } from '../result'
import { WindowImpl } from '../window'
import { matchNotification, loadMessages } from '../utils'
export class NotificationContextsQuery implements PagedQuery<NotificationContext, FindNotificationContextParams> {
private result: QueryResult<NotificationContext> | Promise<QueryResult<NotificationContext>>
private forward: Promise<{ isDone: boolean }> | { isDone: boolean } = { isDone: false }
private backward: Promise<{ isDone: boolean }> | { isDone: boolean } = { isDone: false }
nexLoadedPagesCount = 0
prevLoadedPagesCount = 0
private messagesBuffer: Message[] = []
constructor (
private readonly client: FindClient,
private readonly hulylake: HulylakeWorkspaceClient,
public readonly id: QueryId,
public readonly params: FindNotificationContextParams,
public readonly options: NotificationContextQueryOptions | undefined,
private callback?: PagedQueryCallback<NotificationContext>,
initialResult?: QueryResult<NotificationContext>
) {
this.params = {
...params,
limit: params.limit,
order: params.order ?? defaultQueryParams.order
}
if (initialResult !== undefined) {
this.result = initialResult
void this.notify()
} else {
this.result = this.initResult()
void this.result.then(() => {
void this.notify()
})
}
}
private async rawInitResult (): Promise<QueryResult<NotificationContext>> {
const limit = this.params.limit != null ? this.params.limit + 1 : undefined
const findParams: FindNotificationContextParams = {
...this.params,
order: this.params.order ?? defaultQueryParams.order,
limit
}
const res = await this.find(findParams)
const isComplete = limit == null || res.length < limit
if (!isComplete) res.pop()
const isTail = isComplete || (this.params.lastNotify == null && this.params.order === SortingOrder.Descending)
const isHead = isComplete || (this.params.lastNotify == null && this.params.order === SortingOrder.Ascending)
const result = new QueryResult(res, (it) => it.id)
result.setTail(isTail)
result.setHead(isHead)
return result
}
private async initResult (): Promise<QueryResult<NotificationContext>> {
try {
return await this.rawInitResult()
} catch (error) {
console.error('Failed to initialize query:', error)
return new QueryResult([] as NotificationContext[], (it) => it.id)
}
}
async onEvent (event: Event): Promise<void> {
switch (event.type) {
case MessageEventType.CreateMessage:
await this.onCreateMessageEvent(event)
break
case MessageEventType.BlobPatch:
case MessageEventType.AttachmentPatch:
case MessageEventType.RemovePatch:
case MessageEventType.UpdatePatch: {
await this.onCreatePatchEvent(event)
break
}
case NotificationEventType.CreateNotificationContext: {
await this.onCreateNotificationContextEvent(event)
break
}
case NotificationEventType.UpdateNotificationContext: {
await this.onUpdateNotificationContextEvent(event)
break
}
case NotificationEventType.RemoveNotificationContext: {
await this.onRemoveNotificationContextEvent(event)
break
}
case NotificationEventType.CreateNotification: {
await this.onCreateNotificationEvent(event)
break
}
case NotificationEventType.RemoveNotifications: {
await this.onRemoveNotificationEvent(event)
break
}
case NotificationEventType.UpdateNotification: {
await this.onUpdateNotificationEvent(event)
break
}
case CardEventType.RemoveCard:
await this.onCardRemoved(event)
break
}
}
async onRequest (event: Event): Promise<void> {}
async unsubscribe (): Promise<void> {}
async requestLoadNextPage (notify = true): Promise<{ isDone: boolean }> {
if (this.result instanceof Promise) this.result = await this.result
if (this.forward instanceof Promise) this.forward = await this.forward
if (this.result.isTail()) return { isDone: true }
const last = this.result.getLast()
if (last === undefined) return { isDone: false }
const limit = this.params.limit ?? defaultQueryParams.limit
const findParams: FindNotificationContextParams = {
...this.params,
lastNotify: {
greater: last.lastNotify
},
limit: limit + 1,
order: SortingOrder.Ascending
}
const forward = this.find(findParams)
const forwardPromise = forward.then(async (res) => {
if (this.result instanceof Promise) {
this.result = await this.result
}
const isTail = res.length <= limit
if (!isTail) {
res.pop()
}
this.result.append(res)
this.result.setTail(isTail)
this.nexLoadedPagesCount++
if (notify) {
await this.notify()
}
return { isDone: isTail }
})
this.forward = forwardPromise
return await forwardPromise
}
async requestLoadPrevPage (notify = true): Promise<{ isDone: boolean }> {
if (this.result instanceof Promise) this.result = await this.result
if (this.backward instanceof Promise) this.backward = await this.backward
if (this.result.isHead()) return { isDone: true }
const first = this.params.order === SortingOrder.Ascending ? this.result.getFirst() : this.result.getLast()
if (first === undefined) return { isDone: false }
const limit = this.params.limit ?? defaultQueryParams.limit
const findParams: FindNotificationContextParams = {
...this.params,
lastNotify: {
less: first.lastNotify
},
limit: limit + 1,
order: SortingOrder.Descending
}
const backward = this.find(findParams)
const backwardPromise = backward.then(async (res) => {
if (this.result instanceof Promise) this.result = await this.result
const isHead = res.length <= limit
if (!isHead) {
res.pop()
}
if (this.params.order === SortingOrder.Ascending) {
const reversed = res.reverse()
this.result.prepend(reversed)
} else {
this.result.append(res)
}
this.result.setHead(isHead)
this.prevLoadedPagesCount++
if (notify) {
await this.notify()
}
return { isDone: isHead }
})
this.backward = backwardPromise
return await backwardPromise
}
removeCallback (): void {
this.callback = () => {}
}
setCallback (callback: PagedQueryCallback<NotificationContext>): void {
this.callback = callback
void this.notify()
}
copyResult (): QueryResult<NotificationContext> | undefined {
if (this.result instanceof Promise) {
return undefined
}
return this.result.copy()
}
private async findMessage (cardId: CardID, blobId: BlobID, messageId: MessageID): Promise<Message | undefined> {
const fromBuffer = this.messagesBuffer.find((it) => it.cardId === cardId && it.id === messageId)
if (fromBuffer != null) {
return fromBuffer
}
return (await loadMessages(this.hulylake, cardId, blobId, { cardId, id: messageId }, { attachments: true }))[0]
}
async loadMessages (contexts: NotificationContext[]): Promise<NotificationContext[]> {
const cache = new Map<BlobID, Promise<MessagesDoc | undefined>>()
const newContexts = await Promise.all(
contexts.map(async (context) => {
const notifications = context.notifications ?? []
context.notifications = await Promise.all(
notifications.map(async (notification) => {
const message = (
await loadMessages(
this.hulylake,
context.cardId,
notification.blobId,
{ cardId: context.cardId, id: notification.messageId },
{ attachments: true },
cache
)
)[0]
if (message !== undefined) {
return {
...notification,
message
}
}
return notification
})
)
return context
})
)
cache.clear()
return newContexts
}
private async find (params: FindNotificationContextParams): Promise<NotificationContext[]> {
const contexts = await this.client.findNotificationContexts(params, this.id)
if (this.options?.message !== true || this.params.notifications == null) return contexts
return await this.loadMessages(contexts)
}
private async onCreateNotificationContextEvent (event: CreateNotificationContextEvent): Promise<void> {
if (event.contextId === undefined) return
if (this.forward instanceof Promise) this.forward = await this.forward
if (this.backward instanceof Promise) this.backward = await this.backward
if (this.result instanceof Promise) this.result = await this.result
if (this.result.get(event.contextId) !== undefined) {
return
}
const context = NotificationContextProcessor.create(event)
if (!this.match(context)) {
return
}
this.addContext(context, this.result)
void this.notify()
}
private async onCreateMessageEvent (event: CreateMessageEvent): Promise<void> {
if (this.params.notifications == null || this.options?.message !== true) return
if (this.forward instanceof Promise) this.forward = await this.forward
if (this.backward instanceof Promise) this.backward = await this.backward
if (this.result instanceof Promise) this.result = await this.result
const context = this.result.getResult().find((it) => it.cardId === event.cardId)
if (context === undefined) return
this.messagesBuffer.push(MessageProcessor.create(event))
const delay = 10 * 60 * 1000
this.messagesBuffer = this.messagesBuffer.filter((it) => it.created.getTime() - Date.now() < delay)
}
private async onCreatePatchEvent (event: PatchEvent): Promise<void> {
const isUpdated = await this.updateMessage(event.cardId, event.messageId, (message) =>
MessageProcessor.applyPatch(message, event)
)
if (isUpdated) {
void this.notify()
}
this.messagesBuffer = this.messagesBuffer
.map((it) => {
if (it.cardId === event.cardId && it.id === event.messageId) {
return MessageProcessor.applyPatch(it, event)
}
return it
})
.filter((it): it is Message => it != null)
}
private async onRemoveNotificationEvent (event: RemoveNotificationsEvent): Promise<void> {
if (this.params.notifications == null) return
if (this.forward instanceof Promise) this.forward = await this.forward
if (this.backward instanceof Promise) this.backward = await this.backward
if (this.result instanceof Promise) this.result = await this.result
const context = this.result.get(event.contextId)
if (context === undefined) return
if (this.params.notifications.total === true) {
const c = (await this.find({ id: context.id, limit: 1, notifications: this.params.notifications }))[0]
if (c != null) {
this.result.update(c)
void this.notify()
} else {
this.result.delete(context.id)
void this.notify()
}
return
}
if (context?.notifications === undefined) return
const filtered = context.notifications.filter((it) => !event.ids.includes(it.id))
if (filtered.length === context.notifications.length) return
const limit = this.params.notifications.limit ?? 0
if (filtered.length < limit && context.notifications.length >= limit) {
const contextUpdated = (
await this.find({ id: context.id, limit: 1, notifications: this.params.notifications })
)[0]
if (contextUpdated !== undefined) {
this.result.update(contextUpdated)
} else {
this.result.delete(context.id)
}
} else {
this.result.update({
...context,
notifications: filtered
})
}
void this.notify()
}
private async onUpdateNotificationEvent (event: UpdateNotificationEvent): Promise<void> {
if (this.params.notifications == null) return
if (this.forward instanceof Promise) this.forward = await this.forward
if (this.backward instanceof Promise) this.backward = await this.backward
if (this.result instanceof Promise) this.result = await this.result
const context = this.result.get(event.contextId)
if (context === undefined) return
const totalNotifications = this.getNotificationsTotal(context, event)
const shouldRefreshTotal = this.params.notifications.total === true && this.params.notifications.type != null
let matchQuery: FindNotificationsParams = { ...event.query, contextId: event.contextId, account: event.account }
if (event.query.untilDate != null) {
matchQuery = { ...matchQuery, created: { lessOrEqual: event.query.untilDate } }
}
const toUpdate = (context.notifications ?? []).filter(
(it) => matchNotification(it, matchQuery) && it.read !== event.updates.read
)
if (toUpdate === undefined || (toUpdate?.length ?? 0) === 0) {
if (shouldRefreshTotal) {
const nRes = await this.client.findNotifications({
...this.params.notifications,
contextId: context.id,
limit: 1,
total: true
})
this.result.update({
...context,
totalNotifications: nRes.total
})
void this.notify()
} else if (totalNotifications !== context.totalNotifications) {
this.result.update({
...context,
totalNotifications
})
void this.notify()
}
return
}
const toUpdateMap = new Map(toUpdate.map((it) => [it.id, it]))
const currentLength = context.notifications?.length ?? 0
const newNotifications = (context.notifications ?? []).map((it) =>
toUpdateMap.has(it.id) ? { ...it, ...event.updates } : it
)
const newLength = newNotifications.length
const shouldRefreshLimit = newLength < currentLength && newLength < this.params.notifications.limit
if (shouldRefreshLimit) {
const updated: NotificationContext = (
await this.find({ id: context.id, limit: 1, notifications: this.params.notifications })
)[0]
if (updated !== undefined) {
this.result.update(updated)
} else {
this.result.delete(context.id)
}
} else if (shouldRefreshTotal) {
const nRes = await this.client.findNotifications({
...this.params.notifications,
contextId: context.id,
limit: 1,
total: true
})
this.result.update({
...context,
notifications: newNotifications,
totalNotifications: nRes.total
})
} else {
this.result.update({
...context,
notifications: newNotifications,
totalNotifications
})
}
void this.notify()
}
private getNotificationsTotal (context: NotificationContext, event: UpdateNotificationEvent): number {
const updatedCount = event.updated ?? 0
if (this.params.notifications?.total === true && updatedCount > 0) {
if (this.params.notifications?.read != null && this.params.notifications.read === event.updates.read) {
return (context.totalNotifications ?? 0) + updatedCount
} else if (this.params.notifications?.read != null && this.params.notifications.read !== event.updates.read) {
return Math.max(0, (context.totalNotifications ?? 0) - updatedCount)
}
}
return context.totalNotifications ?? 0
}
private async onCreateNotificationEvent (event: CreateNotificationEvent): Promise<void> {
if (this.params.notifications == null || event.notificationId == null) return
if (this.forward instanceof Promise) this.forward = await this.forward
if (this.backward instanceof Promise) this.backward = await this.backward
if (this.result instanceof Promise) this.result = await this.result
const notification = NotificationProcessor.create(event)
const match = matchNotification(notification, {
type: this.params.notifications.type,
read: this.params.notifications.read
})
if (!match) return
const context = this.result.get(notification.contextId)
if ((context?.notifications ?? []).some((it) => it.id === notification.id)) return
if (context !== undefined) {
const message =
this.options?.message === true
? await this.findMessage(context.cardId, notification.blobId, notification.messageId)
: undefined
const notifications = [
{
...notification,
message
},
...(context.notifications ?? [])
]
if (notifications.length > this.params.notifications.limit) {
notifications.pop()
}
this.result.update({
...context,
...(this.params.notifications.total === true
? { totalNotifications: (context.totalNotifications ?? 0) + 1 }
: {}),
notifications
})
void this.notify()
} else {
const newContext = (
await this.find({ id: notification.contextId, notifications: this.params.notifications, limit: 1 })
)[0]
if (newContext !== undefined) {
this.addContext(newContext, this.result)
void this.notify()
}
}
}
private async onRemoveNotificationContextEvent (event: RemoveNotificationContextEvent): Promise<void> {
if (this.forward instanceof Promise) this.forward = await this.forward
if (this.backward instanceof Promise) this.backward = await this.backward
if (this.result instanceof Promise) this.result = await this.result
const length = this.result.length
const deleted = this.result.delete(event.contextId)
if (deleted != null) {
if (this.params.limit != null && length >= this.params.limit && this.result.length < this.params.limit) {
await this.reinit(length)
} else {
void this.notify()
}
}
}
private async reinit (limit: number): Promise<void> {
if (this.forward instanceof Promise) this.forward = await this.forward
if (this.backward instanceof Promise) this.backward = await this.backward
if (this.result instanceof Promise) this.result = await this.result
this.result = this.find({ ...this.params, limit: limit + 1 }).then((res) => {
const isTail =
res.length <= limit || (this.params.order === SortingOrder.Descending && this.params.lastNotify == null)
const isHead =
res.length <= limit || (this.params.order === SortingOrder.Ascending && this.params.lastNotify == null)
if (res.length > limit) {
res.pop()
}
const result = new QueryResult(res, (it) => it.id)
result.setHead(isHead)
result.setTail(isTail)
return result
})
void this.result.then((res) => {
void this.notify()
return res
})
}
private async onUpdateNotificationContextEvent (event: UpdateNotificationContextEvent): Promise<void> {
if (this.forward instanceof Promise) this.forward = await this.forward
if (this.backward instanceof Promise) this.backward = await this.backward
if (this.result instanceof Promise) this.result = await this.result
const contextToUpdate = this.result.get(event.contextId)
if (contextToUpdate === undefined) return
const updated: NotificationContext = {
...contextToUpdate,
lastUpdate: event.updates.lastUpdate ?? contextToUpdate.lastUpdate,
lastView: event.updates.lastView ?? contextToUpdate.lastView,
lastNotify: event.updates.lastNotify ?? contextToUpdate.lastNotify
}
this.result.update(updated)
if (event.updates.lastNotify != null) {
this.sort(this.result)
}
void this.notify()
}
async onCardRemoved (event: RemoveCardEvent): Promise<void> {
if (this.result instanceof Promise) this.result = await this.result
let updated = false
const result = this.result.getResult()
for (const context of result) {
if (context.cardId === event.cardId) {
this.result.delete(context.id)
updated = true
}
}
if (updated) {
if (this.params.limit != null && this.result.length < this.params.limit && result.length >= this.params.limit) {
const contexts = await this.find(this.params)
this.result = new QueryResult(contexts, (x) => x.id)
}
void this.notify()
}
}
private addContext (context: NotificationContext, result: QueryResult<NotificationContext>): void {
if (result.get(context.id) !== undefined) return
if (result.isTail()) {
if (this.params.order === SortingOrder.Ascending) {
result.push(context)
} else {
result.unshift(context)
}
}
if (this.params.limit != null && result.length > this.params.limit) {
result.pop()
}
this.sort(result)
}
private sort (result: QueryResult<NotificationContext>): void {
result.sort((a, b) =>
this.params.order === SortingOrder.Descending
? (b.lastNotify?.getTime() ?? 0) - (a.lastNotify?.getTime() ?? 0)
: (a.lastNotify?.getTime() ?? 0) - (b.lastNotify?.getTime() ?? 0)
)
}
private match (context: NotificationContext): boolean {
if (this.params.cardId !== undefined) {
const cards = Array.isArray(this.params.cardId) ? this.params.cardId : [this.params.cardId]
if (!cards.includes(context.cardId)) return false
}
if (this.params.id !== undefined && context.id !== this.params.id) {
return false
}
if (this.params.lastNotify !== undefined) {
if (
'greater' in this.params.lastNotify &&
this.params.lastNotify.greater != null &&
(context.lastNotify?.getTime() ?? 0) <= this.params.lastNotify.greater.getTime()
) {
return false
}
if (
'less' in this.params.lastNotify &&
this.params.lastNotify.less != null &&
(context.lastNotify?.getTime() ?? 0) >= this.params.lastNotify.less.getTime()
) {
return false
}
if (
'greaterOrEqual' in this.params.lastNotify &&
this.params.lastNotify.greaterOrEqual != null &&
(context.lastNotify?.getTime() ?? 0) < this.params.lastNotify.greaterOrEqual.getTime()
) {
return false
}
if (
'lessOrEqual' in this.params.lastNotify &&
this.params.lastNotify.lessOrEqual != null &&
(context.lastNotify?.getTime() ?? 0) > this.params.lastNotify.lessOrEqual.getTime()
) {
return false
}
if (this.params.lastNotify instanceof Date && this.params.lastNotify !== context.lastNotify) {
return false
}
}
return true
}
private async notify (): Promise<void> {
if (this.callback === undefined) return
if (this.result instanceof Promise) {
this.result = await this.result
}
const result = this.result.getResult()
const isTail = this.result.isTail()
const isHead = this.result.isHead()
const window = new WindowImpl(result, this.result.getTotal(), isTail, isHead, this)
this.callback(window)
}
private async updateMessage (
card: CardID,
messageId: MessageID,
updater: (message: Message) => Message | undefined
): Promise<boolean> {
if (this.params.notifications == null || this.options?.message !== true) return false
if (this.forward instanceof Promise) this.forward = await this.forward
if (this.backward instanceof Promise) this.backward = await this.backward
if (this.result instanceof Promise) this.result = await this.result
const context = this.result.getResult().find((it) => it.cardId === card)
if (context === undefined || (context.notifications ?? []).length === 0) return false
const hasMessage = context.notifications?.some((it) => it.messageId === messageId) ?? false
if (!hasMessage) return false
this.result.update({
...context,
notifications: context.notifications?.map((it) => ({
...it,
message: it.messageId === messageId && it.message != null ? updater(it.message) : it.message
}))
})
return true
}
async refresh (): Promise<void> {
const nextPagesCount = this.nexLoadedPagesCount
const prevPagesCount = this.prevLoadedPagesCount
this.result = new QueryResult([] as NotificationContext[], (it) => it.id)
this.nexLoadedPagesCount = 0
this.prevLoadedPagesCount = 0
this.result = await this.rawInitResult()
for (let i = 0; i < nextPagesCount; i++) {
const { isDone } = await this.requestLoadNextPage(false)
if (!isDone) break
}
for (let i = 0; i < prevPagesCount; i++) {
const { isDone } = await this.requestLoadPrevPage(false)
if (!isDone) break
}
await this.notify()
}
}
@@ -1,421 +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 FindNotificationsParams,
type Notification,
SortingOrder,
WithTotal
} from '@hcengineering/communication-types'
import {
CreateNotificationEvent,
type Event,
type FindClient,
NotificationEventType,
type PagedQueryCallback,
RemoveNotificationContextEvent,
RemoveNotificationsEvent,
UpdateNotificationEvent
} from '@hcengineering/communication-sdk-types'
import { NotificationProcessor } from '@hcengineering/communication-shared'
import { type HulylakeWorkspaceClient } from '@hcengineering/hulylake-client'
import { defaultQueryParams, NotificationQueryParams, type PagedQuery, type QueryId, QueryOptions } from '../types'
import { QueryResult } from '../result'
import { WindowImpl } from '../window'
import { matchNotification } from '../utils'
export class NotificationQuery implements PagedQuery<Notification, NotificationQueryParams> {
private result: QueryResult<Notification> | Promise<QueryResult<Notification>>
nexLoadedPagesCount = 0
prevLoadedPagesCount = 0
constructor (
private readonly client: FindClient,
private readonly hulylake: HulylakeWorkspaceClient,
public readonly id: QueryId,
public readonly params: NotificationQueryParams,
public readonly options: QueryOptions | undefined,
private callback?: PagedQueryCallback<Notification>,
initialResult?: QueryResult<Notification>
) {
if (initialResult !== undefined) {
this.result = initialResult
void this.notify()
} else {
this.result = this.initResult()
void this.result.then(() => {
void this.notify()
})
}
}
private async rawInitResult (): Promise<QueryResult<Notification>> {
const limit = this.params.limit != null ? this.params.limit + 1 : undefined
const findParams: FindNotificationsParams = {
...this.params,
order: this.params.order ?? defaultQueryParams.order,
limit: this.params.strict === true ? this.params.limit : limit
}
const res = await this.find(findParams)
const isComplete = this.params.limit == null || this.params.strict === true || res.length <= this.params.limit
if (!isComplete) res.pop()
const result = new QueryResult(res, (it) => it.id)
result.setTail(isComplete)
result.setHead(isComplete)
if (this.params.total === true) {
result.setTotal(res.total)
}
return result
}
private async initResult (): Promise<QueryResult<Notification>> {
try {
return await this.rawInitResult()
} catch (error) {
console.error('Failed to initialize query:', error)
return new QueryResult([] as Notification[], (it) => it.id)
}
}
async onEvent (event: Event): Promise<void> {
switch (event.type) {
case NotificationEventType.CreateNotification: {
await this.onCreateNotificationEvent(event)
break
}
case NotificationEventType.RemoveNotifications: {
await this.onRemoveNotificationsEvent(event)
break
}
case NotificationEventType.UpdateNotification: {
await this.onUpdateNotificationEvent(event)
break
}
case NotificationEventType.RemoveNotificationContext:
await this.onRemoveNotificationContextEvent(event)
break
}
}
async onRequest (event: Event): Promise<void> {}
async unsubscribe (): Promise<void> {}
async requestLoadNextPage (notify = true): Promise<{ isDone: boolean }> {
if (this.params.strict === true) return { isDone: true }
if (this.result instanceof Promise) this.result = await this.result
if (this.result.isTail()) return { isDone: true }
const result = await this.loadPage(SortingOrder.Ascending, this.result.getLast()?.created, notify)
this.nexLoadedPagesCount++
return result
}
async requestLoadPrevPage (notify = true): Promise<{ isDone: boolean }> {
if (this.params.strict === true) return { isDone: true }
if (this.result instanceof Promise) this.result = await this.result
if (this.result.isHead()) return { isDone: true }
const result = await this.loadPage(SortingOrder.Descending, this.result.getFirst()?.created, notify)
this.prevLoadedPagesCount++
return result
}
private async loadPage (order: SortingOrder, created?: Date, notify = true): Promise<{ isDone: boolean }> {
if (created == null) return { isDone: false }
if (this.params.limit == null) return { isDone: true }
if (this.result instanceof Promise) this.result = await this.result
const limit = this.params.limit
const findParams: FindNotificationsParams = {
...this.params,
total: false,
created: order === SortingOrder.Ascending ? { greater: created } : { less: created },
limit: limit + 1,
order
}
try {
const res = await this.find(findParams)
const isDone = res.length <= limit
if (!isDone) res.pop()
if (order === SortingOrder.Ascending) {
this.result.append(res)
this.result.setTail(isDone)
} else {
this.result.prepend(res)
this.result.setHead(isDone)
}
if (notify) {
await this.notify()
}
return { isDone }
} catch (error) {
console.error(`Failed to load ${order === SortingOrder.Ascending ? 'next' : 'previous'} page:`, error)
}
return { isDone: false }
}
removeCallback (): void {
this.callback = () => {}
}
setCallback (callback: PagedQueryCallback<Notification>): void {
this.callback = callback
void this.notify()
}
copyResult (): QueryResult<Notification> | undefined {
return this.result instanceof Promise ? undefined : this.result.copy()
}
private async find (params: NotificationQueryParams): Promise<WithTotal<Notification>> {
delete params.strict
return await this.client.findNotifications(params, this.id)
}
private async onCreateNotificationEvent (event: CreateNotificationEvent): Promise<void> {
if (event.notificationId == null) return
if (this.result instanceof Promise) this.result = await this.result
if (this.result.get(event.notificationId) != null) return
const notification = NotificationProcessor.create(event)
const match = matchNotification(notification, { ...this.params, created: undefined })
if (!match) return
const res =
this.params.order === SortingOrder.Ascending ? this.result.getResult() : this.result.getResult().reverse()
const first = res[0]
const last = res[res.length - 1]
const inRange =
first != null &&
last != null &&
notification.created.getTime() >= first.created.getTime() &&
notification.created.getTime() <= last.created.getTime()
if (!this.result.isTail() && !inRange) {
return
}
if (this.params.total === true) {
const total = this.result.getTotal()
this.result.setTotal(total + 1)
}
if (inRange) {
this.result.push(notification)
this.result.sort((a, b) =>
this.params.order === SortingOrder.Ascending
? a.created.getTime() - b.created.getTime()
: b.created.getTime() - a.created.getTime()
)
} else if (this.params.order === SortingOrder.Ascending) {
if (this.params.limit !== undefined && this.result.length === this.params.limit && this.params.strict === true) {
this.result.setTail(false)
} else {
this.result.push(notification)
}
} else {
if (this.params.limit !== undefined && this.result.length === this.params.limit && this.params.strict === true) {
this.result.pop()
this.result.setHead(false)
}
this.result.unshift(notification)
}
await this.notify()
}
private async updateNotificationRead (result: QueryResult<Notification>, updated: Notification[]): Promise<void> {
const isAllowed = (n: Notification): boolean => {
if (this.params.read == null) return true
return n.read === this.params.read
}
const currentLength = result.length
for (const notification of updated) {
if (isAllowed(notification)) {
result.update(notification)
} else {
result.delete(notification.id)
}
}
const newLength = result.length
const { limit } = this.params
if (limit != null && currentLength !== newLength && currentLength >= limit && newLength < limit) {
await this.reinit(currentLength)
} else {
void this.notify()
}
}
private async onUpdateNotificationEvent (event: UpdateNotificationEvent): Promise<void> {
if (this.result instanceof Promise) this.result = await this.result
const updatedCount = event.updated ?? 0
let totalUpdated = false
if (this.params.total === true && updatedCount > 0) {
if (this.params.read != null && this.params.read === event.updates.read) {
this.result.setTotal(this.result.getTotal() + updatedCount)
totalUpdated = true
} else if (this.params.read != null && this.params.read !== event.updates.read) {
this.result.setTotal(this.result.getTotal() - updatedCount)
totalUpdated = true
}
}
const toUpdate = (
event.query.id != null
? [this.result.get(event.query.id)].filter((it): it is Notification => it != null)
: this.result.getResult().filter((it) => matchNotification(it, event.query))
).filter((it) => it.read !== event.updates.read)
if (toUpdate === undefined || toUpdate.length === 0) {
if (totalUpdated) {
await this.notify()
}
return
}
const updated = toUpdate.map((it) => ({ ...it, ...event.updates }))
await this.updateNotificationRead(this.result, updated)
}
private async onRemoveNotificationsEvent (event: RemoveNotificationsEvent): Promise<void> {
if (this.params.contextId !== undefined && this.params.contextId !== event.contextId) return
if (this.result instanceof Promise) this.result = await this.result
if (this.params.total === true) {
await this.refresh()
return
}
const currentLength = this.result.length
let isDeleted = false
for (const id of event.ids) {
const deleted = this.result.delete(id)
isDeleted = isDeleted || deleted !== undefined
}
const newLength = this.result.length
const { limit } = this.params
if (limit != null && currentLength >= limit && newLength < limit) {
void this.reinit(currentLength)
} else if (isDeleted) {
void this.notify()
}
}
private async onRemoveNotificationContextEvent (event: RemoveNotificationContextEvent): Promise<void> {
if (this.params.contextId != null && this.params.contextId !== event.contextId) return
if (this.result instanceof Promise) this.result = await this.result
if (event.contextId === this.params.contextId) {
if (this.result.length === 0) return
this.result.setTotal(-1)
this.result.deleteAll()
this.result.setHead(true)
this.result.setTail(true)
void this.notify()
} else if (this.params.total === true) {
await this.refresh()
} else {
const toRemove = this.result.getResult().filter((it) => it.contextId === event.contextId)
if (toRemove.length === 0) return
const length = this.result.length
for (const notification of toRemove) {
this.result.delete(notification.id)
}
const { limit } = this.params
if (limit != null && length >= limit && this.result.length < limit) {
void this.reinit(this.result.length)
} else {
void this.notify()
}
}
}
private async notify (): Promise<void> {
if (this.callback == null) return
if (this.result instanceof Promise) this.result = await this.result
const window = new WindowImpl(
this.result.getResult(),
this.result.getTotal(),
this.result.isTail(),
this.result.isHead(),
this
)
this.callback(window)
}
private async reinit (limit: number): Promise<void> {
if (this.result instanceof Promise) this.result = await this.result
this.result = this.find({ ...this.params, limit: limit + 1 }).then((res) => {
const isTail = res.length <= limit
const isHead = res.length <= limit
if (res.length > limit) {
res.pop()
}
const result = new QueryResult(res, (it) => it.id)
result.setHead(isHead)
result.setTail(isTail)
result.setTotal(res.total)
return result
})
void this.result.then((res) => {
void this.notify()
return res
})
}
async refresh (): Promise<void> {
const nextPagesCount = this.nexLoadedPagesCount
const prevPagesCount = this.prevLoadedPagesCount
this.result = new QueryResult([] as Notification[], (it) => it.id)
this.nexLoadedPagesCount = 0
this.prevLoadedPagesCount = 0
this.result = await this.rawInitResult()
for (let i = 0; i < nextPagesCount; i++) {
const { isDone } = await this.requestLoadNextPage(false)
if (!isDone) break
}
for (let i = 0; i < prevPagesCount; i++) {
const { isDone } = await this.requestLoadPrevPage(false)
if (!isDone) break
}
await this.notify()
}
}
@@ -1,138 +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 { ID } from '@hcengineering/communication-types'
export class QueryResult<T> {
private objectById: Map<ID, T>
private total: number = -1
private tail: boolean = false
private head: boolean = false
get length (): number {
return this.objectById.size
}
constructor (
messages: T[],
private readonly getId: (it: T) => ID
) {
this.objectById = new Map(messages.map((it) => [getId(it), it]))
}
setTotal (total: number): void {
this.total = total
}
getTotal (): number {
return Math.max(this.total, 0)
}
isTail (): boolean {
return this.tail
}
isHead (): boolean {
return this.head
}
setHead (head: boolean): void {
this.head = head
}
setTail (tail: boolean): void {
this.tail = tail
}
getResult (): T[] {
return Array.from(this.objectById.values())
}
get (id: ID): Readonly<T> | undefined {
return this.objectById.get(id)
}
delete (id: ID): T | undefined {
const object = this.objectById.get(id)
this.objectById.delete(id)
return object
}
deleteAll (): void {
this.objectById.clear()
}
push (object: T): void {
this.objectById.set(this.getId(object), object)
}
unshift (object: T): void {
this.objectById = new Map([[this.getId(object), object], ...this.objectById])
}
pop (): T | undefined {
const array = Array.from(this.objectById.values())
const last = array[array.length - 1]
if (last === undefined) return
this.objectById.delete(this.getId(last))
return last
}
shift (): T | undefined {
const array = Array.from(this.objectById.values())
const first = array[0]
if (first === undefined) return
this.objectById.delete(this.getId(first))
return first
}
update (object: T): void {
this.objectById.set(this.getId(object), object)
}
getFirst (): T | undefined {
return Array.from(this.objectById.values())[0]
}
getLast (): T | undefined {
return Array.from(this.objectById.values())[this.objectById.size - 1]
}
prepend (objects: T[]): void {
const current = Array.from(this.objectById.entries())
this.objectById = new Map([...objects.map<[ID, T]>((object) => [this.getId(object), object]), ...current])
}
append (objects: T[]): void {
for (const object of objects) {
this.objectById.set(this.getId(object), object)
}
}
sort (compare: (a: T, b: T) => number): void {
const current = Array.from(this.objectById.values())
const sorted = current.sort(compare)
this.objectById = new Map(sorted.map<[ID, T]>((object) => [this.getId(object), object]))
}
copy (): QueryResult<T> {
const copy = new QueryResult(Array.from(this.objectById.values()), this.getId)
copy.setHead(this.head)
copy.setTail(this.tail)
return copy
}
}
@@ -1,98 +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, Event } from '@hcengineering/communication-sdk-types'
import {
SortingOrder,
type Window,
type CardID,
type MessageID,
FindNotificationsParams,
FindMessagesOptions,
BlobID
} from '@hcengineering/communication-types'
import { QueryResult } from './result'
export type QueryId = number
export const defaultQueryParams = {
limit: 50,
order: SortingOrder.Ascending
}
export enum Direction {
Forward = 'forward',
Backward = 'backward'
}
export type FindParams = Partial<typeof defaultQueryParams>
export type QueryOptions = Record<string, any>
interface BaseQuery<R = any, P = FindParams> {
readonly id: QueryId
readonly params: P
readonly options: QueryOptions | undefined
onEvent: (event: Event) => Promise<void>
onRequest: (event: Event, promise: Promise<EventResult>) => Promise<void>
unsubscribe: () => Promise<void>
removeCallback: () => void
setCallback: (callback: (result: any) => void) => void
copyResult: () => QueryResult<R> | undefined
refresh: () => Promise<void>
}
export interface PagedQuery<R = any, P = FindParams> extends BaseQuery<R, P> {
nexLoadedPagesCount: number
prevLoadedPagesCount: number
requestLoadNextPage: (notify?: boolean) => Promise<{ isDone: boolean }>
requestLoadPrevPage: (notify?: boolean) => Promise<{ isDone: boolean }>
setCallback: (callback: (window: Window<R>) => void) => void
}
export interface Query<R = any, P = FindParams> extends BaseQuery<R, P> {
setCallback: (callback: (result: R[]) => void) => void
}
export type AnyQuery = Query | PagedQuery
interface BaseMessageQueryParams {
cardId: CardID
limit?: number
order?: SortingOrder
}
export interface ManyMessagesQueryParams extends BaseMessageQueryParams {
from?: Date
}
export interface OneMessageQueryParams extends BaseMessageQueryParams {
id: MessageID
blobId?: BlobID
from: never
}
export interface NotificationContextQueryOptions {
message?: boolean
}
export type MessageQueryParams = OneMessageQueryParams | ManyMessagesQueryParams
export interface MessageQueryOptions extends QueryOptions, FindMessagesOptions { autoExpand?: boolean, language?: string }
export type NotificationQueryParams = FindNotificationsParams & { strict?: boolean }
@@ -1,103 +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 { parseMessagesDoc, parseTranslatedMessagesDoc } from '@hcengineering/communication-shared'
import {
BlobID,
type CardID,
FindMessagesOptions,
FindMessagesParams,
type FindNotificationsParams,
type Message,
MessagesDoc,
type Notification, TranslatedMessage, TranslatedMessagesDoc
} from '@hcengineering/communication-types'
import { type HulylakeWorkspaceClient } from '@hcengineering/hulylake-client'
export async function loadTranslatedMessages (client: HulylakeWorkspaceClient, cardId: CardID, blobId: BlobID, lang: string): Promise<TranslatedMessage[]> {
try {
const res = await client.getJson<TranslatedMessagesDoc>(`${cardId}/messages/${lang}/${blobId}`)
if (res?.body == null) return []
return parseTranslatedMessagesDoc(res.body)
} catch (e) {
console.error(e)
return []
}
}
export async function loadMessages (client: HulylakeWorkspaceClient, cardId: CardID, blobId: BlobID, params: FindMessagesParams, options?: FindMessagesOptions, cache?: Map<BlobID, Promise<MessagesDoc | undefined>>): Promise<Message[]> {
const doc = await loadMessagesDoc(client, cardId, blobId, cache)
if (doc === undefined) {
return []
}
return parseMessagesDoc(doc, params, options)
}
async function requestMessagesDoc (client: HulylakeWorkspaceClient, cardId: CardID, blobId: BlobID): Promise<MessagesDoc | undefined> {
const res = await client.getJson<MessagesDoc>(`${cardId}/messages/${blobId}`, {
maxRetries: 3,
isRetryable: () => true,
delayStrategy: {
getDelay: () => 500
}
})
if (res?.body === undefined) {
return undefined
}
return res.body
}
async function loadMessagesDoc (
client: HulylakeWorkspaceClient,
cardId: CardID,
blobId: BlobID,
cache?: Map<BlobID, Promise<MessagesDoc | undefined>>
): Promise<MessagesDoc | undefined> {
if (cache != null && cache.has(blobId)) {
return await cache.get(blobId)
}
const messagesPromise = requestMessagesDoc(client, cardId, blobId)
if (cache != null) {
cache.set(blobId, messagesPromise)
}
return await messagesPromise
}
export function matchNotification (notification: Notification, params: FindNotificationsParams): boolean {
if (params.type !== undefined && params.type !== notification.type) return false
if (params.read !== undefined && params.read !== notification.read) return false
if (params.id !== undefined && params.id !== notification.id) return false
if (params.contextId !== undefined && params.contextId !== notification.contextId) return false
if (params.cardId !== undefined && params.cardId !== notification.cardId) return false
const created = notification.created.getTime()
if (params.created != null) {
if (params.created instanceof Date) {
if (created !== params.created.getTime()) return false
} else {
const { greater, less, greaterOrEqual, lessOrEqual } = params.created
if (greater != null && created <= greater.getTime()) return false
if (less != null && created >= less.getTime()) return false
if (greaterOrEqual != null && created < greaterOrEqual.getTime()) return false
if (lessOrEqual != null && created > lessOrEqual.getTime()) return false
}
}
return true
}
@@ -1,54 +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'
import type { PagedQuery } from './types'
export class WindowImpl<T> implements Window<T> {
constructor (
private readonly result: T[],
private readonly total: number,
private readonly isTail: boolean,
private readonly isHead: boolean,
private readonly query: PagedQuery<T>
) {}
getResult (): T[] {
return this.result
}
getTotal (): number {
return this.total
}
async loadNextPage (): Promise<void> {
if (!this.hasNextPage()) return
await this.query.requestLoadNextPage()
}
async loadPrevPage (): Promise<void> {
if (!this.hasPrevPage()) return
await this.query.requestLoadPrevPage()
}
hasNextPage (): boolean {
return !this.isTail
}
hasPrevPage (): boolean {
return !this.isHead
}
}
@@ -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"]
}
@@ -1,7 +0,0 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}
@@ -1,68 +0,0 @@
{
"name": "@hcengineering/communication-rest-client",
"entries": [
{
"version": "0.7.12",
"tag": "@hcengineering/communication-rest-client_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`"
},
{
"comment": "Updating dependency \"@hcengineering/communication-sdk-types\" from `^0.7.11` to `0.7.12`"
}
]
}
},
{
"version": "0.7.9",
"tag": "@hcengineering/communication-rest-client_v0.7.9",
"date": "Sat, 25 Oct 2025 22:20:35 GMT",
"comments": {
"patch": [
{
"comment": "Bump core"
}
],
"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-rest-client_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`"
},
{
"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`"
}
]
}
}
]
}
@@ -1,25 +0,0 @@
# Change Log - @hcengineering/communication-rest-client
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
- Bump core
## 0.7.4
Tue, 14 Oct 2025 10:12:38 GMT
### Patches
- update deps
@@ -1,4 +0,0 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig"
}
@@ -1,60 +0,0 @@
{
"name": "@hcengineering/communication-rest-client",
"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",
"@types/snappyjs": "^0.7.1"
},
"dependencies": {
"@hcengineering/communication-types": "workspace:^0.7.12",
"@hcengineering/communication-sdk-types": "workspace:^0.7.12",
"@hcengineering/communication-shared": "workspace:^0.7.11",
"@hcengineering/core": "workspace:^0.7.22",
"snappyjs": "^0.7.0"
},
"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"
}
}
}
@@ -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 { createRestClient } from './rest'
export type { RestClient } from './types'
@@ -1,288 +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, generateId, OperationDomain, TxDomainEvent } from '@hcengineering/core'
import {
type EventResult,
type Event,
type CreateMessageResult,
type CreateMessageOptions,
UpdatePatchOptions,
MessageEventType
} from '@hcengineering/communication-sdk-types'
import {
type FindNotificationContextParams,
type NotificationContext,
type FindNotificationsParams,
type Notification,
type MessageID,
type MessageMeta,
type CardID,
type Markdown,
type SocialID,
type CardType,
type MessageType,
type MessageExtra,
AttachmentData,
AttachmentID,
FindMessagesMetaParams,
FindMessagesGroupParams,
MessagesGroup
} from '@hcengineering/communication-types'
import { retry } from '@hcengineering/communication-shared'
import { extractJson } from './utils'
import type { RestClient } from './types'
const retries = 3
export function createRestClient (endpoint: string, workspaceId: string, token: string): RestClient {
return new RestClientImpl(endpoint, workspaceId, token)
}
class RestClientImpl implements RestClient {
endpoint: string
constructor (
endpoint: string,
readonly workspace: string,
readonly token: string
) {
this.endpoint = endpoint.replace('ws', 'http')
}
private jsonHeaders (): Record<string, string> {
return {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + this.token,
'accept-encoding': 'snappy, gzip'
}
}
private requestInit (): RequestInit {
return {
method: 'GET',
keepalive: true,
headers: this.jsonHeaders()
}
}
private wrapEvent (event: Event, modifiedBy: SocialID): TxDomainEvent {
return {
_id: generateId(),
_class: core.class.TxDomainEvent,
space: core.space.Tx,
objectSpace: core.space.Tx,
domain: 'communication' as OperationDomain,
event,
modifiedBy,
modifiedOn: Date.now()
}
}
private async find<T>(operation: string, params: Record<string, any>): Promise<T[]> {
const searchParams = new URLSearchParams()
if (Object.keys(params).length > 0) {
searchParams.append('params', JSON.stringify(params))
}
const requestUrl = concatLink(
this.endpoint,
`/api/v1/request/communication/${operation}/${this.workspace}?${searchParams.toString()}`
)
return await retry(
async () => {
const response = await fetch(requestUrl, this.requestInit())
if (!response.ok) {
throw new Error(response.statusText)
}
return await extractJson<T[]>(response)
},
{ retries }
)
}
async event (event: Event, socialId: SocialID): Promise<EventResult> {
const response = await fetch(concatLink(this.endpoint, `/api/v1/tx/${this.workspace}`), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + this.token
},
keepalive: true,
body: JSON.stringify(this.wrapEvent(event, socialId))
})
if (!response.ok) {
throw new Error(response.statusText)
}
return (await response.json()) as EventResult
}
async createMessage (
cardId: CardID,
cardType: CardType,
content: Markdown,
type: MessageType,
extra: MessageExtra | undefined,
socialId: SocialID,
date?: Date,
messageId?: MessageID,
options?: CreateMessageOptions
): Promise<CreateMessageResult> {
const result = await this.event(
{
type: MessageEventType.CreateMessage,
messageType: type,
cardId,
cardType,
content,
extra,
socialId,
date,
messageId,
options
},
socialId
)
return result as CreateMessageResult
}
async updateMessage (
cardId: CardID,
messageId: MessageID,
content: Markdown | undefined,
extra: MessageExtra | undefined,
socialId: SocialID,
date?: Date,
options?: UpdatePatchOptions
): Promise<void> {
await this.event(
{
type: MessageEventType.UpdatePatch,
cardId,
messageId,
content,
extra,
socialId,
date,
options
},
socialId
)
}
async removeMessage (cardId: CardID, messageId: MessageID, socialId: SocialID, date?: Date): Promise<void> {
await this.event(
{
type: MessageEventType.RemovePatch,
cardId,
messageId,
socialId,
date
},
socialId
)
}
async addAttachments (
cardId: CardID,
messageId: MessageID,
data: AttachmentData[],
socialId: SocialID,
date?: Date
): Promise<void> {
await this.event(
{
type: MessageEventType.AttachmentPatch,
cardId,
messageId,
operations: [
{
opcode: 'add',
attachments: data
}
],
socialId,
date
},
socialId
)
}
async removeAttachments (
cardId: CardID,
messageId: MessageID,
ids: AttachmentID[],
socialId: SocialID,
date?: Date
): Promise<void> {
await this.event(
{
type: MessageEventType.AttachmentPatch,
cardId,
messageId,
operations: [
{
opcode: 'remove',
ids
}
],
socialId,
date
},
socialId
)
}
async setAttachments (
cardId: CardID,
messageId: MessageID,
data: AttachmentData[],
socialId: SocialID,
date?: Date
): Promise<void> {
await this.event(
{
type: MessageEventType.AttachmentPatch,
cardId,
messageId,
operations: [
{
opcode: 'set',
attachments: data
}
],
socialId,
date
},
socialId
)
}
async findNotificationContexts (params: FindNotificationContextParams): Promise<NotificationContext[]> {
return await this.find<NotificationContext>('findNotificationContexts', params)
}
async findNotifications (params: FindNotificationsParams): Promise<Notification[]> {
return await this.find<Notification>('findNotifications', params)
}
async findMessagesMeta (params: FindMessagesMetaParams): Promise<MessageMeta[]> {
return await this.find<MessageMeta>('findMessagesMeta', params)
}
async findMessagesGroups (params: FindMessagesGroupParams): Promise<MessagesGroup[]> {
return await this.find<MessagesGroup>('findMessagesGroups', params)
}
}
@@ -1,95 +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 {
CreateMessageOptions,
CreateMessageResult,
EventResult,
UpdatePatchOptions,
Event
} from '@hcengineering/communication-sdk-types'
import type {
FindNotificationsParams,
FindNotificationContextParams,
FindMessagesMetaParams,
NotificationContext,
Notification,
MessageID,
MessageMeta,
Markdown,
SocialID,
MessageType,
CardID,
CardType,
MessageExtra,
AttachmentData,
AttachmentParams,
AttachmentID,
MessagesGroup,
FindMessagesGroupParams
} from '@hcengineering/communication-types'
export interface RestClient {
findMessagesMeta: (params: FindMessagesMetaParams) => Promise<MessageMeta[]>
findMessagesGroups: (params: FindMessagesGroupParams) => Promise<MessagesGroup[]>
findNotificationContexts: (params: FindNotificationContextParams) => Promise<NotificationContext[]>
findNotifications: (params: FindNotificationsParams) => Promise<Notification[]>
event: (event: Event, socialId: SocialID) => Promise<EventResult>
createMessage: (
cardId: CardID,
cardType: CardType,
content: Markdown,
type: MessageType,
extra: MessageExtra | undefined,
socialId: SocialID,
date?: Date,
messageId?: MessageID,
options?: CreateMessageOptions
) => Promise<CreateMessageResult>
updateMessage: (
cardId: CardID,
messageId: MessageID,
content: Markdown | undefined,
extra: MessageExtra | undefined,
socialId: SocialID,
date?: Date,
options?: UpdatePatchOptions
) => Promise<void>
removeMessage: (cardId: CardID, messageId: MessageID, socialId: SocialID, date?: Date) => Promise<void>
addAttachments: <P extends AttachmentParams>(
cardId: CardID,
messageId: MessageID,
data: AttachmentData<P>[],
socialId: SocialID,
date?: Date
) => Promise<void>
removeAttachments: (
cardId: CardID,
messageId: MessageID,
ids: AttachmentID[],
socialId: SocialID,
date?: Date
) => Promise<void>
setAttachments: <P extends AttachmentParams>(
cardId: CardID,
messageId: MessageID,
data: AttachmentData<P>[],
socialId: SocialID,
date?: Date
) => Promise<void>
}
@@ -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<T> (response: Response): Promise<T> {
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
}
@@ -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"]
}
@@ -1,7 +0,0 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}
@@ -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`"
}
]
}
}
]
}
@@ -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
@@ -1,4 +0,0 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig"
}

Some files were not shown because too many files have changed in this diff Show More