Skip to content

Commit ed6d7b1

Browse files
Refactored PostUpdatedEvent to implement SerializableEvent to comply with ADR-0011
ref https://linear.app/ghost/issue/BER-3164/refactor-postupdatedevent-to-implement-serializableevent-adr-0011 Refactored `PostUpdatedEvent` to implement `SerializableEvent` to comply with ADR-0011 Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent 646b879 commit ed6d7b1

File tree

7 files changed

+145
-13
lines changed

7 files changed

+145
-13
lines changed

src/activitypub/fediverse-bridge.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
buildUpdateActivityAndObjectFromPost,
2020
} from '@/helpers/activitypub/activity';
2121
import { PostType } from '@/post/post.entity';
22+
import type { KnexPostRepository } from '@/post/post.repository.knex';
2223
import { PostCreatedEvent } from '@/post/post-created.event';
2324
import { PostDeletedEvent } from '@/post/post-deleted.event';
2425
import { PostUpdatedEvent } from '@/post/post-updated.event';
@@ -28,6 +29,7 @@ export class FediverseBridge {
2829
private readonly events: EventEmitter,
2930
private readonly fedifyContextFactory: FedifyContextFactory,
3031
private readonly accountService: AccountService,
32+
private readonly postRepository: KnexPostRepository,
3133
) {}
3234

