Skip to content

Commit 2bfc22d

Browse files
authored
Merge pull request #6 from sredevopsorg/upstream
2 parents e7dee56 + 64f13b1 commit 2bfc22d

33 files changed

+1193
-199
lines changed

.github/workflows/deploy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ jobs:
9393
commit-sha=${{ github.sha }}
9494
9595
deploy-production:
96-
name: Deploy
9796
runs-on: ubuntu-latest
97+
needs: [deploy-staging]
9898
strategy:
9999
matrix:
100100
region: [europe-west4, europe-west3]

AGENTS.md

Lines changed: 251 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# ActivityPub AI Assistant Guide
22

3-
This file provides guidance for AI agents contributing to this repository
3+
This file provides comprehensive guidance for AI agents contributing to this repository.
4+
5+
> **Note:** This document contains detailed code examples and implementation patterns. For a concise human-readable overview, see [README.md](README.md).
46
57
## Project Overview
68

@@ -234,6 +236,8 @@ Currently unsupported
234236

235237
## Architecture Patterns
236238

239+
**📚 See `/adr` directory for Architecture Decision Records**
240+
237241
- Dependency injection is heavily used to manage dependencies and facilitate
238242
testing
239243
- The `Result` pattern is preferred over throwing errors, with an exhaustive
@@ -250,6 +254,81 @@ Currently unsupported
250254
- Views can talk directly to the database if necessary
251255
- Views should not be responsible for any business logic
252256

257+
### Read/Write Separation
258+
259+
The codebase follows a CQRS-inspired pattern:
260+
261+
**Write Path (Commands):**
262+
- Controller → Service → Repository → Entity
263+
- Follows strict layering and repository pattern
264+
- Handles business logic, validations, and domain events
265+
266+
**Read Path (Queries):**
267+
- Controller → View → Database
268+
- Views make optimized queries directly to the database
269+
- Returns DTOs with presentation-ready data
270+
- Includes user-specific context (e.g., followedByMe, blockedByMe)
271+
272+
---
273+
274+
## Critical Patterns & Gotchas
275+
276+
### Database Lookups Use SHA256 Hashes
277+
278+
⚠️ **Never use direct string comparisons for ActivityPub IDs** - see [ADR-0009](adr/0009-hash-based-database-lookups.md)
279+
280+
```typescript
281+
// ❌ WRONG - Returns no results!
282+
await db('accounts').where('ap_id', apId)
283+
284+
// ✅ CORRECT - Use hash lookup
285+
await db('accounts').whereRaw('ap_id_hash = UNHEX(SHA2(?, 256))', [apId])
286+
```
287+
288+
### Result Type Usage
289+
290+
Always use the helper functions with Result types:
291+
292+
```typescript
293+
// ✅ CORRECT - Use helpers
294+
const result = await someFunction();
295+
if (isError(result)) {
296+
const error = getError(result);
297+
// handle error
298+
} else {
299+
const value = getValue(result);
300+
// use value
301+
}
302+
303+
// ❌ WRONG - Don't destructure directly
304+
const [error, value] = someResult; // Implementation detail - don't do this!
305+
```
306+
307+
### Dependency Injection Names Must Match
308+
309+
Awilix uses CLASSIC injection mode - parameter names must match registration names:
310+
311+
```typescript
312+
constructor(
313+
private readonly accountService: AccountService, // Must be registered as 'accountService'
314+
private readonly db: Knex, // Must be registered as 'db'
315+
)
316+
```
317+
318+
### Routes Use Decorators
319+
320+
Routes are defined using decorators, not direct registration - see [ADR-0010](adr/0010-decorator-based-routing.md)
321+
322+
```typescript
323+
@APIRoute('GET', 'account/:handle') // Defines route
324+
@RequireRoles(GhostRole.Owner) // Adds role check
325+
async handleGetAccount() { }
326+
```
327+
328+
### Legacy Code Warning
329+
330+
`dispatchers.ts` contains 1100+ lines of legacy factory functions. New handlers should follow the class-based pattern in `/activity-handlers/` - see [ADR-0006](adr/0006-class-based-architecture.md)
331+
253332
---
254333

255334
## Code Conventions
@@ -264,6 +343,177 @@ Currently unsupported
264343

265344
---
266345

