feat: attachments on tickets and comments
Files are uploaded over REST, stored on disk (a volume in Docker) and recorded on the ticket as events; the bytes are written first, so no attachment ever points at a missing file. Only images, video, audio, PDF and plain text are shown inline, everything with nosniff and a sandbox CSP. The web shows thumbnails, uploads by drag and drop or by pasting a screenshot, and attaches files to new comments. Also adds `nx run api:contracts`, which writes schema.gql and openapi.json without starting the API or any database. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,9 @@ import {
|
||||
SerializableEvent,
|
||||
VersionedAggregateRoot,
|
||||
} from '@task-stream/api/shared/domain';
|
||||
import { Readable } from 'node:stream';
|
||||
import { NotFoundError } from '@task-stream/api/shared/domain';
|
||||
import { AttachmentStorage } from '../lib/ports/attachment-storage';
|
||||
import { IssueNumbersRepository } from '../lib/ports/issue-numbers.repository';
|
||||
import { ProjectKeysRepository } from '../lib/ports/project-keys.repository';
|
||||
import { ReleaseIssuesRepository } from '../lib/ports/release-issues.repository';
|
||||
@@ -244,3 +247,28 @@ export class InMemoryTrackerStreams extends TrackerStreamsRepository {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Attachment storage double: files in a map, keyed like on disk. */
|
||||
export class InMemoryAttachmentStorage extends AttachmentStorage {
|
||||
readonly files = new Map<string, Buffer>();
|
||||
|
||||
async put(key: string, content: Buffer) {
|
||||
this.files.set(key, content);
|
||||
}
|
||||
|
||||
async read(key: string) {
|
||||
const content = this.files.get(key);
|
||||
if (!content) throw new NotFoundError('Attachment', key);
|
||||
return Readable.from(content);
|
||||
}
|
||||
|
||||
async remove(key: string) {
|
||||
this.files.delete(key);
|
||||
}
|
||||
|
||||
async removePrefix(prefix: string) {
|
||||
for (const key of [...this.files.keys()]) {
|
||||
if (key.startsWith(prefix)) this.files.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user