rhythia-api 230.0.0 → 231.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/checkQualified.ts +83 -0
- package/api/getBeatmapPage.ts +65 -28
- package/api/getBeatmapPageById.ts +61 -24
- package/api/{nominateMap.ts → qualifyMap.ts} +86 -82
- package/api/submitScoreInternal.ts +449 -426
- package/api/{approveMap.ts → vetoMap.ts} +94 -78
- package/index.ts +116 -70
- package/package.json +4 -3
- package/queries/get_user_scores_lastday.sql +17 -6
- package/queries/get_user_scores_top_and_stats.sql +59 -59
- package/types/database.ts +1224 -1179
- package/utils/getUserBySession.ts +1 -1
- package/utils/requestUtils.ts +127 -88
|
@@ -1,78 +1,94 @@
|
|
|
1
|
-
import { NextResponse } from "next/server";
|
|
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";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}),
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (mapData
|
|
56
|
-
return NextResponse.json({ error: "
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (
|
|
60
|
-
return NextResponse.json({
|
|
61
|
-
error: "
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
id
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
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";
|
|
7
|
+
|
|
8
|
+
const VETO_BADGES = ["Team Ranked"];
|
|
9
|
+
|
|
10
|
+
export const Schema = {
|
|
11
|
+
input: z.strictObject({
|
|
12
|
+
session: z.string(),
|
|
13
|
+
mapId: z.number(),
|
|
14
|
+
reason: z.string().min(1).max(1000),
|
|
15
|
+
}),
|
|
16
|
+
output: z.object({
|
|
17
|
+
error: z.string().optional(),
|
|
18
|
+
}),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export async function POST(request: Request) {
|
|
22
|
+
return protectedApi({
|
|
23
|
+
request,
|
|
24
|
+
schema: Schema,
|
|
25
|
+
authorization: validUser,
|
|
26
|
+
activity: handler,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function handler(data: (typeof Schema)["input"]["_type"]) {
|
|
31
|
+
const user = (await getUserBySession(data.session)) as User;
|
|
32
|
+
const { data: queryUserData } = await supabase
|
|
33
|
+
.from("profiles")
|
|
34
|
+
.select("*")
|
|
35
|
+
.eq("uid", user.id)
|
|
36
|
+
.single();
|
|
37
|
+
|
|
38
|
+
if (!queryUserData) {
|
|
39
|
+
return NextResponse.json({ error: "Can't find user" });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const tags = (queryUserData?.badges || []) as string[];
|
|
43
|
+
const hasVetoAccess = VETO_BADGES.some((badge) => tags.includes(badge));
|
|
44
|
+
|
|
45
|
+
if (!hasVetoAccess) {
|
|
46
|
+
return NextResponse.json({ error: "Only management can veto maps!" });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const { data: mapData } = await supabase
|
|
50
|
+
.from("beatmapPages")
|
|
51
|
+
.select("id,qualified")
|
|
52
|
+
.eq("id", data.mapId)
|
|
53
|
+
.single();
|
|
54
|
+
|
|
55
|
+
if (!mapData) {
|
|
56
|
+
return NextResponse.json({ error: "Bad map" });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!mapData.qualified) {
|
|
60
|
+
return NextResponse.json({
|
|
61
|
+
error: "Only qualified maps can be vetoed",
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const { data: vetoData, error: vetoError } = await supabase
|
|
66
|
+
.from("vetos")
|
|
67
|
+
.insert({
|
|
68
|
+
beatmapPage: data.mapId,
|
|
69
|
+
user: queryUserData.id,
|
|
70
|
+
veto_reason: data.reason,
|
|
71
|
+
})
|
|
72
|
+
.select("id")
|
|
73
|
+
.single();
|
|
74
|
+
|
|
75
|
+
if (vetoError) {
|
|
76
|
+
return NextResponse.json({ error: vetoError.message });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const { error: updateError } = await supabase.from("beatmapPages").upsert({
|
|
80
|
+
id: data.mapId,
|
|
81
|
+
qualified: false,
|
|
82
|
+
qualifiedAt: null,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (updateError) {
|
|
86
|
+
if (vetoData?.id) {
|
|
87
|
+
await supabase.from("vetos").delete().eq("id", vetoData.id);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return NextResponse.json({ error: updateError.message });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return NextResponse.json({});
|
|
94
|
+
}
|
package/index.ts
CHANGED
|
@@ -33,22 +33,6 @@ import { Schema as AddCollectionMap } from "./api/addCollectionMap"
|
|
|
33
33
|
export { Schema as SchemaAddCollectionMap } from "./api/addCollectionMap"
|
|
34
34
|
export const addCollectionMap = handleApi({url:"/api/addCollectionMap",...AddCollectionMap})
|
|
35
35
|
|
|
36
|
-
// ./api/approveMap.ts API
|
|
37
|
-
|
|
38
|
-
/*
|
|
39
|
-
export const Schema = {
|
|
40
|
-
input: z.strictObject({
|
|
41
|
-
session: z.string(),
|
|
42
|
-
mapId: z.number(),
|
|
43
|
-
}),
|
|
44
|
-
output: z.object({
|
|
45
|
-
error: z.string().optional(),
|
|
46
|
-
}),
|
|
47
|
-
};*/
|
|
48
|
-
import { Schema as ApproveMap } from "./api/approveMap"
|
|
49
|
-
export { Schema as SchemaApproveMap } from "./api/approveMap"
|
|
50
|
-
export const approveMap = handleApi({url:"/api/approveMap",...ApproveMap})
|
|
51
|
-
|
|
52
36
|
// ./api/chartPublicStats.ts API
|
|
53
37
|
|
|
54
38
|
/*
|
|
@@ -60,6 +44,22 @@ import { Schema as ChartPublicStats } from "./api/chartPublicStats"
|
|
|
60
44
|
export { Schema as SchemaChartPublicStats } from "./api/chartPublicStats"
|
|
61
45
|
export const chartPublicStats = handleApi({url:"/api/chartPublicStats",...ChartPublicStats})
|
|
62
46
|
|
|
47
|
+
// ./api/checkQualified.ts API
|
|
48
|
+
|
|
49
|
+
/*
|
|
50
|
+
export const Schema = {
|
|
51
|
+
input: z.strictObject({
|
|
52
|
+
secret: z.string(),
|
|
53
|
+
}),
|
|
54
|
+
output: z.object({
|
|
55
|
+
error: z.string().optional(),
|
|
56
|
+
updated: z.number(),
|
|
57
|
+
}),
|
|
58
|
+
};*/
|
|
59
|
+
import { Schema as CheckQualified } from "./api/checkQualified"
|
|
60
|
+
export { Schema as SchemaCheckQualified } from "./api/checkQualified"
|
|
61
|
+
export const checkQualified = handleApi({url:"/api/checkQualified",...CheckQualified})
|
|
62
|
+
|
|
63
63
|
// ./api/createBeatmap.ts API
|
|
64
64
|
|
|
65
65
|
/*
|
|
@@ -469,16 +469,30 @@ export const Schema = {
|
|
|
469
469
|
image: z.string().nullable().optional(),
|
|
470
470
|
imageLarge: z.string().nullable().optional(),
|
|
471
471
|
starRating: z.number().nullable().optional(),
|
|
472
|
-
owner: z.number().nullable().optional(),
|
|
473
|
-
ownerUsername: z.string().nullable().optional(),
|
|
474
|
-
ownerAvatar: z.string().nullable().optional(),
|
|
475
|
-
status: z.string().nullable().optional(),
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
472
|
+
owner: z.number().nullable().optional(),
|
|
473
|
+
ownerUsername: z.string().nullable().optional(),
|
|
474
|
+
ownerAvatar: z.string().nullable().optional(),
|
|
475
|
+
status: z.string().nullable().optional(),
|
|
476
|
+
qualified: z.boolean().nullable().optional(),
|
|
477
|
+
qualifiedAt: z.string().nullable().optional(),
|
|
478
|
+
description: z.string().nullable().optional(),
|
|
479
|
+
tags: z.string().nullable().optional(),
|
|
480
|
+
videoUrl: z.string().nullable().optional(),
|
|
481
|
+
vetos: z
|
|
482
|
+
.array(
|
|
483
|
+
z.object({
|
|
484
|
+
id: z.number(),
|
|
485
|
+
userId: z.number().nullable().optional(),
|
|
486
|
+
username: z.string().nullable().optional(),
|
|
487
|
+
avatar_url: z.string().nullable().optional(),
|
|
488
|
+
veto_reason: z.string().nullable().optional(),
|
|
489
|
+
created_at: z.string().nullable().optional(),
|
|
490
|
+
})
|
|
491
|
+
)
|
|
492
|
+
.optional(),
|
|
493
|
+
})
|
|
494
|
+
.optional(),
|
|
495
|
+
}),
|
|
482
496
|
};*/
|
|
483
497
|
import { Schema as GetBeatmapPage } from "./api/getBeatmapPage"
|
|
484
498
|
export { Schema as SchemaGetBeatmapPage } from "./api/getBeatmapPage"
|
|
@@ -528,14 +542,28 @@ export const Schema = {
|
|
|
528
542
|
beatmapFile: z.string().nullable().optional(),
|
|
529
543
|
image: z.string().nullable().optional(),
|
|
530
544
|
starRating: z.number().nullable().optional(),
|
|
531
|
-
owner: z.number().nullable().optional(),
|
|
532
|
-
ownerUsername: z.string().nullable().optional(),
|
|
533
|
-
ownerAvatar: z.string().nullable().optional(),
|
|
534
|
-
status: z.string().nullable().optional(),
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
545
|
+
owner: z.number().nullable().optional(),
|
|
546
|
+
ownerUsername: z.string().nullable().optional(),
|
|
547
|
+
ownerAvatar: z.string().nullable().optional(),
|
|
548
|
+
status: z.string().nullable().optional(),
|
|
549
|
+
qualified: z.boolean().nullable().optional(),
|
|
550
|
+
qualifiedAt: z.string().nullable().optional(),
|
|
551
|
+
videoUrl: z.string().nullable(),
|
|
552
|
+
vetos: z
|
|
553
|
+
.array(
|
|
554
|
+
z.object({
|
|
555
|
+
id: z.number(),
|
|
556
|
+
userId: z.number().nullable().optional(),
|
|
557
|
+
username: z.string().nullable().optional(),
|
|
558
|
+
avatar_url: z.string().nullable().optional(),
|
|
559
|
+
veto_reason: z.string().nullable().optional(),
|
|
560
|
+
created_at: z.string().nullable().optional(),
|
|
561
|
+
})
|
|
562
|
+
)
|
|
563
|
+
.optional(),
|
|
564
|
+
})
|
|
565
|
+
.optional(),
|
|
566
|
+
}),
|
|
539
567
|
};*/
|
|
540
568
|
import { Schema as GetBeatmapPageById } from "./api/getBeatmapPageById"
|
|
541
569
|
export { Schema as SchemaGetBeatmapPageById } from "./api/getBeatmapPageById"
|
|
@@ -1216,22 +1244,6 @@ import { Schema as GetVideoUploadUrl } from "./api/getVideoUploadUrl"
|
|
|
1216
1244
|
export { Schema as SchemaGetVideoUploadUrl } from "./api/getVideoUploadUrl"
|
|
1217
1245
|
export const getVideoUploadUrl = handleApi({url:"/api/getVideoUploadUrl",...GetVideoUploadUrl})
|
|
1218
1246
|
|
|
1219
|
-
// ./api/nominateMap.ts API
|
|
1220
|
-
|
|
1221
|
-
/*
|
|
1222
|
-
export const Schema = {
|
|
1223
|
-
input: z.strictObject({
|
|
1224
|
-
session: z.string(),
|
|
1225
|
-
mapId: z.number(),
|
|
1226
|
-
}),
|
|
1227
|
-
output: z.object({
|
|
1228
|
-
error: z.string().optional(),
|
|
1229
|
-
}),
|
|
1230
|
-
};*/
|
|
1231
|
-
import { Schema as NominateMap } from "./api/nominateMap"
|
|
1232
|
-
export { Schema as SchemaNominateMap } from "./api/nominateMap"
|
|
1233
|
-
export const nominateMap = handleApi({url:"/api/nominateMap",...NominateMap})
|
|
1234
|
-
|
|
1235
1247
|
// ./api/postBeatmapComment.ts API
|
|
1236
1248
|
|
|
1237
1249
|
/*
|
|
@@ -1249,6 +1261,23 @@ import { Schema as PostBeatmapComment } from "./api/postBeatmapComment"
|
|
|
1249
1261
|
export { Schema as SchemaPostBeatmapComment } from "./api/postBeatmapComment"
|
|
1250
1262
|
export const postBeatmapComment = handleApi({url:"/api/postBeatmapComment",...PostBeatmapComment})
|
|
1251
1263
|
|
|
1264
|
+
// ./api/qualifyMap.ts API
|
|
1265
|
+
|
|
1266
|
+
/*
|
|
1267
|
+
export const Schema = {
|
|
1268
|
+
input: z.strictObject({
|
|
1269
|
+
session: z.string(),
|
|
1270
|
+
mapId: z.number(),
|
|
1271
|
+
}),
|
|
1272
|
+
output: z.object({
|
|
1273
|
+
error: z.string().optional(),
|
|
1274
|
+
qualifiedAt: z.string().optional(),
|
|
1275
|
+
}),
|
|
1276
|
+
};*/
|
|
1277
|
+
import { Schema as QualifyMap } from "./api/qualifyMap"
|
|
1278
|
+
export { Schema as SchemaQualifyMap } from "./api/qualifyMap"
|
|
1279
|
+
export const qualifyMap = handleApi({url:"/api/qualifyMap",...QualifyMap})
|
|
1280
|
+
|
|
1252
1281
|
// ./api/rankMapsArchive.ts API
|
|
1253
1282
|
|
|
1254
1283
|
/*
|
|
@@ -1338,26 +1367,26 @@ export const submitScore = handleApi({url:"/api/submitScore",...SubmitScore})
|
|
|
1338
1367
|
// ./api/submitScoreInternal.ts API
|
|
1339
1368
|
|
|
1340
1369
|
/*
|
|
1341
|
-
export const Schema = {
|
|
1342
|
-
input: z.strictObject({
|
|
1343
|
-
session: z.string(),
|
|
1344
|
-
secret: z.string(),
|
|
1345
|
-
token: z.string(),
|
|
1346
|
-
data: z.strictObject({
|
|
1347
|
-
onlineMapId: z.number(),
|
|
1348
|
-
misses: z.number(),
|
|
1349
|
-
hits: z.number(),
|
|
1350
|
-
speed: z.number(),
|
|
1351
|
-
mods: z.array(z.string()),
|
|
1352
|
-
spin: z.boolean(),
|
|
1353
|
-
replayBytes: z.string().nullable().optional(),
|
|
1354
|
-
pauses: z.number().nullable().optional(),
|
|
1355
|
-
failTime: z.number().nullable().optional(),
|
|
1356
|
-
}),
|
|
1357
|
-
}),
|
|
1358
|
-
output: z.object({
|
|
1359
|
-
error: z.string().optional(),
|
|
1360
|
-
}),
|
|
1370
|
+
export const Schema = {
|
|
1371
|
+
input: z.strictObject({
|
|
1372
|
+
session: z.string(),
|
|
1373
|
+
secret: z.string(),
|
|
1374
|
+
token: z.string(),
|
|
1375
|
+
data: z.strictObject({
|
|
1376
|
+
onlineMapId: z.number(),
|
|
1377
|
+
misses: z.number(),
|
|
1378
|
+
hits: z.number(),
|
|
1379
|
+
speed: z.number(),
|
|
1380
|
+
mods: z.array(z.string()),
|
|
1381
|
+
spin: z.boolean(),
|
|
1382
|
+
replayBytes: z.string().nullable().optional(),
|
|
1383
|
+
pauses: z.number().nullable().optional(),
|
|
1384
|
+
failTime: z.number().nullable().optional(),
|
|
1385
|
+
}),
|
|
1386
|
+
}),
|
|
1387
|
+
output: z.object({
|
|
1388
|
+
error: z.string().optional(),
|
|
1389
|
+
}),
|
|
1361
1390
|
};*/
|
|
1362
1391
|
import { Schema as SubmitScoreInternal } from "./api/submitScoreInternal"
|
|
1363
1392
|
export { Schema as SchemaSubmitScoreInternal } from "./api/submitScoreInternal"
|
|
@@ -1382,4 +1411,21 @@ export const Schema = {
|
|
|
1382
1411
|
import { Schema as UpdateBeatmapPage } from "./api/updateBeatmapPage"
|
|
1383
1412
|
export { Schema as SchemaUpdateBeatmapPage } from "./api/updateBeatmapPage"
|
|
1384
1413
|
export const updateBeatmapPage = handleApi({url:"/api/updateBeatmapPage",...UpdateBeatmapPage})
|
|
1414
|
+
|
|
1415
|
+
// ./api/vetoMap.ts API
|
|
1416
|
+
|
|
1417
|
+
/*
|
|
1418
|
+
export const Schema = {
|
|
1419
|
+
input: z.strictObject({
|
|
1420
|
+
session: z.string(),
|
|
1421
|
+
mapId: z.number(),
|
|
1422
|
+
reason: z.string().min(1).max(1000),
|
|
1423
|
+
}),
|
|
1424
|
+
output: z.object({
|
|
1425
|
+
error: z.string().optional(),
|
|
1426
|
+
}),
|
|
1427
|
+
};*/
|
|
1428
|
+
import { Schema as VetoMap } from "./api/vetoMap"
|
|
1429
|
+
export { Schema as SchemaVetoMap } from "./api/vetoMap"
|
|
1430
|
+
export const vetoMap = handleApi({url:"/api/vetoMap",...VetoMap})
|
|
1385
1431
|
export { handleApi } from "./handleApi"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rhythia-api",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "231.0.0",
|
|
4
4
|
"main": "index.ts",
|
|
5
5
|
"author": "online-contributors-cunev",
|
|
6
6
|
"scripts": {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"ci-deploy": "tsx ./scripts/ci-deploy.ts",
|
|
9
9
|
"test": "tsx ./scripts/test.ts",
|
|
10
10
|
"cache:clear-user-scores": "bun run scripts/clear-user-score-cache.ts",
|
|
11
|
+
"db:optimize-user-scores": "bun run scripts/optimize-user-scores-indexes.ts",
|
|
11
12
|
"query-pull": "bun run scripts/pull-queries.ts",
|
|
12
13
|
"query-push": "bun run scripts/deploy-queries.ts",
|
|
13
14
|
"queries:pull": "bun run scripts/pull-queries.ts",
|
|
@@ -52,7 +53,7 @@
|
|
|
52
53
|
"sharp": "^0.33.5",
|
|
53
54
|
"short-uuid": "^5.2.0",
|
|
54
55
|
"simple-git": "^3.25.0",
|
|
55
|
-
"supabase": "^2.76.
|
|
56
|
+
"supabase": "^2.76.16",
|
|
56
57
|
"tsx": "^4.17.0",
|
|
57
58
|
"utf-8-validate": "^6.0.4",
|
|
58
59
|
"uuid": "^11.1.0",
|
|
@@ -61,4 +62,4 @@
|
|
|
61
62
|
"zod": "^3.24.2"
|
|
62
63
|
},
|
|
63
64
|
"packageManager": "yarn@1.22.22"
|
|
64
|
-
}
|
|
65
|
+
}
|
|
@@ -3,12 +3,23 @@ CREATE OR REPLACE FUNCTION public.get_user_scores_lastday(userid integer, limit_
|
|
|
3
3
|
LANGUAGE sql
|
|
4
4
|
AS $function$
|
|
5
5
|
with s as (
|
|
6
|
-
select
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
select
|
|
7
|
+
sc.created_at,
|
|
8
|
+
sc.id,
|
|
9
|
+
sc.passed,
|
|
10
|
+
sc."userId",
|
|
11
|
+
sc.awarded_sp,
|
|
12
|
+
sc."beatmapHash",
|
|
13
|
+
sc.misses,
|
|
14
|
+
sc.replay_url,
|
|
15
|
+
sc."songId",
|
|
16
|
+
sc.speed,
|
|
17
|
+
sc.spin
|
|
18
|
+
from public.scores sc
|
|
19
|
+
where sc."userId" = userid
|
|
20
|
+
and sc.passed = true
|
|
21
|
+
order by sc.created_at desc
|
|
22
|
+
limit least(coalesce(limit_param, 100), 100)
|
|
12
23
|
)
|
|
13
24
|
select coalesce(
|
|
14
25
|
jsonb_agg(
|
|
@@ -1,24 +1,8 @@
|
|
|
1
1
|
CREATE OR REPLACE FUNCTION public.get_user_scores_top_and_stats(userid integer, limit_param integer)
|
|
2
2
|
RETURNS jsonb
|
|
3
|
-
LANGUAGE
|
|
3
|
+
LANGUAGE sql
|
|
4
4
|
AS $function$
|
|
5
|
-
|
|
6
|
-
top_json jsonb := '[]'::jsonb;
|
|
7
|
-
total_scores int := 0;
|
|
8
|
-
spin_scores int := 0;
|
|
9
|
-
begin
|
|
10
|
-
-- stats
|
|
11
|
-
select
|
|
12
|
-
count(*)::int,
|
|
13
|
-
count(*) filter (where spin = true)::int
|
|
14
|
-
into total_scores, spin_scores
|
|
15
|
-
from public.scores s
|
|
16
|
-
where s."userId" = userid
|
|
17
|
-
and s.passed = true
|
|
18
|
-
and s.awarded_sp is not null and s.awarded_sp <> 0;
|
|
19
|
-
|
|
20
|
-
-- top
|
|
21
|
-
with base as (
|
|
5
|
+
with filtered as materialized (
|
|
22
6
|
select
|
|
23
7
|
s.created_at,
|
|
24
8
|
s.id,
|
|
@@ -34,51 +18,67 @@ begin
|
|
|
34
18
|
from public.scores s
|
|
35
19
|
where s."userId" = userid
|
|
36
20
|
and s.passed = true
|
|
37
|
-
and s.awarded_sp is not null
|
|
21
|
+
and s.awarded_sp is not null
|
|
22
|
+
and s.awarded_sp <> 0
|
|
38
23
|
),
|
|
39
|
-
|
|
40
|
-
select
|
|
41
|
-
|
|
42
|
-
|
|
24
|
+
stats as (
|
|
25
|
+
select
|
|
26
|
+
count(*)::int as total_scores,
|
|
27
|
+
count(*) filter (where spin = true)::int as spin_scores
|
|
28
|
+
from filtered
|
|
43
29
|
),
|
|
44
30
|
top_rows as (
|
|
45
31
|
select *
|
|
46
|
-
from
|
|
32
|
+
from (
|
|
33
|
+
select distinct on (f."beatmapHash")
|
|
34
|
+
f.created_at,
|
|
35
|
+
f.id,
|
|
36
|
+
f.passed,
|
|
37
|
+
f."userId",
|
|
38
|
+
f.awarded_sp,
|
|
39
|
+
f."beatmapHash",
|
|
40
|
+
f.misses,
|
|
41
|
+
f.replay_url,
|
|
42
|
+
f."songId",
|
|
43
|
+
f.speed,
|
|
44
|
+
f.spin
|
|
45
|
+
from filtered f
|
|
46
|
+
order by f."beatmapHash", f.awarded_sp desc
|
|
47
|
+
) best_per_hash
|
|
47
48
|
order by awarded_sp desc
|
|
48
|
-
limit least(limit_param, 100)
|
|
49
|
+
limit least(coalesce(limit_param, 100), 100)
|
|
50
|
+
),
|
|
51
|
+
top_json as (
|
|
52
|
+
select coalesce(
|
|
53
|
+
jsonb_agg(
|
|
54
|
+
jsonb_build_object(
|
|
55
|
+
'created_at', t.created_at,
|
|
56
|
+
'id', t.id,
|
|
57
|
+
'passed', t.passed,
|
|
58
|
+
'userId', t."userId",
|
|
59
|
+
'awarded_sp', t.awarded_sp,
|
|
60
|
+
'beatmapHash', t."beatmapHash",
|
|
61
|
+
'misses', t.misses,
|
|
62
|
+
'replay_url', t.replay_url,
|
|
63
|
+
'rank', null, -- keep key for Zod compatibility
|
|
64
|
+
'songId', t."songId",
|
|
65
|
+
'beatmapDifficulty', b.difficulty,
|
|
66
|
+
'beatmapNotes', b."noteCount",
|
|
67
|
+
'beatmapTitle', b.title,
|
|
68
|
+
'speed', t.speed,
|
|
69
|
+
'spin', t.spin
|
|
70
|
+
)
|
|
71
|
+
),
|
|
72
|
+
'[]'::jsonb
|
|
73
|
+
) as value
|
|
74
|
+
from top_rows t
|
|
75
|
+
left join public.beatmaps b on b."beatmapHash" = t."beatmapHash"
|
|
49
76
|
)
|
|
50
|
-
select
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
'awarded_sp', t.awarded_sp,
|
|
58
|
-
'beatmapHash', t."beatmapHash",
|
|
59
|
-
'misses', t.misses,
|
|
60
|
-
'replay_url', t.replay_url,
|
|
61
|
-
'rank', null, -- keep key for Zod compatibility
|
|
62
|
-
'songId', t."songId",
|
|
63
|
-
'beatmapDifficulty', b.difficulty,
|
|
64
|
-
'beatmapNotes', b."noteCount",
|
|
65
|
-
'beatmapTitle', b.title,
|
|
66
|
-
'speed', t.speed,
|
|
67
|
-
'spin', t.spin
|
|
68
|
-
)
|
|
69
|
-
),
|
|
70
|
-
'[]'::jsonb
|
|
71
|
-
)
|
|
72
|
-
into top_json
|
|
73
|
-
from top_rows t
|
|
74
|
-
left join public.beatmaps b on b."beatmapHash" = t."beatmapHash";
|
|
75
|
-
|
|
76
|
-
return jsonb_build_object(
|
|
77
|
-
'top', top_json,
|
|
78
|
-
'stats', jsonb_build_object(
|
|
79
|
-
'totalScores', total_scores,
|
|
80
|
-
'spinScores', spin_scores
|
|
81
|
-
)
|
|
82
|
-
);
|
|
83
|
-
end;
|
|
77
|
+
select jsonb_build_object(
|
|
78
|
+
'top', (select value from top_json),
|
|
79
|
+
'stats', jsonb_build_object(
|
|
80
|
+
'totalScores', coalesce((select total_scores from stats), 0),
|
|
81
|
+
'spinScores', coalesce((select spin_scores from stats), 0)
|
|
82
|
+
)
|
|
83
|
+
);
|
|
84
84
|
$function$
|