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:
2026-03-05 11:01:11 +01:00
parent c6126c13e9
commit 97269788ee
31 changed files with 839 additions and 822 deletions

View File

@@ -14,6 +14,7 @@
"check": "tsc --noEmit"
},
"dependencies": {
"@sexy.pivoine.art/types": "workspace:*",
"@fastify/cookie": "^11.0.2",
"@fastify/cors": "^10.0.2",
"@fastify/multipart": "^9.0.3",

View File

@@ -25,6 +25,7 @@ builder.queryField("commentsForVideo", (t) =>
id: users.id,
first_name: users.first_name,
last_name: users.last_name,
artist_name: users.artist_name,
avatar: users.avatar,
})
.from(users)
@@ -66,6 +67,7 @@ builder.mutationField("createCommentForVideo", (t) =>
id: users.id,
first_name: users.first_name,
last_name: users.last_name,
artist_name: users.artist_name,
avatar: users.avatar,
})
.from(users)

View File

@@ -1,383 +1,269 @@
import type {
MediaFile,
User,
VideoModel,
VideoFile,
Video,
ModelPhoto,
Model,
ArticleAuthor,
Article,
CommentUser,
Comment,
Stats,
Recording,
VideoLikeStatus,
VideoLikeResponse,
VideoPlayResponse,
VideoAnalytics,
Analytics,
LeaderboardEntry,
UserStats,
UserAchievement,
RecentPoint,
UserGamification,
Achievement,
} from "@sexy.pivoine.art/types";
import { builder } from "../builder";
// File type
export const FileType = builder
.objectRef<{
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;
}>("File")
export const FileType = builder.objectRef<MediaFile>("File").implement({
fields: (t) => ({
id: t.exposeString("id"),
title: t.exposeString("title", { nullable: true }),
description: t.exposeString("description", { nullable: true }),
filename: t.exposeString("filename"),
mime_type: t.exposeString("mime_type", { nullable: true }),
filesize: t.exposeFloat("filesize", { nullable: true }),
duration: t.exposeInt("duration", { nullable: true }),
uploaded_by: t.exposeString("uploaded_by", { nullable: true }),
date_created: t.expose("date_created", { type: "DateTime" }),
}),
});
export const UserType = builder.objectRef<User>("User").implement({
fields: (t) => ({
id: t.exposeString("id"),
email: t.exposeString("email"),
first_name: t.exposeString("first_name", { nullable: true }),
last_name: t.exposeString("last_name", { nullable: true }),
artist_name: t.exposeString("artist_name", { nullable: true }),
slug: t.exposeString("slug", { nullable: true }),
description: t.exposeString("description", { nullable: true }),
tags: t.exposeStringList("tags", { nullable: true }),
role: t.exposeString("role"),
avatar: t.exposeString("avatar", { nullable: true }),
banner: t.exposeString("banner", { nullable: true }),
email_verified: t.exposeBoolean("email_verified"),
date_created: t.expose("date_created", { type: "DateTime" }),
}),
});
// CurrentUser is the same shape as User
export const CurrentUserType = builder.objectRef<User>("CurrentUser").implement({
fields: (t) => ({
id: t.exposeString("id"),
email: t.exposeString("email"),
first_name: t.exposeString("first_name", { nullable: true }),
last_name: t.exposeString("last_name", { nullable: true }),
artist_name: t.exposeString("artist_name", { nullable: true }),
slug: t.exposeString("slug", { nullable: true }),
description: t.exposeString("description", { nullable: true }),
tags: t.exposeStringList("tags", { nullable: true }),
role: t.exposeString("role"),
avatar: t.exposeString("avatar", { nullable: true }),
banner: t.exposeString("banner", { nullable: true }),
email_verified: t.exposeBoolean("email_verified"),
date_created: t.expose("date_created", { type: "DateTime" }),
}),
});
export const VideoModelType = builder.objectRef<VideoModel>("VideoModel").implement({
fields: (t) => ({
id: t.exposeString("id"),
artist_name: t.exposeString("artist_name", { nullable: true }),
slug: t.exposeString("slug", { nullable: true }),
avatar: t.exposeString("avatar", { nullable: true }),
}),
});
export const VideoFileType = builder.objectRef<VideoFile>("VideoFile").implement({
fields: (t) => ({
id: t.exposeString("id"),
filename: t.exposeString("filename"),
mime_type: t.exposeString("mime_type", { nullable: true }),
duration: t.exposeInt("duration", { nullable: true }),
}),
});
export const VideoType = builder.objectRef<Video>("Video").implement({
fields: (t) => ({
id: t.exposeString("id"),
slug: t.exposeString("slug"),
title: t.exposeString("title"),
description: t.exposeString("description", { nullable: true }),
image: t.exposeString("image", { nullable: true }),
movie: t.exposeString("movie", { nullable: true }),
tags: t.exposeStringList("tags", { nullable: true }),
upload_date: t.expose("upload_date", { type: "DateTime" }),
premium: t.exposeBoolean("premium", { nullable: true }),
featured: t.exposeBoolean("featured", { nullable: true }),
likes_count: t.exposeInt("likes_count", { nullable: true }),
plays_count: t.exposeInt("plays_count", { nullable: true }),
models: t.expose("models", { type: [VideoModelType], nullable: true }),
movie_file: t.expose("movie_file", { type: VideoFileType, nullable: true }),
}),
});
export const ModelPhotoType = builder.objectRef<ModelPhoto>("ModelPhoto").implement({
fields: (t) => ({
id: t.exposeString("id"),
filename: t.exposeString("filename"),
}),
});
export const ModelType = builder.objectRef<Model>("Model").implement({
fields: (t) => ({
id: t.exposeString("id"),
slug: t.exposeString("slug", { nullable: true }),
artist_name: t.exposeString("artist_name", { nullable: true }),
description: t.exposeString("description", { nullable: true }),
avatar: t.exposeString("avatar", { nullable: true }),
banner: t.exposeString("banner", { nullable: true }),
tags: t.exposeStringList("tags", { nullable: true }),
date_created: t.expose("date_created", { type: "DateTime" }),
photos: t.expose("photos", { type: [ModelPhotoType], nullable: true }),
}),
});
export const ArticleAuthorType = builder.objectRef<ArticleAuthor>("ArticleAuthor").implement({
fields: (t) => ({
first_name: t.exposeString("first_name", { nullable: true }),
last_name: t.exposeString("last_name", { nullable: true }),
avatar: t.exposeString("avatar", { nullable: true }),
description: t.exposeString("description", { nullable: true }),
}),
});
export const ArticleType = builder.objectRef<Article>("Article").implement({
fields: (t) => ({
id: t.exposeString("id"),
slug: t.exposeString("slug"),
title: t.exposeString("title"),
excerpt: t.exposeString("excerpt", { nullable: true }),
content: t.exposeString("content", { nullable: true }),
image: t.exposeString("image", { nullable: true }),
tags: t.exposeStringList("tags", { nullable: true }),
publish_date: t.expose("publish_date", { type: "DateTime" }),
category: t.exposeString("category", { nullable: true }),
featured: t.exposeBoolean("featured", { nullable: true }),
author: t.expose("author", { type: ArticleAuthorType, nullable: true }),
}),
});
export const CommentUserType = builder.objectRef<CommentUser>("CommentUser").implement({
fields: (t) => ({
id: t.exposeString("id"),
first_name: t.exposeString("first_name", { nullable: true }),
last_name: t.exposeString("last_name", { nullable: true }),
artist_name: t.exposeString("artist_name", { nullable: true }),
avatar: t.exposeString("avatar", { nullable: true }),
}),
});
export const CommentType = builder.objectRef<Comment>("Comment").implement({
fields: (t) => ({
id: t.exposeInt("id"),
collection: t.exposeString("collection"),
item_id: t.exposeString("item_id"),
comment: t.exposeString("comment"),
user_id: t.exposeString("user_id"),
date_created: t.expose("date_created", { type: "DateTime" }),
user: t.expose("user", { type: CommentUserType, nullable: true }),
}),
});
export const StatsType = builder.objectRef<Stats>("Stats").implement({
fields: (t) => ({
videos_count: t.exposeInt("videos_count"),
models_count: t.exposeInt("models_count"),
viewers_count: t.exposeInt("viewers_count"),
}),
});
export const RecordingType = builder.objectRef<Recording>("Recording").implement({
fields: (t) => ({
id: t.exposeString("id"),
title: t.exposeString("title"),
description: t.exposeString("description", { nullable: true }),
slug: t.exposeString("slug"),
duration: t.exposeInt("duration"),
events: t.expose("events", { type: "JSON", nullable: true }),
device_info: t.expose("device_info", { type: "JSON", nullable: true }),
user_id: t.exposeString("user_id"),
status: t.exposeString("status"),
tags: t.exposeStringList("tags", { nullable: true }),
linked_video: t.exposeString("linked_video", { nullable: true }),
featured: t.exposeBoolean("featured", { nullable: true }),
public: t.exposeBoolean("public", { nullable: true }),
date_created: t.expose("date_created", { type: "DateTime" }),
date_updated: t.expose("date_updated", { type: "DateTime", nullable: true }),
}),
});
export const VideoLikeResponseType = builder
.objectRef<VideoLikeResponse>("VideoLikeResponse")
.implement({
fields: (t) => ({
id: t.exposeString("id"),
title: t.exposeString("title", { nullable: true }),
description: t.exposeString("description", { nullable: true }),
filename: t.exposeString("filename"),
mime_type: t.exposeString("mime_type", { nullable: true }),
filesize: t.exposeFloat("filesize", { nullable: true }),
duration: t.exposeInt("duration", { nullable: true }),
uploaded_by: t.exposeString("uploaded_by", { nullable: true }),
date_created: t.expose("date_created", { type: "DateTime" }),
liked: t.exposeBoolean("liked"),
likes_count: t.exposeInt("likes_count"),
}),
});
// User type
export const UserType = builder
.objectRef<{
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";
avatar: string | null;
banner: string | null;
email_verified: boolean;
date_created: Date;
}>("User")
export const VideoPlayResponseType = builder
.objectRef<VideoPlayResponse>("VideoPlayResponse")
.implement({
fields: (t) => ({
id: t.exposeString("id"),
email: t.exposeString("email"),
first_name: t.exposeString("first_name", { nullable: true }),
last_name: t.exposeString("last_name", { nullable: true }),
artist_name: t.exposeString("artist_name", { nullable: true }),
slug: t.exposeString("slug", { nullable: true }),
description: t.exposeString("description", { nullable: true }),
tags: t.exposeStringList("tags", { nullable: true }),
role: t.exposeString("role"),
avatar: t.exposeString("avatar", { nullable: true }),
banner: t.exposeString("banner", { nullable: true }),
email_verified: t.exposeBoolean("email_verified"),
date_created: t.expose("date_created", { type: "DateTime" }),
success: t.exposeBoolean("success"),
play_id: t.exposeString("play_id"),
plays_count: t.exposeInt("plays_count"),
}),
});
// CurrentUser type (same shape, used for auth context)
export const CurrentUserType = builder
.objectRef<{
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";
avatar: string | null;
banner: string | null;
email_verified: boolean;
date_created: Date;
}>("CurrentUser")
export const VideoLikeStatusType = builder
.objectRef<VideoLikeStatus>("VideoLikeStatus")
.implement({
fields: (t) => ({
id: t.exposeString("id"),
email: t.exposeString("email"),
first_name: t.exposeString("first_name", { nullable: true }),
last_name: t.exposeString("last_name", { nullable: true }),
artist_name: t.exposeString("artist_name", { nullable: true }),
slug: t.exposeString("slug", { nullable: true }),
description: t.exposeString("description", { nullable: true }),
tags: t.exposeStringList("tags", { nullable: true }),
role: t.exposeString("role"),
avatar: t.exposeString("avatar", { nullable: true }),
banner: t.exposeString("banner", { nullable: true }),
email_verified: t.exposeBoolean("email_verified"),
date_created: t.expose("date_created", { type: "DateTime" }),
liked: t.exposeBoolean("liked"),
}),
});
// Video type
export const VideoType = builder
.objectRef<{
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?: {
id: string;
artist_name: string | null;
slug: string | null;
avatar: string | null;
}[];
movie_file?: {
id: string;
filename: string;
mime_type: string | null;
duration: number | null;
} | null;
}>("Video")
.implement({
fields: (t) => ({
id: t.exposeString("id"),
slug: t.exposeString("slug"),
title: t.exposeString("title"),
description: t.exposeString("description", { nullable: true }),
image: t.exposeString("image", { nullable: true }),
movie: t.exposeString("movie", { nullable: true }),
tags: t.exposeStringList("tags", { nullable: true }),
upload_date: t.expose("upload_date", { type: "DateTime" }),
premium: t.exposeBoolean("premium", { nullable: true }),
featured: t.exposeBoolean("featured", { nullable: true }),
likes_count: t.exposeInt("likes_count", { nullable: true }),
plays_count: t.exposeInt("plays_count", { nullable: true }),
models: t.expose("models", { type: [VideoModelType], nullable: true }),
movie_file: t.expose("movie_file", { type: VideoFileType, nullable: true }),
}),
});
export const VideoAnalyticsType = builder.objectRef<VideoAnalytics>("VideoAnalytics").implement({
fields: (t) => ({
id: t.exposeString("id"),
title: t.exposeString("title"),
slug: t.exposeString("slug"),
upload_date: t.expose("upload_date", { type: "DateTime" }),
likes: t.exposeInt("likes"),
plays: t.exposeInt("plays"),
completed_plays: t.exposeInt("completed_plays"),
completion_rate: t.exposeFloat("completion_rate"),
avg_watch_time: t.exposeInt("avg_watch_time"),
}),
});
export const VideoModelType = builder
.objectRef<{
id: string;
artist_name: string | null;
slug: string | null;
avatar: string | null;
}>("VideoModel")
.implement({
fields: (t) => ({
id: t.exposeString("id"),
artist_name: t.exposeString("artist_name", { nullable: true }),
slug: t.exposeString("slug", { nullable: true }),
avatar: t.exposeString("avatar", { nullable: true }),
}),
});
export const AnalyticsType = builder.objectRef<Analytics>("Analytics").implement({
fields: (t) => ({
total_videos: t.exposeInt("total_videos"),
total_likes: t.exposeInt("total_likes"),
total_plays: t.exposeInt("total_plays"),
plays_by_date: t.expose("plays_by_date", { type: "JSON" }),
likes_by_date: t.expose("likes_by_date", { type: "JSON" }),
videos: t.expose("videos", { type: [VideoAnalyticsType] }),
}),
});
export const VideoFileType = builder
.objectRef<{
id: string;
filename: string;
mime_type: string | null;
duration: number | null;
}>("VideoFile")
.implement({
fields: (t) => ({
id: t.exposeString("id"),
filename: t.exposeString("filename"),
mime_type: t.exposeString("mime_type", { nullable: true }),
duration: t.exposeInt("duration", { nullable: true }),
}),
});
// Model type (model profile, enriched user)
export const ModelType = builder
.objectRef<{
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?: { id: string; filename: string }[];
}>("Model")
.implement({
fields: (t) => ({
id: t.exposeString("id"),
slug: t.exposeString("slug", { nullable: true }),
artist_name: t.exposeString("artist_name", { nullable: true }),
description: t.exposeString("description", { nullable: true }),
avatar: t.exposeString("avatar", { nullable: true }),
banner: t.exposeString("banner", { nullable: true }),
tags: t.exposeStringList("tags", { nullable: true }),
date_created: t.expose("date_created", { type: "DateTime" }),
photos: t.expose("photos", { type: [ModelPhotoType], nullable: true }),
}),
});
export const ModelPhotoType = builder
.objectRef<{
id: string;
filename: string;
}>("ModelPhoto")
.implement({
fields: (t) => ({
id: t.exposeString("id"),
filename: t.exposeString("filename"),
}),
});
// Article type
export const ArticleType = builder
.objectRef<{
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?: {
first_name: string | null;
last_name: string | null;
avatar: string | null;
description: string | null;
} | null;
}>("Article")
.implement({
fields: (t) => ({
id: t.exposeString("id"),
slug: t.exposeString("slug"),
title: t.exposeString("title"),
excerpt: t.exposeString("excerpt", { nullable: true }),
content: t.exposeString("content", { nullable: true }),
image: t.exposeString("image", { nullable: true }),
tags: t.exposeStringList("tags", { nullable: true }),
publish_date: t.expose("publish_date", { type: "DateTime" }),
category: t.exposeString("category", { nullable: true }),
featured: t.exposeBoolean("featured", { nullable: true }),
author: t.expose("author", { type: ArticleAuthorType, nullable: true }),
}),
});
export const ArticleAuthorType = builder
.objectRef<{
first_name: string | null;
last_name: string | null;
avatar: string | null;
description: string | null;
}>("ArticleAuthor")
.implement({
fields: (t) => ({
first_name: t.exposeString("first_name", { nullable: true }),
last_name: t.exposeString("last_name", { nullable: true }),
avatar: t.exposeString("avatar", { nullable: true }),
description: t.exposeString("description", { nullable: true }),
}),
});
// Recording type
export const RecordingType = builder
.objectRef<{
id: string;
title: string;
description: string | null;
slug: string;
duration: number;
events: object[] | null;
device_info: object[] | null;
user_id: string;
status: string;
tags: string[] | null;
linked_video: string | null;
featured: boolean | null;
public: boolean | null;
date_created: Date;
date_updated: Date | null;
}>("Recording")
.implement({
fields: (t) => ({
id: t.exposeString("id"),
title: t.exposeString("title"),
description: t.exposeString("description", { nullable: true }),
slug: t.exposeString("slug"),
duration: t.exposeInt("duration"),
events: t.expose("events", { type: "JSON", nullable: true }),
device_info: t.expose("device_info", { type: "JSON", nullable: true }),
user_id: t.exposeString("user_id"),
status: t.exposeString("status"),
tags: t.exposeStringList("tags", { nullable: true }),
linked_video: t.exposeString("linked_video", { nullable: true }),
featured: t.exposeBoolean("featured", { nullable: true }),
public: t.exposeBoolean("public", { nullable: true }),
date_created: t.expose("date_created", { type: "DateTime" }),
date_updated: t.expose("date_updated", { type: "DateTime", nullable: true }),
}),
});
// Comment type
export const CommentType = builder
.objectRef<{
id: number;
collection: string;
item_id: string;
comment: string;
user_id: string;
date_created: Date;
user?: {
id: string;
first_name: string | null;
last_name: string | null;
avatar: string | null;
} | null;
}>("Comment")
.implement({
fields: (t) => ({
id: t.exposeInt("id"),
collection: t.exposeString("collection"),
item_id: t.exposeString("item_id"),
comment: t.exposeString("comment"),
user_id: t.exposeString("user_id"),
date_created: t.expose("date_created", { type: "DateTime" }),
user: t.expose("user", { type: CommentUserType, nullable: true }),
}),
});
export const CommentUserType = builder
.objectRef<{
id: string;
first_name: string | null;
last_name: string | null;
avatar: string | null;
}>("CommentUser")
.implement({
fields: (t) => ({
id: t.exposeString("id"),
first_name: t.exposeString("first_name", { nullable: true }),
last_name: t.exposeString("last_name", { nullable: true }),
avatar: t.exposeString("avatar", { nullable: true }),
}),
});
// Stats type
export const StatsType = builder
.objectRef<{
videos_count: number;
models_count: number;
viewers_count: number;
}>("Stats")
.implement({
fields: (t) => ({
videos_count: t.exposeInt("videos_count"),
models_count: t.exposeInt("models_count"),
viewers_count: t.exposeInt("viewers_count"),
}),
});
// Gamification types
export const LeaderboardEntryType = builder
.objectRef<{
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;
}>("LeaderboardEntry")
.objectRef<LeaderboardEntry>("LeaderboardEntry")
.implement({
fields: (t) => ({
user_id: t.exposeString("user_id"),
@@ -392,60 +278,44 @@ export const LeaderboardEntryType = builder
}),
});
export const AchievementType = builder
.objectRef<{
id: string;
code: string;
name: string;
description: string | null;
icon: string | null;
category: string | null;
required_count: number;
points_reward: number;
}>("Achievement")
.implement({
fields: (t) => ({
id: t.exposeString("id"),
code: t.exposeString("code"),
name: t.exposeString("name"),
description: t.exposeString("description", { nullable: true }),
icon: t.exposeString("icon", { nullable: true }),
category: t.exposeString("category", { nullable: true }),
required_count: t.exposeInt("required_count"),
points_reward: t.exposeInt("points_reward"),
}),
});
export const UserStatsType = builder.objectRef<UserStats>("UserStats").implement({
fields: (t) => ({
user_id: t.exposeString("user_id"),
total_raw_points: t.exposeInt("total_raw_points", { nullable: true }),
total_weighted_points: t.exposeFloat("total_weighted_points", { nullable: true }),
recordings_count: t.exposeInt("recordings_count", { nullable: true }),
playbacks_count: t.exposeInt("playbacks_count", { nullable: true }),
comments_count: t.exposeInt("comments_count", { nullable: true }),
achievements_count: t.exposeInt("achievements_count", { nullable: true }),
rank: t.exposeInt("rank"),
}),
});
export const UserAchievementType = builder.objectRef<UserAchievement>("UserAchievement").implement({
fields: (t) => ({
id: t.exposeString("id"),
code: t.exposeString("code"),
name: t.exposeString("name"),
description: t.exposeString("description", { nullable: true }),
icon: t.exposeString("icon", { nullable: true }),
category: t.exposeString("category", { nullable: true }),
date_unlocked: t.expose("date_unlocked", { type: "DateTime" }),
progress: t.exposeInt("progress", { nullable: true }),
required_count: t.exposeInt("required_count"),
}),
});
export const RecentPointType = builder.objectRef<RecentPoint>("RecentPoint").implement({
fields: (t) => ({
action: t.exposeString("action"),
points: t.exposeInt("points"),
date_created: t.expose("date_created", { type: "DateTime" }),
recording_id: t.exposeString("recording_id", { nullable: true }),
}),
});
export const UserGamificationType = builder
.objectRef<{
stats: {
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;
} | null;
achievements: {
id: string;
code: string;
name: string;
description: string | null;
icon: string | null;
category: string | null;
date_unlocked: Date;
progress: number | null;
required_count: number;
}[];
recent_points: {
action: string;
points: number;
date_created: Date;
recording_id: string | null;
}[];
}>("UserGamification")
.objectRef<UserGamification>("UserGamification")
.implement({
fields: (t) => ({
stats: t.expose("stats", { type: UserStatsType, nullable: true }),
@@ -454,162 +324,15 @@ export const UserGamificationType = builder
}),
});
export const UserStatsType = builder
.objectRef<{
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;
}>("UserStats")
.implement({
fields: (t) => ({
user_id: t.exposeString("user_id"),
total_raw_points: t.exposeInt("total_raw_points", { nullable: true }),
total_weighted_points: t.exposeFloat("total_weighted_points", { nullable: true }),
recordings_count: t.exposeInt("recordings_count", { nullable: true }),
playbacks_count: t.exposeInt("playbacks_count", { nullable: true }),
comments_count: t.exposeInt("comments_count", { nullable: true }),
achievements_count: t.exposeInt("achievements_count", { nullable: true }),
rank: t.exposeInt("rank"),
}),
});
export const UserAchievementType = builder
.objectRef<{
id: string;
code: string;
name: string;
description: string | null;
icon: string | null;
category: string | null;
date_unlocked: Date;
progress: number | null;
required_count: number;
}>("UserAchievement")
.implement({
fields: (t) => ({
id: t.exposeString("id"),
code: t.exposeString("code"),
name: t.exposeString("name"),
description: t.exposeString("description", { nullable: true }),
icon: t.exposeString("icon", { nullable: true }),
category: t.exposeString("category", { nullable: true }),
date_unlocked: t.expose("date_unlocked", { type: "DateTime" }),
progress: t.exposeInt("progress", { nullable: true }),
required_count: t.exposeInt("required_count"),
}),
});
export const RecentPointType = builder
.objectRef<{
action: string;
points: number;
date_created: Date;
recording_id: string | null;
}>("RecentPoint")
.implement({
fields: (t) => ({
action: t.exposeString("action"),
points: t.exposeInt("points"),
date_created: t.expose("date_created", { type: "DateTime" }),
recording_id: t.exposeString("recording_id", { nullable: true }),
}),
});
// Analytics types
export const AnalyticsType = builder
.objectRef<{
total_videos: number;
total_likes: number;
total_plays: number;
plays_by_date: Record<string, number>;
likes_by_date: Record<string, number>;
videos: {
id: string;
title: string;
slug: string;
upload_date: Date;
likes: number;
plays: number;
completed_plays: number;
completion_rate: number;
avg_watch_time: number;
}[];
}>("Analytics")
.implement({
fields: (t) => ({
total_videos: t.exposeInt("total_videos"),
total_likes: t.exposeInt("total_likes"),
total_plays: t.exposeInt("total_plays"),
plays_by_date: t.expose("plays_by_date", { type: "JSON" }),
likes_by_date: t.expose("likes_by_date", { type: "JSON" }),
videos: t.expose("videos", { type: [VideoAnalyticsType] }),
}),
});
export const VideoAnalyticsType = builder
.objectRef<{
id: string;
title: string;
slug: string;
upload_date: Date;
likes: number;
plays: number;
completed_plays: number;
completion_rate: number;
avg_watch_time: number;
}>("VideoAnalytics")
.implement({
fields: (t) => ({
id: t.exposeString("id"),
title: t.exposeString("title"),
slug: t.exposeString("slug"),
upload_date: t.expose("upload_date", { type: "DateTime" }),
likes: t.exposeInt("likes"),
plays: t.exposeInt("plays"),
completed_plays: t.exposeInt("completed_plays"),
completion_rate: t.exposeFloat("completion_rate"),
avg_watch_time: t.exposeInt("avg_watch_time"),
}),
});
// Response types
export const VideoLikeResponseType = builder
.objectRef<{
liked: boolean;
likes_count: number;
}>("VideoLikeResponse")
.implement({
fields: (t) => ({
liked: t.exposeBoolean("liked"),
likes_count: t.exposeInt("likes_count"),
}),
});
export const VideoPlayResponseType = builder
.objectRef<{
success: boolean;
play_id: string;
plays_count: number;
}>("VideoPlayResponse")
.implement({
fields: (t) => ({
success: t.exposeBoolean("success"),
play_id: t.exposeString("play_id"),
plays_count: t.exposeInt("plays_count"),
}),
});
export const VideoLikeStatusType = builder
.objectRef<{
liked: boolean;
}>("VideoLikeStatus")
.implement({
fields: (t) => ({
liked: t.exposeBoolean("liked"),
}),
});
export const AchievementType = builder.objectRef<Achievement>("Achievement").implement({
fields: (t) => ({
id: t.exposeString("id"),
code: t.exposeString("code"),
name: t.exposeString("name"),
description: t.exposeString("description", { nullable: true }),
icon: t.exposeString("icon", { nullable: true }),
category: t.exposeString("category", { nullable: true }),
required_count: t.exposeInt("required_count"),
points_reward: t.exposeInt("points_reward"),
}),
});