Files
sexy/packages/frontend/src/lib/services.ts
Sebastian Krüger bff354094e
Some checks failed
Build and Push Backend Image / build (push) Successful in 43s
Build and Push Frontend Image / build (push) Has been cancelled
fix: add adminGetVideo/adminGetArticle queries to fix 404 on edit pages
The edit page loaders were calling adminListVideos/adminListArticles with the
old pre-pagination signatures and filtering by ID client-side, which broke
after pagination limited results to 50. Now fetches the single item by ID
directly via new adminGetVideo and adminGetArticle backend queries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 11:05:21 +01:00

1779 lines
40 KiB
TypeScript

import { gql, GraphQLClient } from "graphql-request";
import { apiUrl, getGraphQLClient } from "$lib/api";
import type {
Analytics,
Article,
CurrentUser,
Model,
Recording,
Stats,
User,
Video,
VideoLikeStatus,
VideoLikeResponse,
VideoPlayResponse,
} from "$lib/types";
import { logger } from "$lib/logger";
// Helper to log API calls
async function loggedApiCall<T>(
operationName: string,
operation: () => Promise<T>,
context?: Record<string, unknown>,
): Promise<T> {
const startTime = Date.now();
try {
logger.debug(`🔄 API: ${operationName}`, { context });
const result = await operation();
const duration = Date.now() - startTime;
logger.info(`✅ API: ${operationName} succeeded`, { duration, context });
return result;
} catch (error) {
const duration = Date.now() - startTime;
logger.error(`❌ API: ${operationName} failed`, {
duration,
context,
error: error instanceof Error ? error : new Error(String(error)),
});
throw error;
}
}
// For server-side auth checks: forward cookie header manually
function getAuthClient(token: string, fetchFn?: typeof globalThis.fetch) {
return new GraphQLClient(`${apiUrl}/graphql`, {
fetch: fetchFn || globalThis.fetch,
headers: { cookie: `session_token=${token}` },
});
}
// ─── Auth ────────────────────────────────────────────────────────────────────
const ME_QUERY = gql`
query Me {
me {
id
email
first_name
last_name
artist_name
slug
description
tags
role
is_admin
avatar
banner
email_verified
date_created
}
}
`;
export async function isAuthenticated(token: string, fetchFn?: typeof globalThis.fetch) {
return loggedApiCall(
"isAuthenticated",
async () => {
try {
const client = getAuthClient(token, fetchFn);
const data = await client.request<{ me: CurrentUser | null }>(ME_QUERY);
if (data.me) {
return { authenticated: true, user: data.me };
}
return { authenticated: false };
} catch {
return { authenticated: false };
}
},
{ hasToken: !!token },
);
}
const LOGIN_MUTATION = gql`
mutation Login($email: String!, $password: String!) {
login(email: $email, password: $password) {
id
email
first_name
last_name
artist_name
slug
description
tags
role
avatar
banner
email_verified
date_created
}
}
`;
export async function login(email: string, password: string) {
return loggedApiCall(
"login",
async () => {
const data = await getGraphQLClient().request<{ login: CurrentUser }>(LOGIN_MUTATION, {
email,
password,
});
return data.login;
},
{ email },
);
}
const LOGOUT_MUTATION = gql`
mutation Logout {
logout
}
`;
export async function logout() {
return loggedApiCall("logout", async () => {
await getGraphQLClient().request(LOGOUT_MUTATION);
});
}
const REGISTER_MUTATION = gql`
mutation Register($email: String!, $password: String!, $firstName: String!, $lastName: String!) {
register(email: $email, password: $password, firstName: $firstName, lastName: $lastName)
}
`;
export async function register(
email: string,
password: string,
firstName: string,
lastName: string,
) {
return loggedApiCall(
"register",
async () => {
await getGraphQLClient().request(REGISTER_MUTATION, {
email,
password,
firstName,
lastName,
});
},
{ email, firstName, lastName },
);
}
const VERIFY_EMAIL_MUTATION = gql`
mutation VerifyEmail($token: String!) {
verifyEmail(token: $token)
}
`;
export async function verify(token: string, fetchFn?: typeof globalThis.fetch) {
return loggedApiCall(
"verify",
async () => {
await getGraphQLClient(fetchFn).request(VERIFY_EMAIL_MUTATION, { token });
},
{ hasToken: !!token },
);
}
const REQUEST_PASSWORD_MUTATION = gql`
mutation RequestPasswordReset($email: String!) {
requestPasswordReset(email: $email)
}
`;
export async function requestPassword(email: string) {
return loggedApiCall(
"requestPassword",
async () => {
await getGraphQLClient().request(REQUEST_PASSWORD_MUTATION, { email });
},
{ email },
);
}
const RESET_PASSWORD_MUTATION = gql`
mutation ResetPassword($token: String!, $newPassword: String!) {
resetPassword(token: $token, newPassword: $newPassword)
}
`;
export async function resetPassword(token: string, password: string) {
return loggedApiCall(
"resetPassword",
async () => {
await getGraphQLClient().request(RESET_PASSWORD_MUTATION, {
token,
newPassword: password,
});
},
{ hasToken: !!token },
);
}
// ─── Articles ────────────────────────────────────────────────────────────────
const ARTICLES_QUERY = gql`
query GetArticles(
$search: String
$category: String
$sortBy: String
$offset: Int
$limit: Int
$featured: Boolean
$tag: String
) {
articles(
search: $search
category: $category
sortBy: $sortBy
offset: $offset
limit: $limit
featured: $featured
tag: $tag
) {
items {
id
slug
title
excerpt
content
image
tags
publish_date
category
featured
author {
id
artist_name
slug
avatar
}
}
total
}
}
`;
export async function getArticles(
params: {
search?: string;
category?: string;
sortBy?: string;
offset?: number;
limit?: number;
featured?: boolean;
tag?: string;
} = {},
fetchFn?: typeof globalThis.fetch,
): Promise<{ items: Article[]; total: number }> {
return loggedApiCall("getArticles", async () => {
const data = await getGraphQLClient(fetchFn).request<{
articles: { items: Article[]; total: number };
}>(ARTICLES_QUERY, params);
return data.articles;
});
}
const ARTICLE_BY_SLUG_QUERY = gql`
query GetArticleBySlug($slug: String!) {
article(slug: $slug) {
id
slug
title
excerpt
content
image
tags
publish_date
category
featured
author {
id
artist_name
slug
avatar
}
}
}
`;
export async function getArticleBySlug(slug: string, fetchFn?: typeof globalThis.fetch) {
return loggedApiCall(
"getArticleBySlug",
async () => {
const data = await getGraphQLClient(fetchFn).request<{ article: Article | null }>(
ARTICLE_BY_SLUG_QUERY,
{ slug },
);
if (!data.article) throw new Error("Article not found");
return data.article;
},
{ slug },
);
}
// ─── Videos ──────────────────────────────────────────────────────────────────
const VIDEOS_QUERY = gql`
query GetVideos(
$modelId: String
$featured: Boolean
$limit: Int
$search: String
$offset: Int
$sortBy: String
$duration: String
$tag: String
) {
videos(
modelId: $modelId
featured: $featured
limit: $limit
search: $search
offset: $offset
sortBy: $sortBy
duration: $duration
tag: $tag
) {
items {
id
slug
title
description
image
movie
tags
upload_date
premium
featured
likes_count
plays_count
models {
id
artist_name
slug
avatar
}
movie_file {
id
filename
mime_type
duration
}
}
total
}
}
`;
export async function getVideos(
params: {
search?: string;
sortBy?: string;
duration?: string;
offset?: number;
limit?: number;
tag?: string;
} = {},
fetchFn?: typeof globalThis.fetch,
): Promise<{ items: Video[]; total: number }> {
return loggedApiCall("getVideos", async () => {
const data = await getGraphQLClient(fetchFn).request<{
videos: { items: Video[]; total: number };
}>(VIDEOS_QUERY, params);
return data.videos;
});
}
export async function getVideosForModel(id: string, fetchFn?: typeof globalThis.fetch) {
return loggedApiCall(
"getVideosForModel",
async () => {
const data = await getGraphQLClient(fetchFn).request<{
videos: { items: Video[]; total: number };
}>(VIDEOS_QUERY, { modelId: id, limit: 10000 });
return data.videos.items;
},
{ modelId: id },
);
}
export async function getFeaturedVideos(
limit: number,
fetchFn: typeof globalThis.fetch = globalThis.fetch,
) {
return loggedApiCall(
"getFeaturedVideos",
async () => {
const data = await getGraphQLClient(fetchFn).request<{
videos: { items: Video[]; total: number };
}>(VIDEOS_QUERY, { featured: true, limit });
return data.videos.items;
},
{ limit },
);
}
const VIDEO_BY_SLUG_QUERY = gql`
query GetVideoBySlug($slug: String!) {
video(slug: $slug) {
id
slug
title
description
image
movie
tags
upload_date
premium
featured
likes_count
plays_count
models {
id
artist_name
slug
avatar
}
movie_file {
id
filename
mime_type
duration
}
}
}
`;
export async function getVideoBySlug(slug: string, fetchFn?: typeof globalThis.fetch) {
return loggedApiCall(
"getVideoBySlug",
async () => {
const data = await getGraphQLClient(fetchFn).request<{ video: Video | null }>(
VIDEO_BY_SLUG_QUERY,
{ slug },
);
if (!data.video) throw new Error("Video not found");
return data.video;
},
{ slug },
);
}
// ─── Models ──────────────────────────────────────────────────────────────────
const MODELS_QUERY = gql`
query GetModels(
$featured: Boolean
$limit: Int
$search: String
$offset: Int
$sortBy: String
$tag: String
) {
models(
featured: $featured
limit: $limit
search: $search
offset: $offset
sortBy: $sortBy
tag: $tag
) {
items {
id
slug
artist_name
description
avatar
banner
tags
date_created
photos {
id
filename
}
}
total
}
}
`;
export async function getModels(
params: { search?: string; sortBy?: string; offset?: number; limit?: number; tag?: string } = {},
fetchFn?: typeof globalThis.fetch,
): Promise<{ items: Model[]; total: number }> {
return loggedApiCall("getModels", async () => {
const data = await getGraphQLClient(fetchFn).request<{
models: { items: Model[]; total: number };
}>(MODELS_QUERY, params);
return data.models;
});
}
export async function getFeaturedModels(
limit = 3,
fetchFn: typeof globalThis.fetch = globalThis.fetch,
) {
return loggedApiCall(
"getFeaturedModels",
async () => {
const data = await getGraphQLClient(fetchFn).request<{
models: { items: Model[]; total: number };
}>(MODELS_QUERY, { featured: true, limit });
return data.models.items;
},
{ limit },
);
}
const MODEL_BY_SLUG_QUERY = gql`
query GetModelBySlug($slug: String!) {
model(slug: $slug) {
id
slug
artist_name
description
avatar
banner
tags
date_created
photos {
id
filename
}
}
}
`;
export async function getModelBySlug(slug: string, fetchFn?: typeof globalThis.fetch) {
return loggedApiCall(
"getModelBySlug",
async () => {
const data = await getGraphQLClient(fetchFn).request<{ model: Model | null }>(
MODEL_BY_SLUG_QUERY,
{ slug },
);
if (!data.model) throw new Error("Model not found");
return data.model;
},
{ slug },
);
}
// ─── Profile ─────────────────────────────────────────────────────────────────
const UPDATE_PROFILE_MUTATION = gql`
mutation UpdateProfile(
$firstName: String
$lastName: String
$artistName: String
$description: String
$tags: [String!]
) {
updateProfile(
firstName: $firstName
lastName: $lastName
artistName: $artistName
description: $description
tags: $tags
) {
id
email
first_name
last_name
artist_name
slug
description
tags
role
avatar
banner
date_created
}
}
`;
export async function updateProfile(user: Partial<User> & { password?: string }) {
return loggedApiCall(
"updateProfile",
async () => {
const data = await getGraphQLClient().request<{ updateProfile: User }>(
UPDATE_PROFILE_MUTATION,
{
firstName: user.first_name,
lastName: user.last_name,
artistName: user.artist_name,
description: user.description,
tags: user.tags,
},
);
return data.updateProfile;
},
{ userId: user.id },
);
}
// ─── Stats ───────────────────────────────────────────────────────────────────
const STATS_QUERY = gql`
query GetStats {
stats {
videos_count
models_count
viewers_count
}
}
`;
export async function getStats(fetchFn?: typeof globalThis.fetch) {
return loggedApiCall("getStats", async () => {
const data = await getGraphQLClient(fetchFn).request<{ stats: Stats }>(STATS_QUERY);
return data.stats;
});
}
// Stub — Directus folder concept dropped
export async function getFolders(_fetchFn?: typeof globalThis.fetch) {
return loggedApiCall("getFolders", async () => [] as { id: string; name: string }[]);
}
// ─── Files ───────────────────────────────────────────────────────────────────
export async function removeFile(id: string) {
return loggedApiCall(
"removeFile",
async () => {
// File deletion via REST DELETE /assets/:id (backend handles it)
const response = await fetch(`${apiUrl}/assets/${id}`, {
method: "DELETE",
credentials: "include",
});
if (!response.ok) throw new Error(`Failed to delete file: ${response.statusText}`);
},
{ fileId: id },
);
}
export async function uploadFile(data: FormData) {
return loggedApiCall("uploadFile", async () => {
const response = await fetch(`${apiUrl}/upload`, {
method: "POST",
body: data,
credentials: "include",
});
if (!response.ok) throw new Error(`Upload failed: ${response.statusText}`);
return response.json();
});
}
// ─── Comments ────────────────────────────────────────────────────────────────
const COMMENTS_FOR_VIDEO_QUERY = gql`
query CommentsForVideo($videoId: String!) {
commentsForVideo(videoId: $videoId) {
id
comment
item_id
user_id
date_created
user {
id
first_name
last_name
avatar
}
}
}
`;
export async function getCommentsForVideo(item: string, fetchFn?: typeof globalThis.fetch) {
return loggedApiCall(
"getCommentsForVideo",
async () => {
const data = await getGraphQLClient(fetchFn).request<{
commentsForVideo: {
id: number;
comment: string;
item_id: string;
user_id: string;
date_created: string;
user: {
id: string;
first_name: string | null;
last_name: string | null;
artist_name: string | null;
avatar: string | null;
} | null;
}[];
}>(COMMENTS_FOR_VIDEO_QUERY, { videoId: item });
return data.commentsForVideo;
},
{ videoId: item },
);
}
const CREATE_COMMENT_MUTATION = gql`
mutation CreateCommentForVideo($videoId: String!, $comment: String!) {
createCommentForVideo(videoId: $videoId, comment: $comment) {
id
comment
item_id
user_id
date_created
}
}
`;
export async function createCommentForVideo(item: string, comment: string) {
return loggedApiCall(
"createCommentForVideo",
async () => {
const data = await getGraphQLClient().request(CREATE_COMMENT_MUTATION, {
videoId: item,
comment,
});
return data;
},
{ videoId: item, commentLength: comment.length },
);
}
export async function countCommentsForModel(
_user_created: string,
_fetchFn?: typeof globalThis.fetch,
) {
// Not directly available in new API, return 0
return 0;
}
// ─── Tags ────────────────────────────────────────────────────────────────────
export async function getItemsByTag(
category: "video" | "article" | "model",
tag: string,
fetchFn?: typeof globalThis.fetch,
) {
return loggedApiCall(
"getItemsByTag",
async () => {
switch (category) {
case "video":
return getVideos({ tag, limit: 1000 }, fetchFn).then((r) => r.items);
case "model":
return getModels({ tag, limit: 1000 }, fetchFn).then((r) => r.items);
case "article":
return getArticles({ tag, limit: 1000 }, fetchFn).then((r) => r.items);
}
},
{ category, tag },
);
}
// ─── Recordings ──────────────────────────────────────────────────────────────
const RECORDINGS_QUERY = gql`
query GetRecordings(
$status: String
$tags: String
$linkedVideoId: String
$limit: Int
$page: Int
) {
recordings(
status: $status
tags: $tags
linkedVideoId: $linkedVideoId
limit: $limit
page: $page
) {
id
title
description
slug
duration
events
device_info
user_id
status
tags
linked_video
featured
public
date_created
date_updated
}
}
`;
export async function getRecordings(fetchFn?: typeof globalThis.fetch) {
return loggedApiCall(
"getRecordings",
async () => {
const data = await getGraphQLClient(fetchFn).request<{ recordings: Recording[] }>(
RECORDINGS_QUERY,
);
return data.recordings;
},
{},
);
}
const CREATE_RECORDING_MUTATION = gql`
mutation CreateRecording(
$title: String!
$description: String
$duration: Int!
$events: JSON!
$deviceInfo: JSON!
$tags: [String!]
$status: String
$linkedVideoId: String
) {
createRecording(
title: $title
description: $description
duration: $duration
events: $events
deviceInfo: $deviceInfo
tags: $tags
status: $status
linkedVideoId: $linkedVideoId
) {
id
title
description
slug
duration
events
device_info
user_id
status
tags
linked_video
featured
public
date_created
date_updated
}
}
`;
export async function createRecording(
recording: {
title: string;
description?: string;
duration: number;
events: unknown[];
device_info: unknown[];
tags?: string[];
status?: string;
},
fetchFn?: typeof globalThis.fetch,
) {
return loggedApiCall(
"createRecording",
async () => {
const data = await getGraphQLClient(fetchFn).request<{ createRecording: Recording }>(
CREATE_RECORDING_MUTATION,
{
title: recording.title,
description: recording.description,
duration: recording.duration,
events: recording.events,
deviceInfo: recording.device_info,
tags: recording.tags,
status: recording.status,
},
);
return data.createRecording;
},
{ title: recording.title, eventCount: recording.events.length },
);
}
const DELETE_RECORDING_MUTATION = gql`
mutation DeleteRecording($id: String!) {
deleteRecording(id: $id)
}
`;
export async function deleteRecording(id: string) {
return loggedApiCall(
"deleteRecording",
async () => {
await getGraphQLClient().request(DELETE_RECORDING_MUTATION, { id });
},
{ id },
);
}
const RECORDING_QUERY = gql`
query GetRecording($id: String!) {
recording(id: $id) {
id
title
description
slug
duration
events
device_info
user_id
status
tags
linked_video
featured
public
date_created
date_updated
}
}
`;
export async function getRecording(id: string, fetchFn?: typeof globalThis.fetch) {
return loggedApiCall(
"getRecording",
async () => {
const data = await getGraphQLClient(fetchFn).request<{ recording: Recording | null }>(
RECORDING_QUERY,
{ id },
);
return data.recording;
},
{ id },
);
}
// ─── Video likes & plays ─────────────────────────────────────────────────────
const LIKE_VIDEO_MUTATION = gql`
mutation LikeVideo($videoId: String!) {
likeVideo(videoId: $videoId) {
liked
likes_count
}
}
`;
export async function likeVideo(videoId: string) {
return loggedApiCall(
"likeVideo",
async () => {
const data = await getGraphQLClient().request<{ likeVideo: VideoLikeResponse }>(
LIKE_VIDEO_MUTATION,
{ videoId },
);
return data.likeVideo;
},
{ videoId },
);
}
const UNLIKE_VIDEO_MUTATION = gql`
mutation UnlikeVideo($videoId: String!) {
unlikeVideo(videoId: $videoId) {
liked
likes_count
}
}
`;
export async function unlikeVideo(videoId: string) {
return loggedApiCall(
"unlikeVideo",
async () => {
const data = await getGraphQLClient().request<{ unlikeVideo: VideoLikeResponse }>(
UNLIKE_VIDEO_MUTATION,
{ videoId },
);
return data.unlikeVideo;
},
{ videoId },
);
}
const VIDEO_LIKE_STATUS_QUERY = gql`
query VideoLikeStatus($videoId: String!) {
videoLikeStatus(videoId: $videoId) {
liked
}
}
`;
export async function getVideoLikeStatus(videoId: string, fetchFn?: typeof globalThis.fetch) {
return loggedApiCall(
"getVideoLikeStatus",
async () => {
const data = await getGraphQLClient(fetchFn).request<{ videoLikeStatus: VideoLikeStatus }>(
VIDEO_LIKE_STATUS_QUERY,
{ videoId },
);
return data.videoLikeStatus;
},
{ videoId },
);
}
const RECORD_VIDEO_PLAY_MUTATION = gql`
mutation RecordVideoPlay($videoId: String!, $sessionId: String) {
recordVideoPlay(videoId: $videoId, sessionId: $sessionId) {
success
play_id
plays_count
}
}
`;
export async function recordVideoPlay(videoId: string, sessionId?: string) {
return loggedApiCall(
"recordVideoPlay",
async () => {
const data = await getGraphQLClient().request<{ recordVideoPlay: VideoPlayResponse }>(
RECORD_VIDEO_PLAY_MUTATION,
{ videoId, sessionId },
);
return data.recordVideoPlay;
},
{ videoId },
);
}
const UPDATE_VIDEO_PLAY_MUTATION = gql`
mutation UpdateVideoPlay(
$videoId: String!
$playId: String!
$durationWatched: Int!
$completed: Boolean!
) {
updateVideoPlay(
videoId: $videoId
playId: $playId
durationWatched: $durationWatched
completed: $completed
)
}
`;
export async function updateVideoPlay(
videoId: string,
playId: string,
durationWatched: number,
completed: boolean,
) {
return loggedApiCall(
"updateVideoPlay",
async () => {
await getGraphQLClient().request(UPDATE_VIDEO_PLAY_MUTATION, {
videoId,
playId,
durationWatched,
completed,
});
},
{ videoId, playId, durationWatched, completed },
);
}
// ─── Delete comment ──────────────────────────────────────────────────────────
const DELETE_COMMENT_MUTATION = gql`
mutation DeleteComment($id: Int!) {
deleteComment(id: $id)
}
`;
export async function deleteComment(id: number) {
return loggedApiCall(
"deleteComment",
async () => {
await getGraphQLClient().request(DELETE_COMMENT_MUTATION, { id });
},
{ id },
);
}
// ─── Admin: Users ─────────────────────────────────────────────────────────────
const ADMIN_LIST_USERS_QUERY = gql`
query AdminListUsers($role: String, $search: String, $limit: Int, $offset: Int) {
adminListUsers(role: $role, search: $search, limit: $limit, offset: $offset) {
total
items {
id
email
first_name
last_name
artist_name
slug
role
is_admin
avatar
email_verified
date_created
}
}
}
`;
export async function adminListUsers(
opts: { role?: string; search?: string; limit?: number; offset?: number } = {},
fetchFn?: typeof globalThis.fetch,
token?: string,
) {
return loggedApiCall(
"adminListUsers",
async () => {
const client = token ? getAuthClient(token) : getGraphQLClient(fetchFn);
const data = await client.request<{
adminListUsers: { total: number; items: User[] };
}>(ADMIN_LIST_USERS_QUERY, opts);
return data.adminListUsers;
},
opts,
);
}
const ADMIN_UPDATE_USER_MUTATION = gql`
mutation AdminUpdateUser(
$userId: String!
$role: String
$isAdmin: Boolean
$firstName: String
$lastName: String
$artistName: String
$avatarId: String
$bannerId: String
) {
adminUpdateUser(
userId: $userId
role: $role
isAdmin: $isAdmin
firstName: $firstName
lastName: $lastName
artistName: $artistName
avatarId: $avatarId
bannerId: $bannerId
) {
id
email
first_name
last_name
artist_name
role
is_admin
avatar
banner
date_created
}
}
`;
export async function adminUpdateUser(input: {
userId: string;
role?: string;
isAdmin?: boolean;
firstName?: string;
lastName?: string;
artistName?: string;
avatarId?: string;
bannerId?: string;
}) {
return loggedApiCall(
"adminUpdateUser",
async () => {
const data = await getGraphQLClient().request<{ adminUpdateUser: User | null }>(
ADMIN_UPDATE_USER_MUTATION,
input,
);
return data.adminUpdateUser;
},
{ userId: input.userId },
);
}
const ADMIN_DELETE_USER_MUTATION = gql`
mutation AdminDeleteUser($userId: String!) {
adminDeleteUser(userId: $userId)
}
`;
export async function adminDeleteUser(userId: string) {
return loggedApiCall(
"adminDeleteUser",
async () => {
await getGraphQLClient().request(ADMIN_DELETE_USER_MUTATION, { userId });
},
{ userId },
);
}
const ADMIN_GET_USER_QUERY = gql`
query AdminGetUser($userId: String!) {
adminGetUser(userId: $userId) {
id
email
first_name
last_name
artist_name
slug
role
is_admin
avatar
banner
description
tags
email_verified
date_created
photos {
id
filename
}
}
}
`;
export async function adminGetUser(userId: string, token?: string) {
return loggedApiCall(
"adminGetUser",
async () => {
const client = token ? getAuthClient(token) : getGraphQLClient();
const data = await client.request<{ adminGetUser: any }>(ADMIN_GET_USER_QUERY, { userId });
return data.adminGetUser;
},
{ userId },
);
}
const ADMIN_ADD_USER_PHOTO_MUTATION = gql`
mutation AdminAddUserPhoto($userId: String!, $fileId: String!) {
adminAddUserPhoto(userId: $userId, fileId: $fileId)
}
`;
export async function adminAddUserPhoto(userId: string, fileId: string) {
return loggedApiCall("adminAddUserPhoto", async () => {
await getGraphQLClient().request(ADMIN_ADD_USER_PHOTO_MUTATION, { userId, fileId });
});
}
const ADMIN_REMOVE_USER_PHOTO_MUTATION = gql`
mutation AdminRemoveUserPhoto($userId: String!, $fileId: String!) {
adminRemoveUserPhoto(userId: $userId, fileId: $fileId)
}
`;
export async function adminRemoveUserPhoto(userId: string, fileId: string) {
return loggedApiCall("adminRemoveUserPhoto", async () => {
await getGraphQLClient().request(ADMIN_REMOVE_USER_PHOTO_MUTATION, { userId, fileId });
});
}
// ─── Admin: Videos ────────────────────────────────────────────────────────────
const ADMIN_LIST_VIDEOS_QUERY = gql`
query AdminListVideos(
$search: String
$premium: Boolean
$featured: Boolean
$limit: Int
$offset: Int
) {
adminListVideos(
search: $search
premium: $premium
featured: $featured
limit: $limit
offset: $offset
) {
items {
id
slug
title
description
image
movie
tags
upload_date
premium
featured
likes_count
plays_count
models {
id
artist_name
slug
avatar
}
movie_file {
id
filename
mime_type
duration
}
}
total
}
}
`;
export async function adminListVideos(
opts: {
search?: string;
premium?: boolean;
featured?: boolean;
limit?: number;
offset?: number;
} = {},
fetchFn?: typeof globalThis.fetch,
token?: string,
): Promise<{ items: Video[]; total: number }> {
return loggedApiCall("adminListVideos", async () => {
const client = token ? getAuthClient(token) : getGraphQLClient(fetchFn);
const data = await client.request<{ adminListVideos: { items: Video[]; total: number } }>(
ADMIN_LIST_VIDEOS_QUERY,
opts,
);
return data.adminListVideos;
});
}
const ADMIN_GET_VIDEO_QUERY = gql`
query AdminGetVideo($id: String!) {
adminGetVideo(id: $id) {
id
slug
title
description
image
movie
tags
upload_date
premium
featured
likes_count
plays_count
models {
id
artist_name
slug
avatar
}
movie_file {
id
filename
mime_type
duration
}
}
}
`;
export async function adminGetVideo(
id: string,
fetchFn?: typeof globalThis.fetch,
token?: string,
): Promise<Video | null> {
return loggedApiCall("adminGetVideo", async () => {
const client = token ? getAuthClient(token) : getGraphQLClient(fetchFn);
const data = await client.request<{ adminGetVideo: Video | null }>(ADMIN_GET_VIDEO_QUERY, {
id,
});
return data.adminGetVideo;
});
}
const CREATE_VIDEO_MUTATION = gql`
mutation CreateVideo(
$title: String!
$slug: String!
$description: String
$imageId: String
$movieId: String
$tags: [String!]
$premium: Boolean
$featured: Boolean
$uploadDate: String
) {
createVideo(
title: $title
slug: $slug
description: $description
imageId: $imageId
movieId: $movieId
tags: $tags
premium: $premium
featured: $featured
uploadDate: $uploadDate
) {
id
slug
title
}
}
`;
export async function createVideo(input: {
title: string;
slug: string;
description?: string;
imageId?: string;
movieId?: string;
tags?: string[];
premium?: boolean;
featured?: boolean;
uploadDate?: string;
}) {
return loggedApiCall(
"createVideo",
async () => {
const data = await getGraphQLClient().request<{ createVideo: Video }>(
CREATE_VIDEO_MUTATION,
input,
);
return data.createVideo;
},
{ title: input.title },
);
}
const UPDATE_VIDEO_MUTATION = gql`
mutation UpdateVideo(
$id: String!
$title: String
$slug: String
$description: String
$imageId: String
$movieId: String
$tags: [String!]
$premium: Boolean
$featured: Boolean
$uploadDate: String
) {
updateVideo(
id: $id
title: $title
slug: $slug
description: $description
imageId: $imageId
movieId: $movieId
tags: $tags
premium: $premium
featured: $featured
uploadDate: $uploadDate
) {
id
slug
title
}
}
`;
export async function updateVideo(input: {
id: string;
title?: string;
slug?: string;
description?: string;
imageId?: string;
movieId?: string;
tags?: string[];
premium?: boolean;
featured?: boolean;
uploadDate?: string;
}) {
return loggedApiCall(
"updateVideo",
async () => {
const data = await getGraphQLClient().request<{ updateVideo: Video | null }>(
UPDATE_VIDEO_MUTATION,
input,
);
return data.updateVideo;
},
{ id: input.id },
);
}
const DELETE_VIDEO_MUTATION = gql`
mutation DeleteVideo($id: String!) {
deleteVideo(id: $id)
}
`;
export async function deleteVideo(id: string) {
return loggedApiCall(
"deleteVideo",
async () => {
await getGraphQLClient().request(DELETE_VIDEO_MUTATION, { id });
},
{ id },
);
}
const SET_VIDEO_MODELS_MUTATION = gql`
mutation SetVideoModels($videoId: String!, $userIds: [String!]!) {
setVideoModels(videoId: $videoId, userIds: $userIds)
}
`;
export async function setVideoModels(videoId: string, userIds: string[]) {
return loggedApiCall(
"setVideoModels",
async () => {
await getGraphQLClient().request(SET_VIDEO_MODELS_MUTATION, { videoId, userIds });
},
{ videoId, count: userIds.length },
);
}
// ─── Admin: Articles ──────────────────────────────────────────────────────────
const ADMIN_LIST_ARTICLES_QUERY = gql`
query AdminListArticles(
$search: String
$category: String
$featured: Boolean
$limit: Int
$offset: Int
) {
adminListArticles(
search: $search
category: $category
featured: $featured
limit: $limit
offset: $offset
) {
items {
id
slug
title
excerpt
image
tags
publish_date
category
featured
content
author {
id
artist_name
slug
avatar
}
}
total
}
}
`;
export async function adminListArticles(
opts: {
search?: string;
category?: string;
featured?: boolean;
limit?: number;
offset?: number;
} = {},
fetchFn?: typeof globalThis.fetch,
token?: string,
): Promise<{ items: Article[]; total: number }> {
return loggedApiCall("adminListArticles", async () => {
const client = token ? getAuthClient(token) : getGraphQLClient(fetchFn);
const data = await client.request<{ adminListArticles: { items: Article[]; total: number } }>(
ADMIN_LIST_ARTICLES_QUERY,
opts,
);
return data.adminListArticles;
});
}
const ADMIN_GET_ARTICLE_QUERY = gql`
query AdminGetArticle($id: String!) {
adminGetArticle(id: $id) {
id
slug
title
excerpt
content
image
tags
publish_date
category
featured
author {
id
artist_name
slug
avatar
}
}
}
`;
export async function adminGetArticle(
id: string,
fetchFn?: typeof globalThis.fetch,
token?: string,
): Promise<Article | null> {
return loggedApiCall("adminGetArticle", async () => {
const client = token ? getAuthClient(token) : getGraphQLClient(fetchFn);
const data = await client.request<{ adminGetArticle: Article | null }>(
ADMIN_GET_ARTICLE_QUERY,
{ id },
);
return data.adminGetArticle;
});
}
const CREATE_ARTICLE_MUTATION = gql`
mutation CreateArticle(
$title: String!
$slug: String!
$excerpt: String
$content: String
$imageId: String
$tags: [String!]
$category: String
$featured: Boolean
$publishDate: String
) {
createArticle(
title: $title
slug: $slug
excerpt: $excerpt
content: $content
imageId: $imageId
tags: $tags
category: $category
featured: $featured
publishDate: $publishDate
) {
id
slug
title
}
}
`;
export async function createArticle(input: {
title: string;
slug: string;
excerpt?: string;
content?: string;
imageId?: string;
tags?: string[];
category?: string;
featured?: boolean;
publishDate?: string;
}) {
return loggedApiCall(
"createArticle",
async () => {
const data = await getGraphQLClient().request<{ createArticle: Article }>(
CREATE_ARTICLE_MUTATION,
input,
);
return data.createArticle;
},
{ title: input.title },
);
}
const UPDATE_ARTICLE_MUTATION = gql`
mutation UpdateArticle(
$id: String!
$title: String
$slug: String
$excerpt: String
$content: String
$imageId: String
$authorId: String
$tags: [String!]
$category: String
$featured: Boolean
$publishDate: String
) {
updateArticle(
id: $id
title: $title
slug: $slug
excerpt: $excerpt
content: $content
imageId: $imageId
authorId: $authorId
tags: $tags
category: $category
featured: $featured
publishDate: $publishDate
) {
id
slug
title
}
}
`;
export async function updateArticle(input: {
id: string;
title?: string;
slug?: string;
excerpt?: string;
content?: string;
imageId?: string;
authorId?: string | null;
tags?: string[];
category?: string;
featured?: boolean;
publishDate?: string;
}) {
return loggedApiCall(
"updateArticle",
async () => {
const data = await getGraphQLClient().request<{ updateArticle: Article | null }>(
UPDATE_ARTICLE_MUTATION,
input,
);
return data.updateArticle;
},
{ id: input.id },
);
}
const DELETE_ARTICLE_MUTATION = gql`
mutation DeleteArticle($id: String!) {
deleteArticle(id: $id)
}
`;
export async function deleteArticle(id: string) {
return loggedApiCall(
"deleteArticle",
async () => {
await getGraphQLClient().request(DELETE_ARTICLE_MUTATION, { id });
},
{ id },
);
}
// ─── Analytics ───────────────────────────────────────────────────────────────
const ANALYTICS_QUERY = gql`
query GetAnalytics {
analytics
}
`;
export async function getAnalytics(fetchFn?: typeof globalThis.fetch) {
return loggedApiCall(
"getAnalytics",
async () => {
const data = await getGraphQLClient(fetchFn).request<{ analytics: Analytics | null }>(
ANALYTICS_QUERY,
);
return data.analytics;
},
{},
);
}