rhythia-api 216.0.0 → 217.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/deleteBeatmapPage.ts +26 -19
- package/api/executeAdminOperation.ts +113 -77
- package/api/getBeatmapComments.ts +32 -18
- package/api/getBeatmapPage.ts +67 -44
- package/api/getBeatmapPageById.ts +63 -40
- package/api/getOnlinePlayers.ts +78 -0
- package/api/getUserScores.ts +59 -30
- package/api/postBeatmapComment.ts +23 -19
- package/api/submitScore.ts +47 -17
- package/index.ts +40 -18
- package/package.json +4 -2
- package/types/database.ts +149 -88
- package/utils/cache.ts +60 -0
package/api/deleteBeatmapPage.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import z from "zod";
|
|
3
|
-
import { protectedApi, validUser } from "../utils/requestUtils";
|
|
4
|
-
import { supabase } from "../utils/supabase";
|
|
5
|
-
import { getUserBySession } from "../utils/getUserBySession";
|
|
6
|
-
import { User } from "@supabase/supabase-js";
|
|
3
|
+
import { protectedApi, validUser } from "../utils/requestUtils";
|
|
4
|
+
import { supabase } from "../utils/supabase";
|
|
5
|
+
import { getUserBySession } from "../utils/getUserBySession";
|
|
6
|
+
import { User } from "@supabase/supabase-js";
|
|
7
|
+
import { invalidateCache, invalidateCachePrefix } from "../utils/cache";
|
|
7
8
|
|
|
8
9
|
export const Schema = {
|
|
9
10
|
input: z.strictObject({
|
|
@@ -54,24 +55,30 @@ export async function handler({
|
|
|
54
55
|
if (!userData) return NextResponse.json({ error: "No user." });
|
|
55
56
|
if (!beatmapData) return NextResponse.json({ error: "No beatmap." });
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
(userData.badges as string[]).includes("Developer") ||
|
|
60
|
-
(userData.badges as string[]).includes("Global Moderator");
|
|
58
|
+
const badges = (userData.badges || []) as string[];
|
|
59
|
+
const hasDeletionRole = badges.includes("RCT") || badges.includes("MMT");
|
|
61
60
|
|
|
62
|
-
|
|
61
|
+
if (!hasDeletionRole) {
|
|
62
|
+
return NextResponse.json({
|
|
63
|
+
error: "Only RCT or MMT members can delete beatmaps.",
|
|
64
|
+
});
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
if (pageData.status !== "UNRANKED")
|
|
66
68
|
return NextResponse.json({ error: "Only unranked maps can be updated" });
|
|
67
69
|
|
|
68
|
-
await supabase.from("beatmapPageComments").delete().eq("beatmapPage", id);
|
|
69
|
-
await supabase.from("collectionRelations").delete().eq("beatmapPage", id);
|
|
70
|
-
await supabase.from("beatmapPages").delete().eq("id", id);
|
|
71
|
-
await supabase
|
|
72
|
-
.from("beatmaps")
|
|
73
|
-
.delete()
|
|
74
|
-
.eq("beatmapHash", beatmapData.beatmapHash);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
70
|
+
await supabase.from("beatmapPageComments").delete().eq("beatmapPage", id);
|
|
71
|
+
await supabase.from("collectionRelations").delete().eq("beatmapPage", id);
|
|
72
|
+
await supabase.from("beatmapPages").delete().eq("id", id);
|
|
73
|
+
await supabase
|
|
74
|
+
.from("beatmaps")
|
|
75
|
+
.delete()
|
|
76
|
+
.eq("beatmapHash", beatmapData.beatmapHash);
|
|
77
|
+
|
|
78
|
+
await invalidateCache(`beatmap-comments:${id}`);
|
|
79
|
+
if (pageData.latestBeatmapHash) {
|
|
80
|
+
await invalidateCachePrefix(`beatmap-scores:${pageData.latestBeatmapHash}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return NextResponse.json({});
|
|
84
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
|
-
import z from "zod";
|
|
3
|
-
import { Database } from "../types/database";
|
|
4
|
-
import { protectedApi, validUser } from "../utils/requestUtils";
|
|
5
|
-
import { supabase } from "../utils/supabase";
|
|
6
|
-
import { getUserBySession } from "../utils/getUserBySession";
|
|
7
|
-
import { User } from "@supabase/supabase-js";
|
|
2
|
+
import z from "zod";
|
|
3
|
+
import { Database } from "../types/database";
|
|
4
|
+
import { protectedApi, validUser } from "../utils/requestUtils";
|
|
5
|
+
import { supabase } from "../utils/supabase";
|
|
6
|
+
import { getUserBySession } from "../utils/getUserBySession";
|
|
7
|
+
import { User } from "@supabase/supabase-js";
|
|
8
|
+
import { invalidateCache, invalidateCachePrefix } from "../utils/cache";
|
|
8
9
|
|
|
9
10
|
// Define supported admin operations and their parameter types
|
|
10
11
|
const adminOperations = {
|
|
@@ -145,17 +146,21 @@ export async function handler(
|
|
|
145
146
|
{ status: 403 }
|
|
146
147
|
);
|
|
147
148
|
}
|
|
148
|
-
|
|
149
|
-
// Execute the requested admin operation
|
|
150
|
-
try {
|
|
151
|
-
let result;
|
|
152
|
-
const operation = data.data.operation;
|
|
153
|
-
const params = data.data.params as any;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
149
|
+
|
|
150
|
+
// Execute the requested admin operation
|
|
151
|
+
try {
|
|
152
|
+
let result: { data?: any; error?: any } | null = null;
|
|
153
|
+
const operation = data.data.operation;
|
|
154
|
+
const params = data.data.params as any;
|
|
155
|
+
const targetUserId =
|
|
156
|
+
"userId" in params && typeof params.userId === "number"
|
|
157
|
+
? params.userId
|
|
158
|
+
: null;
|
|
159
|
+
|
|
160
|
+
switch (operation) {
|
|
161
|
+
case "deleteUser":
|
|
162
|
+
result = await supabase.rpc("admin_delete_user", {
|
|
163
|
+
user_id: params.userId,
|
|
159
164
|
});
|
|
160
165
|
break;
|
|
161
166
|
|
|
@@ -216,21 +221,23 @@ export async function handler(
|
|
|
216
221
|
})
|
|
217
222
|
.select();
|
|
218
223
|
break;
|
|
219
|
-
case "changeBadges":
|
|
220
|
-
// Allow only developers to modify badges.
|
|
221
|
-
if ((queryUserData.badges as string[]).includes("Developer")) {
|
|
222
|
-
result = await supabase
|
|
223
|
-
.from("profiles")
|
|
224
|
-
.upsert({
|
|
225
|
-
id: params.userId,
|
|
226
|
-
badges: JSON.parse(params.badges),
|
|
227
|
-
})
|
|
228
|
-
.select();
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
224
|
+
case "changeBadges":
|
|
225
|
+
// Allow only developers to modify badges.
|
|
226
|
+
if ((queryUserData.badges as string[]).includes("Developer")) {
|
|
227
|
+
result = await supabase
|
|
228
|
+
.from("profiles")
|
|
229
|
+
.upsert({
|
|
230
|
+
id: params.userId,
|
|
231
|
+
badges: JSON.parse(params.badges),
|
|
232
|
+
})
|
|
233
|
+
.select();
|
|
234
|
+
} else {
|
|
235
|
+
result = { data: null, error: { message: "Unauthorized" } };
|
|
236
|
+
}
|
|
237
|
+
break;
|
|
238
|
+
|
|
239
|
+
case "addBadge":
|
|
240
|
+
// Allow only developers to modify badges.
|
|
234
241
|
if ((queryUserData.badges as string[]).includes("Developer")) {
|
|
235
242
|
// Get current badges
|
|
236
243
|
const { data: targetUser } = await supabase
|
|
@@ -242,21 +249,23 @@ export async function handler(
|
|
|
242
249
|
const currentBadges = (targetUser?.badges || []) as string[];
|
|
243
250
|
if (!currentBadges.includes(params.badge)) {
|
|
244
251
|
currentBadges.push(params.badge);
|
|
245
|
-
result = await supabase
|
|
246
|
-
.from("profiles")
|
|
247
|
-
.upsert({
|
|
248
|
-
id: params.userId,
|
|
249
|
-
badges: currentBadges,
|
|
250
|
-
})
|
|
251
|
-
.select();
|
|
252
|
-
} else {
|
|
253
|
-
result = { data: targetUser, error: null };
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
252
|
+
result = await supabase
|
|
253
|
+
.from("profiles")
|
|
254
|
+
.upsert({
|
|
255
|
+
id: params.userId,
|
|
256
|
+
badges: currentBadges,
|
|
257
|
+
})
|
|
258
|
+
.select();
|
|
259
|
+
} else {
|
|
260
|
+
result = { data: targetUser, error: null };
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
result = { data: null, error: { message: "Unauthorized" } };
|
|
264
|
+
}
|
|
265
|
+
break;
|
|
266
|
+
|
|
267
|
+
case "removeBadge":
|
|
268
|
+
// Allow only developers to modify badges.
|
|
260
269
|
if ((queryUserData.badges as string[]).includes("Developer")) {
|
|
261
270
|
// Get current badges
|
|
262
271
|
const { data: targetUser } = await supabase
|
|
@@ -268,15 +277,17 @@ export async function handler(
|
|
|
268
277
|
const currentBadges = (targetUser?.badges || []) as string[];
|
|
269
278
|
const updatedBadges = currentBadges.filter(b => b !== params.badge);
|
|
270
279
|
|
|
271
|
-
result = await supabase
|
|
272
|
-
.from("profiles")
|
|
273
|
-
.upsert({
|
|
274
|
-
id: params.userId,
|
|
275
|
-
badges: updatedBadges,
|
|
276
|
-
})
|
|
277
|
-
.select();
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
+
result = await supabase
|
|
281
|
+
.from("profiles")
|
|
282
|
+
.upsert({
|
|
283
|
+
id: params.userId,
|
|
284
|
+
badges: updatedBadges,
|
|
285
|
+
})
|
|
286
|
+
.select();
|
|
287
|
+
} else {
|
|
288
|
+
result = { data: null, error: { message: "Unauthorized" } };
|
|
289
|
+
}
|
|
290
|
+
break;
|
|
280
291
|
|
|
281
292
|
case "getScoresPaginated":
|
|
282
293
|
const offset = (params.page - 1) * params.limit;
|
|
@@ -338,27 +349,52 @@ export async function handler(
|
|
|
338
349
|
}
|
|
339
350
|
|
|
340
351
|
// Log the admin action
|
|
341
|
-
await supabase.rpc("admin_log_action", {
|
|
342
|
-
admin_id: queryUserData.id,
|
|
343
|
-
action_type: operation,
|
|
344
|
-
target_id: "userId" in params ? params.userId : null,
|
|
345
|
-
details: { params },
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
if (result
|
|
349
|
-
return NextResponse.json(
|
|
350
|
-
{
|
|
351
|
-
success: false,
|
|
352
|
-
error: result.error.message,
|
|
353
|
-
},
|
|
354
|
-
{ status: 500 }
|
|
355
|
-
);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
352
|
+
await supabase.rpc("admin_log_action", {
|
|
353
|
+
admin_id: queryUserData.id,
|
|
354
|
+
action_type: operation,
|
|
355
|
+
target_id: "userId" in params ? params.userId : null,
|
|
356
|
+
details: { params },
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
if (result?.error) {
|
|
360
|
+
return NextResponse.json(
|
|
361
|
+
{
|
|
362
|
+
success: false,
|
|
363
|
+
error: result.error.message,
|
|
364
|
+
},
|
|
365
|
+
{ status: 500 }
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (targetUserId !== null && !result?.error) {
|
|
370
|
+
await invalidateCachePrefix(`userscore:${targetUserId}`);
|
|
371
|
+
|
|
372
|
+
if (
|
|
373
|
+
operation === "removeAllScores" ||
|
|
374
|
+
operation === "invalidateRankedScores" ||
|
|
375
|
+
operation === "deleteUser"
|
|
376
|
+
) {
|
|
377
|
+
const { data: beatmapHashes } = await supabase
|
|
378
|
+
.from("scores")
|
|
379
|
+
.select("beatmapHash")
|
|
380
|
+
.eq("userId", targetUserId);
|
|
381
|
+
|
|
382
|
+
const uniqueHashes = new Set(
|
|
383
|
+
(beatmapHashes || [])
|
|
384
|
+
.map((row) => row.beatmapHash)
|
|
385
|
+
.filter((hash): hash is string => Boolean(hash))
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
for (const hash of uniqueHashes) {
|
|
389
|
+
await invalidateCachePrefix(`beatmap-scores:${hash}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return NextResponse.json({
|
|
395
|
+
success: true,
|
|
396
|
+
result: result?.data,
|
|
397
|
+
});
|
|
362
398
|
} catch (err: any) {
|
|
363
399
|
return NextResponse.json(
|
|
364
400
|
{
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { NextResponse } from "next/server";
|
|
2
|
-
import z from "zod";
|
|
3
|
-
import { protectedApi, validUser } from "../utils/requestUtils";
|
|
4
|
-
import {
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import z from "zod";
|
|
3
|
+
import { protectedApi, validUser } from "../utils/requestUtils";
|
|
4
|
+
import { getCacheValue, setCacheValue } from "../utils/cache";
|
|
5
|
+
import { supabase } from "../utils/supabase";
|
|
5
6
|
|
|
6
7
|
export const Schema = {
|
|
7
8
|
input: z.strictObject({
|
|
@@ -34,16 +35,25 @@ export async function POST(request: Request): Promise<NextResponse> {
|
|
|
34
35
|
});
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
export async function handler({
|
|
38
|
-
page,
|
|
39
|
-
}: (typeof Schema)["input"]["_type"]): Promise<
|
|
40
|
-
NextResponse<(typeof Schema)["output"]["_type"]>
|
|
41
|
-
> {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
38
|
+
export async function handler({
|
|
39
|
+
page,
|
|
40
|
+
}: (typeof Schema)["input"]["_type"]): Promise<
|
|
41
|
+
NextResponse<(typeof Schema)["output"]["_type"]>
|
|
42
|
+
> {
|
|
43
|
+
const cacheKey = `beatmap-comments:${page}`;
|
|
44
|
+
const cachedComments = await getCacheValue<
|
|
45
|
+
(typeof Schema)["output"]["_type"]["comments"]
|
|
46
|
+
>(cacheKey);
|
|
47
|
+
|
|
48
|
+
if (cachedComments) {
|
|
49
|
+
return NextResponse.json({ comments: cachedComments });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let { data: userData, error: userError } = await supabase
|
|
53
|
+
.from("beatmapPageComments")
|
|
54
|
+
.select(
|
|
55
|
+
`
|
|
56
|
+
*,
|
|
47
57
|
profiles!inner(
|
|
48
58
|
username,
|
|
49
59
|
avatar_url,
|
|
@@ -51,7 +61,11 @@ export async function handler({
|
|
|
51
61
|
)
|
|
52
62
|
`
|
|
53
63
|
)
|
|
54
|
-
.eq("beatmapPage", page);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
64
|
+
.eq("beatmapPage", page);
|
|
65
|
+
|
|
66
|
+
if (userData) {
|
|
67
|
+
await setCacheValue(cacheKey, userData);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return NextResponse.json({ comments: userData! });
|
|
71
|
+
}
|
package/api/getBeatmapPage.ts
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
import { NextResponse } from "next/server";
|
|
2
|
-
import z from "zod";
|
|
3
|
-
import { protectedApi } from "../utils/requestUtils";
|
|
4
|
-
import {
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import z from "zod";
|
|
3
|
+
import { protectedApi } from "../utils/requestUtils";
|
|
4
|
+
import { getCacheValue, setCacheValue } from "../utils/cache";
|
|
5
|
+
import { supabase } from "../utils/supabase";
|
|
5
6
|
|
|
6
|
-
export const Schema = {
|
|
7
|
-
input: z.strictObject({
|
|
8
|
-
session: z.string(),
|
|
9
|
-
id: z.number(),
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
export const Schema = {
|
|
8
|
+
input: z.strictObject({
|
|
9
|
+
session: z.string(),
|
|
10
|
+
id: z.number(),
|
|
11
|
+
limit: z.number().min(1).max(200).default(50),
|
|
12
|
+
}),
|
|
13
|
+
output: z.object({
|
|
14
|
+
error: z.string().optional(),
|
|
15
|
+
scores: z
|
|
16
|
+
.array(
|
|
15
17
|
z.object({
|
|
16
18
|
id: z.number(),
|
|
17
19
|
awarded_sp: z.number().nullable(),
|
|
@@ -66,15 +68,17 @@ export async function POST(request: Request): Promise<NextResponse> {
|
|
|
66
68
|
});
|
|
67
69
|
}
|
|
68
70
|
|
|
69
|
-
export async function handler(
|
|
70
|
-
data: (typeof Schema)["input"]["_type"],
|
|
71
|
-
req: Request
|
|
72
|
-
): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
71
|
+
export async function handler(
|
|
72
|
+
data: (typeof Schema)["input"]["_type"],
|
|
73
|
+
req: Request
|
|
74
|
+
): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
|
|
75
|
+
const limit = data.limit ?? 50;
|
|
76
|
+
|
|
77
|
+
let { data: beatmapPage, error: errorlast } = await supabase
|
|
78
|
+
.from("beatmapPages")
|
|
79
|
+
.select(
|
|
80
|
+
`
|
|
81
|
+
*,
|
|
78
82
|
beatmaps (
|
|
79
83
|
created_at,
|
|
80
84
|
playcount,
|
|
@@ -93,28 +97,47 @@ export async function handler(
|
|
|
93
97
|
avatar_url
|
|
94
98
|
)
|
|
95
99
|
`
|
|
96
|
-
)
|
|
97
|
-
.eq("id", data.id)
|
|
98
|
-
.single();
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
100
|
+
)
|
|
101
|
+
.eq("id", data.id)
|
|
102
|
+
.single();
|
|
103
|
+
|
|
104
|
+
if (!beatmapPage) return NextResponse.json({});
|
|
105
|
+
|
|
106
|
+
const beatmapHash = beatmapPage?.latestBeatmapHash || "";
|
|
107
|
+
const isCacheable =
|
|
108
|
+
beatmapPage?.status === "RANKED" || beatmapPage?.status === "APPROVED";
|
|
109
|
+
const cacheKey = `beatmap-scores:${beatmapHash}:limit=${limit}`;
|
|
110
|
+
|
|
111
|
+
let scoreData: any[] | null = null;
|
|
112
|
+
|
|
113
|
+
if (isCacheable && beatmapHash) {
|
|
114
|
+
scoreData = await getCacheValue<any[]>(cacheKey);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!scoreData) {
|
|
118
|
+
const { data: rpcScores, error } = await supabase.rpc(
|
|
119
|
+
"get_top_scores_for_beatmap",
|
|
120
|
+
{ beatmap_hash: beatmapHash }
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
if (error) {
|
|
124
|
+
return NextResponse.json({ error: JSON.stringify(error) });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
scoreData = (rpcScores || []).slice(0, limit);
|
|
128
|
+
|
|
129
|
+
if (isCacheable && beatmapHash) {
|
|
130
|
+
await setCacheValue(cacheKey, scoreData);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return NextResponse.json({
|
|
135
|
+
scores: (scoreData || []).map((score: any) => ({
|
|
136
|
+
id: score.id,
|
|
137
|
+
awarded_sp: score.awarded_sp,
|
|
138
|
+
created_at: score.created_at,
|
|
139
|
+
misses: score.misses,
|
|
140
|
+
mods: score.mods,
|
|
118
141
|
passed: score.passed,
|
|
119
142
|
songId: score.songid,
|
|
120
143
|
speed: score.speed,
|
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
import { NextResponse } from "next/server";
|
|
2
|
-
import z from "zod";
|
|
3
|
-
import { protectedApi } from "../utils/requestUtils";
|
|
4
|
-
import {
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import z from "zod";
|
|
3
|
+
import { protectedApi } from "../utils/requestUtils";
|
|
4
|
+
import { getCacheValue, setCacheValue } from "../utils/cache";
|
|
5
|
+
import { supabase } from "../utils/supabase";
|
|
5
6
|
|
|
6
|
-
export const Schema = {
|
|
7
|
-
input: z.strictObject({
|
|
8
|
-
session: z.string(),
|
|
9
|
-
mapId: z.string(),
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
export const Schema = {
|
|
8
|
+
input: z.strictObject({
|
|
9
|
+
session: z.string(),
|
|
10
|
+
mapId: z.string(),
|
|
11
|
+
limit: z.number().min(1).max(200).default(50),
|
|
12
|
+
}),
|
|
13
|
+
output: z.object({
|
|
14
|
+
error: z.string().optional(),
|
|
15
|
+
scores: z
|
|
16
|
+
.array(
|
|
15
17
|
z.object({
|
|
16
18
|
id: z.number(),
|
|
17
19
|
awarded_sp: z.number().nullable(),
|
|
@@ -62,12 +64,14 @@ export async function POST(request: Request): Promise<NextResponse> {
|
|
|
62
64
|
});
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
export async function handler(
|
|
66
|
-
data: (typeof Schema)["input"]["_type"],
|
|
67
|
-
req: Request
|
|
68
|
-
): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
export async function handler(
|
|
68
|
+
data: (typeof Schema)["input"]["_type"],
|
|
69
|
+
req: Request
|
|
70
|
+
): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
|
|
71
|
+
const limit = data.limit ?? 50;
|
|
72
|
+
|
|
73
|
+
let { data: beatmapPage, error: errorlast } = await supabase
|
|
74
|
+
.from("beatmapPages")
|
|
71
75
|
.select(
|
|
72
76
|
`
|
|
73
77
|
*,
|
|
@@ -89,27 +93,46 @@ export async function handler(
|
|
|
89
93
|
)
|
|
90
94
|
`
|
|
91
95
|
)
|
|
92
|
-
.eq("latestBeatmapHash", data.mapId)
|
|
93
|
-
.single();
|
|
94
|
-
|
|
95
|
-
if (!beatmapPage) return NextResponse.json({});
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
96
|
+
.eq("latestBeatmapHash", data.mapId)
|
|
97
|
+
.single();
|
|
98
|
+
|
|
99
|
+
if (!beatmapPage) return NextResponse.json({});
|
|
100
|
+
|
|
101
|
+
const beatmapHash = beatmapPage?.latestBeatmapHash || "";
|
|
102
|
+
const isCacheable =
|
|
103
|
+
beatmapPage?.status === "RANKED" || beatmapPage?.status === "APPROVED";
|
|
104
|
+
const cacheKey = `beatmap-scores:${beatmapHash}:limit=${limit}`;
|
|
105
|
+
|
|
106
|
+
let scoreData: any[] | null = null;
|
|
107
|
+
|
|
108
|
+
if (isCacheable && beatmapHash) {
|
|
109
|
+
scoreData = await getCacheValue<any[]>(cacheKey);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!scoreData) {
|
|
113
|
+
const { data: rpcScores, error } = await supabase.rpc(
|
|
114
|
+
"get_top_scores_for_beatmap",
|
|
115
|
+
{ beatmap_hash: beatmapHash }
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
if (error) {
|
|
119
|
+
return NextResponse.json({ error: JSON.stringify(error) });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
scoreData = (rpcScores || []).slice(0, limit);
|
|
123
|
+
|
|
124
|
+
if (isCacheable && beatmapHash) {
|
|
125
|
+
await setCacheValue(cacheKey, scoreData);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return NextResponse.json({
|
|
130
|
+
scores: (scoreData || []).map((score: any) => ({
|
|
131
|
+
id: score.id,
|
|
132
|
+
awarded_sp: score.awarded_sp,
|
|
133
|
+
created_at: score.created_at,
|
|
134
|
+
misses: score.misses,
|
|
135
|
+
mods: score.mods,
|
|
113
136
|
passed: score.passed,
|
|
114
137
|
songId: score.songid,
|
|
115
138
|
speed: score.speed,
|