wip
This commit is contained in:
8
packages/social/application/dto/CurrentUserSocialDTO.ts
Normal file
8
packages/social/application/dto/CurrentUserSocialDTO.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface CurrentUserSocialDTO {
|
||||
driverId: string;
|
||||
displayName: string;
|
||||
avatarUrl: string;
|
||||
countryCode: string;
|
||||
primaryTeamId?: string;
|
||||
primaryLeagueId?: string;
|
||||
}
|
||||
9
packages/social/application/dto/FriendDTO.ts
Normal file
9
packages/social/application/dto/FriendDTO.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface FriendDTO {
|
||||
driverId: string;
|
||||
displayName: string;
|
||||
avatarUrl: string;
|
||||
isOnline: boolean;
|
||||
lastSeen: Date;
|
||||
primaryLeagueId?: string;
|
||||
primaryTeamId?: string;
|
||||
}
|
||||
17
packages/social/domain/entities/FeedItem.ts
Normal file
17
packages/social/domain/entities/FeedItem.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { FeedItemType } from '../value-objects/FeedItemType';
|
||||
|
||||
export interface FeedItem {
|
||||
id: string;
|
||||
timestamp: Date;
|
||||
type: FeedItemType;
|
||||
actorFriendId?: string;
|
||||
actorDriverId?: string;
|
||||
leagueId?: string;
|
||||
raceId?: string;
|
||||
teamId?: string;
|
||||
position?: number;
|
||||
headline: string;
|
||||
body?: string;
|
||||
ctaLabel?: string;
|
||||
ctaHref?: string;
|
||||
}
|
||||
6
packages/social/domain/repositories/IFeedRepository.ts
Normal file
6
packages/social/domain/repositories/IFeedRepository.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { FeedItem } from '../entities/FeedItem';
|
||||
|
||||
export interface IFeedRepository {
|
||||
getFeedForDriver(driverId: string, limit?: number): Promise<FeedItem[]>;
|
||||
getGlobalFeed(limit?: number): Promise<FeedItem[]>;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { Driver } from '@gridpilot/racing/domain/entities/Driver';
|
||||
|
||||
export interface ISocialGraphRepository {
|
||||
getFriends(driverId: string): Promise<Driver[]>;
|
||||
getFriendIds(driverId: string): Promise<string[]>;
|
||||
getSuggestedFriends(driverId: string, limit?: number): Promise<Driver[]>;
|
||||
}
|
||||
8
packages/social/domain/value-objects/FeedItemType.ts
Normal file
8
packages/social/domain/value-objects/FeedItemType.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export type FeedItemType =
|
||||
| 'friend-joined-league'
|
||||
| 'friend-joined-team'
|
||||
| 'friend-finished-race'
|
||||
| 'friend-new-personal-best'
|
||||
| 'new-race-scheduled'
|
||||
| 'new-result-posted'
|
||||
| 'league-highlight';
|
||||
106
packages/social/infrastructure/inmemory/InMemorySocialAndFeed.ts
Normal file
106
packages/social/infrastructure/inmemory/InMemorySocialAndFeed.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import type { Driver } from '@gridpilot/racing/domain/entities/Driver';
|
||||
import type { FeedItem } from '@gridpilot/social/domain/entities/FeedItem';
|
||||
import type { IFeedRepository } from '@gridpilot/social/domain/repositories/IFeedRepository';
|
||||
import type { ISocialGraphRepository } from '@gridpilot/social/domain/repositories/ISocialGraphRepository';
|
||||
|
||||
export type Friendship = {
|
||||
driverId: string;
|
||||
friendId: string;
|
||||
};
|
||||
|
||||
export type RacingSeedData = {
|
||||
drivers: Driver[];
|
||||
friendships: Friendship[];
|
||||
feedEvents: FeedItem[];
|
||||
};
|
||||
|
||||
export class InMemoryFeedRepository implements IFeedRepository {
|
||||
private readonly feedEvents: FeedItem[];
|
||||
private readonly friendships: Friendship[];
|
||||
private readonly driversById: Map<string, Driver>;
|
||||
|
||||
constructor(seed: RacingSeedData) {
|
||||
this.feedEvents = seed.feedEvents;
|
||||
this.friendships = seed.friendships;
|
||||
this.driversById = new Map(seed.drivers.map((d) => [d.id, d]));
|
||||
}
|
||||
|
||||
async getFeedForDriver(driverId: string, limit?: number): Promise<FeedItem[]> {
|
||||
const friendIds = new Set(
|
||||
this.friendships
|
||||
.filter((f) => f.driverId === driverId)
|
||||
.map((f) => f.friendId),
|
||||
);
|
||||
|
||||
const items = this.feedEvents.filter((item) => {
|
||||
if (item.actorDriverId && friendIds.has(item.actorDriverId)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const sorted = items
|
||||
.slice()
|
||||
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
||||
|
||||
return typeof limit === 'number' ? sorted.slice(0, limit) : sorted;
|
||||
}
|
||||
|
||||
async getGlobalFeed(limit?: number): Promise<FeedItem[]> {
|
||||
const sorted = this.feedEvents
|
||||
.slice()
|
||||
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
||||
|
||||
return typeof limit === 'number' ? sorted.slice(0, limit) : sorted;
|
||||
}
|
||||
}
|
||||
|
||||
export class InMemorySocialGraphRepository implements ISocialGraphRepository {
|
||||
private readonly friendships: Friendship[];
|
||||
private readonly driversById: Map<string, Driver>;
|
||||
|
||||
constructor(seed: RacingSeedData) {
|
||||
this.friendships = seed.friendships;
|
||||
this.driversById = new Map(seed.drivers.map((d) => [d.id, d]));
|
||||
}
|
||||
|
||||
async getFriendIds(driverId: string): Promise<string[]> {
|
||||
return this.friendships
|
||||
.filter((f) => f.driverId === driverId)
|
||||
.map((f) => f.friendId);
|
||||
}
|
||||
|
||||
async getFriends(driverId: string): Promise<Driver[]> {
|
||||
const ids = await this.getFriendIds(driverId);
|
||||
return ids
|
||||
.map((id) => this.driversById.get(id))
|
||||
.filter((d): d is Driver => Boolean(d));
|
||||
}
|
||||
|
||||
async getSuggestedFriends(driverId: string, limit?: number): Promise<Driver[]> {
|
||||
const directFriendIds = new Set(await this.getFriendIds(driverId));
|
||||
const suggestions = new Map<string, number>();
|
||||
|
||||
for (const friendship of this.friendships) {
|
||||
if (!directFriendIds.has(friendship.driverId)) continue;
|
||||
const friendOfFriendId = friendship.friendId;
|
||||
if (friendOfFriendId === driverId) continue;
|
||||
if (directFriendIds.has(friendOfFriendId)) continue;
|
||||
|
||||
suggestions.set(friendOfFriendId, (suggestions.get(friendOfFriendId) ?? 0) + 1);
|
||||
}
|
||||
|
||||
const rankedIds = Array.from(suggestions.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([id]) => id);
|
||||
|
||||
const drivers = rankedIds
|
||||
.map((id) => this.driversById.get(id))
|
||||
.filter((d): d is Driver => Boolean(d));
|
||||
|
||||
if (typeof limit === 'number') {
|
||||
return drivers.slice(0, limit);
|
||||
}
|
||||
return drivers;
|
||||
}
|
||||
}
|
||||
10
packages/social/package.json
Normal file
10
packages/social/package.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "@gridpilot/social",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./domain/*": "./domain/*",
|
||||
"./application/*": "./application/*",
|
||||
"./infrastructure/*": "./infrastructure/*"
|
||||
}
|
||||
}
|
||||
11
packages/social/tsconfig.json
Normal file
11
packages/social/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"outDir": "dist",
|
||||
"composite": false,
|
||||
"declaration": true,
|
||||
"declarationMap": false
|
||||
},
|
||||
"include": ["./**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user