865 lines
23 KiB
Go
865 lines
23 KiB
Go
package db
|
|
|
|
import "context"
|
|
import "database/sql"
|
|
import "errors"
|
|
import "strings"
|
|
import "time"
|
|
|
|
import "codit/internal/models"
|
|
import "codit/internal/util"
|
|
|
|
var ErrCommentNotFound = errors.New("comment not found")
|
|
var ErrChecklistItemNotFound = errors.New("checklist item not found")
|
|
var ErrBlockAttachmentNotFound = errors.New("block attachment not found")
|
|
|
|
const blockCommentSelectCols = `
|
|
c.public_id, bl.public_id,
|
|
c.content,
|
|
COALESCE(u.public_id, ''), COALESCE(u.username, ''),
|
|
c.created_at, c.updated_at, c.delete_at`
|
|
|
|
const blockCommentSelectFrom = `
|
|
FROM block_comments c
|
|
JOIN blocks bl ON bl.id = c.block_id
|
|
LEFT JOIN users u ON u.id = c.created_by`
|
|
|
|
func scanBlockComment(row interface{ Scan(...any) error }, bc *models.BlockComment) error {
|
|
return row.Scan(
|
|
&bc.ID,
|
|
&bc.BlockID,
|
|
&bc.Content,
|
|
&bc.CreatedBy, &bc.CreatedByName,
|
|
&bc.CreatedAt, &bc.UpdatedAt, &bc.DeleteAt,
|
|
)
|
|
}
|
|
|
|
func (s *Store) ListBlockComments(boardID, blockID string) ([]models.BlockComment, error) {
|
|
var rows *sql.Rows
|
|
var err error
|
|
var comments []models.BlockComment
|
|
var bc models.BlockComment
|
|
|
|
rows, err = s.Query(
|
|
`SELECT`+blockCommentSelectCols+blockCommentSelectFrom+`
|
|
JOIN boards brd ON brd.id = bl.board_id
|
|
WHERE brd.public_id = ? AND bl.public_id = ? AND c.delete_at = 0
|
|
ORDER BY c.created_at ASC`, boardID, blockID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
bc = models.BlockComment{}
|
|
err = scanBlockComment(rows, &bc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
comments = append(comments, bc)
|
|
}
|
|
return comments, rows.Err()
|
|
}
|
|
|
|
func (s *Store) CreateBlockComment(ctx context.Context, boardID, blockID, content, userID string) (models.BlockComment, error) {
|
|
var id string
|
|
var err error
|
|
var now int64
|
|
var internalBlockID int64
|
|
var internalUserID int64
|
|
var bc models.BlockComment
|
|
var tx txExecutor
|
|
var owned bool
|
|
|
|
id, err = util.NewID()
|
|
if err != nil {
|
|
return bc, err
|
|
}
|
|
now = time.Now().Unix()
|
|
|
|
tx, owned, err = s.beginImmediateContext(ctx)
|
|
if err != nil {
|
|
return bc, err
|
|
}
|
|
err = tx.QueryRow(`
|
|
SELECT bl.id FROM blocks bl
|
|
JOIN boards brd ON brd.id = bl.board_id
|
|
WHERE brd.public_id = ? AND bl.public_id = ? AND bl.delete_at = 0`,
|
|
boardID, blockID).Scan(&internalBlockID)
|
|
if err == sql.ErrNoRows {
|
|
rollbackIfOwned(tx, owned)
|
|
return bc, sql.ErrNoRows
|
|
}
|
|
if err != nil {
|
|
rollbackIfOwned(tx, owned)
|
|
return bc, err
|
|
}
|
|
|
|
err = tx.QueryRow(`SELECT id FROM users WHERE public_id = ?`, userID).Scan(&internalUserID)
|
|
if err != nil {
|
|
rollbackIfOwned(tx, owned)
|
|
return bc, err
|
|
}
|
|
|
|
_, err = tx.Exec(`
|
|
INSERT INTO block_comments (public_id, block_id, content, created_by, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
id, internalBlockID, content, internalUserID, now, now)
|
|
if err != nil {
|
|
rollbackIfOwned(tx, owned)
|
|
return bc, err
|
|
}
|
|
|
|
err = tx.QueryRow(
|
|
`SELECT`+blockCommentSelectCols+blockCommentSelectFrom+`
|
|
WHERE c.public_id = ?`, id).Scan(
|
|
&bc.ID, &bc.BlockID, &bc.Content,
|
|
&bc.CreatedBy, &bc.CreatedByName,
|
|
&bc.CreatedAt, &bc.UpdatedAt, &bc.DeleteAt)
|
|
if err != nil {
|
|
rollbackIfOwned(tx, owned)
|
|
return bc, err
|
|
}
|
|
err = commitIfOwned(tx, owned)
|
|
if err != nil {
|
|
return bc, err
|
|
}
|
|
return bc, nil
|
|
}
|
|
|
|
func (s *Store) DeleteBlockComment(boardID, blockID, commentID string) error {
|
|
var now int64
|
|
var result sql.Result
|
|
var n int64
|
|
var err error
|
|
|
|
now = time.Now().Unix()
|
|
result, err = s.Exec(`
|
|
UPDATE block_comments SET delete_at = ?
|
|
WHERE public_id = ?
|
|
AND delete_at = 0
|
|
AND block_id = (
|
|
SELECT bl.id FROM blocks bl
|
|
JOIN boards brd ON brd.id = bl.board_id
|
|
WHERE brd.public_id = ? AND bl.public_id = ?
|
|
)`, now, commentID, boardID, blockID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n, err = result.RowsAffected()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if n == 0 {
|
|
return ErrCommentNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
const blockChecklistItemSelectCols = `
|
|
ci.public_id, bl.public_id,
|
|
ci.title, ci.done, ci.display_order,
|
|
COALESCE(cu.public_id, ''), COALESCE(uu.public_id, ''),
|
|
ci.created_at, ci.updated_at, ci.delete_at`
|
|
|
|
const blockChecklistItemSelectFrom = `
|
|
FROM block_checklist_items ci
|
|
JOIN blocks bl ON bl.id = ci.block_id
|
|
LEFT JOIN users cu ON cu.id = ci.created_by
|
|
LEFT JOIN users uu ON uu.id = ci.updated_by`
|
|
|
|
func scanBlockChecklistItem(row interface{ Scan(...any) error }, item *models.BlockChecklistItem) error {
|
|
return row.Scan(
|
|
&item.ID,
|
|
&item.BlockID,
|
|
&item.Title,
|
|
&item.Done,
|
|
&item.DisplayOrder,
|
|
&item.CreatedBy, &item.UpdatedBy,
|
|
&item.CreatedAt, &item.UpdatedAt, &item.DeleteAt,
|
|
)
|
|
}
|
|
|
|
func (s *Store) ListBlockChecklistItems(boardID, blockID string) ([]models.BlockChecklistItem, error) {
|
|
var rows *sql.Rows
|
|
var err error
|
|
var items []models.BlockChecklistItem
|
|
var item models.BlockChecklistItem
|
|
|
|
rows, err = s.Query(
|
|
`SELECT`+blockChecklistItemSelectCols+blockChecklistItemSelectFrom+`
|
|
JOIN boards brd ON brd.id = bl.board_id
|
|
WHERE brd.public_id = ? AND bl.public_id = ? AND ci.delete_at = 0
|
|
ORDER BY ci.display_order ASC, ci.created_at ASC`, boardID, blockID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
item = models.BlockChecklistItem{}
|
|
err = scanBlockChecklistItem(rows, &item)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
return items, rows.Err()
|
|
}
|
|
|
|
func (s *Store) CreateBlockChecklistItem(ctx context.Context, boardID, blockID, title, userID string) (models.BlockChecklistItem, error) {
|
|
var id string
|
|
var err error
|
|
var now int64
|
|
var internalBlockID int64
|
|
var internalUserID int64
|
|
var maxOrder int64
|
|
var item models.BlockChecklistItem
|
|
var tx txExecutor
|
|
var owned bool
|
|
|
|
id, err = util.NewID()
|
|
if err != nil {
|
|
return item, err
|
|
}
|
|
now = time.Now().Unix()
|
|
|
|
tx, owned, err = s.beginImmediateContext(ctx)
|
|
if err != nil {
|
|
return item, err
|
|
}
|
|
err = tx.QueryRow(`
|
|
SELECT bl.id FROM blocks bl
|
|
JOIN boards brd ON brd.id = bl.board_id
|
|
WHERE brd.public_id = ? AND bl.public_id = ? AND bl.delete_at = 0`,
|
|
boardID, blockID).Scan(&internalBlockID)
|
|
if err == sql.ErrNoRows {
|
|
rollbackIfOwned(tx, owned)
|
|
return item, sql.ErrNoRows
|
|
}
|
|
if err != nil {
|
|
rollbackIfOwned(tx, owned)
|
|
return item, err
|
|
}
|
|
|
|
err = tx.QueryRow(`SELECT id FROM users WHERE public_id = ?`, userID).Scan(&internalUserID)
|
|
if err != nil {
|
|
rollbackIfOwned(tx, owned)
|
|
return item, err
|
|
}
|
|
|
|
err = tx.QueryRow(`SELECT COALESCE(MAX(display_order), -1) FROM block_checklist_items WHERE block_id = ? AND delete_at = 0`, internalBlockID).Scan(&maxOrder)
|
|
if err != nil {
|
|
rollbackIfOwned(tx, owned)
|
|
return item, err
|
|
}
|
|
|
|
_, err = tx.Exec(`
|
|
INSERT INTO block_checklist_items (public_id, block_id, title, done, display_order, created_by, updated_by, created_at, updated_at)
|
|
VALUES (?, ?, ?, 0, ?, ?, ?, ?, ?)`,
|
|
id, internalBlockID, title, maxOrder+1, internalUserID, internalUserID, now, now)
|
|
if err != nil {
|
|
rollbackIfOwned(tx, owned)
|
|
return item, err
|
|
}
|
|
|
|
err = scanBlockChecklistItem(
|
|
tx.QueryRow(`SELECT`+blockChecklistItemSelectCols+blockChecklistItemSelectFrom+` WHERE ci.public_id = ?`, id),
|
|
&item)
|
|
if err != nil {
|
|
rollbackIfOwned(tx, owned)
|
|
return item, err
|
|
}
|
|
err = commitIfOwned(tx, owned)
|
|
if err != nil {
|
|
return item, err
|
|
}
|
|
return item, nil
|
|
}
|
|
|
|
func (s *Store) UpdateBlockChecklistItem(boardID, blockID, itemID, title string, done bool, userID string) (models.BlockChecklistItem, error) {
|
|
var now int64
|
|
var result sql.Result
|
|
var n int64
|
|
var err error
|
|
var item models.BlockChecklistItem
|
|
|
|
now = time.Now().Unix()
|
|
result, err = s.Exec(`
|
|
UPDATE block_checklist_items SET title = ?, done = ?, updated_by = (SELECT id FROM users WHERE public_id = ?), updated_at = ?
|
|
WHERE public_id = ?
|
|
AND delete_at = 0
|
|
AND block_id = (
|
|
SELECT bl.id FROM blocks bl
|
|
JOIN boards brd ON brd.id = bl.board_id
|
|
WHERE brd.public_id = ? AND bl.public_id = ? AND bl.delete_at = 0
|
|
)`, title, done, userID, now, itemID, boardID, blockID)
|
|
if err != nil {
|
|
return item, err
|
|
}
|
|
n, err = result.RowsAffected()
|
|
if err != nil {
|
|
return item, err
|
|
}
|
|
if n == 0 {
|
|
return item, ErrChecklistItemNotFound
|
|
}
|
|
|
|
err = scanBlockChecklistItem(
|
|
s.QueryRow(`SELECT`+blockChecklistItemSelectCols+blockChecklistItemSelectFrom+` WHERE ci.public_id = ?`, itemID),
|
|
&item)
|
|
return item, err
|
|
}
|
|
|
|
func (s *Store) DeleteBlockChecklistItem(boardID, blockID, itemID string) error {
|
|
var now int64
|
|
var result sql.Result
|
|
var n int64
|
|
var err error
|
|
|
|
now = time.Now().Unix()
|
|
result, err = s.Exec(`
|
|
UPDATE block_checklist_items SET delete_at = ?
|
|
WHERE public_id = ?
|
|
AND delete_at = 0
|
|
AND block_id = (
|
|
SELECT bl.id FROM blocks bl
|
|
JOIN boards brd ON brd.id = bl.board_id
|
|
WHERE brd.public_id = ? AND bl.public_id = ? AND bl.delete_at = 0
|
|
)`, now, itemID, boardID, blockID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n, err = result.RowsAffected()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if n == 0 {
|
|
return ErrChecklistItemNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) ReorderBlockChecklistItems(ctx context.Context, boardID, blockID string, ids []string) ([]models.BlockChecklistItem, error) {
|
|
var internalBlockID int64
|
|
var err error
|
|
var now int64
|
|
var tx txExecutor
|
|
var owned bool
|
|
var i int
|
|
var id string
|
|
var result sql.Result
|
|
var n int64
|
|
|
|
now = time.Now().Unix()
|
|
tx, owned, err = s.beginContext(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = tx.QueryRow(`
|
|
SELECT bl.id FROM blocks bl
|
|
JOIN boards brd ON brd.id = bl.board_id
|
|
WHERE brd.public_id = ? AND bl.public_id = ? AND bl.delete_at = 0`,
|
|
boardID, blockID).Scan(&internalBlockID)
|
|
if err == sql.ErrNoRows {
|
|
rollbackIfOwned(tx, owned)
|
|
return nil, sql.ErrNoRows
|
|
}
|
|
if err != nil {
|
|
rollbackIfOwned(tx, owned)
|
|
return nil, err
|
|
}
|
|
|
|
for i = 0; i < len(ids); i++ {
|
|
id = ids[i]
|
|
result, err = tx.Exec(`
|
|
UPDATE block_checklist_items SET display_order = ?, updated_at = ?
|
|
WHERE public_id = ? AND block_id = ? AND delete_at = 0`,
|
|
i, now, id, internalBlockID)
|
|
if err != nil {
|
|
rollbackIfOwned(tx, owned)
|
|
return nil, err
|
|
}
|
|
n, err = result.RowsAffected()
|
|
if err != nil {
|
|
rollbackIfOwned(tx, owned)
|
|
return nil, err
|
|
}
|
|
if n == 0 {
|
|
rollbackIfOwned(tx, owned)
|
|
return nil, ErrChecklistItemNotFound
|
|
}
|
|
}
|
|
err = commitIfOwned(tx, owned)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s.ListBlockChecklistItems(boardID, blockID)
|
|
}
|
|
|
|
const blockAttachmentSelectCols = `
|
|
ba.public_id, bl.public_id,
|
|
ba.filename, ba.content_type, ba.size, ba.storage_path,
|
|
COALESCE(u.public_id, ''),
|
|
ba.created_at, ba.delete_at`
|
|
|
|
const blockAttachmentSelectFrom = `
|
|
FROM block_attachments ba
|
|
JOIN blocks bl ON bl.id = ba.block_id
|
|
LEFT JOIN users u ON u.id = ba.created_by`
|
|
|
|
func scanBlockAttachment(row interface{ Scan(...any) error }, item *models.BlockAttachment) error {
|
|
return row.Scan(
|
|
&item.ID,
|
|
&item.BlockID,
|
|
&item.Filename,
|
|
&item.ContentType,
|
|
&item.Size,
|
|
&item.StoragePath,
|
|
&item.CreatedBy,
|
|
&item.CreatedAt,
|
|
&item.DeleteAt,
|
|
)
|
|
}
|
|
|
|
func (s *Store) ListBlockAttachments(boardID, blockID string) ([]models.BlockAttachment, error) {
|
|
var rows *sql.Rows
|
|
var err error
|
|
var items []models.BlockAttachment
|
|
var item models.BlockAttachment
|
|
|
|
rows, err = s.Query(
|
|
`SELECT`+blockAttachmentSelectCols+blockAttachmentSelectFrom+`
|
|
JOIN boards brd ON brd.id = bl.board_id
|
|
WHERE brd.public_id = ? AND bl.public_id = ? AND ba.delete_at = 0
|
|
ORDER BY ba.created_at DESC`, boardID, blockID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
item = models.BlockAttachment{}
|
|
err = scanBlockAttachment(rows, &item)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
return items, rows.Err()
|
|
}
|
|
|
|
func (s *Store) CreateBlockAttachment(ctx context.Context, boardID, blockID string, item models.BlockAttachment) (models.BlockAttachment, error) {
|
|
var err error
|
|
var internalBlockID int64
|
|
var internalUserID int64
|
|
var now int64
|
|
var tx txExecutor
|
|
var owned bool
|
|
|
|
now = time.Now().Unix()
|
|
tx, owned, err = s.beginImmediateContext(ctx)
|
|
if err != nil {
|
|
return item, err
|
|
}
|
|
err = tx.QueryRow(`
|
|
SELECT bl.id FROM blocks bl
|
|
JOIN boards brd ON brd.id = bl.board_id
|
|
WHERE brd.public_id = ? AND bl.public_id = ? AND bl.delete_at = 0`,
|
|
boardID, blockID).Scan(&internalBlockID)
|
|
if err == sql.ErrNoRows {
|
|
rollbackIfOwned(tx, owned)
|
|
return item, sql.ErrNoRows
|
|
}
|
|
if err != nil {
|
|
rollbackIfOwned(tx, owned)
|
|
return item, err
|
|
}
|
|
|
|
err = tx.QueryRow(`SELECT id FROM users WHERE public_id = ?`, item.CreatedBy).Scan(&internalUserID)
|
|
if err != nil {
|
|
rollbackIfOwned(tx, owned)
|
|
return item, err
|
|
}
|
|
|
|
item.BlockID = blockID
|
|
item.CreatedAt = now
|
|
_, err = tx.Exec(`
|
|
INSERT INTO block_attachments (public_id, block_id, filename, content_type, size, storage_path, created_by, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
item.ID, internalBlockID, item.Filename, item.ContentType, item.Size, item.StoragePath, internalUserID, now)
|
|
if err != nil {
|
|
rollbackIfOwned(tx, owned)
|
|
return item, err
|
|
}
|
|
|
|
err = scanBlockAttachment(
|
|
tx.QueryRow(`SELECT`+blockAttachmentSelectCols+blockAttachmentSelectFrom+` WHERE ba.public_id = ?`, item.ID),
|
|
&item)
|
|
if err != nil {
|
|
rollbackIfOwned(tx, owned)
|
|
return item, err
|
|
}
|
|
err = commitIfOwned(tx, owned)
|
|
if err != nil { return item, err }
|
|
return item, nil
|
|
}
|
|
|
|
func (s *Store) GetBlockAttachment(boardID, blockID, attachmentID string) (models.BlockAttachment, error) {
|
|
var item models.BlockAttachment
|
|
var err error
|
|
|
|
err = scanBlockAttachment(
|
|
s.QueryRow(`SELECT`+blockAttachmentSelectCols+blockAttachmentSelectFrom+`
|
|
JOIN boards brd ON brd.id = bl.board_id
|
|
WHERE brd.public_id = ? AND bl.public_id = ? AND ba.public_id = ? AND ba.delete_at = 0`,
|
|
boardID, blockID, attachmentID),
|
|
&item)
|
|
if err == sql.ErrNoRows {
|
|
return item, ErrBlockAttachmentNotFound
|
|
}
|
|
return item, err
|
|
}
|
|
|
|
func (s *Store) DeleteBlockAttachment(ctx context.Context, boardID, blockID, attachmentID string) (models.BlockAttachment, error) {
|
|
var item models.BlockAttachment
|
|
var now int64
|
|
var result sql.Result
|
|
var n int64
|
|
var err error
|
|
var tx txExecutor
|
|
var owned bool
|
|
|
|
tx, owned, err = s.beginImmediateContext(ctx)
|
|
if err != nil {
|
|
return item, err
|
|
}
|
|
err = scanBlockAttachment(
|
|
tx.QueryRow(`SELECT`+blockAttachmentSelectCols+blockAttachmentSelectFrom+`
|
|
JOIN boards brd ON brd.id = bl.board_id
|
|
WHERE brd.public_id = ? AND bl.public_id = ? AND bl.delete_at = 0 AND ba.public_id = ? AND ba.delete_at = 0`,
|
|
boardID, blockID, attachmentID),
|
|
&item)
|
|
if err == sql.ErrNoRows {
|
|
rollbackIfOwned(tx, owned)
|
|
return item, ErrBlockAttachmentNotFound
|
|
}
|
|
if err != nil {
|
|
rollbackIfOwned(tx, owned)
|
|
return item, err
|
|
}
|
|
|
|
now = time.Now().Unix()
|
|
result, err = tx.Exec(`
|
|
UPDATE block_attachments SET delete_at = ?
|
|
WHERE public_id = ?
|
|
AND delete_at = 0
|
|
AND block_id = (
|
|
SELECT bl.id FROM blocks bl
|
|
JOIN boards brd ON brd.id = bl.board_id
|
|
WHERE brd.public_id = ? AND bl.public_id = ? AND bl.delete_at = 0
|
|
)`, now, attachmentID, boardID, blockID)
|
|
if err != nil {
|
|
rollbackIfOwned(tx, owned)
|
|
return item, err
|
|
}
|
|
n, err = result.RowsAffected()
|
|
if err != nil {
|
|
rollbackIfOwned(tx, owned)
|
|
return item, err
|
|
}
|
|
if n == 0 {
|
|
rollbackIfOwned(tx, owned)
|
|
return item, ErrBlockAttachmentNotFound
|
|
}
|
|
item.DeleteAt = now
|
|
|
|
err = commitIfOwned(tx, owned)
|
|
if err != nil { return item, err }
|
|
return item, nil
|
|
}
|
|
|
|
func (s *Store) GetBlockProperties(boardID, blockID string) (models.BlockProperties, error) {
|
|
var bp models.BlockProperties
|
|
var err error
|
|
|
|
err = s.QueryRow(`
|
|
SELECT bl.public_id,
|
|
COALESCE(p.status, ''), COALESCE(p.card_type, ''),
|
|
COALESCE(p.priority, ''), COALESCE(p.due_date, ''),
|
|
COALESCE(p.sprint, ''), COALESCE(p.description, ''),
|
|
COALESCE(ci.total, 0), COALESCE(ci.done, 0),
|
|
COALESCE(ba.total, 0), COALESCE(ba.images, 0)
|
|
FROM blocks bl
|
|
JOIN boards brd ON brd.id = bl.board_id
|
|
LEFT JOIN block_properties p ON p.block_id = bl.id
|
|
LEFT JOIN (
|
|
SELECT block_id, COUNT(*) AS total, SUM(CASE WHEN done THEN 1 ELSE 0 END) AS done
|
|
FROM block_checklist_items
|
|
WHERE delete_at = 0
|
|
GROUP BY block_id
|
|
) ci ON ci.block_id = bl.id
|
|
LEFT JOIN (
|
|
SELECT block_id, COUNT(*) AS total, SUM(CASE WHEN content_type LIKE 'image/%' THEN 1 ELSE 0 END) AS images
|
|
FROM block_attachments
|
|
WHERE delete_at = 0
|
|
GROUP BY block_id
|
|
) ba ON ba.block_id = bl.id
|
|
WHERE brd.public_id = ? AND bl.public_id = ? AND bl.delete_at = 0`,
|
|
boardID, blockID).Scan(
|
|
&bp.BlockID,
|
|
&bp.Status, &bp.CardType,
|
|
&bp.Priority, &bp.DueDate,
|
|
&bp.Sprint, &bp.Description,
|
|
&bp.ChecklistTotal, &bp.ChecklistDone,
|
|
&bp.AttachmentTotal, &bp.ImageTotal)
|
|
if err != nil {
|
|
return bp, err
|
|
}
|
|
err = s.populateBlockPropertiesAssignees(boardID, &bp)
|
|
return bp, err
|
|
}
|
|
|
|
func (s *Store) ListAllBlockProperties(boardID string) ([]models.BlockProperties, error) {
|
|
var rows *sql.Rows
|
|
var err error
|
|
var result []models.BlockProperties
|
|
var bp models.BlockProperties
|
|
var assignees map[string][]models.BoardAssignableUser
|
|
var i int
|
|
|
|
rows, err = s.Query(`
|
|
SELECT bl.public_id,
|
|
COALESCE(p.status, ''), COALESCE(p.card_type, ''),
|
|
COALESCE(p.priority, ''), COALESCE(p.due_date, ''),
|
|
COALESCE(p.sprint, ''), COALESCE(p.description, ''),
|
|
COALESCE(ci.total, 0), COALESCE(ci.done, 0),
|
|
COALESCE(ba.total, 0), COALESCE(ba.images, 0)
|
|
FROM blocks bl
|
|
JOIN boards brd ON brd.id = bl.board_id
|
|
LEFT JOIN block_properties p ON p.block_id = bl.id
|
|
LEFT JOIN (
|
|
SELECT block_id, COUNT(*) AS total, SUM(CASE WHEN done THEN 1 ELSE 0 END) AS done
|
|
FROM block_checklist_items
|
|
WHERE delete_at = 0
|
|
GROUP BY block_id
|
|
) ci ON ci.block_id = bl.id
|
|
LEFT JOIN (
|
|
SELECT block_id, COUNT(*) AS total, SUM(CASE WHEN content_type LIKE 'image/%' THEN 1 ELSE 0 END) AS images
|
|
FROM block_attachments
|
|
WHERE delete_at = 0
|
|
GROUP BY block_id
|
|
) ba ON ba.block_id = bl.id
|
|
WHERE brd.public_id = ? AND bl.type = 'card' AND bl.delete_at = 0
|
|
ORDER BY COALESCE(p.sprint, ''), bl.created_at ASC`, boardID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
bp = models.BlockProperties{}
|
|
err = rows.Scan(
|
|
&bp.BlockID,
|
|
&bp.Status, &bp.CardType,
|
|
&bp.Priority, &bp.DueDate,
|
|
&bp.Sprint, &bp.Description,
|
|
&bp.ChecklistTotal, &bp.ChecklistDone,
|
|
&bp.AttachmentTotal, &bp.ImageTotal)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result = append(result, bp)
|
|
}
|
|
err = rows.Err()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
assignees, err = s.listBlockAssigneesForBoard(boardID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for i = 0; i < len(result); i++ {
|
|
setBlockPropertiesAssignees(&result[i], assignees[result[i].BlockID])
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func normalizeBlockAssigneeIDs(props models.BlockProperties) []string {
|
|
var seen map[string]bool
|
|
var ids []string
|
|
var id string
|
|
var i int
|
|
|
|
seen = map[string]bool{}
|
|
for i = 0; i < len(props.AssigneeIDs); i++ {
|
|
id = strings.TrimSpace(props.AssigneeIDs[i])
|
|
if id == "" || seen[id] {
|
|
continue
|
|
}
|
|
seen[id] = true
|
|
ids = append(ids, id)
|
|
}
|
|
for i = 0; i < len(props.Assignees); i++ {
|
|
id = strings.TrimSpace(props.Assignees[i].ID)
|
|
if id == "" || seen[id] {
|
|
continue
|
|
}
|
|
seen[id] = true
|
|
ids = append(ids, id)
|
|
}
|
|
id = strings.TrimSpace(props.AssigneeID)
|
|
if id != "" && !seen[id] {
|
|
ids = append(ids, id)
|
|
}
|
|
return ids
|
|
}
|
|
|
|
func setBlockPropertiesAssignees(bp *models.BlockProperties, assignees []models.BoardAssignableUser) {
|
|
var i int
|
|
|
|
bp.Assignees = assignees
|
|
bp.AssigneeIDs = []string{}
|
|
bp.AssigneeID = ""
|
|
bp.AssigneeName = ""
|
|
for i = 0; i < len(assignees); i++ {
|
|
bp.AssigneeIDs = append(bp.AssigneeIDs, assignees[i].ID)
|
|
}
|
|
if len(assignees) > 0 {
|
|
bp.AssigneeID = assignees[0].ID
|
|
bp.AssigneeName = assignees[0].DisplayName
|
|
if bp.AssigneeName == "" {
|
|
bp.AssigneeName = assignees[0].Username
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Store) populateBlockPropertiesAssignees(boardID string, bp *models.BlockProperties) error {
|
|
var all map[string][]models.BoardAssignableUser
|
|
var err error
|
|
|
|
all, err = s.listBlockAssigneesForBoard(boardID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
setBlockPropertiesAssignees(bp, all[bp.BlockID])
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) listBlockAssigneesForBoard(boardID string) (map[string][]models.BoardAssignableUser, error) {
|
|
var rows *sql.Rows
|
|
var err error
|
|
var result map[string][]models.BoardAssignableUser
|
|
var blockID string
|
|
var user models.BoardAssignableUser
|
|
|
|
result = map[string][]models.BoardAssignableUser{}
|
|
rows, err = s.Query(`
|
|
SELECT bl.public_id, u.public_id, u.username, u.display_name,
|
|
CASE WHEN u.avatar_storage_path != '' THEN '/api/users/' || u.public_id || '/avatar?v=' || u.avatar_updated_at ELSE '' END
|
|
FROM block_assignees ba
|
|
JOIN blocks bl ON bl.id = ba.block_id
|
|
JOIN boards brd ON brd.id = bl.board_id
|
|
JOIN users u ON u.id = ba.user_id
|
|
WHERE brd.public_id = ? AND brd.delete_at = 0 AND bl.delete_at = 0 AND u.disabled = 0
|
|
ORDER BY ba.assigned_at ASC, u.username ASC`, boardID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
user = models.BoardAssignableUser{}
|
|
err = rows.Scan(&blockID, &user.ID, &user.Username, &user.DisplayName, &user.AvatarURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result[blockID] = append(result[blockID], user)
|
|
}
|
|
return result, rows.Err()
|
|
}
|
|
|
|
func (s *Store) UpsertBlockProperties(ctx context.Context, boardID, blockID string, props models.BlockProperties) (models.BlockProperties, error) {
|
|
var internalBlockID int64
|
|
var err error
|
|
var tx txExecutor
|
|
var owned bool
|
|
var assigneeIDs []string
|
|
var i int
|
|
var result sql.Result
|
|
var affected int64
|
|
|
|
assigneeIDs = normalizeBlockAssigneeIDs(props)
|
|
tx, owned, err = s.beginContext(ctx)
|
|
if err != nil {
|
|
return props, err
|
|
}
|
|
err = tx.QueryRow(`
|
|
SELECT bl.id FROM blocks bl
|
|
JOIN boards brd ON brd.id = bl.board_id
|
|
WHERE brd.public_id = ? AND bl.public_id = ? AND bl.delete_at = 0`,
|
|
boardID, blockID).Scan(&internalBlockID)
|
|
if err == sql.ErrNoRows {
|
|
rollbackIfOwned(tx, owned)
|
|
return props, sql.ErrNoRows
|
|
}
|
|
if err != nil {
|
|
rollbackIfOwned(tx, owned)
|
|
return props, err
|
|
}
|
|
_, err = tx.Exec(`
|
|
INSERT INTO block_properties (block_id, status, card_type, priority, due_date, assignee_id, sprint, description)
|
|
VALUES (?, ?, ?, ?, ?, NULL, ?, ?)
|
|
ON CONFLICT(block_id) DO UPDATE SET
|
|
status = excluded.status,
|
|
card_type = excluded.card_type,
|
|
priority = excluded.priority,
|
|
due_date = excluded.due_date,
|
|
assignee_id = excluded.assignee_id,
|
|
sprint = excluded.sprint,
|
|
description = excluded.description`,
|
|
internalBlockID, props.Status, props.CardType,
|
|
props.Priority, props.DueDate,
|
|
props.Sprint, props.Description)
|
|
if err != nil {
|
|
rollbackIfOwned(tx, owned)
|
|
return props, err
|
|
}
|
|
_, err = tx.Exec(`DELETE FROM block_assignees WHERE block_id = ?`, internalBlockID)
|
|
if err != nil {
|
|
rollbackIfOwned(tx, owned)
|
|
return props, err
|
|
}
|
|
for i = 0; i < len(assigneeIDs); i++ {
|
|
result, err = tx.Exec(`
|
|
INSERT INTO block_assignees (block_id, user_id, assigned_at)
|
|
SELECT ?, u.id, ?
|
|
FROM users u
|
|
WHERE u.public_id = ? AND u.disabled = 0`,
|
|
internalBlockID, time.Now().Unix(), assigneeIDs[i])
|
|
if err != nil {
|
|
rollbackIfOwned(tx, owned)
|
|
return props, err
|
|
}
|
|
affected, err = result.RowsAffected()
|
|
if err != nil {
|
|
rollbackIfOwned(tx, owned)
|
|
return props, err
|
|
}
|
|
if affected == 0 {
|
|
rollbackIfOwned(tx, owned)
|
|
return props, ErrUserNotFound
|
|
}
|
|
}
|
|
|
|
// Keep blocks.fields status in sync so the kanban view can group without extra queries.
|
|
_, err = tx.Exec(`
|
|
UPDATE blocks SET fields = json_set(COALESCE(fields, '{}'), '$.status', ?), updated_at = ?
|
|
WHERE id = ?`, props.Status, time.Now().Unix(), internalBlockID)
|
|
if err != nil {
|
|
rollbackIfOwned(tx, owned)
|
|
return props, err
|
|
}
|
|
err = commitIfOwned(tx, owned)
|
|
if err != nil {
|
|
return props, err
|
|
}
|
|
|
|
return s.GetBlockProperties(boardID, blockID)
|
|
}
|