/**
* 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
}