/**
* Repo/issue namespace apply handler for unified MetadataBase
*
* Extracted from lib/repo-link/apply.js
* Processes single per-repo operations within the unified apply batch.
* All keys are scoped by repoName to avoid collisions in the shared view.
*
* Key schema (unified, repo-scoped):
* repo-writer:{repoName}:{userId} → { role, serverKey, identityKey, proof, addedBy, addedAt }
* repo-ref:{repoName}:{refPath} → { hash, updatedBy, timestamp }
* issue:{repoName}:{id} → { id, title, body, author, status, labels, images, createdAt, updatedAt }
* comment:{repoName}:{issueId}:{commentId} → { body, author, createdAt }
* label:{repoName}:{name} → { color, description, displayName }
* repo-meta:{repoName}:{key} → { value }
* issue-counter:{repoName} → { next: number }
*/
/**
* Apply a single issue/repo-namespace operation
*
* @param {object} op - Operation with { ns: 'issue', type, repoName, ... }
* @param {object} batch - Hyperbee batch (caller flushes)
* @param {object} base - Autobase host (for addWriter/removeWriter)
*/
export async function applyRepoOp (op, batch, base) {
const repo = op.repoName
switch (op.type) {
case 'add-writer': {
await batch.put(`repo-writer:${repo}:${op.userId}`, {
role: op.role,
serverKey: op.serverKey,
identityKey: op.identityKey || null,
proof: op.proof || null,
addedBy: op.addedBy || null,
addedAt: op.timestamp || Date.now()
})
if (op.serverKey && op.role !== 'mirror') {
try {
await base.addWriter(Buffer.from(op.serverKey, 'hex'), {
indexer: op.role === 'owner'
})
} catch (err) {
console.error(`[RepoApply] add-writer failed for ${op.serverKey.slice(0, 16)}...: ${err.message}`)
}
}
break
}
case 'remove-writer': {
const existing = await batch.get(`repo-writer:${repo}:${op.userId}`)
if (existing) {
const writerData = existing.value
await batch.del(`repo-writer:${repo}:${op.userId}`)
if (writerData.serverKey) {
try {
await base.removeWriter(Buffer.from(writerData.serverKey, 'hex'))
} catch (err) {
console.error(`[RepoApply] remove-writer failed for ${writerData.serverKey.slice(0, 16)}...: ${err.message}`)
}
}
}
break
}
case 'update-ref': {
await batch.put(`repo-ref:${repo}:${op.ref}`, {
hash: op.hash,
updatedBy: op.userId,
timestamp: op.timestamp || Date.now()
})
break
}
case 'delete-ref': {
await batch.del(`repo-ref:${repo}:${op.ref}`)
break
}
case 'create-issue': {
await batch.put(`issue:${repo}:${op.id}`, {
id: op.id,
title: op.title,
body: op.body || '',
author: op.userId,
status: op.status || 'open',
labels: op.labels || [],
images: op.images || [],
createdAt: op.timestamp || Date.now(),
updatedAt: op.timestamp || Date.now()
})
// Update counter
const counter = await batch.get(`issue-counter:${repo}`)
const currentNext = counter ? counter.value.next : 0
if (op.id >= currentNext) {
await batch.put(`issue-counter:${repo}`, { next: op.id + 1 })
}
break
}
case 'update-issue': {
const entry = await batch.get(`issue:${repo}:${op.id}`)
if (entry) {
const issue = entry.value
if (op.title !== undefined) issue.title = op.title
if (op.body !== undefined) issue.body = op.body
if (op.status !== undefined) issue.status = op.status
if (op.labels !== undefined) issue.labels = op.labels
if (op.images !== undefined) issue.images = op.images
issue.updatedAt = op.timestamp || Date.now()
await batch.put(`issue:${repo}:${op.id}`, issue)
}
break
}
case 'create-comment': {
await batch.put(`comment:${repo}:${op.issueId}:${op.commentId}`, {
body: op.body,
author: op.userId,
createdAt: op.timestamp || Date.now()
})
// Touch parent issue updatedAt
const issueEntry = await batch.get(`issue:${repo}:${op.issueId}`)
if (issueEntry) {
const issue = issueEntry.value
issue.updatedAt = op.timestamp || Date.now()
await batch.put(`issue:${repo}:${op.issueId}`, issue)
}
break
}
case 'create-label': {
await batch.put(`label:${repo}:${op.name}`, {
color: op.color || '#ccc',
description: op.description || '',
displayName: op.displayName || op.name
})
break
}
case 'update-label': {
const labelEntry = await batch.get(`label:${repo}:${op.name}`)
if (labelEntry) {
const label = labelEntry.value
if (op.color !== undefined) label.color = op.color
if (op.description !== undefined) label.description = op.description
if (op.displayName !== undefined) label.displayName = op.displayName
await batch.put(`label:${repo}:${op.name}`, label)
}
break
}
case 'delete-label': {
await batch.del(`label:${repo}:${op.name}`)
break
}
case 'set-meta': {
await batch.put(`repo-meta:${repo}:${op.key}`, { value: op.value })
break
}
}
}