Files
sexy/packages/backend/src/graphql/resolvers/recordings.ts

334 lines
10 KiB
TypeScript
Raw Normal View History

import { GraphQLError } from "graphql";
import { builder } from "../builder";
import { RecordingType } from "../types/index";
import { recordings, recording_plays } from "../../db/schema/index";
import { eq, and, desc } from "drizzle-orm";
import { slugify } from "../../lib/slugify";
import { awardPoints, checkAchievements } from "../../lib/gamification";
builder.queryField("recordings", (t) =>
t.field({
type: [RecordingType],
args: {
status: t.arg.string(),
tags: t.arg.string(),
linkedVideoId: t.arg.string(),
limit: t.arg.int(),
page: t.arg.int(),
},
resolve: async (_root, args, ctx) => {
if (!ctx.currentUser) throw new GraphQLError("Unauthorized");
const conditions = [eq(recordings.user_id, ctx.currentUser.id)];
if (args.status) conditions.push(eq(recordings.status, args.status as any));
if (args.linkedVideoId) conditions.push(eq(recordings.linked_video, args.linkedVideoId));
const limit = args.limit || 50;
const page = args.page || 1;
const offset = (page - 1) * limit;
return ctx.db
.select()
.from(recordings)
.where(and(...conditions))
.orderBy(desc(recordings.date_created))
.limit(limit)
.offset(offset);
},
}),
);
builder.queryField("recording", (t) =>
t.field({
type: RecordingType,
nullable: true,
args: {
id: t.arg.string({ required: true }),
},
resolve: async (_root, args, ctx) => {
if (!ctx.currentUser) throw new GraphQLError("Unauthorized");
const recording = await ctx.db
.select()
.from(recordings)
.where(eq(recordings.id, args.id))
.limit(1);
if (!recording[0]) return null;
if (recording[0].user_id !== ctx.currentUser.id && !recording[0].public) {
throw new GraphQLError("Forbidden");
}
return recording[0];
},
}),
);
builder.queryField("communityRecordings", (t) =>
t.field({
type: [RecordingType],
args: {
limit: t.arg.int(),
offset: t.arg.int(),
},
resolve: async (_root, args, ctx) => {
return ctx.db
.select()
.from(recordings)
.where(and(eq(recordings.status, "published"), eq(recordings.public, true)))
.orderBy(desc(recordings.date_created))
.limit(args.limit || 50)
.offset(args.offset || 0);
},
}),
);
builder.mutationField("createRecording", (t) =>
t.field({
type: RecordingType,
args: {
title: t.arg.string({ required: true }),
description: t.arg.string(),
duration: t.arg.int({ required: true }),
events: t.arg({ type: "JSON", required: true }),
deviceInfo: t.arg({ type: "JSON", required: true }),
tags: t.arg.stringList(),
status: t.arg.string(),
linkedVideoId: t.arg.string(),
},
resolve: async (_root, args, ctx) => {
if (!ctx.currentUser) throw new GraphQLError("Unauthorized");
const slug = slugify(args.title);
const newRecording = await ctx.db
.insert(recordings)
.values({
title: args.title,
description: args.description || null,
slug,
duration: args.duration,
events: (args.events as object[]) || [],
device_info: (args.deviceInfo as object[]) || [],
user_id: ctx.currentUser.id,
tags: args.tags || [],
linked_video: args.linkedVideoId || null,
status: (args.status as any) || "draft",
public: false,
})
.returning();
const recording = newRecording[0];
// Gamification: award points if published
if (recording.status === "published") {
await awardPoints(ctx.db, ctx.currentUser.id, "RECORDING_CREATE", recording.id);
await checkAchievements(ctx.db, ctx.currentUser.id, "recordings");
}
return recording;
},
}),
);
builder.mutationField("updateRecording", (t) =>
t.field({
type: RecordingType,
nullable: true,
args: {
id: t.arg.string({ required: true }),
title: t.arg.string(),
description: t.arg.string(),
tags: t.arg.stringList(),
status: t.arg.string(),
public: t.arg.boolean(),
linkedVideoId: t.arg.string(),
},
resolve: async (_root, args, ctx) => {
if (!ctx.currentUser) throw new GraphQLError("Unauthorized");
const existing = await ctx.db
.select()
.from(recordings)
.where(eq(recordings.id, args.id))
.limit(1);
if (!existing[0]) throw new GraphQLError("Recording not found");
if (existing[0].user_id !== ctx.currentUser.id) throw new GraphQLError("Forbidden");
const updates: Record<string, unknown> = { date_updated: new Date() };
if (args.title !== null && args.title !== undefined) {
updates.title = args.title;
updates.slug = slugify(args.title);
}
if (args.description !== null && args.description !== undefined) updates.description = args.description;
if (args.tags !== null && args.tags !== undefined) updates.tags = args.tags;
if (args.status !== null && args.status !== undefined) updates.status = args.status;
if (args.public !== null && args.public !== undefined) updates.public = args.public;
if (args.linkedVideoId !== null && args.linkedVideoId !== undefined) updates.linked_video = args.linkedVideoId;
const updated = await ctx.db
.update(recordings)
.set(updates as any)
.where(eq(recordings.id, args.id))
.returning();
const recording = updated[0];
// Gamification: if newly published
if (args.status === "published" && existing[0].status !== "published") {
await awardPoints(ctx.db, ctx.currentUser.id, "RECORDING_CREATE", recording.id);
await checkAchievements(ctx.db, ctx.currentUser.id, "recordings");
}
if (args.status === "published" && recording.featured && !existing[0].featured) {
await awardPoints(ctx.db, ctx.currentUser.id, "RECORDING_FEATURED", recording.id);
await checkAchievements(ctx.db, ctx.currentUser.id, "recordings");
}
return recording;
},
}),
);
builder.mutationField("deleteRecording", (t) =>
t.field({
type: "Boolean",
args: {
id: t.arg.string({ required: true }),
},
resolve: async (_root, args, ctx) => {
if (!ctx.currentUser) throw new GraphQLError("Unauthorized");
const existing = await ctx.db
.select()
.from(recordings)
.where(eq(recordings.id, args.id))
.limit(1);
if (!existing[0]) throw new GraphQLError("Recording not found");
if (existing[0].user_id !== ctx.currentUser.id) throw new GraphQLError("Forbidden");
await ctx.db
.update(recordings)
.set({ status: "archived", date_updated: new Date() })
.where(eq(recordings.id, args.id));
return true;
},
}),
);
builder.mutationField("duplicateRecording", (t) =>
t.field({
type: RecordingType,
args: {
id: t.arg.string({ required: true }),
},
resolve: async (_root, args, ctx) => {
if (!ctx.currentUser) throw new GraphQLError("Unauthorized");
const original = await ctx.db
.select()
.from(recordings)
.where(eq(recordings.id, args.id))
.limit(1);
if (!original[0]) throw new GraphQLError("Recording not found");
if (original[0].status !== "published" || !original[0].public) {
throw new GraphQLError("Recording is not publicly shared");
}
const slug = `${slugify(original[0].title)}-copy-${Date.now()}`;
const duplicated = await ctx.db
.insert(recordings)
.values({
title: `${original[0].title} (Copy)`,
description: original[0].description,
slug,
duration: original[0].duration,
events: original[0].events || [],
device_info: original[0].device_info || [],
user_id: ctx.currentUser.id,
tags: original[0].tags || [],
status: "draft",
public: false,
original_recording_id: original[0].id,
})
.returning();
return duplicated[0];
},
}),
);
builder.mutationField("recordRecordingPlay", (t) =>
t.field({
type: "JSON",
args: {
recordingId: t.arg.string({ required: true }),
},
resolve: async (_root, args, ctx) => {
const recording = await ctx.db
.select()
.from(recordings)
.where(eq(recordings.id, args.recordingId))
.limit(1);
if (!recording[0]) throw new GraphQLError("Recording not found");
const play = await ctx.db
.insert(recording_plays)
.values({
recording_id: args.recordingId,
user_id: ctx.currentUser?.id || null,
duration_played: 0,
completed: false,
})
.returning({ id: recording_plays.id });
// Gamification
if (ctx.currentUser && recording[0].user_id !== ctx.currentUser.id) {
await awardPoints(ctx.db, ctx.currentUser.id, "RECORDING_PLAY", args.recordingId);
await checkAchievements(ctx.db, ctx.currentUser.id, "playback");
}
return { success: true, play_id: play[0].id };
},
}),
);
builder.mutationField("updateRecordingPlay", (t) =>
t.field({
type: "Boolean",
args: {
playId: t.arg.string({ required: true }),
durationPlayed: t.arg.int({ required: true }),
completed: t.arg.boolean({ required: true }),
},
resolve: async (_root, args, ctx) => {
const existing = await ctx.db
.select()
.from(recording_plays)
.where(eq(recording_plays.id, args.playId))
.limit(1);
if (!existing[0]) throw new GraphQLError("Play record not found");
const wasCompleted = existing[0].completed;
await ctx.db
.update(recording_plays)
.set({ duration_played: args.durationPlayed, completed: args.completed, date_updated: new Date() })
.where(eq(recording_plays.id, args.playId));
if (args.completed && !wasCompleted && ctx.currentUser) {
await awardPoints(ctx.db, ctx.currentUser.id, "RECORDING_COMPLETE", existing[0].recording_id);
await checkAchievements(ctx.db, ctx.currentUser.id, "playback");
}
return true;
},
}),
);