OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / lib / services / migrate / migrations.js

124 lines · javascript ref:
/**
 * The migration registry — passed to runMigrations(). Add a new entry
 * (next version number) for each future schema upgrade.
 */

import { HyperIssues } from 'hyper-issues'
import { readOldOrgs } from './read-old-orgs.js'
import { migrateOrgs } from './migrate-orgs.js'
import { readOldIssues, readOldLabels } from './read-old-issues.js'
import { migrateIssues } from './migrate-issues.js'

/**
 * Replay each org's v1 separate issue tracker (`issues:org:<name>`) into its
 * v2 linked-core org room (#42). Per-repo trackers are untouched (they pass
 * through unchanged). Optional within the v2 migration: only runs when the
 * boot wiring supplies `openOldOrgTracker` (the source opener). The target is
 * the org's own composed Room (newOrgs.getOrgRoom(name)), wrapped read/write
 * by a HyperIssues that does NOT own the room.
 *
 * ctx:
 *   newOrgs              the migrated per-org HyperOrgs (has getOrgRoom)
 *   openOldOrgTracker    async (orgName) -> opened old issue tracker base | null
 *   orgNames?            explicit list (defaults to newOrgs.list() names)
 *   log?
 * @returns summary { orgs, issues, comments, labels, skipped, errors }
 */
export async function migrateOrgIssues (ctx = {}) {
  const { newOrgs, openOldOrgTracker, log } = ctx
  const summary = { orgs: 0, issues: 0, comments: 0, labels: 0, skipped: 0, errors: 0 }
  if (typeof openOldOrgTracker !== 'function') return summary
  if (!newOrgs || typeof newOrgs.getOrgRoom !== 'function') {
    throw new Error('migrateOrgIssues needs ctx.newOrgs with getOrgRoom')
  }

  let orgNames = ctx.orgNames
  if (!orgNames) {
    const list = await newOrgs.list()
    orgNames = list.map(o => o.name)
  }

  for (const name of orgNames) {
    let oldTracker = null
    try {
      oldTracker = await openOldOrgTracker(name)
    } catch (err) {
      summary.errors++
      if (log) log('open old org tracker ' + name + ' failed: ' + err.message)
      continue
    }
    if (!oldTracker) continue // org had no separate issue tracker — nothing to migrate

    const room = newOrgs.getOrgRoom(name)
    if (!room) {
      summary.errors++
      if (log) log('org ' + name + ' has no room in target — skipped issues')
      continue
    }
    // Wrap the org's composed room; ownsRoom:false so closing this tracker
    // doesn't tear down the org that owns the room.
    const target = new HyperIssues(null, { room, ownsRoom: false })

    const s = await migrateIssues({
      oldIssues: readOldIssues(oldTracker),
      oldLabels: readOldLabels(oldTracker),
      createIssue: (id, opts) => target.createWithId(id, opts),
      createComment: (iid, cid, opts) => target.commentWithId(iid, cid, opts),
      createLabel: (n, color, desc, dn) => target.createLabel(n, color, desc, dn),
      onError: (key, err) => { if (log) log('org ' + name + ' ' + key + ': ' + err.message) }
    })
    summary.orgs++
    summary.issues += s.issues
    summary.comments += s.comments
    summary.labels += s.labels
    summary.skipped += s.skipped
    summary.errors += s.errors
  }

  return summary
}

// v2: orgs single-autobase (v1) → one autobase per org (#37) + resourceKeys
// (Phase B), AND org issues from their separate `issues:org:<name>` autobase
// → the org's own room (#42 linked-core). In-place: reads the old `orgs`
// autobase + old per-org issue trackers, writes per-org HyperOrgs (with
// composed issues schema) in the same corestore.
//
// ctx (supplied by the boot wiring / test):
//   oldOrgsBase          - opened old single-orgs autobase (has .view)
//   newOrgs              - the new per-org HyperOrgs (target; getOrgRoom)
//   openOldOrgTracker?   - async (orgName) -> old issue tracker base | null
//   resolveResourceKey?  - (repoName) -> tracker bootstrapKey | null
//   log?
export const orgsToPerOrg = {
  version: 2,
  name: 'orgs-single-to-per-org',
  async up (ctx = {}) {
    const { oldOrgsBase, newOrgs, resolveResourceKey, openOldOrgTracker, log } = ctx
    if (!oldOrgsBase || !newOrgs) throw new Error('orgs migration needs ctx.oldOrgsBase + ctx.newOrgs')

    const orgs = await migrateOrgs({
      oldOrgs: readOldOrgs(oldOrgsBase),
      create: (o) => newOrgs.create(o.name, o.type || 'group', {
        displayName: o.displayName,
        description: o.description,
        about: o.about,
        visibility: o.visibility,
        userId: o.userId
      }),
      addMember: (name, m) => newOrgs.addMember(name, m.userId, m.role),
      addResource: (name, r) => newOrgs.addResource(name, r.name, r.bootstrapKey ? { bootstrapKey: r.bootstrapKey } : {}),
      resolveResourceKey,
      onError: (n, e) => { if (log) log('org ' + n + ' failed: ' + e.message) }
    })

    // #42: replay org issues into their rooms (no-op unless boot supplies the opener).
    const issues = await migrateOrgIssues({ newOrgs, openOldOrgTracker, log })

    return { orgs, issues }
  }
}

export const MIGRATIONS = [orgsToPerOrg]
export const TARGET_VERSION = MIGRATIONS.reduce((m, x) => Math.max(m, x.version), 0)