mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-07 18:27:44 +02:00
remove to prepare for subtree
Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
0.1.195
|
||||
@@ -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.
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"changes": [
|
||||
{
|
||||
"packageName": "@hcengineering/communication-rest-client",
|
||||
"comment": "update deps",
|
||||
"type": "patch"
|
||||
}
|
||||
],
|
||||
"packageName": "@hcengineering/communication-rest-client"
|
||||
}
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"changes": [
|
||||
{
|
||||
"packageName": "@hcengineering/communication-sdk-types",
|
||||
"comment": "update deps",
|
||||
"type": "patch"
|
||||
}
|
||||
],
|
||||
"packageName": "@hcengineering/communication-sdk-types"
|
||||
}
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"changes": [
|
||||
{
|
||||
"packageName": "@hcengineering/communication-server",
|
||||
"comment": "update deps",
|
||||
"type": "patch"
|
||||
}
|
||||
],
|
||||
"packageName": "@hcengineering/communication-server"
|
||||
}
|
||||
-10
@@ -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 }
|
||||
}
|
||||
}
|
||||
-6444
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 ==="
|
||||
@@ -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
Reference in New Issue
Block a user