346+
## Code Patterns
347+
348+
These patterns are based on our architecture decisions (see `/adr` directory):
349+
350+
### Immutable Entities with Domain Events
351+
352+
```typescript
353+
// ❌ Avoid: Mutable entities with dirty flags
354+
class Post {
355+
private _likeCount: number;
356+
private _likeCountDirty: boolean;
357+
358+
like() {
359+
this._likeCount++;
360+
this._likeCountDirty = true;
361+
}
362+
}
363+
364+
// ✅ Prefer: Immutable entities that generate events
365+
class Post {
366+
constructor(
367+
readonly id: string,
368+
readonly likeCount: number,
369+
private events: DomainEvent[] = []
370+
) {}
371+
372+
like(): Post {
373+
const newPost = new Post(this.id, this.likeCount + 1);
374+
newPost.events.push(new PostLikedEvent(this.id));
375+
return newPost;
376+
}
377+
378+
pullEvents(): DomainEvent[] {
379+
return [...this.events];
380+
}
381+
}
382+
```
383+
384+
### Error Objects in Result Types
385+
386+
```typescript
387+
// ❌ Avoid: String literal errors without context
388+
Result<Account, 'not-found' | 'network-error'>
389+
390+
// ✅ Prefer: Error objects with context
391+
type AccountError =
392+
| { type: 'not-found'; accountId: string }
393+
| { type: 'network-error'; retryable: boolean }
394+
395+
async function getAccount(id: string): Promise<Result<Account, AccountError>> {
396+
const account = await repository.findById(id);
397+
if (!account) {
398+
return error({ type: 'not-found', accountId: id });
399+
}
400+
return ok(account);
401+
}
402+
403+
// Usage with exhaustive handling
404+
const result = await getAccount('123');
405+
if (isError(result)) {
406+
const err = getError(result);
407+
switch (err.type) {
408+
case 'not-found':
409+
log(`Account ${err.accountId} not found`);
410+
break;
411+
case 'network-error':
412+
if (err.retryable) retry();
413+
break;
414+
default:
415+
exhaustiveCheck(err);
416+
}
417+
}
418+
```
419+
420+
### Class-Based Architecture
421+
422+
```typescript
423+
// ❌ Avoid: Function factories
424+
export function createFollowHandler(accountService: AccountService) {
425+
return async function handleFollow(ctx: Context, follow: Follow) {
426+
// implementation
427+
}
428+
}
429+
430+
// ✅ Prefer: Classes with dependency injection
431+
export class FollowHandler {
432+
constructor(
433+
private readonly accountService: AccountService,
434+
private readonly notificationService: NotificationService
435+
) {}
436+
437+
async handle(ctx: Context, follow: Follow) {
438+
// implementation
439+
}
440+
}
441+
442+
// Registration with Awilix
443+
container.register('followHandler', asClass(FollowHandler).singleton())
444+
```
445+
446+
### Repository Pattern
447+
448+
```typescript
449+
// ❌ Avoid: Direct database queries in services
450+
class AccountService {
451+
async getFollowers(accountId: number) {
452+
return await this.db('follows')
453+
.join('accounts', 'accounts.id', 'follows.follower_id')
454+
.where('follows.following_id', accountId);
455+
}
456+
}
457+
458+
// ✅ Prefer: Repository handles all data access
459+
class AccountRepository {
460+
async getFollowers(accountId: number) {
461+
return await this.db('follows')
462+
.join('accounts', 'accounts.id', 'follows.follower_id')
463+
.where('follows.following_id', accountId);
464+
}
465+
}
466+
467+
class AccountService {
468+
constructor(private readonly accountRepository: AccountRepository) {}
469+
470+
async getFollowers(accountId: number) {
471+
return await this.accountRepository.getFollowers(accountId);
472+
}
473+
}
474+
```
475+
476+
### View Pattern for Reads
477+
478+
```typescript
479+
// Views are used for complex read operations that need optimization
480+
export class AccountView {
481+
constructor(private readonly db: Knex) {}
482+
483+
async viewById(id: number, context: ViewContext): Promise<AccountDTO> {
484+
// Direct database query with complex joins and aggregations
485+
const accountData = await this.db('accounts')
486+
.innerJoin('users', 'users.account_id', 'accounts.id')
487+
.select(
488+
'accounts.*',
489+
this.db.raw('(select count(*) from posts where posts.author_id = accounts.id) as post_count'),
490+
this.db.raw('(select count(*) from follows where follows.follower_id = accounts.id) as following_count')
491+
)
492+
.where('accounts.id', id)
493+
.first();
494+
495+
// Add user-specific context
496+
const followedByMe = context.requestUserAccount
497+
? await this.db('follows')
498+
.where('follower_id', context.requestUserAccount.id)
499+
.where('following_id', id)
500+
.first() !== undefined
501+
: false;
502+
503+
// Return presentation-ready DTO
504+
return {
505+
id: accountData.id,
506+
handle: accountData.handle,
507+
postCount: accountData.post_count,
508+
followingCount: accountData.following_count,
509+
followedByMe
510+
};
511+
}
512+
}
513+
```
514+
515+
---
516+
267517
## Common Workflows
268518

269519
### Adding / changing functionality

CONTRIBUTING.md

Lines changed: 12 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,20 @@
1-
# CONTRIBUTING.md
2-
3-
This document outlines internal development practices and conventions for our
4-
ActivityPub project. It serves as a reference guide for our engineering team to
5-
maintain consistency and avoid common pitfalls.
6-
7-
## Project Structure
8-
9-
This is the directory structure under `src`
10-
11-
```
12-
├── account
13-
├── post
14-
├── site
15-
├── feed
16-
├── notification
17-
├── activity-handlers # Fedify incoming Activity handlers
18-
├── activitypub
19-
│   └── object-dispatchers # Fedify object dispatchers
20-
├── core
21-
├── events
22-
├── helpers
23-
│   └── activitypub
24-
├── http
25-
│   └── api # Where all our API endpoints should be
26-
│   └── helpers
27-
├── mq # Implementation of a PubSub backed MessageQueue for Fedify
28-
├── publishing
29-
└── test # Helpers for tests
30-
```
1+
# Contributing to ActivityPub Service
312

323
## Development Guidelines
334

34-
This section documents quirks and conventions specific to our implementation.
5+
For architectural patterns, code standards, and development guidelines, please see the [Architecture & Development Guidelines](README.md#️-architecture--development-guidelines) section in the main README.
356

36-
### Do's
7+
## Submitting Changes
378

38-
- Do model business logic in the Entities
39-
- Do use services in HTTP handlers & Fedify wirings
9+
1. Create a feature branch from `main`
10+
2. Make your changes following the patterns documented in README
11+
3. Ensure tests pass: `yarn test`
12+
4. Submit a pull request with a clear description
4013

41-
### Don'ts
14+
## Code Review Process
4215

43-
- Don't add code to dispatchers.ts
44-
- Don't add code to handlers.ts
16+
- All PRs require at least one review
17+
- Check that new code follows our architectural patterns (see ADRs)
18+
- Ensure proper error handling with Result types
19+
- Verify no direct database queries in services
4520
- Don't use the AccountType in new code

0 commit comments

Comments
 (0)