mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-04 22:37:00 +00:00
Merge remote-tracking branch 'origin/develop'
This commit is contained in:
commit
5960d4c95f
1
.vscode/launch.json
vendored
1
.vscode/launch.json
vendored
@ -41,6 +41,7 @@
|
|||||||
"METRICS_FILE": "${workspaceRoot}/metrics.txt", // Show metrics in console evert 30 seconds.,
|
"METRICS_FILE": "${workspaceRoot}/metrics.txt", // Show metrics in console evert 30 seconds.,
|
||||||
"STORAGE_CONFIG": "minio|localhost?accessKey=minioadmin&secretKey=minioadmin",
|
"STORAGE_CONFIG": "minio|localhost?accessKey=minioadmin&secretKey=minioadmin",
|
||||||
"SERVER_SECRET": "secret",
|
"SERVER_SECRET": "secret",
|
||||||
|
"ENABLE_CONSOLE": "true",
|
||||||
"COLLABORATOR_URL": "ws://localhost:3078",
|
"COLLABORATOR_URL": "ws://localhost:3078",
|
||||||
"COLLABORATOR_API_URL": "http://localhost:3078",
|
"COLLABORATOR_API_URL": "http://localhost:3078",
|
||||||
"REKONI_URL": "http://localhost:4004",
|
"REKONI_URL": "http://localhost:4004",
|
||||||
|
@ -1 +1 @@
|
|||||||
"0.6.272"
|
"0.6.274"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "desktop",
|
"name": "desktop",
|
||||||
"version": "0.6.266",
|
"version": "0.6.271",
|
||||||
"main": "dist/main/electron.js",
|
"main": "dist/main/electron.js",
|
||||||
"author": "Hardcore Engineering <hey@huly.io>",
|
"author": "Hardcore Engineering <hey@huly.io>",
|
||||||
"template": "@hcengineering/default-package",
|
"template": "@hcengineering/default-package",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hcengineering/desktop",
|
"name": "@hcengineering/desktop",
|
||||||
"version": "0.6.266",
|
"version": "0.6.271",
|
||||||
"main": "dist/main/electron.js",
|
"main": "dist/main/electron.js",
|
||||||
"template": "@hcengineering/webpack-package",
|
"template": "@hcengineering/webpack-package",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -329,13 +329,13 @@ export class TDocIndexState extends TDoc implements DocIndexState {
|
|||||||
attributes!: Record<string, any>
|
attributes!: Record<string, any>
|
||||||
|
|
||||||
@Prop(TypeBoolean(), getEmbeddedLabel('Removed'))
|
@Prop(TypeBoolean(), getEmbeddedLabel('Removed'))
|
||||||
@Index(IndexKind.Indexed)
|
// @Index(IndexKind.Indexed)
|
||||||
@Hidden()
|
@Hidden()
|
||||||
removed!: boolean
|
removed!: boolean
|
||||||
|
|
||||||
// States for different stages
|
// States for different stages
|
||||||
@Prop(TypeRecord(), getEmbeddedLabel('Stages'))
|
@Prop(TypeRecord(), getEmbeddedLabel('Stages'))
|
||||||
@Index(IndexKind.Indexed)
|
// @Index(IndexKind.Indexed)
|
||||||
@Hidden()
|
@Hidden()
|
||||||
stages!: Record<string, boolean | string>
|
stages!: Record<string, boolean | string>
|
||||||
|
|
||||||
|
@ -27,7 +27,6 @@ import {
|
|||||||
type AttachedDoc,
|
type AttachedDoc,
|
||||||
type Class,
|
type Class,
|
||||||
type Doc,
|
type Doc,
|
||||||
type DocIndexState,
|
|
||||||
type IndexingConfiguration,
|
type IndexingConfiguration,
|
||||||
type TxCollectionCUD
|
type TxCollectionCUD
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
@ -284,8 +283,10 @@ export function createModel (builder: Builder): void {
|
|||||||
|
|
||||||
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
|
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
|
||||||
domain: DOMAIN_DOC_INDEX_STATE,
|
domain: DOMAIN_DOC_INDEX_STATE,
|
||||||
|
indexes: [{ keys: { removed: 1 }, filter: { removed: true } }],
|
||||||
disabled: [
|
disabled: [
|
||||||
{ attachedToClass: 1 },
|
{ attachedToClass: 1 },
|
||||||
|
{ objectClass: 1 },
|
||||||
{ stages: 1 },
|
{ stages: 1 },
|
||||||
{ generationId: 1 },
|
{ generationId: 1 },
|
||||||
{ space: 1 },
|
{ space: 1 },
|
||||||
@ -298,24 +299,6 @@ export function createModel (builder: Builder): void {
|
|||||||
skip: ['stages.']
|
skip: ['stages.']
|
||||||
})
|
})
|
||||||
|
|
||||||
builder.mixin<Class<DocIndexState>, IndexingConfiguration<TxCollectionCUD<Doc, AttachedDoc>>>(
|
|
||||||
core.class.DocIndexState,
|
|
||||||
core.class.Class,
|
|
||||||
core.mixin.IndexConfiguration,
|
|
||||||
{
|
|
||||||
indexes: [
|
|
||||||
{
|
|
||||||
keys: {
|
|
||||||
_class: 1,
|
|
||||||
stages: 1,
|
|
||||||
_id: 1,
|
|
||||||
modifiedOn: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
builder.mixin(core.class.Space, core.class.Class, core.mixin.FullTextSearchContext, {
|
builder.mixin(core.class.Space, core.class.Class, core.mixin.FullTextSearchContext, {
|
||||||
childProcessingAllowed: false
|
childProcessingAllowed: false
|
||||||
})
|
})
|
||||||
|
@ -227,6 +227,12 @@ export function createModel (builder: Builder): void {
|
|||||||
'status',
|
'status',
|
||||||
'attachments',
|
'attachments',
|
||||||
'comments',
|
'comments',
|
||||||
|
{
|
||||||
|
key: '',
|
||||||
|
label: tracker.string.RelatedIssues,
|
||||||
|
presenter: tracker.component.RelatedIssueSelector,
|
||||||
|
displayProps: { key: 'related', suffix: true }
|
||||||
|
},
|
||||||
'modifiedOn',
|
'modifiedOn',
|
||||||
{
|
{
|
||||||
key: '$lookup.attachedTo.$lookup.channels',
|
key: '$lookup.attachedTo.$lookup.channels',
|
||||||
@ -294,6 +300,12 @@ export function createModel (builder: Builder): void {
|
|||||||
},
|
},
|
||||||
{ key: 'attachments', displayProps: { key: 'attachments', suffix: true } },
|
{ key: 'attachments', displayProps: { key: 'attachments', suffix: true } },
|
||||||
{ key: 'comments', displayProps: { key: 'comments', suffix: true } },
|
{ key: 'comments', displayProps: { key: 'comments', suffix: true } },
|
||||||
|
{
|
||||||
|
key: '',
|
||||||
|
label: tracker.string.RelatedIssues,
|
||||||
|
presenter: tracker.component.RelatedIssueSelector,
|
||||||
|
displayProps: { key: 'related', suffix: true }
|
||||||
|
},
|
||||||
{ key: '', displayProps: { grow: true } },
|
{ key: '', displayProps: { grow: true } },
|
||||||
{
|
{
|
||||||
key: '$lookup.attachedTo.$lookup.channels',
|
key: '$lookup.attachedTo.$lookup.channels',
|
||||||
@ -441,7 +453,7 @@ export function createModel (builder: Builder): void {
|
|||||||
groupDepth: 1
|
groupDepth: 1
|
||||||
},
|
},
|
||||||
options: lookupLeadOptions,
|
options: lookupLeadOptions,
|
||||||
config: ['attachedTo', 'attachments', 'comments', 'dueDate', 'assignee'],
|
config: ['attachedTo', 'status', 'attachments', 'comments', 'dueDate', 'assignee'],
|
||||||
configOptions: {
|
configOptions: {
|
||||||
strict: true
|
strict: true
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ import calendar from '@hcengineering/model-calendar'
|
|||||||
import chunter from '@hcengineering/model-chunter'
|
import chunter from '@hcengineering/model-chunter'
|
||||||
import contact from '@hcengineering/model-contact'
|
import contact from '@hcengineering/model-contact'
|
||||||
import core from '@hcengineering/model-core'
|
import core from '@hcengineering/model-core'
|
||||||
|
import gmail from '@hcengineering/model-gmail'
|
||||||
import { generateClassNotificationTypes } from '@hcengineering/model-notification'
|
import { generateClassNotificationTypes } from '@hcengineering/model-notification'
|
||||||
import presentation from '@hcengineering/model-presentation'
|
import presentation from '@hcengineering/model-presentation'
|
||||||
import tags from '@hcengineering/model-tags'
|
import tags from '@hcengineering/model-tags'
|
||||||
@ -33,7 +34,6 @@ import { type IntlString } from '@hcengineering/platform'
|
|||||||
import { recruitId, type Applicant } from '@hcengineering/recruit'
|
import { recruitId, type Applicant } from '@hcengineering/recruit'
|
||||||
import setting from '@hcengineering/setting'
|
import setting from '@hcengineering/setting'
|
||||||
import { type KeyBinding, type ViewOptionModel, type ViewOptionsModel } from '@hcengineering/view'
|
import { type KeyBinding, type ViewOptionModel, type ViewOptionsModel } from '@hcengineering/view'
|
||||||
import gmail from '@hcengineering/model-gmail'
|
|
||||||
|
|
||||||
import recruit from './plugin'
|
import recruit from './plugin'
|
||||||
import { createReviewModel, reviewTableConfig, reviewTableOptions } from './review'
|
import { createReviewModel, reviewTableConfig, reviewTableOptions } from './review'
|
||||||
@ -475,6 +475,12 @@ export function createModel (builder: Builder): void {
|
|||||||
'status',
|
'status',
|
||||||
'attachments',
|
'attachments',
|
||||||
'comments',
|
'comments',
|
||||||
|
{
|
||||||
|
key: '',
|
||||||
|
label: tracker.string.RelatedIssues,
|
||||||
|
presenter: tracker.component.RelatedIssueSelector,
|
||||||
|
displayProps: { key: 'related', suffix: true }
|
||||||
|
},
|
||||||
'modifiedOn',
|
'modifiedOn',
|
||||||
'$lookup.space.company',
|
'$lookup.space.company',
|
||||||
{
|
{
|
||||||
@ -604,6 +610,12 @@ export function createModel (builder: Builder): void {
|
|||||||
},
|
},
|
||||||
{ key: 'attachments', displayProps: { key: 'attachments', suffix: true } },
|
{ key: 'attachments', displayProps: { key: 'attachments', suffix: true } },
|
||||||
{ key: 'comments', displayProps: { key: 'comments', suffix: true } },
|
{ key: 'comments', displayProps: { key: 'comments', suffix: true } },
|
||||||
|
{
|
||||||
|
key: '',
|
||||||
|
label: tracker.string.RelatedIssues,
|
||||||
|
presenter: tracker.component.RelatedIssueSelector,
|
||||||
|
displayProps: { key: 'related', suffix: true }
|
||||||
|
},
|
||||||
{ key: '', displayProps: { grow: true } },
|
{ key: '', displayProps: { grow: true } },
|
||||||
{
|
{
|
||||||
key: '$lookup.space.company',
|
key: '$lookup.space.company',
|
||||||
|
@ -72,14 +72,6 @@ export function createModel (builder: Builder): void {
|
|||||||
TNotificationProviderResources
|
TNotificationProviderResources
|
||||||
)
|
)
|
||||||
|
|
||||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
|
||||||
trigger: serverNotification.trigger.OnActivityNotificationViewed,
|
|
||||||
txMatch: {
|
|
||||||
_class: core.class.TxUpdateDoc,
|
|
||||||
objectClass: notification.class.ActivityInboxNotification
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||||
trigger: serverNotification.trigger.OnAttributeCreate,
|
trigger: serverNotification.trigger.OnAttributeCreate,
|
||||||
txMatch: {
|
txMatch: {
|
||||||
|
@ -579,7 +579,7 @@ export function isActivityMessage (message?: Doc): message is ActivityMessage {
|
|||||||
return getClient().getHierarchy().isDerived(message._class, activity.class.ActivityMessage)
|
return getClient().getHierarchy().isDerived(message._class, activity.class.ActivityMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isReactionMessage (message?: ActivityMessage): boolean {
|
export function isReactionMessage (message?: ActivityMessage): message is DocUpdateMessage {
|
||||||
if (message === undefined) {
|
if (message === undefined) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -46,6 +46,7 @@ import { getClient } from '@hcengineering/presentation'
|
|||||||
import { type AnySvelteComponent } from '@hcengineering/ui'
|
import { type AnySvelteComponent } from '@hcengineering/ui'
|
||||||
import { classIcon, getDocLinkTitle, getDocTitle } from '@hcengineering/view-resources'
|
import { classIcon, getDocLinkTitle, getDocTitle } from '@hcengineering/view-resources'
|
||||||
import { get, writable, type Unsubscriber } from 'svelte/store'
|
import { get, writable, type Unsubscriber } from 'svelte/store'
|
||||||
|
import { isReactionMessage } from '@hcengineering/activity-resources'
|
||||||
|
|
||||||
import ChannelIcon from './components/ChannelIcon.svelte'
|
import ChannelIcon from './components/ChannelIcon.svelte'
|
||||||
import DirectIcon from './components/DirectIcon.svelte'
|
import DirectIcon from './components/DirectIcon.svelte'
|
||||||
@ -439,7 +440,16 @@ export async function readChannelMessages (
|
|||||||
const allIds = getAllIds(messages).filter((id) => !readMessages.has(id))
|
const allIds = getAllIds(messages).filter((id) => !readMessages.has(id))
|
||||||
|
|
||||||
const notifications = get(inboxClient.activityInboxNotifications)
|
const notifications = get(inboxClient.activityInboxNotifications)
|
||||||
.filter(({ _id, attachedTo }) => allIds.includes(attachedTo))
|
.filter(({ attachedTo, $lookup, isViewed }) => {
|
||||||
|
if (isViewed) return false
|
||||||
|
const includes = allIds.includes(attachedTo)
|
||||||
|
if (includes) return true
|
||||||
|
const msg = $lookup?.attachedTo
|
||||||
|
if (isReactionMessage(msg)) {
|
||||||
|
return allIds.includes(msg.attachedTo as Ref<ActivityMessage>)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
.map((n) => n._id)
|
.map((n) => n._id)
|
||||||
|
|
||||||
const relatedMentions = get(inboxClient.otherInboxNotifications)
|
const relatedMentions = get(inboxClient.otherInboxNotifications)
|
||||||
|
@ -52,6 +52,7 @@
|
|||||||
"@hcengineering/presentation": "^0.6.3",
|
"@hcengineering/presentation": "^0.6.3",
|
||||||
"@hcengineering/task": "^0.6.20",
|
"@hcengineering/task": "^0.6.20",
|
||||||
"@hcengineering/task-resources": "^0.6.0",
|
"@hcengineering/task-resources": "^0.6.0",
|
||||||
|
"@hcengineering/tracker": "^0.6.24",
|
||||||
"@hcengineering/text-editor-resources": "^0.6.0",
|
"@hcengineering/text-editor-resources": "^0.6.0",
|
||||||
"@hcengineering/ui": "^0.6.15",
|
"@hcengineering/ui": "^0.6.15",
|
||||||
"@hcengineering/view": "^0.6.13",
|
"@hcengineering/view": "^0.6.13",
|
||||||
|
@ -23,16 +23,18 @@
|
|||||||
import notification from '@hcengineering/notification'
|
import notification from '@hcengineering/notification'
|
||||||
import { getClient } from '@hcengineering/presentation'
|
import { getClient } from '@hcengineering/presentation'
|
||||||
import task from '@hcengineering/task'
|
import task from '@hcengineering/task'
|
||||||
import { AssigneePresenter } from '@hcengineering/task-resources'
|
import { AssigneePresenter, StateRefPresenter } from '@hcengineering/task-resources'
|
||||||
import { ActionIcon, Component, DueDatePresenter, IconMoreH } from '@hcengineering/ui'
|
import { ActionIcon, Component, DueDatePresenter, IconMoreH } from '@hcengineering/ui'
|
||||||
import { BuildModelKey } from '@hcengineering/view'
|
import { BuildModelKey } from '@hcengineering/view'
|
||||||
import { enabledConfig, openDoc, showMenu, statusStore } from '@hcengineering/view-resources'
|
import { enabledConfig, openDoc, showMenu, statusStore } from '@hcengineering/view-resources'
|
||||||
|
import tracker from '@hcengineering/tracker'
|
||||||
|
|
||||||
import lead from '../plugin'
|
import lead from '../plugin'
|
||||||
import LeadPresenter from './LeadPresenter.svelte'
|
import LeadPresenter from './LeadPresenter.svelte'
|
||||||
|
|
||||||
export let object: WithLookup<Lead>
|
export let object: WithLookup<Lead>
|
||||||
export let config: (string | BuildModelKey)[]
|
export let config: (string | BuildModelKey)[]
|
||||||
|
export let groupByKey: string
|
||||||
|
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
const assigneeAttribute = client.getHierarchy().getAttribute(lead.class.Lead, 'assignee')
|
const assigneeAttribute = client.getHierarchy().getAttribute(lead.class.Lead, 'assignee')
|
||||||
@ -69,8 +71,21 @@
|
|||||||
<ContactPresenter value={object.$lookup.attachedTo} avatarSize={'small'} />
|
<ContactPresenter value={object.$lookup.attachedTo} avatarSize={'small'} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if enabledConfig(config, 'dueDate')}
|
<div class="card-labels mb-2">
|
||||||
<div class="card-labels labels mb-2">
|
{#if groupByKey !== 'status' && enabledConfig(config, 'status')}
|
||||||
|
<StateRefPresenter
|
||||||
|
size={'small'}
|
||||||
|
kind={'link-bordered'}
|
||||||
|
space={object.space}
|
||||||
|
shrink={1}
|
||||||
|
value={object.status}
|
||||||
|
onChange={(status) => {
|
||||||
|
client.update(object, { status })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<Component showLoading={false} is={tracker.component.RelatedIssueSelector} props={{ object, size: 'small' }} />
|
||||||
|
{#if enabledConfig(config, 'dueDate')}
|
||||||
<DueDatePresenter
|
<DueDatePresenter
|
||||||
size={'small'}
|
size={'small'}
|
||||||
kind={'link-bordered'}
|
kind={'link-bordered'}
|
||||||
@ -82,8 +97,8 @@
|
|||||||
await client.update(object, { dueDate: e })
|
await client.update(object, { dueDate: e })
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
</div>
|
||||||
<div class="flex-between">
|
<div class="flex-between">
|
||||||
<div class="flex-row-center gap-3 reverse mr-4">
|
<div class="flex-row-center gap-3 reverse mr-4">
|
||||||
<LeadPresenter value={object} />
|
<LeadPresenter value={object} />
|
||||||
@ -105,3 +120,17 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.card-labels {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
&.labels {
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 1;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -111,7 +111,7 @@
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<!-- <Component showLoading={false} is={tracker.component.RelatedIssueSelector} props={{ object, size: 'small' }} /> -->
|
<Component showLoading={false} is={tracker.component.RelatedIssueSelector} props={{ object, size: 'small' }} />
|
||||||
{#if enabledConfig(config, 'dueDate')}
|
{#if enabledConfig(config, 'dueDate')}
|
||||||
<DueDatePresenter
|
<DueDatePresenter
|
||||||
size={'small'}
|
size={'small'}
|
||||||
|
@ -28,44 +28,30 @@
|
|||||||
let state: UppyState<any, any> = upload.uppy.getState()
|
let state: UppyState<any, any> = upload.uppy.getState()
|
||||||
let progress: number = state.totalProgress
|
let progress: number = state.totalProgress
|
||||||
|
|
||||||
|
$: state = upload.uppy.getState()
|
||||||
|
$: progress = state.totalProgress
|
||||||
$: files = Object.values(state.files)
|
$: files = Object.values(state.files)
|
||||||
$: filesTotal = files.length
|
$: filesTotal = files.length
|
||||||
$: filesComplete = files.filter((p) => p.progress?.uploadComplete).length
|
$: filesComplete = files.filter((p) => p.progress?.uploadComplete).length
|
||||||
|
|
||||||
function handleProgress (totalProgress: number): void {
|
function updateState (): void {
|
||||||
progress = totalProgress
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleUploadProgress (): void {
|
|
||||||
state = upload.uppy.getState()
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleUploadSuccess (): void {
|
|
||||||
state = upload.uppy.getState()
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleFileRemoved (): void {
|
|
||||||
state = upload.uppy.getState()
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleError (): void {
|
|
||||||
state = upload.uppy.getState()
|
state = upload.uppy.getState()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
upload.uppy.on('error', handleError)
|
upload.uppy.on('error', updateState)
|
||||||
upload.uppy.on('progress', handleProgress)
|
upload.uppy.on('progress', updateState)
|
||||||
upload.uppy.on('upload-progress', handleUploadProgress)
|
upload.uppy.on('upload-progress', updateState)
|
||||||
upload.uppy.on('upload-success', handleUploadSuccess)
|
upload.uppy.on('upload-success', updateState)
|
||||||
upload.uppy.on('file-removed', handleFileRemoved)
|
upload.uppy.on('file-removed', updateState)
|
||||||
})
|
})
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
upload.uppy.off('error', handleError)
|
upload.uppy.off('error', updateState)
|
||||||
upload.uppy.off('progress', handleProgress)
|
upload.uppy.off('progress', updateState)
|
||||||
upload.uppy.off('upload-progress', handleUploadProgress)
|
upload.uppy.off('upload-progress', updateState)
|
||||||
upload.uppy.off('upload-success', handleUploadSuccess)
|
upload.uppy.off('upload-success', updateState)
|
||||||
upload.uppy.off('file-removed', handleFileRemoved)
|
upload.uppy.off('file-removed', updateState)
|
||||||
})
|
})
|
||||||
|
|
||||||
function handleClick (ev: MouseEvent): void {
|
function handleClick (ev: MouseEvent): void {
|
||||||
|
@ -40,28 +40,21 @@
|
|||||||
|
|
||||||
let state: UppyState<any, any> = upload.uppy.getState()
|
let state: UppyState<any, any> = upload.uppy.getState()
|
||||||
|
|
||||||
|
$: state = upload.uppy.getState()
|
||||||
$: files = Object.values(state.files)
|
$: files = Object.values(state.files)
|
||||||
$: capabilities = state.capabilities ?? {}
|
$: capabilities = state.capabilities ?? {}
|
||||||
$: individualCancellation = 'individualCancellation' in capabilities && capabilities.individualCancellation
|
$: individualCancellation = 'individualCancellation' in capabilities && capabilities.individualCancellation
|
||||||
|
|
||||||
|
function updateState (): void {
|
||||||
|
state = upload.uppy.getState()
|
||||||
|
}
|
||||||
|
|
||||||
function handleComplete (): void {
|
function handleComplete (): void {
|
||||||
if (upload.uppy.getState().error === undefined) {
|
if (upload.uppy.getState().error === undefined) {
|
||||||
dispatch('close')
|
dispatch('close')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleUploadError (): void {
|
|
||||||
state = upload.uppy.getState()
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleUploadProgress (): void {
|
|
||||||
state = upload.uppy.getState()
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleUploadSuccess (): void {
|
|
||||||
state = upload.uppy.getState()
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleFileRemoved (): void {
|
function handleFileRemoved (): void {
|
||||||
state = upload.uppy.getState()
|
state = upload.uppy.getState()
|
||||||
const files = upload.uppy.getFiles()
|
const files = upload.uppy.getFiles()
|
||||||
@ -84,18 +77,18 @@
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
upload.uppy.on('complete', handleComplete)
|
upload.uppy.on('complete', handleComplete)
|
||||||
upload.uppy.on('upload-error', handleUploadError)
|
|
||||||
upload.uppy.on('upload-progress', handleUploadProgress)
|
|
||||||
upload.uppy.on('upload-success', handleUploadSuccess)
|
|
||||||
upload.uppy.on('file-removed', handleFileRemoved)
|
upload.uppy.on('file-removed', handleFileRemoved)
|
||||||
|
upload.uppy.on('upload-error', updateState)
|
||||||
|
upload.uppy.on('upload-progress', updateState)
|
||||||
|
upload.uppy.on('upload-success', updateState)
|
||||||
})
|
})
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
upload.uppy.off('complete', handleComplete)
|
upload.uppy.off('complete', handleComplete)
|
||||||
upload.uppy.off('upload-error', handleUploadError)
|
|
||||||
upload.uppy.off('upload-progress', handleUploadProgress)
|
|
||||||
upload.uppy.off('upload-success', handleUploadSuccess)
|
|
||||||
upload.uppy.off('file-removed', handleFileRemoved)
|
upload.uppy.off('file-removed', handleFileRemoved)
|
||||||
|
upload.uppy.off('upload-error', updateState)
|
||||||
|
upload.uppy.off('upload-progress', updateState)
|
||||||
|
upload.uppy.off('upload-success', updateState)
|
||||||
})
|
})
|
||||||
|
|
||||||
function getFileError (file: UppyFile<any, any>): string | undefined {
|
function getFileError (file: UppyFile<any, any>): string | undefined {
|
||||||
|
@ -1495,59 +1495,80 @@ export const permissionsStore = writable<PermissionsStore>({
|
|||||||
ap: {},
|
ap: {},
|
||||||
whitelist: new Set()
|
whitelist: new Set()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const spaceTypesQuery = createQuery(true)
|
||||||
const permissionsQuery = createQuery(true)
|
const permissionsQuery = createQuery(true)
|
||||||
|
type TargetClassesProjection = Record<Ref<Class<Space>>, number>
|
||||||
|
|
||||||
permissionsQuery.query(core.class.Space, {}, (res) => {
|
spaceTypesQuery.query(core.class.SpaceType, {}, (types) => {
|
||||||
const whitelistedSpaces = new Set<Ref<Space>>()
|
const targetClasses = types.reduce<TargetClassesProjection>((acc, st) => {
|
||||||
const permissionsBySpace: PermissionsBySpace = {}
|
acc[st.targetClass] = 1
|
||||||
const accountsByPermission: AccountsByPermission = {}
|
return acc
|
||||||
const client = getClient()
|
}, {})
|
||||||
const hierarchy = client.getHierarchy()
|
|
||||||
const me = getCurrentAccount()
|
|
||||||
|
|
||||||
for (const s of res) {
|
permissionsQuery.query(
|
||||||
if (hierarchy.isDerived(s._class, core.class.TypedSpace)) {
|
core.class.Space,
|
||||||
const type = client.getModel().findAllSync(core.class.SpaceType, { _id: (s as TypedSpace).type })[0]
|
{},
|
||||||
const mixin = type?.targetClass
|
(res) => {
|
||||||
|
const whitelistedSpaces = new Set<Ref<Space>>()
|
||||||
|
const permissionsBySpace: PermissionsBySpace = {}
|
||||||
|
const accountsByPermission: AccountsByPermission = {}
|
||||||
|
const client = getClient()
|
||||||
|
const hierarchy = client.getHierarchy()
|
||||||
|
const me = getCurrentAccount()
|
||||||
|
|
||||||
if (mixin === undefined) {
|
for (const s of res) {
|
||||||
permissionsBySpace[s._id] = new Set()
|
if (hierarchy.isDerived(s._class, core.class.TypedSpace)) {
|
||||||
accountsByPermission[s._id] = {}
|
const type = client.getModel().findAllSync(core.class.SpaceType, { _id: (s as TypedSpace).type })[0]
|
||||||
continue
|
const mixin = type?.targetClass
|
||||||
}
|
|
||||||
|
|
||||||
const asMixin = hierarchy.as(s, mixin)
|
if (mixin === undefined) {
|
||||||
const roles = client.getModel().findAllSync(core.class.Role, { attachedTo: type._id })
|
permissionsBySpace[s._id] = new Set()
|
||||||
const myRoles = roles.filter((r) => ((asMixin as any)[r._id] ?? []).includes(me._id))
|
accountsByPermission[s._id] = {}
|
||||||
permissionsBySpace[s._id] = new Set(myRoles.flatMap((r) => r.permissions))
|
continue
|
||||||
|
|
||||||
accountsByPermission[s._id] = {}
|
|
||||||
|
|
||||||
for (const role of roles) {
|
|
||||||
const assignment: Array<Ref<Account>> = (asMixin as any)[role._id] ?? []
|
|
||||||
|
|
||||||
if (assignment.length === 0) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const permissionId of role.permissions) {
|
|
||||||
if (accountsByPermission[s._id][permissionId] === undefined) {
|
|
||||||
accountsByPermission[s._id][permissionId] = new Set()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assignment.forEach((acc) => accountsByPermission[s._id][permissionId].add(acc))
|
const asMixin = hierarchy.as(s, mixin)
|
||||||
|
const roles = client.getModel().findAllSync(core.class.Role, { attachedTo: type._id })
|
||||||
|
const myRoles = roles.filter((r) => ((asMixin as any)[r._id] ?? []).includes(me._id))
|
||||||
|
permissionsBySpace[s._id] = new Set(myRoles.flatMap((r) => r.permissions))
|
||||||
|
|
||||||
|
accountsByPermission[s._id] = {}
|
||||||
|
|
||||||
|
for (const role of roles) {
|
||||||
|
const assignment: Array<Ref<Account>> = (asMixin as any)[role._id] ?? []
|
||||||
|
|
||||||
|
if (assignment.length === 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const permissionId of role.permissions) {
|
||||||
|
if (accountsByPermission[s._id][permissionId] === undefined) {
|
||||||
|
accountsByPermission[s._id][permissionId] = new Set()
|
||||||
|
}
|
||||||
|
|
||||||
|
assignment.forEach((acc) => accountsByPermission[s._id][permissionId].add(acc))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
whitelistedSpaces.add(s._id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
whitelistedSpaces.add(s._id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
permissionsStore.set({
|
permissionsStore.set({
|
||||||
ps: permissionsBySpace,
|
ps: permissionsBySpace,
|
||||||
ap: accountsByPermission,
|
ap: accountsByPermission,
|
||||||
whitelist: whitelistedSpaces
|
whitelist: whitelistedSpaces
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
projection: {
|
||||||
|
_id: 1,
|
||||||
|
type: 1,
|
||||||
|
...targetClasses
|
||||||
|
} as any
|
||||||
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export function getCollaborationUser (): CollaborationUser {
|
export function getCollaborationUser (): CollaborationUser {
|
||||||
|
@ -1516,58 +1516,6 @@ export async function removeDocInboxNotifications (_id: Ref<ActivityMessage>, co
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function OnActivityNotificationViewed (
|
|
||||||
tx: TxUpdateDoc<InboxNotification>,
|
|
||||||
control: TriggerControl
|
|
||||||
): Promise<Tx[]> {
|
|
||||||
if (tx.objectClass !== notification.class.ActivityInboxNotification || tx.operations.isViewed !== true) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const inboxNotification = (
|
|
||||||
await control.findAll(
|
|
||||||
notification.class.ActivityInboxNotification,
|
|
||||||
{
|
|
||||||
_id: tx.objectId as Ref<ActivityInboxNotification>
|
|
||||||
},
|
|
||||||
{ projection: { _id: 1, attachedTo: 1, user: 1 } }
|
|
||||||
)
|
|
||||||
)[0]
|
|
||||||
|
|
||||||
if (inboxNotification === undefined) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read reactions notifications when message is read
|
|
||||||
const { attachedTo, user } = inboxNotification
|
|
||||||
|
|
||||||
const reactionMessages = await control.findAll(
|
|
||||||
activity.class.DocUpdateMessage,
|
|
||||||
{
|
|
||||||
attachedTo,
|
|
||||||
objectClass: activity.class.Reaction
|
|
||||||
},
|
|
||||||
{ projection: { _id: 1 } }
|
|
||||||
)
|
|
||||||
|
|
||||||
if (reactionMessages.length === 0) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const reactionNotifications = await control.findAll(
|
|
||||||
notification.class.ActivityInboxNotification,
|
|
||||||
{
|
|
||||||
attachedTo: { $in: reactionMessages.map(({ _id }) => _id) },
|
|
||||||
user
|
|
||||||
},
|
|
||||||
{ projection: { _id: 1, _class: 1, space: 1 } }
|
|
||||||
)
|
|
||||||
|
|
||||||
return reactionNotifications.map(({ _id, _class, space }) =>
|
|
||||||
control.txFactory.createTxUpdateDoc(_class, space, _id, { isViewed: true })
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getCollaborators (
|
export async function getCollaborators (
|
||||||
doc: Doc,
|
doc: Doc,
|
||||||
control: TriggerControl,
|
control: TriggerControl,
|
||||||
@ -1659,7 +1607,6 @@ export default async () => ({
|
|||||||
trigger: {
|
trigger: {
|
||||||
OnAttributeCreate,
|
OnAttributeCreate,
|
||||||
OnAttributeUpdate,
|
OnAttributeUpdate,
|
||||||
OnActivityNotificationViewed,
|
|
||||||
OnDocRemove
|
OnDocRemove
|
||||||
},
|
},
|
||||||
function: {
|
function: {
|
||||||
|
@ -180,7 +180,6 @@ export default plugin(serverNotificationId, {
|
|||||||
OnAttributeCreate: '' as Resource<TriggerFunc>,
|
OnAttributeCreate: '' as Resource<TriggerFunc>,
|
||||||
OnAttributeUpdate: '' as Resource<TriggerFunc>,
|
OnAttributeUpdate: '' as Resource<TriggerFunc>,
|
||||||
OnReactionChanged: '' as Resource<TriggerFunc>,
|
OnReactionChanged: '' as Resource<TriggerFunc>,
|
||||||
OnActivityNotificationViewed: '' as Resource<TriggerFunc>,
|
|
||||||
OnDocRemove: '' as Resource<TriggerFunc>
|
OnDocRemove: '' as Resource<TriggerFunc>
|
||||||
},
|
},
|
||||||
function: {
|
function: {
|
||||||
|
@ -23,7 +23,6 @@ import {
|
|||||||
type FindOptions,
|
type FindOptions,
|
||||||
type FindResult,
|
type FindResult,
|
||||||
type Hierarchy,
|
type Hierarchy,
|
||||||
type IndexingConfiguration,
|
|
||||||
type MeasureContext,
|
type MeasureContext,
|
||||||
type ModelDb,
|
type ModelDb,
|
||||||
type Ref,
|
type Ref,
|
||||||
@ -38,19 +37,23 @@ import type { ServerFindOptions } from './types'
|
|||||||
export interface DomainHelperOperations {
|
export interface DomainHelperOperations {
|
||||||
create: (domain: Domain) => Promise<void>
|
create: (domain: Domain) => Promise<void>
|
||||||
exists: (domain: Domain) => boolean
|
exists: (domain: Domain) => boolean
|
||||||
|
|
||||||
|
listDomains: () => Promise<Set<Domain>>
|
||||||
createIndex: (domain: Domain, value: string | FieldIndexConfig<Doc>, options?: { name: string }) => Promise<void>
|
createIndex: (domain: Domain, value: string | FieldIndexConfig<Doc>, options?: { name: string }) => Promise<void>
|
||||||
dropIndex: (domain: Domain, name: string) => Promise<void>
|
dropIndex: (domain: Domain, name: string) => Promise<void>
|
||||||
listIndexes: (domain: Domain) => Promise<{ name: string }[]>
|
listIndexes: (domain: Domain) => Promise<{ name: string }[]>
|
||||||
hasDocuments: (domain: Domain, count: number) => Promise<boolean>
|
|
||||||
|
// Could return 0 even if it has documents
|
||||||
|
estimatedCount: (domain: Domain) => Promise<number>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DomainHelper {
|
export interface DomainHelper {
|
||||||
checkDomain: (
|
checkDomain: (
|
||||||
ctx: MeasureContext,
|
ctx: MeasureContext,
|
||||||
domain: Domain,
|
domain: Domain,
|
||||||
forceCreate: boolean,
|
documents: number,
|
||||||
operations: DomainHelperOperations
|
operations: DomainHelperOperations
|
||||||
) => Promise<boolean>
|
) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RawDBAdapterStream<T extends Doc> {
|
export interface RawDBAdapterStream<T extends Doc> {
|
||||||
@ -87,15 +90,20 @@ export interface RawDBAdapter {
|
|||||||
close: () => Promise<void>
|
close: () => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DbAdapterHandler = (
|
||||||
|
domain: Domain,
|
||||||
|
event: 'add' | 'update' | 'delete' | 'read',
|
||||||
|
count: number,
|
||||||
|
time: number,
|
||||||
|
helper: DomainHelperOperations
|
||||||
|
) => void
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export interface DbAdapter {
|
export interface DbAdapter {
|
||||||
init?: () => Promise<void>
|
init?: () => Promise<void>
|
||||||
|
|
||||||
helper?: () => DomainHelperOperations
|
helper: () => DomainHelperOperations
|
||||||
createIndexes: (domain: Domain, config: Pick<IndexingConfiguration<Doc>, 'indexes'>) => Promise<void>
|
|
||||||
removeOldIndex: (domain: Domain, deletePattern: RegExp[], keepPattern: RegExp[]) => Promise<void>
|
|
||||||
|
|
||||||
close: () => Promise<void>
|
close: () => Promise<void>
|
||||||
findAll: <T extends Doc>(
|
findAll: <T extends Doc>(
|
||||||
@ -116,6 +124,9 @@ export interface DbAdapter {
|
|||||||
|
|
||||||
// Bulk update operations
|
// Bulk update operations
|
||||||
update: (ctx: MeasureContext, domain: Domain, operations: Map<Ref<Doc>, DocumentUpdate<Doc>>) => Promise<void>
|
update: (ctx: MeasureContext, domain: Domain, operations: Map<Ref<Doc>, DocumentUpdate<Doc>>) => Promise<void>
|
||||||
|
|
||||||
|
// Allow to register a handler to listen for domain operations
|
||||||
|
on?: (handler: DbAdapterHandler) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -349,23 +349,36 @@ export class FullTextIndexPipeline implements FullTextPipeline {
|
|||||||
keepPattern.push(new RegExp(st.stageId))
|
keepPattern.push(new RegExp(st.stageId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const helper = this.storage.helper()
|
||||||
if (deletePattern.length > 0) {
|
if (deletePattern.length > 0) {
|
||||||
await this.storage.removeOldIndex(DOMAIN_DOC_INDEX_STATE, deletePattern, keepPattern)
|
try {
|
||||||
|
const existingIndexes = await helper.listIndexes(DOMAIN_DOC_INDEX_STATE)
|
||||||
|
for (const existingIndex of existingIndexes) {
|
||||||
|
if (existingIndex.name !== undefined) {
|
||||||
|
const name: string = existingIndex.name
|
||||||
|
if (deletePattern.some((it) => it.test(name)) && !keepPattern.some((it) => it.test(name))) {
|
||||||
|
await helper.dropIndex(DOMAIN_DOC_INDEX_STATE, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const st of this.stages) {
|
for (const st of this.stages) {
|
||||||
if (this.cancelling) {
|
if (this.cancelling) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await this.storage.createIndexes(DOMAIN_DOC_INDEX_STATE, {
|
await this.storage.helper().createIndex(
|
||||||
indexes: [
|
DOMAIN_DOC_INDEX_STATE,
|
||||||
{
|
{
|
||||||
keys: {
|
keys: {
|
||||||
['stages.' + st.stageId]: 1
|
['stages.' + st.stageId]: 1
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
},
|
||||||
})
|
{ name: 'stages.' + st.stageId + '_1' }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -481,7 +494,9 @@ export class FullTextIndexPipeline implements FullTextPipeline {
|
|||||||
async (ctx) =>
|
async (ctx) =>
|
||||||
await this.storage.findAll(ctx, core.class.DocIndexState, q, {
|
await this.storage.findAll(ctx, core.class.DocIndexState, q, {
|
||||||
sort: { modifiedOn: SortingOrder.Descending },
|
sort: { modifiedOn: SortingOrder.Descending },
|
||||||
limit: globalIndexer.processingSize
|
limit: globalIndexer.processingSize,
|
||||||
|
skipClass: true,
|
||||||
|
skipSpace: true
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
const toRemove: DocIndexState[] = []
|
const toRemove: DocIndexState[] = []
|
||||||
@ -594,7 +609,9 @@ export class FullTextIndexPipeline implements FullTextPipeline {
|
|||||||
_id: 1,
|
_id: 1,
|
||||||
stages: 1,
|
stages: 1,
|
||||||
objectClass: 1
|
objectClass: 1
|
||||||
}
|
},
|
||||||
|
skipSpace: true,
|
||||||
|
skipClass: true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ import core, {
|
|||||||
type TxResult,
|
type TxResult,
|
||||||
type WorkspaceId
|
type WorkspaceId
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import { type DbAdapter } from './adapter'
|
import { type DbAdapter, type DomainHelperOperations } from './adapter'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
@ -49,6 +49,18 @@ export class DummyDbAdapter implements DbAdapter {
|
|||||||
|
|
||||||
async init (): Promise<void> {}
|
async init (): Promise<void> {}
|
||||||
|
|
||||||
|
helper (): DomainHelperOperations {
|
||||||
|
return {
|
||||||
|
create: async () => {},
|
||||||
|
exists: () => true,
|
||||||
|
listDomains: async () => new Set(),
|
||||||
|
createIndex: async () => {},
|
||||||
|
dropIndex: async () => {},
|
||||||
|
listIndexes: async () => [],
|
||||||
|
estimatedCount: async () => 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async createIndexes (domain: Domain, config: Pick<IndexingConfiguration<Doc>, 'indexes'>): Promise<void> {}
|
async createIndexes (domain: Domain, config: Pick<IndexingConfiguration<Doc>, 'indexes'>): Promise<void> {}
|
||||||
async removeOldIndex (domain: Domain, deletePattern: RegExp[], keepPattern: RegExp[]): Promise<void> {}
|
async removeOldIndex (domain: Domain, deletePattern: RegExp[], keepPattern: RegExp[]): Promise<void> {}
|
||||||
|
|
||||||
|
@ -74,41 +74,24 @@ export class DomainIndexHelperImpl implements DomainHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* return false if and only if domain underline structures are not required.
|
* Check if some indexes need to be created for domain.
|
||||||
*/
|
*/
|
||||||
async checkDomain (
|
async checkDomain (
|
||||||
ctx: MeasureContext,
|
ctx: MeasureContext,
|
||||||
domain: Domain,
|
domain: Domain,
|
||||||
forceCreate: boolean,
|
documents: number,
|
||||||
operations: DomainHelperOperations
|
operations: DomainHelperOperations
|
||||||
): Promise<boolean> {
|
): Promise<void> {
|
||||||
const domainInfo = this.domains.get(domain)
|
const domainInfo = this.domains.get(domain)
|
||||||
const cfg = this.domainConfigurations.find((it) => it.domain === domain)
|
const cfg = this.domainConfigurations.find((it) => it.domain === domain)
|
||||||
|
|
||||||
let exists = operations.exists(domain)
|
|
||||||
const hasDocuments = exists && (await operations.hasDocuments(domain, 1))
|
|
||||||
// Drop collection if it exists and should not exists or doesn't have documents.
|
|
||||||
if (exists && (cfg?.disableCollection === true || (!hasDocuments && !forceCreate))) {
|
|
||||||
// We do not need this collection
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (forceCreate && !exists) {
|
|
||||||
await operations.create(domain)
|
|
||||||
ctx.info('collection will be created', domain)
|
|
||||||
exists = true
|
|
||||||
}
|
|
||||||
if (!exists) {
|
|
||||||
// Do not need to create, since not force and no documents.
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const bb: (string | FieldIndexConfig<Doc>)[] = []
|
const bb: (string | FieldIndexConfig<Doc>)[] = []
|
||||||
const added = new Set<string>()
|
const added = new Set<string>()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const has50Documents = await operations.hasDocuments(domain, 50)
|
const has50Documents = documents > 50
|
||||||
const allIndexes = (await operations.listIndexes(domain)).filter((it) => it.name !== '_id_')
|
const allIndexes = (await operations.listIndexes(domain)).filter((it) => it.name !== '_id_')
|
||||||
ctx.info('check indexes', { domain, has50Documents })
|
ctx.info('check indexes', { domain, has50Documents, documents })
|
||||||
if (has50Documents) {
|
if (has50Documents) {
|
||||||
for (const vv of [...(domainInfo?.values() ?? []), ...(cfg?.indexes ?? [])]) {
|
for (const vv of [...(domainInfo?.values() ?? []), ...(cfg?.indexes ?? [])]) {
|
||||||
try {
|
try {
|
||||||
@ -188,7 +171,5 @@ export class DomainIndexHelperImpl implements DomainHelper {
|
|||||||
if (bb.length > 0) {
|
if (bb.length > 0) {
|
||||||
ctx.info('created indexes', { domain, bb })
|
ctx.info('created indexes', { domain, bb })
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -167,7 +167,7 @@ export async function createServerStorage (
|
|||||||
|
|
||||||
const domainHelper = new DomainIndexHelperImpl(metrics, hierarchy, modelDb, conf.workspace)
|
const domainHelper = new DomainIndexHelperImpl(metrics, hierarchy, modelDb, conf.workspace)
|
||||||
|
|
||||||
return new TServerStorage(
|
const serverStorage = new TServerStorage(
|
||||||
conf.domains,
|
conf.domains,
|
||||||
conf.defaultAdapter,
|
conf.defaultAdapter,
|
||||||
adapters,
|
adapters,
|
||||||
@ -184,6 +184,10 @@ export async function createServerStorage (
|
|||||||
model,
|
model,
|
||||||
domainHelper
|
domainHelper
|
||||||
)
|
)
|
||||||
|
await ctx.with('init-domain-info', {}, async () => {
|
||||||
|
await serverStorage.initDomainInfo()
|
||||||
|
})
|
||||||
|
return serverStorage
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -79,6 +79,11 @@ import type {
|
|||||||
} from '../types'
|
} from '../types'
|
||||||
import { SessionContextImpl, createBroadcastEvent } from '../utils'
|
import { SessionContextImpl, createBroadcastEvent } from '../utils'
|
||||||
|
|
||||||
|
interface DomainInfo {
|
||||||
|
exists: boolean
|
||||||
|
documents: number
|
||||||
|
}
|
||||||
|
|
||||||
export class TServerStorage implements ServerStorage {
|
export class TServerStorage implements ServerStorage {
|
||||||
private readonly fulltext: FullTextIndex
|
private readonly fulltext: FullTextIndex
|
||||||
hierarchy: Hierarchy
|
hierarchy: Hierarchy
|
||||||
@ -92,14 +97,8 @@ export class TServerStorage implements ServerStorage {
|
|||||||
liveQuery: LQ
|
liveQuery: LQ
|
||||||
branding: Branding | null
|
branding: Branding | null
|
||||||
|
|
||||||
domainInfo = new Map<
|
domainInfo = new Map<Domain, DomainInfo>()
|
||||||
Domain,
|
statsCtx: MeasureContext
|
||||||
{
|
|
||||||
exists: boolean
|
|
||||||
checkPromise: Promise<boolean> | undefined
|
|
||||||
lastCheck: number
|
|
||||||
}
|
|
||||||
>()
|
|
||||||
|
|
||||||
emptyAdapter = new DummyDbAdapter()
|
emptyAdapter = new DummyDbAdapter()
|
||||||
|
|
||||||
@ -126,6 +125,71 @@ export class TServerStorage implements ServerStorage {
|
|||||||
this.branding = options.branding
|
this.branding = options.branding
|
||||||
|
|
||||||
this.setModel(model)
|
this.setModel(model)
|
||||||
|
this.statsCtx = metrics.newChild('stats-' + this.workspaceId.name, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
async initDomainInfo (): Promise<void> {
|
||||||
|
const adapterDomains = new Map<DbAdapter, Set<Domain>>()
|
||||||
|
for (const d of this.hierarchy.domains()) {
|
||||||
|
// We need to init domain info
|
||||||
|
const info = this.getDomainInfo(d)
|
||||||
|
const adapter = this.adapters.get(d) ?? this.adapters.get(this.defaultAdapter)
|
||||||
|
if (adapter !== undefined) {
|
||||||
|
const h = adapter.helper?.()
|
||||||
|
if (h !== undefined) {
|
||||||
|
const dbDomains = adapterDomains.get(adapter) ?? (await h.listDomains())
|
||||||
|
adapterDomains.set(adapter, dbDomains)
|
||||||
|
const dbIdIndex = dbDomains.has(d)
|
||||||
|
info.exists = dbIdIndex !== undefined
|
||||||
|
if (info.exists) {
|
||||||
|
info.documents = await h.estimatedCount(d)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
info.exists = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
info.exists = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const adapter of this.adapters.values()) {
|
||||||
|
adapter.on?.((domain, event, count, time, helper) => {
|
||||||
|
const info = this.getDomainInfo(domain)
|
||||||
|
const oldDocuments = info.documents
|
||||||
|
switch (event) {
|
||||||
|
case 'add':
|
||||||
|
info.documents += count
|
||||||
|
break
|
||||||
|
case 'update':
|
||||||
|
break
|
||||||
|
case 'delete':
|
||||||
|
info.documents -= count
|
||||||
|
break
|
||||||
|
case 'read':
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldDocuments < 50 && info.documents > 50) {
|
||||||
|
// We have more 50 documents, we need to check for indexes
|
||||||
|
void this.domainHelper.checkDomain(this.metrics, domain, info.documents, helper)
|
||||||
|
}
|
||||||
|
if (oldDocuments > 50 && info.documents < 50) {
|
||||||
|
// We have more 50 documents, we need to check for indexes
|
||||||
|
void this.domainHelper.checkDomain(this.metrics, domain, info.documents, helper)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDomainInfo (domain: Domain): DomainInfo {
|
||||||
|
let info = this.domainInfo.get(domain)
|
||||||
|
if (info === undefined) {
|
||||||
|
info = {
|
||||||
|
documents: -1,
|
||||||
|
exists: false
|
||||||
|
}
|
||||||
|
this.domainInfo.set(domain, info)
|
||||||
|
}
|
||||||
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
private newCastClient (hierarchy: Hierarchy, modelDb: ModelDb, metrics: MeasureContext): Client {
|
private newCastClient (hierarchy: Hierarchy, modelDb: ModelDb, metrics: MeasureContext): Client {
|
||||||
@ -172,13 +236,6 @@ export class TServerStorage implements ServerStorage {
|
|||||||
|
|
||||||
async close (): Promise<void> {
|
async close (): Promise<void> {
|
||||||
await this.fulltext.close()
|
await this.fulltext.close()
|
||||||
for (const [domain, info] of this.domainInfo.entries()) {
|
|
||||||
if (info.checkPromise !== undefined) {
|
|
||||||
this.metrics.info('wait for check domain', { domain })
|
|
||||||
// We need to be sure we wait for check to be complete
|
|
||||||
await info.checkPromise
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const o of this.adapters.values()) {
|
for (const o of this.adapters.values()) {
|
||||||
await o.close()
|
await o.close()
|
||||||
}
|
}
|
||||||
@ -193,36 +250,13 @@ export class TServerStorage implements ServerStorage {
|
|||||||
throw new Error('adapter not provided: ' + name)
|
throw new Error('adapter not provided: ' + name)
|
||||||
}
|
}
|
||||||
|
|
||||||
const helper = adapter.helper?.()
|
const info = this.getDomainInfo(domain)
|
||||||
if (helper !== undefined) {
|
|
||||||
let info = this.domainInfo.get(domain)
|
if (!info.exists && !requireExists) {
|
||||||
if (info == null) {
|
return this.emptyAdapter
|
||||||
// For first time, lets assume all is fine
|
|
||||||
info = {
|
|
||||||
exists: true,
|
|
||||||
lastCheck: Date.now(),
|
|
||||||
checkPromise: undefined
|
|
||||||
}
|
|
||||||
this.domainInfo.set(domain, info)
|
|
||||||
return adapter
|
|
||||||
}
|
|
||||||
if (Date.now() - info.lastCheck > 5 * 60 * 1000) {
|
|
||||||
// Re-check every 5 minutes
|
|
||||||
const exists = helper.exists(domain)
|
|
||||||
// We will create necessary indexes if required, and not touch collection if not required.
|
|
||||||
info = {
|
|
||||||
exists,
|
|
||||||
lastCheck: Date.now(),
|
|
||||||
checkPromise: this.domainHelper.checkDomain(this.metrics, domain, requireExists, helper)
|
|
||||||
}
|
|
||||||
this.domainInfo.set(domain, info)
|
|
||||||
}
|
|
||||||
if (!info.exists && !requireExists) {
|
|
||||||
return this.emptyAdapter
|
|
||||||
}
|
|
||||||
// If we require it exists, it will be exists
|
|
||||||
info.exists = true
|
|
||||||
}
|
}
|
||||||
|
// If we require it exists, it will be exists
|
||||||
|
info.exists = true
|
||||||
|
|
||||||
return adapter
|
return adapter
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,9 @@ import { type StorageAdapter } from './storage'
|
|||||||
export interface ServerFindOptions<T extends Doc> extends FindOptions<T> {
|
export interface ServerFindOptions<T extends Doc> extends FindOptions<T> {
|
||||||
domain?: Domain // Allow to find for Doc's in specified domain only.
|
domain?: Domain // Allow to find for Doc's in specified domain only.
|
||||||
prefix?: string
|
prefix?: string
|
||||||
|
|
||||||
|
skipClass?: boolean
|
||||||
|
skipSpace?: boolean
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
|
@ -33,7 +33,7 @@ import {
|
|||||||
WorkspaceId
|
WorkspaceId
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import { getMetadata } from '@hcengineering/platform'
|
import { getMetadata } from '@hcengineering/platform'
|
||||||
import serverCore, { DbAdapter } from '@hcengineering/server-core'
|
import serverCore, { DbAdapter, type DomainHelperOperations } from '@hcengineering/server-core'
|
||||||
|
|
||||||
function getIndexName (): string {
|
function getIndexName (): string {
|
||||||
return getMetadata(serverCore.metadata.ElasticIndexName) ?? 'storage_index'
|
return getMetadata(serverCore.metadata.ElasticIndexName) ?? 'storage_index'
|
||||||
@ -61,7 +61,24 @@ class ElasticDataAdapter implements DbAdapter {
|
|||||||
this.getDocId = (fulltext) => fulltext.slice(0, -1 * (this.workspaceString.length + 1)) as Ref<Doc>
|
this.getDocId = (fulltext) => fulltext.slice(0, -1 * (this.workspaceString.length + 1)) as Ref<Doc>
|
||||||
}
|
}
|
||||||
|
|
||||||
async groupBy<T>(ctx: MeasureContext, domain: Domain, field: string): Promise<Set<T>> {
|
helper (): DomainHelperOperations {
|
||||||
|
return {
|
||||||
|
create: async () => {},
|
||||||
|
exists: () => true,
|
||||||
|
listDomains: async () => new Set(),
|
||||||
|
createIndex: async () => {},
|
||||||
|
dropIndex: async () => {},
|
||||||
|
listIndexes: async () => [],
|
||||||
|
estimatedCount: async () => 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async groupBy<T, D extends Doc = Doc>(
|
||||||
|
ctx: MeasureContext,
|
||||||
|
domain: Domain,
|
||||||
|
field: string,
|
||||||
|
query?: DocumentQuery<D>
|
||||||
|
): Promise<Set<T>> {
|
||||||
return new Set()
|
return new Set()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,7 +109,7 @@ export function startFront (ctx: MeasureContext, extraConfig?: Record<string, st
|
|||||||
|
|
||||||
if (previewConfig === undefined) {
|
if (previewConfig === undefined) {
|
||||||
// Use universal preview config
|
// Use universal preview config
|
||||||
previewConfig = `*|${uploadUrl}/:workspace?file=:blobId&size=:size`
|
previewConfig = `${uploadUrl}/:workspace?file=:blobId&size=:size`
|
||||||
}
|
}
|
||||||
|
|
||||||
const pushPublicKey = process.env.PUSH_PUBLIC_KEY
|
const pushPublicKey = process.env.PUSH_PUBLIC_KEY
|
||||||
|
@ -325,43 +325,6 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getTxTargets (ctx: SessionContext, tx: Tx): Promise<string[] | undefined> {
|
|
||||||
const h = this.storage.hierarchy
|
|
||||||
let targets: string[] | undefined
|
|
||||||
|
|
||||||
if (TxProcessor.isExtendsCUD(tx._class)) {
|
|
||||||
const account = await getUser(this.storage, ctx)
|
|
||||||
if (tx.objectSpace === (account._id as string)) {
|
|
||||||
targets = [account.email, systemAccountEmail]
|
|
||||||
} else if ([...this.systemSpaces, ...this.mainSpaces].includes(tx.objectSpace)) {
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
const cudTx = tx as TxCUD<Doc>
|
|
||||||
const isSpace = h.isDerived(cudTx.objectClass, core.class.Space)
|
|
||||||
|
|
||||||
if (isSpace) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const space = this.spacesMap.get(tx.objectSpace)
|
|
||||||
|
|
||||||
if (space !== undefined) {
|
|
||||||
targets = await this.getTargets(space.members)
|
|
||||||
if (!isOwner(account, ctx)) {
|
|
||||||
const allowed = this.getAllAllowedSpaces(account, true)
|
|
||||||
if (allowed === undefined || !allowed.includes(tx.objectSpace)) {
|
|
||||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
|
||||||
}
|
|
||||||
} else if (!targets.includes(account.email)) {
|
|
||||||
targets.push(account.email)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return targets
|
|
||||||
}
|
|
||||||
|
|
||||||
private async processTxSpaceDomain (tx: TxCUD<Doc>): Promise<void> {
|
private async processTxSpaceDomain (tx: TxCUD<Doc>): Promise<void> {
|
||||||
const actualTx = TxProcessor.extractTx(tx)
|
const actualTx = TxProcessor.extractTx(tx)
|
||||||
if (actualTx._class === core.class.TxCreateDoc) {
|
if (actualTx._class === core.class.TxCreateDoc) {
|
||||||
@ -450,15 +413,10 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
|
|||||||
return isData ? res : [...res, ...this.publicSpaces]
|
return isData ? res : [...res, ...this.publicSpaces]
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadDomainSpaces (ctx: MeasureContext, domain: Domain): Promise<Set<Ref<Space>>> {
|
|
||||||
const field = this.getKey(domain)
|
|
||||||
return await this.storage.groupBy<Ref<Space>>(ctx, domain, field)
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDomainSpaces (domain: Domain): Promise<Set<Ref<Space>>> {
|
async getDomainSpaces (domain: Domain): Promise<Set<Ref<Space>>> {
|
||||||
let domainSpaces = this._domainSpaces.get(domain)
|
let domainSpaces = this._domainSpaces.get(domain)
|
||||||
if (domainSpaces === undefined) {
|
if (domainSpaces === undefined) {
|
||||||
const p = this.loadDomainSpaces(this.spaceMeasureCtx, domain)
|
const p = this.storage.groupBy<Ref<Space>>(this.spaceMeasureCtx, domain, this.getKey(domain))
|
||||||
this._domainSpaces.set(domain, p)
|
this._domainSpaces.set(domain, p)
|
||||||
domainSpaces = await p
|
domainSpaces = await p
|
||||||
this._domainSpaces.set(domain, domainSpaces)
|
this._domainSpaces.set(domain, domainSpaces)
|
||||||
@ -466,9 +424,17 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
|
|||||||
return domainSpaces instanceof Promise ? await domainSpaces : domainSpaces
|
return domainSpaces instanceof Promise ? await domainSpaces : domainSpaces
|
||||||
}
|
}
|
||||||
|
|
||||||
private async filterByDomain (domain: Domain, spaces: Ref<Space>[]): Promise<Ref<Space>[]> {
|
private async filterByDomain (
|
||||||
|
domain: Domain,
|
||||||
|
spaces: Ref<Space>[]
|
||||||
|
): Promise<{ result: Ref<Space>[], allDomainSpaces: boolean, domainSpaces: Set<Ref<Space>> }> {
|
||||||
const domainSpaces = await this.getDomainSpaces(domain)
|
const domainSpaces = await this.getDomainSpaces(domain)
|
||||||
return spaces.filter((p) => domainSpaces.has(p))
|
const result = spaces.filter((p) => domainSpaces.has(p))
|
||||||
|
return {
|
||||||
|
result: spaces.filter((p) => domainSpaces.has(p)),
|
||||||
|
allDomainSpaces: result.length === domainSpaces.size,
|
||||||
|
domainSpaces
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async mergeQuery<T extends Doc>(
|
private async mergeQuery<T extends Doc>(
|
||||||
@ -476,19 +442,33 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
|
|||||||
query: ObjQueryType<T['space']>,
|
query: ObjQueryType<T['space']>,
|
||||||
domain: Domain,
|
domain: Domain,
|
||||||
isSpace: boolean
|
isSpace: boolean
|
||||||
): Promise<ObjQueryType<T['space']>> {
|
): Promise<ObjQueryType<T['space']> | undefined> {
|
||||||
const spaces = await this.filterByDomain(domain, this.getAllAllowedSpaces(account, !isSpace))
|
const spaces = await this.filterByDomain(domain, this.getAllAllowedSpaces(account, !isSpace))
|
||||||
if (query == null) {
|
if (query == null) {
|
||||||
return { $in: spaces }
|
if (spaces.allDomainSpaces) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return { $in: spaces.result }
|
||||||
}
|
}
|
||||||
if (typeof query === 'string') {
|
if (typeof query === 'string') {
|
||||||
if (!spaces.includes(query)) {
|
if (!spaces.result.includes(query)) {
|
||||||
return { $in: [] }
|
return { $in: [] }
|
||||||
}
|
}
|
||||||
} else if (query.$in != null) {
|
} else if (query.$in != null) {
|
||||||
query.$in = query.$in.filter((p) => spaces.includes(p))
|
query.$in = query.$in.filter((p) => spaces.result.includes(p))
|
||||||
|
if (query.$in.length === spaces.domainSpaces.size) {
|
||||||
|
// all domain spaces
|
||||||
|
delete query.$in
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
query.$in = spaces
|
if (spaces.allDomainSpaces) {
|
||||||
|
delete query.$in
|
||||||
|
} else {
|
||||||
|
query.$in = spaces.result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Object.keys(query).length === 0) {
|
||||||
|
return undefined
|
||||||
}
|
}
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
@ -515,22 +495,31 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
|
|||||||
if (!isOwner(account, ctx) || !isSpace) {
|
if (!isOwner(account, ctx) || !isSpace) {
|
||||||
if (query[field] !== undefined) {
|
if (query[field] !== undefined) {
|
||||||
const res = await this.mergeQuery(account, query[field], domain, isSpace)
|
const res = await this.mergeQuery(account, query[field], domain, isSpace)
|
||||||
;(newQuery as any)[field] = res
|
if (res === undefined) {
|
||||||
if (typeof res === 'object') {
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||||
if (Array.isArray(res.$in) && res.$in.length === 1 && Object.keys(res).length === 1) {
|
delete (query as any)[field]
|
||||||
;(newQuery as any)[field] = res.$in[0]
|
} else {
|
||||||
|
;(newQuery as any)[field] = res
|
||||||
|
if (typeof res === 'object') {
|
||||||
|
if (Array.isArray(res.$in) && res.$in.length === 1 && Object.keys(res).length === 1) {
|
||||||
|
;(newQuery as any)[field] = res.$in[0]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const spaces = await this.filterByDomain(domain, this.getAllAllowedSpaces(account, !isSpace))
|
const spaces = await this.filterByDomain(domain, this.getAllAllowedSpaces(account, !isSpace))
|
||||||
if (spaces.length === 1) {
|
if (spaces.allDomainSpaces) {
|
||||||
;(newQuery as any)[field] = spaces[0]
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||||
|
delete (newQuery as any)[field]
|
||||||
|
} else if (spaces.result.length === 1) {
|
||||||
|
;(newQuery as any)[field] = spaces.result[0]
|
||||||
} else {
|
} else {
|
||||||
;(newQuery as any)[field] = { $in: spaces }
|
;(newQuery as any)[field] = { $in: spaces.result }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const findResult = await this.provideFindAll(ctx, _class, newQuery, options)
|
const findResult = await this.provideFindAll(ctx, _class, newQuery, options)
|
||||||
if (!isOwner(account, ctx) && account.role !== AccountRole.DocGuest) {
|
if (!isOwner(account, ctx) && account.role !== AccountRole.DocGuest) {
|
||||||
if (options?.lookup !== undefined) {
|
if (options?.lookup !== undefined) {
|
||||||
@ -564,7 +553,7 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
|
|||||||
}
|
}
|
||||||
passedDomains.add(domain)
|
passedDomains.add(domain)
|
||||||
const spaces = await this.filterByDomain(domain, allSpaces)
|
const spaces = await this.filterByDomain(domain, allSpaces)
|
||||||
for (const space of spaces) {
|
for (const space of spaces.result) {
|
||||||
res.add(space)
|
res.add(space)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,6 @@ import core, {
|
|||||||
type FindResult,
|
type FindResult,
|
||||||
type FullParamsType,
|
type FullParamsType,
|
||||||
type Hierarchy,
|
type Hierarchy,
|
||||||
type IndexingConfiguration,
|
|
||||||
type Lookup,
|
type Lookup,
|
||||||
type MeasureContext,
|
type MeasureContext,
|
||||||
type Mixin,
|
type Mixin,
|
||||||
@ -65,6 +64,7 @@ import {
|
|||||||
estimateDocSize,
|
estimateDocSize,
|
||||||
updateHashForDoc,
|
updateHashForDoc,
|
||||||
type DbAdapter,
|
type DbAdapter,
|
||||||
|
type DbAdapterHandler,
|
||||||
type DomainHelperOperations,
|
type DomainHelperOperations,
|
||||||
type ServerFindOptions,
|
type ServerFindOptions,
|
||||||
type StorageAdapter,
|
type StorageAdapter,
|
||||||
@ -76,7 +76,6 @@ import {
|
|||||||
type AbstractCursor,
|
type AbstractCursor,
|
||||||
type AnyBulkWriteOperation,
|
type AnyBulkWriteOperation,
|
||||||
type Collection,
|
type Collection,
|
||||||
type CreateIndexesOptions,
|
|
||||||
type Db,
|
type Db,
|
||||||
type Document,
|
type Document,
|
||||||
type Filter,
|
type Filter,
|
||||||
@ -131,6 +130,18 @@ abstract class MongoAdapterBase implements DbAdapter {
|
|||||||
findRateLimit = new RateLimiter(parseInt(process.env.FIND_RLIMIT ?? '1000'))
|
findRateLimit = new RateLimiter(parseInt(process.env.FIND_RLIMIT ?? '1000'))
|
||||||
rateLimit = new RateLimiter(parseInt(process.env.TX_RLIMIT ?? '5'))
|
rateLimit = new RateLimiter(parseInt(process.env.TX_RLIMIT ?? '5'))
|
||||||
|
|
||||||
|
handlers: DbAdapterHandler[] = []
|
||||||
|
|
||||||
|
on (handler: DbAdapterHandler): void {
|
||||||
|
this.handlers.push(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEvent (domain: Domain, event: 'add' | 'update' | 'delete' | 'read', count: number, time: number): void {
|
||||||
|
for (const handler of this.handlers) {
|
||||||
|
handler(domain, event, count, time, this._db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
protected readonly db: Db,
|
protected readonly db: Db,
|
||||||
protected readonly hierarchy: Hierarchy,
|
protected readonly hierarchy: Hierarchy,
|
||||||
@ -151,45 +162,6 @@ abstract class MongoAdapterBase implements DbAdapter {
|
|||||||
return this._db
|
return this._db
|
||||||
}
|
}
|
||||||
|
|
||||||
async createIndexes (domain: Domain, config: Pick<IndexingConfiguration<Doc>, 'indexes'>): Promise<void> {
|
|
||||||
for (const value of config.indexes) {
|
|
||||||
try {
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
await this.collection(domain).createIndex(value)
|
|
||||||
} else {
|
|
||||||
const opt: CreateIndexesOptions = {}
|
|
||||||
if (value.filter !== undefined) {
|
|
||||||
opt.partialFilterExpression = value.filter
|
|
||||||
} else if (value.sparse === true) {
|
|
||||||
opt.sparse = true
|
|
||||||
}
|
|
||||||
await this.collection(domain).createIndex(value.keys, opt)
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error('failed to create index', domain, value, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeOldIndex (domain: Domain, deletePattern: RegExp[], keepPattern: RegExp[]): Promise<void> {
|
|
||||||
try {
|
|
||||||
const existingIndexes = await this.collection(domain).indexes()
|
|
||||||
for (const existingIndex of existingIndexes) {
|
|
||||||
if (existingIndex.name !== undefined) {
|
|
||||||
const name: string = existingIndex.name
|
|
||||||
if (
|
|
||||||
deletePattern.some((it) => it.test(name)) &&
|
|
||||||
(existingIndex.sparse === true || !keepPattern.some((it) => it.test(name)))
|
|
||||||
) {
|
|
||||||
await this.collection(domain).dropIndex(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async tx (ctx: MeasureContext, ...tx: Tx[]): Promise<TxResult[]> {
|
async tx (ctx: MeasureContext, ...tx: Tx[]): Promise<TxResult[]> {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@ -198,7 +170,11 @@ abstract class MongoAdapterBase implements DbAdapter {
|
|||||||
this.client.close()
|
this.client.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
private translateQuery<T extends Doc>(clazz: Ref<Class<T>>, query: DocumentQuery<T>): Filter<Document> {
|
private translateQuery<T extends Doc>(
|
||||||
|
clazz: Ref<Class<T>>,
|
||||||
|
query: DocumentQuery<T>,
|
||||||
|
options?: ServerFindOptions<T>
|
||||||
|
): Filter<Document> {
|
||||||
const translated: any = {}
|
const translated: any = {}
|
||||||
for (const key in query) {
|
for (const key in query) {
|
||||||
const value = (query as any)[key]
|
const value = (query as any)[key]
|
||||||
@ -213,6 +189,13 @@ abstract class MongoAdapterBase implements DbAdapter {
|
|||||||
}
|
}
|
||||||
translated[tkey] = value
|
translated[tkey] = value
|
||||||
}
|
}
|
||||||
|
if (options?.skipSpace === true) {
|
||||||
|
delete translated.space
|
||||||
|
}
|
||||||
|
if (options?.skipClass === true) {
|
||||||
|
delete translated._class
|
||||||
|
return translated
|
||||||
|
}
|
||||||
const baseClass = this.hierarchy.getBaseClass(clazz)
|
const baseClass = this.hierarchy.getBaseClass(clazz)
|
||||||
if (baseClass !== core.class.Doc) {
|
if (baseClass !== core.class.Doc) {
|
||||||
const classes = this.hierarchy.getDescendants(baseClass).filter((it) => !this.hierarchy.isMixin(it))
|
const classes = this.hierarchy.getDescendants(baseClass).filter((it) => !this.hierarchy.isMixin(it))
|
||||||
@ -473,12 +456,15 @@ abstract class MongoAdapterBase implements DbAdapter {
|
|||||||
|
|
||||||
private async findWithPipeline<T extends Doc>(
|
private async findWithPipeline<T extends Doc>(
|
||||||
ctx: MeasureContext,
|
ctx: MeasureContext,
|
||||||
|
domain: Domain,
|
||||||
clazz: Ref<Class<T>>,
|
clazz: Ref<Class<T>>,
|
||||||
query: DocumentQuery<T>,
|
query: DocumentQuery<T>,
|
||||||
options?: ServerFindOptions<T>
|
options: ServerFindOptions<T>,
|
||||||
|
stTime: number
|
||||||
): Promise<FindResult<T>> {
|
): Promise<FindResult<T>> {
|
||||||
|
const st = Date.now()
|
||||||
const pipeline: any[] = []
|
const pipeline: any[] = []
|
||||||
const match = { $match: this.translateQuery(clazz, query) }
|
const match = { $match: this.translateQuery(clazz, query, options) }
|
||||||
const slowPipeline = isLookupQuery(query) || isLookupSort(options?.sort)
|
const slowPipeline = isLookupQuery(query) || isLookupSort(options?.sort)
|
||||||
const steps = await ctx.with('get-lookups', {}, async () => await this.getLookups(clazz, options?.lookup))
|
const steps = await ctx.with('get-lookups', {}, async () => await this.getLookups(clazz, options?.lookup))
|
||||||
if (slowPipeline) {
|
if (slowPipeline) {
|
||||||
@ -506,9 +492,6 @@ abstract class MongoAdapterBase implements DbAdapter {
|
|||||||
pipeline.push({ $project: projection })
|
pipeline.push({ $project: projection })
|
||||||
}
|
}
|
||||||
|
|
||||||
// const domain = this.hierarchy.getDomain(clazz)
|
|
||||||
const domain = options?.domain ?? this.hierarchy.getDomain(clazz)
|
|
||||||
|
|
||||||
const cursor = this.collection(domain).aggregate<WithLookup<T>>(pipeline)
|
const cursor = this.collection(domain).aggregate<WithLookup<T>>(pipeline)
|
||||||
let result: WithLookup<T>[] = []
|
let result: WithLookup<T>[] = []
|
||||||
let total = options?.total === true ? 0 : -1
|
let total = options?.total === true ? 0 : -1
|
||||||
@ -558,6 +541,17 @@ abstract class MongoAdapterBase implements DbAdapter {
|
|||||||
)
|
)
|
||||||
total = arr?.[0]?.total ?? 0
|
total = arr?.[0]?.total ?? 0
|
||||||
}
|
}
|
||||||
|
const edTime = Date.now()
|
||||||
|
if (edTime - stTime > 1000 || st - stTime > 1000) {
|
||||||
|
ctx.error('aggregate', {
|
||||||
|
time: edTime - stTime,
|
||||||
|
clazz,
|
||||||
|
query: cutObjectArray(query),
|
||||||
|
options,
|
||||||
|
queueTime: st - stTime
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.handleEvent(domain, 'read', result.length, edTime - st)
|
||||||
return toFindResult(this.stripHash(result) as T[], total)
|
return toFindResult(this.stripHash(result) as T[], total)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -643,7 +637,12 @@ abstract class MongoAdapterBase implements DbAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@withContext('groupBy')
|
@withContext('groupBy')
|
||||||
async groupBy<T>(ctx: MeasureContext, domain: Domain, field: string): Promise<Set<T>> {
|
async groupBy<T, D extends Doc = Doc>(
|
||||||
|
ctx: MeasureContext,
|
||||||
|
domain: Domain,
|
||||||
|
field: string,
|
||||||
|
query?: DocumentQuery<D>
|
||||||
|
): Promise<Set<T>> {
|
||||||
const result = await ctx.with(
|
const result = await ctx.with(
|
||||||
'groupBy',
|
'groupBy',
|
||||||
{ domain },
|
{ domain },
|
||||||
@ -651,6 +650,7 @@ abstract class MongoAdapterBase implements DbAdapter {
|
|||||||
const coll = this.collection(domain)
|
const coll = this.collection(domain)
|
||||||
const grResult = await coll
|
const grResult = await coll
|
||||||
.aggregate([
|
.aggregate([
|
||||||
|
...(query !== undefined ? [{ $match: query }] : []),
|
||||||
{
|
{
|
||||||
$group: {
|
$group: {
|
||||||
_id: '$' + field
|
_id: '$' + field
|
||||||
@ -716,20 +716,20 @@ abstract class MongoAdapterBase implements DbAdapter {
|
|||||||
const stTime = Date.now()
|
const stTime = Date.now()
|
||||||
return await this.findRateLimit.exec(async () => {
|
return await this.findRateLimit.exec(async () => {
|
||||||
const st = Date.now()
|
const st = Date.now()
|
||||||
|
const domain = options?.domain ?? this.hierarchy.getDomain(_class)
|
||||||
const result = await this.collectOps(
|
const result = await this.collectOps(
|
||||||
ctx,
|
ctx,
|
||||||
this.hierarchy.findDomain(_class),
|
domain,
|
||||||
'find',
|
'find',
|
||||||
async (ctx) => {
|
async (ctx) => {
|
||||||
const domain = options?.domain ?? this.hierarchy.getDomain(_class)
|
|
||||||
if (
|
if (
|
||||||
options != null &&
|
options != null &&
|
||||||
(options?.lookup != null || this.isEnumSort(_class, options) || this.isRulesSort(options))
|
(options?.lookup != null || this.isEnumSort(_class, options) || this.isRulesSort(options))
|
||||||
) {
|
) {
|
||||||
return await this.findWithPipeline(ctx, _class, query, options)
|
return await this.findWithPipeline(ctx, domain, _class, query, options, stTime)
|
||||||
}
|
}
|
||||||
const coll = this.collection(domain)
|
const coll = this.collection(domain)
|
||||||
const mongoQuery = this.translateQuery(_class, query)
|
const mongoQuery = this.translateQuery(_class, query, options)
|
||||||
|
|
||||||
if (options?.limit === 1) {
|
if (options?.limit === 1) {
|
||||||
// Skip sort/projection/etc.
|
// Skip sort/projection/etc.
|
||||||
@ -825,6 +825,7 @@ abstract class MongoAdapterBase implements DbAdapter {
|
|||||||
queueTime: st - stTime
|
queueTime: st - stTime
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
this.handleEvent(domain, 'read', result.length, edTime - st)
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -1122,7 +1123,6 @@ class MongoAdapter extends MongoAdapterBase {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await this.rateLimit.exec(async () => {
|
await this.rateLimit.exec(async () => {
|
||||||
const domains: Promise<void>[] = []
|
|
||||||
for (const [domain, txs] of byDomain) {
|
for (const [domain, txs] of byDomain) {
|
||||||
if (domain === undefined) {
|
if (domain === undefined) {
|
||||||
continue
|
continue
|
||||||
@ -1146,75 +1146,80 @@ class MongoAdapter extends MongoAdapterBase {
|
|||||||
) {
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
domains.push(
|
await this.collectOps(
|
||||||
this.collectOps(
|
ctx,
|
||||||
ctx,
|
domain,
|
||||||
domain,
|
'tx',
|
||||||
'tx',
|
async (ctx) => {
|
||||||
async (ctx) => {
|
const coll = this.db.collection<Doc>(domain)
|
||||||
const coll = this.db.collection<Doc>(domain)
|
|
||||||
|
|
||||||
// Minir optimizations
|
// Minir optimizations
|
||||||
// Add Remove optimization
|
// Add Remove optimization
|
||||||
|
|
||||||
if (domainBulk.add.length > 0) {
|
if (domainBulk.add.length > 0) {
|
||||||
await ctx.with('insertMany', {}, async () => {
|
await ctx.with('insertMany', {}, async () => {
|
||||||
await coll.insertMany(domainBulk.add, { ordered: false })
|
const st = Date.now()
|
||||||
})
|
const result = await coll.insertMany(domainBulk.add, { ordered: false })
|
||||||
}
|
this.handleEvent(domain, 'add', result.insertedCount, Date.now() - st)
|
||||||
if (domainBulk.update.size > 0) {
|
})
|
||||||
// Extract similar update to update many if possible
|
|
||||||
// TODO:
|
|
||||||
await ctx.with('updateMany-bulk', {}, async () => {
|
|
||||||
await coll.bulkWrite(
|
|
||||||
Array.from(domainBulk.update.entries()).map((it) => ({
|
|
||||||
updateOne: {
|
|
||||||
filter: { _id: it[0] },
|
|
||||||
update: {
|
|
||||||
$set: it[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
{
|
|
||||||
ordered: false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (domainBulk.bulkOperations.length > 0) {
|
|
||||||
await ctx.with('bulkWrite', {}, async () => {
|
|
||||||
await coll.bulkWrite(domainBulk.bulkOperations, {
|
|
||||||
ordered: false
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (domainBulk.findUpdate.size > 0) {
|
|
||||||
await ctx.with('find-result', {}, async () => {
|
|
||||||
const docs = await coll.find({ _id: { $in: Array.from(domainBulk.findUpdate) } }).toArray()
|
|
||||||
result.push(...docs)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (domainBulk.raw.length > 0) {
|
|
||||||
await ctx.with('raw', {}, async () => {
|
|
||||||
for (const r of domainBulk.raw) {
|
|
||||||
result.push({ object: await r() })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
domain,
|
|
||||||
add: domainBulk.add.length,
|
|
||||||
update: domainBulk.update.size,
|
|
||||||
bulk: domainBulk.bulkOperations.length,
|
|
||||||
find: domainBulk.findUpdate.size,
|
|
||||||
raw: domainBulk.raw.length
|
|
||||||
}
|
}
|
||||||
)
|
if (domainBulk.update.size > 0) {
|
||||||
|
// Extract similar update to update many if possible
|
||||||
|
// TODO:
|
||||||
|
await ctx.with('updateMany-bulk', {}, async () => {
|
||||||
|
const st = Date.now()
|
||||||
|
const result = await coll.bulkWrite(
|
||||||
|
Array.from(domainBulk.update.entries()).map((it) => ({
|
||||||
|
updateOne: {
|
||||||
|
filter: { _id: it[0] },
|
||||||
|
update: {
|
||||||
|
$set: it[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
ordered: false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
this.handleEvent(domain, 'update', result.modifiedCount, Date.now() - st)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (domainBulk.bulkOperations.length > 0) {
|
||||||
|
await ctx.with('bulkWrite', {}, async () => {
|
||||||
|
const st = Date.now()
|
||||||
|
const result = await coll.bulkWrite(domainBulk.bulkOperations, {
|
||||||
|
ordered: false
|
||||||
|
})
|
||||||
|
this.handleEvent(domain, 'update', result.modifiedCount, Date.now() - st)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (domainBulk.findUpdate.size > 0) {
|
||||||
|
await ctx.with('find-result', {}, async () => {
|
||||||
|
const st = Date.now()
|
||||||
|
const docs = await coll.find({ _id: { $in: Array.from(domainBulk.findUpdate) } }).toArray()
|
||||||
|
result.push(...docs)
|
||||||
|
this.handleEvent(domain, 'read', docs.length, Date.now() - st)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domainBulk.raw.length > 0) {
|
||||||
|
await ctx.with('raw', {}, async () => {
|
||||||
|
for (const r of domainBulk.raw) {
|
||||||
|
result.push({ object: await r() })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
domain,
|
||||||
|
add: domainBulk.add.length,
|
||||||
|
update: domainBulk.update.size,
|
||||||
|
bulk: domainBulk.bulkOperations.length,
|
||||||
|
find: domainBulk.findUpdate.size,
|
||||||
|
raw: domainBulk.raw.length
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
await Promise.all(domains)
|
|
||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@ -1395,6 +1400,7 @@ class MongoAdapter extends MongoAdapterBase {
|
|||||||
|
|
||||||
if (tx.retrieve === true) {
|
if (tx.retrieve === true) {
|
||||||
bulk.raw.push(async () => {
|
bulk.raw.push(async () => {
|
||||||
|
const st = Date.now()
|
||||||
const res = await this.collection(domain).findOneAndUpdate(
|
const res = await this.collection(domain).findOneAndUpdate(
|
||||||
{ _id: tx.objectId },
|
{ _id: tx.objectId },
|
||||||
{
|
{
|
||||||
@ -1407,6 +1413,9 @@ class MongoAdapter extends MongoAdapterBase {
|
|||||||
} as unknown as UpdateFilter<Document>,
|
} as unknown as UpdateFilter<Document>,
|
||||||
{ returnDocument: 'after', includeResultMetadata: true }
|
{ returnDocument: 'after', includeResultMetadata: true }
|
||||||
)
|
)
|
||||||
|
const dnow = Date.now() - st
|
||||||
|
this.handleEvent(domain, 'read', 1, dnow)
|
||||||
|
this.handleEvent(domain, 'update', 1, dnow)
|
||||||
return res.value as TxResult
|
return res.value as TxResult
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@ -1459,6 +1468,7 @@ class MongoTxAdapter extends MongoAdapterBase implements TxAdapter {
|
|||||||
if (tx.length === 0) {
|
if (tx.length === 0) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
const st = Date.now()
|
||||||
await this.collectOps(
|
await this.collectOps(
|
||||||
ctx,
|
ctx,
|
||||||
DOMAIN_TX,
|
DOMAIN_TX,
|
||||||
@ -1468,6 +1478,7 @@ class MongoTxAdapter extends MongoAdapterBase implements TxAdapter {
|
|||||||
},
|
},
|
||||||
{ tx: tx.length }
|
{ tx: tx.length }
|
||||||
)
|
)
|
||||||
|
this.handleEvent(DOMAIN_TX, 'add', tx.length, Date.now() - st)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,6 +158,11 @@ export class DBCollectionHelper implements DomainHelperOperations {
|
|||||||
collections = new Map<string, Collection<any>>()
|
collections = new Map<string, Collection<any>>()
|
||||||
constructor (readonly db: Db) {}
|
constructor (readonly db: Db) {}
|
||||||
|
|
||||||
|
async listDomains (): Promise<Set<Domain>> {
|
||||||
|
const collections = await this.db.listCollections({}, { nameOnly: true }).toArray()
|
||||||
|
return new Set(collections.map((it) => it.name as unknown as Domain))
|
||||||
|
}
|
||||||
|
|
||||||
async init (domain?: Domain): Promise<void> {
|
async init (domain?: Domain): Promise<void> {
|
||||||
if (domain === undefined) {
|
if (domain === undefined) {
|
||||||
// Init existing collecfions
|
// Init existing collecfions
|
||||||
@ -224,7 +229,8 @@ export class DBCollectionHelper implements DomainHelperOperations {
|
|||||||
return await this.collection(domain).listIndexes().toArray()
|
return await this.collection(domain).listIndexes().toArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
async hasDocuments (domain: Domain, count: number): Promise<boolean> {
|
async estimatedCount (domain: Domain): Promise<number> {
|
||||||
return (await this.collection(domain).countDocuments({}, { limit: count })) >= count
|
const c = this.collection(domain)
|
||||||
|
return await c.estimatedDocumentCount()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
/* eslint-disable @typescript-eslint/unbound-method */
|
/* eslint-disable @typescript-eslint/unbound-method */
|
||||||
import { type Branding, type MeasureContext, type WorkspaceId } from '@hcengineering/core'
|
import { type Branding, type MeasureContext, type WorkspaceId } from '@hcengineering/core'
|
||||||
import { OpenAIEmbeddingsStage } from '@hcengineering/openai'
|
|
||||||
import { CollaborativeContentRetrievalStage } from '@hcengineering/server-collaboration'
|
import { CollaborativeContentRetrievalStage } from '@hcengineering/server-collaboration'
|
||||||
import {
|
import {
|
||||||
ContentRetrievalStage,
|
ContentRetrievalStage,
|
||||||
@ -65,14 +64,14 @@ export function createIndexStages (
|
|||||||
const pushStage = new FullTextPushStage(storage, adapter, workspace, branding)
|
const pushStage = new FullTextPushStage(storage, adapter, workspace, branding)
|
||||||
stages.push(pushStage)
|
stages.push(pushStage)
|
||||||
|
|
||||||
// OpenAI prepare stage
|
// // OpenAI prepare stage
|
||||||
const openAIStage = new OpenAIEmbeddingsStage(adapter, workspace)
|
// const openAIStage = new OpenAIEmbeddingsStage(adapter, workspace)
|
||||||
// We depend on all available stages.
|
// // We depend on all available stages.
|
||||||
openAIStage.require = stages.map((it) => it.stageId)
|
// openAIStage.require = stages.map((it) => it.stageId)
|
||||||
|
|
||||||
openAIStage.updateSummary(summaryStage)
|
// openAIStage.updateSummary(summaryStage)
|
||||||
|
|
||||||
stages.push(openAIStage)
|
// stages.push(openAIStage)
|
||||||
|
|
||||||
return stages
|
return stages
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,12 @@ import core, {
|
|||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import { createMongoAdapter } from '@hcengineering/mongo'
|
import { createMongoAdapter } from '@hcengineering/mongo'
|
||||||
import { PlatformError, unknownError } from '@hcengineering/platform'
|
import { PlatformError, unknownError } from '@hcengineering/platform'
|
||||||
import { DbAdapter, StorageAdapter, type StorageAdapterEx } from '@hcengineering/server-core'
|
import {
|
||||||
|
DbAdapter,
|
||||||
|
StorageAdapter,
|
||||||
|
type DomainHelperOperations,
|
||||||
|
type StorageAdapterEx
|
||||||
|
} from '@hcengineering/server-core'
|
||||||
|
|
||||||
class StorageBlobAdapter implements DbAdapter {
|
class StorageBlobAdapter implements DbAdapter {
|
||||||
constructor (
|
constructor (
|
||||||
@ -53,6 +58,10 @@ class StorageBlobAdapter implements DbAdapter {
|
|||||||
return await this.blobAdapter.findAll(ctx, _class, query, options)
|
return await this.blobAdapter.findAll(ctx, _class, query, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
helper (): DomainHelperOperations {
|
||||||
|
return this.blobAdapter.helper()
|
||||||
|
}
|
||||||
|
|
||||||
async groupBy<T>(ctx: MeasureContext, domain: Domain, field: string): Promise<Set<T>> {
|
async groupBy<T>(ctx: MeasureContext, domain: Domain, field: string): Promise<Set<T>> {
|
||||||
return await this.blobAdapter.groupBy(ctx, domain, field)
|
return await this.blobAdapter.groupBy(ctx, domain, field)
|
||||||
}
|
}
|
||||||
|
@ -516,17 +516,7 @@ async function createUpdateIndexes (
|
|||||||
if (domain === DOMAIN_MODEL || domain === DOMAIN_TRANSIENT || domain === DOMAIN_BENCHMARK) {
|
if (domain === DOMAIN_MODEL || domain === DOMAIN_TRANSIENT || domain === DOMAIN_BENCHMARK) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const result = await domainHelper.checkDomain(ctx, domain, false, dbHelper)
|
await domainHelper.checkDomain(ctx, domain, await dbHelper.estimatedCount(domain), dbHelper)
|
||||||
if (!result && dbHelper.exists(domain)) {
|
|
||||||
try {
|
|
||||||
logger.log('dropping domain', { domain })
|
|
||||||
if ((await db.collection(domain).countDocuments({})) === 0) {
|
|
||||||
await db.dropCollection(domain)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.error('error: failed to delete collection', { domain, err })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
completed++
|
completed++
|
||||||
await progress((100 / allDomains.length) * completed)
|
await progress((100 / allDomains.length) * completed)
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ export SERVER_SECRET=secret
|
|||||||
# Restore workspace contents in mongo/elastic
|
# Restore workspace contents in mongo/elastic
|
||||||
./tool-local.sh backup-restore ./sanity-ws sanity-ws
|
./tool-local.sh backup-restore ./sanity-ws sanity-ws
|
||||||
|
|
||||||
./tool-local.sh upgrade-workspace sanity-ws
|
./tool-local.sh upgrade-workspace sanity-ws --indexes
|
||||||
|
|
||||||
# Re-assign user to workspace.
|
# Re-assign user to workspace.
|
||||||
./tool-local.sh assign-workspace user1 sanity-ws
|
./tool-local.sh assign-workspace user1 sanity-ws
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
# Restore workspace contents in mongo/elastic
|
# Restore workspace contents in mongo/elastic
|
||||||
./tool.sh backup-restore ./sanity-ws sanity-ws
|
./tool.sh backup-restore ./sanity-ws sanity-ws
|
||||||
|
|
||||||
./tool.sh upgrade-workspace sanity-ws
|
./tool.sh upgrade-workspace sanity-ws --indexes
|
||||||
|
|
||||||
# Re-assign user to workspace.
|
# Re-assign user to workspace.
|
||||||
./tool.sh assign-workspace user1 sanity-ws
|
./tool.sh assign-workspace user1 sanity-ws
|
||||||
|
@ -188,6 +188,7 @@ test.describe('channel tests', () => {
|
|||||||
await channelPage.clickChannelTab()
|
await channelPage.clickChannelTab()
|
||||||
await channelPage.clickOnUser(data.lastName + ' ' + data.firstName)
|
await channelPage.clickOnUser(data.lastName + ' ' + data.firstName)
|
||||||
await channelPage.addMemberToChannel(newUser2.lastName + ' ' + newUser2.firstName)
|
await channelPage.addMemberToChannel(newUser2.lastName + ' ' + newUser2.firstName)
|
||||||
|
await channelPage.pressEscape()
|
||||||
await leftSideMenuPageSecond.clickChunter()
|
await leftSideMenuPageSecond.clickChunter()
|
||||||
await channelPageSecond.checkIfChannelDefaultExist(true, data.channelName)
|
await channelPageSecond.checkIfChannelDefaultExist(true, data.channelName)
|
||||||
await channelPageSecond.clickChannelTab()
|
await channelPageSecond.clickChannelTab()
|
||||||
@ -409,4 +410,30 @@ test.describe('channel tests', () => {
|
|||||||
await channelPage.addMemberToChannelPreview(newUser2.lastName + ' ' + newUser2.firstName)
|
await channelPage.addMemberToChannelPreview(newUser2.lastName + ' ' + newUser2.firstName)
|
||||||
await page2.close()
|
await page2.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Checking backlinks in the Chat', async ({ browser, page }) => {
|
||||||
|
await leftSideMenuPage.openProfileMenu()
|
||||||
|
await leftSideMenuPage.inviteToWorkspace()
|
||||||
|
await leftSideMenuPage.getInviteLink()
|
||||||
|
const linkText = await page.locator('.antiPopup .link').textContent()
|
||||||
|
await leftSideMenuPage.clickOnCloseInvite()
|
||||||
|
const page2 = await browser.newPage()
|
||||||
|
const leftSideMenuPageSecond = new LeftSideMenuPage(page2)
|
||||||
|
const channelPageSecond = new ChannelPage(page2)
|
||||||
|
await api.createAccount(newUser2.email, newUser2.password, newUser2.firstName, newUser2.lastName)
|
||||||
|
await page2.goto(linkText ?? '')
|
||||||
|
const joinPage = new SignInJoinPage(page2)
|
||||||
|
await joinPage.join(newUser2)
|
||||||
|
|
||||||
|
await leftSideMenuPage.clickChunter()
|
||||||
|
await channelPage.clickChannel('general')
|
||||||
|
const mentionName = `${newUser2.lastName} ${newUser2.firstName}`
|
||||||
|
await channelPage.sendMention(mentionName)
|
||||||
|
await channelPage.checkMessageExist(`@${mentionName}`, true, `@${mentionName}`)
|
||||||
|
|
||||||
|
await leftSideMenuPageSecond.clickChunter()
|
||||||
|
await channelPageSecond.clickChannel('general')
|
||||||
|
await channelPageSecond.checkMessageExist(`@${mentionName}`, true, `@${mentionName}`)
|
||||||
|
await page2.close()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,15 +1,19 @@
|
|||||||
import { expect, type Locator, type Page } from '@playwright/test'
|
import { expect, type Locator, type Page } from '@playwright/test'
|
||||||
|
import { CommonPage } from './common-page'
|
||||||
|
|
||||||
export class ChannelPage {
|
export class ChannelPage extends CommonPage {
|
||||||
readonly page: Page
|
readonly page: Page
|
||||||
|
|
||||||
constructor (page: Page) {
|
constructor (page: Page) {
|
||||||
|
super(page)
|
||||||
this.page = page
|
this.page = page
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly inputMessage = (): Locator => this.page.locator('div[class~="text-editor-view"]')
|
readonly inputMessage = (): Locator => this.page.locator('div[class~="text-editor-view"]')
|
||||||
readonly buttonSendMessage = (): Locator => this.page.locator('g#Send')
|
readonly buttonSendMessage = (): Locator => this.page.locator('g#Send')
|
||||||
readonly textMessage = (messageText: string): Locator => this.page.getByText(messageText)
|
readonly textMessage = (messageText: string): Locator =>
|
||||||
|
this.page.locator('.hulyComponent .activityMessage', { hasText: messageText })
|
||||||
|
|
||||||
readonly channelName = (channel: string): Locator => this.page.getByText('general random').getByText(channel)
|
readonly channelName = (channel: string): Locator => this.page.getByText('general random').getByText(channel)
|
||||||
readonly channelTab = (): Locator => this.page.getByRole('link', { name: 'Channels' }).getByRole('button')
|
readonly channelTab = (): Locator => this.page.getByRole('link', { name: 'Channels' }).getByRole('button')
|
||||||
readonly channelTable = (): Locator => this.page.getByRole('table')
|
readonly channelTable = (): Locator => this.page.getByRole('table')
|
||||||
@ -71,6 +75,12 @@ export class ChannelPage {
|
|||||||
await this.buttonSendMessage().click()
|
await this.buttonSendMessage().click()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async sendMention (message: string): Promise<void> {
|
||||||
|
await this.inputMessage().fill(`@${message}`)
|
||||||
|
await this.selectMention(message)
|
||||||
|
await this.buttonSendMessage().click()
|
||||||
|
}
|
||||||
|
|
||||||
async clickOnOpenChannelDetails (): Promise<void> {
|
async clickOnOpenChannelDetails (): Promise<void> {
|
||||||
await this.openChannelDetails().click()
|
await this.openChannelDetails().click()
|
||||||
}
|
}
|
||||||
@ -232,9 +242,9 @@ export class ChannelPage {
|
|||||||
|
|
||||||
async checkMessageExist (message: string, messageExists: boolean, messageText: string): Promise<void> {
|
async checkMessageExist (message: string, messageExists: boolean, messageText: string): Promise<void> {
|
||||||
if (messageExists) {
|
if (messageExists) {
|
||||||
await expect(this.textMessage(messageText).filter({ hasText: message })).toBeVisible()
|
await expect(this.textMessage(messageText)).toBeVisible()
|
||||||
} else {
|
} else {
|
||||||
await expect(this.textMessage(messageText).filter({ hasText: message })).toBeHidden()
|
await expect(this.textMessage(messageText)).toBeHidden()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ export class CommonPage {
|
|||||||
historyBoxButtonFirst = (): Locator => this.page.locator('div.history-box button:first-child')
|
historyBoxButtonFirst = (): Locator => this.page.locator('div.history-box button:first-child')
|
||||||
inboxNotyButton = (): Locator => this.page.locator('button[id$="Inbox"] > div.noty')
|
inboxNotyButton = (): Locator => this.page.locator('button[id$="Inbox"] > div.noty')
|
||||||
mentionPopupListItem = (mentionName: string): Locator =>
|
mentionPopupListItem = (mentionName: string): Locator =>
|
||||||
this.page.locator('form.mentionPoup div.list-item span.name', { hasText: mentionName })
|
this.page.locator('form.mentionPoup div.list-item', { hasText: mentionName })
|
||||||
|
|
||||||
hulyPopupRowButton = (name: string): Locator =>
|
hulyPopupRowButton = (name: string): Locator =>
|
||||||
this.page.locator('div.hulyPopup-container button.hulyPopup-row', { hasText: name })
|
this.page.locator('div.hulyPopup-container button.hulyPopup-row', { hasText: name })
|
||||||
@ -298,4 +298,12 @@ export class CommonPage {
|
|||||||
async openRowInTableByText (text: string): Promise<void> {
|
async openRowInTableByText (text: string): Promise<void> {
|
||||||
await this.linesFromTable(text).locator('a', { hasText: text }).click()
|
await this.linesFromTable(text).locator('a', { hasText: text }).click()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async checkRowsInListExist (text: string, count: number = 1): Promise<void> {
|
||||||
|
await expect(this.linesFromList(text)).toHaveCount(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
async pressEscape (): Promise<void> {
|
||||||
|
await this.page.keyboard.press('Escape')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ export class IssuesDetailsPage extends CommonTrackerPage {
|
|||||||
this.page = page
|
this.page = page
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly issueTitle = (): Locator => this.page.locator('div.hulyHeader-container div.title')
|
||||||
readonly inputTitle = (): Locator => this.page.locator('div.popupPanel-body input[type="text"]')
|
readonly inputTitle = (): Locator => this.page.locator('div.popupPanel-body input[type="text"]')
|
||||||
readonly inputDescription = (): Locator => this.page.locator('div.popupPanel-body div.textInput div.tiptap')
|
readonly inputDescription = (): Locator => this.page.locator('div.popupPanel-body div.textInput div.tiptap')
|
||||||
readonly textIdentifier = (): Locator => this.page.locator('div.title.not-active')
|
readonly textIdentifier = (): Locator => this.page.locator('div.title.not-active')
|
||||||
|
@ -19,6 +19,7 @@ export class IssuesPage extends CommonTrackerPage {
|
|||||||
inputPopupCreateNewIssueDescription = (): Locator =>
|
inputPopupCreateNewIssueDescription = (): Locator =>
|
||||||
this.page.locator('form[id="tracker:string:NewIssue"] div.tiptap')
|
this.page.locator('form[id="tracker:string:NewIssue"] div.tiptap')
|
||||||
|
|
||||||
|
buttonPopupCreateNewIssueProject = (): Locator => this.page.locator('[id="space\\.selector"]')
|
||||||
buttonPopupCreateNewIssueStatus = (): Locator =>
|
buttonPopupCreateNewIssueStatus = (): Locator =>
|
||||||
this.page.locator('form[id="tracker:string:NewIssue"] div#status-editor button')
|
this.page.locator('form[id="tracker:string:NewIssue"] div#status-editor button')
|
||||||
|
|
||||||
@ -279,10 +280,6 @@ export class IssuesPage extends CommonTrackerPage {
|
|||||||
await this.modifiedDateMenuItem().click()
|
await this.modifiedDateMenuItem().click()
|
||||||
}
|
}
|
||||||
|
|
||||||
async pressEscape (): Promise<void> {
|
|
||||||
await this.page.keyboard.press('Escape')
|
|
||||||
}
|
|
||||||
|
|
||||||
async clickEstimationContainer (): Promise<void> {
|
async clickEstimationContainer (): Promise<void> {
|
||||||
await this.estimationContainer().click()
|
await this.estimationContainer().click()
|
||||||
}
|
}
|
||||||
@ -423,6 +420,10 @@ export class IssuesPage extends CommonTrackerPage {
|
|||||||
async fillNewIssueForm (data: NewIssue): Promise<void> {
|
async fillNewIssueForm (data: NewIssue): Promise<void> {
|
||||||
await this.inputPopupCreateNewIssueTitle().fill(data.title)
|
await this.inputPopupCreateNewIssueTitle().fill(data.title)
|
||||||
await this.inputPopupCreateNewIssueDescription().fill(data.description)
|
await this.inputPopupCreateNewIssueDescription().fill(data.description)
|
||||||
|
if (data.projectName != null) {
|
||||||
|
await this.buttonPopupCreateNewIssueProject().click()
|
||||||
|
await this.selectMenuItem(this.page, data.projectName)
|
||||||
|
}
|
||||||
if (data.status != null) {
|
if (data.status != null) {
|
||||||
await this.buttonPopupCreateNewIssueStatus().click()
|
await this.buttonPopupCreateNewIssueStatus().click()
|
||||||
await this.selectFromDropdown(this.page, data.status)
|
await this.selectFromDropdown(this.page, data.status)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
export interface NewIssue extends Issue {
|
export interface NewIssue extends Issue {
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
|
projectName?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Issue {
|
export interface Issue {
|
||||||
|
@ -3,6 +3,7 @@ import { generateId, PlatformSetting, PlatformURI } from '../utils'
|
|||||||
import { LeftSideMenuPage } from '../model/left-side-menu-page'
|
import { LeftSideMenuPage } from '../model/left-side-menu-page'
|
||||||
import { IssuesPage } from '../model/tracker/issues-page'
|
import { IssuesPage } from '../model/tracker/issues-page'
|
||||||
import { IssuesDetailsPage } from '../model/tracker/issues-details-page'
|
import { IssuesDetailsPage } from '../model/tracker/issues-details-page'
|
||||||
|
import { TrackerNavigationMenuPage } from '../model/tracker/tracker-navigation-menu-page'
|
||||||
import { NewIssue } from '../model/tracker/types'
|
import { NewIssue } from '../model/tracker/types'
|
||||||
import { EmployeeDetailsPage } from '../model/contacts/employee-details-page'
|
import { EmployeeDetailsPage } from '../model/contacts/employee-details-page'
|
||||||
|
|
||||||
@ -92,4 +93,41 @@ test.describe('Mentions issue tests', () => {
|
|||||||
lastName: mentionName.split(' ')[0]
|
lastName: mentionName.split(' ')[0]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Checking backlinks in different spaces', async ({ page }) => {
|
||||||
|
const backlinkIssueDefault: NewIssue = {
|
||||||
|
title: `Issue for Default project-${generateId()}`,
|
||||||
|
description: 'Description',
|
||||||
|
projectName: 'Default'
|
||||||
|
}
|
||||||
|
const backlinkIssueSecond: NewIssue = {
|
||||||
|
title: `Issue for Second project-${generateId()}`,
|
||||||
|
description: 'Description',
|
||||||
|
projectName: 'Second Project'
|
||||||
|
}
|
||||||
|
await leftSideMenuPage.clickTracker()
|
||||||
|
await issuesPage.createNewIssue(backlinkIssueDefault)
|
||||||
|
await issuesPage.createNewIssue(backlinkIssueSecond)
|
||||||
|
const issuesNavigationPage = new TrackerNavigationMenuPage(page)
|
||||||
|
await issuesNavigationPage.issuesLinkForProject(backlinkIssueDefault.projectName ?? '').click()
|
||||||
|
await issuesPage.clickModelSelectorAll()
|
||||||
|
await issuesPage.searchIssueByName(backlinkIssueDefault.title)
|
||||||
|
await issuesPage.checkRowsInListExist(backlinkIssueDefault.title)
|
||||||
|
const defaultId = await issuesPage.getIssueId(backlinkIssueDefault.title)
|
||||||
|
await issuesNavigationPage.issuesLinkForProject(backlinkIssueSecond.projectName ?? '').click()
|
||||||
|
await issuesPage.clickModelSelectorAll()
|
||||||
|
await issuesPage.searchIssueByName(backlinkIssueSecond.title)
|
||||||
|
await issuesPage.checkRowsInListExist(backlinkIssueSecond.title)
|
||||||
|
const secondId = await issuesPage.getIssueId(backlinkIssueSecond.title)
|
||||||
|
await issuesPage.openIssueByName(backlinkIssueSecond.title)
|
||||||
|
await issuesDetailsPage.addMentions(defaultId)
|
||||||
|
await issuesDetailsPage.checkCommentExist(`@${defaultId}`)
|
||||||
|
await issuesDetailsPage.openLinkFromActivitiesByText(`@${defaultId}`)
|
||||||
|
await issuesDetailsPage.checkIssue(backlinkIssueDefault)
|
||||||
|
await issuesDetailsPage.addMentions(secondId)
|
||||||
|
await issuesDetailsPage.checkCommentExist(`@${secondId}`)
|
||||||
|
await issuesDetailsPage.openLinkFromActivitiesByText(`@${secondId}`)
|
||||||
|
await issuesDetailsPage.checkIssue(backlinkIssueSecond)
|
||||||
|
await issuesDetailsPage.clickCloseIssueButton()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user