mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 21:39:54 +02:00
* MUL-3130: persist a stable attachment download URL in comment markdown Comment image attachments rendered as broken placeholders ~30 minutes after upload because the editor was persisting a short-lived HMAC-signed URL into the comment body. After PR #3903 (MUL-3132) hardened /uploads/* with auth, `attachmentToResponse` started signing `attachment.url` as `/uploads/<key>?exp=<unix>&sig=<HMAC>` for LocalStorage so token-auth clients could keep loading inline images. The signature has a 30-min TTL by design — but `useFileUpload` was returning that signed value as `link` and the editor was writing `` straight into the markdown, so the comment permanently captured a URL that stopped working as soon as the signature expired. The fix is to persist a stable per-attachment URL that the server can re-sign on every request: * `useFileUpload` now returns `link = /api/attachments/<id>/download` (avatar uploads without an id still fall back to `att.url` so the pre-attachment-row code paths keep working). * `DownloadAttachment` self-resolves the workspace from the attachment row instead of reading X-Workspace-Slug / X-Workspace-ID headers, and the route is registered under the auth-only group so a native browser <img>/<video> resource load (which cannot attach those headers) succeeds. Membership is checked inside the handler with a 404 deny shape so the route does not act as an IDOR oracle. * A new `GetAttachmentByIDOnly` SQL query supports the workspace- derivation step. * `AttachmentDownloadProvider` now extracts the attachment id from the stable URL when matching markdown refs to attachment records, with a fallback to the existing url-equality check for legacy comments (and S3/CloudFront markdown that points straight at the CDN). * `contentReferencesAttachment` covers both URL shapes for the composer / standalone-list dedup paths so an attachment uploaded before the fix and one uploaded after both deduplicate cleanly. Tests: - New unit tests for the URL helpers (16 tests, packages/core). - Backend regression test: bare `<img src>`-style request without workspace headers now succeeds for a member (200) and 404s for a non-member, replacing the previous "400 without workspace context" contract. - Existing TestDownload*, TestServeLocalUpload*, TestAttachmentTo Response* and the 1220 frontend views tests all pass. Refs: MUL-3130, GitHub issue #3891 Co-authored-by: multica-agent <github@multica.ai> * MUL-3130: address PR review — split markdown link from upload link, swap render src Two follow-ups from GPT-Boy's review on PR #3937. (1) Don't reroute every upload consumer through the workspace-gated download endpoint. The previous change made `useFileUpload`'s `link` field unconditionally return `/api/attachments/<id>/download` whenever the upload had an id. But `useFileUpload` is also used by avatar / logo pickers (account-tab, workspace-tab, agents/avatar-picker, squads/squad-detail-page) that persist `result.link` directly into `avatar_url`. Avatars are referenced cross-workspace (mention chips, member lists, inbox items), so binding their URL to a workspace-membership-gated endpoint would silently break cross-workspace avatar visibility. The fix splits the URL into two semantically distinct fields: - `link` — same as `att.url` (legacy contract). Avatar / logo callers continue to use this and remain on whatever URL semantics the storage backend dictates. - `markdownLink` — the stable per-attachment URL `/api/attachments/<id>/download`. Only the editor's markdown-persisting flow consumes this. Falls back to `link` for the no-workspace upload branch (where there is no attachment-row id to address). `editor/extensions/file-upload.ts` switches `image.src` and `fileCard.href` to `markdownLink ?? link` so comment markdown gets the stable shape while avatar callers stay on `link` unchanged. (2) Make the render-time img src loadable for token-mode clients. Persisting the stable `/api/attachments/<id>/download` URL fixes the expiry problem but the path itself sits behind `middleware.Auth`, which expects either a `multica_auth` cookie or a Bearer token in `Authorization`. Native `<img>`/`<video>` resource loads from token-mode clients (Electron's default mode, the mobile app, legacy-token web sessions) cannot attach the Authorization header, so the bare URL would 401 immediately rather than 30 minutes later. `Attachment.normalize` now runs the resolved record through a new `pickInlineMediaURL` helper that returns: - `record.download_url` when it's an absolute URL with a recognised CDN signature query (CloudFront-signed `Signature` / `Expires` / `Key-Pair-Id`, or `X-Amz-Signature` for raw S3 presigns) — these load as native resource src in any client. - else `record.url`, which on the LocalStorage backend carries a freshly-minted `/uploads/<key>?exp&sig` query whose signature IS the auth (token-mode-loadable). On non-CF S3 backends this is the raw stored URL — same behaviour as today. - else the original input URL (legacy / unresolved markdown keeps its existing path). This gives the same effect for both `kind: "record"` and `kind: "url"` attachment inputs: once a record is in hand, the rendered media src is whichever URL the current backend exposes a working signature on. Tests: - New `file-upload.test.ts` regression pinning that `markdownLink` is what lands in the markdown body when the upload result returns both a short-lived storage URL and a stable download path. - Updated `attachment.test.tsx` to reflect the new render-time swap (the rendered img src now follows the freshly signed URL, not the raw storage URL) and added a record-mode regression pinning the LocalStorage default — when `download_url` is the bare /api/attachments/<id>/download path, the renderer must fall through to the signed `record.url`. - Updated `chat-input.test.tsx` makeUpload helper for the new `markdownLink` UploadResult field. - 1222 frontend views tests + 507 core tests + typecheck across @multica/{core,ui,views} all pass. Refs: MUL-3130, GitHub issue #3891. Builds ona740f7a35. Co-authored-by: multica-agent <github@multica.ai> * MUL-3130: chat upload map keys on persisted markdownLink, not the short-lived link GPT-Boy's second-round review on PR #3937 caught a chat-only blocker left over from the previous fix. After the previous commit split `UploadResult.link` into `link` (legacy avatar/logo URL) and `markdownLink` (stable per-attachment URL persisted into markdown), the comment editor's image src + file card href correctly switched to `markdownLink ?? link`. But chat input still kept the upload-map key on the old `link`: uploadMapRef.current.set(result.link, result.id) … if (content.includes(url)) activeIds.push(id) In the LocalStorage backend `link` is the short-lived `/uploads/<key>?exp=&sig=` URL. The editor persists the stable `/api/attachments/<id>/download` URL into the message body, so `content.includes(url)` never matches and the send call drops `attachment_ids`. The attachment ends up bound only to the chat session, not to the message — agents reading message-level metadata see no attachments. Fix: key the upload map on the same value the editor actually wrote into the markdown body (`markdownLink || link`). The `content.includes(url)` check then matches and the attachment id is correctly forwarded on send. Tests: - Updated the chat-input mock editor to insert `markdownLink || link` into its value, mirroring the real editor's persisted-URL choice (uploadAndInsertFile in editor/extensions/file-upload.ts). Without this the mock would silently paper over the bug. - Added a regression test where the upload result returns a short-lived `link = /uploads/...?exp&sig` and a stable `markdownLink = /api/attachments/<id>/download`. Asserts (a) the message body carries the stable URL and never the signed query, and (b) the bound `attachment_ids` includes the attachment id. All 1223 frontend views tests pass (was 1222, +1 new regression). Typecheck and 507 core tests still green. Refs: MUL-3130, PR #3937 review by GPT-Boy. Builds onf66a522d0. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai>
506 lines
14 KiB
Go
506 lines
14 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.31.1
|
|
// source: attachment.sql
|
|
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const createAttachment = `-- name: CreateAttachment :one
|
|
INSERT INTO attachment (
|
|
id, workspace_id, issue_id, comment_id, chat_session_id,
|
|
uploader_type, uploader_id, filename, url, content_type, size_bytes
|
|
)
|
|
VALUES (
|
|
$1, $2, $9, $10, $11,
|
|
$3, $4, $5, $6, $7, $8
|
|
)
|
|
RETURNING id, workspace_id, issue_id, comment_id, uploader_type, uploader_id, filename, url, content_type, size_bytes, created_at, chat_session_id, chat_message_id
|
|
`
|
|
|
|
type CreateAttachmentParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
UploaderType string `json:"uploader_type"`
|
|
UploaderID pgtype.UUID `json:"uploader_id"`
|
|
Filename string `json:"filename"`
|
|
Url string `json:"url"`
|
|
ContentType string `json:"content_type"`
|
|
SizeBytes int64 `json:"size_bytes"`
|
|
IssueID pgtype.UUID `json:"issue_id"`
|
|
CommentID pgtype.UUID `json:"comment_id"`
|
|
ChatSessionID pgtype.UUID `json:"chat_session_id"`
|
|
}
|
|
|
|
func (q *Queries) CreateAttachment(ctx context.Context, arg CreateAttachmentParams) (Attachment, error) {
|
|
row := q.db.QueryRow(ctx, createAttachment,
|
|
arg.ID,
|
|
arg.WorkspaceID,
|
|
arg.UploaderType,
|
|
arg.UploaderID,
|
|
arg.Filename,
|
|
arg.Url,
|
|
arg.ContentType,
|
|
arg.SizeBytes,
|
|
arg.IssueID,
|
|
arg.CommentID,
|
|
arg.ChatSessionID,
|
|
)
|
|
var i Attachment
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.IssueID,
|
|
&i.CommentID,
|
|
&i.UploaderType,
|
|
&i.UploaderID,
|
|
&i.Filename,
|
|
&i.Url,
|
|
&i.ContentType,
|
|
&i.SizeBytes,
|
|
&i.CreatedAt,
|
|
&i.ChatSessionID,
|
|
&i.ChatMessageID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const deleteAttachment = `-- name: DeleteAttachment :exec
|
|
DELETE FROM attachment WHERE id = $1 AND workspace_id = $2
|
|
`
|
|
|
|
type DeleteAttachmentParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
func (q *Queries) DeleteAttachment(ctx context.Context, arg DeleteAttachmentParams) error {
|
|
_, err := q.db.Exec(ctx, deleteAttachment, arg.ID, arg.WorkspaceID)
|
|
return err
|
|
}
|
|
|
|
const getAttachment = `-- name: GetAttachment :one
|
|
SELECT id, workspace_id, issue_id, comment_id, uploader_type, uploader_id, filename, url, content_type, size_bytes, created_at, chat_session_id, chat_message_id FROM attachment
|
|
WHERE id = $1 AND workspace_id = $2
|
|
`
|
|
|
|
type GetAttachmentParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
func (q *Queries) GetAttachment(ctx context.Context, arg GetAttachmentParams) (Attachment, error) {
|
|
row := q.db.QueryRow(ctx, getAttachment, arg.ID, arg.WorkspaceID)
|
|
var i Attachment
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.IssueID,
|
|
&i.CommentID,
|
|
&i.UploaderType,
|
|
&i.UploaderID,
|
|
&i.Filename,
|
|
&i.Url,
|
|
&i.ContentType,
|
|
&i.SizeBytes,
|
|
&i.CreatedAt,
|
|
&i.ChatSessionID,
|
|
&i.ChatMessageID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getAttachmentByIDOnly = `-- name: GetAttachmentByIDOnly :one
|
|
SELECT id, workspace_id, issue_id, comment_id, uploader_type, uploader_id, filename, url, content_type, size_bytes, created_at, chat_session_id, chat_message_id FROM attachment
|
|
WHERE id = $1
|
|
`
|
|
|
|
// Used by the download endpoint, which derives workspace context from the
|
|
// attachment row itself rather than from request headers/query params. The
|
|
// caller still has to verify the requester is a member of the returned
|
|
// workspace_id before serving the bytes — this query is access-neutral on
|
|
// purpose so a self-contained URL like /api/attachments/{id}/download can
|
|
// work as a native <img>/<video> resource load (no header attachment).
|
|
func (q *Queries) GetAttachmentByIDOnly(ctx context.Context, id pgtype.UUID) (Attachment, error) {
|
|
row := q.db.QueryRow(ctx, getAttachmentByIDOnly, id)
|
|
var i Attachment
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.IssueID,
|
|
&i.CommentID,
|
|
&i.UploaderType,
|
|
&i.UploaderID,
|
|
&i.Filename,
|
|
&i.Url,
|
|
&i.ContentType,
|
|
&i.SizeBytes,
|
|
&i.CreatedAt,
|
|
&i.ChatSessionID,
|
|
&i.ChatMessageID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const linkAttachmentsToChatMessage = `-- name: LinkAttachmentsToChatMessage :exec
|
|
UPDATE attachment
|
|
SET chat_message_id = $1
|
|
WHERE chat_session_id = $2
|
|
AND chat_message_id IS NULL
|
|
AND id = ANY($3::uuid[])
|
|
`
|
|
|
|
type LinkAttachmentsToChatMessageParams struct {
|
|
ChatMessageID pgtype.UUID `json:"chat_message_id"`
|
|
ChatSessionID pgtype.UUID `json:"chat_session_id"`
|
|
Column3 []pgtype.UUID `json:"column_3"`
|
|
}
|
|
|
|
func (q *Queries) LinkAttachmentsToChatMessage(ctx context.Context, arg LinkAttachmentsToChatMessageParams) error {
|
|
_, err := q.db.Exec(ctx, linkAttachmentsToChatMessage, arg.ChatMessageID, arg.ChatSessionID, arg.Column3)
|
|
return err
|
|
}
|
|
|
|
const linkAttachmentsToComment = `-- name: LinkAttachmentsToComment :exec
|
|
UPDATE attachment
|
|
SET comment_id = $1
|
|
WHERE issue_id = $2
|
|
AND comment_id IS NULL
|
|
AND id = ANY($3::uuid[])
|
|
`
|
|
|
|
type LinkAttachmentsToCommentParams struct {
|
|
CommentID pgtype.UUID `json:"comment_id"`
|
|
IssueID pgtype.UUID `json:"issue_id"`
|
|
Column3 []pgtype.UUID `json:"column_3"`
|
|
}
|
|
|
|
func (q *Queries) LinkAttachmentsToComment(ctx context.Context, arg LinkAttachmentsToCommentParams) error {
|
|
_, err := q.db.Exec(ctx, linkAttachmentsToComment, arg.CommentID, arg.IssueID, arg.Column3)
|
|
return err
|
|
}
|
|
|
|
const linkAttachmentsToIssue = `-- name: LinkAttachmentsToIssue :exec
|
|
UPDATE attachment
|
|
SET issue_id = $1
|
|
WHERE workspace_id = $2
|
|
AND issue_id IS NULL
|
|
AND id = ANY($3::uuid[])
|
|
`
|
|
|
|
type LinkAttachmentsToIssueParams struct {
|
|
IssueID pgtype.UUID `json:"issue_id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Column3 []pgtype.UUID `json:"column_3"`
|
|
}
|
|
|
|
func (q *Queries) LinkAttachmentsToIssue(ctx context.Context, arg LinkAttachmentsToIssueParams) error {
|
|
_, err := q.db.Exec(ctx, linkAttachmentsToIssue, arg.IssueID, arg.WorkspaceID, arg.Column3)
|
|
return err
|
|
}
|
|
|
|
const listAttachmentURLsByCommentID = `-- name: ListAttachmentURLsByCommentID :many
|
|
SELECT url FROM attachment
|
|
WHERE comment_id = $1
|
|
`
|
|
|
|
func (q *Queries) ListAttachmentURLsByCommentID(ctx context.Context, commentID pgtype.UUID) ([]string, error) {
|
|
rows, err := q.db.Query(ctx, listAttachmentURLsByCommentID, commentID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []string{}
|
|
for rows.Next() {
|
|
var url string
|
|
if err := rows.Scan(&url); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, url)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listAttachmentURLsByIssueOrComments = `-- name: ListAttachmentURLsByIssueOrComments :many
|
|
SELECT a.url FROM attachment a
|
|
WHERE a.issue_id = $1
|
|
OR a.comment_id IN (SELECT c.id FROM comment c WHERE c.issue_id = $1)
|
|
`
|
|
|
|
func (q *Queries) ListAttachmentURLsByIssueOrComments(ctx context.Context, issueID pgtype.UUID) ([]string, error) {
|
|
rows, err := q.db.Query(ctx, listAttachmentURLsByIssueOrComments, issueID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []string{}
|
|
for rows.Next() {
|
|
var url string
|
|
if err := rows.Scan(&url); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, url)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listAttachmentsByChatMessage = `-- name: ListAttachmentsByChatMessage :many
|
|
SELECT id, workspace_id, issue_id, comment_id, uploader_type, uploader_id, filename, url, content_type, size_bytes, created_at, chat_session_id, chat_message_id FROM attachment
|
|
WHERE chat_message_id = $1 AND workspace_id = $2
|
|
ORDER BY created_at ASC
|
|
`
|
|
|
|
type ListAttachmentsByChatMessageParams struct {
|
|
ChatMessageID pgtype.UUID `json:"chat_message_id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
func (q *Queries) ListAttachmentsByChatMessage(ctx context.Context, arg ListAttachmentsByChatMessageParams) ([]Attachment, error) {
|
|
rows, err := q.db.Query(ctx, listAttachmentsByChatMessage, arg.ChatMessageID, arg.WorkspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []Attachment{}
|
|
for rows.Next() {
|
|
var i Attachment
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.IssueID,
|
|
&i.CommentID,
|
|
&i.UploaderType,
|
|
&i.UploaderID,
|
|
&i.Filename,
|
|
&i.Url,
|
|
&i.ContentType,
|
|
&i.SizeBytes,
|
|
&i.CreatedAt,
|
|
&i.ChatSessionID,
|
|
&i.ChatMessageID,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listAttachmentsByChatMessageIDs = `-- name: ListAttachmentsByChatMessageIDs :many
|
|
SELECT id, workspace_id, issue_id, comment_id, uploader_type, uploader_id, filename, url, content_type, size_bytes, created_at, chat_session_id, chat_message_id FROM attachment
|
|
WHERE chat_message_id = ANY($1::uuid[]) AND workspace_id = $2
|
|
ORDER BY created_at ASC
|
|
`
|
|
|
|
type ListAttachmentsByChatMessageIDsParams struct {
|
|
Column1 []pgtype.UUID `json:"column_1"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
func (q *Queries) ListAttachmentsByChatMessageIDs(ctx context.Context, arg ListAttachmentsByChatMessageIDsParams) ([]Attachment, error) {
|
|
rows, err := q.db.Query(ctx, listAttachmentsByChatMessageIDs, arg.Column1, arg.WorkspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []Attachment{}
|
|
for rows.Next() {
|
|
var i Attachment
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.IssueID,
|
|
&i.CommentID,
|
|
&i.UploaderType,
|
|
&i.UploaderID,
|
|
&i.Filename,
|
|
&i.Url,
|
|
&i.ContentType,
|
|
&i.SizeBytes,
|
|
&i.CreatedAt,
|
|
&i.ChatSessionID,
|
|
&i.ChatMessageID,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listAttachmentsByComment = `-- name: ListAttachmentsByComment :many
|
|
SELECT id, workspace_id, issue_id, comment_id, uploader_type, uploader_id, filename, url, content_type, size_bytes, created_at, chat_session_id, chat_message_id FROM attachment
|
|
WHERE comment_id = $1 AND workspace_id = $2
|
|
ORDER BY created_at ASC
|
|
`
|
|
|
|
type ListAttachmentsByCommentParams struct {
|
|
CommentID pgtype.UUID `json:"comment_id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
func (q *Queries) ListAttachmentsByComment(ctx context.Context, arg ListAttachmentsByCommentParams) ([]Attachment, error) {
|
|
rows, err := q.db.Query(ctx, listAttachmentsByComment, arg.CommentID, arg.WorkspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []Attachment{}
|
|
for rows.Next() {
|
|
var i Attachment
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.IssueID,
|
|
&i.CommentID,
|
|
&i.UploaderType,
|
|
&i.UploaderID,
|
|
&i.Filename,
|
|
&i.Url,
|
|
&i.ContentType,
|
|
&i.SizeBytes,
|
|
&i.CreatedAt,
|
|
&i.ChatSessionID,
|
|
&i.ChatMessageID,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listAttachmentsByCommentIDs = `-- name: ListAttachmentsByCommentIDs :many
|
|
SELECT id, workspace_id, issue_id, comment_id, uploader_type, uploader_id, filename, url, content_type, size_bytes, created_at, chat_session_id, chat_message_id FROM attachment
|
|
WHERE comment_id = ANY($1::uuid[]) AND workspace_id = $2
|
|
ORDER BY created_at ASC
|
|
`
|
|
|
|
type ListAttachmentsByCommentIDsParams struct {
|
|
Column1 []pgtype.UUID `json:"column_1"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
func (q *Queries) ListAttachmentsByCommentIDs(ctx context.Context, arg ListAttachmentsByCommentIDsParams) ([]Attachment, error) {
|
|
rows, err := q.db.Query(ctx, listAttachmentsByCommentIDs, arg.Column1, arg.WorkspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []Attachment{}
|
|
for rows.Next() {
|
|
var i Attachment
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.IssueID,
|
|
&i.CommentID,
|
|
&i.UploaderType,
|
|
&i.UploaderID,
|
|
&i.Filename,
|
|
&i.Url,
|
|
&i.ContentType,
|
|
&i.SizeBytes,
|
|
&i.CreatedAt,
|
|
&i.ChatSessionID,
|
|
&i.ChatMessageID,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listAttachmentsByIssue = `-- name: ListAttachmentsByIssue :many
|
|
SELECT id, workspace_id, issue_id, comment_id, uploader_type, uploader_id, filename, url, content_type, size_bytes, created_at, chat_session_id, chat_message_id FROM attachment
|
|
WHERE issue_id = $1 AND workspace_id = $2
|
|
ORDER BY created_at ASC
|
|
`
|
|
|
|
type ListAttachmentsByIssueParams struct {
|
|
IssueID pgtype.UUID `json:"issue_id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
func (q *Queries) ListAttachmentsByIssue(ctx context.Context, arg ListAttachmentsByIssueParams) ([]Attachment, error) {
|
|
rows, err := q.db.Query(ctx, listAttachmentsByIssue, arg.IssueID, arg.WorkspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []Attachment{}
|
|
for rows.Next() {
|
|
var i Attachment
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.IssueID,
|
|
&i.CommentID,
|
|
&i.UploaderType,
|
|
&i.UploaderID,
|
|
&i.Filename,
|
|
&i.Url,
|
|
&i.ContentType,
|
|
&i.SizeBytes,
|
|
&i.CreatedAt,
|
|
&i.ChatSessionID,
|
|
&i.ChatMessageID,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const replaceCommentAttachments = `-- name: ReplaceCommentAttachments :exec
|
|
UPDATE attachment
|
|
SET comment_id = CASE
|
|
WHEN id = ANY($3::uuid[]) THEN $1
|
|
ELSE NULL
|
|
END
|
|
WHERE issue_id = $2
|
|
AND (
|
|
comment_id = $1
|
|
OR (comment_id IS NULL AND id = ANY($3::uuid[]))
|
|
)
|
|
`
|
|
|
|
type ReplaceCommentAttachmentsParams struct {
|
|
CommentID pgtype.UUID `json:"comment_id"`
|
|
IssueID pgtype.UUID `json:"issue_id"`
|
|
AttachmentIds []pgtype.UUID `json:"attachment_ids"`
|
|
}
|
|
|
|
func (q *Queries) ReplaceCommentAttachments(ctx context.Context, arg ReplaceCommentAttachmentsParams) error {
|
|
_, err := q.db.Exec(ctx, replaceCommentAttachments, arg.CommentID, arg.IssueID, arg.AttachmentIds)
|
|
return err
|
|
}
|