Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-274Done

5.2.2 `attachmentsService` management surface — attach-to-issue, paged list, permission-gated delete w/ blob cleanup + revision trail, routes

Estimate: 30m · Depends on: 5.2.1

The management half of the service (the upload half is 2.3.7, reused verbatim). Per the 4-layer rule: extend lib/services/attachmentsService.ts; HTTP-only routes; typed errors in lib/blob/errors.ts (extend the 2.3.7 set); DTOs + mappers.

attachToWorkItem(workItemId, file, ctx) — the panel upload: validates the caller can view the issue AND holds a creating role (admin/member; read-only viewer → typed forbidden — Jira's "Create attachments" on the 6.4 roles), delegates to the 2.3.7 uploadAttachment gates (size/MIME/rate — NOT re-implemented), then links the row source: 'panel' and writes the work_item_revision attachment-added entry in the same tx.

listForWorkItem(workItemId, { cursor }, ctx) — view-gated, cursor-paged (take 50, newest first) + totalCount; DTO carries id, filename, mime, size, createdAt, uploader (id/name), source, blobUrl, and isImage/isPdf flags for the card + preview affordances. Bounded always (finding #57).

deleteAttachment(id, ctx) — the Jira permission split: uploader deletes own; project admin + workspace admin/owner delete all; viewer never. Editor-sourced rows are REJECTED with a typed error (AttachmentEditorSourcedError → 409): the mirror blocks panel-deleting comment/field-sourced files, and the block is what prevents the broken-embed hole. Order of operations: row delete + the revision attachment-removed entry in ONE tx; the blob delete AFTER commit, best-effort (a blob-store failure must not un-delete the row; the 5.2.7 GC is the backstop for stranded blobs). Hard delete — no tombstone.

Routes: GET/POST /api/work-items/[id]/attachments, DELETE /api/attachments/[id] — parse → one service call → typed-error mapping (403 role / 404 cross-workspace per finding #44 / 409 editor-sourced / the 2.3.7 413/415/429 pass-through).

Acceptance criteria

  • attachToWorkItem reuses the 2.3.7 gates (no duplicated validation), links source: 'panel', records the revision entry, and is role-gated (viewer forbidden).
  • listForWorkItem is cursor-paged + counted, view-gated, cross-workspace → 404; no unbounded read exists.
  • deleteAttachment enforces own/all per the role matrix, rejects editor-sourced rows with the typed 409, removes row + revision entry transactionally and the blob post-commit best-effort; a simulated blob failure leaves the row deleted and the blob GC-sweepable.
  • Both History entries (added/removed) appear in the revision trail with actor + filename (covers editor-sourced adds too via 5.2.3 — the trail is uniform).
  • Routes are HTTP-only; pnpm test:coverage ≥90% on the touched service/repo files.

Context refs

  • lib/services/attachmentsService.ts + lib/blob/allowlist.ts + lib/blob/errors.ts (2.3.7 — the primitive + typed-error vocabulary to extend)
  • lib/services/assignableMembersService.ts + the 6.4 role checks (the same gating 5.1.2 uses); finding #44 (404-not-403)
  • lib/services/workItemRevisionsService.ts (1.4.6) — the History entries
  • The verified Jira contract in the Story 5.2 description (three permissions; editor-sourced block; hard delete)