feat: add shared @sexy.pivoine.art/types package and fix type safety across frontend/backend
- Create packages/types with shared TypeScript domain model interfaces (User, Video, Model, Article, Comment, Recording, etc.) - Wire both frontend and backend packages to use @sexy.pivoine.art/types via workspace:* - Update backend Pothos objectRef types to use shared interfaces instead of inline types - Update frontend $lib/types.ts to re-export from shared package - Fix all type errors introduced by more accurate nullable types (avatar/banner as string|null UUIDs, author nullable, events/device_info as object[]) - Add artist_name to comment user select in backend resolver - Widen utility function signatures (getAssetUrl, getUserInitials, calcReadingTime) to accept null/undefined Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
16
packages/types/package.json
Normal file
16
packages/types/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@sexy.pivoine.art/types",
|
||||
"version": "1.0.0",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"check": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
281
packages/types/src/index.ts
Normal file
281
packages/types/src/index.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
// ─── Core entities ───────────────────────────────────────────────────────────
|
||||
|
||||
export interface MediaFile {
|
||||
id: string;
|
||||
title: string | null;
|
||||
description: string | null;
|
||||
filename: string;
|
||||
mime_type: string | null;
|
||||
filesize: number | null;
|
||||
duration: number | null;
|
||||
uploaded_by: string | null;
|
||||
date_created: Date;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
first_name: string | null;
|
||||
last_name: string | null;
|
||||
artist_name: string | null;
|
||||
slug: string | null;
|
||||
description: string | null;
|
||||
tags: string[] | null;
|
||||
role: "model" | "viewer" | "admin";
|
||||
/** UUID of the avatar file */
|
||||
avatar: string | null;
|
||||
/** UUID of the banner file */
|
||||
banner: string | null;
|
||||
email_verified: boolean;
|
||||
date_created: Date;
|
||||
}
|
||||
|
||||
export type CurrentUser = User;
|
||||
|
||||
// ─── Video ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface VideoModel {
|
||||
id: string;
|
||||
artist_name: string | null;
|
||||
slug: string | null;
|
||||
avatar: string | null;
|
||||
}
|
||||
|
||||
export interface VideoFile {
|
||||
id: string;
|
||||
filename: string;
|
||||
mime_type: string | null;
|
||||
duration: number | null;
|
||||
}
|
||||
|
||||
export interface Video {
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string | null;
|
||||
image: string | null;
|
||||
movie: string | null;
|
||||
tags: string[] | null;
|
||||
upload_date: Date;
|
||||
premium: boolean | null;
|
||||
featured: boolean | null;
|
||||
likes_count: number | null;
|
||||
plays_count: number | null;
|
||||
models?: VideoModel[];
|
||||
movie_file?: VideoFile | null;
|
||||
}
|
||||
|
||||
// ─── Model ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface ModelPhoto {
|
||||
id: string;
|
||||
filename: string;
|
||||
}
|
||||
|
||||
export interface Model {
|
||||
id: string;
|
||||
slug: string | null;
|
||||
artist_name: string | null;
|
||||
description: string | null;
|
||||
avatar: string | null;
|
||||
banner: string | null;
|
||||
tags: string[] | null;
|
||||
date_created: Date;
|
||||
photos?: ModelPhoto[];
|
||||
}
|
||||
|
||||
// ─── Article ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface ArticleAuthor {
|
||||
first_name: string | null;
|
||||
last_name: string | null;
|
||||
avatar: string | null;
|
||||
description: string | null;
|
||||
website?: string | null;
|
||||
}
|
||||
|
||||
export interface Article {
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
excerpt: string | null;
|
||||
content: string | null;
|
||||
image: string | null;
|
||||
tags: string[] | null;
|
||||
publish_date: Date;
|
||||
category: string | null;
|
||||
featured: boolean | null;
|
||||
author?: ArticleAuthor | null;
|
||||
}
|
||||
|
||||
// ─── Comment ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface CommentUser {
|
||||
id: string;
|
||||
first_name: string | null;
|
||||
last_name: string | null;
|
||||
artist_name: string | null;
|
||||
avatar: string | null;
|
||||
}
|
||||
|
||||
export interface Comment {
|
||||
id: number;
|
||||
collection: string;
|
||||
item_id: string;
|
||||
comment: string;
|
||||
user_id: string;
|
||||
date_created: Date;
|
||||
user?: CommentUser | null;
|
||||
}
|
||||
|
||||
// ─── Stats ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface Stats {
|
||||
videos_count: number;
|
||||
models_count: number;
|
||||
viewers_count: number;
|
||||
}
|
||||
|
||||
// ─── Recording ───────────────────────────────────────────────────────────────
|
||||
|
||||
export interface RecordedEvent {
|
||||
timestamp: number;
|
||||
deviceIndex: number;
|
||||
deviceName: string;
|
||||
actuatorIndex: number;
|
||||
actuatorType: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface DeviceInfo {
|
||||
name: string;
|
||||
index: number;
|
||||
capabilities: string[];
|
||||
}
|
||||
|
||||
export interface Recording {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string | null;
|
||||
slug: string;
|
||||
duration: number;
|
||||
events: object[] | null;
|
||||
device_info: object[] | null;
|
||||
user_id: string;
|
||||
status: "draft" | "published" | "archived";
|
||||
tags: string[] | null;
|
||||
linked_video: string | null;
|
||||
featured: boolean | null;
|
||||
public: boolean | null;
|
||||
original_recording_id?: string | null;
|
||||
date_created: Date;
|
||||
date_updated: Date | null;
|
||||
}
|
||||
|
||||
// ─── Video interactions ───────────────────────────────────────────────────────
|
||||
|
||||
export interface VideoLikeStatus {
|
||||
liked: boolean;
|
||||
}
|
||||
|
||||
export interface VideoPlayRecord {
|
||||
id: string;
|
||||
video_id: string;
|
||||
duration_watched?: number;
|
||||
completed: boolean;
|
||||
}
|
||||
|
||||
export interface VideoLikeResponse {
|
||||
liked: boolean;
|
||||
likes_count: number;
|
||||
}
|
||||
|
||||
export interface VideoPlayResponse {
|
||||
success: boolean;
|
||||
play_id: string;
|
||||
plays_count: number;
|
||||
}
|
||||
|
||||
// ─── Analytics ───────────────────────────────────────────────────────────────
|
||||
|
||||
export interface VideoAnalytics {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
upload_date: Date;
|
||||
likes: number;
|
||||
plays: number;
|
||||
completed_plays: number;
|
||||
completion_rate: number;
|
||||
avg_watch_time: number;
|
||||
}
|
||||
|
||||
export interface Analytics {
|
||||
total_videos: number;
|
||||
total_likes: number;
|
||||
total_plays: number;
|
||||
plays_by_date: Record<string, number>;
|
||||
likes_by_date: Record<string, number>;
|
||||
videos: VideoAnalytics[];
|
||||
}
|
||||
|
||||
// ─── Gamification ────────────────────────────────────────────────────────────
|
||||
|
||||
export interface LeaderboardEntry {
|
||||
user_id: string;
|
||||
display_name: string | null;
|
||||
avatar: string | null;
|
||||
total_weighted_points: number | null;
|
||||
total_raw_points: number | null;
|
||||
recordings_count: number | null;
|
||||
playbacks_count: number | null;
|
||||
achievements_count: number | null;
|
||||
rank: number;
|
||||
}
|
||||
|
||||
export interface UserStats {
|
||||
user_id: string;
|
||||
total_raw_points: number | null;
|
||||
total_weighted_points: number | null;
|
||||
recordings_count: number | null;
|
||||
playbacks_count: number | null;
|
||||
comments_count: number | null;
|
||||
achievements_count: number | null;
|
||||
rank: number;
|
||||
}
|
||||
|
||||
export interface UserAchievement {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
icon: string | null;
|
||||
category: string | null;
|
||||
date_unlocked: Date;
|
||||
progress: number | null;
|
||||
required_count: number;
|
||||
}
|
||||
|
||||
export interface RecentPoint {
|
||||
action: string;
|
||||
points: number;
|
||||
date_created: Date;
|
||||
recording_id: string | null;
|
||||
}
|
||||
|
||||
export interface UserGamification {
|
||||
stats: UserStats | null;
|
||||
achievements: UserAchievement[];
|
||||
recent_points: RecentPoint[];
|
||||
}
|
||||
|
||||
export interface Achievement {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
icon: string | null;
|
||||
category: string | null;
|
||||
required_count: number;
|
||||
points_reward: number;
|
||||
}
|
||||
10
packages/types/tsconfig.json
Normal file
10
packages/types/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user