mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-18 22:38:33 +00:00
UBER-734 Rework makeRank and migrate ranks (#7104)
Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
parent
585b3f40e7
commit
e3353df572
@ -21,7 +21,8 @@ import {
|
|||||||
type Status,
|
type Status,
|
||||||
type StatusCategory,
|
type StatusCategory,
|
||||||
type Doc,
|
type Doc,
|
||||||
type Class
|
type Class,
|
||||||
|
type Rank
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import { Model, Prop, TypeRef, TypeString, UX } from '@hcengineering/model'
|
import { Model, Prop, TypeRef, TypeString, UX } from '@hcengineering/model'
|
||||||
import { type Asset, type IntlString } from '@hcengineering/platform'
|
import { type Asset, type IntlString } from '@hcengineering/platform'
|
||||||
@ -54,7 +55,7 @@ export class TStatus extends TDoc implements Status {
|
|||||||
@Prop(TypeString(), core.string.Description)
|
@Prop(TypeString(), core.string.Description)
|
||||||
description!: string
|
description!: string
|
||||||
|
|
||||||
rank!: string
|
rank!: Rank
|
||||||
}
|
}
|
||||||
|
|
||||||
@Model(core.class.StatusCategory, core.class.Doc, DOMAIN_MODEL)
|
@Model(core.class.StatusCategory, core.class.Doc, DOMAIN_MODEL)
|
||||||
|
@ -17,6 +17,7 @@ import { type CollaborativeDoc, DOMAIN_TX, MeasureMetricsContext, SortingOrder }
|
|||||||
import { type DocumentSnapshot, type Document, type Teamspace } from '@hcengineering/document'
|
import { type DocumentSnapshot, type Document, type Teamspace } from '@hcengineering/document'
|
||||||
import {
|
import {
|
||||||
tryMigrate,
|
tryMigrate,
|
||||||
|
migrateSpaceRanks,
|
||||||
type MigrateOperation,
|
type MigrateOperation,
|
||||||
type MigrateUpdate,
|
type MigrateUpdate,
|
||||||
type MigrationClient,
|
type MigrationClient,
|
||||||
@ -270,6 +271,16 @@ async function restoreContentField (client: MigrationClient): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function migrateRanks (client: MigrationClient): Promise<void> {
|
||||||
|
const classes = client.hierarchy.getDescendants(document.class.Teamspace)
|
||||||
|
for (const _class of classes) {
|
||||||
|
const spaces = await client.find<Teamspace>(DOMAIN_SPACE, { _class })
|
||||||
|
for (const space of spaces) {
|
||||||
|
await migrateSpaceRanks(client, DOMAIN_DOCUMENT, space)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const documentOperation: MigrateOperation = {
|
export const documentOperation: MigrateOperation = {
|
||||||
async migrate (client: MigrationClient): Promise<void> {
|
async migrate (client: MigrationClient): Promise<void> {
|
||||||
await tryMigrate(client, documentId, [
|
await tryMigrate(client, documentId, [
|
||||||
@ -306,6 +317,10 @@ export const documentOperation: MigrateOperation = {
|
|||||||
{
|
{
|
||||||
state: 'restoreContentField',
|
state: 'restoreContentField',
|
||||||
func: restoreContentField
|
func: restoreContentField
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: 'migrateRanks',
|
||||||
|
func: migrateRanks
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
},
|
},
|
||||||
|
@ -32,6 +32,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
createOrUpdate,
|
createOrUpdate,
|
||||||
migrateSpace,
|
migrateSpace,
|
||||||
|
migrateSpaceRanks,
|
||||||
tryMigrate,
|
tryMigrate,
|
||||||
tryUpgrade,
|
tryUpgrade,
|
||||||
type MigrateOperation,
|
type MigrateOperation,
|
||||||
@ -44,6 +45,7 @@ import core, { DOMAIN_SPACE } from '@hcengineering/model-core'
|
|||||||
import tags from '@hcengineering/model-tags'
|
import tags from '@hcengineering/model-tags'
|
||||||
import {
|
import {
|
||||||
taskId,
|
taskId,
|
||||||
|
type Project,
|
||||||
type ProjectStatus,
|
type ProjectStatus,
|
||||||
type ProjectType,
|
type ProjectType,
|
||||||
type ProjectTypeDescriptor,
|
type ProjectTypeDescriptor,
|
||||||
@ -548,6 +550,16 @@ export async function migrateDefaultStatusesBase<T extends Task> (
|
|||||||
logger.log('Statuses updated: ', statusIdsBeingMigrated.length)
|
logger.log('Statuses updated: ', statusIdsBeingMigrated.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function migrateRanks (client: MigrationClient): Promise<void> {
|
||||||
|
const classes = client.hierarchy.getDescendants(task.class.Project)
|
||||||
|
for (const _class of classes) {
|
||||||
|
const spaces = await client.find<Project>(DOMAIN_SPACE, { _class })
|
||||||
|
for (const space of spaces) {
|
||||||
|
await migrateSpaceRanks(client, DOMAIN_TASK, space)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function areSameArrays (arr1: any[] | undefined, arr2: any[] | undefined): boolean {
|
function areSameArrays (arr1: any[] | undefined, arr2: any[] | undefined): boolean {
|
||||||
if (arr1 === arr2) {
|
if (arr1 === arr2) {
|
||||||
return true
|
return true
|
||||||
@ -590,6 +602,10 @@ export const taskOperation: MigrateOperation = {
|
|||||||
func: async (client: MigrationClient): Promise<void> => {
|
func: async (client: MigrationClient): Promise<void> => {
|
||||||
await client.update(DOMAIN_TASK, { '%hash%': { $exists: true } }, { $set: { '%hash%': null } })
|
await client.update(DOMAIN_TASK, { '%hash%': { $exists: true } }, { $set: { '%hash%': null } })
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: 'migrateRanks',
|
||||||
|
func: migrateRanks
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
},
|
},
|
||||||
|
@ -42,6 +42,7 @@
|
|||||||
"@hcengineering/platform": "^0.6.11",
|
"@hcengineering/platform": "^0.6.11",
|
||||||
"@hcengineering/storage": "^0.6.0",
|
"@hcengineering/storage": "^0.6.0",
|
||||||
"@hcengineering/analytics": "^0.6.0",
|
"@hcengineering/analytics": "^0.6.0",
|
||||||
|
"@hcengineering/rank": "^0.6.4",
|
||||||
"toposort": "^2.0.2",
|
"toposort": "^2.0.2",
|
||||||
"fast-equals": "^5.0.1"
|
"fast-equals": "^5.0.1"
|
||||||
},
|
},
|
||||||
|
@ -15,13 +15,16 @@ import core, {
|
|||||||
ModelDb,
|
ModelDb,
|
||||||
ObjQueryType,
|
ObjQueryType,
|
||||||
PushOptions,
|
PushOptions,
|
||||||
|
Rank,
|
||||||
Ref,
|
Ref,
|
||||||
|
SortingOrder,
|
||||||
Space,
|
Space,
|
||||||
TxOperations,
|
TxOperations,
|
||||||
UnsetOptions,
|
UnsetOptions,
|
||||||
WorkspaceId,
|
WorkspaceId,
|
||||||
generateId
|
generateId
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
|
import { makeRank } from '@hcengineering/rank'
|
||||||
import { StorageAdapter } from '@hcengineering/storage'
|
import { StorageAdapter } from '@hcengineering/storage'
|
||||||
import { ModelLogger } from './utils'
|
import { ModelLogger } from './utils'
|
||||||
|
|
||||||
@ -252,3 +255,34 @@ export async function migrateSpace (
|
|||||||
}
|
}
|
||||||
await client.update(DOMAIN_TX, { objectSpace: from }, { objectSpace: to })
|
await client.update(DOMAIN_TX, { objectSpace: from }, { objectSpace: to })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function migrateSpaceRanks (client: MigrationClient, domain: Domain, space: Space): Promise<void> {
|
||||||
|
type WithRank = Doc & { rank: Rank }
|
||||||
|
|
||||||
|
const iterator = await client.traverse<WithRank>(
|
||||||
|
domain,
|
||||||
|
{ space: space._id, rank: { $exists: true } },
|
||||||
|
{ sort: { rank: SortingOrder.Ascending } }
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
let rank = '0|100000:'
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const docs = await iterator.next(1000)
|
||||||
|
if (docs === null || docs.length === 0) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
const updates: { filter: MigrationDocumentQuery<Doc<Space>>, update: MigrateUpdate<Doc<Space>> }[] = []
|
||||||
|
for (const doc of docs) {
|
||||||
|
rank = makeRank(rank, undefined)
|
||||||
|
updates.push({ filter: { _id: doc._id }, update: { rank } })
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.bulk(domain, updates)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await iterator.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
91
packages/rank/src/__tests__/utils.test.ts
Normal file
91
packages/rank/src/__tests__/utils.test.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2024 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 { makeRank } from '..'
|
||||||
|
|
||||||
|
describe('makeRank', () => {
|
||||||
|
it('calculates rank when no prev and next', () => {
|
||||||
|
expect(makeRank(undefined, undefined)).toBe('0|hzzzzz:')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['0|hzzzzz:', '0|i00007:'],
|
||||||
|
['0|i00007:', '0|i0000f:'],
|
||||||
|
['0|i0000f:', '0|i0000n:'],
|
||||||
|
['0|zzzzzz:', '0|zzzzzz:']
|
||||||
|
])('calculates rank value when prev is %p', (prev, expected) => {
|
||||||
|
expect(makeRank(prev, undefined)).toBe(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['0|hzzzzz:', '0|hzzzzr:'],
|
||||||
|
['0|hzzzzr:', '0|hzzzzj:'],
|
||||||
|
['0|hzzzzj:', '0|hzzzzb:'],
|
||||||
|
['0|000000:', '0|000000:']
|
||||||
|
])('calculates rank value when next is %p', (next, expected) => {
|
||||||
|
expect(makeRank(undefined, next)).toBe(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['0|000000:', '0|000001:', '0|000000:i'],
|
||||||
|
['0|hzzzzz:', '0|i0000f:', '0|i00007:'],
|
||||||
|
['0|hzzzzz:', '0|hzzzzz:', '0|i00007:'],
|
||||||
|
['0|i00007:', '0|i00007:', '0|i0000f:'],
|
||||||
|
['0|i00007:', '0|i00008:', '0|i00007:i']
|
||||||
|
])('calculates rank value when prev is %p and next is %p', (prev, next, expected) => {
|
||||||
|
expect(makeRank(prev, next)).toBe(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[10, '0|hzzzxr:'],
|
||||||
|
[100, '0|hzzzdr:'],
|
||||||
|
[1000, '0|hzzttr:'],
|
||||||
|
[10000, '0|hzya9r:']
|
||||||
|
])('produces prev rank of reasonable length for %p generations', (count, expected) => {
|
||||||
|
let rank = '0|hzzzzz:'
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
rank = makeRank(undefined, rank)
|
||||||
|
}
|
||||||
|
expect(rank).toBe(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[5, '0|zfqzzz:'],
|
||||||
|
[10, '0|zzd7vh:'],
|
||||||
|
[50, '0|zzzzzy:zzzi'],
|
||||||
|
[100, '0|zzzzzy:zzzzzzzzzzzv']
|
||||||
|
])('produces middle rank of reasonable length for %p generations', (count, expected) => {
|
||||||
|
let rank = '0|hzzzzz:'
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
rank = makeRank(rank, '0|zzzzzz')
|
||||||
|
}
|
||||||
|
expect(rank).toBe(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[10, '0|i00027:'],
|
||||||
|
[100, '0|i000m7:'],
|
||||||
|
[1000, '0|i00667:'],
|
||||||
|
[10000, '0|i01pq7:'],
|
||||||
|
[100000, '0|i0h5a7:'],
|
||||||
|
[1000000, '0|i4rgu7:']
|
||||||
|
])('produces next rank of reasonable length for %p generations', (count, expected) => {
|
||||||
|
let rank = '0|hzzzzz:'
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
rank = makeRank(rank, undefined)
|
||||||
|
}
|
||||||
|
expect(rank).toBe(expected)
|
||||||
|
})
|
||||||
|
})
|
@ -34,9 +34,19 @@ export function genRanks (count: number): Rank[] {
|
|||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function makeRank (prev: Rank | undefined, next: Rank | undefined): Rank {
|
export function makeRank (prev: Rank | undefined, next: Rank | undefined): Rank {
|
||||||
const prevLexoRank = prev === undefined ? LexoRank.min() : LexoRank.parse(prev)
|
if (prev !== undefined && next !== undefined) {
|
||||||
const nextLexoRank = next === undefined ? LexoRank.max() : LexoRank.parse(next)
|
const prevLexoRank = LexoRank.parse(prev)
|
||||||
return prevLexoRank.equals(nextLexoRank)
|
const nextLexoRank = LexoRank.parse(next)
|
||||||
? prevLexoRank.genNext().toString()
|
return prevLexoRank.equals(nextLexoRank)
|
||||||
: prevLexoRank.between(nextLexoRank).toString()
|
? prevLexoRank.genNext().toString()
|
||||||
|
: prevLexoRank.between(nextLexoRank).toString()
|
||||||
|
} else if (prev !== undefined) {
|
||||||
|
const prevLexoRank = LexoRank.parse(prev)
|
||||||
|
return prevLexoRank.genNext().toString()
|
||||||
|
} else if (next !== undefined) {
|
||||||
|
const nextLexoRank = LexoRank.parse(next)
|
||||||
|
return nextLexoRank.genPrev().toString()
|
||||||
|
} else {
|
||||||
|
return LexoRank.middle().toString()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user