Support for rating system (#10124)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev
2025-10-24 10:10:01 +07:00
committed by GitHub
parent 768ff39ab1
commit ffd8d90378
137 changed files with 5005 additions and 712 deletions
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}
+4
View File
@@ -0,0 +1,4 @@
*
!/lib/**
!CHANGELOG.md
/lib/**/__tests__/
+4
View File
@@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig"
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
roots: ["./src"],
coverageReporters: ["text-summary", "html"]
}
+46
View File
@@ -0,0 +1,46 @@
{
"name": "@hcengineering/server-rating",
"version": "0.7.0",
"main": "lib/index.js",
"svelte": "src/index.ts",
"types": "types/index.d.ts",
"files": [
"lib/**/*",
"types/**/*",
"tsconfig.json"
],
"author": "Anticrm Platform Contributors",
"license": "EPL-2.0",
"scripts": {
"build": "compile",
"build:watch": "compile",
"format": "format src",
"test": "jest --passWithNoTests --silent",
"_phase:build": "compile transpile src",
"_phase:test": "jest --passWithNoTests --silent",
"_phase:format": "format src",
"_phase:validate": "compile validate"
},
"devDependencies": {
"@hcengineering/platform-rig": "^0.7.19",
"@types/node": "^22.15.29",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^15.4.0",
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5"
},
"dependencies": {
"@hcengineering/core": "^0.7.10",
"@hcengineering/platform": "^0.7.5",
"@hcengineering/server-core": "^0.7.8",
"@hcengineering/rating": "^0.7.0"
}
}
+139
View File
@@ -0,0 +1,139 @@
//
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021, 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import core, {
TxProcessor,
type Doc,
type MeasureContext,
type Tx,
type TxCreateDoc,
type TxCUD,
type TxRemoveDoc,
type TxUpdateDoc
} from '@hcengineering/core'
import type { Plugin } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import rating, { ReactionKind, type DocReaction } from '@hcengineering/rating'
import {
BaseMiddleware,
type Middleware,
type PipelineContext,
type TxMiddlewareResult
} from '@hcengineering/server-core'
/**
* @public
*/
export const serverRatingId = 'server-rating' as Plugin
export class RatingMiddleware extends BaseMiddleware {
static async create (ctx: MeasureContext, context: PipelineContext, next?: Middleware): Promise<Middleware> {
return new RatingMiddleware(context, next)
}
async tx (ctx: MeasureContext, tx: Tx[]): Promise<TxMiddlewareResult> {
for (const t of tx) {
if (TxProcessor.isExtendsCUD(t._class)) {
const cud = t as TxCUD<Doc>
if (cud.objectClass === rating.class.DocRating || cud.objectClass === rating.class.PersonRating) {
throw new Error('Direct modifications of ratings are not allowed.')
}
const c = rating.class.DocReaction
if (cud.objectClass === c) {
// Star/Like,Rate values could only be used one per user.
switch (cud._class) {
case core.class.TxCreateDoc:
// Check for duplicate of like, rate value, Emojii
await this.validateNewReaction(ctx, cud as TxCreateDoc<DocReaction>)
break
case core.class.TxUpdateDoc:
// Disallow update for Emojii, Star, Like, Usefull
// Allow only for RateValue
await this.validateReactionUpdate(ctx, cud as TxUpdateDoc<DocReaction>)
break
case core.class.TxRemoveDoc:
await this.validateReactionRemove(ctx, cud as TxRemoveDoc<DocReaction>)
break
}
}
}
}
return await this.provideTx(ctx, tx)
}
private async validateNewReaction (ctx: MeasureContext, create: TxCreateDoc<DocReaction>): Promise<void> {
if (create.attributes.reactionType !== ReactionKind.Star) {
// Should allow only one reaction per document per user for Emoji, Like, Usefull
const current = await this.provideFindAll(ctx, rating.class.DocReaction, {
attachedTo: create.attachedTo,
attachedToClass: create.objectClass
})
if (
current.some(
(it) =>
it.reactionType === create.attributes.reactionType &&
(it.value === create.attributes.value ||
(create.attributes.reactionType === ReactionKind.Emoji && it.emoji === create.attributes.emoji))
)
) {
throw new Error('Duplicate emoji reaction is not allowed.')
}
} else {
// For stars, we probable need to merge them into one value.
}
}
private async validateReactionUpdate (ctx: MeasureContext, upd: TxUpdateDoc<DocReaction>): Promise<void> {
if (upd.operations.reactionType !== undefined) {
throw new Error('Modifications of reaction type are not allowed.')
}
// Find current reaction tried to be modified
const current = (
await this.provideFindAll(ctx, rating.class.DocReaction, {
_id: upd.objectId,
_class: upd.objectClass
})
).shift()
if (current === undefined) {
throw new Error('Reaction not found.')
}
if (upd.operations.value === undefined || upd.operations.value < 0 || upd.operations.value > 10) {
throw new Error('Reaction value modification is required.')
}
}
private async validateReactionRemove (ctx: MeasureContext, upd: TxRemoveDoc<DocReaction>): Promise<void> {
// Find current reaction tried to be modified
// const current = (
// await this.provideFindAll(ctx, rating.class.DocReaction, {
// _id: upd.objectId,
// _class: upd.objectClass
// })
// ).shift()
// if (current === undefined) {
// No error, already removed
// }
// if (current.reactionType === ReactionKind.Star) {
// throw new Error('Star reactions could not be removed.')
// }
}
}
/**
* @public
*/
export default plugin(serverRatingId, {})
+12
View File
@@ -0,0 +1,12 @@
{
"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"]
}