Skip to content

Commit

Permalink
feat: impl attach media to note
Browse files Browse the repository at this point in the history
  • Loading branch information
laminne committed Jun 23, 2024
1 parent 4bdbce3 commit 007e57c
Show file tree
Hide file tree
Showing 8 changed files with 265 additions and 6 deletions.
39 changes: 39 additions & 0 deletions pkg/notes/adaptor/repository/dummy.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Option, Result } from '@mikuroxina/mini-fn';

import type { AccountID } from '../../../accounts/model/account.js';
import { type Medium, type MediumID } from '../../../drive/model/medium.js';
import { Bookmark } from '../../model/bookmark.js';
import { type Note, type NoteID } from '../../model/note.js';
import type {
BookmarkRepository,
NoteAttachmentRepository,
NoteRepository,
} from '../../model/repository.js';

Expand Down Expand Up @@ -124,3 +126,40 @@ export class InMemoryBookmarkRepository implements BookmarkRepository {
return Promise.resolve(Option.some(bookmarks));
}
}

export class InMemoryNoteAttachmentRepository
implements NoteAttachmentRepository
{
private readonly attachments: Map<NoteID, MediumID[]>;
private readonly medium: Map<MediumID, Medium>;

constructor(medium: Medium[]) {
this.attachments = new Map();
this.medium = new Map(medium.map((m) => [m.getId(), m]));
}

async create(
noteID: NoteID,
attachmentFileID: MediumID[],
): Promise<Result.Result<Error, void>> {
if (attachmentFileID.map((v) => this.medium.has(v)).includes(false)) {
return Result.err(new Error('medium not found'));
}

Check warning on line 147 in pkg/notes/adaptor/repository/dummy.ts

View check run for this annotation

Codecov / codecov/patch

pkg/notes/adaptor/repository/dummy.ts#L146-L147

Added lines #L146 - L147 were not covered by tests

this.attachments.set(noteID, attachmentFileID);
return Result.ok(undefined);
}

async findByNoteID(noteID: NoteID): Promise<Result.Result<Error, Medium[]>> {
const attachment = this.attachments.get(noteID);
if (!attachment) {
return Result.err(new Error('attachment not found'));
}

// ToDo: make filter more safe (may be fix at TypeScript 5.4)
const res = attachment
.map((id) => this.medium.get(id))
.filter((v): v is Medium => Boolean(v));
return Result.ok(res);
}

Check warning on line 164 in pkg/notes/adaptor/repository/dummy.ts

View check run for this annotation

Codecov / codecov/patch

pkg/notes/adaptor/repository/dummy.ts#L154-L164

Added lines #L154 - L164 were not covered by tests
}
68 changes: 68 additions & 0 deletions pkg/notes/adaptor/repository/prisma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { type Prisma, type PrismaClient } from '@prisma/client';

import type { AccountID } from '../../../accounts/model/account.js';
import type { prismaClient } from '../../../adaptors/prisma.js';
import { Medium, type MediumID } from '../../../drive/model/medium.js';

Check warning on line 6 in pkg/notes/adaptor/repository/prisma.ts

View check run for this annotation

Codecov / codecov/patch

pkg/notes/adaptor/repository/prisma.ts#L6

Added line #L6 was not covered by tests
import { Bookmark } from '../../model/bookmark.js';
import { Note, type NoteID, type NoteVisibility } from '../../model/note.js';
import type {
BookmarkRepository,
NoteAttachmentRepository,

Check warning on line 11 in pkg/notes/adaptor/repository/prisma.ts

View check run for this annotation

Codecov / codecov/patch

pkg/notes/adaptor/repository/prisma.ts#L11

Added line #L11 was not covered by tests
NoteRepository,
} from '../../model/repository.js';

Expand Down Expand Up @@ -239,3 +241,69 @@ export class PrismaBookmarkRepository implements BookmarkRepository {
}
}
}

type DeserializeNoteAttachmentArgs = Prisma.PromiseReturnType<
typeof prismaClient.noteAttachment.findMany<{ include: { medium: true } }>
>;

