Sync with foundation

Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
Artem Savchenko
2026-02-22 13:28:58 +07:00
parent 7f501456e3
commit bd8feb66b8
56 changed files with 594 additions and 94 deletions
+3 -3
View File
@@ -26,16 +26,16 @@
"devDependencies": {
"cross-env": "~7.0.3",
"@hcengineering/platform-rig": "^0.7.19",
"@types/node": "^22.15.29",
"@types/node": "^22.18.1",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^15.4.0",
"eslint": "^8.54.0",
"esbuild": "^0.25.9",
"esbuild": "^0.25.10",
"@typescript-eslint/parser": "^6.21.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
+21
View File
@@ -42,6 +42,7 @@ import type {
LoginInfo,
LoginInfoByToken,
LoginInfoRequestData,
InviteInfo,
LoginInfoWithWorkspaces,
MailboxInfo,
MailboxOptions,
@@ -124,7 +125,9 @@ export interface AccountClient {
}
) => Promise<string>
checkJoin: (inviteId: string) => Promise<WorkspaceLoginInfo>
joinByToken: (inviteId: string) => Promise<WorkspaceLoginInfo>
checkAutoJoin: (inviteId: string, firstName?: string, lastName?: string) => Promise<WorkspaceLoginInfo>
getInviteInfo: (inviteId: string) => Promise<InviteInfo>
getWorkspaceInfo: (updateLastVisit?: boolean) => Promise<WorkspaceInfoWithStatus>
getWorkspacesInfo: (workspaces: WorkspaceUuid[]) => Promise<WorkspaceInfoWithStatus[]>
updateLastVisit: (workspaces: WorkspaceUuid[]) => Promise<void>
@@ -577,6 +580,15 @@ class AccountClientImpl implements AccountClient {
return await this.rpc(request)
}
async joinByToken (inviteId: string): Promise<WorkspaceLoginInfo> {
const request = {
method: 'joinByToken' as const,
params: { inviteId }
}
return await this.rpc(request)
}
async checkAutoJoin (inviteId: string, firstName?: string, lastName?: string): Promise<WorkspaceLoginInfo> {
const request = {
method: 'checkAutoJoin' as const,
@@ -586,6 +598,15 @@ class AccountClientImpl implements AccountClient {
return await this.rpc(request)
}
async getInviteInfo (inviteId: string): Promise<InviteInfo> {
const request = {
method: 'getInviteInfo' as const,
params: { inviteId }
}
return await this.rpc(request)
}
async getWorkspacesInfo (ids: WorkspaceUuid[]): Promise<WorkspaceInfoWithStatus[]> {
const request = {
method: 'getWorkspacesInfo' as const,
+5
View File
@@ -80,6 +80,11 @@ export interface WorkspaceInviteInfo {
name?: string
}
/** Public invite details from getInviteInfo (no auth required). */
export interface InviteInfo {
workspaceName: string | null
}
export interface OtpInfo {
sent: boolean
retryOn: Timestamp
+1 -1
View File
@@ -32,7 +32,7 @@
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
+1 -1
View File
@@ -32,7 +32,7 @@
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
+3 -3
View File
@@ -40,12 +40,12 @@
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"ts-node": "^10.8.0",
"@types/node": "^22.15.29",
"ts-node": "^10.9.2",
"@types/node": "^22.18.1",
"@types/jest": "^29.5.5",
"@types/ws": "^8.5.12",
"@types/snappyjs": "^0.7.1",
+2 -2
View File
@@ -32,13 +32,13 @@
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5",
"@types/snappyjs": "^0.7.1",
"@types/node": "^22.15.29",
"@types/node": "^22.18.1",
"ws": "^8.18.2",
"@types/ws": "^8.5.12",
"eslint-plugin-svelte": "^2.35.1"
+2 -2
View File
@@ -32,12 +32,12 @@
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5",
"@types/node": "^22.15.29",
"@types/node": "^22.18.1",
"eslint-plugin-svelte": "^2.35.1"
},
"dependencies": {
+3 -3
View File
@@ -26,16 +26,16 @@
"devDependencies": {
"cross-env": "~7.0.3",
"@hcengineering/platform-rig": "^0.7.19",
"@types/node": "^22.15.29",
"@types/node": "^22.18.1",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^15.4.0",
"eslint": "^8.54.0",
"esbuild": "^0.25.9",
"esbuild": "^0.25.10",
"@typescript-eslint/parser": "^6.21.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
+4 -1
View File
@@ -61,12 +61,15 @@
"ArchiveSpaceDescription": "Umožňuje uživatelům archivovat prostor",
"AutoJoin": "Automatické připojení",
"AutoJoinDescr": "Automaticky připojit nové zaměstnance k tomuto prostoru",
"RBAC": "Řízení přístupu na základě rolí",
"RBACDescr": "Vyžadovat řízení přístupu na základě rolí pro provádění akcí v tomto prostoru",
"BlobSize": "Velikost",
"BlobContentType": "Typ obsahu",
"Relation": "Vztah",
"Relations": "Vztahy",
"AddRelation": "Přidat vztah",
"PersonId": "Osoba",
"AccountId": "Účet"
"AccountId": "Účet",
"Version": "Verze"
}
}
+4 -1
View File
@@ -61,12 +61,15 @@
"ArchiveSpaceDescription": "Gewährt Benutzern die Möglichkeit, den Arbeitsbereich zu archivieren",
"AutoJoin": "Automatisch beitreten",
"AutoJoinDescr": "Neue Mitarbeiter automatisch diesem Arbeitsbereich hinzufügen",
"RBAC": "Rollenbasierte Zugriffskontrolle",
"RBACDescr": "Erfordert rollenbasierten Zugriff, um Aktionen in diesem Arbeitsbereich auszuführen",
"BlobSize": "Größe",
"BlobContentType": "Inhaltstyp",
"Relation": "Beziehung",
"Relations": "Beziehungen",
"AddRelation": "Beziehung hinzufügen",
"PersonId": "Person",
"AccountId": "Konto"
"AccountId": "Konto",
"Version": "Version"
}
}
+4 -1
View File
@@ -61,12 +61,15 @@
"ArchiveSpaceDescription": "Grants users ability to archive the space",
"AutoJoin": "Auto join",
"AutoJoinDescr": "Automatically join new employees to this space",
"RBAC": "Role-based access control",
"RBACDescr": "Require role-based access to perform actions in this space",
"BlobSize": "Size",
"BlobContentType": "Content type",
"Relation": "Relation",
"Relations": "Relations",
"AddRelation": "Add relation",
"PersonId": "Person",
"AccountId": "Account"
"AccountId": "Account",
"Version": "Version"
}
}
+4 -1
View File
@@ -54,12 +54,15 @@
"ArchiveSpaceDescription": "Concede a los usuarios la capacidad de archivar el espacio",
"AutoJoin": "Auto unirse",
"AutoJoinDescr": "Unirse automáticamente a los nuevos empleados a este espacio",
"RBAC": "Control de acceso basado en roles",
"RBACDescr": "Requerir acceso basado en roles para realizar acciones en este espacio",
"BlobSize": "Tamaño",
"BlobContentType": "Tipo de contenido",
"Relation": "Relación",
"Relations": "Relaciones",
"AddRelation": "Añadir relación",
"PersonId": "Id. de persona",
"AccountId": "Cuenta"
"AccountId": "Cuenta",
"Version": "Versión"
}
}
+4 -1
View File
@@ -61,12 +61,15 @@
"ArchiveSpaceDescription": "Accorde aux utilisateurs la capacité d'archiver l'espace",
"AutoJoin": "Rejoindre automatiquement",
"AutoJoinDescr": "Ajouter automatiquement les nouveaux employés à cet espace",
"RBAC": "Contrôle d'accès basé sur les rôles",
"RBACDescr": "Exiger un accès basé sur les rôles pour effectuer des actions dans cet espace",
"BlobSize": "Taille",
"BlobContentType": "Type de contenu",
"Relation": "Relation",
"Relations": "Relations",
"AddRelation": "Ajouter une relation",
"PersonId": "Id de personne",
"AccountId": "Compte"
"AccountId": "Compte",
"Version": "Version"
}
}
+4 -1
View File
@@ -61,12 +61,15 @@
"ArchiveSpaceDescription": "Concede agli utenti la possibilità di archiviare lo spazio",
"AutoJoin": "Partecipazione automatica",
"AutoJoinDescr": "Aggiungi automaticamente i nuovi dipendenti a questo spazio",
"RBAC": "Controllo accessi basato sui ruoli",
"RBACDescr": "Richiedi accesso basato sui ruoli per eseguire azioni in questo spazio",
"BlobSize": "Dimensione",
"BlobContentType": "Tipo di contenuto",
"Relation": "Relazione",
"Relations": "Relazioni",
"AddRelation": "Aggiungi relazione",
"PersonId": "ID persona",
"AccountId": "ID account"
"AccountId": "ID account",
"Version": "Versione"
}
}
+4 -1
View File
@@ -61,12 +61,15 @@
"ArchiveSpaceDescription": "ユーザーにスペースをアーカイブする権限を付与します",
"AutoJoin": "自動参加",
"AutoJoinDescr": "新しいユーザーを自動的にこのスペースに参加させます",
"RBAC": "ロールベースアクセス制御",
"RBACDescr": "このスペースでの操作を実行するためにロールベースのアクセスを要求します",
"BlobSize": "サイズ",
"BlobContentType": "コンテンツタイプ",
"Relation": "関係",
"Relations": "関係一覧",
"AddRelation": "関係を追加",
"PersonId": "人物",
"AccountId": "アカウント"
"AccountId": "アカウント",
"Version": "バージョン"
}
}
+68
View File
@@ -0,0 +1,68 @@
{
"string": {
"Id": "Id",
"Space": "Espaço",
"Spaces": "Espaços",
"SpacesDescription": "Gerenciamento do tipo de espaço para todos os espaços",
"Modified": "Modificado",
"ModifiedDate": "Data de modificação",
"ModifiedBy": "Modificado por",
"Class": "Classe",
"AttachedTo": "Anexado à",
"AttachedToClass": "Classe anexada",
"Name": "Nome",
"Description": "Descrição",
"Private": "Privado",
"Archived": "Arquivado",
"ClassLabel": "Tipo",
"ClassPropertyLabel": "Legenda",
"String": "Texto",
"Markup": "Marcação",
"Number": "Número",
"Boolean": "Caixa de seleção",
"Timestamp": "Marca de data/hora",
"Date": "Data",
"IntlString": "Texto internacional",
"Ref": "Referência",
"Collection": "Coleção",
"Array": "Seleção múltipla",
"Enum": "Seleção",
"Members": "Membros",
"Hyperlink": "URL",
"MarkupBlobRef": "Colaborativo",
"Object": "Objeto",
"System": "Sistema",
"CreatedBy": "Criado por",
"CreatedDate": "Data de criação",
"Status": "Estado",
"StatusCategory": "Categoria do estado",
"Account": "Conta",
"Rank": "Ranking",
"Owners": "Proprietários",
"Permission": "Permissão",
"CreateObject": "Criar objeto",
"UpdateObject": "Atualizar objeto",
"DeleteObject": "Apagar objeto",
"ForbidDeleteObject": "Proibir apagar objeto",
"UpdateSpace": "Atualizar espaço",
"ArchiveSpace": "Arquivar espaço",
"CreateObjectDescription": "Concede aos usuários a capacidade de criar objetos no espaço",
"UpdateObjectDescription": "Concede aos usuários a capacidade de atualizar objetos no espaço",
"DeleteObjectDescription": "Concede aos usuários a capacidade de apagar objetos no espaço",
"ForbidDeleteObjectDescription": "Proíbe aos usuários a capacidade de apagar objetos no espaço",
"UpdateSpaceDescription": "Concede aos usuários a capacidade de atualizar o espaço",
"ArchiveSpaceDescription": "Concede aos usuários a capacidade de arquivar o espaço",
"AutoJoin": "Auto adesão",
"AutoJoinDescr": "Adesão automática de novos funcionários a este espaço",
"RBAC": "Controle de acesso baseado em funções",
"RBACDescr": "Exigir acesso baseado em funções para executar ações neste espaço",
"BlobSize": "Tamanho",
"BlobContentType": "Tipo de conteúdo",
"Relation": "Relação",
"Relations": "Relações",
"AddRelation": "Adicionar relação",
"PersonId": "ID de pessoa",
"AccountId": "Conta",
"Version": "Versão"
}
}
+4 -1
View File
@@ -54,12 +54,15 @@
"ArchiveSpaceDescription": "Concede aos usuários a capacidade de arquivar o espaço",
"AutoJoin": "Auto adesão",
"AutoJoinDescr": "Adesão automática de novos funcionários a este espaço",
"RBAC": "Controle de acesso baseado em funções",
"RBACDescr": "Exigir acesso baseado em funções para executar ações neste espaço",
"BlobSize": "Tamanho",
"BlobContentType": "Tipo de conteúdo",
"Relation": "Relação",
"Relations": "Relações",
"AddRelation": "Adicionar relação",
"PersonId": "ID de pessoa",
"AccountId": "Conta"
"AccountId": "Conta",
"Version": "Versão"
}
}
+4 -1
View File
@@ -61,12 +61,15 @@
"ArchiveSpaceDescription": "Дает пользователям разрешение архивировать пространство",
"AutoJoin": "Автоприсоединение",
"AutoJoinDescr": "Автоматически присоединять новых сотрудников к этому пространству",
"RBAC": "Ролевой доступ",
"RBACDescr": "Требовать ролевой доступ для выполнения действий в этом пространстве",
"BlobSize": "Размер",
"BlobContentType": "Тип контента",
"Relation": "Связь",
"Relations": "Связи",
"AddRelation": "Добавить связь",
"PersonId": "Персона",
"AccountId": "Аккаунт"
"AccountId": "Аккаунт",
"Version": "Версия"
}
}
+4 -1
View File
@@ -61,12 +61,15 @@
"ArchiveSpaceDescription": "Kullanıcılara alanı arşivleme yetkisi verir",
"AutoJoin": "Otomatik katıl",
"AutoJoinDescr": "Yeni çalışanları bu alana otomatik olarak ekle",
"RBAC": "Rol tabanlı erişim kontrolü",
"RBACDescr": "Bu alanda işlemleri gerçekleştirmek için rol tabanlı erişim gerektir",
"BlobSize": "Boyut",
"BlobContentType": "İçerik türü",
"Relation": "İlişki",
"Relations": "İlişkiler",
"AddRelation": "İlişki ekle",
"PersonId": "Kişi",
"AccountId": "Hesap"
"AccountId": "Hesap",
"Version": "Sürüm"
}
}
+4 -1
View File
@@ -61,12 +61,15 @@
"ArchiveSpaceDescription": "授予用户归档空间的权限",
"AutoJoin": "自动加入",
"AutoJoinDescr": "自动将新员工加入此空间",
"RBAC": "基于角色的访问控制",
"RBACDescr": "要求基于角色的访问权限以在此空间中执行操作",
"BlobSize": "大小",
"BlobContentType": "內容類型",
"Relation": "关系",
"Relations": "关系",
"AddRelation": "添加关系",
"PersonId": "人员 ID",
"AccountId": "帐户"
"AccountId": "帐户",
"Version": "版本"
}
}
+2 -2
View File
@@ -36,12 +36,12 @@
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5",
"@types/node": "^22.15.29",
"@types/node": "^22.18.1",
"eslint-plugin-svelte": "^2.35.1"
},
"dependencies": {
+150 -1
View File
@@ -14,7 +14,7 @@
//
import { type Client, type DomainParams, type DomainRequestOptions, type DomainResult } from '..'
import type { Class, Doc, Obj, OperationDomain, Ref } from '../classes'
import type { Class, Doc, Obj, OperationDomain, Ref, Space } from '../classes'
import core from '../component'
import { Hierarchy } from '../hierarchy'
import { ModelDb, TxDb } from '../memdb'
@@ -293,6 +293,155 @@ describe('memdb', () => {
expect(result2).toHaveLength(1)
})
it('check associations', async () => {
const { model } = await createModel()
const operations = new TxOperations(model, core.account.System)
const association = await operations.findOne(core.class.Association, {})
if (association == null) {
throw new Error('Association not found')
}
const spaces = await operations.findAll(core.class.Space, {})
expect(spaces).toHaveLength(2)
const first = await operations.addCollection(
test.class.TestComment,
core.space.Model,
spaces[0]._id,
spaces[0]._class,
'comments',
{
message: 'msg'
}
)
const second = await operations.addCollection(
test.class.TestComment,
core.space.Model,
first,
test.class.TestComment,
'comments',
{
message: 'msg2'
}
)
await operations.createDoc(core.class.Relation, '' as Ref<Space>, {
docA: first,
docB: second,
association: association._id
})
const r = await operations.findAll(
test.class.TestComment,
{ _id: first },
{
associations: [[association._id, 1]]
}
)
expect(r.length).toEqual(1)
expect((r[0].$associations?.[association._id + '_b'][0] as any)?._id).toEqual(second)
})
it('check deep associations', async () => {
const { model } = await createModel()
const operations = new TxOperations(model, core.account.System)
const association = await operations.findOne(core.class.Association, {})
if (association == null) {
throw new Error('Association not found')
}
const spaces = await operations.findAll(core.class.Space, {})
expect(spaces).toHaveLength(2)
const zero = await operations.addCollection(
test.class.TestComment,
core.space.Model,
spaces[0]._id,
spaces[0]._class,
'comments',
{
message: 'msg'
}
)
const first = await operations.addCollection(
test.class.TestComment,
core.space.Model,
spaces[0]._id,
spaces[0]._class,
'comments',
{
message: 'msg'
}
)
const second = await operations.addCollection(
test.class.TestComment,
core.space.Model,
first,
test.class.TestComment,
'comments',
{
message: 'msg2'
}
)
const second2 = await operations.addCollection(
test.class.TestComment,
core.space.Model,
first,
test.class.TestComment,
'comments',
{
message: 'msg2'
}
)
const third = await operations.addCollection(
test.class.TestComment,
core.space.Model,
spaces[0]._id,
spaces[0]._class,
'comments',
{
message: 'msg3'
}
)
await operations.createDoc(core.class.Relation, '' as Ref<Space>, {
docA: first,
docB: second,
association: association._id
})
await operations.createDoc(core.class.Relation, '' as Ref<Space>, {
docA: first,
docB: second2,
association: association._id
})
await operations.createDoc(core.class.Relation, '' as Ref<Space>, {
docA: second,
docB: third,
association: association._id
})
const r = await operations.findAll(
test.class.TestComment,
{ _id: { $in: [zero, first] } },
{
associations: [[association._id, 1, [[association._id, 1]]]]
}
)
expect(r.length).toEqual(2)
expect(r[1].$associations?.[`${association._id}_b`]).toHaveLength(2)
expect((r[1].$associations?.[`${association._id}_b`][0] as any)?._id).toEqual(second)
expect(r[1].$associations?.[`${association._id}_b`][1]?.$associations?.[`${association._id}_b`]).toHaveLength(0)
expect(
(r[1].$associations?.[`${association._id}_b`][0]?.$associations?.[`${association._id}_b`][0] as any)?._id
).toEqual(third)
})
it('lookups', async () => {
const { model } = await createModel()
+29 -1
View File
@@ -16,7 +16,7 @@
import type { IntlString, Plugin } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import type { Arr, AttachedDoc, Class, Data, Doc, Interface, Mixin, Obj, Ref, Space } from '../classes'
import { ClassifierKind, DOMAIN_MODEL } from '../classes'
import { ClassifierKind, DOMAIN_MODEL, DOMAIN_RELATION } from '../classes'
import core from '../component'
import type { DocumentUpdate, TxCUD, TxCreateDoc, TxRemoveDoc, TxUpdateDoc } from '../tx'
import { DOMAIN_TX, TxFactory } from '../tx'
@@ -252,6 +252,34 @@ export function genMinModel (): TxCUD<Doc>[] {
})
)
txes.push(
createClass(core.class.Relation, {
label: 'Relation' as IntlString,
extends: core.class.Doc,
kind: ClassifierKind.CLASS,
domain: DOMAIN_RELATION
})
)
txes.push(
createClass(core.class.Association, {
label: 'Association' as IntlString,
extends: core.class.Doc,
kind: ClassifierKind.CLASS,
domain: DOMAIN_MODEL
})
)
txes.push(
createDoc(core.class.Association, {
nameA: 'my-assoc',
nameB: 'my-assoc',
classA: test.class.TestComment,
classB: test.class.TestComment,
type: '1:1'
})
)
txes.push(
createDoc(core.class.Space, {
name: 'Sp1',
+12 -1
View File
@@ -15,9 +15,9 @@
//
import type { Asset, IntlString, Plugin } from '@hcengineering/platform'
import { Tx } from '.'
import type { DocumentQuery } from './storage'
import { type WorkspaceDataId, type WorkspaceUuid } from './utils'
import { Tx } from '.'
/**
* @public
@@ -490,6 +490,7 @@ export interface SystemSpace extends Space {}
*/
export interface TypedSpace extends Space {
type: Ref<SpaceType>
restricted?: boolean // if true user must have permission to any txes
}
/**
@@ -551,6 +552,14 @@ export interface Permission extends Doc {
icon?: Asset
}
export interface AttributePermission extends Permission {
attribute: Ref<AnyAttribute>
}
export interface ClassPermission extends Permission {
targetClass: Ref<Class<Doc>>
}
/**
* @public
*/
@@ -935,8 +944,10 @@ export type SocialKey = Pick<SocialId, 'type' | 'value'>
export interface ClassCollaborators<T extends Doc> extends Doc {
attachedTo: Ref<Class<T>>
allFields?: boolean // for all (PersonId | Ref<Employee> | PersonId[] | Ref<Employee>[]) attributes
fields: (keyof T)[] // PersonId | Ref<Employee> | PersonId[] | Ref<Employee>[]
provideSecurity?: boolean // If true, will provide security for collaborators
provideAttachedSecurity?: boolean // If true, will provide security for collaborators of attached doc
}
export interface Collaborator extends AttachedDoc {
+17 -8
View File
@@ -15,18 +15,23 @@
import type { Asset, IntlString, Metadata, Plugin, StatusCode } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import type { BenchmarkDoc } from './benchmark'
import { AccountRole, TxAccessLevel } from './classes'
import type {
Account,
AccountUuid,
AnyAttribute,
ArrOf,
Association,
AttachedDoc,
AttributePermission,
Blob,
Class,
ClassCollaborators,
ClassPermission,
Collaborator,
Collection,
Configuration,
ConfigurationElement,
CustomSequence,
Doc,
DomainIndexConfiguration,
Enum,
@@ -49,7 +54,6 @@ import type {
Relation,
Role,
Sequence,
CustomSequence,
Space,
SpaceType,
SpaceTypeDescriptor,
@@ -60,11 +64,9 @@ import type {
TypeAny,
TypedSpace,
UserStatus,
Version,
AccountUuid,
ClassCollaborators,
Collaborator
Version
} from './classes'
import { AccountRole, TxAccessLevel } from './classes'
import { type Status, type StatusCategory } from './status'
import type {
Tx,
@@ -78,6 +80,7 @@ import type {
TxUpdateDoc,
TxWorkspaceEvent
} from './tx'
import { VersionableClass } from './versioning'
/**
* @public
@@ -129,6 +132,8 @@ export default plugin(coreId, {
SpaceType: '' as Ref<Class<SpaceType>>,
Role: '' as Ref<Class<Role>>,
Permission: '' as Ref<Class<Permission>>,
AttributePermission: '' as Ref<Class<AttributePermission>>,
ClassPermission: '' as Ref<Class<ClassPermission>>,
Type: '' as Ref<Class<Type<any>>>,
TypeRelation: '' as Ref<Class<Type<string>>>,
TypeString: '' as Ref<Class<Type<string>>>,
@@ -195,7 +200,8 @@ export default plugin(coreId, {
IndexConfiguration: '' as Ref<Mixin<IndexingConfiguration<Doc>>>,
SpacesTypeData: '' as Ref<Mixin<Space>>,
TransientConfiguration: '' as Ref<Mixin<TransientConfiguration>>,
TxAccessLevel: '' as Ref<Mixin<TxAccessLevel>>
TxAccessLevel: '' as Ref<Mixin<TxAccessLevel>>,
VersionableClass: '' as Ref<Mixin<VersionableClass>>
},
space: {
Tx: '' as Ref<Space>,
@@ -287,7 +293,10 @@ export default plugin(coreId, {
UpdateSpaceDescription: '' as IntlString,
ArchiveSpaceDescription: '' as IntlString,
AutoJoin: '' as IntlString,
AutoJoinDescr: '' as IntlString
AutoJoinDescr: '' as IntlString,
RBAC: '' as IntlString,
RBACDescr: '' as IntlString,
Version: '' as IntlString
},
descriptor: {
SpacesType: '' as Ref<SpaceTypeDescriptor>
+1
View File
@@ -43,5 +43,6 @@ export * from './common'
export * from './time'
export * from './benchmark'
export * from './collaborators'
export * from './versioning'
export default core
+7 -3
View File
@@ -159,7 +159,7 @@ export abstract class MemDb extends TxProcessor implements Storage {
private async getAssociationValue<T extends Doc>(
doc: T,
associations: AssociationQuery[]
): Promise<Record<string, Doc[]>> {
): Promise<Record<string, WithLookup<Doc>[]>> {
const result: Record<string, Doc[]> = {}
for (const association of associations) {
const _id = association[0]
@@ -170,8 +170,11 @@ export abstract class MemDb extends TxProcessor implements Storage {
const key2 = !isReverse ? 'docB' : 'docA'
const _class = !isReverse ? assoc.classB : assoc.classA
const relations = await this.findAll(core.class.Relation, { association: _id, [key]: doc._id })
const objects = await this.findAll(_class, { _id: { $in: relations.map((r) => r[key2]) } })
result[_id] = objects
let objects = await this.findAll(_class, { _id: { $in: relations.map((r) => r[key2]) } })
if (association[2] !== undefined) {
objects = toFindResult(await this.fillAssociations(objects, association[2]), objects.length)
}
result[`${_id}_${!isReverse ? 'b' : 'a'}`] = objects
}
return result
}
@@ -246,6 +249,7 @@ export abstract class MemDb extends TxProcessor implements Storage {
// We need to filter instances without mixin was set
result = result.filter((r) => (r as any)[_class] !== undefined)
}
if (options?.sort !== undefined) resultSort(result, options?.sort, _class, this.hierarchy, this)
const total = result.length
result = result.slice(0, options?.limit)
+11
View File
@@ -531,6 +531,17 @@ export class ApplyOperations extends TxOperations {
if (typeof window === 'object' && window !== null && this.measureName != null) {
console.log(`measure ${this.measureName}`, dnow - st, 'server time', result.serverTime)
}
if (!result.success) {
console.warn('ops.commit() failed', {
scope: this.scope,
measureName: this.measureName,
matchCount: this.matches.length,
notMatchCount: this.notMatches.length,
txCount: this.txes.length,
matches: this.matches.map((m) => ({ _class: m._class, query: m.query })),
notMatches: this.notMatches.map((m) => ({ _class: m._class, query: m.query }))
})
}
this.txes = []
return {
result: result.success,
+3
View File
@@ -114,6 +114,9 @@ const predicates: Record<string, PredicateFactory> = {
$size: (o, propertyKey) => {
return (docs) =>
execPredicate(docs, propertyKey, (value) => {
if (value == null) {
return false
}
if (!Array.isArray(value)) {
throw new Error('$size predicate requires array')
}
+6 -3
View File
@@ -18,6 +18,7 @@ import type { Asset, Resource } from '@hcengineering/platform'
import type { Association, AttachedDoc, Class, Doc, Domain, Ref, Space } from './classes'
import type { Tx } from './tx'
import type { KeysByType } from './utils'
import { VersionableDoc } from './versioning'
export type ArraySizeSelector =
| {
@@ -128,7 +129,7 @@ export type Projection<T extends Doc> = {
[P in keyof T]?: 0 | 1
}
export type AssociationQuery = [Ref<Association>, 1 | -1]
export type AssociationQuery = [Ref<Association>, 1 | -1] | [Ref<Association>, 1 | -1, AssociationQuery[]]
/**
* @public
@@ -205,7 +206,7 @@ export type LookupData<T extends Doc> = Partial<RefsAsDocs<T>>
*/
export type WithLookup<T extends Doc> = T & {
$lookup?: LookupData<T>
$associations?: Record<string, Doc[]>
$associations?: Record<string, WithLookup<Doc>[]>
$source?: {
$score: number // Score for document result
[key: string]: any
@@ -269,7 +270,9 @@ export interface SearchResultDoc {
description?: string
emojiIcon?: string
score?: number
doc: Pick<Doc, '_id' | '_class' | 'createdOn'> & Partial<Pick<AttachedDoc, 'attachedTo' | 'attachedToClass'>>
doc: Pick<Doc, '_id' | '_class' | 'createdOn'> &
Partial<Pick<AttachedDoc, 'attachedTo' | 'attachedToClass'>> &
Partial<Pick<VersionableDoc, 'baseId'>>
}
/**
+1 -1
View File
@@ -211,7 +211,7 @@ export function fillDefaults<T extends Doc> (
const baseClass = hierarchy.isDerived(_class, core.class.AttachedDoc) ? core.class.AttachedDoc : core.class.Doc
const attributes = hierarchy.getAllAttributes(_class, baseClass)
for (const attribute of attributes) {
if (attribute[1].defaultValue !== undefined) {
if (attribute[1].defaultValue != null) {
if ((object as any)[attribute[0]] === undefined) {
// Clone default value as it might be an object (e.g. array)
;(object as any)[attribute[0]] = structuredClone(attribute[1].defaultValue)
+13
View File
@@ -0,0 +1,13 @@
import { Class, Doc, PersonId, Ref } from './classes'
export interface VersionableDoc extends Doc {
baseId?: Ref<Doc>
version?: number
isLatest?: boolean
docCreatedBy?: PersonId
readonly?: boolean
}
export interface VersionableClass extends Class<Doc> {
enabled: boolean
}
+3 -3
View File
@@ -26,18 +26,18 @@
"devDependencies": {
"@hcengineering/platform-rig": "^0.7.19",
"@types/jest": "^29.5.5",
"@types/node": "^22.15.29",
"@types/node": "^22.18.1",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"cross-env": "~7.0.3",
"esbuild": "^0.25.9",
"esbuild": "^0.25.10",
"eslint": "^8.54.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.4.0",
"eslint-plugin-promise": "^6.1.1",
"jest": "^29.7.0",
"prettier": "^3.1.0",
"prettier": "^3.6.2",
"ts-jest": "^29.1.1",
"typescript": "^5.9.3",
"eslint-plugin-svelte": "^2.35.1"
@@ -42,7 +42,7 @@ describe('performance', () => {
// Overhead should be reasonable (typically < 50% for simple operations)
// This is informational rather than a strict assertion
expect(overheadPercentage).toBeLessThan(200)
expect(overheadPercentage).toBeLessThan(400)
})
it('should measure overhead with deep nested contexts', async () => {
@@ -154,7 +154,7 @@ describe('performance', () => {
console.log(` Overhead: ${overhead.toFixed(2)}ms (${overheadPercentage.toFixed(2)}%)`)
// NoMetricsContext should have very low overhead
expect(overheadPercentage).toBeLessThan(50)
expect(overheadPercentage).toBeLessThan(100)
})
})
@@ -195,7 +195,7 @@ describe('performance', () => {
console.log(` Collected operations: ${aggregated.measurements.request?.operations ?? 0}`)
expect(aggregated.measurements.request).toBeDefined()
expect(overheadPercentage).toBeLessThan(100) // Should be less than 100% overhead
expect(overheadPercentage).toBeLessThan(200) // Should be less than 100% overhead
})
})
})
+2 -2
View File
@@ -27,12 +27,12 @@
"@types/toposort": "^2.0.3",
"@typescript-eslint/parser": "^6.21.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5",
"@types/node": "^22.15.29",
"@types/node": "^22.18.1",
"eslint-plugin-svelte": "^2.35.1"
},
"dependencies": {
+31
View File
@@ -0,0 +1,31 @@
{
"status": {
"LoadingPlugin": "Carregando plugin {plugin}...",
"UnknownError": "Erro desconhecido: {message}",
"InvalidId": "Id inválido: {id}",
"BadRequest": "Pedido inválido",
"Forbidden": "Proibido",
"Conflict": "Conflito",
"ExpiredLink": "Este link de convite expirou",
"Unauthorized": "Não autorizado",
"UnknownMethod": "Método desconhecido: {method}",
"InternalServerError": "Erro interno do servidor",
"MaintenanceWarning": "Manutenção programada dentro de",
"MaintenanceWarningTime": "{time, plural, =1 {menos de um minuto} other {# minutos}}",
"AccountNotFound": "Conta não encontrada",
"AccountNotConfirmed": "Conta não confirmada",
"WorkspaceNotFound": "Espaço de trabalho não encontrado",
"InvalidPassword": "Senha inválida",
"AccountAlreadyExists": "Conta já existe",
"WorkspaceRateLimit": "O servidor está ocupado. Por favor, espere um pouco e tente novamente",
"AccountAlreadyConfirmed": "Conta já confirmada",
"WorkspaceAlreadyExists": "Espaço de trabalho já existe",
"InvalidOtp": "Código inválido",
"PasswordLoginLocked": "O login com senha está bloqueado devido a muitas tentativas incorretas. Use um método de login OTP para desbloquear sua conta.",
"InviteNotFound": "Convite com e-mail:{email} não encontrado.",
"WorkspaceLimitReached": "Você atingiu o limite de espaços de trabalho. Entre em contato conosco...",
"ReadOnlyAccount": "Demonstração anônima de convidado",
"SystemAccount": "Conta do sistema",
"SocialIdAlreadyExists": "ID social já existe"
}
}
+1 -1
View File
@@ -35,7 +35,7 @@
"eslint-plugin-n": "^15.4.0",
"eslint-plugin-promise": "^6.1.1",
"jest": "^29.7.0",
"prettier": "^3.1.0",
"prettier": "^3.6.2",
"ts-jest": "^29.1.1",
"typescript": "^5.9.3",
"eslint-plugin-svelte": "^2.35.1"
+1 -1
View File
@@ -32,7 +32,7 @@
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5",
"@types/node": "^22.15.29",
"@types/node": "^22.18.1",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint": "^8.54.0",
+1 -1
View File
@@ -32,7 +32,7 @@
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
+97 -18
View File
@@ -16,6 +16,7 @@
import { Analytics } from '@hcengineering/analytics'
import core, {
Association,
AssociationQuery,
BulkUpdateEvent,
Class,
Client,
@@ -780,7 +781,9 @@ export class LiveQuery implements WithTx, Client {
}
const docs = q.result.getDocs()
for (const doc of docs) {
const docToUpdate = doc.$associations?.[association._id]?.find((it) => it._id === tx.objectId)
const docToUpdate =
doc.$associations?.[`${association._id}_a`]?.find((it) => it._id === tx.objectId) ??
doc.$associations?.[`${association._id}_b`]?.find((it) => it._id === tx.objectId)
if (docToUpdate !== undefined) {
if (tx._class === core.class.TxMixin) {
TxProcessor.updateMixin4Doc(docToUpdate, tx as TxMixin<Doc, Doc>)
@@ -1114,31 +1117,90 @@ export class LiveQuery implements WithTx, Client {
if (q.options?.associations === undefined) return
if (doc._class !== core.class.Relation) return
const relation = doc as Relation
const assoc = q.options.associations.find((p) => p[0] === relation.association)
const assoc = findAssociation(q.options.associations, relation.association)
if (assoc !== undefined) {
if (q.result instanceof Promise) {
q.result = await q.result
}
const direct = assoc[1] === 1
const res = q.result.findDoc(direct ? relation.docA : relation.docB)
if (res === undefined) return
const association = this.getModel().findObject(assoc[0])
if (association === undefined) return
const docToPush = await this.findOne(direct ? association.classB : association.classA, {
_id: direct ? relation.docB : relation.docA
})
if (docToPush === undefined) return
const arr = res?.$associations?.[relation.association] ?? []
arr.push(docToPush)
if (res?.$associations === undefined) {
res.$associations = {}
const res = await this.fillRelationDoc(q.result, q.result.getDocs(), q.options.associations, relation)
if (res) {
this.queriesToUpdate.set(q.id, q)
}
res.$associations[relation.association] = arr
q.result.updateDoc(res, false)
this.queriesToUpdate.set(q.id, q)
}
}
async fillRelationDoc (
qRes: ResultArray,
docs: WithLookup<Doc>[],
associations: AssociationQuery[] | undefined,
relation: Relation
): Promise<boolean> {
if (associations === undefined) return false
let result = false
for (const assoc of associations) {
if (assoc[0] === relation.association) {
const association = this.getModel().findObject(assoc[0])
if (association === undefined) continue
const sourceId = assoc[1] === 1 ? relation.docA : relation.docB
const targetId = assoc[1] === 1 ? relation.docB : relation.docA
const direct = assoc[1] === 1
for (const doc of docs) {
if (doc._id === sourceId) {
const nestedAssoc = assoc[2]
const options: FindOptions<Doc> | undefined =
nestedAssoc !== undefined
? {
associations: nestedAssoc
}
: undefined
const docToPush = await this.findOne(
direct ? association.classB : association.classA,
{
_id: targetId
},
options
)
if (docToPush === undefined) continue
if (doc.$associations === undefined) {
doc.$associations = {}
}
const key = direct ? 'b' : 'a'
const arr = doc.$associations?.[`${assoc[0]}_${key}`] ?? []
const exists = arr.findIndex((p) => p._id === docToPush._id)
if (exists !== -1) {
arr[exists] = docToPush
} else {
arr.push(docToPush)
}
doc.$associations[`${assoc[0]}_${key}`] = arr
if (qRes.findDoc(doc._id) !== undefined) {
qRes.updateDoc(doc, false)
}
result = true
}
}
}
}
for (const assoc of associations) {
if (assoc[2] !== undefined) {
const key = assoc[1] === 1 ? 'b' : 'a'
for (const doc of docs) {
const arr = doc.$associations?.[`${assoc[0]}_${key}`] ?? []
if (arr.length > 0) {
const res = await this.fillRelationDoc(qRes, arr, assoc[2], relation)
if (res) {
result = true
if (qRes.findDoc(doc._id) !== undefined) {
qRes.updateDoc(doc, false)
}
}
}
}
}
}
return result
}
private async handleDocAddLookup (q: Query, doc: Doc): Promise<void> {
if (q.options?.lookup === undefined) return
const lookup = q.options.lookup
@@ -1608,3 +1670,20 @@ function getNestedLookup (lookup: Ref<Class<Doc>> | [Ref<Class<Doc>>, Lookup<Doc
function getLookupClass (lookup: Ref<Class<Doc>> | [Ref<Class<Doc>>, Lookup<Doc>]): Ref<Class<Doc>> {
return Array.isArray(lookup) ? lookup[0] : lookup
}
function findAssociation (
associations: AssociationQuery[],
association: Ref<Association>
): AssociationQuery | undefined {
for (const assoc of associations) {
if (assoc[0] === association) return assoc
}
for (const assoc of associations) {
if (assoc[2] !== undefined) {
const res = findAssociation(assoc[2], association)
if (res !== undefined) {
return assoc
}
}
}
}
+1 -1
View File
@@ -32,7 +32,7 @@
"eslint-plugin-n": "^15.4.0",
"eslint-plugin-promise": "^6.1.1",
"eslint": "^8.54.0",
"prettier": "^3.1.0",
"prettier": "^3.6.2",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5",
+2 -2
View File
@@ -26,9 +26,9 @@
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"@types/node": "^22.15.29",
"@types/node": "^22.18.1",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5",
+2 -2
View File
@@ -25,7 +25,7 @@
},
"devDependencies": {
"@hcengineering/platform-rig": "^0.7.19",
"@types/node": "^22.15.29",
"@types/node": "^22.18.1",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
@@ -33,7 +33,7 @@
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
+3 -3
View File
@@ -26,16 +26,16 @@
"devDependencies": {
"cross-env": "~7.0.3",
"@hcengineering/platform-rig": "^0.7.19",
"@types/node": "^22.15.29",
"@types/node": "^22.18.1",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^15.4.0",
"eslint": "^8.54.0",
"esbuild": "^0.25.9",
"esbuild": "^0.25.10",
"@typescript-eslint/parser": "^6.21.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
@@ -17,6 +17,11 @@ import { concatLink } from '@hcengineering/core'
import { FileStorage, FileStorageUploadOptions } from '../types'
import { uploadMultipart, uploadXhr } from '../upload'
const getPathname = (url: string): string => {
const base = window?.location?.href !== undefined ? window.location.href : 'http://localhost'
return new URL(url, base).pathname
}
/** @public */
export class DatalakeStorage implements FileStorage {
constructor (private readonly baseUrl: string) {}
@@ -26,6 +31,11 @@ export class DatalakeStorage implements FileStorage {
return concatLink(this.baseUrl, path)
}
getCookiePath (workspace: string): string {
const url = concatLink(this.baseUrl, `/blob/${workspace}`)
return getPathname(url)
}
async getFileMeta (token: string, workspace: string, file: string): Promise<Record<string, any>> {
const url = concatLink(this.baseUrl, `/meta/${encodeURIComponent(workspace)}/${encodeURIComponent(file)}`)
try {
@@ -17,6 +17,11 @@ import { concatLink } from '@hcengineering/core'
import { FileStorage, FileStorageUploadOptions } from '../types'
import { uploadXhr } from '../upload'
const getPathname = (url: string): string => {
const base = window?.location?.href !== undefined ? window.location.href : 'http://localhost'
return new URL(url, base).pathname
}
/** @public */
export class FrontStorage implements FileStorage {
constructor (private readonly baseUrl: string) {}
@@ -26,6 +31,11 @@ export class FrontStorage implements FileStorage {
return concatLink(this.baseUrl, path)
}
getCookiePath (workspace: string): string {
const url = concatLink(this.baseUrl, `/${workspace}`)
return getPathname(url)
}
async getFileMeta (token: string, workspace: string, file: string): Promise<Record<string, any>> {
return {}
}
@@ -17,6 +17,11 @@ import { concatLink } from '@hcengineering/core'
import { FileStorage, FileStorageUploadOptions } from '../types'
import { uploadXhr } from '../upload'
const getPathname = (url: string): string => {
const base = window?.location?.href !== undefined ? window.location.href : 'http://localhost'
return new URL(url, base).pathname
}
/** @public */
export class HulylakeStorage implements FileStorage {
constructor (private readonly baseUrl: string) {}
@@ -26,6 +31,11 @@ export class HulylakeStorage implements FileStorage {
return concatLink(this.baseUrl, path)
}
getCookiePath (workspace: string): string {
const url = concatLink(this.baseUrl, `/api/${workspace}`)
return getPathname(url)
}
async getFileMeta (token: string, workspace: string, file: string): Promise<Record<string, any>> {
return {}
}
+1
View File
@@ -29,6 +29,7 @@ export interface FileStorageUploadOptions {
/** @public */
export interface FileStorage {
getFileUrl: (workspace: string, file: string, filename?: string) => string
getCookiePath: (workspace: string) => string
getFileMeta: (token: string, workspace: string, file: string) => Promise<Record<string, any>>
uploadFile: (
token: string,
+2 -2
View File
@@ -26,12 +26,12 @@
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5",
"@types/node": "^22.15.29",
"@types/node": "^22.18.1",
"eslint-plugin-svelte": "^2.35.1"
},
"dependencies": {
+2 -2
View File
@@ -32,13 +32,13 @@
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5",
"@types/markdown-it": "~13.0.0",
"jest-environment-jsdom": "^29.7.0",
"jest-environment-jsdom": "^30.2.0",
"eslint-plugin-svelte": "^2.35.1"
},
"dependencies": {
+1 -1
View File
@@ -32,7 +32,7 @@
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
+1 -1
View File
@@ -32,7 +32,7 @@
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
+2 -2
View File
@@ -32,12 +32,12 @@
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5",
"jest-environment-jsdom": "^29.7.0",
"jest-environment-jsdom": "^30.2.0",
"fast-equals": "^5.2.2",
"y-prosemirror": "^1.3.7",
"eslint-plugin-svelte": "^2.35.1"
+1 -1
View File
@@ -32,7 +32,7 @@
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
+2 -2
View File
@@ -25,7 +25,7 @@
},
"devDependencies": {
"@hcengineering/platform-rig": "^0.7.19",
"@types/node": "^22.15.29",
"@types/node": "^22.18.1",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
@@ -33,7 +33,7 @@
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",