mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-02 13:19:45 +00:00
Merge remote-tracking branch 'origin/staging' into develop
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
commit
e96f026383
@ -267,6 +267,10 @@ export function metricsToJson (metrics: Metrics): any {
|
||||
return toJson(metricsAggregate(metrics))
|
||||
}
|
||||
|
||||
export function metricsToJson (metrics: Metrics): any {
|
||||
return toJson(metricsAggregate(metrics))
|
||||
}
|
||||
|
||||
function printMetricsParamsRows (
|
||||
params: Record<string, Record<string, MetricsData>>,
|
||||
offset: number
|
||||
|
@ -28,7 +28,7 @@
|
||||
|
||||
const location = getCurrentLocation()
|
||||
Analytics.handleEvent('invite_link_activated')
|
||||
let page = 'login'
|
||||
let page = 'signUp'
|
||||
|
||||
$: fields =
|
||||
page === 'login'
|
||||
@ -60,7 +60,7 @@
|
||||
let status = OK
|
||||
|
||||
$: action = {
|
||||
i18n: login.string.Join,
|
||||
i18n: page === 'login' ? login.string.Join : login.string.SignUp,
|
||||
func: async () => {
|
||||
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
|
||||
|
||||
@ -97,23 +97,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
const signUpAction: BottomAction = {
|
||||
caption: login.string.DoNotHaveAnAccount,
|
||||
i18n: login.string.SignUp,
|
||||
func: () => (page = 'signUp')
|
||||
}
|
||||
|
||||
const loginJoinAction: BottomAction = {
|
||||
caption: login.string.HaveAccount,
|
||||
i18n: login.string.LogIn,
|
||||
func: () => (page = 'login')
|
||||
}
|
||||
|
||||
$: bottom = page === 'login' ? [signUpAction] : [loginJoinAction]
|
||||
$: secondaryButtonLabel = page === 'login' ? login.string.SignUp : undefined
|
||||
$: secondaryButtonAction = () => {
|
||||
page = 'signUp'
|
||||
}
|
||||
$: secondaryButtonLabel = page === 'login' ? login.string.SignUp : login.string.Join
|
||||
$: secondaryButtonAction =
|
||||
page === 'login'
|
||||
? () => {
|
||||
page = 'signUp'
|
||||
}
|
||||
: () => {
|
||||
page = 'login'
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
void check()
|
||||
@ -151,6 +143,6 @@
|
||||
{action}
|
||||
{secondaryButtonLabel}
|
||||
{secondaryButtonAction}
|
||||
bottomActions={[...bottom, loginAction, recoveryAction]}
|
||||
bottomActions={[loginAction, recoveryAction]}
|
||||
withProviders
|
||||
/>
|
||||
|
@ -35,7 +35,6 @@
|
||||
import {
|
||||
fetchWorkspace,
|
||||
getAccount,
|
||||
getHref,
|
||||
getWorkspaces,
|
||||
goTo,
|
||||
navigateToWorkspace,
|
||||
@ -43,9 +42,6 @@
|
||||
getAccountDisplayName
|
||||
} from '../utils'
|
||||
import StatusControl from './StatusControl.svelte'
|
||||
|
||||
export let navigateUrl: string | undefined = undefined
|
||||
|
||||
let workspaces: WorkspaceInfoWithStatus[] = []
|
||||
let status = OK
|
||||
let accountPromise: Promise<LoginInfo | null>
|
||||
|
@ -30,7 +30,6 @@ import documents, {
|
||||
DocumentApprovalRequest,
|
||||
DocumentState,
|
||||
DocumentTemplate,
|
||||
getDocumentId,
|
||||
getEffectiveDocUpdate,
|
||||
type DocumentRequest,
|
||||
type DocumentTraining
|
||||
@ -80,7 +79,7 @@ function archiveDocs (docs: ControlledDocument[], txFactory: TxFactory): Tx[] {
|
||||
function updateMeta (doc: ControlledDocument, txFactory: TxFactory): Tx[] {
|
||||
return [
|
||||
txFactory.createTxUpdateDoc(doc.attachedToClass, doc.space, doc.attachedTo, {
|
||||
title: `${getDocumentId(doc)} ${doc.title}`
|
||||
title: `${doc.code} ${doc.title}`
|
||||
})
|
||||
]
|
||||
}
|
||||
|
@ -101,6 +101,19 @@ export class ModelMiddleware extends BaseMiddleware implements Middleware {
|
||||
return this.provideFindAll(ctx, _class, query, options)
|
||||
}
|
||||
|
||||
findAll<T extends Doc>(
|
||||
ctx: MeasureContext<SessionData>,
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T>
|
||||
): Promise<FindResult<T>> {
|
||||
const d = this.context.hierarchy.findDomain(_class)
|
||||
if (d === DOMAIN_MODEL) {
|
||||
return this.context.modelDb.findAll(_class, query, options)
|
||||
}
|
||||
return this.provideFindAll(ctx, _class, query, options)
|
||||
}
|
||||
|
||||
async init (ctx: MeasureContext): Promise<void> {
|
||||
if (this.context.adapterManager == null) {
|
||||
throw new PlatformError(unknownError('Adapter manager should be configured'))
|
||||
|
@ -68,6 +68,8 @@ import {
|
||||
import { buildStorageFromConfig, storageConfigFromEnv } from '@hcengineering/server-storage'
|
||||
import { createWorkspace, upgradeWorkspace } from './ws-operations'
|
||||
|
||||
const dbCleanTreshold = 256 // Cleanup workspaces if less 256mb
|
||||
|
||||
export interface WorkspaceOptions {
|
||||
errorHandler: (workspace: WorkspaceInfoWithStatus, error: any) => Promise<void>
|
||||
force: boolean
|
||||
@ -395,18 +397,18 @@ export class WorkspaceWorker {
|
||||
/**
|
||||
* If onlyDrop is true, will drop workspace from database, overwize remove only indexes and do full reindex.
|
||||
*/
|
||||
async doCleanup (ctx: MeasureContext, workspace: WorkspaceInfoWithStatus, onlyDrop: boolean): Promise<void> {
|
||||
async doCleanup (ctx: MeasureContext, workspace: WorkspaceInfoWithStatus, cleanIndexes: boolean): Promise<void> {
|
||||
const { dbUrl } = prepareTools([])
|
||||
const adapter = getWorkspaceDestroyAdapter(dbUrl)
|
||||
await adapter.deleteWorkspace(ctx, sharedPipelineContextVars, workspace.uuid, workspace.dataId)
|
||||
|
||||
await this.doReindexFulltext(ctx, workspace, onlyDrop)
|
||||
await this.doReindexFulltext(ctx, workspace, cleanIndexes)
|
||||
}
|
||||
|
||||
private async doReindexFulltext (
|
||||
ctx: MeasureContext,
|
||||
workspace: WorkspaceInfoWithStatus,
|
||||
onlyDrop: boolean
|
||||
cleanIndexes: boolean
|
||||
): Promise<void> {
|
||||
if (this.fulltextUrl !== undefined) {
|
||||
const token = generateToken(systemAccountUuid, workspace.uuid, { service: 'workspace' })
|
||||
@ -417,7 +419,7 @@ export class WorkspaceWorker {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ token, onlyDrop })
|
||||
body: JSON.stringify({ token, onlyDrop: cleanIndexes })
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP Error ${res.status} ${res.statusText}`)
|
||||
@ -523,12 +525,16 @@ export class WorkspaceWorker {
|
||||
case 'migration-clean': {
|
||||
// We should remove DB, not storages.
|
||||
await sendEvent('migrate-clean-started', 0)
|
||||
await this.sendTransactorMaitenance(token, workspace.uuid)
|
||||
try {
|
||||
await this.doCleanup(ctx, workspace, false)
|
||||
} catch (err: any) {
|
||||
Analytics.handleError(err)
|
||||
return
|
||||
await this.sendTransactorMaitenance(token, { name: workspace.uuid })
|
||||
|
||||
const sz = workspace.backupInfo?.backupSize ?? 0
|
||||
if (sz <= dbCleanTreshold) {
|
||||
try {
|
||||
await this.doCleanup(ctx, workspace, false)
|
||||
} catch (err: any) {
|
||||
Analytics.handleError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
await sendEvent('migrate-clean-done', 0)
|
||||
break
|
||||
|
240
server/ws/src/__tests__/rest.test.ts
Normal file
240
server/ws/src/__tests__/rest.test.ts
Normal file
@ -0,0 +1,240 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { generateToken } from '@hcengineering/server-token'
|
||||
|
||||
import { createRestClient, createRestTxOperations, type RestClient } from '@hcengineering/api-client'
|
||||
import core, {
|
||||
generateId,
|
||||
getWorkspaceId,
|
||||
Hierarchy,
|
||||
MeasureMetricsContext,
|
||||
ModelDb,
|
||||
toFindResult,
|
||||
type Class,
|
||||
type Doc,
|
||||
type DocumentQuery,
|
||||
type Domain,
|
||||
type FindOptions,
|
||||
type FindResult,
|
||||
type MeasureContext,
|
||||
type Ref,
|
||||
type Space,
|
||||
type Tx,
|
||||
type TxCreateDoc,
|
||||
type TxOperations,
|
||||
type TxResult
|
||||
} from '@hcengineering/core'
|
||||
import { ClientSession, startSessionManager, type TSessionManager } from '@hcengineering/server'
|
||||
import { createDummyStorageAdapter, type SessionManager, type WorkspaceLoginInfo } from '@hcengineering/server-core'
|
||||
import { startHttpServer } from '../server_http'
|
||||
import { genMinModel, test } from './minmodel'
|
||||
|
||||
describe('rest-server', () => {
|
||||
async function getModelDb (): Promise<{ modelDb: ModelDb, hierarchy: Hierarchy, txes: Tx[] }> {
|
||||
const txes = genMinModel()
|
||||
const hierarchy = new Hierarchy()
|
||||
for (const tx of txes) {
|
||||
hierarchy.tx(tx)
|
||||
}
|
||||
const modelDb = new ModelDb(hierarchy)
|
||||
for (const tx of txes) {
|
||||
await modelDb.tx(tx)
|
||||
}
|
||||
return { modelDb, hierarchy, txes }
|
||||
}
|
||||
|
||||
let shutdown: () => Promise<void>
|
||||
let sessionManager: SessionManager
|
||||
const port: number = 11000
|
||||
|
||||
beforeAll(async () => {
|
||||
;({ shutdown, sessionManager } = startSessionManager(new MeasureMetricsContext('test', {}), {
|
||||
pipelineFactory: async () => {
|
||||
const { modelDb, hierarchy, txes } = await getModelDb()
|
||||
return {
|
||||
hierarchy,
|
||||
modelDb,
|
||||
context: {
|
||||
workspace: {
|
||||
name: 'test-ws',
|
||||
workspaceName: 'test-ws',
|
||||
workspaceUrl: 'test-ws'
|
||||
},
|
||||
hierarchy,
|
||||
modelDb,
|
||||
lastTx: generateId(),
|
||||
lastHash: generateId(),
|
||||
contextVars: {},
|
||||
branding: null
|
||||
},
|
||||
handleBroadcast: async (ctx) => {},
|
||||
findAll: async <T extends Doc>(
|
||||
ctx: MeasureContext,
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T>
|
||||
): Promise<FindResult<T>> => toFindResult(await modelDb.findAll(_class, query, options)),
|
||||
tx: async (ctx: MeasureContext, tx: Tx[]): Promise<[TxResult, Tx[], string[] | undefined]> => [
|
||||
await modelDb.tx(...tx),
|
||||
[],
|
||||
undefined
|
||||
],
|
||||
close: async () => {},
|
||||
domains: async () => hierarchy.domains(),
|
||||
groupBy: async () => new Map(),
|
||||
find: (ctx: MeasureContext, domain: Domain) => ({
|
||||
next: async (ctx: MeasureContext) => undefined,
|
||||
close: async (ctx: MeasureContext) => {}
|
||||
}),
|
||||
load: async (ctx: MeasureContext, domain: Domain, docs: Ref<Doc>[]) => [],
|
||||
upload: async (ctx: MeasureContext, domain: Domain, docs: Doc[]) => {},
|
||||
clean: async (ctx: MeasureContext, domain: Domain, docs: Ref<Doc>[]) => {},
|
||||
searchFulltext: async (ctx, query, options) => {
|
||||
return { docs: [] }
|
||||
},
|
||||
loadModel: async (ctx, lastModelTx, hash) => ({
|
||||
full: true,
|
||||
hash: generateId(),
|
||||
transactions: txes
|
||||
})
|
||||
}
|
||||
},
|
||||
sessionFactory: (token, workspace) => new ClientSession(token, workspace, true),
|
||||
port,
|
||||
brandingMap: {},
|
||||
serverFactory: startHttpServer,
|
||||
accountsUrl: '',
|
||||
externalStorage: createDummyStorageAdapter()
|
||||
}))
|
||||
jest
|
||||
.spyOn(sessionManager as TSessionManager, 'getWorkspaceInfo')
|
||||
.mockImplementation(async (ctx: MeasureContext, token: string): Promise<WorkspaceLoginInfo> => {
|
||||
return {
|
||||
workspaceId: 'test-ws',
|
||||
workspaceUrl: 'test-ws',
|
||||
workspaceName: 'Test Workspace',
|
||||
uuid: 'test-ws',
|
||||
createdBy: 'test-owner',
|
||||
mode: 'active',
|
||||
createdOn: Date.now(),
|
||||
lastVisit: Date.now(),
|
||||
disabled: false,
|
||||
endpoint: `http://localhost:${port}`,
|
||||
region: 'test-region',
|
||||
targetRegion: 'test-region',
|
||||
backupInfo: {
|
||||
dataSize: 0,
|
||||
blobsSize: 0,
|
||||
backupSize: 0,
|
||||
lastBackup: 0,
|
||||
backups: 0
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
afterAll(async () => {
|
||||
await shutdown()
|
||||
})
|
||||
|
||||
function connect (): RestClient {
|
||||
const token: string = generateToken('user1@site.com', getWorkspaceId('test-ws'))
|
||||
return createRestClient(`http://localhost:${port}`, 'test-ws', token)
|
||||
}
|
||||
|
||||
async function connectTx (): Promise<TxOperations> {
|
||||
const token: string = generateToken('user1@site.com', getWorkspaceId('test-ws'))
|
||||
return await createRestTxOperations(`http://localhost:${port}`, 'test-ws', token)
|
||||
}
|
||||
|
||||
it('get account', async () => {
|
||||
const conn = connect()
|
||||
const account = await conn.getAccount()
|
||||
|
||||
expect(account.email).toBe('user1@site.com')
|
||||
expect(account.role).toBe('OWNER')
|
||||
expect(account._id).toBe('User1')
|
||||
expect(account._class).toBe('core:class:Account')
|
||||
expect(account.space).toBe('core:space:Model')
|
||||
expect(account.modifiedBy).toBe('core:account:System')
|
||||
expect(account.createdBy).toBe('core:account:System')
|
||||
expect(typeof account.modifiedOn).toBe('number')
|
||||
expect(typeof account.createdOn).toBe('number')
|
||||
})
|
||||
|
||||
it('find spaces', async () => {
|
||||
const conn = connect()
|
||||
const spaces = await conn.findAll(core.class.Space, {})
|
||||
expect(spaces.length).toBe(2)
|
||||
expect(spaces[0].name).toBe('Sp1')
|
||||
expect(spaces[1].name).toBe('Sp2')
|
||||
})
|
||||
|
||||
it('find avg', async () => {
|
||||
const conn = connect()
|
||||
let ops = 0
|
||||
let total = 0
|
||||
const attempts = 1000
|
||||
for (let i = 0; i < attempts; i++) {
|
||||
const st = performance.now()
|
||||
const spaces = await conn.findAll(core.class.Space, {})
|
||||
expect(spaces.length).toBe(2)
|
||||
expect(spaces[0].name).toBe('Sp1')
|
||||
expect(spaces[1].name).toBe('Sp2')
|
||||
const ed = performance.now()
|
||||
ops++
|
||||
total += ed - st
|
||||
}
|
||||
const avg = total / ops
|
||||
// console.log('ops:', ops, 'total:', total, 'avg:', )
|
||||
expect(ops).toEqual(attempts)
|
||||
expect(avg).toBeLessThan(5) // 5ms max per operation
|
||||
})
|
||||
|
||||
it('add space', async () => {
|
||||
const conn = connect()
|
||||
const account = await conn.getAccount()
|
||||
const tx: TxCreateDoc<Space> = {
|
||||
_class: core.class.TxCreateDoc,
|
||||
space: core.space.Tx,
|
||||
_id: generateId(),
|
||||
objectSpace: core.space.Model,
|
||||
modifiedBy: account._id,
|
||||
modifiedOn: Date.now(),
|
||||
attributes: {
|
||||
name: 'Sp3',
|
||||
description: '',
|
||||
private: false,
|
||||
archived: false,
|
||||
members: [],
|
||||
autoJoin: false
|
||||
},
|
||||
objectClass: core.class.Space,
|
||||
objectId: generateId()
|
||||
}
|
||||
await conn.tx(tx)
|
||||
const spaces = await conn.findAll(core.class.Space, {})
|
||||
expect(spaces.length).toBe(3)
|
||||
})
|
||||
|
||||
it('check-model-operations', async () => {
|
||||
const conn = await connectTx()
|
||||
const h = conn.getHierarchy()
|
||||
const domains = h.domains()
|
||||
expect(domains.length).toBe(2)
|
||||
|
||||
expect(h.isDerived(test.class.TestComment, core.class.AttachedDoc)).toBe(true)
|
||||
})
|
||||
})
|
@ -9,6 +9,7 @@ import core, {
|
||||
BrandingMap,
|
||||
Client,
|
||||
ClientConnectEvent,
|
||||
ClientWorkspaceInfo,
|
||||
DocumentUpdate,
|
||||
isActiveMode,
|
||||
isDeletingMode,
|
||||
|
@ -15,6 +15,7 @@ export class SignInJoinPage extends CommonPage {
|
||||
buttonJoin = (): Locator => this.page.locator('button', { hasText: 'Join' })
|
||||
|
||||
async join (data: Pick<SignUpData, 'email' | 'password'>): Promise<void> {
|
||||
await this.buttonJoin().click()
|
||||
await this.inputEmail().fill(data.email)
|
||||
await this.inputPassword().fill(data.password)
|
||||
expect(await this.buttonJoin().isEnabled()).toBe(true)
|
||||
|
@ -17,7 +17,6 @@ export class SignUpPage extends CommonPage {
|
||||
inputNewPassword = (): Locator => this.page.locator('//div[text()="Password"]/../input')
|
||||
inputRepeatPassword = (): Locator => this.page.locator('//div[text()="Repeat password"]/../input')
|
||||
buttonSignUp = (): Locator => this.page.locator('button', { hasText: 'Sign Up' })
|
||||
buttonJoin = (): Locator => this.page.locator('button', { hasText: 'Join' })
|
||||
|
||||
async enterFirstName (firstName: string): Promise<void> {
|
||||
await this.inputFirstName().fill(firstName)
|
||||
@ -43,25 +42,14 @@ export class SignUpPage extends CommonPage {
|
||||
await this.buttonSignUp().click()
|
||||
}
|
||||
|
||||
async signUpPwd (data: SignUpData, mode: 'join' | 'signup' = 'signup'): Promise<void> {
|
||||
const isOtp = await this.signUpPasswordBtn().isVisible()
|
||||
if (isOtp) {
|
||||
await this.signUpPasswordBtn().click()
|
||||
}
|
||||
async signUp (data: SignUpData): Promise<void> {
|
||||
await this.enterFirstName(data.firstName)
|
||||
await this.enterLastName(data.lastName)
|
||||
await this.enterEmail(data.email)
|
||||
await this.enterPassword(data.password)
|
||||
await this.enterRepeatPassword(data.password)
|
||||
switch (mode) {
|
||||
case 'join':
|
||||
expect(await this.buttonJoin().isEnabled()).toBe(true)
|
||||
await this.buttonJoin().click()
|
||||
break
|
||||
case 'signup':
|
||||
expect(await this.buttonSignUp().isEnabled()).toBe(true)
|
||||
await this.buttonSignUp().click()
|
||||
}
|
||||
expect(await this.buttonSignUp().isEnabled()).toBe(true)
|
||||
await this.buttonSignUp().click()
|
||||
}
|
||||
|
||||
async checkIfSignUpButtonIsDisabled (): Promise<void> {
|
||||
|
@ -140,9 +140,8 @@ test.describe('Workspace tests', () => {
|
||||
password: '1234'
|
||||
}
|
||||
|
||||
await page2.getByRole('link', { name: 'Sign Up' }).click()
|
||||
const signUpPage2 = new SignUpPage(page2)
|
||||
await signUpPage2.signUpPwd(newUser2, 'join')
|
||||
await signUpPage2.signUp(newUser2)
|
||||
|
||||
const leftSideMenuPage2 = new LeftSideMenuPage(page2)
|
||||
await leftSideMenuPage2.clickTracker()
|
||||
|
@ -145,9 +145,8 @@ test.describe('Workspace tests', () => {
|
||||
password: '1234'
|
||||
}
|
||||
|
||||
await page2.getByRole('link', { name: 'Sign Up' }).click()
|
||||
const signUpPage2 = new SignUpPage(page2)
|
||||
await signUpPage2.signUpPwd(newUser2, 'join')
|
||||
await signUpPage2.signUp(newUser2)
|
||||
|
||||
const leftSideMenuPage2 = new LeftSideMenuPage(page2)
|
||||
await leftSideMenuPage2.clickTracker()
|
||||
|
Loading…
Reference in New Issue
Block a user