OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / lib / apply / repo.js

164 lines · javascript ref:
/**
 * 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
    }
  }
}