diff --git a/models/core/src/status.ts b/models/core/src/status.ts index 1ec36b8c32..58261593b7 100644 --- a/models/core/src/status.ts +++ b/models/core/src/status.ts @@ -21,7 +21,8 @@ import { type Status, type StatusCategory, type Doc, - type Class + type Class, + type Rank } from '@hcengineering/core' import { Model, Prop, TypeRef, TypeString, UX } from '@hcengineering/model' import { type Asset, type IntlString } from '@hcengineering/platform' @@ -54,7 +55,7 @@ export class TStatus extends TDoc implements Status { @Prop(TypeString(), core.string.Description) description!: string - rank!: string + rank!: Rank } @Model(core.class.StatusCategory, core.class.Doc, DOMAIN_MODEL) diff --git a/models/document/src/migration.ts b/models/document/src/migration.ts index 4abfd77e50..530ea6d631 100644 --- a/models/document/src/migration.ts +++ b/models/document/src/migration.ts @@ -17,6 +17,7 @@ import { type CollaborativeDoc, DOMAIN_TX, MeasureMetricsContext, SortingOrder } import { type DocumentSnapshot, type Document, type Teamspace } from '@hcengineering/document' import { tryMigrate, + migrateSpaceRanks, type MigrateOperation, type MigrateUpdate, type MigrationClient, @@ -270,6 +271,16 @@ async function restoreContentField (client: MigrationClient): Promise { } } +async function migrateRanks (client: MigrationClient): Promise { + const classes = client.hierarchy.getDescendants(document.class.Teamspace) + for (const _class of classes) { + const spaces = await client.find(DOMAIN_SPACE, { _class }) + for (const space of spaces) { + await migrateSpaceRanks(client, DOMAIN_DOCUMENT, space) + } + } +} + export const documentOperation: MigrateOperation = { async migrate (client: MigrationClient): Promise { await tryMigrate(client, documentId, [ @@ -306,6 +317,10 @@ export const documentOperation: MigrateOperation = { { state: 'restoreContentField', func: restoreContentField + }, + { + state: 'migrateRanks', + func: migrateRanks } ]) }, diff --git a/models/task/src/migration.ts b/models/task/src/migration.ts index a9f45ccacc..9d0f50ac8f 100644 --- a/models/task/src/migration.ts +++ b/models/task/src/migration.ts @@ -32,6 +32,7 @@ import { import { createOrUpdate, migrateSpace, + migrateSpaceRanks, tryMigrate, tryUpgrade, type MigrateOperation, @@ -44,6 +45,7 @@ import core, { DOMAIN_SPACE } from '@hcengineering/model-core' import tags from '@hcengineering/model-tags' import { taskId, + type Project, type ProjectStatus, type ProjectType, type ProjectTypeDescriptor, @@ -548,6 +550,16 @@ export async function migrateDefaultStatusesBase ( logger.log('Statuses updated: ', statusIdsBeingMigrated.length) } +async function migrateRanks (client: MigrationClient): Promise { + const classes = client.hierarchy.getDescendants(task.class.Project) + for (const _class of classes) { + const spaces = await client.find(DOMAIN_SPACE, { _class }) + for (const space of spaces) { + await migrateSpaceRanks(client, DOMAIN_TASK, space) + } + } +} + function areSameArrays (arr1: any[] | undefined, arr2: any[] | undefined): boolean { if (arr1 === arr2) { return true @@ -590,6 +602,10 @@ export const taskOperation: MigrateOperation = { func: async (client: MigrationClient): Promise => { await client.update(DOMAIN_TASK, { '%hash%': { $exists: true } }, { $set: { '%hash%': null } }) } + }, + { + state: 'migrateRanks', + func: migrateRanks } ]) }, diff --git a/packages/model/package.json b/packages/model/package.json index 718f46bd79..7d82c1f375 100644 --- a/packages/model/package.json +++ b/packages/model/package.json @@ -42,6 +42,7 @@ "@hcengineering/platform": "^0.6.11", "@hcengineering/storage": "^0.6.0", "@hcengineering/analytics": "^0.6.0", + "@hcengineering/rank": "^0.6.4", "toposort": "^2.0.2", "fast-equals": "^5.0.1" }, diff --git a/packages/model/src/migration.ts b/packages/model/src/migration.ts index 8d0e7ac17d..1159cbeb8c 100644 --- a/packages/model/src/migration.ts +++ b/packages/model/src/migration.ts @@ -15,13 +15,16 @@ import core, { ModelDb, ObjQueryType, PushOptions, + Rank, Ref, + SortingOrder, Space, TxOperations, UnsetOptions, WorkspaceId, generateId } from '@hcengineering/core' +import { makeRank } from '@hcengineering/rank' import { StorageAdapter } from '@hcengineering/storage' import { ModelLogger } from './utils' @@ -252,3 +255,34 @@ export async function migrateSpace ( } await client.update(DOMAIN_TX, { objectSpace: from }, { objectSpace: to }) } + +export async function migrateSpaceRanks (client: MigrationClient, domain: Domain, space: Space): Promise { + type WithRank = Doc & { rank: Rank } + + const iterator = await client.traverse( + 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>, update: MigrateUpdate> }[] = [] + 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() + } +} diff --git a/packages/rank/src/__tests__/utils.test.ts b/packages/rank/src/__tests__/utils.test.ts new file mode 100644 index 0000000000..c48137fe2a --- /dev/null +++ b/packages/rank/src/__tests__/utils.test.ts @@ -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) + }) +}) diff --git a/packages/rank/src/utils.ts b/packages/rank/src/utils.ts index 80f8c06215..99a65a2c2f 100644 --- a/packages/rank/src/utils.ts +++ b/packages/rank/src/utils.ts @@ -34,9 +34,19 @@ export function genRanks (count: number): Rank[] { /** @public */ export function makeRank (prev: Rank | undefined, next: Rank | undefined): Rank { - const prevLexoRank = prev === undefined ? LexoRank.min() : LexoRank.parse(prev) - const nextLexoRank = next === undefined ? LexoRank.max() : LexoRank.parse(next) - return prevLexoRank.equals(nextLexoRank) - ? prevLexoRank.genNext().toString() - : prevLexoRank.between(nextLexoRank).toString() + if (prev !== undefined && next !== undefined) { + const prevLexoRank = LexoRank.parse(prev) + const nextLexoRank = LexoRank.parse(next) + return prevLexoRank.equals(nextLexoRank) + ? 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() + } }