2022-03-31 08:32:42 +00:00
|
|
|
//
|
2022-04-16 02:59:50 +00:00
|
|
|
// Copyright © 2022 Hardcore Engineering Inc.
|
2022-03-31 08:32:42 +00:00
|
|
|
//
|
|
|
|
// 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.
|
|
|
|
//
|
|
|
|
|
2023-02-10 01:45:22 +00:00
|
|
|
import core, {
|
2023-03-17 06:17:53 +00:00
|
|
|
Class,
|
2023-02-10 01:45:22 +00:00
|
|
|
Doc,
|
|
|
|
DocumentUpdate,
|
|
|
|
DOMAIN_TX,
|
|
|
|
generateId,
|
|
|
|
Ref,
|
|
|
|
SortingOrder,
|
2023-04-04 06:11:49 +00:00
|
|
|
StatusCategory,
|
2023-03-18 17:14:49 +00:00
|
|
|
TxCollectionCUD,
|
2023-03-17 06:17:53 +00:00
|
|
|
TxCreateDoc,
|
2023-02-10 01:45:22 +00:00
|
|
|
TxOperations,
|
2023-03-17 06:17:53 +00:00
|
|
|
TxResult,
|
2023-04-04 06:11:49 +00:00
|
|
|
TxUpdateDoc,
|
|
|
|
DOMAIN_STATUS
|
2023-02-10 01:45:22 +00:00
|
|
|
} from '@hcengineering/core'
|
2022-09-21 08:08:25 +00:00
|
|
|
import { createOrUpdate, MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model'
|
2023-03-17 06:17:53 +00:00
|
|
|
import { DOMAIN_SPACE } from '@hcengineering/model-core'
|
2023-02-09 16:50:30 +00:00
|
|
|
import tags from '@hcengineering/tags'
|
2022-12-07 09:40:19 +00:00
|
|
|
import {
|
2023-02-10 01:45:22 +00:00
|
|
|
calcRank,
|
2023-02-09 16:50:30 +00:00
|
|
|
genRanks,
|
|
|
|
Issue,
|
2022-12-07 09:40:19 +00:00
|
|
|
IssueStatus,
|
2023-03-17 06:17:53 +00:00
|
|
|
IssueTemplate,
|
|
|
|
IssueTemplateChild,
|
|
|
|
Project,
|
2023-05-16 11:56:29 +00:00
|
|
|
Milestone,
|
|
|
|
MilestoneStatus,
|
2023-03-22 10:08:19 +00:00
|
|
|
TimeReportDayType
|
2022-12-07 09:40:19 +00:00
|
|
|
} from '@hcengineering/tracker'
|
2022-05-16 10:38:51 +00:00
|
|
|
import { DOMAIN_TRACKER } from '.'
|
2022-04-16 02:59:50 +00:00
|
|
|
import tracker from './plugin'
|
|
|
|
|
2022-04-23 16:59:57 +00:00
|
|
|
enum DeprecatedIssueStatus {
|
|
|
|
Backlog,
|
|
|
|
Todo,
|
|
|
|
InProgress,
|
|
|
|
Done,
|
|
|
|
Canceled
|
|
|
|
}
|
|
|
|
|
2023-03-17 06:17:53 +00:00
|
|
|
interface CreateProjectIssueStatusesArgs {
|
2022-04-23 16:59:57 +00:00
|
|
|
tx: TxOperations
|
2023-03-17 06:17:53 +00:00
|
|
|
projectId: Ref<Project>
|
2023-04-04 06:11:49 +00:00
|
|
|
categories: StatusCategory[]
|
2022-04-23 16:59:57 +00:00
|
|
|
defaultStatusId?: Ref<IssueStatus>
|
2023-04-04 06:11:49 +00:00
|
|
|
defaultCategoryId?: Ref<StatusCategory>
|
2022-04-23 16:59:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const categoryByDeprecatedIssueStatus = {
|
|
|
|
[DeprecatedIssueStatus.Backlog]: tracker.issueStatusCategory.Backlog,
|
|
|
|
[DeprecatedIssueStatus.Todo]: tracker.issueStatusCategory.Unstarted,
|
|
|
|
[DeprecatedIssueStatus.InProgress]: tracker.issueStatusCategory.Started,
|
|
|
|
[DeprecatedIssueStatus.Done]: tracker.issueStatusCategory.Completed,
|
|
|
|
[DeprecatedIssueStatus.Canceled]: tracker.issueStatusCategory.Canceled
|
|
|
|
} as const
|
|
|
|
|
2023-03-17 06:17:53 +00:00
|
|
|
async function createProjectIssueStatuses ({
|
2022-04-23 16:59:57 +00:00
|
|
|
tx,
|
2023-03-17 06:17:53 +00:00
|
|
|
projectId: attachedTo,
|
2022-04-23 16:59:57 +00:00
|
|
|
categories,
|
|
|
|
defaultStatusId,
|
|
|
|
defaultCategoryId = tracker.issueStatusCategory.Backlog
|
2023-03-17 06:17:53 +00:00
|
|
|
}: CreateProjectIssueStatusesArgs): Promise<void> {
|
2022-04-23 16:59:57 +00:00
|
|
|
const issueStatusRanks = [...genRanks(categories.length)]
|
|
|
|
|
|
|
|
for (const [i, statusCategory] of categories.entries()) {
|
|
|
|
const { _id: category, defaultStatusName } = statusCategory
|
|
|
|
const rank = issueStatusRanks[i]
|
|
|
|
|
2023-04-04 06:11:49 +00:00
|
|
|
if (defaultStatusName !== undefined) {
|
|
|
|
await tx.createDoc(
|
|
|
|
tracker.class.IssueStatus,
|
|
|
|
attachedTo,
|
|
|
|
{ ofAttribute: tracker.attribute.IssueStatus, name: defaultStatusName, category, rank },
|
|
|
|
category === defaultCategoryId ? defaultStatusId : undefined
|
|
|
|
)
|
|
|
|
}
|
2022-04-23 16:59:57 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-17 06:17:53 +00:00
|
|
|
async function createDefaultProject (tx: TxOperations): Promise<void> {
|
|
|
|
const current = await tx.findOne(tracker.class.Project, {
|
|
|
|
_id: tracker.project.DefaultProject
|
2022-04-16 02:59:50 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
const currentDeleted = await tx.findOne(core.class.TxRemoveDoc, {
|
2023-03-17 06:17:53 +00:00
|
|
|
objectId: tracker.project.DefaultProject
|
2022-04-16 02:59:50 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
// Create new if not deleted by customers.
|
|
|
|
if (current === undefined && currentDeleted === undefined) {
|
2022-04-23 16:59:57 +00:00
|
|
|
const defaultStatusId: Ref<IssueStatus> = generateId()
|
2023-04-04 06:11:49 +00:00
|
|
|
const categories = await tx.findAll(core.class.StatusCategory, {}, { sort: { order: SortingOrder.Ascending } })
|
2022-04-23 16:59:57 +00:00
|
|
|
|
2023-03-17 06:17:53 +00:00
|
|
|
await tx.createDoc<Project>(
|
|
|
|
tracker.class.Project,
|
2022-04-16 02:59:50 +00:00
|
|
|
core.space.Space,
|
|
|
|
{
|
|
|
|
name: 'Default',
|
2023-03-17 06:17:53 +00:00
|
|
|
description: 'Default project',
|
2022-04-16 02:59:50 +00:00
|
|
|
private: false,
|
|
|
|
members: [],
|
|
|
|
archived: false,
|
|
|
|
identifier: 'TSK',
|
2022-04-23 16:59:57 +00:00
|
|
|
sequence: 0,
|
|
|
|
issueStatuses: 0,
|
2022-12-07 09:40:19 +00:00
|
|
|
defaultIssueStatus: defaultStatusId,
|
|
|
|
defaultTimeReportDay: TimeReportDayType.PreviousWorkDay,
|
2023-03-22 10:08:19 +00:00
|
|
|
defaultAssignee: undefined
|
2022-04-16 02:59:50 +00:00
|
|
|
},
|
2023-03-17 06:17:53 +00:00
|
|
|
tracker.project.DefaultProject
|
2022-04-16 02:59:50 +00:00
|
|
|
)
|
2023-03-17 06:17:53 +00:00
|
|
|
await createProjectIssueStatuses({ tx, projectId: tracker.project.DefaultProject, categories, defaultStatusId })
|
2022-04-23 16:59:57 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-17 06:17:53 +00:00
|
|
|
async function fixProjectIssueStatusesOrder (tx: TxOperations, project: Project): Promise<TxResult> {
|
2022-06-08 15:49:38 +00:00
|
|
|
const statuses = await tx.findAll(
|
|
|
|
tracker.class.IssueStatus,
|
2023-03-17 06:17:53 +00:00
|
|
|
{ attachedTo: project._id },
|
2023-04-04 06:11:49 +00:00
|
|
|
{ lookup: { category: core.class.StatusCategory } }
|
2022-06-08 15:49:38 +00:00
|
|
|
)
|
|
|
|
statuses.sort((a, b) => (a.$lookup?.category?.order ?? 0) - (b.$lookup?.category?.order ?? 0))
|
|
|
|
const issueStatusRanks = genRanks(statuses.length)
|
|
|
|
return statuses.map((status) => {
|
|
|
|
const rank = issueStatusRanks.next().value
|
|
|
|
if (rank === undefined || status.rank === rank) return undefined
|
|
|
|
return tx.update(status, { rank })
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-03-17 06:17:53 +00:00
|
|
|
async function fixProjectsIssueStatusesOrder (tx: TxOperations): Promise<void> {
|
|
|
|
const projects = await tx.findAll(tracker.class.Project, {})
|
|
|
|
await Promise.all(projects.map((project) => fixProjectIssueStatusesOrder(tx, project)))
|
2022-06-08 15:49:38 +00:00
|
|
|
}
|
|
|
|
|
2023-03-17 06:17:53 +00:00
|
|
|
async function upgradeProjectSettings (tx: TxOperations): Promise<void> {
|
|
|
|
const projects = await tx.findAll(tracker.class.Project, {
|
2023-03-22 10:08:19 +00:00
|
|
|
defaultTimeReportDay: { $exists: false }
|
2022-12-07 09:40:19 +00:00
|
|
|
})
|
|
|
|
await Promise.all(
|
2023-03-17 06:17:53 +00:00
|
|
|
projects.map((project) =>
|
|
|
|
tx.update(project, {
|
2023-03-22 10:08:19 +00:00
|
|
|
defaultTimeReportDay: TimeReportDayType.PreviousWorkDay
|
2022-12-07 09:40:19 +00:00
|
|
|
})
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-03-17 06:17:53 +00:00
|
|
|
async function upgradeProjectIssueStatuses (tx: TxOperations): Promise<void> {
|
|
|
|
const projects = await tx.findAll(tracker.class.Project, { issueStatuses: undefined })
|
2022-04-23 16:59:57 +00:00
|
|
|
|
2023-03-17 06:17:53 +00:00
|
|
|
if (projects.length > 0) {
|
2023-04-04 06:11:49 +00:00
|
|
|
const categories = await tx.findAll(core.class.StatusCategory, {}, { sort: { order: SortingOrder.Ascending } })
|
2022-04-23 16:59:57 +00:00
|
|
|
|
2023-03-17 06:17:53 +00:00
|
|
|
for (const project of projects) {
|
2022-04-23 16:59:57 +00:00
|
|
|
const defaultStatusId: Ref<IssueStatus> = generateId()
|
|
|
|
|
2023-03-17 06:17:53 +00:00
|
|
|
await tx.update(project, { issueStatuses: 0, defaultIssueStatus: defaultStatusId })
|
|
|
|
await createProjectIssueStatuses({ tx, projectId: project._id, categories, defaultStatusId })
|
2022-04-23 16:59:57 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function upgradeIssueStatuses (tx: TxOperations): Promise<void> {
|
|
|
|
const deprecatedStatuses = [
|
|
|
|
DeprecatedIssueStatus.Backlog,
|
|
|
|
DeprecatedIssueStatus.Canceled,
|
|
|
|
DeprecatedIssueStatus.Done,
|
|
|
|
DeprecatedIssueStatus.InProgress,
|
|
|
|
DeprecatedIssueStatus.Todo
|
|
|
|
]
|
|
|
|
const issues = await tx.findAll(tracker.class.Issue, { status: { $in: deprecatedStatuses as any } })
|
|
|
|
|
|
|
|
if (issues.length > 0) {
|
|
|
|
const statusByDeprecatedStatus = new Map<DeprecatedIssueStatus, Ref<IssueStatus>>()
|
|
|
|
|
|
|
|
for (const issue of issues) {
|
|
|
|
const deprecatedStatus = issue.status as unknown as DeprecatedIssueStatus
|
|
|
|
|
|
|
|
if (!statusByDeprecatedStatus.has(deprecatedStatus)) {
|
|
|
|
const category = categoryByDeprecatedIssueStatus[deprecatedStatus]
|
|
|
|
const issueStatus = await tx.findOne(tracker.class.IssueStatus, { category })
|
|
|
|
|
|
|
|
if (issueStatus === undefined) {
|
|
|
|
throw new Error(`Could not find a new status for "${DeprecatedIssueStatus[deprecatedStatus]}"`)
|
|
|
|
}
|
|
|
|
|
|
|
|
statusByDeprecatedStatus.set(deprecatedStatus, issueStatus._id)
|
|
|
|
}
|
|
|
|
|
|
|
|
await tx.update(issue, { status: statusByDeprecatedStatus.get(deprecatedStatus) })
|
|
|
|
}
|
2022-04-16 02:59:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-02 02:42:44 +00:00
|
|
|
async function migrateParentIssues (client: MigrationClient): Promise<void> {
|
|
|
|
let { updated } = await client.update(
|
|
|
|
DOMAIN_TRACKER,
|
|
|
|
{ _class: tracker.class.Issue, attachedToClass: { $exists: false } },
|
|
|
|
{
|
|
|
|
subIssues: 0,
|
|
|
|
collection: 'subIssues',
|
|
|
|
attachedToClass: tracker.class.Issue
|
|
|
|
}
|
|
|
|
)
|
|
|
|
updated += (
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TRACKER,
|
|
|
|
{ _class: tracker.class.Issue, parentIssue: { $exists: true } },
|
|
|
|
{ $rename: { parentIssue: 'attachedTo' } }
|
|
|
|
)
|
|
|
|
).updated
|
|
|
|
updated += (
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TRACKER,
|
|
|
|
{ _class: tracker.class.Issue, attachedTo: { $in: [null, undefined] } },
|
|
|
|
{ attachedTo: tracker.ids.NoParent }
|
|
|
|
)
|
|
|
|
).updated
|
|
|
|
|
|
|
|
if (updated === 0) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const childrenCountById = new Map<Ref<Doc>, number>()
|
|
|
|
const parentIssueIds = (
|
|
|
|
await client.find<Issue>(DOMAIN_TRACKER, {
|
|
|
|
_class: tracker.class.Issue,
|
|
|
|
attachedTo: { $nin: [tracker.ids.NoParent] }
|
|
|
|
})
|
|
|
|
).map((issue) => issue.attachedTo)
|
|
|
|
|
|
|
|
for (const issueId of parentIssueIds) {
|
|
|
|
const count = childrenCountById.get(issueId) ?? 0
|
|
|
|
childrenCountById.set(issueId, count + 1)
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const [_id, childrenCount] of childrenCountById) {
|
|
|
|
await client.update(DOMAIN_TRACKER, { _id }, { subIssues: childrenCount })
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-27 06:04:26 +00:00
|
|
|
async function updateIssueParentInfo (client: MigrationClient, parentIssue: Issue | null): Promise<void> {
|
|
|
|
const parents =
|
|
|
|
parentIssue === null ? [] : [{ parentId: parentIssue._id, parentTitle: parentIssue.title }, ...parentIssue.parents]
|
|
|
|
const migrationResult = await client.update<Issue>(
|
|
|
|
DOMAIN_TRACKER,
|
|
|
|
{
|
|
|
|
_class: tracker.class.Issue,
|
|
|
|
attachedTo: parentIssue?._id ?? tracker.ids.NoParent,
|
|
|
|
parents: { $exists: false }
|
|
|
|
},
|
|
|
|
{ parents }
|
|
|
|
)
|
|
|
|
|
|
|
|
if (migrationResult.matched > 0) {
|
|
|
|
const subIssues = await client.find<Issue>(DOMAIN_TRACKER, {
|
|
|
|
_class: tracker.class.Issue,
|
|
|
|
attachedTo: parentIssue?._id ?? tracker.ids.NoParent,
|
|
|
|
subIssues: { $gt: 0 }
|
|
|
|
})
|
|
|
|
|
|
|
|
for (const issue of subIssues) {
|
|
|
|
await updateIssueParentInfo(client, issue)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function migrateIssueParentInfo (client: MigrationClient): Promise<void> {
|
|
|
|
await updateIssueParentInfo(client, null)
|
|
|
|
}
|
|
|
|
|
2023-03-17 06:17:53 +00:00
|
|
|
async function migrateIssueComponents (client: MigrationClient): Promise<void> {
|
|
|
|
const issues = await client.find(DOMAIN_TRACKER, { _class: tracker.class.Issue, component: { $exists: false } })
|
2022-05-16 10:38:51 +00:00
|
|
|
|
|
|
|
if (issues.length === 0) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const issue of issues) {
|
2023-03-17 06:17:53 +00:00
|
|
|
await client.update(DOMAIN_TRACKER, { _id: issue._id }, { component: null })
|
2022-05-16 10:38:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-16 02:59:50 +00:00
|
|
|
async function createDefaults (tx: TxOperations): Promise<void> {
|
2023-03-17 06:17:53 +00:00
|
|
|
await createDefaultProject(tx)
|
2022-06-19 16:27:47 +00:00
|
|
|
await createOrUpdate(
|
|
|
|
tx,
|
|
|
|
tags.class.TagCategory,
|
|
|
|
tags.space.Tags,
|
|
|
|
{
|
|
|
|
icon: tags.icon.Tags,
|
|
|
|
label: 'Other',
|
|
|
|
targetClass: tracker.class.Issue,
|
|
|
|
tags: [],
|
|
|
|
default: true
|
|
|
|
},
|
|
|
|
tracker.category.Other
|
|
|
|
)
|
2022-04-16 02:59:50 +00:00
|
|
|
}
|
2022-03-31 08:32:42 +00:00
|
|
|
|
2023-02-10 01:45:22 +00:00
|
|
|
async function fillRank (client: MigrationClient): Promise<void> {
|
|
|
|
const docs = await client.find<Issue>(DOMAIN_TRACKER, {
|
|
|
|
_class: tracker.class.Issue,
|
|
|
|
rank: ''
|
|
|
|
})
|
|
|
|
let last = (
|
|
|
|
await client.find<Issue>(
|
|
|
|
DOMAIN_TRACKER,
|
|
|
|
{
|
|
|
|
_class: tracker.class.Issue,
|
|
|
|
rank: { $ne: '' }
|
|
|
|
},
|
|
|
|
{
|
|
|
|
sort: { rank: SortingOrder.Descending },
|
|
|
|
limit: 1
|
|
|
|
}
|
|
|
|
)
|
|
|
|
)[0]
|
|
|
|
for (const doc of docs) {
|
|
|
|
const rank = calcRank(last)
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TRACKER,
|
|
|
|
{
|
|
|
|
_id: doc._id
|
|
|
|
},
|
|
|
|
{
|
|
|
|
rank
|
|
|
|
}
|
|
|
|
)
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TX,
|
|
|
|
{ 'tx.objectId': doc._id, 'tx._class': core.class.TxCreateDoc },
|
|
|
|
{ 'tx.attributes.rank': rank }
|
|
|
|
)
|
|
|
|
doc.rank = rank
|
|
|
|
last = doc
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-17 06:17:53 +00:00
|
|
|
async function upgradeProjects (tx: TxOperations): Promise<void> {
|
|
|
|
await upgradeProjectIssueStatuses(tx)
|
|
|
|
await fixProjectsIssueStatusesOrder(tx)
|
|
|
|
await upgradeProjectSettings(tx)
|
2022-04-23 16:59:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function upgradeIssues (tx: TxOperations): Promise<void> {
|
|
|
|
await upgradeIssueStatuses(tx)
|
2022-09-09 03:44:33 +00:00
|
|
|
|
|
|
|
const issues = await tx.findAll(tracker.class.Issue, {
|
|
|
|
$or: [{ blockedBy: { $exists: true } }, { relatedIssue: { $exists: true } }]
|
|
|
|
})
|
|
|
|
|
|
|
|
for (const i of issues) {
|
|
|
|
const rel = (i as any).relatedIssue as Ref<Issue>[]
|
|
|
|
const upd: DocumentUpdate<Issue> = {}
|
|
|
|
if (rel != null) {
|
|
|
|
;(upd as any).relatedIssue = null
|
|
|
|
upd.relations = rel.map((it) => ({ _id: it, _class: tracker.class.Issue }))
|
|
|
|
}
|
|
|
|
if (i.blockedBy !== undefined) {
|
|
|
|
if ((i.blockedBy as any[]).find((it) => typeof it === 'string') !== undefined) {
|
|
|
|
upd.blockedBy = (i.blockedBy as unknown as Ref<Issue>[]).map((it) => ({ _id: it, _class: tracker.class.Issue }))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (Object.keys(upd).length > 0) {
|
|
|
|
await tx.update(i, upd)
|
|
|
|
}
|
|
|
|
}
|
2022-04-23 16:59:57 +00:00
|
|
|
}
|
|
|
|
|
2023-05-16 11:56:29 +00:00
|
|
|
async function renameSprintToMilestone (client: MigrationClient): Promise<void> {
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TRACKER,
|
|
|
|
{
|
|
|
|
_class: tracker.class.Issue,
|
|
|
|
sprint: { $exists: true }
|
|
|
|
},
|
|
|
|
{
|
|
|
|
$rename: { sprint: 'milestone' }
|
|
|
|
}
|
|
|
|
)
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TRACKER,
|
|
|
|
{
|
|
|
|
_class: 'tracker:class:Sprint' as Ref<Class<Doc>>
|
|
|
|
},
|
|
|
|
{
|
|
|
|
_class: tracker.class.Milestone
|
|
|
|
}
|
|
|
|
)
|
|
|
|
const milestones = await client.find(DOMAIN_TRACKER, { _class: tracker.class.Milestone })
|
|
|
|
for (const milestone of milestones) {
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TX,
|
|
|
|
{
|
|
|
|
objectId: milestone._id,
|
|
|
|
objectClass: 'tracker:class:Sprint' as Ref<Class<Doc>>
|
|
|
|
},
|
|
|
|
{
|
|
|
|
objectClass: tracker.class.Milestone
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TX,
|
|
|
|
{
|
|
|
|
_class: core.class.TxCollectionCUD,
|
|
|
|
'tx._class': core.class.TxCreateDoc,
|
|
|
|
'tx.objectClass': tracker.class.Issue,
|
|
|
|
'tx.attributes.sprint': { $exists: true }
|
|
|
|
},
|
|
|
|
{
|
|
|
|
$rename: { 'tx.attributes.sprint': 'tx.attributes.milestone' }
|
|
|
|
}
|
|
|
|
)
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TX,
|
|
|
|
{
|
|
|
|
_class: core.class.TxCollectionCUD,
|
|
|
|
'tx._class': core.class.TxUpdateDoc,
|
|
|
|
'tx.objectClass': tracker.class.Issue,
|
|
|
|
'tx.operations.sprint': { $exists: true }
|
|
|
|
},
|
|
|
|
{
|
|
|
|
$rename: { 'tx.operations.sprint': 'tx.operations.milestone' }
|
|
|
|
}
|
|
|
|
)
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TX,
|
|
|
|
{
|
|
|
|
objectClass: tracker.class.Issue,
|
|
|
|
_class: core.class.TxUpdateDoc,
|
|
|
|
'operations.sprint': { $exists: true }
|
|
|
|
},
|
|
|
|
{
|
|
|
|
$rename: { 'operations.sprint': 'operations.milestone' }
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
const templates = await client.find<IssueTemplate>(DOMAIN_TRACKER, {
|
|
|
|
_class: tracker.class.IssueTemplate,
|
|
|
|
sprint: { $exists: true }
|
|
|
|
})
|
|
|
|
for (const template of templates) {
|
|
|
|
const children: IssueTemplateChild[] = template.children.map((p) => {
|
|
|
|
const res = {
|
|
|
|
...p,
|
|
|
|
milestone: p.milestone
|
|
|
|
}
|
|
|
|
delete (res as any).sprint
|
|
|
|
return res
|
|
|
|
})
|
|
|
|
await client.update<IssueTemplate>(
|
|
|
|
DOMAIN_TRACKER,
|
|
|
|
{
|
|
|
|
_id: template._id
|
|
|
|
},
|
|
|
|
{
|
|
|
|
children
|
|
|
|
}
|
|
|
|
)
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TRACKER,
|
|
|
|
{
|
|
|
|
_id: template._id
|
|
|
|
},
|
|
|
|
{
|
|
|
|
$rename: { sprint: 'milestone' }
|
|
|
|
}
|
|
|
|
)
|
|
|
|
const createTxes = await client.find<TxCreateDoc<IssueTemplate>>(DOMAIN_TX, {
|
|
|
|
objectId: template._id,
|
|
|
|
_class: core.class.TxCreateDoc
|
|
|
|
})
|
|
|
|
for (const createTx of createTxes) {
|
|
|
|
const children: IssueTemplateChild[] = createTx.attributes.children.map((p) => {
|
|
|
|
const res = {
|
|
|
|
...p,
|
|
|
|
milestone: p.milestone
|
|
|
|
}
|
|
|
|
delete (res as any).sprint
|
|
|
|
return res
|
|
|
|
})
|
|
|
|
await client.update<TxCreateDoc<IssueTemplate>>(
|
|
|
|
DOMAIN_TX,
|
|
|
|
{
|
|
|
|
_id: createTx._id
|
|
|
|
},
|
|
|
|
{
|
|
|
|
children
|
|
|
|
}
|
|
|
|
)
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TX,
|
|
|
|
{
|
|
|
|
_id: createTx._id
|
|
|
|
},
|
|
|
|
{
|
|
|
|
$rename: { 'attributes.sprint': 'attributes.milestone' }
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
const updateTxes = await client.find<TxUpdateDoc<IssueTemplate>>(DOMAIN_TX, {
|
|
|
|
objectId: template._id,
|
|
|
|
_class: core.class.TxUpdateDoc
|
|
|
|
})
|
|
|
|
for (const updateTx of updateTxes) {
|
|
|
|
if ((updateTx.operations as any).sprint !== undefined) {
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TX,
|
|
|
|
{
|
|
|
|
_id: updateTx._id
|
|
|
|
},
|
|
|
|
{
|
|
|
|
$rename: { 'operations.sprint': 'operations.milestone' }
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
if (updateTx.operations.children !== undefined) {
|
|
|
|
const children: IssueTemplateChild[] = updateTx.operations.children.map((p) => {
|
|
|
|
const res = {
|
|
|
|
...p,
|
|
|
|
milestone: p.milestone
|
|
|
|
}
|
|
|
|
delete (res as any).sprint
|
|
|
|
return res
|
|
|
|
})
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TX,
|
|
|
|
{
|
|
|
|
_id: updateTx._id
|
|
|
|
},
|
|
|
|
{
|
|
|
|
children
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-17 06:17:53 +00:00
|
|
|
async function renameProject (client: MigrationClient): Promise<void> {
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TRACKER,
|
|
|
|
{
|
2023-05-16 11:56:29 +00:00
|
|
|
_class: { $in: [tracker.class.Issue, tracker.class.Milestone] },
|
2023-03-17 06:17:53 +00:00
|
|
|
project: { $exists: true }
|
|
|
|
},
|
|
|
|
{
|
|
|
|
$rename: { project: 'component' }
|
|
|
|
}
|
|
|
|
)
|
2023-03-17 16:01:35 +00:00
|
|
|
await client.update(
|
|
|
|
DOMAIN_TRACKER,
|
|
|
|
{
|
2023-03-17 16:35:49 +00:00
|
|
|
_class: tracker.class.Project
|
2023-03-17 16:01:35 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
_class: tracker.class.Component
|
|
|
|
}
|
|
|
|
)
|
|
|
|
const components = await client.find(DOMAIN_TRACKER, { _class: tracker.class.Component })
|
|
|
|
for (const component of components) {
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TX,
|
|
|
|
{
|
|
|
|
objectId: component._id,
|
|
|
|
objectClass: tracker.class.Project
|
|
|
|
},
|
|
|
|
{
|
|
|
|
objectClass: tracker.class.Component
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-03-17 06:17:53 +00:00
|
|
|
await client.update(
|
|
|
|
DOMAIN_TX,
|
|
|
|
{
|
|
|
|
_class: core.class.TxCollectionCUD,
|
|
|
|
'tx._class': core.class.TxCreateDoc,
|
|
|
|
'tx.objectClass': tracker.class.Issue,
|
|
|
|
'tx.attributes.project': { $exists: true }
|
|
|
|
},
|
|
|
|
{
|
|
|
|
$rename: { 'tx.attributes.project': 'tx.attributes.component' }
|
|
|
|
}
|
|
|
|
)
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TX,
|
|
|
|
{
|
|
|
|
_class: core.class.TxCollectionCUD,
|
|
|
|
'tx._class': core.class.TxUpdateDoc,
|
|
|
|
'tx.objectClass': tracker.class.Issue,
|
|
|
|
'tx.operations.project': { $exists: true }
|
|
|
|
},
|
|
|
|
{
|
|
|
|
$rename: { 'tx.operations.project': 'tx.operations.component' }
|
|
|
|
}
|
|
|
|
)
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TX,
|
|
|
|
{
|
2023-05-16 11:56:29 +00:00
|
|
|
objectClass: tracker.class.Milestone,
|
2023-03-17 06:17:53 +00:00
|
|
|
_class: core.class.TxCreateDoc,
|
|
|
|
'attributes.project': { $exists: true }
|
|
|
|
},
|
|
|
|
{
|
|
|
|
$rename: { 'attributes.project': 'attributes.component' }
|
|
|
|
}
|
|
|
|
)
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TX,
|
|
|
|
{
|
2023-05-16 11:56:29 +00:00
|
|
|
objectClass: { $in: [tracker.class.Issue, tracker.class.Milestone] },
|
2023-03-17 06:17:53 +00:00
|
|
|
_class: core.class.TxUpdateDoc,
|
|
|
|
'operations.project': { $exists: true }
|
|
|
|
},
|
|
|
|
{
|
|
|
|
$rename: { 'operations.project': 'operations.component' }
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
const templates = await client.find<IssueTemplate>(DOMAIN_TRACKER, {
|
|
|
|
_class: tracker.class.IssueTemplate,
|
|
|
|
project: { $exists: true }
|
|
|
|
})
|
|
|
|
for (const template of templates) {
|
|
|
|
const children: IssueTemplateChild[] = template.children.map((p) => {
|
|
|
|
const res = {
|
|
|
|
...p,
|
|
|
|
component: p.component
|
|
|
|
}
|
|
|
|
delete (res as any).project
|
|
|
|
return res
|
|
|
|
})
|
|
|
|
await client.update<IssueTemplate>(
|
|
|
|
DOMAIN_TRACKER,
|
|
|
|
{
|
|
|
|
_id: template._id
|
|
|
|
},
|
|
|
|
{
|
|
|
|
children
|
|
|
|
}
|
|
|
|
)
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TRACKER,
|
|
|
|
{
|
|
|
|
_id: template._id
|
|
|
|
},
|
|
|
|
{
|
|
|
|
$rename: { project: 'component' }
|
|
|
|
}
|
|
|
|
)
|
|
|
|
const createTxes = await client.find<TxCreateDoc<IssueTemplate>>(DOMAIN_TX, {
|
|
|
|
objectId: template._id,
|
|
|
|
_class: core.class.TxCreateDoc
|
|
|
|
})
|
|
|
|
for (const createTx of createTxes) {
|
|
|
|
const children: IssueTemplateChild[] = createTx.attributes.children.map((p) => {
|
|
|
|
const res = {
|
|
|
|
...p,
|
|
|
|
component: p.component
|
|
|
|
}
|
|
|
|
delete (res as any).project
|
|
|
|
return res
|
|
|
|
})
|
|
|
|
await client.update<TxCreateDoc<IssueTemplate>>(
|
|
|
|
DOMAIN_TX,
|
|
|
|
{
|
|
|
|
_id: createTx._id
|
|
|
|
},
|
|
|
|
{
|
|
|
|
children
|
|
|
|
}
|
|
|
|
)
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TX,
|
|
|
|
{
|
|
|
|
_id: createTx._id
|
|
|
|
},
|
|
|
|
{
|
|
|
|
$rename: { 'attributes.project': 'attributes.component' }
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
const updateTxes = await client.find<TxUpdateDoc<IssueTemplate>>(DOMAIN_TX, {
|
|
|
|
objectId: template._id,
|
|
|
|
_class: core.class.TxUpdateDoc
|
|
|
|
})
|
|
|
|
for (const updateTx of updateTxes) {
|
|
|
|
if ((updateTx.operations as any).project !== undefined) {
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TX,
|
|
|
|
{
|
|
|
|
_id: updateTx._id
|
|
|
|
},
|
|
|
|
{
|
|
|
|
$rename: { 'operations.project': 'operations.component' }
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
if (updateTx.operations.children !== undefined) {
|
|
|
|
const children: IssueTemplateChild[] = updateTx.operations.children.map((p) => {
|
|
|
|
const res = {
|
|
|
|
...p,
|
|
|
|
component: p.component
|
|
|
|
}
|
|
|
|
delete (res as any).project
|
|
|
|
return res
|
|
|
|
})
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TX,
|
|
|
|
{
|
|
|
|
_id: updateTx._id
|
|
|
|
},
|
|
|
|
{
|
|
|
|
children
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const defaultSpace = (
|
|
|
|
await client.find<Project>(DOMAIN_SPACE, {
|
|
|
|
_id: 'tracker:team:DefaultTeam' as Ref<Project>
|
|
|
|
})
|
|
|
|
)[0]
|
|
|
|
if (defaultSpace !== undefined) {
|
|
|
|
await client.delete(DOMAIN_SPACE, tracker.project.DefaultProject)
|
|
|
|
await client.create(DOMAIN_SPACE, {
|
|
|
|
...defaultSpace,
|
|
|
|
_id: tracker.project.DefaultProject,
|
|
|
|
_class: tracker.class.Project,
|
|
|
|
description: defaultSpace.description === 'Default team' ? 'Default project' : defaultSpace.description
|
|
|
|
})
|
|
|
|
await client.delete(DOMAIN_SPACE, defaultSpace._id)
|
|
|
|
}
|
|
|
|
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_SPACE,
|
|
|
|
{
|
|
|
|
_id: 'tracker:team:DefaultTeam' as Ref<Project>,
|
|
|
|
_class: 'tracker:class:Team' as Ref<Class<Doc>>
|
|
|
|
},
|
|
|
|
{
|
|
|
|
_id: tracker.project.DefaultProject,
|
|
|
|
_class: tracker.class.Project,
|
|
|
|
description: 'Default project'
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TRACKER,
|
|
|
|
{
|
|
|
|
attachedTo: 'tracker:team:DefaultTeam' as Ref<Doc>
|
|
|
|
},
|
|
|
|
{
|
|
|
|
attachedTo: tracker.project.DefaultProject
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TRACKER,
|
|
|
|
{
|
|
|
|
space: 'tracker:team:DefaultTeam' as Ref<Project>
|
|
|
|
},
|
|
|
|
{
|
|
|
|
space: tracker.project.DefaultProject
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TRACKER,
|
|
|
|
{
|
|
|
|
attachedToClass: 'tracker:class:Team' as Ref<Class<Doc>>
|
|
|
|
},
|
|
|
|
{
|
|
|
|
attachedToClass: tracker.class.Project
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TX,
|
|
|
|
{
|
|
|
|
objectId: 'tracker:team:DefaultTeam' as Ref<Project>
|
|
|
|
},
|
|
|
|
{
|
|
|
|
objectId: tracker.project.DefaultProject
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TX,
|
|
|
|
{
|
|
|
|
objectClass: 'tracker:class:Team' as Ref<Class<Doc>>
|
|
|
|
},
|
|
|
|
{
|
|
|
|
objectClass: tracker.class.Project
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TX,
|
|
|
|
{
|
|
|
|
'tx.objectClass': 'tracker:class:Team' as Ref<Class<Doc>>
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'tx.objectClass': tracker.class.Project
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TX,
|
|
|
|
{
|
|
|
|
objectSpace: 'tracker:team:DefaultTeam' as Ref<Project>
|
|
|
|
},
|
|
|
|
{
|
|
|
|
objectSpace: tracker.project.DefaultProject
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TX,
|
|
|
|
{
|
|
|
|
'tx.objectSpace': 'tracker:team:DefaultTeam' as Ref<Project>
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'tx.objectSpace': tracker.project.DefaultProject
|
|
|
|
}
|
|
|
|
)
|
2022-05-16 10:38:51 +00:00
|
|
|
}
|
|
|
|
|
2023-03-18 17:14:49 +00:00
|
|
|
async function setCreate (client: MigrationClient): Promise<void> {
|
|
|
|
while (true) {
|
|
|
|
const docs = await client.find<Issue>(
|
|
|
|
DOMAIN_TRACKER,
|
|
|
|
{
|
|
|
|
_class: tracker.class.Issue,
|
|
|
|
createOn: { $exists: false }
|
|
|
|
},
|
|
|
|
{ limit: 500 }
|
|
|
|
)
|
|
|
|
if (docs.length === 0) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
const creates = await client.find<TxCollectionCUD<Issue, Issue>>(DOMAIN_TX, {
|
|
|
|
'tx.objectId': { $in: docs.map((it) => it._id) },
|
|
|
|
'tx._class': core.class.TxCreateDoc
|
|
|
|
})
|
|
|
|
for (const doc of docs) {
|
|
|
|
const tx = creates.find((it) => it.tx.objectId === doc._id)
|
|
|
|
if (tx !== undefined) {
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TRACKER,
|
|
|
|
{
|
|
|
|
_id: doc._id
|
|
|
|
},
|
|
|
|
{
|
|
|
|
createOn: tx.modifiedOn
|
|
|
|
}
|
|
|
|
)
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TX,
|
|
|
|
{
|
|
|
|
_id: tx._id
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'tx.attributes.createOn': tx.modifiedOn
|
|
|
|
}
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TRACKER,
|
|
|
|
{
|
|
|
|
_id: doc._id
|
|
|
|
},
|
|
|
|
{
|
|
|
|
createOn: doc.modifiedOn
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-16 11:56:29 +00:00
|
|
|
async function fixMilestoneEmptyStatuses (client: MigrationClient): Promise<void> {
|
|
|
|
await client.update<Milestone>(
|
2023-05-12 17:01:04 +00:00
|
|
|
DOMAIN_TRACKER,
|
2023-05-16 11:56:29 +00:00
|
|
|
{ _class: tracker.class.Milestone, $or: [{ status: null }, { status: undefined }] },
|
|
|
|
{ status: MilestoneStatus.Planned }
|
2023-05-12 17:01:04 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-03-31 08:32:42 +00:00
|
|
|
export const trackerOperation: MigrateOperation = {
|
2022-05-16 10:38:51 +00:00
|
|
|
async migrate (client: MigrationClient): Promise<void> {
|
2022-08-16 10:19:33 +00:00
|
|
|
await client.update(
|
|
|
|
DOMAIN_TRACKER,
|
|
|
|
{ _class: tracker.class.Issue, reports: { $exists: false } },
|
|
|
|
{
|
|
|
|
reports: 0,
|
|
|
|
estimation: 0,
|
|
|
|
reportedTime: 0
|
|
|
|
}
|
|
|
|
)
|
2023-03-17 06:17:53 +00:00
|
|
|
await Promise.all([migrateIssueComponents(client), migrateParentIssues(client)])
|
2022-06-27 06:04:26 +00:00
|
|
|
await migrateIssueParentInfo(client)
|
2023-02-10 01:45:22 +00:00
|
|
|
await fillRank(client)
|
2023-05-16 11:56:29 +00:00
|
|
|
await renameSprintToMilestone(client)
|
2023-03-17 06:17:53 +00:00
|
|
|
await renameProject(client)
|
2023-03-18 17:14:49 +00:00
|
|
|
await setCreate(client)
|
2023-04-04 06:11:49 +00:00
|
|
|
|
|
|
|
// Move all status objects into status domain
|
|
|
|
await client.move(
|
|
|
|
DOMAIN_TRACKER,
|
|
|
|
{
|
|
|
|
_class: tracker.class.IssueStatus
|
|
|
|
},
|
|
|
|
DOMAIN_STATUS
|
|
|
|
)
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_STATUS,
|
|
|
|
{ _class: tracker.class.IssueStatus, ofAttribute: { $exists: false } },
|
|
|
|
{
|
|
|
|
ofAttribute: tracker.attribute.IssueStatus
|
|
|
|
}
|
|
|
|
)
|
2023-05-12 17:01:04 +00:00
|
|
|
|
2023-05-16 11:56:29 +00:00
|
|
|
await fixMilestoneEmptyStatuses(client)
|
2022-05-16 10:38:51 +00:00
|
|
|
},
|
2022-03-31 08:32:42 +00:00
|
|
|
async upgrade (client: MigrationUpgradeClient): Promise<void> {
|
2022-04-16 02:59:50 +00:00
|
|
|
const tx = new TxOperations(client, core.account.System)
|
|
|
|
await createDefaults(tx)
|
2022-05-16 10:38:51 +00:00
|
|
|
await upgradeProjects(tx)
|
2023-03-17 06:17:53 +00:00
|
|
|
await upgradeIssues(tx)
|
2022-03-31 08:32:42 +00:00
|
|
|
}
|
|
|
|
}
|