export class PrismaNoteAttachmentRepository
implements NoteAttachmentRepository
{
constructor(private readonly client: PrismaClient) {}

private deserialize(data: DeserializeNoteAttachmentArgs): Medium[] {
data.map((v) => {
const medium = v.medium;
return Medium.reconstruct({
authorId: medium.authorId as AccountID,
hash: medium.hash,
id: medium.id as MediumID,
mime: medium.mime,
name: medium.name,
nsfw: medium.nsfw,
thumbnailUrl: medium.thumbnailUrl,
url: medium.url,
});
});

return [];
}

async create(
noteID: NoteID,
attachmentFileID: MediumID[],
): Promise<Result.Result<Error, void>> {
const data = attachmentFileID.map((v) => {
return {
noteId: noteID,
mediumId: v,
alt: '',
};
});

try {
await this.client.noteAttachment.createMany({
data,
});
return Result.ok(undefined);
} catch (e) {
return Result.err(e as Error);
}
}

async findByNoteID(noteID: NoteID): Promise<Result.Result<Error, Medium[]>> {
try {
const res = await this.client.noteAttachment.findMany({
where: {
noteId: noteID,
},
include: {
medium: true,
},
});
return Result.ok(this.deserialize(res));
} catch (e) {
return Result.err(e as Error);
}
}
}

Check warning on line 309 in pkg/notes/adaptor/repository/prisma.ts

View check run for this annotation

Codecov / codecov/patch

pkg/notes/adaptor/repository/prisma.ts#L244-L309

Added lines #L244 - L309 were not covered by tests
17 changes: 15 additions & 2 deletions pkg/notes/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ import { BookmarkController } from './adaptor/controller/bookmark.js';
import { NoteController } from './adaptor/controller/note.js';
import {
InMemoryBookmarkRepository,
InMemoryNoteAttachmentRepository,

Check warning on line 18 in pkg/notes/mod.ts

View check run for this annotation

Codecov / codecov/patch

pkg/notes/mod.ts#L18

Added line #L18 was not covered by tests
InMemoryNoteRepository,
} from './adaptor/repository/dummy.js';
import {
PrismaBookmarkRepository,
PrismaNoteAttachmentRepository,

Check warning on line 23 in pkg/notes/mod.ts

View check run for this annotation

Codecov / codecov/patch

pkg/notes/mod.ts#L23

Added line #L23 was not covered by tests
PrismaNoteRepository,
} from './adaptor/repository/prisma.js';
import {
Expand All @@ -45,6 +47,9 @@ const noteRepository = isProduction
const bookmarkRepository = isProduction
? new PrismaBookmarkRepository(prismaClient)
: new InMemoryBookmarkRepository();
const attachmentRepository = isProduction
? new PrismaNoteAttachmentRepository(prismaClient)
: new InMemoryNoteAttachmentRepository([]);

Check warning on line 52 in pkg/notes/mod.ts

View check run for this annotation

Codecov / codecov/patch

pkg/notes/mod.ts#L50-L52

Added lines #L50 - L52 were not covered by tests
const idGenerator = new SnowflakeIDGenerator(0, {
now: () => BigInt(Date.now()),
});
Expand All @@ -66,9 +71,17 @@ const AuthMiddleware = await Ether.runEtherT(
const accountModule = new AccountModule();

// Note
const createService = new CreateService(noteRepository, idGenerator);
const createService = new CreateService(
noteRepository,
idGenerator,
attachmentRepository,
);

Check warning on line 78 in pkg/notes/mod.ts

View check run for this annotation

Codecov / codecov/patch

pkg/notes/mod.ts#L74-L78

Added lines #L74 - L78 were not covered by tests
const fetchService = new FetchService(noteRepository, accountModule);
const renoteService = new RenoteService(noteRepository, idGenerator);
const renoteService = new RenoteService(
noteRepository,
idGenerator,
attachmentRepository,
);

Check warning on line 84 in pkg/notes/mod.ts

View check run for this annotation

Codecov / codecov/patch

pkg/notes/mod.ts#L80-L84

Added lines #L80 - L84 were not covered by tests
const controller = new NoteController(
createService,
fetchService,
Expand Down
9 changes: 9 additions & 0 deletions pkg/notes/model/repository.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Option, Result } from '@mikuroxina/mini-fn';

import type { AccountID } from '../../accounts/model/account.js';
import { type Medium, type MediumID } from '../../drive/model/medium.js';

Check warning on line 4 in pkg/notes/model/repository.ts

View check run for this annotation

Codecov / codecov/patch

pkg/notes/model/repository.ts#L4

Added line #L4 was not covered by tests
import type { Bookmark } from './bookmark.js';
import type { Note, NoteID } from './note.js';

Expand Down Expand Up @@ -29,3 +30,11 @@ export interface BookmarkRepository {
accountID: AccountID;
}): Promise<Result.Result<Error, void>>;
}

