mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-21 16:09:12 +03:00
UBERF-7532: Bulk operations for triggers (#6023)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
fa82ee1939
commit
4eac1927f0
@ -13,11 +13,11 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { addLocation } from '@hcengineering/platform'
|
||||
import { devModelId } from '@hcengineering/devmodel'
|
||||
import { PresentationClientHook } from '@hcengineering/devmodel-resources'
|
||||
import login from '@hcengineering/login'
|
||||
import { setMetadata } from '@hcengineering/platform'
|
||||
import devmodel, { devModelId } from '@hcengineering/devmodel'
|
||||
import client from '@hcengineering/client'
|
||||
import { addLocation, setMetadata } from '@hcengineering/platform'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
|
||||
export function configurePlatformDevServer() {
|
||||
console.log('Use Endpoint override:', process.env.LOGIN_ENDPOINT)
|
||||
@ -28,6 +28,6 @@ export function configurePlatformDevServer() {
|
||||
}
|
||||
|
||||
function enableDevModel() {
|
||||
setMetadata(client.metadata.ClientHook, devmodel.hook.Hook)
|
||||
setMetadata(presentation.metadata.ClientHook, new PresentationClientHook())
|
||||
addLocation(devModelId, () => import(/* webpackChunkName: "devmodel" */ '@hcengineering/devmodel-resources'))
|
||||
}
|
||||
|
@ -122,9 +122,6 @@ describe('client', () => {
|
||||
clean: async (domain: Domain, docs: Ref<Doc>[]) => {},
|
||||
loadModel: async (last: Timestamp) => clone(txes),
|
||||
getAccount: async () => null as unknown as Account,
|
||||
measure: async () => {
|
||||
return async () => ({ time: 0, serverTime: 0 })
|
||||
},
|
||||
sendForceClose: async () => {}
|
||||
}
|
||||
}
|
||||
|
@ -73,7 +73,6 @@ export async function connect (handler: (tx: Tx) => void): Promise<ClientConnect
|
||||
clean: async (domain: Domain, docs: Ref<Doc>[]) => {},
|
||||
loadModel: async (last: Timestamp) => txes,
|
||||
getAccount: async () => null as unknown as Account,
|
||||
measure: async () => async () => ({ time: 0, serverTime: 0 }),
|
||||
sendForceClose: async () => {}
|
||||
}
|
||||
}
|
||||
|
@ -47,17 +47,10 @@ export interface Client extends Storage, FulltextStorage {
|
||||
close: () => Promise<void>
|
||||
}
|
||||
|
||||
export type MeasureDoneOperation = () => Promise<{ time: number, serverTime: number }>
|
||||
|
||||
export interface MeasureClient extends Client {
|
||||
// Will perform on server operation measure and will return a local client time and on server time
|
||||
measure: (operationName: string) => Promise<MeasureDoneOperation>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface AccountClient extends MeasureClient {
|
||||
export interface AccountClient extends Client {
|
||||
getAccount: () => Promise<Account>
|
||||
}
|
||||
|
||||
@ -97,11 +90,9 @@ export interface ClientConnection extends Storage, FulltextStorage, BackupClient
|
||||
// If hash is passed, will return LoadModelResponse
|
||||
loadModel: (last: Timestamp, hash?: string) => Promise<Tx[] | LoadModelResponse>
|
||||
getAccount: () => Promise<Account>
|
||||
|
||||
measure: (operationName: string) => Promise<MeasureDoneOperation>
|
||||
}
|
||||
|
||||
class ClientImpl implements AccountClient, BackupClient, MeasureClient {
|
||||
class ClientImpl implements AccountClient, BackupClient {
|
||||
notify?: (...tx: Tx[]) => void
|
||||
hierarchy!: Hierarchy
|
||||
model!: ModelDb
|
||||
@ -163,10 +154,6 @@ class ClientImpl implements AccountClient, BackupClient, MeasureClient {
|
||||
return result
|
||||
}
|
||||
|
||||
async measure (operationName: string): Promise<MeasureDoneOperation> {
|
||||
return await this.conn.measure(operationName)
|
||||
}
|
||||
|
||||
async updateFromRemote (...tx: Tx[]): Promise<void> {
|
||||
for (const t of tx) {
|
||||
try {
|
||||
|
@ -312,8 +312,8 @@ export class TxOperations implements Omit<Client, 'notify'> {
|
||||
return this.removeDoc(doc._class, doc.space, doc._id)
|
||||
}
|
||||
|
||||
apply (scope: string): ApplyOperations {
|
||||
return new ApplyOperations(this, scope)
|
||||
apply (scope: string, measure?: string): ApplyOperations {
|
||||
return new ApplyOperations(this, scope, measure)
|
||||
}
|
||||
|
||||
async diffUpdate<T extends Doc = Doc>(
|
||||
@ -423,6 +423,12 @@ export class TxOperations implements Omit<Client, 'notify'> {
|
||||
}
|
||||
}
|
||||
|
||||
export interface CommitResult {
|
||||
result: boolean
|
||||
time: number
|
||||
serverTime: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*
|
||||
@ -436,7 +442,8 @@ export class ApplyOperations extends TxOperations {
|
||||
notMatches: DocumentClassQuery<Doc>[] = []
|
||||
constructor (
|
||||
readonly ops: TxOperations,
|
||||
readonly scope: string
|
||||
readonly scope: string,
|
||||
readonly measureName?: string
|
||||
) {
|
||||
const txClient: Client = {
|
||||
getHierarchy: () => ops.client.getHierarchy(),
|
||||
@ -465,23 +472,28 @@ export class ApplyOperations extends TxOperations {
|
||||
return this
|
||||
}
|
||||
|
||||
async commit (notify: boolean = true, extraNotify: Ref<Class<Doc>>[] = []): Promise<boolean> {
|
||||
async commit (notify: boolean = true, extraNotify: Ref<Class<Doc>>[] = []): Promise<CommitResult> {
|
||||
if (this.txes.length > 0) {
|
||||
return (
|
||||
await ((await this.ops.tx(
|
||||
this.ops.txFactory.createTxApplyIf(
|
||||
core.space.Tx,
|
||||
this.scope,
|
||||
this.matches,
|
||||
this.notMatches,
|
||||
this.txes,
|
||||
notify,
|
||||
extraNotify
|
||||
)
|
||||
)) as Promise<TxApplyResult>)
|
||||
).success
|
||||
const st = Date.now()
|
||||
const result = await ((await this.ops.tx(
|
||||
this.ops.txFactory.createTxApplyIf(
|
||||
core.space.Tx,
|
||||
this.scope,
|
||||
this.matches,
|
||||
this.notMatches,
|
||||
this.txes,
|
||||
this.measureName,
|
||||
notify,
|
||||
extraNotify
|
||||
)
|
||||
)) as Promise<TxApplyResult>)
|
||||
return {
|
||||
result: result.success,
|
||||
time: Date.now() - st,
|
||||
serverTime: result.serverTime
|
||||
}
|
||||
}
|
||||
return true
|
||||
return { result: true, time: 0, serverTime: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -33,14 +33,15 @@ export interface StorageIterator {
|
||||
close: (ctx: MeasureContext) => Promise<void>
|
||||
}
|
||||
|
||||
export type BroadcastTargets = Record<string, (tx: Tx) => string[] | undefined>
|
||||
|
||||
export interface SessionOperationContext {
|
||||
ctx: MeasureContext
|
||||
// A parts of derived data to deal with after operation will be complete
|
||||
derived: {
|
||||
derived: Tx[]
|
||||
target?: string[]
|
||||
}[]
|
||||
|
||||
txes: Tx[]
|
||||
targets: BroadcastTargets // A set of broadcast filters if required
|
||||
}
|
||||
with: <T>(
|
||||
name: string,
|
||||
params: ParamsType,
|
||||
|
@ -28,13 +28,13 @@ import type {
|
||||
Space,
|
||||
Timestamp
|
||||
} from './classes'
|
||||
import { clone } from './clone'
|
||||
import core from './component'
|
||||
import { setObjectValue } from './objvalue'
|
||||
import { _getOperator } from './operator'
|
||||
import { _toDoc } from './proxy'
|
||||
import type { DocumentQuery, TxResult } from './storage'
|
||||
import { generateId } from './utils'
|
||||
import { clone } from './clone'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -137,10 +137,14 @@ export interface TxApplyIf extends Tx {
|
||||
|
||||
// If passed, will send WorkspaceEvent.BulkUpdate event with list of classes to update
|
||||
extraNotify?: Ref<Class<Doc>>[]
|
||||
|
||||
// If defined will go into a separate measure section
|
||||
measureName?: string
|
||||
}
|
||||
|
||||
export interface TxApplyResult {
|
||||
success: boolean
|
||||
serverTime: number
|
||||
}
|
||||
|
||||
/**
|
||||
@ -618,6 +622,7 @@ export class TxFactory {
|
||||
match: DocumentClassQuery<Doc>[],
|
||||
notMatch: DocumentClassQuery<Doc>[],
|
||||
txes: TxCUD<Doc>[],
|
||||
measureName: string | undefined,
|
||||
notify: boolean = true,
|
||||
extraNotify: Ref<Class<Doc>>[] = [],
|
||||
modifiedOn?: Timestamp,
|
||||
@ -634,6 +639,7 @@ export class TxFactory {
|
||||
match,
|
||||
notMatch,
|
||||
txes,
|
||||
measureName,
|
||||
notify,
|
||||
extraNotify
|
||||
}
|
||||
|
@ -8,8 +8,6 @@ import {
|
||||
type FindOptions,
|
||||
type FindResult,
|
||||
type Hierarchy,
|
||||
type MeasureClient,
|
||||
type MeasureDoneOperation,
|
||||
type ModelDb,
|
||||
type Ref,
|
||||
type SearchOptions,
|
||||
@ -65,7 +63,7 @@ export type PresentationMiddlewareCreator = (client: Client, next?: Presentation
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface PresentationPipeline extends MeasureClient, Exclude<PresentationMiddleware, 'next'> {
|
||||
export interface PresentationPipeline extends Client, Exclude<PresentationMiddleware, 'next'> {
|
||||
close: () => Promise<void>
|
||||
}
|
||||
|
||||
@ -75,7 +73,7 @@ export interface PresentationPipeline extends MeasureClient, Exclude<Presentatio
|
||||
export class PresentationPipelineImpl implements PresentationPipeline {
|
||||
private head: PresentationMiddleware | undefined
|
||||
|
||||
private constructor (readonly client: MeasureClient) {}
|
||||
private constructor (readonly client: Client) {}
|
||||
|
||||
getHierarchy (): Hierarchy {
|
||||
return this.client.getHierarchy()
|
||||
@ -89,11 +87,7 @@ export class PresentationPipelineImpl implements PresentationPipeline {
|
||||
await this.head?.notifyTx(...tx)
|
||||
}
|
||||
|
||||
async measure (operationName: string): Promise<MeasureDoneOperation> {
|
||||
return await this.client.measure(operationName)
|
||||
}
|
||||
|
||||
static create (client: MeasureClient, constructors: PresentationMiddlewareCreator[]): PresentationPipeline {
|
||||
static create (client: Client, constructors: PresentationMiddlewareCreator[]): PresentationPipeline {
|
||||
const pipeline = new PresentationPipelineImpl(client)
|
||||
pipeline.head = pipeline.buildChain(constructors)
|
||||
return pipeline
|
||||
|
@ -14,26 +14,64 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { type Mixin, type Class, type Ref } from '@hcengineering/core'
|
||||
import {
|
||||
type Class,
|
||||
type Client,
|
||||
type Doc,
|
||||
type DocumentQuery,
|
||||
type FindOptions,
|
||||
type FindResult,
|
||||
type Mixin,
|
||||
type Ref,
|
||||
type SearchOptions,
|
||||
type SearchQuery,
|
||||
type SearchResult,
|
||||
type Tx,
|
||||
type TxResult,
|
||||
type WithLookup
|
||||
} from '@hcengineering/core'
|
||||
import type { Asset, IntlString, Metadata, Plugin, StatusCode } from '@hcengineering/platform'
|
||||
import { plugin } from '@hcengineering/platform'
|
||||
import { type ComponentExtensionId } from '@hcengineering/ui'
|
||||
import { type PresentationMiddlewareFactory } from './pipeline'
|
||||
import type { PreviewConfig } from './preview'
|
||||
import {
|
||||
type ComponentPointExtension,
|
||||
type DocRules,
|
||||
type DocCreateExtension,
|
||||
type DocRules,
|
||||
type FilePreviewExtension,
|
||||
type ObjectSearchCategory,
|
||||
type InstantTransactions
|
||||
type InstantTransactions,
|
||||
type ObjectSearchCategory
|
||||
} from './types'
|
||||
import type { PreviewConfig } from './preview'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const presentationId = 'presentation' as Plugin
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface ClientHook {
|
||||
findAll: <T extends Doc>(
|
||||
client: Client,
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T>
|
||||
) => Promise<FindResult<T>>
|
||||
|
||||
findOne: <T extends Doc>(
|
||||
client: Client,
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T>
|
||||
) => Promise<WithLookup<T> | undefined>
|
||||
|
||||
tx: (client: Client, tx: Tx) => Promise<TxResult>
|
||||
|
||||
searchFulltext: (client: Client, query: SearchQuery, options: SearchOptions) => Promise<SearchResult>
|
||||
}
|
||||
|
||||
export default plugin(presentationId, {
|
||||
class: {
|
||||
ObjectSearchCategory: '' as Ref<Class<ObjectSearchCategory>>,
|
||||
@ -95,7 +133,8 @@ export default plugin(presentationId, {
|
||||
CollaboratorApiUrl: '' as Metadata<string>,
|
||||
Token: '' as Metadata<string>,
|
||||
FrontUrl: '' as Asset,
|
||||
PreviewConfig: '' as Metadata<PreviewConfig | undefined>
|
||||
PreviewConfig: '' as Metadata<PreviewConfig | undefined>,
|
||||
ClientHook: '' as Metadata<ClientHook>
|
||||
},
|
||||
status: {
|
||||
FileTooLarge: '' as StatusCode
|
||||
|
@ -33,8 +33,6 @@ import core, {
|
||||
type FindOptions,
|
||||
type FindResult,
|
||||
type Hierarchy,
|
||||
type MeasureClient,
|
||||
type MeasureDoneOperation,
|
||||
type Mixin,
|
||||
type Obj,
|
||||
type Blob as PlatformBlob,
|
||||
@ -65,7 +63,7 @@ export { reduceCalls } from '@hcengineering/core'
|
||||
|
||||
let liveQuery: LQ
|
||||
let rawLiveQuery: LQ
|
||||
let client: TxOperations & MeasureClient & OptimisticTxes
|
||||
let client: TxOperations & Client & OptimisticTxes
|
||||
let pipeline: PresentationPipeline
|
||||
|
||||
const txListeners: Array<(...tx: Tx[]) => void> = []
|
||||
@ -95,16 +93,15 @@ export interface OptimisticTxes {
|
||||
pendingCreatedDocs: Writable<Record<Ref<Doc>, boolean>>
|
||||
}
|
||||
|
||||
class UIClient extends TxOperations implements Client, MeasureClient, OptimisticTxes {
|
||||
class UIClient extends TxOperations implements Client, OptimisticTxes {
|
||||
hook = getMetadata(plugin.metadata.ClientHook)
|
||||
constructor (
|
||||
client: MeasureClient,
|
||||
client: Client,
|
||||
private readonly liveQuery: Client
|
||||
) {
|
||||
super(client, getCurrentAccount()._id)
|
||||
}
|
||||
|
||||
afterMeasure: Tx[] = []
|
||||
measureOp?: MeasureDoneOperation
|
||||
protected pendingTxes = new Set<Ref<Tx>>()
|
||||
protected _pendingCreatedDocs = writable<Record<Ref<Doc>, boolean>>({})
|
||||
|
||||
@ -113,34 +110,30 @@ class UIClient extends TxOperations implements Client, MeasureClient, Optimistic
|
||||
}
|
||||
|
||||
async doNotify (...tx: Tx[]): Promise<void> {
|
||||
if (this.measureOp !== undefined) {
|
||||
this.afterMeasure.push(...tx)
|
||||
} else {
|
||||
const pending = get(this._pendingCreatedDocs)
|
||||
let pendingUpdated = false
|
||||
tx.forEach((t) => {
|
||||
if (this.pendingTxes.has(t._id)) {
|
||||
this.pendingTxes.delete(t._id)
|
||||
const pending = get(this._pendingCreatedDocs)
|
||||
let pendingUpdated = false
|
||||
tx.forEach((t) => {
|
||||
if (this.pendingTxes.has(t._id)) {
|
||||
this.pendingTxes.delete(t._id)
|
||||
|
||||
// Only CUD tx can be pending now
|
||||
const innerTx = TxProcessor.extractTx(t) as TxCUD<Doc>
|
||||
// Only CUD tx can be pending now
|
||||
const innerTx = TxProcessor.extractTx(t) as TxCUD<Doc>
|
||||
|
||||
if (innerTx._class === core.class.TxCreateDoc) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete pending[innerTx.objectId]
|
||||
pendingUpdated = true
|
||||
}
|
||||
if (innerTx._class === core.class.TxCreateDoc) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete pending[innerTx.objectId]
|
||||
pendingUpdated = true
|
||||
}
|
||||
})
|
||||
if (pendingUpdated) {
|
||||
this._pendingCreatedDocs.set(pending)
|
||||
}
|
||||
|
||||
// We still want to notify about all transactions because there might be queries created after
|
||||
// the early applied transaction
|
||||
// For old queries there's a check anyway that prevents the same document from being added twice
|
||||
await this.provideNotify(...tx)
|
||||
})
|
||||
if (pendingUpdated) {
|
||||
this._pendingCreatedDocs.set(pending)
|
||||
}
|
||||
|
||||
// We still want to notify about all transactions because there might be queries created after
|
||||
// the early applied transaction
|
||||
// For old queries there's a check anyway that prevents the same document from being added twice
|
||||
await this.provideNotify(...tx)
|
||||
}
|
||||
|
||||
private async provideNotify (...tx: Tx[]): Promise<void> {
|
||||
@ -165,6 +158,9 @@ class UIClient extends TxOperations implements Client, MeasureClient, Optimistic
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T>
|
||||
): Promise<FindResult<T>> {
|
||||
if (this.hook !== undefined) {
|
||||
return await this.hook.findAll(this.liveQuery, _class, query, options)
|
||||
}
|
||||
return await this.liveQuery.findAll(_class, query, options)
|
||||
}
|
||||
|
||||
@ -173,12 +169,17 @@ class UIClient extends TxOperations implements Client, MeasureClient, Optimistic
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T>
|
||||
): Promise<WithLookup<T> | undefined> {
|
||||
if (this.hook !== undefined) {
|
||||
return await this.hook.findOne(this.liveQuery, _class, query, options)
|
||||
}
|
||||
return await this.liveQuery.findOne(_class, query, options)
|
||||
}
|
||||
|
||||
override async tx (tx: Tx): Promise<TxResult> {
|
||||
void this.notifyEarly(tx)
|
||||
|
||||
if (this.hook !== undefined) {
|
||||
return await this.hook.tx(this.client, tx)
|
||||
}
|
||||
return await this.client.tx(tx)
|
||||
}
|
||||
|
||||
@ -221,39 +222,24 @@ class UIClient extends TxOperations implements Client, MeasureClient, Optimistic
|
||||
}
|
||||
|
||||
async searchFulltext (query: SearchQuery, options: SearchOptions): Promise<SearchResult> {
|
||||
return await this.client.searchFulltext(query, options)
|
||||
}
|
||||
|
||||
async measure (operationName: string): Promise<MeasureDoneOperation> {
|
||||
// return await (this.client as MeasureClient).measure(operationName)
|
||||
const mop = await (this.client as MeasureClient).measure(operationName)
|
||||
this.measureOp = mop
|
||||
return async () => {
|
||||
const result = await mop()
|
||||
this.measureOp = undefined
|
||||
if (this.afterMeasure.length > 0) {
|
||||
const txes = this.afterMeasure
|
||||
this.afterMeasure = []
|
||||
for (const tx of txes) {
|
||||
await this.doNotify(tx)
|
||||
}
|
||||
}
|
||||
return result
|
||||
if (this.hook !== undefined) {
|
||||
return await this.hook.searchFulltext(this.client, query, options)
|
||||
}
|
||||
return await this.client.searchFulltext(query, options)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function getClient (): TxOperations & MeasureClient & OptimisticTxes {
|
||||
export function getClient (): TxOperations & Client & OptimisticTxes {
|
||||
return client
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function setClient (_client: MeasureClient): Promise<void> {
|
||||
export async function setClient (_client: Client): Promise<void> {
|
||||
if (liveQuery !== undefined) {
|
||||
await liveQuery.close()
|
||||
}
|
||||
@ -276,6 +262,7 @@ export async function setClient (_client: MeasureClient): Promise<void> {
|
||||
liveQuery = new LQ(pipeline)
|
||||
|
||||
const uiClient = new UIClient(pipeline, liveQuery)
|
||||
|
||||
client = uiClient
|
||||
|
||||
_client.notify = (...tx: Tx[]) => {
|
||||
@ -285,7 +272,6 @@ export async function setClient (_client: MeasureClient): Promise<void> {
|
||||
await refreshClient(true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
|
@ -98,7 +98,6 @@ FulltextStorage & {
|
||||
searchFulltext: async (query: SearchQuery, options: SearchOptions): Promise<SearchResult> => {
|
||||
return { docs: [] }
|
||||
},
|
||||
measure: async () => async () => ({ time: 0, serverTime: 0 }),
|
||||
sendForceClose: async () => {}
|
||||
}
|
||||
}
|
||||
|
@ -14,12 +14,12 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { DirectMessage } from '@hcengineering/chunter'
|
||||
import { Avatar, CombineAvatars, personAccountByIdStore } from '@hcengineering/contact-resources'
|
||||
import { Icon, IconSize } from '@hcengineering/ui'
|
||||
import contact, { Person, PersonAccount } from '@hcengineering/contact'
|
||||
import { classIcon } from '@hcengineering/view-resources'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Avatar, CombineAvatars, personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources'
|
||||
import { Account, IdMap } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Icon, IconSize } from '@hcengineering/ui'
|
||||
import { classIcon } from '@hcengineering/view-resources'
|
||||
|
||||
import chunter from '../plugin'
|
||||
import { getDmPersons } from '../utils'
|
||||
@ -33,10 +33,11 @@
|
||||
|
||||
let persons: Person[] = []
|
||||
|
||||
$: value &&
|
||||
getDmPersons(client, value).then((res) => {
|
||||
$: if (value !== undefined) {
|
||||
void getDmPersons(client, value, $personByIdStore).then((res) => {
|
||||
persons = res
|
||||
})
|
||||
}
|
||||
|
||||
let avatarSize = size
|
||||
|
||||
|
@ -13,15 +13,15 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import activity, { ActivityMessage } from '@hcengineering/activity'
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
|
||||
import { Class, Doc, generateId, getCurrentAccount, Ref } from '@hcengineering/core'
|
||||
import { createQuery, DraftController, draftsStore, getClient, isSpace } from '@hcengineering/presentation'
|
||||
import chunter, { ChatMessage, ThreadMessage } from '@hcengineering/chunter'
|
||||
import { PersonAccount } from '@hcengineering/contact'
|
||||
import activity, { ActivityMessage } from '@hcengineering/activity'
|
||||
import { Class, Doc, generateId, getCurrentAccount, Ref, type CommitResult } from '@hcengineering/core'
|
||||
import { createQuery, DraftController, draftsStore, getClient, isSpace } from '@hcengineering/presentation'
|
||||
import { EmptyMarkup } from '@hcengineering/text-editor'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let object: Doc
|
||||
export let chatMessage: ChatMessage | undefined = undefined
|
||||
@ -100,32 +100,20 @@
|
||||
}
|
||||
|
||||
async function handleCreate (event: CustomEvent, _id: Ref<ChatMessage>): Promise<void> {
|
||||
const doneOp = getClient().measure(`chunter.create.${_class} ${object._class}`)
|
||||
try {
|
||||
await createMessage(event, _id)
|
||||
const res = await createMessage(event, _id, `chunter.create.${_class} ${object._class}`)
|
||||
|
||||
const d1 = Date.now()
|
||||
void (await doneOp)().then((res) => {
|
||||
console.log(`create.${_class} measure`, res, Date.now() - d1)
|
||||
})
|
||||
console.log(`create.${_class} measure`, res.serverTime, res.time)
|
||||
} catch (err: any) {
|
||||
void (await doneOp)()
|
||||
Analytics.handleError(err)
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEdit (event: CustomEvent): Promise<void> {
|
||||
const doneOp = getClient().measure(`chunter.edit.${_class} ${object._class}`)
|
||||
try {
|
||||
await editMessage(event)
|
||||
|
||||
const d1 = Date.now()
|
||||
void (await doneOp)().then((res) => {
|
||||
console.log(`edit.${_class} measure`, res, Date.now() - d1)
|
||||
})
|
||||
} catch (err: any) {
|
||||
void (await doneOp)()
|
||||
Analytics.handleError(err)
|
||||
console.error(err)
|
||||
}
|
||||
@ -148,9 +136,9 @@
|
||||
loading = false
|
||||
}
|
||||
|
||||
async function createMessage (event: CustomEvent, _id: Ref<ChatMessage>): Promise<void> {
|
||||
async function createMessage (event: CustomEvent, _id: Ref<ChatMessage>, msg: string): Promise<CommitResult> {
|
||||
const { message, attachments } = event.detail
|
||||
const operations = client.apply(_id)
|
||||
const operations = client.apply(_id, msg)
|
||||
|
||||
if (_class === chunter.class.ThreadMessage) {
|
||||
const parentMessage = object as ActivityMessage
|
||||
@ -188,7 +176,7 @@
|
||||
_id
|
||||
)
|
||||
}
|
||||
await operations.commit()
|
||||
return await operations.commit()
|
||||
}
|
||||
|
||||
async function editMessage (event: CustomEvent): Promise<void> {
|
||||
|
@ -13,17 +13,17 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Class, Doc, getCurrentAccount, groupByArray, Ref, SortingOrder } from '@hcengineering/core'
|
||||
import notification, { DocNotifyContext } from '@hcengineering/notification'
|
||||
import { createQuery, getClient, LiveQuery } from '@hcengineering/presentation'
|
||||
import activity from '@hcengineering/activity'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { Action } from '@hcengineering/ui'
|
||||
import { Class, Doc, getCurrentAccount, groupByArray, reduceCalls, Ref, SortingOrder } from '@hcengineering/core'
|
||||
import notification, { DocNotifyContext } from '@hcengineering/notification'
|
||||
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { createQuery, getClient, LiveQuery } from '@hcengineering/presentation'
|
||||
import { Action } from '@hcengineering/ui'
|
||||
|
||||
import chunter from '../../../plugin'
|
||||
import { ChatGroup, ChatNavGroupModel } from '../types'
|
||||
import ChatNavSection from './ChatNavSection.svelte'
|
||||
import chunter from '../../../plugin'
|
||||
|
||||
export let object: Doc | undefined
|
||||
export let model: ChatNavGroupModel
|
||||
@ -68,7 +68,11 @@
|
||||
|
||||
$: loadObjects(contexts)
|
||||
|
||||
$: void getSections(objectsByClass, model, shouldPushObject ? object : undefined).then((res) => {
|
||||
$: pushObj = shouldPushObject ? object : undefined
|
||||
|
||||
const getPushObj = () => pushObj as Doc
|
||||
|
||||
$: void getSections(objectsByClass, model, pushObj, getPushObj, (res) => {
|
||||
sections = res
|
||||
})
|
||||
|
||||
@ -112,63 +116,68 @@
|
||||
return 'activity'
|
||||
}
|
||||
|
||||
async function getSections (
|
||||
objectsByClass: Map<Ref<Class<Doc>>, Doc[]>,
|
||||
model: ChatNavGroupModel,
|
||||
object: Doc | undefined
|
||||
): Promise<Section[]> {
|
||||
const result: Section[] = []
|
||||
const getSections = reduceCalls(
|
||||
async (
|
||||
objectsByClass: Map<Ref<Class<Doc>>, Doc[]>,
|
||||
model: ChatNavGroupModel,
|
||||
object: { _id: Doc['_id'], _class: Doc['_class'] } | undefined,
|
||||
getPushObj: () => Doc,
|
||||
handler: (result: Section[]) => void
|
||||
): Promise<void> => {
|
||||
const result: Section[] = []
|
||||
|
||||
if (!model.wrap) {
|
||||
result.push({
|
||||
id: model.id,
|
||||
objects: Array.from(objectsByClass.values()).flat(),
|
||||
label: model.label ?? chunter.string.Channels
|
||||
})
|
||||
if (!model.wrap) {
|
||||
result.push({
|
||||
id: model.id,
|
||||
objects: Array.from(objectsByClass.values()).flat(),
|
||||
label: model.label ?? chunter.string.Channels
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
let isObjectPushed = false
|
||||
|
||||
if (
|
||||
Array.from(objectsByClass.values())
|
||||
.flat()
|
||||
.some((o) => o._id === object?._id)
|
||||
) {
|
||||
isObjectPushed = true
|
||||
}
|
||||
|
||||
for (const [_class, objects] of objectsByClass.entries()) {
|
||||
const clazz = hierarchy.getClass(_class)
|
||||
const sectionObjects = [...objects]
|
||||
|
||||
if (object && _class === object._class && !objects.some(({ _id }) => _id === object._id)) {
|
||||
isObjectPushed = true
|
||||
sectionObjects.push(object)
|
||||
handler(result)
|
||||
return
|
||||
}
|
||||
|
||||
result.push({
|
||||
id: _class,
|
||||
_class,
|
||||
objects: sectionObjects,
|
||||
label: clazz.pluralLabel ?? clazz.label
|
||||
})
|
||||
let isObjectPushed = false
|
||||
|
||||
if (
|
||||
Array.from(objectsByClass.values())
|
||||
.flat()
|
||||
.some((o) => o._id === object?._id)
|
||||
) {
|
||||
isObjectPushed = true
|
||||
}
|
||||
|
||||
for (const [_class, objects] of objectsByClass.entries()) {
|
||||
const clazz = hierarchy.getClass(_class)
|
||||
const sectionObjects = [...objects]
|
||||
|
||||
if (object !== undefined && _class === object._class && !objects.some(({ _id }) => _id === object._id)) {
|
||||
isObjectPushed = true
|
||||
sectionObjects.push(getPushObj())
|
||||
}
|
||||
|
||||
result.push({
|
||||
id: _class,
|
||||
_class,
|
||||
objects: sectionObjects,
|
||||
label: clazz.pluralLabel ?? clazz.label
|
||||
})
|
||||
}
|
||||
|
||||
if (!isObjectPushed && object !== undefined) {
|
||||
const clazz = hierarchy.getClass(object._class)
|
||||
|
||||
result.push({
|
||||
id: object._id,
|
||||
_class: object._class,
|
||||
objects: [getPushObj()],
|
||||
label: clazz.pluralLabel ?? clazz.label
|
||||
})
|
||||
}
|
||||
|
||||
handler(result.sort((s1, s2) => s1.label.localeCompare(s2.label)))
|
||||
}
|
||||
|
||||
if (!isObjectPushed && object) {
|
||||
const clazz = hierarchy.getClass(object._class)
|
||||
|
||||
result.push({
|
||||
id: object._id,
|
||||
_class: object._class,
|
||||
objects: [object],
|
||||
label: clazz.pluralLabel ?? clazz.label
|
||||
})
|
||||
}
|
||||
|
||||
return result.sort((s1, s2) => s1.label.localeCompare(s2.label))
|
||||
}
|
||||
)
|
||||
|
||||
function getSectionActions (section: Section, contexts: DocNotifyContext[]): Action[] {
|
||||
if (model.getActionsFn === undefined) {
|
||||
|
@ -13,20 +13,20 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Doc, Ref } from '@hcengineering/core'
|
||||
import contact from '@hcengineering/contact'
|
||||
import { personAccountByIdStore, statusByUserStore } from '@hcengineering/contact-resources'
|
||||
import { Doc, reduceCalls, Ref } from '@hcengineering/core'
|
||||
import { DocNotifyContext } from '@hcengineering/notification'
|
||||
import { getResource, IntlString, translate } from '@hcengineering/platform'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import ui, { Action, AnySvelteComponent, IconSize, ModernButton, NavGroup } from '@hcengineering/ui'
|
||||
import { getDocTitle } from '@hcengineering/view-resources'
|
||||
import contact from '@hcengineering/contact'
|
||||
import { getResource, IntlString, translate } from '@hcengineering/platform'
|
||||
import view from '@hcengineering/view'
|
||||
import { personAccountByIdStore, statusByUserStore } from '@hcengineering/contact-resources'
|
||||
import { getDocTitle } from '@hcengineering/view-resources'
|
||||
|
||||
import ChatNavItem from './ChatNavItem.svelte'
|
||||
import chunter from '../../../plugin'
|
||||
import { getChannelName, getObjectIcon } from '../../../utils'
|
||||
import { ChatNavItemModel, SortFnOptions } from '../types'
|
||||
import { getObjectIcon, getChannelName } from '../../../utils'
|
||||
import ChatNavItem from './ChatNavItem.svelte'
|
||||
|
||||
export let id: string
|
||||
export let header: IntlString
|
||||
@ -47,7 +47,7 @@
|
||||
let canShowMore = false
|
||||
let isShownMore = false
|
||||
|
||||
$: void getChatNavItems(objects).then((res) => {
|
||||
$: void getChatNavItems(objects, (res) => {
|
||||
items = res
|
||||
})
|
||||
|
||||
@ -60,44 +60,46 @@
|
||||
|
||||
$: visibleItems = getVisibleItems(canShowMore, isShownMore, maxItems, sortedItems, objectId)
|
||||
|
||||
async function getChatNavItems (objects: Doc[]): Promise<ChatNavItemModel[]> {
|
||||
const items: ChatNavItemModel[] = []
|
||||
const getChatNavItems = reduceCalls(
|
||||
async (objects: Doc[], handler: (items: ChatNavItemModel[]) => void): Promise<void> => {
|
||||
const items: ChatNavItemModel[] = []
|
||||
|
||||
for (const object of objects) {
|
||||
const { _class } = object
|
||||
const iconMixin = hierarchy.classHierarchyMixin(_class, view.mixin.ObjectIcon)
|
||||
const titleIntl = client.getHierarchy().getClass(_class).label
|
||||
for (const object of objects) {
|
||||
const { _class } = object
|
||||
const iconMixin = hierarchy.classHierarchyMixin(_class, view.mixin.ObjectIcon)
|
||||
const titleIntl = client.getHierarchy().getClass(_class).label
|
||||
|
||||
const isPerson = hierarchy.isDerived(_class, contact.class.Person)
|
||||
const isDocChat = !hierarchy.isDerived(_class, chunter.class.ChunterSpace)
|
||||
const isDirect = hierarchy.isDerived(_class, chunter.class.DirectMessage)
|
||||
const isPerson = hierarchy.isDerived(_class, contact.class.Person)
|
||||
const isDocChat = !hierarchy.isDerived(_class, chunter.class.ChunterSpace)
|
||||
const isDirect = hierarchy.isDerived(_class, chunter.class.DirectMessage)
|
||||
|
||||
const iconSize: IconSize = isDirect || isPerson ? 'x-small' : 'small'
|
||||
const iconSize: IconSize = isDirect || isPerson ? 'x-small' : 'small'
|
||||
|
||||
let icon: AnySvelteComponent | undefined = undefined
|
||||
let icon: AnySvelteComponent | undefined = undefined
|
||||
|
||||
if (iconMixin?.component) {
|
||||
icon = await getResource(iconMixin.component)
|
||||
if (iconMixin?.component) {
|
||||
icon = await getResource(iconMixin.component)
|
||||
}
|
||||
|
||||
const hasId = hierarchy.classHierarchyMixin(object._class, view.mixin.ObjectIdentifier) !== undefined
|
||||
const showDescription = hasId && isDocChat && !isPerson
|
||||
|
||||
items.push({
|
||||
id: object._id,
|
||||
object,
|
||||
title: (await getChannelName(object._id, object._class, object)) ?? (await translate(titleIntl, {})),
|
||||
description: showDescription ? await getDocTitle(client, object._id, object._class, object) : undefined,
|
||||
icon: icon ?? getObjectIcon(_class),
|
||||
iconProps: { showStatus: true },
|
||||
iconSize,
|
||||
withIconBackground: !isDirect && !isPerson,
|
||||
isSecondary: isDocChat && !isPerson
|
||||
})
|
||||
}
|
||||
|
||||
const hasId = hierarchy.classHierarchyMixin(object._class, view.mixin.ObjectIdentifier) !== undefined
|
||||
const showDescription = hasId && isDocChat && !isPerson
|
||||
|
||||
items.push({
|
||||
id: object._id,
|
||||
object,
|
||||
title: (await getChannelName(object._id, object._class, object)) ?? (await translate(titleIntl, {})),
|
||||
description: showDescription ? await getDocTitle(client, object._id, object._class, object) : undefined,
|
||||
icon: icon ?? getObjectIcon(_class),
|
||||
iconProps: { showStatus: true },
|
||||
iconSize,
|
||||
withIconBackground: !isDirect && !isPerson,
|
||||
isSecondary: isDocChat && !isPerson
|
||||
})
|
||||
handler(items)
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
)
|
||||
|
||||
function onShowMore (): void {
|
||||
isShownMore = !isShownMore
|
||||
|
@ -110,7 +110,7 @@
|
||||
/>
|
||||
</div>
|
||||
<Scroller shrink>
|
||||
{#each chatNavGroupModels as model}
|
||||
{#each chatNavGroupModels as model (model.id)}
|
||||
<ChatNavGroup {object} {model} on:select />
|
||||
{/each}
|
||||
</Scroller>
|
||||
|
@ -304,8 +304,7 @@ function getPinnedActions (contexts: DocNotifyContext[]): Action[] {
|
||||
}
|
||||
|
||||
async function unpinAllChannels (contexts: DocNotifyContext[]): Promise<void> {
|
||||
const doneOp = await getClient().measure('unpinAllChannels')
|
||||
const ops = getClient().apply(generateId())
|
||||
const ops = getClient().apply(generateId(), 'unpinAllChannels')
|
||||
|
||||
try {
|
||||
for (const context of contexts) {
|
||||
@ -313,7 +312,6 @@ async function unpinAllChannels (contexts: DocNotifyContext[]): Promise<void> {
|
||||
}
|
||||
} finally {
|
||||
await ops.commit()
|
||||
await doneOp()
|
||||
}
|
||||
}
|
||||
|
||||
@ -404,8 +402,7 @@ export function loadSavedAttachments (): void {
|
||||
export async function removeActivityChannels (contexts: DocNotifyContext[]): Promise<void> {
|
||||
const client = InboxNotificationsClientImpl.getClient()
|
||||
const notificationsByContext = get(client.inboxNotificationsByContext)
|
||||
const doneOp = await getClient().measure('removeActivityChannels')
|
||||
const ops = getClient().apply(generateId())
|
||||
const ops = getClient().apply(generateId(), 'removeActivityChannels')
|
||||
|
||||
try {
|
||||
for (const context of contexts) {
|
||||
@ -418,15 +415,13 @@ export async function removeActivityChannels (contexts: DocNotifyContext[]): Pro
|
||||
}
|
||||
} finally {
|
||||
await ops.commit()
|
||||
await doneOp()
|
||||
}
|
||||
}
|
||||
|
||||
export async function readActivityChannels (contexts: DocNotifyContext[]): Promise<void> {
|
||||
const client = InboxNotificationsClientImpl.getClient()
|
||||
const notificationsByContext = get(client.inboxNotificationsByContext)
|
||||
const doneOp = await getClient().measure('readActivityChannels')
|
||||
const ops = getClient().apply(generateId())
|
||||
const ops = getClient().apply(generateId(), 'readActivityChannels')
|
||||
|
||||
try {
|
||||
for (const context of contexts) {
|
||||
@ -441,6 +436,5 @@ export async function readActivityChannels (contexts: DocNotifyContext[]): Promi
|
||||
}
|
||||
} finally {
|
||||
await ops.commit()
|
||||
await doneOp()
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +32,8 @@ import {
|
||||
type IdMap,
|
||||
type Ref,
|
||||
type Space,
|
||||
type Timestamp
|
||||
type Timestamp,
|
||||
type WithLookup
|
||||
} from '@hcengineering/core'
|
||||
import notification, { type DocNotifyContext, type InboxNotification } from '@hcengineering/notification'
|
||||
import {
|
||||
@ -163,7 +164,11 @@ async function getDmAccounts (client: Client, space?: Space): Promise<PersonAcco
|
||||
})
|
||||
}
|
||||
|
||||
export async function getDmPersons (client: Client, space: Space): Promise<Person[]> {
|
||||
export async function getDmPersons (
|
||||
client: Client,
|
||||
space: Space,
|
||||
personsMap: Map<Ref<WithLookup<Person>>, WithLookup<Person>>
|
||||
): Promise<Person[]> {
|
||||
const personAccounts: PersonAccount[] = await getDmAccounts(client, space)
|
||||
const me = getCurrentAccount() as PersonAccount
|
||||
const persons: Person[] = []
|
||||
@ -172,7 +177,7 @@ export async function getDmPersons (client: Client, space: Space): Promise<Perso
|
||||
let myPerson: Person | undefined
|
||||
|
||||
for (const personRef of personRefs) {
|
||||
const person = await client.findOne(contact.class.Person, { _id: personRef })
|
||||
const person = personsMap.get(personRef) ?? (await client.findOne(contact.class.Person, { _id: personRef }))
|
||||
if (person === undefined) {
|
||||
continue
|
||||
}
|
||||
@ -192,8 +197,12 @@ export async function getDmPersons (client: Client, space: Space): Promise<Perso
|
||||
return myPerson !== undefined ? [myPerson] : []
|
||||
}
|
||||
|
||||
export async function DirectTitleProvider (client: Client, id: Ref<DirectMessage>): Promise<string> {
|
||||
const direct = await client.findOne(chunter.class.DirectMessage, { _id: id })
|
||||
export async function DirectTitleProvider (
|
||||
client: Client,
|
||||
id: Ref<DirectMessage>,
|
||||
doc?: DirectMessage
|
||||
): Promise<string> {
|
||||
const direct = doc ?? (await client.findOne(chunter.class.DirectMessage, { _id: id }))
|
||||
|
||||
if (direct === undefined) {
|
||||
return ''
|
||||
@ -202,8 +211,8 @@ export async function DirectTitleProvider (client: Client, id: Ref<DirectMessage
|
||||
return await getDmName(client, direct)
|
||||
}
|
||||
|
||||
export async function ChannelTitleProvider (client: Client, id: Ref<Channel>): Promise<string> {
|
||||
const channel = await client.findOne(chunter.class.Channel, { _id: id })
|
||||
export async function ChannelTitleProvider (client: Client, id: Ref<Channel>, doc?: Channel): Promise<string> {
|
||||
const channel = doc ?? (await client.findOne(chunter.class.Channel, { _id: id }))
|
||||
|
||||
if (channel === undefined) {
|
||||
return ''
|
||||
|
@ -28,7 +28,6 @@ import core, {
|
||||
FindOptions,
|
||||
FindResult,
|
||||
LoadModelResponse,
|
||||
MeasureDoneOperation,
|
||||
Ref,
|
||||
SearchOptions,
|
||||
SearchQuery,
|
||||
@ -524,26 +523,6 @@ class Connection implements ClientConnection {
|
||||
return await promise.promise
|
||||
}
|
||||
|
||||
async measure (operationName: string): Promise<MeasureDoneOperation> {
|
||||
const dateNow = Date.now()
|
||||
|
||||
// Send measure-start
|
||||
const mid = await this.sendRequest({
|
||||
method: 'measure',
|
||||
params: [operationName]
|
||||
})
|
||||
return async () => {
|
||||
const serverTime: number = await this.sendRequest({
|
||||
method: 'measure-done',
|
||||
params: [operationName, mid]
|
||||
})
|
||||
return {
|
||||
time: Date.now() - dateNow,
|
||||
serverTime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async loadModel (last: Timestamp, hash?: string): Promise<Tx[] | LoadModelResponse> {
|
||||
return await this.sendRequest({ method: 'loadModel', params: [last, hash] })
|
||||
}
|
||||
|
@ -28,14 +28,7 @@ import core, {
|
||||
createClient,
|
||||
type ClientConnection
|
||||
} from '@hcengineering/core'
|
||||
import platform, {
|
||||
Severity,
|
||||
Status,
|
||||
getMetadata,
|
||||
getPlugins,
|
||||
getResource,
|
||||
setPlatformStatus
|
||||
} from '@hcengineering/platform'
|
||||
import platform, { Severity, Status, getMetadata, getPlugins, setPlatformStatus } from '@hcengineering/platform'
|
||||
import { connect } from './connection'
|
||||
|
||||
export { connect }
|
||||
@ -88,7 +81,7 @@ export default async () => {
|
||||
): Promise<AccountClient> => {
|
||||
const filterModel = getMetadata(clientPlugin.metadata.FilterModel) ?? false
|
||||
|
||||
let client = createClient(
|
||||
const client = createClient(
|
||||
(handler: TxHandler) => {
|
||||
const url = concatLink(endpoint, `/${token}`)
|
||||
|
||||
@ -144,8 +137,6 @@ export default async () => {
|
||||
createModelPersistence(getWSFromToken(token)),
|
||||
ctx
|
||||
)
|
||||
// Check if we had dev hook for client.
|
||||
client = hookClient(client)
|
||||
return await client
|
||||
}
|
||||
}
|
||||
@ -203,24 +194,6 @@ function createModelPersistence (workspace: string): TxPersistenceStore | undefi
|
||||
}
|
||||
}
|
||||
|
||||
async function hookClient (client: Promise<AccountClient>): Promise<AccountClient> {
|
||||
const hook = getMetadata(clientPlugin.metadata.ClientHook)
|
||||
if (hook !== undefined) {
|
||||
const hookProc = await getResource(hook)
|
||||
const _client = client
|
||||
client = new Promise((resolve, reject) => {
|
||||
_client
|
||||
.then((res) => {
|
||||
resolve(hookProc(res))
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
return await client
|
||||
}
|
||||
|
||||
function getWSFromToken (token: string): string {
|
||||
const parts = token.split('.')
|
||||
|
||||
|
@ -22,11 +22,6 @@ import { Metadata, plugin } from '@hcengineering/platform'
|
||||
*/
|
||||
export const clientId = 'client' as Plugin
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type ClientHook = (client: AccountClient) => Promise<AccountClient>
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -74,7 +69,6 @@ export type ClientFactory = (
|
||||
|
||||
export default plugin(clientId, {
|
||||
metadata: {
|
||||
ClientHook: '' as Metadata<Resource<ClientHook>>,
|
||||
ClientSocketFactory: '' as Metadata<ClientSocketFactory>,
|
||||
FilterModel: '' as Metadata<boolean>,
|
||||
ExtraPlugins: '' as Metadata<Plugin[]>,
|
||||
|
@ -194,7 +194,7 @@ async function createControlledDoc (
|
||||
|
||||
const success = await ops.commit()
|
||||
|
||||
return { seqNumber, success }
|
||||
return { seqNumber, success: success.result }
|
||||
}
|
||||
|
||||
export async function createDocumentTemplate (
|
||||
@ -327,7 +327,7 @@ export async function createDocumentTemplate (
|
||||
|
||||
const success = await ops.commit()
|
||||
|
||||
return { seqNumber, success }
|
||||
return { seqNumber, success: success.result }
|
||||
}
|
||||
|
||||
export function getCollaborativeDocForDocument (
|
||||
|
@ -13,20 +13,15 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import core, {
|
||||
import {
|
||||
DOMAIN_MODEL,
|
||||
cutObjectArray,
|
||||
type Account,
|
||||
type AccountClient,
|
||||
type Class,
|
||||
type Client,
|
||||
type Doc,
|
||||
type DocumentQuery,
|
||||
type FindOptions,
|
||||
type FindResult,
|
||||
type Hierarchy,
|
||||
type MeasureDoneOperation,
|
||||
type ModelDb,
|
||||
type Ref,
|
||||
type SearchOptions,
|
||||
type SearchQuery,
|
||||
@ -35,13 +30,10 @@ import core, {
|
||||
type TxResult,
|
||||
type WithLookup
|
||||
} from '@hcengineering/core'
|
||||
import { devModelId } from '@hcengineering/devmodel'
|
||||
import { Builder } from '@hcengineering/model'
|
||||
import { getMetadata, type IntlString, type Resources } from '@hcengineering/platform'
|
||||
import { addTxListener } from '@hcengineering/presentation'
|
||||
import type { ClientHook } from '@hcengineering/presentation/src/plugin'
|
||||
import { testing } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import workbench from '@hcengineering/workbench'
|
||||
import ModelView from './components/ModelView.svelte'
|
||||
import devmodel from './plugin'
|
||||
|
||||
export interface TxWitHResult {
|
||||
@ -57,48 +49,46 @@ export interface QueryWithResult {
|
||||
findOne: boolean
|
||||
}
|
||||
|
||||
class ModelClient implements AccountClient {
|
||||
export class PresentationClientHook implements ClientHook {
|
||||
notifyEnabled = true
|
||||
constructor (readonly client: AccountClient) {
|
||||
constructor () {
|
||||
this.notifyEnabled = (localStorage.getItem('#platform.notification.logging') ?? 'true') === 'true'
|
||||
|
||||
client.notify = (...tx) => {
|
||||
this.notify?.(...tx)
|
||||
addTxListener((...tx) => {
|
||||
if (this.notifyEnabled) {
|
||||
console.debug(
|
||||
'devmodel# notify=>',
|
||||
testing ? JSON.stringify(cutObjectArray(tx)).slice(0, 160) : tx.length === 1 ? tx[0] : tx
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
stackLine (): string {
|
||||
const stack = (new Error().stack ?? '').split('\n')
|
||||
|
||||
let candidate = ''
|
||||
for (let l of stack) {
|
||||
l = l.trim()
|
||||
if (l.includes('.svelte')) {
|
||||
return l
|
||||
}
|
||||
if (l.includes('plugins/') && !l.includes('devmodel-resources/') && l.includes('.ts') && candidate === '') {
|
||||
candidate = l
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async measure (operationName: string): Promise<MeasureDoneOperation> {
|
||||
return await this.client.measure(operationName)
|
||||
}
|
||||
|
||||
notify?: (...tx: Tx[]) => void
|
||||
|
||||
getHierarchy (): Hierarchy {
|
||||
return this.client.getHierarchy()
|
||||
}
|
||||
|
||||
getModel (): ModelDb {
|
||||
return this.client.getModel()
|
||||
}
|
||||
|
||||
async getAccount (): Promise<Account> {
|
||||
return await this.client.getAccount()
|
||||
return candidate
|
||||
}
|
||||
|
||||
async findOne<T extends Doc>(
|
||||
client: Client,
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T>
|
||||
): Promise<WithLookup<T> | undefined> {
|
||||
const startTime = Date.now()
|
||||
const isModel = this.getHierarchy().findDomain(_class) === DOMAIN_MODEL
|
||||
const result = await this.client.findOne(_class, query, options)
|
||||
const isModel = client.getHierarchy().findDomain(_class) === DOMAIN_MODEL
|
||||
const result = await client.findOne(_class, query, options)
|
||||
if (this.notifyEnabled && !isModel) {
|
||||
console.debug(
|
||||
'devmodel# findOne=>',
|
||||
@ -108,22 +98,24 @@ class ModelClient implements AccountClient {
|
||||
'result => ',
|
||||
testing ? JSON.stringify(cutObjectArray(result)) : result,
|
||||
' =>model',
|
||||
this.client.getModel(),
|
||||
client.getModel(),
|
||||
getMetadata(devmodel.metadata.DevModel),
|
||||
Date.now() - startTime
|
||||
Date.now() - startTime,
|
||||
this.stackLine()
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async findAll<T extends Doc>(
|
||||
client: Client,
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T>
|
||||
): Promise<FindResult<T>> {
|
||||
const startTime = Date.now()
|
||||
const isModel = this.getHierarchy().findDomain(_class) === DOMAIN_MODEL
|
||||
const result = await this.client.findAll(_class, query, options)
|
||||
const isModel = client.getHierarchy().findDomain(_class) === DOMAIN_MODEL
|
||||
const result = await client.findAll(_class, query, options)
|
||||
if (this.notifyEnabled && !isModel) {
|
||||
console.debug(
|
||||
'devmodel# findAll=>',
|
||||
@ -133,17 +125,18 @@ class ModelClient implements AccountClient {
|
||||
'result => ',
|
||||
testing ? JSON.stringify(cutObjectArray(result)).slice(0, 160) : result,
|
||||
' =>model',
|
||||
this.client.getModel(),
|
||||
client.getModel(),
|
||||
getMetadata(devmodel.metadata.DevModel),
|
||||
Date.now() - startTime,
|
||||
JSON.stringify(result).length
|
||||
JSON.stringify(result).length,
|
||||
this.stackLine()
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async searchFulltext (query: SearchQuery, options: SearchOptions): Promise<SearchResult> {
|
||||
const result = await this.client.searchFulltext(query, options)
|
||||
async searchFulltext (client: Client, query: SearchQuery, options: SearchOptions): Promise<SearchResult> {
|
||||
const result = await client.searchFulltext(query, options)
|
||||
if (this.notifyEnabled) {
|
||||
console.debug(
|
||||
'devmodel# searchFulltext=>',
|
||||
@ -156,71 +149,25 @@ class ModelClient implements AccountClient {
|
||||
return result
|
||||
}
|
||||
|
||||
async tx (tx: Tx): Promise<TxResult> {
|
||||
async tx (client: Client, tx: Tx): Promise<TxResult> {
|
||||
const startTime = Date.now()
|
||||
const result = await this.client.tx(tx)
|
||||
const result = await client.tx(tx)
|
||||
if (this.notifyEnabled) {
|
||||
console.debug(
|
||||
'devmodel# tx=>',
|
||||
testing ? JSON.stringify(cutObjectArray(tx)).slice(0, 160) : tx,
|
||||
result,
|
||||
getMetadata(devmodel.metadata.DevModel),
|
||||
Date.now() - startTime
|
||||
Date.now() - startTime,
|
||||
this.stackLine()
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async close (): Promise<void> {
|
||||
await this.client.close()
|
||||
}
|
||||
}
|
||||
export async function Hook (client: AccountClient): Promise<Client> {
|
||||
console.debug('devmodel# Client HOOKED by DevModel')
|
||||
|
||||
// Client is alive here, we could hook with some model extensions special for DevModel plugin.
|
||||
const builder = new Builder()
|
||||
|
||||
builder.createDoc(
|
||||
workbench.class.Application,
|
||||
core.space.Model,
|
||||
{
|
||||
label: 'DevModel' as IntlString,
|
||||
icon: view.icon.DevModel,
|
||||
alias: devModelId,
|
||||
hidden: false,
|
||||
navigatorModel: {
|
||||
spaces: [],
|
||||
specials: [
|
||||
{
|
||||
label: 'Transactions' as IntlString,
|
||||
icon: view.icon.Table,
|
||||
id: 'transactions',
|
||||
component: devmodel.component.ModelView
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
devmodel.ids.DevModelApp
|
||||
)
|
||||
|
||||
const model = client.getModel()
|
||||
for (const tx of builder.getTxes()) {
|
||||
await model.tx(tx)
|
||||
}
|
||||
|
||||
return new ModelClient(client)
|
||||
}
|
||||
|
||||
export function toIntl (value: string): IntlString {
|
||||
return value as IntlString
|
||||
}
|
||||
|
||||
export default async (): Promise<Resources> => ({
|
||||
component: {
|
||||
ModelView
|
||||
},
|
||||
hook: {
|
||||
Hook
|
||||
}
|
||||
})
|
||||
export default async (): Promise<Resources> => ({})
|
||||
|
@ -13,8 +13,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { ClientHook } from '@hcengineering/client'
|
||||
import type { Asset, Metadata, Plugin, Resource } from '@hcengineering/platform'
|
||||
import type { Asset, Metadata, Plugin } from '@hcengineering/platform'
|
||||
import { plugin } from '@hcengineering/platform'
|
||||
import type { AnyComponent } from '@hcengineering/ui'
|
||||
|
||||
@ -32,9 +31,6 @@ export default plugin(devModelId, {
|
||||
TransactionView: '' as AnyComponent,
|
||||
NotificationsView: '' as AnyComponent
|
||||
},
|
||||
hook: {
|
||||
Hook: '' as Resource<ClientHook>
|
||||
},
|
||||
metadata: {
|
||||
DevModel: '' as Metadata<any>
|
||||
}
|
||||
|
@ -13,37 +13,36 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import activity, { ActivityMessage } from '@hcengineering/activity'
|
||||
import chunter from '@hcengineering/chunter'
|
||||
import { getCurrentAccount, groupByArray, IdMap, Ref, SortingOrder } from '@hcengineering/core'
|
||||
import { DocNotifyContext, InboxNotification, notificationId } from '@hcengineering/notification'
|
||||
import { ActionContext, createQuery, getClient } from '@hcengineering/presentation'
|
||||
import view, { decodeObjectURI } from '@hcengineering/view'
|
||||
import {
|
||||
AnyComponent,
|
||||
Component,
|
||||
defineSeparators,
|
||||
deviceOptionsStore as deviceInfo,
|
||||
Label,
|
||||
location as locationStore,
|
||||
Location,
|
||||
location as locationStore,
|
||||
restoreLocation,
|
||||
Scroller,
|
||||
Separator,
|
||||
TabItem,
|
||||
TabList,
|
||||
deviceOptionsStore as deviceInfo
|
||||
TabList
|
||||
} from '@hcengineering/ui'
|
||||
import chunter from '@hcengineering/chunter'
|
||||
import activity, { ActivityMessage } from '@hcengineering/activity'
|
||||
import { get } from 'svelte/store'
|
||||
import { translate } from '@hcengineering/platform'
|
||||
import { getCurrentAccount, groupByArray, IdMap, Ref, SortingOrder } from '@hcengineering/core'
|
||||
import view, { decodeObjectURI } from '@hcengineering/view'
|
||||
import { parseLinkId } from '@hcengineering/view-resources'
|
||||
import { get } from 'svelte/store'
|
||||
|
||||
import { InboxNotificationsClientImpl } from '../../inboxNotificationsClient'
|
||||
import SettingsButton from './SettingsButton.svelte'
|
||||
import { getDisplayInboxData, resetInboxContext, resolveLocation, selectInboxContext } from '../../utils'
|
||||
import { InboxData, InboxNotificationsFilter } from '../../types'
|
||||
import InboxGroupedListView from './InboxGroupedListView.svelte'
|
||||
import notification from '../../plugin'
|
||||
import { InboxData, InboxNotificationsFilter } from '../../types'
|
||||
import { getDisplayInboxData, resetInboxContext, resolveLocation, selectInboxContext } from '../../utils'
|
||||
import InboxGroupedListView from './InboxGroupedListView.svelte'
|
||||
import InboxMenuButton from './InboxMenuButton.svelte'
|
||||
import SettingsButton from './SettingsButton.svelte'
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
@ -248,8 +247,7 @@
|
||||
|
||||
const contextNotifications = $notificationsByContextStore.get(selectedContext._id) ?? []
|
||||
|
||||
const doneOp = await getClient().measure('readNotifications')
|
||||
const ops = getClient().apply(selectedContext._id)
|
||||
const ops = getClient().apply(selectedContext._id, 'readNotifications')
|
||||
try {
|
||||
await inboxClient.readNotifications(
|
||||
ops,
|
||||
@ -261,7 +259,6 @@
|
||||
)
|
||||
} finally {
|
||||
await ops.commit()
|
||||
await doneOp()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -242,8 +242,7 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
|
||||
}
|
||||
|
||||
async archiveAllNotifications (): Promise<void> {
|
||||
const doneOp = await getClient().measure('archiveAllNotifications')
|
||||
const ops = getClient().apply(generateId())
|
||||
const ops = getClient().apply(generateId(), 'archiveAllNotifications')
|
||||
|
||||
try {
|
||||
const inboxNotifications = await ops.findAll(
|
||||
@ -264,13 +263,11 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
|
||||
}
|
||||
} finally {
|
||||
await ops.commit()
|
||||
await doneOp()
|
||||
}
|
||||
}
|
||||
|
||||
async readAllNotifications (): Promise<void> {
|
||||
const doneOp = await getClient().measure('readAllNotifications')
|
||||
const ops = getClient().apply(generateId())
|
||||
const ops = getClient().apply(generateId(), 'readAllNotifications')
|
||||
|
||||
try {
|
||||
const inboxNotifications = await ops.findAll(
|
||||
@ -291,13 +288,11 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
|
||||
}
|
||||
} finally {
|
||||
await ops.commit()
|
||||
await doneOp()
|
||||
}
|
||||
}
|
||||
|
||||
async unreadAllNotifications (): Promise<void> {
|
||||
const doneOp = await getClient().measure('unreadAllNotifications')
|
||||
const ops = getClient().apply(generateId())
|
||||
const ops = getClient().apply(generateId(), 'unreadAllNotifications')
|
||||
|
||||
try {
|
||||
const inboxNotifications = await ops.findAll(
|
||||
@ -332,7 +327,6 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
|
||||
}
|
||||
} finally {
|
||||
await ops.commit()
|
||||
await doneOp()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -49,21 +49,21 @@ import { MessageBox, getClient } from '@hcengineering/presentation'
|
||||
import {
|
||||
getCurrentLocation,
|
||||
getLocation,
|
||||
locationStorageKeyId,
|
||||
navigate,
|
||||
parseLocation,
|
||||
showPopup,
|
||||
type Location,
|
||||
type ResolvedLocation,
|
||||
locationStorageKeyId
|
||||
type ResolvedLocation
|
||||
} from '@hcengineering/ui'
|
||||
import { get, writable } from 'svelte/store'
|
||||
|
||||
import chunter, { type ThreadMessage } from '@hcengineering/chunter'
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
import { decodeObjectURI, encodeObjectURI, type LinkIdProvider } from '@hcengineering/view'
|
||||
import { getObjectLinkId } from '@hcengineering/view-resources'
|
||||
import { InboxNotificationsClientImpl } from './inboxNotificationsClient'
|
||||
import { type InboxData, type InboxNotificationsFilter } from './types'
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
import { getObjectLinkId } from '@hcengineering/view-resources'
|
||||
import { decodeObjectURI, encodeObjectURI, type LinkIdProvider } from '@hcengineering/view'
|
||||
import chunter, { type ThreadMessage } from '@hcengineering/chunter'
|
||||
|
||||
export async function hasDocNotifyContextPinAction (docNotifyContext: DocNotifyContext): Promise<boolean> {
|
||||
if (docNotifyContext.hidden) {
|
||||
@ -107,8 +107,7 @@ export async function readNotifyContext (doc: DocNotifyContext): Promise<void> {
|
||||
const inboxClient = InboxNotificationsClientImpl.getClient()
|
||||
const inboxNotifications = get(inboxClient.inboxNotificationsByContext).get(doc._id) ?? []
|
||||
|
||||
const doneOp = await getClient().measure('readNotifyContext')
|
||||
const ops = getClient().apply(doc._id)
|
||||
const ops = getClient().apply(doc._id, 'readNotifyContext')
|
||||
try {
|
||||
await inboxClient.readNotifications(
|
||||
ops,
|
||||
@ -117,7 +116,6 @@ export async function readNotifyContext (doc: DocNotifyContext): Promise<void> {
|
||||
await ops.update(doc, { lastViewedTimestamp: Date.now() })
|
||||
} finally {
|
||||
await ops.commit()
|
||||
await doneOp()
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,8 +131,7 @@ export async function unReadNotifyContext (doc: DocNotifyContext): Promise<void>
|
||||
return
|
||||
}
|
||||
|
||||
const doneOp = await getClient().measure('unReadNotifyContext')
|
||||
const ops = getClient().apply(doc._id)
|
||||
const ops = getClient().apply(doc._id, 'unReadNotifyContext')
|
||||
|
||||
try {
|
||||
await inboxClient.unreadNotifications(
|
||||
@ -154,7 +151,6 @@ export async function unReadNotifyContext (doc: DocNotifyContext): Promise<void>
|
||||
}
|
||||
} finally {
|
||||
await ops.commit()
|
||||
await doneOp()
|
||||
}
|
||||
}
|
||||
|
||||
@ -166,8 +162,7 @@ export async function archiveContextNotifications (doc?: DocNotifyContext): Prom
|
||||
return
|
||||
}
|
||||
|
||||
const doneOp = await getClient().measure('archiveContextNotifications')
|
||||
const ops = getClient().apply(doc._id)
|
||||
const ops = getClient().apply(doc._id, 'archiveContextNotifications')
|
||||
|
||||
try {
|
||||
const notifications = await ops.findAll(
|
||||
@ -182,7 +177,6 @@ export async function archiveContextNotifications (doc?: DocNotifyContext): Prom
|
||||
await ops.update(doc, { lastViewedTimestamp: Date.now() })
|
||||
} finally {
|
||||
await ops.commit()
|
||||
await doneOp()
|
||||
}
|
||||
}
|
||||
|
||||
@ -194,8 +188,7 @@ export async function unarchiveContextNotifications (doc?: DocNotifyContext): Pr
|
||||
return
|
||||
}
|
||||
|
||||
const doneOp = await getClient().measure('unarchiveContextNotifications')
|
||||
const ops = getClient().apply(doc._id)
|
||||
const ops = getClient().apply(doc._id, 'unarchiveContextNotifications')
|
||||
|
||||
try {
|
||||
const notifications = await ops.findAll(
|
||||
@ -209,7 +202,6 @@ export async function unarchiveContextNotifications (doc?: DocNotifyContext): Pr
|
||||
}
|
||||
} finally {
|
||||
await ops.commit()
|
||||
await doneOp()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -442,10 +442,8 @@
|
||||
|
||||
// TODO: We need a measure client and mark all operations with it as measure under one root,
|
||||
// to prevent other operations to infer our measurement.
|
||||
const doneOp = await getClient().measure('tracker.createIssue')
|
||||
|
||||
try {
|
||||
const operations = client.apply(_id)
|
||||
const operations = client.apply(_id, 'tracker.createIssue')
|
||||
|
||||
const lastOne = await client.findOne<Issue>(
|
||||
tracker.class.Issue,
|
||||
@ -533,7 +531,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
await operations.commit()
|
||||
const result = await operations.commit()
|
||||
await descriptionBox?.createAttachments(_id)
|
||||
|
||||
const parents: IssueParentInfo[] =
|
||||
@ -565,15 +563,12 @@
|
||||
descriptionBox?.removeDraft(false)
|
||||
isAssigneeTouched = false
|
||||
const d1 = Date.now()
|
||||
void doneOp().then((res) => {
|
||||
console.log('createIssue measure', res, Date.now() - d1)
|
||||
})
|
||||
console.log('createIssue measure', result, Date.now() - d1)
|
||||
} catch (err: any) {
|
||||
resetObject()
|
||||
draftController.remove()
|
||||
descriptionBox?.removeDraft(false)
|
||||
console.error(err)
|
||||
await doneOp() // Complete in case of error
|
||||
Analytics.handleError(err)
|
||||
}
|
||||
}
|
||||
|
@ -235,7 +235,7 @@
|
||||
await ops.createDoc(tracker.class.Project, core.space.Space, { ...projectData, type: typeId }, projectId)
|
||||
const succeeded = await ops.commit()
|
||||
|
||||
if (succeeded) {
|
||||
if (succeeded.result) {
|
||||
// Add space type's mixin with roles assignments
|
||||
await client.createMixin(
|
||||
projectId,
|
||||
|
@ -25,8 +25,8 @@ export async function issueIdentifierProvider (client: TxOperations, ref: Ref<Is
|
||||
return object.identifier
|
||||
}
|
||||
|
||||
export async function issueTitleProvider (client: TxOperations, ref: Ref<Doc>): Promise<string> {
|
||||
const object = await client.findOne(tracker.class.Issue, { _id: ref as Ref<Issue> })
|
||||
export async function issueTitleProvider (client: TxOperations, ref: Ref<Doc>, doc?: Issue): Promise<string> {
|
||||
const object = doc ?? (await client.findOne(tracker.class.Issue, { _id: ref as Ref<Issue> }))
|
||||
|
||||
if (object === undefined) {
|
||||
return ''
|
||||
|
@ -652,7 +652,7 @@ async function ActivityReferenceCreate (tx: TxCUD<Doc>, control: TriggerControl)
|
||||
)
|
||||
|
||||
if (txes.length !== 0) {
|
||||
await control.apply(txes, true)
|
||||
await control.apply(txes)
|
||||
}
|
||||
|
||||
return []
|
||||
@ -699,7 +699,7 @@ async function ActivityReferenceUpdate (tx: TxCUD<Doc>, control: TriggerControl)
|
||||
)
|
||||
|
||||
if (txes.length !== 0) {
|
||||
await control.apply(txes, true)
|
||||
await control.apply(txes)
|
||||
}
|
||||
|
||||
return []
|
||||
@ -723,7 +723,7 @@ async function ActivityReferenceRemove (tx: Tx, control: TriggerControl): Promis
|
||||
|
||||
const txes: Tx[] = await getRemoveActivityReferenceTxes(control, txFactory, ctx.objectId)
|
||||
if (txes.length !== 0) {
|
||||
await control.apply(txes, true)
|
||||
await control.apply(txes)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,9 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import activity, { ActivityMessage, ActivityReference } from '@hcengineering/activity'
|
||||
import chunter, { Channel, ChatMessage, chunterId, ChunterSpace, ThreadMessage } from '@hcengineering/chunter'
|
||||
import { Person, PersonAccount } from '@hcengineering/contact'
|
||||
import core, {
|
||||
Account,
|
||||
AttachedDoc,
|
||||
@ -38,14 +40,12 @@ import notification, { Collaborators, NotificationContent } from '@hcengineering
|
||||
import { getMetadata, IntlString } from '@hcengineering/platform'
|
||||
import serverCore, { TriggerControl } from '@hcengineering/server-core'
|
||||
import {
|
||||
createCollaboratorNotifications,
|
||||
getDocCollaborators,
|
||||
getMixinTx,
|
||||
createCollaboratorNotifications
|
||||
getMixinTx
|
||||
} from '@hcengineering/server-notification-resources'
|
||||
import { workbenchId } from '@hcengineering/workbench'
|
||||
import { stripTags } from '@hcengineering/text'
|
||||
import { Person, PersonAccount } from '@hcengineering/contact'
|
||||
import activity, { ActivityMessage, ActivityReference } from '@hcengineering/activity'
|
||||
import { workbenchId } from '@hcengineering/workbench'
|
||||
|
||||
import { NOTIFICATION_BODY_SIZE } from '@hcengineering/server-notification'
|
||||
|
||||
@ -258,13 +258,24 @@ async function OnThreadMessageDeleted (tx: Tx, control: TriggerControl): Promise
|
||||
* @public
|
||||
*/
|
||||
export async function ChunterTrigger (tx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> {
|
||||
const res = await Promise.all([
|
||||
OnThreadMessageCreated(tx, control),
|
||||
OnThreadMessageDeleted(tx, control),
|
||||
OnCollaboratorsChanged(tx as TxMixin<Doc, Collaborators>, control),
|
||||
OnChatMessageCreated(tx, control)
|
||||
])
|
||||
return res.flat()
|
||||
const res: Tx[] = []
|
||||
res.push(
|
||||
...(await control.ctx.with('OnThreadMessageCreated', {}, async (ctx) => await OnThreadMessageCreated(tx, control)))
|
||||
)
|
||||
res.push(
|
||||
...(await control.ctx.with('OnThreadMessageDeleted', {}, async (ctx) => await OnThreadMessageDeleted(tx, control)))
|
||||
)
|
||||
res.push(
|
||||
...(await control.ctx.with(
|
||||
'OnCollaboratorsChanged',
|
||||
{},
|
||||
async (ctx) => await OnCollaboratorsChanged(tx as TxMixin<Doc, Collaborators>, control)
|
||||
))
|
||||
)
|
||||
res.push(
|
||||
...(await control.ctx.with('OnChatMessageCreated', {}, async (ctx) => await OnChatMessageCreated(tx, control)))
|
||||
)
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
@ -393,7 +404,7 @@ async function OnChannelMembersChanged (tx: TxUpdateDoc<Channel>, control: Trigg
|
||||
lastViewedTimestamp: tx.modifiedOn
|
||||
})
|
||||
|
||||
await control.apply([createTx], true)
|
||||
await control.apply([createTx])
|
||||
} else {
|
||||
const updateTx = control.txFactory.createTxUpdateDoc(context._class, context.space, context._id, {
|
||||
hidden: false,
|
||||
|
@ -284,7 +284,7 @@ async function createDocumentTrainingRequest (doc: ControlledDocument, control:
|
||||
// Force space to make transaction persistent and raise notifications
|
||||
resTx.space = core.space.Tx
|
||||
|
||||
await control.apply([resTx], true)
|
||||
await control.apply([resTx])
|
||||
|
||||
return []
|
||||
}
|
||||
@ -368,7 +368,7 @@ export async function OnDocPlannedEffectiveDateChanged (
|
||||
if (tx.operations.plannedEffectiveDate === 0 && doc.controlledState === ControlledDocumentState.Approved) {
|
||||
// Create with not derived tx factory in order for notifications to work
|
||||
const factory = new TxFactory(control.txFactory.account)
|
||||
await control.apply([makeDocEffective(doc, factory)], true)
|
||||
await control.apply([makeDocEffective(doc, factory)])
|
||||
}
|
||||
|
||||
return []
|
||||
@ -385,7 +385,7 @@ export async function OnDocApprovalRequestApproved (
|
||||
|
||||
// Create with not derived tx factory in order for notifications to work
|
||||
const factory = new TxFactory(control.txFactory.account)
|
||||
await control.apply([makeDocEffective(doc, factory)], true)
|
||||
await control.apply([makeDocEffective(doc, factory)])
|
||||
|
||||
// make doc effective immediately
|
||||
return []
|
||||
|
@ -97,9 +97,10 @@ async function createUserInfo (acc: Ref<Account>, control: TriggerControl): Prom
|
||||
query: { person }
|
||||
}
|
||||
],
|
||||
[tx]
|
||||
[tx],
|
||||
'createUserInfo'
|
||||
)
|
||||
await control.apply([ptx], true)
|
||||
await control.apply([ptx])
|
||||
return []
|
||||
}
|
||||
|
||||
@ -114,7 +115,7 @@ async function removeUserInfo (acc: Ref<Account>, control: TriggerControl): Prom
|
||||
const person = account.person
|
||||
const infos = await control.findAll(love.class.ParticipantInfo, { person })
|
||||
for (const info of infos) {
|
||||
await control.apply([control.txFactory.createTxRemoveDoc(info._class, info.space, info._id)], true)
|
||||
await control.apply([control.txFactory.createTxRemoveDoc(info._class, info.space, info._id)])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,7 @@
|
||||
//
|
||||
|
||||
import activity, { ActivityMessage, DocUpdateMessage } from '@hcengineering/activity'
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import chunter, { ChatMessage } from '@hcengineering/chunter'
|
||||
import contact, {
|
||||
type AvatarInfo,
|
||||
@ -42,6 +43,7 @@ import core, {
|
||||
RefTo,
|
||||
Space,
|
||||
Timestamp,
|
||||
toIdMap,
|
||||
Tx,
|
||||
TxCollectionCUD,
|
||||
TxCreateDoc,
|
||||
@ -75,12 +77,11 @@ import serverNotification, {
|
||||
getPersonAccountById,
|
||||
NOTIFICATION_BODY_SIZE
|
||||
} from '@hcengineering/server-notification'
|
||||
import serverView from '@hcengineering/server-view'
|
||||
import { stripTags } from '@hcengineering/text'
|
||||
import { encodeObjectURI } from '@hcengineering/view'
|
||||
import { workbenchId } from '@hcengineering/workbench'
|
||||
import webpush, { WebPushError } from 'web-push'
|
||||
import { encodeObjectURI } from '@hcengineering/view'
|
||||
import serverView from '@hcengineering/server-view'
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
|
||||
import { Content, NotifyParams, NotifyResult, UserInfo } from './types'
|
||||
import {
|
||||
@ -396,14 +397,24 @@ export async function pushInboxNotifications (
|
||||
hidden: false,
|
||||
lastUpdateTimestamp: shouldUpdateTimestamp ? modifiedOn : undefined
|
||||
})
|
||||
await control.apply([createContextTx], true, [account.email])
|
||||
await control.apply([createContextTx])
|
||||
control.operationContext.derived.targets['docNotifyContext' + createContextTx._id] = (it) => {
|
||||
if (it._id === createContextTx._id) {
|
||||
return [account.email]
|
||||
}
|
||||
}
|
||||
docNotifyContextId = createContextTx.objectId
|
||||
} else {
|
||||
if (shouldUpdateTimestamp && context.lastUpdateTimestamp !== modifiedOn) {
|
||||
const updateTx = control.txFactory.createTxUpdateDoc(context._class, context.space, context._id, {
|
||||
lastUpdateTimestamp: modifiedOn
|
||||
})
|
||||
await control.apply([updateTx], true, [account.email])
|
||||
await control.apply([updateTx])
|
||||
control.operationContext.derived.targets['docNotifyContext' + updateTx._id] = (it) => {
|
||||
if (it._id === updateTx._id) {
|
||||
return [account.email]
|
||||
}
|
||||
}
|
||||
}
|
||||
docNotifyContextId = context._id
|
||||
}
|
||||
@ -636,7 +647,7 @@ async function sendPushToSubscription (
|
||||
console.log('Cannot send push notification to', targetUser, err)
|
||||
if (err instanceof WebPushError && err.body.includes('expired')) {
|
||||
const tx = control.txFactory.createTxRemoveDoc(subscription._class, subscription.space, subscription._id)
|
||||
await control.apply([tx], true)
|
||||
await control.apply([tx])
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1286,7 +1297,14 @@ async function applyUserTxes (
|
||||
|
||||
if (account !== undefined) {
|
||||
cache.set(account._id, account)
|
||||
await control.apply(txs, true, [account.email])
|
||||
await control.apply(txs)
|
||||
|
||||
const m1 = toIdMap(txes)
|
||||
control.operationContext.derived.targets.docNotifyContext = (it) => {
|
||||
if (m1.has(it._id)) {
|
||||
return [account.email]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,7 +99,7 @@ async function OnRequestUpdate (tx: TxCollectionCUD<Doc, Request>, control: Trig
|
||||
}
|
||||
|
||||
if (applyTxes.length > 0) {
|
||||
await control.apply(applyTxes, true)
|
||||
await control.apply(applyTxes)
|
||||
}
|
||||
|
||||
return []
|
||||
|
@ -107,7 +107,7 @@ export async function OnWorkSlotCreate (tx: Tx, control: TriggerControl): Promis
|
||||
issue.collection,
|
||||
innerTx
|
||||
)
|
||||
await control.apply([outerTx], true)
|
||||
await control.apply([outerTx])
|
||||
return []
|
||||
}
|
||||
}
|
||||
@ -153,7 +153,7 @@ export async function OnToDoRemove (tx: Tx, control: TriggerControl): Promise<Tx
|
||||
issue.collection,
|
||||
innerTx
|
||||
)
|
||||
await control.apply([outerTx], true)
|
||||
await control.apply([outerTx])
|
||||
return []
|
||||
}
|
||||
}
|
||||
@ -304,7 +304,7 @@ export async function OnToDoUpdate (tx: Tx, control: TriggerControl): Promise<Tx
|
||||
if (funcs !== undefined) {
|
||||
const func = await getResource(funcs.onDone)
|
||||
const todoRes = await func(control, resEvents, todo)
|
||||
await control.apply(todoRes, true)
|
||||
await control.apply(todoRes)
|
||||
}
|
||||
return res
|
||||
}
|
||||
@ -447,7 +447,7 @@ async function createIssueHandler (issue: Issue, control: TriggerControl): Promi
|
||||
if (status.category === task.statusCategory.Active || status.category === task.statusCategory.ToDo) {
|
||||
const tx = await getCreateToDoTx(issue, issue.assignee, control)
|
||||
if (tx !== undefined) {
|
||||
await control.apply([tx], true)
|
||||
await control.apply([tx])
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -561,7 +561,7 @@ async function changeIssueStatusHandler (
|
||||
if (todos.length === 0) {
|
||||
const tx = await getCreateToDoTx(issue, issue.assignee, control)
|
||||
if (tx !== undefined) {
|
||||
await control.apply([tx], true)
|
||||
await control.apply([tx])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,9 +24,10 @@ import core, {
|
||||
TxProcessor,
|
||||
cutObjectArray,
|
||||
toFindResult,
|
||||
type Branding,
|
||||
type Account,
|
||||
type AttachedDoc,
|
||||
type Branding,
|
||||
type BroadcastTargets,
|
||||
type Class,
|
||||
type Client,
|
||||
type Collection,
|
||||
@ -50,6 +51,7 @@ import core, {
|
||||
type Timestamp,
|
||||
type Tx,
|
||||
type TxApplyIf,
|
||||
type TxApplyResult,
|
||||
type TxCUD,
|
||||
type TxCollectionCUD,
|
||||
type TxRemoveDoc,
|
||||
@ -612,7 +614,7 @@ export class TServerStorage implements ServerStorage {
|
||||
return result
|
||||
}
|
||||
|
||||
private async broadcastCtx (derived: SessionOperationContext['derived']): Promise<void> {
|
||||
private async broadcastCtx (derived: Tx[], targets?: BroadcastTargets): Promise<void> {
|
||||
const toSendTarget = new Map<string, Tx[]>()
|
||||
|
||||
const getTxes = (key: string): Tx[] => {
|
||||
@ -626,16 +628,23 @@ export class TServerStorage implements ServerStorage {
|
||||
|
||||
// Put current user as send target
|
||||
for (const txd of derived) {
|
||||
if (txd.target === undefined) {
|
||||
let target: string[] | undefined
|
||||
for (const tt of Object.values(targets ?? {})) {
|
||||
target = tt(txd)
|
||||
if (target !== undefined) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if (target === undefined) {
|
||||
getTxes('') // Be sure we have empty one
|
||||
|
||||
// Also add to all other targeted sends
|
||||
for (const v of toSendTarget.values()) {
|
||||
v.push(...txd.derived)
|
||||
v.push(txd)
|
||||
}
|
||||
} else {
|
||||
for (const t of txd.target) {
|
||||
getTxes(t).push(...txd.derived)
|
||||
for (const t of target) {
|
||||
getTxes(t).push(txd)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -708,7 +717,9 @@ export class TServerStorage implements ServerStorage {
|
||||
)
|
||||
const moves = await ctx.with('process-move', {}, (ctx) => this.processMove(ctx.ctx, txes, findAll))
|
||||
|
||||
const triggerControl: Omit<TriggerControl, 'txFactory' | 'ctx' | 'result'> = {
|
||||
const applyTxes: Tx[] = []
|
||||
|
||||
const triggerControl: Omit<TriggerControl, 'txFactory' | 'ctx' | 'result' | 'apply'> = {
|
||||
operationContext: ctx,
|
||||
removedMap,
|
||||
workspace: this.workspaceId,
|
||||
@ -719,52 +730,73 @@ export class TServerStorage implements ServerStorage {
|
||||
findAllCtx: findAll,
|
||||
modelDb: this.modelDb,
|
||||
hierarchy: this.hierarchy,
|
||||
apply: async (tx, broadcast, target) => {
|
||||
return await this.apply(ctx, tx, broadcast, target)
|
||||
},
|
||||
applyCtx: async (ctx, tx, broadcast, target) => {
|
||||
return await this.apply(ctx, tx, broadcast, target)
|
||||
applyCtx: async (ctx, tx, needResult) => {
|
||||
if (needResult === true) {
|
||||
return await this.apply(ctx, tx)
|
||||
} else {
|
||||
applyTxes.push(...tx)
|
||||
}
|
||||
return {}
|
||||
},
|
||||
// Will create a live query if missing and return values immediately if already asked.
|
||||
queryFind: async (_class, query, options) => {
|
||||
return await this.liveQuery.queryFind(_class, query, options)
|
||||
}
|
||||
}
|
||||
const triggers = await ctx.with('process-triggers', {}, async (ctx) => {
|
||||
const result: Tx[] = []
|
||||
const { transactions, performAsync } = await this.triggers.apply(ctx, txes, {
|
||||
...triggerControl,
|
||||
ctx: ctx.ctx,
|
||||
findAll: fAll(ctx.ctx),
|
||||
result
|
||||
})
|
||||
result.push(...transactions)
|
||||
const triggers = await ctx.with(
|
||||
'process-triggers',
|
||||
{},
|
||||
async (ctx) => {
|
||||
const result: Tx[] = []
|
||||
const { transactions, performAsync } = await this.triggers.apply(ctx, txes, {
|
||||
...triggerControl,
|
||||
ctx: ctx.ctx,
|
||||
findAll: fAll(ctx.ctx),
|
||||
result
|
||||
})
|
||||
result.push(...transactions)
|
||||
|
||||
if (performAsync !== undefined) {
|
||||
const asyncTriggerProcessor = async (): Promise<void> => {
|
||||
await ctx.with('process-async-triggers', {}, async (ctx) => {
|
||||
const sctx = ctx as SessionContext
|
||||
const applyCtx: SessionContextImpl = new SessionContextImpl(
|
||||
ctx.ctx,
|
||||
sctx.userEmail,
|
||||
sctx.sessionId,
|
||||
sctx.admin,
|
||||
[],
|
||||
this.workspaceId,
|
||||
this.options.branding
|
||||
if (applyTxes.length > 0) {
|
||||
await this.apply(ctx, applyTxes)
|
||||
}
|
||||
|
||||
if (performAsync !== undefined) {
|
||||
const asyncTriggerProcessor = async (): Promise<void> => {
|
||||
await ctx.with(
|
||||
'process-async-triggers',
|
||||
{},
|
||||
async (ctx) => {
|
||||
const sctx = ctx as SessionContext
|
||||
const applyCtx: SessionContextImpl = new SessionContextImpl(
|
||||
ctx.ctx,
|
||||
sctx.userEmail,
|
||||
sctx.sessionId,
|
||||
sctx.admin,
|
||||
{ txes: [], targets: {} },
|
||||
this.workspaceId,
|
||||
this.options.branding,
|
||||
true
|
||||
)
|
||||
const result = await performAsync(applyCtx)
|
||||
|
||||
if (applyTxes.length > 0) {
|
||||
await this.apply(applyCtx, applyTxes)
|
||||
}
|
||||
// We need to broadcast changes
|
||||
await this.broadcastCtx(applyCtx.derived.txes.concat(result), applyCtx.derived.targets)
|
||||
},
|
||||
{ count: txes.length }
|
||||
)
|
||||
const result = await performAsync(applyCtx)
|
||||
// We need to broadcast changes
|
||||
await this.broadcastCtx([{ derived: result }, ...applyCtx.derived])
|
||||
}
|
||||
setTimeout(() => {
|
||||
void asyncTriggerProcessor()
|
||||
})
|
||||
}
|
||||
setTimeout(() => {
|
||||
void asyncTriggerProcessor()
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
return result
|
||||
},
|
||||
{ count: txes.length }
|
||||
)
|
||||
|
||||
const derived = [...removed, ...collections, ...moves, ...triggers]
|
||||
|
||||
@ -835,8 +867,8 @@ export class TServerStorage implements ServerStorage {
|
||||
return { passed, onEnd }
|
||||
}
|
||||
|
||||
async apply (ctx: SessionOperationContext, txes: Tx[], broadcast: boolean, target?: string[]): Promise<TxResult> {
|
||||
return await this.processTxes(ctx, txes, broadcast, target)
|
||||
async apply (ctx: SessionOperationContext, txes: Tx[]): Promise<TxResult> {
|
||||
return await this.processTxes(ctx, txes)
|
||||
}
|
||||
|
||||
fillTxes (txes: Tx[], txToStore: Tx[], modelTx: Tx[], txToProcess: Tx[], applyTxes: Tx[]): void {
|
||||
@ -861,12 +893,7 @@ export class TServerStorage implements ServerStorage {
|
||||
}
|
||||
}
|
||||
|
||||
async processTxes (
|
||||
ctx: SessionOperationContext,
|
||||
txes: Tx[],
|
||||
broadcast: boolean,
|
||||
target?: string[]
|
||||
): Promise<TxResult> {
|
||||
async processTxes (ctx: SessionOperationContext, txes: Tx[]): Promise<TxResult> {
|
||||
// store tx
|
||||
const _findAll: ServerStorage['findAll'] = async <T extends Doc>(
|
||||
ctx: MeasureContext,
|
||||
@ -914,16 +941,25 @@ export class TServerStorage implements ServerStorage {
|
||||
await this.triggers.tx(tx)
|
||||
await this.modelDb.tx(tx)
|
||||
}
|
||||
await ctx.with('domain-tx', {}, async (ctx) => await this.getAdapter(DOMAIN_TX, true).tx(ctx.ctx, ...txToStore))
|
||||
result.push(...(await ctx.with('apply', {}, (ctx) => this.routeTx(ctx.ctx, removedMap, ...txToProcess))))
|
||||
await ctx.with('domain-tx', {}, async (ctx) => await this.getAdapter(DOMAIN_TX, true).tx(ctx.ctx, ...txToStore), {
|
||||
count: txToStore.length
|
||||
})
|
||||
result.push(...(await ctx.with('apply', {}, (ctx) => this.routeTx(ctx.ctx, removedMap, ...txToProcess))), {
|
||||
count: txToProcess.length
|
||||
})
|
||||
|
||||
// invoke triggers and store derived objects
|
||||
derived.push(...(await this.processDerived(ctx, txToProcess, _findAll, removedMap)))
|
||||
|
||||
// index object
|
||||
await ctx.with('fulltext-tx', {}, async (ctx) => {
|
||||
await this.fulltext.tx(ctx.ctx, [...txToProcess, ...derived])
|
||||
})
|
||||
await ctx.with(
|
||||
'fulltext-tx',
|
||||
{},
|
||||
async (ctx) => {
|
||||
await this.fulltext.tx(ctx.ctx, [...txToProcess, ...derived])
|
||||
},
|
||||
{ count: txToProcess.length + derived.length }
|
||||
)
|
||||
} catch (err: any) {
|
||||
ctx.ctx.error('error process tx', { error: err })
|
||||
Analytics.handleError(err)
|
||||
@ -933,16 +969,33 @@ export class TServerStorage implements ServerStorage {
|
||||
p()
|
||||
})
|
||||
}
|
||||
if (derived.length > 0 && broadcast) {
|
||||
ctx.derived.push({ derived, target })
|
||||
if (derived.length > 0) {
|
||||
ctx.derived.txes.push(...derived)
|
||||
}
|
||||
return result[0]
|
||||
}
|
||||
|
||||
async tx (ctx: SessionOperationContext, tx: Tx): Promise<TxResult> {
|
||||
return await ctx.with('client-tx', { _class: tx._class }, async (ctx) => {
|
||||
return await this.processTxes(ctx, [tx], true)
|
||||
})
|
||||
let measureName: string | undefined
|
||||
let st: number | undefined
|
||||
if (tx._class === core.class.TxApplyIf && (tx as TxApplyIf).measureName !== undefined) {
|
||||
measureName = (tx as TxApplyIf).measureName
|
||||
st = Date.now()
|
||||
}
|
||||
|
||||
const result = await ctx.with(
|
||||
measureName !== undefined ? `📶 ${measureName}` : 'client-tx',
|
||||
{ _class: tx._class },
|
||||
async (ctx) => {
|
||||
return await this.processTxes(ctx, [tx])
|
||||
}
|
||||
)
|
||||
|
||||
if (measureName !== undefined && st !== undefined) {
|
||||
;(result as TxApplyResult).serverTime = Date.now() - st
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
find (ctx: MeasureContext, domain: Domain, recheck?: boolean): StorageIterator {
|
||||
|
@ -17,6 +17,7 @@
|
||||
import core, {
|
||||
TxFactory,
|
||||
cutObjectArray,
|
||||
groupByArray,
|
||||
matchQuery,
|
||||
type AttachedDoc,
|
||||
type Class,
|
||||
@ -31,15 +32,18 @@ import core, {
|
||||
type TxCreateDoc
|
||||
} from '@hcengineering/core'
|
||||
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import { getResource, type Resource } from '@hcengineering/platform'
|
||||
import type { Trigger, TriggerControl, TriggerFunc } from './types'
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
|
||||
import serverCore from './plugin'
|
||||
import type { SessionContextImpl } from './utils'
|
||||
|
||||
interface TriggerRecord {
|
||||
query?: DocumentQuery<Tx>
|
||||
trigger: { op: TriggerFunc, resource: Resource<TriggerFunc>, isAsync: boolean }
|
||||
|
||||
arrays: boolean
|
||||
}
|
||||
/**
|
||||
* @public
|
||||
@ -64,7 +68,8 @@ export class Triggers {
|
||||
|
||||
this.triggers.push({
|
||||
query: match,
|
||||
trigger: { op: func, resource: trigger, isAsync }
|
||||
trigger: { op: func, resource: trigger, isAsync },
|
||||
arrays: (createTx as TxCreateDoc<Trigger>).attributes.arrays === true
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -73,47 +78,55 @@ export class Triggers {
|
||||
async apply (
|
||||
ctx: SessionOperationContext,
|
||||
tx: Tx[],
|
||||
ctrl: Omit<TriggerControl, 'txFactory'>
|
||||
ctrl: Omit<TriggerControl, 'txFactory' | 'apply'>
|
||||
): Promise<{
|
||||
transactions: Tx[]
|
||||
performAsync?: (ctx: SessionOperationContext) => Promise<Tx[]>
|
||||
}> {
|
||||
const result: Tx[] = []
|
||||
|
||||
const suppressAsync = (ctx as SessionContextImpl).isAsyncContext ?? false
|
||||
|
||||
const asyncRequest: {
|
||||
matches: Tx[]
|
||||
trigger: TriggerRecord['trigger']
|
||||
arrays: TriggerRecord['arrays']
|
||||
}[] = []
|
||||
|
||||
const applyTrigger = async (
|
||||
ctx: SessionOperationContext,
|
||||
matches: Tx[],
|
||||
trigger: TriggerRecord['trigger'],
|
||||
{ trigger, arrays }: TriggerRecord,
|
||||
result: Tx[]
|
||||
): Promise<void> => {
|
||||
for (const tx of matches) {
|
||||
try {
|
||||
result.push(
|
||||
...(await trigger.op(tx, {
|
||||
...ctrl,
|
||||
ctx: ctx.ctx,
|
||||
txFactory: new TxFactory(tx.modifiedBy, true),
|
||||
findAll: async (clazz, query, options) => await ctrl.findAllCtx(ctx.ctx, clazz, query, options),
|
||||
apply: async (tx, broadcast, target) => {
|
||||
return await ctrl.applyCtx(ctx, tx, broadcast, target)
|
||||
},
|
||||
result
|
||||
}))
|
||||
)
|
||||
} catch (err: any) {
|
||||
ctx.ctx.error('failed to process trigger', { trigger: trigger.resource, tx, err })
|
||||
Analytics.handleError(err)
|
||||
const group = groupByArray(matches, (it) => it.modifiedBy)
|
||||
|
||||
const tctrl: TriggerControl = {
|
||||
...ctrl,
|
||||
operationContext: ctx,
|
||||
ctx: ctx.ctx,
|
||||
txFactory: new TxFactory(core.account.System, true),
|
||||
findAll: async (clazz, query, options) => await ctrl.findAllCtx(ctx.ctx, clazz, query, options),
|
||||
apply: async (tx, needResult) => {
|
||||
return await ctrl.applyCtx(ctx, tx, needResult)
|
||||
},
|
||||
result
|
||||
}
|
||||
for (const [k, v] of group.entries()) {
|
||||
const m = arrays ? [v] : v
|
||||
tctrl.txFactory = new TxFactory(k, true)
|
||||
for (const tx of m) {
|
||||
try {
|
||||
result.push(...(await trigger.op(tx, tctrl)))
|
||||
} catch (err: any) {
|
||||
ctx.ctx.error('failed to process trigger', { trigger: trigger.resource, tx, err })
|
||||
Analytics.handleError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const promises: Promise<void>[] = []
|
||||
for (const { query, trigger } of this.triggers) {
|
||||
for (const { query, trigger, arrays } of this.triggers) {
|
||||
let matches = tx
|
||||
if (query !== undefined) {
|
||||
this.addDerived(query, 'objectClass')
|
||||
@ -121,23 +134,26 @@ export class Triggers {
|
||||
matches = matchQuery(tx, query, core.class.Tx, ctrl.hierarchy) as Tx[]
|
||||
}
|
||||
if (matches.length > 0) {
|
||||
if (trigger.isAsync) {
|
||||
if (trigger.isAsync && !suppressAsync) {
|
||||
asyncRequest.push({
|
||||
matches,
|
||||
trigger
|
||||
trigger,
|
||||
arrays
|
||||
})
|
||||
} else {
|
||||
promises.push(
|
||||
ctx.with(trigger.resource, {}, async (ctx) => {
|
||||
await applyTrigger(ctx, matches, trigger, result)
|
||||
})
|
||||
await ctx.with(
|
||||
trigger.resource,
|
||||
{},
|
||||
async (ctx) => {
|
||||
const tresult: Tx[] = []
|
||||
await applyTrigger(ctx, matches, { trigger, arrays }, tresult)
|
||||
result.push(...tresult)
|
||||
},
|
||||
{ count: matches.length, arrays }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Wait all regular triggers to complete in parallel
|
||||
await Promise.all(promises)
|
||||
|
||||
return {
|
||||
transactions: result,
|
||||
performAsync:
|
||||
@ -148,7 +164,7 @@ export class Triggers {
|
||||
for (const request of asyncRequest) {
|
||||
try {
|
||||
await ctx.with(request.trigger.resource, {}, async (ctx) => {
|
||||
await applyTrigger(ctx, request.matches, request.trigger, result)
|
||||
await applyTrigger(ctx, request.matches, request, result)
|
||||
})
|
||||
} catch (err: any) {
|
||||
ctx.ctx.error('failed to process trigger', {
|
||||
|
@ -16,6 +16,7 @@
|
||||
import {
|
||||
MeasureMetricsContext,
|
||||
type Account,
|
||||
type Branding,
|
||||
type Class,
|
||||
type Doc,
|
||||
type DocumentQuery,
|
||||
@ -40,8 +41,7 @@ import {
|
||||
type TxFactory,
|
||||
type TxResult,
|
||||
type WorkspaceId,
|
||||
type WorkspaceIdWithUrl,
|
||||
type Branding
|
||||
type WorkspaceIdWithUrl
|
||||
} from '@hcengineering/core'
|
||||
import type { Asset, Resource } from '@hcengineering/platform'
|
||||
import { type Readable } from 'stream'
|
||||
@ -162,8 +162,8 @@ export interface TriggerControl {
|
||||
storageAdapter: StorageAdapter
|
||||
serviceAdaptersManager: ServiceAdaptersManager
|
||||
// Bulk operations in case trigger require some
|
||||
apply: (tx: Tx[], broadcast: boolean, target?: string[]) => Promise<TxResult>
|
||||
applyCtx: (ctx: SessionOperationContext, tx: Tx[], broadcast: boolean, target?: string[]) => Promise<TxResult>
|
||||
apply: (tx: Tx[], needResult?: boolean) => Promise<TxResult>
|
||||
applyCtx: (ctx: SessionOperationContext, tx: Tx[], needResult?: boolean) => Promise<TxResult>
|
||||
|
||||
// Will create a live query if missing and return values immediately if already asked.
|
||||
queryFind: <T extends Doc>(
|
||||
@ -179,7 +179,7 @@ export interface TriggerControl {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type TriggerFunc = (tx: Tx, ctrl: TriggerControl) => Promise<Tx[]>
|
||||
export type TriggerFunc = (tx: Tx | Tx[], ctrl: TriggerControl) => Promise<Tx[]>
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -192,6 +192,9 @@ export interface Trigger extends Doc {
|
||||
|
||||
// We should match transaction
|
||||
txMatch?: DocumentQuery<Tx>
|
||||
|
||||
// If set trigger will handle Tx[] instead of Tx
|
||||
arrays?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -142,7 +142,8 @@ export class SessionContextImpl implements SessionContext {
|
||||
readonly admin: boolean | undefined,
|
||||
readonly derived: SessionContext['derived'],
|
||||
readonly workspace: WorkspaceIdWithUrl,
|
||||
readonly branding: Branding | null
|
||||
readonly branding: Branding | null,
|
||||
readonly isAsyncContext: boolean
|
||||
) {}
|
||||
|
||||
with<T>(
|
||||
@ -163,7 +164,8 @@ export class SessionContextImpl implements SessionContext {
|
||||
this.admin,
|
||||
this.derived,
|
||||
this.workspace,
|
||||
this.branding
|
||||
this.branding,
|
||||
this.isAsyncContext
|
||||
)
|
||||
),
|
||||
fullParams
|
||||
|
@ -32,7 +32,7 @@ import platform, { PlatformError, Severity, Status } from '@hcengineering/platfo
|
||||
import { Middleware, SessionContext, TxMiddlewareResult, type ServerStorage } from '@hcengineering/server-core'
|
||||
import { DOMAIN_PREFERENCE } from '@hcengineering/server-preference'
|
||||
import { BaseMiddleware } from './base'
|
||||
import { getUser, mergeTargets } from './utils'
|
||||
import { getUser } from './utils'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -65,12 +65,7 @@ export class PrivateMiddleware extends BaseMiddleware implements Middleware {
|
||||
}
|
||||
}
|
||||
}
|
||||
const res = await this.provideTx(ctx, tx)
|
||||
// Add target to all broadcasts
|
||||
ctx.derived.forEach((it) => {
|
||||
it.target = mergeTargets(target, it.target)
|
||||
})
|
||||
return res
|
||||
return await this.provideTx(ctx, tx)
|
||||
}
|
||||
|
||||
override async findAll<T extends Doc>(
|
||||
|
@ -335,10 +335,8 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle
|
||||
await this.processPermissionsUpdatesFromTx(ctx, tx)
|
||||
await this.checkPermissions(ctx, tx)
|
||||
const res = await this.provideTx(ctx, tx)
|
||||
for (const txd of ctx.derived) {
|
||||
for (const tx of txd.derived) {
|
||||
await this.processPermissionsUpdatesFromTx(ctx, tx)
|
||||
}
|
||||
for (const txd of ctx.derived.txes) {
|
||||
await this.processPermissionsUpdatesFromTx(ctx, txd)
|
||||
}
|
||||
return res
|
||||
}
|
||||
@ -347,7 +345,9 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle
|
||||
if (tx._class === core.class.TxApplyIf) {
|
||||
const applyTx = tx as TxApplyIf
|
||||
|
||||
await Promise.all(applyTx.txes.map((t) => this.checkPermissions(ctx, t)))
|
||||
for (const t of applyTx.txes) {
|
||||
await this.checkPermissions(ctx, t)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -46,7 +46,7 @@ import core, {
|
||||
import platform, { PlatformError, Severity, Status } from '@hcengineering/platform'
|
||||
import { Middleware, SessionContext, TxMiddlewareResult, type ServerStorage } from '@hcengineering/server-core'
|
||||
import { BaseMiddleware } from './base'
|
||||
import { getUser, isOwner, isSystem, mergeTargets } from './utils'
|
||||
import { getUser, isOwner, isSystem } from './utils'
|
||||
|
||||
type SpaceWithMembers = Pick<Space, '_id' | 'members' | 'private' | '_class'>
|
||||
|
||||
@ -246,10 +246,13 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
|
||||
space: core.space.DerivedTx,
|
||||
params: null
|
||||
}
|
||||
ctx.derived.push({
|
||||
derived: [tx],
|
||||
target: targets
|
||||
})
|
||||
ctx.derived.txes.push(tx)
|
||||
ctx.derived.targets.security = (it) => {
|
||||
// TODO: I'm not sure it is called
|
||||
if (it._id === tx._id) {
|
||||
return targets
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async broadcastNonMembers (ctx: SessionContext, space: SpaceWithMembers): Promise<void> {
|
||||
@ -413,17 +416,10 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
||||
}
|
||||
await this.processTx(ctx, tx)
|
||||
const targets = await this.getTxTargets(ctx, tx)
|
||||
const res = await this.provideTx(ctx, tx)
|
||||
for (const txd of ctx.derived) {
|
||||
for (const tx of txd.derived) {
|
||||
await this.processTx(ctx, tx)
|
||||
}
|
||||
for (const txd of ctx.derived.txes) {
|
||||
await this.processTx(ctx, txd)
|
||||
}
|
||||
ctx.derived.forEach((it) => {
|
||||
it.target = mergeTargets(targets, it.target)
|
||||
})
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
@ -435,6 +431,7 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
|
||||
for (const tx of txes) {
|
||||
const h = this.storage.hierarchy
|
||||
if (h.isDerived(tx._class, core.class.TxCUD)) {
|
||||
// TODO: Do we need security check here?
|
||||
const cudTx = tx as TxCUD<Doc>
|
||||
await this.processTxSpaceDomain(cudTx)
|
||||
} else if (tx._class === core.class.TxWorkspaceEvent) {
|
||||
|
@ -17,18 +17,6 @@ import core, { Account, AccountRole, systemAccountEmail } from '@hcengineering/c
|
||||
import platform, { PlatformError, Severity, Status } from '@hcengineering/platform'
|
||||
import { SessionContext, type ServerStorage } from '@hcengineering/server-core'
|
||||
|
||||
export function mergeTargets (current: string[] | undefined, prev: string[] | undefined): string[] | undefined {
|
||||
if (current === undefined) return prev
|
||||
if (prev === undefined) return current
|
||||
const res: string[] = []
|
||||
for (const value of current) {
|
||||
if (prev.includes(value)) {
|
||||
res.push(value)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
export async function getUser (storage: ServerStorage, ctx: SessionContext): Promise<Account> {
|
||||
if (ctx.userEmail === undefined) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
||||
|
@ -182,7 +182,10 @@ describe('mongo operations', () => {
|
||||
})
|
||||
const soCtx: SessionOperationContext = {
|
||||
ctx,
|
||||
derived: [],
|
||||
derived: {
|
||||
txes: [],
|
||||
targets: {}
|
||||
},
|
||||
with: async <T>(
|
||||
name: string,
|
||||
params: ParamsType,
|
||||
@ -206,7 +209,6 @@ describe('mongo operations', () => {
|
||||
clean: async (domain: Domain, docs: Ref<Doc>[]) => {},
|
||||
loadModel: async () => txes,
|
||||
getAccount: async () => ({}) as any,
|
||||
measure: async () => async () => ({ time: 0, serverTime: 0 }),
|
||||
sendForceClose: async () => {}
|
||||
}
|
||||
return st
|
||||
|
@ -114,9 +114,13 @@ export class ClientSession implements Session {
|
||||
this.token.email,
|
||||
this.sessionId,
|
||||
this.token.extra?.admin === 'true',
|
||||
[],
|
||||
{
|
||||
txes: [],
|
||||
targets: {}
|
||||
},
|
||||
this._pipeline.storage.workspaceId,
|
||||
this._pipeline.storage.branding
|
||||
this._pipeline.storage.branding,
|
||||
false
|
||||
)
|
||||
await this._pipeline.tx(context, createTx)
|
||||
const acc = TxProcessor.createDoc2Doc(createTx)
|
||||
@ -144,9 +148,13 @@ export class ClientSession implements Session {
|
||||
this.token.email,
|
||||
this.sessionId,
|
||||
this.token.extra?.admin === 'true',
|
||||
[],
|
||||
{
|
||||
txes: [],
|
||||
targets: {}
|
||||
},
|
||||
this._pipeline.storage.workspaceId,
|
||||
this._pipeline.storage.branding
|
||||
this._pipeline.storage.branding,
|
||||
false
|
||||
)
|
||||
return await this._pipeline.findAll(context, _class, query, options)
|
||||
}
|
||||
@ -167,9 +175,13 @@ export class ClientSession implements Session {
|
||||
this.token.email,
|
||||
this.sessionId,
|
||||
this.token.extra?.admin === 'true',
|
||||
[],
|
||||
{
|
||||
txes: [],
|
||||
targets: {}
|
||||
},
|
||||
this._pipeline.storage.workspaceId,
|
||||
this._pipeline.storage.branding
|
||||
this._pipeline.storage.branding,
|
||||
false
|
||||
)
|
||||
await ctx.sendResponse(await this._pipeline.searchFulltext(context, query, options))
|
||||
}
|
||||
@ -183,9 +195,13 @@ export class ClientSession implements Session {
|
||||
this.token.email,
|
||||
this.sessionId,
|
||||
this.token.extra?.admin === 'true',
|
||||
[],
|
||||
{
|
||||
txes: [],
|
||||
targets: {}
|
||||
},
|
||||
this._pipeline.storage.workspaceId,
|
||||
this._pipeline.storage.branding
|
||||
this._pipeline.storage.branding,
|
||||
false
|
||||
)
|
||||
|
||||
const result = await this._pipeline.tx(context, tx)
|
||||
@ -209,18 +225,24 @@ export class ClientSession implements Session {
|
||||
}
|
||||
|
||||
// Put current user as send target
|
||||
toSendTarget.set(this.getUser(), [])
|
||||
for (const txd of context.derived) {
|
||||
if (txd.target === undefined) {
|
||||
getTxes('') // be sure we have empty one
|
||||
for (const txd of context.derived.txes) {
|
||||
let target: string[] | undefined
|
||||
for (const tt of Object.values(context.derived.targets ?? {})) {
|
||||
target = tt(txd)
|
||||
if (target !== undefined) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if (target === undefined) {
|
||||
getTxes('') // Be sure we have empty one
|
||||
|
||||
// Also add to all other targeted sends
|
||||
for (const v of toSendTarget.values()) {
|
||||
v.push(...txd.derived)
|
||||
v.push(txd)
|
||||
}
|
||||
} else {
|
||||
for (const t of txd.target) {
|
||||
getTxes(t).push(...txd.derived)
|
||||
for (const t of target) {
|
||||
getTxes(t).push(txd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -912,10 +912,6 @@ class TSessionManager implements SessionManager {
|
||||
}
|
||||
})
|
||||
|
||||
if (request.method === 'measure' || request.method === 'measure-done') {
|
||||
await this.handleMeasure<S>(service, request, opContext(ctx))
|
||||
return
|
||||
}
|
||||
service.requests.set(reqId, {
|
||||
id: reqId,
|
||||
params: request,
|
||||
@ -930,9 +926,7 @@ class TSessionManager implements SessionManager {
|
||||
try {
|
||||
const params = [...request.params]
|
||||
|
||||
service.measureCtx?.ctx !== undefined
|
||||
? await f.apply(service, [opContext(service.measureCtx?.ctx), ...params])
|
||||
: await ctx.with('🧨 process', {}, async (callTx) => f.apply(service, [opContext(callTx), ...params]))
|
||||
await ctx.with('🧨 process', {}, async (callTx) => f.apply(service, [opContext(callTx), ...params]))
|
||||
} catch (err: any) {
|
||||
Analytics.handleError(err)
|
||||
if (LOGGING_ENABLED) {
|
||||
@ -955,34 +949,7 @@ class TSessionManager implements SessionManager {
|
||||
service.requests.delete(reqId)
|
||||
}
|
||||
}
|
||||
|
||||
private async handleMeasure<S extends Session>(
|
||||
service: S,
|
||||
request: Request<any[]>,
|
||||
ctx: ClientSessionCtx
|
||||
): Promise<void> {
|
||||
let serverTime = 0
|
||||
if (request.method === 'measure') {
|
||||
service.measureCtx = { ctx: ctx.ctx.newChild('📶 ' + request.params[0], {}), time: Date.now() }
|
||||
} else {
|
||||
if (service.measureCtx !== undefined) {
|
||||
serverTime = Date.now() - service.measureCtx.time
|
||||
service.measureCtx.ctx.end(serverTime)
|
||||
service.measureCtx = undefined
|
||||
}
|
||||
}
|
||||
try {
|
||||
await ctx.sendResponse(request.method === 'measure' ? 'started' : serverTime)
|
||||
} catch (err: any) {
|
||||
Analytics.handleError(err)
|
||||
if (LOGGING_ENABLED) {
|
||||
ctx.ctx.error('error handle measure', { error: err, request })
|
||||
}
|
||||
await ctx.sendError(JSON.parse(JSON.stringify(err?.stack)), unknownError(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
|
@ -77,8 +77,6 @@ export interface Session {
|
||||
current: StatisticsElement
|
||||
mins5: StatisticsElement
|
||||
|
||||
measureCtx?: { ctx: MeasureContext, time: number }
|
||||
|
||||
lastRequest: number
|
||||
|
||||
isUpgradeClient: () => boolean
|
||||
|
Loading…
Reference in New Issue
Block a user