mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-26 14:30:13 +00:00
5e2accd179
* docs(store): the per-call users read in the bot ceiling is accepted; how to remove it if profiled (TASK-3401) * feat(server): app write routes: items create/update, comments create/update/delete, the write rule ahead of the editor fast path, SSE and watch parity (TASK-3401, U6b) * test(server): app write routes: census, read-token refusal, write rule, refusals, disable race, SSE/watch parity (TASK-3401, U6b) * test(server): the write-rule test gives the bot a role and a companion control, so only the rule can refuse (TASK-3401, U6b) * fix(apps): an app write's status notification comes from its own transaction, never the handler's earlier read (TASK-3401, U6b codex r1 P2) * fix(apps): an app item write's response is built from its own transaction: slug, schema, via_app, creator and ETag read before commit, nothing read after (TASK-3401, U6b codex r2 P2 x2) * fix(apps): app comment writes publish the item and actor display their own transaction read, not the handler's earlier read (TASK-3401, U6b codex r3 P1+P3) * docs(server): name the second layer behind the app comment same-item check (TASK-3401, U6b)
140 lines
4.2 KiB
Go
140 lines
4.2 KiB
Go
package appstore
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
|
|
"github.com/PerpetualSoftware/pad/internal/models"
|
|
"github.com/PerpetualSoftware/pad/internal/store"
|
|
)
|
|
|
|
// AppCommentCreate is the only input an app comment create takes.
|
|
type AppCommentCreate struct {
|
|
Body string
|
|
ParentID string
|
|
// Author is the display name recorded on the comment; the server sets it
|
|
// from the actor (U6), never from the app's request.
|
|
Author string
|
|
}
|
|
|
|
// A comment body may carry a pad-attachment: reference, unlike an item's
|
|
// content, but only to an attachment already bound to the comment's item
|
|
// (DOC-3371 §4). The store checks that under the fence
|
|
// (store.ErrAppAttachmentUnavailable); nothing here inspects the reference.
|
|
|
|
func refuseBlankBody(body string) error {
|
|
if strings.TrimSpace(body) == "" {
|
|
return refuse("body is required")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CommentWrite is a committed app comment write with the item it is on and
|
|
// the actor's display name, both read in the same transaction: what the
|
|
// write publishes describes the item as the write found it.
|
|
type CommentWrite struct {
|
|
Comment *models.Comment
|
|
Item *models.Item
|
|
ActorDisplay string
|
|
}
|
|
|
|
// CreateComment writes a comment on a companion item, in one FencedTx.
|
|
func (a *Store) CreateComment(ctx context.Context, spec store.FenceSpec, itemID string, in AppCommentCreate, actor store.FencedActor) (*models.Comment, error) {
|
|
w, err := a.CreateCommentWrite(ctx, spec, itemID, in, actor)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return w.Comment, nil
|
|
}
|
|
|
|
// CreateCommentWrite is CreateComment with the item and actor display. An
|
|
// empty Author is filled with the actor's display name, read in the
|
|
// transaction.
|
|
func (a *Store) CreateCommentWrite(ctx context.Context, spec store.FenceSpec, itemID string, in AppCommentCreate, actor store.FencedActor) (*CommentWrite, error) {
|
|
if err := refuseBlankBody(in.Body); err != nil {
|
|
return nil, err
|
|
}
|
|
ftx, err := a.s.BeginFenced(ctx, spec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = ftx.Rollback() }()
|
|
|
|
who, err := ftx.ActorDisplay(actor)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
author := in.Author
|
|
if author == "" {
|
|
author = who
|
|
}
|
|
c, err := ftx.CreateComment(store.FencedCommentCreate{
|
|
ItemID: itemID, ParentID: in.ParentID, Body: in.Body, Author: author, Actor: actor,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
item, err := ftx.Item(itemID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := ftx.Commit(); err != nil {
|
|
return nil, err
|
|
}
|
|
return &CommentWrite{Comment: c, Item: item, ActorDisplay: who}, nil
|
|
}
|
|
|
|
// UpdateComment replaces the body of a comment this install wrote as this
|
|
// actor, addressed through its item, in one FencedTx.
|
|
func (a *Store) UpdateComment(ctx context.Context, spec store.FenceSpec, itemID, commentID, body string, actor store.FencedActor) (*models.Comment, error) {
|
|
w, err := a.UpdateCommentWrite(ctx, spec, itemID, commentID, body, actor)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return w.Comment, nil
|
|
}
|
|
|
|
// UpdateCommentWrite is UpdateComment with the item and actor display.
|
|
func (a *Store) UpdateCommentWrite(ctx context.Context, spec store.FenceSpec, itemID, commentID, body string, actor store.FencedActor) (*CommentWrite, error) {
|
|
if err := refuseBlankBody(body); err != nil {
|
|
return nil, err
|
|
}
|
|
ftx, err := a.s.BeginFenced(ctx, spec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = ftx.Rollback() }()
|
|
|
|
c, err := ftx.UpdateComment(itemID, commentID, body, actor)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
item, err := ftx.Item(itemID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
who, err := ftx.ActorDisplay(actor)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := ftx.Commit(); err != nil {
|
|
return nil, err
|
|
}
|
|
return &CommentWrite{Comment: c, Item: item, ActorDisplay: who}, nil
|
|
}
|
|
|
|
// DeleteComment deletes (or tombstones) a comment this install wrote as this
|
|
// actor, addressed through its item, in one FencedTx.
|
|
func (a *Store) DeleteComment(ctx context.Context, spec store.FenceSpec, itemID, commentID string, actor store.FencedActor) error {
|
|
ftx, err := a.s.BeginFenced(ctx, spec)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = ftx.Rollback() }()
|
|
|
|
if err := ftx.DeleteComment(itemID, commentID, actor); err != nil {
|
|
return err
|
|
}
|
|
return ftx.Commit()
|
|
}
|