export interface NoteAttachmentRepository {
create(
noteID: NoteID,
attachmentFileID: readonly MediumID[],
): Promise<Result.Result<Error, void>>;
findByNoteID(noteID: NoteID): Promise<Result.Result<Error, Medium[]>>;
}

Check warning on line 40 in pkg/notes/model/repository.ts

View check run for this annotation

Codecov / codecov/patch

pkg/notes/model/repository.ts#L33-L40

Added lines #L33 - L40 were not covered by tests
47 changes: 46 additions & 1 deletion pkg/notes/service/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,35 @@ import { Option, Result } from '@mikuroxina/mini-fn';
import { describe, expect, it } from 'vitest';

import type { AccountID } from '../../accounts/model/account.js';
import { Medium, type MediumID } from '../../drive/model/medium.js';
import { SnowflakeIDGenerator } from '../../id/mod.js';
import { InMemoryNoteRepository } from '../adaptor/repository/dummy.js';
import {
InMemoryNoteAttachmentRepository,
InMemoryNoteRepository,
} from '../adaptor/repository/dummy.js';
import { CreateService } from './create.js';

const noteRepository = new InMemoryNoteRepository();
const attachmentRepository = new InMemoryNoteAttachmentRepository(
Array.from({ length: 16 }, (_, i) => {
return Medium.reconstruct({
id: (i + 10).toString() as MediumID,
name: (i + 10).toString(),
mime: 'image/png',
hash: 'ewkjnfgr]g:ge+ealksmc',
url: '',
thumbnailUrl: '',
nsfw: false,
authorId: '1' as AccountID,
});
}),
);
const createService = new CreateService(
noteRepository,
new SnowflakeIDGenerator(0, {
now: () => BigInt(Date.UTC(2023, 9, 10, 0, 0)),
}),
attachmentRepository,
);

describe('CreateService', () => {
Expand All @@ -28,6 +47,32 @@ describe('CreateService', () => {
expect(Result.isOk(res)).toBe(true);
});

it('with attachments', async () => {
const res = await createService.handle(
'Hello world',
'',
Option.none(),
'1' as AccountID,
['10' as MediumID, '11' as MediumID],
'PUBLIC',
);

expect(Result.isOk(res)).toBe(true);
});

it('note attachment must be less than 16', async () => {
const res = await createService.handle(
'Hello world',
'',
Option.none(),
'1' as AccountID,
Array.from({ length: 17 }, (_, i) => i.toString() as MediumID),
'PUBLIC',
);

expect(Result.isErr(res)).toBe(true);
});

it('note content must be less than 3000 chars', async () => {
const res = await createService.handle(
'a'.repeat(3001),
Expand Down
19 changes: 18 additions & 1 deletion pkg/notes/service/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import type { AccountID } from '../../accounts/model/account.js';
import type { MediumID } from '../../drive/model/medium.js';
import type { SnowflakeIDGenerator } from '../../id/mod.js';
import { Note, type NoteID, type NoteVisibility } from '../model/note.js';
import type { NoteRepository } from '../model/repository.js';
import type {
NoteAttachmentRepository,
NoteRepository,
} from '../model/repository.js';

export class CreateService {
async handle(
Expand All @@ -15,6 +18,9 @@ export class CreateService {
attachmentFileID: MediumID[],
visibility: NoteVisibility,
): Promise<Result.Result<Error, Note>> {
if (attachmentFileID.length > 16) {
return Result.err(new Error('Too many attachments'));
}
const id = this.idGenerator.generate<Note>();
if (Result.isErr(id)) {
return id;
Expand All @@ -36,6 +42,16 @@ export class CreateService {
return res;
}

if (attachmentFileID.length !== 0) {
const attachmentRes = await this.noteAttachmentRepository.create(
note.getID(),
note.getAttachmentFileID(),
);
if (Result.isErr(attachmentRes)) {
return attachmentRes;
}

Check warning on line 52 in pkg/notes/service/create.ts

View check run for this annotation

Codecov / codecov/patch

pkg/notes/service/create.ts#L51-L52

Added lines #L51 - L52 were not covered by tests
}

return Result.ok(note);
} catch (e) {
return Result.err(e as unknown as Error);
Expand All @@ -45,5 +61,6 @@ export class CreateService {
constructor(
private readonly noteRepository: NoteRepository,
private readonly idGenerator: SnowflakeIDGenerator,
private readonly noteAttachmentRepository: NoteAttachmentRepository,
) {}
}
Loading

0 comments on commit 007e57c

Please sign in to comment.