3335
async init() {
@@ -138,8 +140,8 @@ export class FediverseBridge {
138140
}
139141

140142
private async handlePostUpdated(event: PostUpdatedEvent) {
141-
const post = event.getPost();
142-
if (!post.author.isInternal) {
143+
const post = await this.postRepository.getById(event.getPostId());
144+
if (!post || !post.author.isInternal) {
143145
return;
144146
}
145147

src/activitypub/fediverse-bridge.unit.test.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { FediverseBridge } from '@/activitypub/fediverse-bridge';
1212
import type { UriBuilder } from '@/activitypub/uri';
1313
import type { FedifyContext } from '@/app';
1414
import { Post, PostType } from '@/post/post.entity';
15+
import type { KnexPostRepository } from '@/post/post.repository.knex';
1516
import { PostCreatedEvent } from '@/post/post-created.event';
1617
import { PostDeletedEvent } from '@/post/post-deleted.event';
1718
import { PostUpdatedEvent } from '@/post/post-updated.event';
@@ -29,6 +30,7 @@ vi.mock('node:crypto', async (importOriginal) => {
2930
describe('FediverseBridge', () => {
3031
let events: EventEmitter;
3132
let accountService: AccountService;
33+
let postRepository: KnexPostRepository;
3234
let context: FedifyContext;
3335
let fedifyContextFactory: FedifyContextFactory;
3436
let mockUriBuilder: UriBuilder<FedifyObject>;
@@ -39,6 +41,9 @@ describe('FediverseBridge', () => {
3941
accountService = {
4042
getAccountById: vi.fn(),
4143
} as unknown as AccountService;
44+
postRepository = {
45+
getById: vi.fn(),
46+
} as unknown as KnexPostRepository;
4247
mockUriBuilder = {
4348
buildObjectUri: vi.fn().mockImplementation((object, { id }) => {
4449
return new URL(
@@ -75,6 +80,7 @@ describe('FediverseBridge', () => {
7580
events,
7681
fedifyContextFactory,
7782
accountService,
83+
postRepository,
7884
);
7985
});
8086

@@ -163,6 +169,7 @@ describe('FediverseBridge', () => {
163169
events,
164170
fedifyContextFactory,
165171
accountService,
172+
postRepository,
166173
);
167174
await bridge.init();
168175

@@ -246,6 +253,7 @@ describe('FediverseBridge', () => {
246253
events,
247254
fedifyContextFactory,
248255
accountService,
256+
postRepository,
249257
);
250258
await bridge.init();
251259

@@ -291,6 +299,7 @@ describe('FediverseBridge', () => {
291299
events,
292300
fedifyContextFactory,
293301
accountService,
302+
postRepository,
294303
);
295304
await bridge.init();
296305

@@ -333,6 +342,7 @@ describe('FediverseBridge', () => {
333342
events,
334343
fedifyContextFactory,
335344
accountService,
345+
postRepository,
336346
);
337347
await bridge.init();
338348

@@ -515,10 +525,13 @@ describe('FediverseBridge', () => {
515525
post.apId = new URL('https://example.com/article/post-456');
516526
post.publishedAt = new Date('2025-01-01T00:00:00Z');
517527

518-
const event = new PostUpdatedEvent(post);
528+
vi.mocked(postRepository.getById).mockResolvedValue(post);
529+
530+
const event = new PostUpdatedEvent(456);
519531
events.emit(PostUpdatedEvent.getName(), event);
520532

521533
await nextTick();
534+
expect(postRepository.getById).toHaveBeenCalledWith(456);
522535
expect(sendActivity).toHaveBeenCalledOnce();
523536
expect(context.data.globaldb.set).toHaveBeenCalledTimes(2);
524537

@@ -547,10 +560,29 @@ describe('FediverseBridge', () => {
547560
post.content = 'Updated post content';
548561
post.apId = new URL('https://external.com/article/post-456');
549562

550-
const event = new PostUpdatedEvent(post);
563+
vi.mocked(postRepository.getById).mockResolvedValue(post);
564+
565+
const event = new PostUpdatedEvent(456);
566+
events.emit(PostUpdatedEvent.getName(), event);
567+
568+
await nextTick();
569+
expect(postRepository.getById).toHaveBeenCalledWith(456);
570+
expect(sendActivity).not.toHaveBeenCalled();
571+
expect(context.data.globaldb.set).not.toHaveBeenCalled();
572+
});
573+
574+
it('should not send update activities on the PostUpdatedEvent if post is not found', async () => {
575+
await bridge.init();
576+
577+
const sendActivity = vi.spyOn(context, 'sendActivity');
578+
579+
vi.mocked(postRepository.getById).mockResolvedValue(null);
580+
581+
const event = new PostUpdatedEvent(999);
551582
events.emit(PostUpdatedEvent.getName(), event);
552583

553584
await nextTick();
585+
expect(postRepository.getById).toHaveBeenCalledWith(999);
554586
expect(sendActivity).not.toHaveBeenCalled();
555587
expect(context.data.globaldb.set).not.toHaveBeenCalled();
556588
});

src/app.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ import type { PostInteractionCountsService } from '@/post/post-interaction-count
122122
import { PostInteractionCountsUpdateRequestedEvent } from '@/post/post-interaction-counts-update-requested.event';
123123
import { PostLikedEvent } from '@/post/post-liked.event';
124124
import { PostRepostedEvent } from '@/post/post-reposted.event';
125+
import { PostUpdatedEvent } from '@/post/post-updated.event';
125126
import type { Site } from '@/site/site.service';
126127

127128
function toLogLevel(level: unknown): LogLevel | null {
@@ -279,6 +280,9 @@ container
279280
container
280281
.resolve<EventSerializer>('eventSerializer')
281282
.register(PostRepostedEvent.getName(), PostRepostedEvent);
283+
container
284+
.resolve<EventSerializer>('eventSerializer')
285+
.register(PostUpdatedEvent.getName(), PostUpdatedEvent);
282286

283287
/** Fedify */
284288

src/post/post-updated.event.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,30 @@
1-
import type { Post } from '@/post/post.entity';
1+
import type { SerializableEvent } from '@/events/event';
22

3-
export class PostUpdatedEvent {
4-
constructor(private readonly post: Post) {}
3+
export class PostUpdatedEvent implements SerializableEvent {
4+
constructor(private readonly postId: number) {}
55

6-
getPost(): Post {
7-
return this.post;
6+
getPostId(): number {
7+
return this.postId;
8+
}
9+
10+
getName(): string {
11+
return PostUpdatedEvent.getName();
812
}
913

1014
static getName(): string {
1115
return 'post.updated';
1216
}
17+
18+
toJSON(): Record<string, unknown> {
19+
return {
20+
postId: this.postId,
21+
};
22+
}
23+
24+
static fromJSON(data: Record<string, unknown>): PostUpdatedEvent {
25+
if (typeof data.postId !== 'number') {
26+
throw new Error('postId must be a number');
27+
}
28+
return new PostUpdatedEvent(data.postId);
29+
}
1330
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { PostUpdatedEvent } from '@/post/post-updated.event';
4+
5+
describe('PostUpdatedEvent', () => {
6+
describe('getName', () => {
7+
it('should return the event name from static method', () => {
8+
expect(PostUpdatedEvent.getName()).toBe('post.updated');
9+
});
10+
11+
it('should return the event name from instance method', () => {
12+
const event = new PostUpdatedEvent(123);
13+
14+
expect(event.getName()).toBe('post.updated');
15+
});
16+
});
17+
18+
describe('getPostId', () => {
19+
it('should return the post id', () => {
20+
const event = new PostUpdatedEvent(123);
21+
22+
expect(event.getPostId()).toBe(123);
23+
});
24+
});
25+
26+
describe('toJSON', () => {
27+
it('should serialize the event to JSON', () => {
28+
const event = new PostUpdatedEvent(123);
29+
30+
expect(event.toJSON()).toEqual({
31+
postId: 123,
32+
});
33+
});
34+
});
35+
36+
describe('fromJSON', () => {
37+
it('should deserialize the event from JSON', () => {
38+
const event = PostUpdatedEvent.fromJSON({
39+
postId: 123,
40+
});
41+
42+
expect(event.getPostId()).toBe(123);
43+
});
44+
45+
it('should throw an error if postId is missing', () => {
46+
expect(() => PostUpdatedEvent.fromJSON({})).toThrow(
47+
'postId must be a number',
48+
);
49+
});
50+
51+
it('should throw an error if postId is not a number', () => {
52+
expect(() =>
53+
PostUpdatedEvent.fromJSON({
54+
postId: 'not a number',
55+
}),
56+
).toThrow('postId must be a number');
57+
});
58+
59+
it('should throw an error if postId is null', () => {
60+
expect(() =>
61+
PostUpdatedEvent.fromJSON({
62+
postId: null,
63+
}),
64+
).toThrow('postId must be a number');
65+
});
66+
});
67+
68+
describe('round-trip serialization', () => {
69+
it('should correctly serialize and deserialize', () => {
70+
const original = new PostUpdatedEvent(999);
71+
const json = original.toJSON();
72+
const restored = PostUpdatedEvent.fromJSON(json);
73+
74+
expect(restored.getPostId()).toBe(original.getPostId());
75+
});
76+
});
77+
});

src/post/post.repository.knex.integration.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2094,14 +2094,14 @@ describe('KnexPostRepository', () => {
20942094
expect(eventsEmitSpy).toHaveBeenCalledWith(
20952095
PostUpdatedEvent.getName(),
20962096
expect.objectContaining({
2097-
getPost: expect.any(Function),
2097+
getPostId: expect.any(Function),
20982098
}),
20992099
);
21002100

21012101
const emittedEvent = eventsEmitSpy.mock.calls.find(
21022102
(call) => call[0] === PostUpdatedEvent.getName(),
2103-
)?.[1];
2104-
expect(emittedEvent?.getPost()).toEqual(post);
2103+
)?.[1] as PostUpdatedEvent | undefined;
2104+
expect(emittedEvent?.getPostId()).toEqual(post.id);
21052105

21062106
const updatedRowInDb = await client('posts')
21072107
.where({ uuid: post.uuid })

src/post/post.repository.knex.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -516,7 +516,7 @@ export class KnexPostRepository {
516516
if (wasUpdated) {
517517
await this.events.emitAsync(
518518
PostUpdatedEvent.getName(),
519-
new PostUpdatedEvent(post),
519+
new PostUpdatedEvent(post.id as number),
520520
);
521521
}
522522

0 commit comments

Comments
 (0)