UBERF-6469: Rework workspace creation to more informative (#5291)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2024-04-10 20:33:17 +07:00 committed by GitHub
parent d971c46b28
commit 9419278973
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 344 additions and 93 deletions

View File

@ -35,8 +35,9 @@
</script>
<div class="spinner-container" class:fullSize={!shrink}>
<div data-label={label} class="inner" class:labeled={label !== ''}>
<div data-label={label} class="inner flex-row-center" class:labeled={label !== ''}>
<Spinner {size} />
<slot />
</div>
</div>

View File

@ -124,6 +124,9 @@
<div class="flex flex-col flex-grow">
<span class="label overflow-label flex-center">
{wsName}
{#if workspace.creating === true}
({workspace.createProgress}%)
{/if}
</span>
{#if isAdmin && wsName !== workspace.workspace}
<span class="text-xs flex-center">

View File

@ -17,7 +17,15 @@
import { type IntlString } from '@hcengineering/platform'
import InviteLink from './components/InviteLink.svelte'
import LoginApp from './components/LoginApp.svelte'
import { changePassword, getWorkspaces, leaveWorkspace, selectWorkspace, sendInvite, getEnpoint } from './utils'
import {
changePassword,
getWorkspaces,
leaveWorkspace,
selectWorkspace,
sendInvite,
getEnpoint,
fetchWorkspace
} from './utils'
/*!
* Anticrm Platform Login Plugin
* © 2020, 2021 Anticrm Platform Contributors.
@ -34,6 +42,7 @@ export default async () => ({
LeaveWorkspace: leaveWorkspace,
ChangePassword: changePassword,
SelectWorkspace: selectWorkspace,
FetchWorkspace: fetchWorkspace,
GetWorkspaces: getWorkspaces,
SendInvite: sendInvite,
GetEndpoint: getEnpoint

View File

@ -348,6 +348,55 @@ export async function selectWorkspace (workspace: string): Promise<[Status, Work
}
}
export async function fetchWorkspace (workspace: string): Promise<[Status, WorkspaceLoginInfo | undefined]> {
const accountsUrl = getMetadata(login.metadata.AccountsUrl)
if (accountsUrl === undefined) {
throw new Error('accounts url not specified')
}
const overrideToken = getMetadata(login.metadata.OverrideLoginToken)
const email = fetchMetadataLocalStorage(login.metadata.LoginEmail) ?? ''
if (overrideToken !== undefined) {
const endpoint = getMetadata(login.metadata.OverrideEndpoint)
if (endpoint !== undefined) {
return [OK, { token: overrideToken, endpoint, email, workspace, confirmed: true }]
}
}
const token = getMetadata(presentation.metadata.Token)
if (token === undefined) {
return [unknownStatus('Please login'), undefined]
}
const request = {
method: 'getWorkspaceInfo',
params: [token]
}
try {
const response = await fetch(accountsUrl, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + token,
'Content-Type': 'application/json'
},
body: JSON.stringify(request)
})
const result = await response.json()
if (result.error == null) {
Analytics.handleEvent('Fetch workspace')
Analytics.setTag('workspace', workspace)
} else {
await handleStatusError('Fetch workspace error', result.error)
}
return [result.error ?? OK, result.result]
} catch (err: any) {
Analytics.handleError(err)
return [unknownError(err), undefined]
}
}
export function setLoginInfo (loginInfo: WorkspaceLoginInfo): void {
const tokens: Record<string, string> = fetchMetadataLocalStorage(login.metadata.LoginTokens) ?? {}
tokens[loginInfo.workspace] = loginInfo.token

View File

@ -29,6 +29,9 @@ export interface Workspace {
workspace: string // workspace Url
workspaceName?: string // A company name
workspaceId: string // A unique identifier for the workspace
creating?: boolean
createProgress?: number
}
/**
@ -36,6 +39,8 @@ export interface Workspace {
*/
export interface WorkspaceLoginInfo extends LoginInfo {
workspace: string
creating?: boolean
createProgress?: number
}
/**
@ -76,6 +81,7 @@ export default plugin(loginId, {
LeaveWorkspace: '' as Resource<(email: string) => Promise<void>>,
ChangePassword: '' as Resource<(oldPassword: string, password: string) => Promise<void>>,
SelectWorkspace: '' as Resource<(workspace: string) => Promise<[Status, WorkspaceLoginInfo | undefined]>>,
FetchWorkspace: '' as Resource<(workspace: string) => Promise<[Status, WorkspaceLoginInfo | undefined]>>,
GetWorkspaces: '' as Resource<() => Promise<Workspace[]>>,
GetEndpoint: '' as Resource<() => Promise<string>>
}

View File

@ -30,6 +30,7 @@
"PleaseUpdate": "Please update",
"ServerUnderMaintenance": "Server is under maintenance",
"MobileNotSupported": "Sorry, mobile devices support coming soon. In the meantime, please use Desktop",
"LogInAnyway": "Log in anyway"
"LogInAnyway": "Log in anyway",
"WorkspaceCreating": "Creation in progress..."
}
}

View File

@ -30,6 +30,7 @@
"PleaseUpdate": "Por favor, actualice",
"ServerUnderMaintenance": "El servidor está en mantenimiento",
"MobileNotSupported": "Disculpa, el soporte para dispositivos móviles estará disponible próximamente. Mientras tanto, por favor usa el escritorio.",
"LogInAnyway": "Iniciar sesión de todas formas"
"LogInAnyway": "Iniciar sesión de todas formas",
"WorkspaceCreating": "Creation in progress..."
}
}

View File

@ -30,6 +30,7 @@
"PleaseUpdate": "Atualize",
"ServerUnderMaintenance": "Servidor em manutenção",
"MobileNotSupported": "Desculpe, o suporte para dispositivos móveis estará disponível em breve. Enquanto isso, por favor, use o Desktop.",
"LogInAnyway": "Entrar de qualquer maneira"
"LogInAnyway": "Entrar de qualquer maneira",
"WorkspaceCreating": "Creation in progress..."
}
}

View File

@ -30,6 +30,7 @@
"PleaseUpdate": "Пожалуйста, обновите приложение",
"ServerUnderMaintenance": "Обслуживание сервера",
"MobileNotSupported": "Простите, поддержка мобильных устройств скоро будет доступна. Пока воспользуйтесь компьютером.",
"LogInAnyway": "Все равно войти"
"LogInAnyway": "Все равно войти",
"WorkspaceCreating": "Пространство создается..."
}
}

View File

@ -28,8 +28,9 @@
import { connect, disconnect, versionError } from '../connect'
import { workbenchId } from '@hcengineering/workbench'
import workbench from '../plugin'
import { onDestroy } from 'svelte'
import workbench from '../plugin'
import { workspaceCreating } from '../utils'
const isNeedUpgrade = window.location.host === ''
@ -54,7 +55,14 @@
{:else}
{#key $location.path[1]}
{#await connect(getMetadata(workbench.metadata.PlatformTitle) ?? 'Platform')}
<Loading />
<Loading>
{#if ($workspaceCreating ?? -1) > 0}
<div class="ml-1">
<Label label={workbench.string.WorkspaceCreating} />
{$workspaceCreating} %
</div>
{/if}
</Loading>
{:then client}
{#if !client && versionError}
<div class="version-wrapper">

View File

@ -23,6 +23,7 @@ import {
setMetadataLocalStorage
} from '@hcengineering/ui'
import plugin from './plugin'
import { workspaceCreating } from './utils'
export let versionError: string | undefined = ''
@ -63,6 +64,7 @@ export async function connect (title: string): Promise<Client | undefined> {
}
const tokens: Record<string, string> = fetchMetadataLocalStorage(login.metadata.LoginTokens) ?? {}
let token = tokens[ws]
if (token === undefined && getMetadata(presentation.metadata.Token) !== undefined) {
const selectWorkspace = await getResource(login.function.SelectWorkspace)
const loginInfo = await ctx.with('select-workspace', {}, async () => (await selectWorkspace(ws))[1])
@ -73,6 +75,22 @@ export async function connect (title: string): Promise<Client | undefined> {
}
}
setMetadata(presentation.metadata.Token, token)
const fetchWorkspace = await getResource(login.function.FetchWorkspace)
let loginInfo = await ctx.with('select-workspace', {}, async () => (await fetchWorkspace(ws))[1])
if (loginInfo?.creating === true) {
while (true) {
workspaceCreating.set(loginInfo?.createProgress ?? 0)
loginInfo = await ctx.with('select-workspace', {}, async () => (await fetchWorkspace(ws))[1])
workspaceCreating.set(loginInfo?.createProgress)
if (loginInfo?.creating === false) {
workspaceCreating.set(-1)
break
}
await new Promise<void>((resolve) => setTimeout(resolve, 1000))
}
}
document.cookie =
encodeURIComponent(presentation.metadata.Token.replaceAll(':', '-')) + '=' + encodeURIComponent(token) + '; path=/'

View File

@ -43,7 +43,8 @@ export default mergeIds(workbenchId, workbench, {
NewVersionAvailable: '' as IntlString,
PleaseUpdate: '' as IntlString,
MobileNotSupported: '' as IntlString,
LogInAnyway: '' as IntlString
LogInAnyway: '' as IntlString,
WorkspaceCreating: '' as IntlString
},
metadata: {
MobileAllowed: '' as Metadata<boolean>

View File

@ -33,6 +33,8 @@ import view from '@hcengineering/view'
import workbench, { type Application, type NavigatorModel } from '@hcengineering/workbench'
import { writable } from 'svelte/store'
export const workspaceCreating = writable<number | undefined>(undefined)
export function getSpecialSpaceClass (model: NavigatorModel): Array<Ref<Class<Space>>> {
const spaceResult = model.spaces.map((x) => x.spaceClass)
const result = (model.specials ?? [])

View File

@ -14,7 +14,7 @@
// limitations under the License.
//
import account, { ACCOUNT_DB, type AccountMethod, accountId } from '@hcengineering/account'
import account, { ACCOUNT_DB, type AccountMethod, accountId, cleanInProgressWorkspaces } from '@hcengineering/account'
import accountEn from '@hcengineering/account/lang/en.json'
import accountRu from '@hcengineering/account/lang/ru.json'
import { registerProviders } from '@hcengineering/auth-providers'
@ -93,6 +93,9 @@ export function serveAccount (measureCtx: MeasureContext, methods: Record<string
void client.then((p: MongoClient) => {
const db = p.db(ACCOUNT_DB)
registerProviders(measureCtx, app, router, db, productId, serverSecret, frontURL)
// We need to clean workspace with creating === true, since server is restarted.
void cleanInProgressWorkspaces(db, productId)
})
const extractToken = (header: IncomingHttpHeaders): string | undefined => {

View File

@ -31,6 +31,7 @@ import core, {
generateId,
getWorkspaceId,
MeasureContext,
MeasureMetricsContext,
RateLimiter,
Ref,
systemAccountEmail,
@ -112,6 +113,9 @@ export interface Workspace {
lastVisit: number
createdBy: string
creating?: boolean
createProgress?: number // Some progress
}
/**
@ -129,6 +133,9 @@ export interface LoginInfo {
export interface WorkspaceLoginInfo extends LoginInfo {
workspace: string
productId: string
creating?: boolean
createProgress?: number
}
/**
@ -344,12 +351,14 @@ export async function selectWorkspace (
email,
token: generateToken(email, getWorkspaceId(workspaceInfo.workspace, productId), getExtra(accountInfo)),
workspace: workspaceUrl,
productId
productId,
creating: workspaceInfo.creating,
createProgress: workspaceInfo.createProgress
}
}
if (workspaceInfo !== null) {
if (workspaceInfo.disabled === true) {
if (workspaceInfo.disabled === true && workspaceInfo.creating !== true) {
await ctx.error('workspace disabled', { workspaceUrl, email })
throw new PlatformError(
new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace: workspaceUrl })
@ -364,7 +373,9 @@ export async function selectWorkspace (
email,
token: generateToken(email, getWorkspaceId(workspaceInfo.workspace, productId), getExtra(accountInfo)),
workspace: workspaceUrl,
productId
productId,
creating: workspaceInfo.creating,
createProgress: workspaceInfo.createProgress
}
return result
}
@ -675,6 +686,19 @@ export async function setWorkspaceDisabled (db: Db, workspaceId: Workspace['_id'
await db.collection<Workspace>(WORKSPACE_COLLECTION).updateOne({ _id: workspaceId }, { $set: { disabled } })
}
export async function cleanInProgressWorkspaces (db: Db, productId: string): Promise<void> {
const toDelete = (
await db
.collection<Workspace>(WORKSPACE_COLLECTION)
.find(withProductId(productId, { creating: true }))
.toArray()
).map((it) => ({ ...it, productId }))
const ctx = new MeasureMetricsContext('clean', {})
for (const d of toDelete) {
await dropWorkspace(ctx, db, productId, d.workspace)
}
}
/**
* @public
*/
@ -737,6 +761,8 @@ async function generateWorkspaceRecord (
workspaceName,
accounts: [],
disabled: true,
creating: true,
createProgress: 0,
createdOn: Date.now(),
lastVisit: Date.now(),
createdBy: email
@ -767,6 +793,8 @@ async function generateWorkspaceRecord (
workspaceName,
accounts: [],
disabled: true,
creating: true,
createProgress: 0,
createdOn: Date.now(),
lastVisit: Date.now(),
createdBy: email
@ -808,7 +836,8 @@ export async function createWorkspace (
productId: string,
email: string,
workspaceName: string,
workspace?: string
workspace?: string,
notifyHandler?: (workspace: Workspace) => void
): Promise<{ workspaceInfo: Workspace, err?: any, client?: Client }> {
return await rateLimiter.exec(async () => {
// We need to search for duplicate workspaceUrl
@ -818,6 +847,18 @@ export async function createWorkspace (
searchPromise = generateWorkspaceRecord(db, email, productId, version, workspaceName, workspace)
const workspaceInfo = await searchPromise
notifyHandler?.(workspaceInfo)
const wsColl = db.collection<Omit<Workspace, '_id'>>(WORKSPACE_COLLECTION)
async function updateInfo (ops: Partial<Workspace>): Promise<void> {
await wsColl.updateOne({ _id: workspaceInfo._id }, { $set: ops })
console.log('update', ops)
}
await updateInfo({ createProgress: 10 })
let client: Client | undefined
const childLogger = ctx.newChild(
'createWorkspace',
@ -838,24 +879,54 @@ export async function createWorkspace (
const wsId = getWorkspaceId(workspaceInfo.workspace, productId)
if (initWS !== undefined && (await getWorkspaceById(db, productId, initWS)) !== null) {
// Just any valid model for transactor to be able to function
await initModel(ctx, getTransactor(), wsId, txes, [], ctxModellogger, true)
await (
await initModel(ctx, getTransactor(), wsId, txes, [], ctxModellogger, async (value) => {
await updateInfo({ createProgress: Math.round((Math.min(value, 100) / 100) * 20) })
})
).close()
await updateInfo({ createProgress: 20 })
// Clone init workspace.
await cloneWorkspace(
getTransactor(),
getWorkspaceId(initWS, productId),
getWorkspaceId(workspaceInfo.workspace, productId)
getWorkspaceId(workspaceInfo.workspace, productId),
true,
async (value) => {
await updateInfo({ createProgress: 20 + Math.round((Math.min(value, 100) / 100) * 30) })
}
)
client = await upgradeModel(ctx, getTransactor(), wsId, txes, migrationOperation, ctxModellogger)
await updateInfo({ createProgress: 50 })
client = await upgradeModel(
ctx,
getTransactor(),
wsId,
txes,
migrationOperation,
ctxModellogger,
true,
async (value) => {
await updateInfo({ createProgress: Math.round(50 + (Math.min(value, 100) / 100) * 40) })
}
)
await updateInfo({ createProgress: 90 })
} else {
client = await initModel(ctx, getTransactor(), wsId, txes, migrationOperation, ctxModellogger)
client = await initModel(
ctx,
getTransactor(),
wsId,
txes,
migrationOperation,
ctxModellogger,
async (value) => {
await updateInfo({ createProgress: Math.round(Math.min(value, 100)) })
}
)
}
} catch (err: any) {
return { workspaceInfo, err, client: null as any }
}
// Workspace is created, we need to clear disabled flag.
await db
.collection<Omit<Workspace, '_id'>>(WORKSPACE_COLLECTION)
.updateOne({ _id: workspaceInfo._id }, { $set: { disabled: false } })
await updateInfo({ createProgress: 100, disabled: false, creating: false })
return { workspaceInfo, client }
})
}
@ -901,7 +972,16 @@ export async function upgradeWorkspace (
}
)
await (
await upgradeModel(ctx, getTransactor(), getWorkspaceId(ws.workspace, productId), txes, migrationOperation, logger)
await upgradeModel(
ctx,
getTransactor(),
getWorkspaceId(ws.workspace, productId),
txes,
migrationOperation,
logger,
false,
async (value) => {}
)
).close()
return versionStr
}
@ -933,42 +1013,56 @@ export const createUserWorkspace =
}
}
const { workspaceInfo, err, client } = await createWorkspace(
ctx,
version,
txes,
migrationOperation,
db,
productId,
email,
workspaceName
)
if (err != null) {
await ctx.error('failed to create workspace', { err, workspaceName, email })
// We need to drop workspace, to prevent wrong data usage.
await db.collection(WORKSPACE_COLLECTION).updateOne(
{
_id: workspaceInfo._id
},
{ $set: { disabled: true, message: JSON.stringify(err?.message ?? ''), err: JSON.stringify(err) } }
async function doCreate (info: Account, notifyHandler: (workspace: Workspace) => void): Promise<void> {
const { workspaceInfo, err, client } = await createWorkspace(
ctx,
version,
txes,
migrationOperation,
db,
productId,
email,
workspaceName,
undefined,
notifyHandler
)
throw err
}
try {
info.lastWorkspace = Date.now()
// Update last workspace time.
await db.collection(ACCOUNT_COLLECTION).updateOne({ _id: info._id }, { $set: { lastWorkspace: Date.now() } })
if (err != null) {
await ctx.error('failed to create workspace', { err, workspaceName, email })
// We need to drop workspace, to prevent wrong data usage.
const initWS = getMetadata(toolPlugin.metadata.InitWorkspace)
const shouldUpdateAccount = initWS !== undefined && (await getWorkspaceById(db, productId, initWS)) !== null
await assignWorkspace(ctx, db, productId, email, workspaceInfo.workspace, shouldUpdateAccount, client)
await setRole(email, workspaceInfo.workspace, productId, AccountRole.Owner, client)
} finally {
await client?.close()
await db.collection(WORKSPACE_COLLECTION).updateOne(
{
_id: workspaceInfo._id
},
{ $set: { disabled: true, message: JSON.stringify(err?.message ?? ''), err: JSON.stringify(err) } }
)
throw err
}
try {
info.lastWorkspace = Date.now()
// Update last workspace time.
await db.collection(ACCOUNT_COLLECTION).updateOne({ _id: info._id }, { $set: { lastWorkspace: Date.now() } })
const initWS = getMetadata(toolPlugin.metadata.InitWorkspace)
const shouldUpdateAccount = initWS !== undefined && (await getWorkspaceById(db, productId, initWS)) !== null
await assignWorkspace(ctx, db, productId, email, workspaceInfo.workspace, shouldUpdateAccount, client)
await setRole(email, workspaceInfo.workspace, productId, AccountRole.Owner, client)
await ctx.info('Creating server side done', { workspaceName, email })
} finally {
await client?.close()
}
}
const workspaceInfo = await new Promise<Workspace>((resolve) => {
void doCreate(info, (info: Workspace) => {
resolve(info)
})
})
await assignWorkspaceRaw(db, { account: info, workspace: workspaceInfo })
const result = {
endpoint: getEndpoint(),
email,
@ -976,7 +1070,7 @@ export const createUserWorkspace =
productId,
workspace: workspaceInfo.workspaceUrl
}
await ctx.info('Creating workspace done', { workspaceName, email })
await ctx.info('Creating user side done', { workspaceName, email })
return result
}
@ -1051,7 +1145,7 @@ export async function getUserWorkspaces (
.find(withProductId(productId, account.admin === true ? {} : { _id: { $in: account.workspaces } }))
.toArray()
)
.filter((it) => it.disabled !== true)
.filter((it) => it.disabled !== true || it.creating === true)
.map(mapToClientWorkspace)
}
@ -1094,7 +1188,7 @@ export async function getWorkspaceInfo (
const [ws] = (
await db.collection<Workspace>(WORKSPACE_COLLECTION).find(withProductId(productId, query)).toArray()
).filter((it) => it.disabled !== true || account?.admin === true)
).filter((it) => it.disabled !== true || account?.admin === true || it.creating === true)
if (ws == null) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
}
@ -1198,6 +1292,13 @@ export async function assignWorkspace (
}
// Add account into workspace.
await assignWorkspaceRaw(db, workspaceInfo)
await ctx.info('assign-workspace success', { email, workspaceId })
return workspaceInfo.workspace
}
async function assignWorkspaceRaw (db: Db, workspaceInfo: { account: Account, workspace: Workspace }): Promise<void> {
await db
.collection(WORKSPACE_COLLECTION)
.updateOne({ _id: workspaceInfo.workspace._id }, { $addToSet: { accounts: workspaceInfo.account._id } })
@ -1206,9 +1307,6 @@ export async function assignWorkspace (
await db
.collection(ACCOUNT_COLLECTION)
.updateOne({ _id: workspaceInfo.account._id }, { $addToSet: { workspaces: workspaceInfo.workspace._id } })
await ctx.info('assign-workspace success', { email, workspaceId })
return workspaceInfo.workspace
}
async function createEmployee (ops: TxOperations, name: string, _email: string): Promise<Ref<Person>> {

View File

@ -206,7 +206,8 @@ export async function cloneWorkspace (
transactorUrl: string,
sourceWorkspaceId: WorkspaceId,
targetWorkspaceId: WorkspaceId,
clearTime: boolean = true
clearTime: boolean = true,
progress: (value: number) => Promise<void>
): Promise<void> {
const sourceConnection = (await connect(transactorUrl, sourceWorkspaceId, undefined, {
mode: 'backup'
@ -220,6 +221,7 @@ export async function cloneWorkspace (
.domains()
.filter((it) => it !== DOMAIN_TRANSIENT && it !== DOMAIN_MODEL)
let i = 0
for (const c of domains) {
console.log('clone domain...', c)
@ -322,6 +324,9 @@ export async function cloneWorkspace (
continue
}
}
i++
await progress((100 / domains.length) * i)
}
} catch (err: any) {
console.error(err)

View File

@ -112,8 +112,8 @@ export async function initModel (
rawTxes: Tx[],
migrateOperations: [string, MigrateOperation][],
logger: ModelLogger = consoleModelLogger,
skipOperations: boolean = false
): Promise<CoreClient | undefined> {
progress: (value: number) => Promise<void>
): Promise<CoreClient> {
const { mongodbUri, storageAdapter: minio, txes } = prepareTools(rawTxes)
if (txes.some((tx) => tx.objectSpace !== core.space.Model)) {
throw Error('Model txes must target only core.space.Model')
@ -129,39 +129,48 @@ export async function initModel (
const result = await db.collection(DOMAIN_TX).insertMany(txes as Document[])
logger.log('model transactions inserted.', { count: result.insertedCount })
await progress(10)
logger.log('creating data...', { transactorUrl })
const { model } = await fetchModelFromMongo(ctx, mongodbUri, workspaceId)
await progress(20)
logger.log('create minio bucket', { workspaceId })
if (!(await minio.exists(ctx, workspaceId))) {
await minio.make(ctx, workspaceId)
}
if (!skipOperations) {
connection = (await connect(
transactorUrl,
workspaceId,
undefined,
{
model: 'upgrade',
admin: 'true'
},
model
)) as unknown as CoreClient & BackupClient
connection = (await connect(
transactorUrl,
workspaceId,
undefined,
{
model: 'upgrade',
admin: 'true'
},
model
)) as unknown as CoreClient & BackupClient
try {
for (const op of migrateOperations) {
logger.log('Migrate', { name: op[0] })
await op[1].upgrade(connection, logger)
}
// Create update indexes
await createUpdateIndexes(ctx, connection, db, logger)
} catch (e: any) {
logger.error('error', { error: e })
throw e
try {
let i = 0
for (const op of migrateOperations) {
logger.log('Migrate', { name: op[0] })
await op[1].upgrade(connection, logger)
i++
await progress(20 + (((100 / migrateOperations.length) * i) / 100) * 10)
}
return connection
await progress(30)
// Create update indexes
await createUpdateIndexes(ctx, connection, db, logger, async (value) => {
await progress(30 + (Math.min(value, 100) / 100) * 70)
})
await progress(100)
} catch (e: any) {
logger.error('error', { error: e })
throw e
}
return connection
} finally {
_client.close()
}
@ -177,7 +186,8 @@ export async function upgradeModel (
rawTxes: Tx[],
migrateOperations: [string, MigrateOperation][],
logger: ModelLogger = consoleModelLogger,
skipTxUpdate: boolean = false
skipTxUpdate: boolean = false,
progress: (value: number) => Promise<void>
): Promise<CoreClient> {
const { mongodbUri, txes } = prepareTools(rawTxes)
@ -193,6 +203,7 @@ export async function upgradeModel (
if (!skipTxUpdate) {
logger.log('removing model...', { workspaceId: workspaceId.name })
await progress(10)
// we're preserving accounts (created by core.account.System).
const result = await ctx.with(
'mongo-delete',
@ -214,16 +225,20 @@ export async function upgradeModel (
)
logger.log('model transactions inserted.', { workspaceId: workspaceId.name, count: insert.insertedCount })
await progress(20)
}
const { hierarchy, modelDb, model } = await fetchModelFromMongo(ctx, mongodbUri, workspaceId)
await ctx.with('migrate', {}, async () => {
const migrateClient = new MigrateClientImpl(db, hierarchy, modelDb, logger)
let i = 0
for (const op of migrateOperations) {
const t = Date.now()
await op[1].migrate(migrateClient, logger)
logger.log('migrate:', { workspaceId: workspaceId.name, operation: op[0], time: Date.now() - t })
await progress(20 + ((100 / migrateOperations.length) * i * 20) / 100)
i++
}
})
logger.log('Apply upgrade operations', { workspaceId: workspaceId.name })
@ -245,16 +260,23 @@ export async function upgradeModel (
)
)
// Create update indexes
await ctx.with('create-indexes', {}, async (ctx) => {
await createUpdateIndexes(ctx, connection, db, logger)
})
if (!skipTxUpdate) {
// Create update indexes
await ctx.with('create-indexes', {}, async (ctx) => {
await createUpdateIndexes(ctx, connection, db, logger, async (value) => {
await progress(40 + (Math.min(value, 100) / 100) * 20)
})
})
}
await ctx.with('upgrade', {}, async () => {
let i = 0
for (const op of migrateOperations) {
const t = Date.now()
await op[1].upgrade(connection, logger)
logger.log('upgrade:', { operation: op[0], time: Date.now() - t, workspaceId: workspaceId.name })
await progress(60 + ((100 / migrateOperations.length) * i * 40) / 100)
i++
}
})
return connection
@ -295,7 +317,8 @@ async function createUpdateIndexes (
ctx: MeasureContext,
connection: CoreClient,
db: Db,
logger: ModelLogger
logger: ModelLogger,
progress: (value: number) => Promise<void>
): Promise<void> {
const classes = await ctx.with('find-classes', {}, async () => await connection.findAll(core.class.Class, {}))
@ -339,6 +362,8 @@ async function createUpdateIndexes (
{},
async () => await db.listCollections({}, { nameOnly: true }).toArray()
)
const promises: Promise<void>[] = []
let completed = 0
for (const [d, v] of domains.entries()) {
const collInfo = collections.find((it) => it.name === d)
if (collInfo == null) {
@ -352,13 +377,25 @@ async function createUpdateIndexes (
const name = typeof vv === 'string' ? `${key}_1` : `${key}_${vv[key]}`
const exists = await collection.indexExists(name)
if (!exists) {
await collection.createIndex(vv)
bb.push(vv)
}
} catch (err: any) {
logger.error('error: failed to create index', { d, vv, err })
}
}
for (const vv of bb) {
promises.push(
collection
.createIndex(vv, {
background: true
})
.then(async () => {
completed++
await progress((100 / bb.length) * completed)
})
)
}
await Promise.all(promises)
if (bb.length > 0) {
logger.log('created indexes', { d, bb })
}

View File

@ -17,6 +17,7 @@ import core, {
TxFactory,
WorkspaceEvent,
generateId,
systemAccountEmail,
toWorkspaceString,
type MeasureContext,
type Ref,
@ -183,6 +184,7 @@ class TSessionManager implements SessionManager {
workspace: string
workspaceUrl?: string | null
workspaceName?: string
creating?: boolean
}> {
const userInfo = await (
await fetch(accounts, {
@ -219,6 +221,10 @@ class TSessionManager implements SessionManager {
let workspaceInfo = await ctx.with('check-token', {}, async (ctx) =>
accountsUrl !== '' ? await this.getWorkspaceInfo(accountsUrl, rawToken) : this.wsFromToken(token)
)
if (workspaceInfo?.creating === true && token.email !== systemAccountEmail) {
// No access to workspace for token.
return { error: new Error(`Workspace during creation phase ${token.email} ${token.workspace.name}`) }
}
if (workspaceInfo === undefined && token.extra?.admin !== 'true') {
// No access to workspace for token.
return { error: new Error(`No access to workspace for token ${token.email} ${token.workspace.name}`) }
@ -307,6 +313,7 @@ class TSessionManager implements SessionManager {
workspace: string
workspaceUrl?: string | null
workspaceName?: string
creating?: boolean
} {
return {
workspace: token.workspace.name,