Files
gerbeur/api/model/interfaces.ts
2026-03-22 21:07:17 +00:00

471 lines
9.8 KiB
TypeScript

/**
* Backend
*/
export interface RichContent {
type: string;
url: string;
siteName?: string;
title?: string;
description?: string;
thumbnailUrl?: string;
videoId?: string;
embedUrl?: string;
}
export interface Dump {
id: string;
kind: "url" | "file";
title: string;
slug?: string;
comment?: string;
userId: string;
createdAt: Date;
updatedAt?: Date;
url?: string;
richContent?: RichContent;
fileName?: string;
fileMime?: string;
fileSize?: number;
voteCount: number;
commentCount: number;
isPrivate: boolean;
}
/**
* Authentication
*/
export interface User {
id: string;
username: string;
passwordHash: string;
isAdmin: boolean;
createdAt: Date;
updatedAt?: Date;
avatarMime?: string;
description?: string;
invitedByUsername?: string;
}
export interface LoginUserRequest {
username: string;
password: string;
}
export interface RegisterUserRequest {
username: string;
password: string;
inviteToken: string;
}
export interface UpdateUserRequest {
username?: string;
password?: string;
isAdmin?: boolean;
description?: string | null;
}
export function isLoginUserRequest(obj: unknown): obj is LoginUserRequest {
return !!obj && typeof obj === "object" &&
"username" in obj && typeof obj.username === "string" &&
"password" in obj && typeof obj.password === "string";
}
export function isRegisterUserRequest(
obj: unknown,
): obj is RegisterUserRequest {
return !!obj && typeof obj === "object" &&
"username" in obj && typeof obj.username === "string" &&
"password" in obj && typeof obj.password === "string" &&
"inviteToken" in obj && typeof obj.inviteToken === "string";
}
export function isUpdateUserRequest(obj: unknown): obj is UpdateUserRequest {
return !!obj && typeof obj === "object" &&
(!("username" in obj) || typeof obj.username === "string") &&
(!("password" in obj) || typeof obj.password === "string") &&
(!("isAdmin" in obj) || typeof obj.isAdmin === "boolean") &&
(!("description" in obj) || typeof obj.description === "string" ||
obj.description === null);
}
export interface AuthResponse {
token: string;
user: User;
}
export interface AuthPayload {
userId: string;
username: string;
isAdmin: boolean;
exp: number;
}
export function isAuthPayload(obj: unknown): obj is AuthPayload {
return !!obj &&
typeof obj === "object" &&
"userId" in obj && typeof obj.userId === "string" &&
"username" in obj && typeof obj.username === "string" &&
"isAdmin" in obj && typeof obj.isAdmin === "boolean" &&
"exp" in obj && typeof obj.exp === "number";
}
export interface InvitePayload {
purpose: "invite";
inviterId: string;
exp: number;
}
export function isInvitePayload(obj: unknown): obj is InvitePayload {
return !!obj && typeof obj === "object" &&
"purpose" in obj && (obj as Record<string, unknown>).purpose === "invite" &&
"inviterId" in obj &&
typeof (obj as Record<string, unknown>).inviterId === "string";
}
/**
* API
*/
export enum APIErrorCode {
BAD_REQUEST = "BAD_REQUEST",
NOT_FOUND = "NOT_FOUND",
SERVER_ERROR = "SERVER_ERROR",
TIMEOUT = "TIMEOUT",
UNAUTHORIZED = "UNAUTHORIZED",
VALIDATION_ERROR = "VALIDATION_ERROR",
}
export interface APIError {
code: APIErrorCode;
message: string;
}
export interface APISuccess<T> {
success: true;
data: T;
error?: never;
}
export interface APIFailure {
success: false;
data?: never;
error: APIError;
}
export type APIResponse<T> = APISuccess<T> | APIFailure;
export interface PaginatedData<T> {
items: T[];
total: number;
hasMore: boolean;
}
export class APIException extends Error {
readonly code: APIErrorCode;
readonly status: number;
constructor(code: APIErrorCode, status: number, message: string) {
super(message);
this.code = code;
this.status = status;
}
}
/**
* Comments
*/
export interface Comment {
id: string;
dumpId: string;
userId: string;
parentId?: string;
body: string;
createdAt: Date;
updatedAt?: Date;
deleted: boolean;
authorUsername: string;
authorAvatarMime?: string;
}
export interface CreateCommentRequest {
body: string;
parentId?: string;
}
export function isCreateCommentRequest(
obj: unknown,
): obj is CreateCommentRequest {
if (!obj || typeof obj !== "object") return false;
const o = obj as Record<string, unknown>;
return typeof o.body === "string" && (o.body as string).trim().length > 0 &&
(!("parentId" in o) || typeof o.parentId === "string" ||
o.parentId === null);
}
export interface UpdateCommentRequest {
body: string;
}
export function isUpdateCommentRequest(
obj: unknown,
): obj is UpdateCommentRequest {
if (!obj || typeof obj !== "object") return false;
const o = obj as Record<string, unknown>;
return typeof o.body === "string" && (o.body as string).trim().length > 0;
}
/**
* Playlists
*/
export interface Playlist {
id: string;
userId: string;
title: string;
slug?: string;
description?: string;
isPublic: boolean;
createdAt: Date;
updatedAt?: Date;
imageMime?: string;
dumpCount?: number;
ownerUsername?: string;
}
export interface PlaylistWithDumps extends Playlist {
dumps: Dump[];
}
export interface PlaylistMembership {
playlist: Playlist;
hasDump: boolean;
}
export interface CreatePlaylistRequest {
title: string;
description?: string;
isPublic: boolean;
}
export interface UpdatePlaylistRequest {
title?: string;
description?: string;
isPublic?: boolean;
}
export interface ReorderPlaylistRequest {
dumpIds: string[];
}
export function isCreatePlaylistRequest(
obj: unknown,
): obj is CreatePlaylistRequest {
return !!obj && typeof obj === "object" &&
"title" in obj && typeof obj.title === "string" &&
(!("description" in obj) || typeof obj.description === "string" ||
obj.description === null) &&
"isPublic" in obj && typeof obj.isPublic === "boolean";
}
export function isUpdatePlaylistRequest(
obj: unknown,
): obj is UpdatePlaylistRequest {
return !!obj && typeof obj === "object" &&
(!("title" in obj) || typeof obj.title === "string") &&
(!("description" in obj) || typeof obj.description === "string" ||
obj.description === null) &&
(!("isPublic" in obj) || typeof obj.isPublic === "boolean");
}
export function isReorderPlaylistRequest(
obj: unknown,
): obj is ReorderPlaylistRequest {
return !!obj && typeof obj === "object" &&
"dumpIds" in obj && Array.isArray(obj.dumpIds) &&
(obj.dumpIds as unknown[]).every((id) => typeof id === "string");
}
/**
* Request DTOs
*/
export interface CreateUrlDumpRequest {
url: string;
comment?: string;
isPrivate?: boolean;
}
export function isCreateUrlDumpRequest(
obj: unknown,
): obj is CreateUrlDumpRequest {
return !!obj &&
typeof obj === "object" &&
"url" in obj && typeof obj.url === "string" &&
(!("comment" in obj) ||
typeof obj.comment === "string" || obj.comment === null) &&
(!("isPrivate" in obj) || typeof obj.isPrivate === "boolean");
}
export interface UpdateDumpRequest {
url?: string;
comment?: string;
isPrivate?: boolean;
}
export function isUpdateDumpRequest(obj: unknown): obj is UpdateDumpRequest {
return !!obj &&
typeof obj === "object" &&
(!("url" in obj) || typeof obj.url === "string" || obj.url === null) &&
(!("comment" in obj) ||
typeof obj.comment === "string" || obj.comment === null) &&
(!("isPrivate" in obj) || typeof obj.isPrivate === "boolean");
}
/**
* WebSockets
*/
export interface VoteCastMessage {
type: "vote_cast";
dumpId: string;
userId: string;
}
export interface VoteAckMessageFailure {
type: "vote_ack";
dumpId: string;
success: false;
error: APIError;
}
export interface VoteAckMessageSuccess {
type: "vote_ack";
dumpId: string;
action: "cast" | "remove";
success: true;
voteCount: number;
error?: never;
}
export type VoteAckMessage = VoteAckMessageSuccess | VoteAckMessageFailure;
export interface VoteRemoveMessage {
type: "vote_remove";
dumpId: string;
}
export interface VotesUpdateMessage {
type: "votes_update";
dumpId: string;
voteCount: number;
}
export interface OnlineUser {
userId: string;
username: string;
hasAvatar: boolean;
avatarVersion?: number;
}
export interface WelcomeMessage {
type: "welcome";
users: OnlineUser[];
myVotes: string[];
}
export interface PresenceUpdateMessage {
type: "presence_update";
users: OnlineUser[];
}
export interface PingMessage {
type: "ping";
}
export interface PongMessage {
type: "pong";
}
/**
* Follows
*/
export interface FollowStatus {
followedUserIds: string[];
followedPlaylistIds: string[];
}
/**
* Notifications
*/
export type NotificationType =
| "playlist_followed"
| "user_followed"
| "user_dump_posted"
| "playlist_dump_added"
| "dump_upvoted"
| "user_mentioned";
export interface PlaylistFollowedData {
followerId: string;
followerUsername: string;
playlistId: string;
playlistTitle: string;
}
export interface UserFollowedData {
followerId: string;
followerUsername: string;
}
export interface UserDumpPostedData {
dumperId: string;
dumperUsername: string;
dumpId: string;
dumpTitle: string;
}
export interface PlaylistDumpAddedData {
dumpId: string;
dumpTitle: string;
playlistId: string;
playlistTitle: string;
}
export interface DumpUpvotedData {
voterId: string;
voterUsername: string;
dumpId: string;
dumpTitle: string;
}
export interface UserMentionedData {
mentionerId: string;
mentionerUsername: string;
contextType: "comment" | "dump" | "playlist";
contextId: string;
contextTitle: string;
dumpId?: string;
}
export type NotificationData =
| PlaylistFollowedData
| UserFollowedData
| UserDumpPostedData
| PlaylistDumpAddedData
| DumpUpvotedData
| UserMentionedData;
export interface Notification {
id: string;
userId: string;
type: NotificationType;
data: NotificationData;
read: boolean;
createdAt: Date;
}