OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / lib / services / migrate / read-old-issues.js

73 lines · javascript ref:
/**
 * Read a legacy org issue tracker (issues v1): yield each issue (with its
 * comments attached) and, separately, each label, from the linearized view.
 *
 * In v1, an org's issues lived in their OWN separate autobase
 * (`issues:org:<name>`), a HyperIssues whose Hyperbee view is keyed:
 *   issue:{id}                 -> { id, title, body, author, status, labels, images, createdAt, updatedAt }
 *   comment:{issueId}:{cid}    -> { body, author, createdAt, editedAt?, editedBy? }
 *   label:{name}               -> { color, description, displayName }
 *
 * v2 (#42 linked-core) moves org issues INTO the org's own room. This reader
 * is the migration SOURCE: it streams those v1 records read-only so
 * migrateIssues can replay them into the org room via createWithId/
 * commentWithId/createLabel.
 *
 * Generic over anything exposing a Hyperbee `.view` (a HyperIssues, a
 * hyper-rooms Room, or a raw Autobase). Read-only — it linearizes the view
 * to latest before reading but never appends.
 *
 * Pairs with migrateIssues (which takes async-iterables of these records).
 *
 * @param {object} base - has `.view` (Hyperbee) and optionally `.update()`
 */

import { linearize } from './linearize.js'

function requireView (base, who) {
  if (!base) throw new Error(who + ' requires an opened issue tracker/room')
  const view = base.view
  if (!view || typeof view.createReadStream !== 'function') {
    throw new Error(who + ': base.view must be a Hyperbee')
  }
  return view
}

/**
 * Yield each issue record, with its comments attached as `comments: [...]`.
 * @yields { id, title, body, author, status, labels, images, createdAt, updatedAt, comments }
 */
export async function * readOldIssues (base) {
  const view = requireView(base, 'readOldIssues')
  // Linearize to the latest state before reading (pulls in replicated ops on
  // a follower; no-op on a single writer with nothing pending).
  await linearize(base)

  for await (const entry of view.createReadStream({ gt: 'issue:', lt: 'issue:\xff' })) {
    const issue = entry && entry.value
    if (!issue || typeof issue.id !== 'number') continue
    const comments = []
    // comment:{id}: .. comment:{id}:\xff — same range hyper-issues getComments
    // uses; id 1 vs 11 don't overlap (the trailing ':' guards the prefix).
    for await (const c of view.createReadStream({ gt: 'comment:' + issue.id + ':', lt: 'comment:' + issue.id + ':\xff' })) {
      if (!c || !c.value) continue
      comments.push({ commentId: c.key.split(':').slice(2).join(':'), ...c.value })
    }
    yield { ...issue, comments }
  }
}

/**
 * Yield each label record.
 * @yields { name, color, description, displayName }
 */
export async function * readOldLabels (base) {
  const view = requireView(base, 'readOldLabels')
  await linearize(base)

  for await (const entry of view.createReadStream({ gt: 'label:', lt: 'label:\xff' })) {
    if (!entry || !entry.value) continue
    yield { name: entry.key.slice('label:'.length), ...entry.value }
  }
}