import { randomBytes } from 'crypto'
const DEFAULT_LANES = ['backlog', 'todo', 'in-progress', 'done']
class BoardStore {
constructor (bee) {
this.bee = bee
}
_cardKey (id) {
return `card/${id}`
}
_laneKey (name) {
return `lane/${name}`
}
_genId () {
return randomBytes(8).toString('hex')
}
async ready () {
await this.bee.ready()
// Initialize default lanes if none exist
const lanes = await this.listLanes()
if (lanes.length === 0) {
for (let i = 0; i < DEFAULT_LANES.length; i++) {
await this.bee.put(this._laneKey(DEFAULT_LANES[i]), {
name: DEFAULT_LANES[i],
order: i,
created: Date.now()
})
}
}
}
async createCard (opts = {}) {
const id = opts.id || this._genId()
const now = Date.now()
const card = {
id,
title: opts.title || 'Untitled',
description: opts.description || '',
lane: opts.lane || 'backlog',
assignee: opts.assignee || '',
tags: opts.tags || [],
priority: opts.priority || 'normal', // low, normal, high, urgent
created: now,
updated: now
}
await this.bee.put(this._cardKey(id), card)
return card
}
async getCard (id) {
const node = await this.bee.get(this._cardKey(id))
return node ? node.value : null
}
async updateCard (id, updates = {}) {
const existing = await this.getCard(id)
if (!existing) throw new Error(`Card not found: ${id}`)
const card = {
...existing,
...updates,
id, // never overwrite id
created: existing.created, // never overwrite created
updated: Date.now()
}
await this.bee.put(this._cardKey(id), card)
return card
}
async deleteCard (id) {
const existing = await this.getCard(id)
if (!existing) return false
await this.bee.del(this._cardKey(id))
return true
}
async moveCard (id, lane) {
return this.updateCard(id, { lane })
}
async listCards (opts = {}) {
const cards = []
for await (const node of this.bee.createReadStream({
gte: 'card/',
lte: 'card/~'
})) {
const card = node.value
if (!card) continue
// Filter by lane
if (opts.lane && card.lane !== opts.lane) continue
// Filter by assignee
if (opts.assignee && card.assignee !== opts.assignee) continue
// Filter by tag
if (opts.tag && !card.tags?.includes(opts.tag)) continue
// Filter by priority
if (opts.priority && card.priority !== opts.priority) continue
cards.push(card)
}
// Sort by priority then created date
const priorityOrder = { urgent: 0, high: 1, normal: 2, low: 3 }
cards.sort((a, b) => {
const pa = priorityOrder[a.priority] ?? 2
const pb = priorityOrder[b.priority] ?? 2
if (pa !== pb) return pa - pb
return a.created - b.created
})
return cards
}
async addLane (name, order) {
const existing = await this.bee.get(this._laneKey(name))
if (existing) throw new Error(`Lane already exists: ${name}`)
if (order === undefined) {
const lanes = await this.listLanes()
order = lanes.length
}
const lane = { name, order, created: Date.now() }
await this.bee.put(this._laneKey(name), lane)
return lane
}
async removeLane (name) {
// Don't allow removing lanes that have cards
const cards = await this.listCards({ lane: name })
if (cards.length > 0) {
throw new Error(`Cannot remove lane "${name}": ${cards.length} cards still in it`)
}
await this.bee.del(this._laneKey(name))
return true
}
async listLanes () {
const lanes = []
for await (const node of this.bee.createReadStream({
gte: 'lane/',
lte: 'lane/~'
})) {
if (node.value) lanes.push(node.value)
}
lanes.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
return lanes
}
async search (query) {
const q = query.toLowerCase()
const cards = await this.listCards()
return cards.filter(card => {
if (card.title.toLowerCase().includes(q)) return true
if (card.description.toLowerCase().includes(q)) return true
if (card.tags?.some(t => t.toLowerCase().includes(q))) return true
return false
})
}
async stats () {
const cards = await this.listCards()
const lanes = await this.listLanes()
const byLane = {}
for (const lane of lanes) {
byLane[lane.name] = 0
}
for (const card of cards) {
byLane[card.lane] = (byLane[card.lane] || 0) + 1
}
return {
totalCards: cards.length,
totalLanes: lanes.length,
byLane
}
}
}
export { BoardStore, DEFAULT_LANES }