OverviewFilesCommitsIssuesDocsReleasesPeers

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

81 lines · javascript ref:
/**
 * Migration upgrade function: orgs v1 (single shared autobase) → v2
 * (one autobase per org, #37) + resourceKeys (Phase B).
 *
 * Pure transformation, dependency-injected so it's unit-testable without
 * a live corestore. The real wiring (a later increment) mounts prod's
 * old single-`orgs` autobase READ-ONLY, yields its `org:*` records, and
 * maps create/addMember/addResource onto the NEW per-org HyperOrgs on a
 * separate target store. Source is never mutated.
 *
 * Old org record shape (prod, single-autobase):
 *   { name, displayName, description, about, type, userId,
 *     members: [{ userId, role, addedAt }],
 *     resources: [ repoName ],            // name-only, no keys in v1
 *     visibility, createdAt, updatedAt }
 *
 * deps:
 *   oldOrgs              async iterable | array of old org records
 *   create(org)          create the org in the target (per-org autobase)
 *   addMember(name, m)   replay a member  { userId, role }
 *   addResource(name, r) replay a resource { name, bootstrapKey|null }
 *   resolveResourceKey(repoName) -> bootstrapKey | null   (optional)
 *   onError(name, err)   observer (optional)
 *
 * Idempotent at the org level: a `seen` set (passed in for resumability)
 * skips orgs already migrated. Returns a summary { orgs, members, resources, skipped, errors }.
 */
export async function migrateOrgs (deps = {}) {
  const {
    oldOrgs, create, addMember, addResource,
    resolveResourceKey, seen = new Set(), onError
  } = deps
  if (!oldOrgs || typeof create !== 'function') {
    throw new Error('migrateOrgs requires { oldOrgs, create }')
  }

  const summary = { orgs: 0, members: 0, resources: 0, skipped: 0, errors: 0 }

  for await (const org of oldOrgs) {
    if (!org || !org.name) continue
    if (seen.has(org.name)) { summary.skipped++; continue }
    try {
      await create({
        name: org.name,
        type: org.type || 'group',
        displayName: org.displayName || org.name,
        description: org.description || '',
        about: org.about || '',
        visibility: org.visibility || 'public',
        userId: org.userId || null
      })
      summary.orgs++

      for (const m of (org.members || [])) {
        if (!m || !m.userId) continue
        // Skip the creator if create() already enrolled them as owner —
        // addMember is expected to be idempotent on the target side.
        await addMember(org.name, { userId: m.userId, role: m.role || 'member' })
        summary.members++
      }

      for (const repoName of (org.resources || [])) {
        if (!repoName) continue
        let bootstrapKey = null
        if (typeof resolveResourceKey === 'function') {
          try { bootstrapKey = await resolveResourceKey(repoName) } catch { bootstrapKey = null }
        }
        await addResource(org.name, { name: repoName, bootstrapKey })
        summary.resources++
      }

      seen.add(org.name)
    } catch (err) {
      summary.errors++
      if (onError) onError(org.name, err)
    }
  }

  return summary
}