rhythia-api 233.0.0 → 234.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.
Files changed (90) hide show
  1. package/.codex +0 -0
  2. package/.env +1 -12
  3. package/README.md +4 -4
  4. package/api/acceptInvite.ts +1 -1
  5. package/api/addCollectionMap.ts +1 -1
  6. package/api/chartPublicStats.ts +1 -1
  7. package/api/checkQualified.ts +93 -93
  8. package/api/createBeatmap.ts +53 -62
  9. package/api/createBeatmapPage.ts +1 -1
  10. package/api/createClan.ts +1 -1
  11. package/api/createCollection.ts +1 -1
  12. package/api/createInvite.ts +1 -1
  13. package/api/createSupporter.ts +1 -1
  14. package/api/deleteBeatmapPage.ts +2 -5
  15. package/api/deleteCollection.ts +1 -1
  16. package/api/deleteCollectionMap.ts +1 -1
  17. package/api/editAboutMe.ts +1 -1
  18. package/api/editClan.ts +1 -1
  19. package/api/editCollection.ts +1 -2
  20. package/api/editProfile.ts +1 -1
  21. package/api/enhancedSearch.ts +113 -113
  22. package/api/executeAdminOperation.ts +1 -22
  23. package/api/getAvatarUploadUrl.ts +1 -1
  24. package/api/getBadgeLeaders.ts +1 -1
  25. package/api/getBadgedUsers.ts +1 -1
  26. package/api/getBeatmapComments.ts +1 -1
  27. package/api/getBeatmapPage.ts +74 -106
  28. package/api/getBeatmapPageById.ts +70 -109
  29. package/api/getBeatmapStarRating.ts +1 -1
  30. package/api/getBeatmaps.ts +1 -1
  31. package/api/getClan.ts +1 -1
  32. package/api/getClans.ts +1 -1
  33. package/api/getCollection.ts +1 -1
  34. package/api/getCollections.ts +1 -1
  35. package/api/getInventory.ts +1 -1
  36. package/api/getLeaderboard.ts +1 -1
  37. package/api/getMapUploadUrl.ts +2 -2
  38. package/api/getOnlinePlayers.ts +1 -1
  39. package/api/getPassToken.ts +1 -1
  40. package/api/getProfile.ts +51 -31
  41. package/api/getPublicStats.ts +5 -5
  42. package/api/getRawStarRating.ts +1 -1
  43. package/api/getScore.ts +1 -1
  44. package/api/getStoryBeatmaps.ts +1 -1
  45. package/api/getTimestamp.ts +1 -1
  46. package/api/getUserScores.ts +19 -19
  47. package/api/getVerified.ts +1 -1
  48. package/api/getVideoUploadUrl.ts +1 -1
  49. package/api/postBeatmapComment.ts +1 -1
  50. package/api/qualifyMap.ts +97 -92
  51. package/api/rankMapsArchive.ts +20 -20
  52. package/api/searchUsers.ts +1 -1
  53. package/api/setPasskey.ts +1 -1
  54. package/api/submitScore.ts +1 -6
  55. package/api/submitScoreInternal.ts +461 -449
  56. package/api/updateBeatmapPage.ts +1 -1
  57. package/api/vetoMap.ts +101 -101
  58. package/index.ts +165 -153
  59. package/package.json +7 -12
  60. package/queries/admin_delete_user.sql +39 -39
  61. package/queries/admin_exclude_user.sql +21 -21
  62. package/queries/admin_invalidate_ranked_scores.sql +18 -18
  63. package/queries/admin_log_action.sql +10 -10
  64. package/queries/admin_profanity_clear.sql +29 -29
  65. package/queries/admin_remove_all_scores.sql +29 -29
  66. package/queries/admin_restrict_user.sql +21 -21
  67. package/queries/admin_search_users.sql +24 -24
  68. package/queries/admin_silence_user.sql +21 -21
  69. package/queries/admin_unban_user.sql +21 -21
  70. package/queries/enhanced_search.sql +217 -217
  71. package/queries/get_badge_leaderboard.sql +50 -50
  72. package/queries/get_clan_leaderboard.sql +68 -68
  73. package/queries/get_collections_v4.sql +109 -109
  74. package/queries/get_top_scores_for_beatmap.sql +44 -44
  75. package/queries/get_top_scores_for_beatmap3.sql +38 -0
  76. package/queries/get_user_by_email.sql +32 -32
  77. package/queries/get_user_scores_lastday.sql +47 -47
  78. package/queries/get_user_scores_reign.sql +31 -31
  79. package/queries/get_user_scores_top_and_stats.sql +84 -84
  80. package/queries/grant_special_badges.sql +69 -69
  81. package/types/database.ts +1288 -1248
  82. package/utils/beatmapTopScores.ts +84 -0
  83. package/utils/mapLifecycleWebhook.ts +287 -277
  84. package/utils/requestGeo.ts +13 -0
  85. package/utils/requestUtils.ts +127 -127
  86. package/utils/response.ts +11 -0
  87. package/worker.ts +189 -0
  88. package/wrangler.jsonc +10 -0
  89. package/index.html +0 -3
  90. package/vercel.json +0 -13
@@ -1,9 +1,8 @@
1
- import { NextResponse } from "next/server";
1
+ import { NextResponse } from "../utils/response";
2
2
  import z from "zod";
3
3
  import { protectedApi } from "../utils/requestUtils";
4
- import { getCacheValue, setCacheValue } from "../utils/cache";
5
4
  import { supabase } from "../utils/supabase";
6
- import { getActiveProfileIdSet } from "../utils/activityStatus";
5
+ import { getVisibleTopScoresForBeatmap } from "../utils/beatmapTopScores";
7
6
 
8
7
  export const Schema = {
9
8
  input: z.strictObject({
@@ -28,6 +27,7 @@ export const Schema = {
28
27
  userId: z.number().nullable(),
29
28
  username: z.string().nullable(),
30
29
  avatar_url: z.string().nullable(),
30
+ accuracy: z.number().nullable(),
31
31
  })
32
32
  )
33
33
  .optional(),
@@ -46,29 +46,29 @@ export const Schema = {
46
46
  beatmapFile: z.string().nullable().optional(),
47
47
  image: z.string().nullable().optional(),
48
48
  starRating: z.number().nullable().optional(),
49
- owner: z.number().nullable().optional(),
50
- ownerUsername: z.string().nullable().optional(),
51
- ownerAvatar: z.string().nullable().optional(),
52
- status: z.string().nullable().optional(),
53
- qualified: z.boolean().nullable().optional(),
54
- qualifiedAt: z.string().nullable().optional(),
55
- videoUrl: z.string().nullable(),
56
- vetos: z
57
- .array(
58
- z.object({
59
- id: z.number(),
60
- userId: z.number().nullable().optional(),
61
- username: z.string().nullable().optional(),
62
- avatar_url: z.string().nullable().optional(),
63
- veto_reason: z.string().nullable().optional(),
64
- created_at: z.string().nullable().optional(),
65
- })
66
- )
67
- .optional(),
68
- })
69
- .optional(),
70
- }),
71
- };
49
+ owner: z.number().nullable().optional(),
50
+ ownerUsername: z.string().nullable().optional(),
51
+ ownerAvatar: z.string().nullable().optional(),
52
+ status: z.string().nullable().optional(),
53
+ qualified: z.boolean().nullable().optional(),
54
+ qualifiedAt: z.string().nullable().optional(),
55
+ videoUrl: z.string().nullable(),
56
+ vetos: z
57
+ .array(
58
+ z.object({
59
+ id: z.number(),
60
+ userId: z.number().nullable().optional(),
61
+ username: z.string().nullable().optional(),
62
+ avatar_url: z.string().nullable().optional(),
63
+ veto_reason: z.string().nullable().optional(),
64
+ created_at: z.string().nullable().optional(),
65
+ })
66
+ )
67
+ .optional(),
68
+ })
69
+ .optional(),
70
+ }),
71
+ };
72
72
 
73
73
  export async function POST(request: Request): Promise<NextResponse> {
74
74
  return protectedApi({
@@ -102,88 +102,49 @@ export async function handler(
102
102
  noteCount,
103
103
  title
104
104
  ),
105
- profiles (
106
- username,
107
- avatar_url
108
- ),
109
- vetos (
110
- id,
111
- user,
112
- veto_reason,
113
- created_at,
114
- profiles (
115
- id,
116
- username,
117
- avatar_url
118
- )
119
- )
120
- `
121
- )
105
+ profiles (
106
+ username,
107
+ avatar_url
108
+ ),
109
+ vetos (
110
+ id,
111
+ user,
112
+ veto_reason,
113
+ created_at,
114
+ profiles (
115
+ id,
116
+ username,
117
+ avatar_url
118
+ )
119
+ )
120
+ `
121
+ )
122
122
  .eq("latestBeatmapHash", data.mapId)
123
123
  .single();
124
124
 
125
125
  if (!beatmapPage) return NextResponse.json({});
126
126
 
127
127
  const beatmapHash = beatmapPage?.latestBeatmapHash || "";
128
- const isCacheable =
129
- beatmapPage?.status === "RANKED" || beatmapPage?.status === "APPROVED";
130
- const cacheKey = `beatmap-scores:${beatmapHash}`;
131
-
132
- let scoreData: any[] | null = null;
133
-
134
- if (isCacheable && beatmapHash) {
135
- scoreData = await getCacheValue<any[]>(cacheKey);
136
- }
137
-
138
- if (!scoreData) {
139
- const { data: rpcScores, error } = await supabase.rpc(
140
- "get_top_scores_for_beatmap",
141
- { beatmap_hash: beatmapHash }
142
- );
143
-
144
- if (error) {
145
- return NextResponse.json({ error: JSON.stringify(error) });
146
- }
147
-
148
- scoreData = (rpcScores || []).slice(0, 200);
128
+ const { scores, error: scoreError } = await getVisibleTopScoresForBeatmap(
129
+ beatmapHash,
130
+ limit
131
+ );
149
132
 
150
- if (isCacheable && beatmapHash) {
151
- await setCacheValue(cacheKey, scoreData);
152
- }
133
+ if (scoreError) {
134
+ return NextResponse.json({ error: scoreError });
153
135
  }
154
136
 
155
- const userIds = Array.from(
156
- new Set((scoreData || []).map((score) => score.userid).filter(Boolean))
157
- );
158
- const activeUserIds = await getActiveProfileIdSet(userIds);
159
- const visibleScores = (scoreData || [])
160
- .filter((score) => activeUserIds.has(score.userid))
161
- .slice(0, limit);
137
+ const mappedVetos = ((beatmapPage as any).vetos || []).map((veto: any) => ({
138
+ id: veto.id,
139
+ userId: veto.user,
140
+ username: veto.profiles?.username,
141
+ avatar_url: veto.profiles?.avatar_url,
142
+ veto_reason: veto.veto_reason,
143
+ created_at: veto.created_at,
144
+ }));
162
145
 
163
- const mappedVetos = ((beatmapPage as any).vetos || []).map((veto: any) => ({
164
- id: veto.id,
165
- userId: veto.user,
166
- username: veto.profiles?.username,
167
- avatar_url: veto.profiles?.avatar_url,
168
- veto_reason: veto.veto_reason,
169
- created_at: veto.created_at,
170
- }));
171
-
172
- return NextResponse.json({
173
- scores: visibleScores.map((score: any) => ({
174
- id: score.id,
175
- awarded_sp: score.awarded_sp,
176
- created_at: score.created_at,
177
- misses: score.misses,
178
- mods: score.mods,
179
- passed: score.passed,
180
- songId: score.songid,
181
- speed: score.speed,
182
- spin: score.spin,
183
- userId: score.userid,
184
- username: score.username,
185
- avatar_url: score.avatar_url,
186
- })),
146
+ return NextResponse.json({
147
+ scores,
187
148
  beatmap: {
188
149
  playcount: beatmapPage.beatmaps?.playcount,
189
150
  created_at: beatmapPage.created_at,
@@ -198,14 +159,14 @@ export async function handler(
198
159
  starRating: beatmapPage.beatmaps?.starRating,
199
160
  owner: beatmapPage.owner,
200
161
  ownerUsername: beatmapPage.profiles?.username,
201
- ownerAvatar: beatmapPage.profiles?.avatar_url,
202
- id: beatmapPage.id,
203
- status: beatmapPage.status,
204
- qualified: beatmapPage.qualified,
205
- qualifiedAt: beatmapPage.qualifiedAt,
206
- nominations: beatmapPage.nominations as number[],
207
- videoUrl: beatmapPage.video_url,
208
- vetos: mappedVetos.length > 0 ? mappedVetos : undefined,
209
- },
210
- });
211
- }
162
+ ownerAvatar: beatmapPage.profiles?.avatar_url,
163
+ id: beatmapPage.id,
164
+ status: beatmapPage.status,
165
+ qualified: beatmapPage.qualified,
166
+ qualifiedAt: beatmapPage.qualifiedAt,
167
+ nominations: beatmapPage.nominations as number[],
168
+ videoUrl: beatmapPage.video_url,
169
+ vetos: mappedVetos.length > 0 ? mappedVetos : undefined,
170
+ },
171
+ });
172
+ }
@@ -1,4 +1,4 @@
1
- import { NextResponse } from "next/server";
1
+ import { NextResponse } from "../utils/response";
2
2
  import z from "zod";
3
3
  import { protectedApi } from "../utils/requestUtils";
4
4
  import { supabase } from "../utils/supabase";
@@ -1,4 +1,4 @@
1
- import { NextResponse } from "next/server";
1
+ import { NextResponse } from "../utils/response";
2
2
  import z from "zod";
3
3
  import { protectedApi } from "../utils/requestUtils";
4
4
  import { supabase } from "../utils/supabase";
package/api/getClan.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { NextResponse } from "next/server";
1
+ import { NextResponse } from "../utils/response";
2
2
  import z from "zod";
3
3
  import { protectedApi, validUser } from "../utils/requestUtils";
4
4
  import { supabase } from "../utils/supabase";
package/api/getClans.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { NextResponse } from "next/server";
1
+ import { NextResponse } from "../utils/response";
2
2
  import z from "zod";
3
3
  import { protectedApi, validUser } from "../utils/requestUtils";
4
4
  import { supabase } from "../utils/supabase";
@@ -1,4 +1,4 @@
1
- import { NextResponse } from "next/server";
1
+ import { NextResponse } from "../utils/response";
2
2
  import z from "zod";
3
3
  import { protectedApi } from "../utils/requestUtils";
4
4
  import { supabase } from "../utils/supabase";
@@ -1,4 +1,4 @@
1
- import { NextResponse } from "next/server";
1
+ import { NextResponse } from "../utils/response";
2
2
  import z from "zod";
3
3
  import { protectedApi } from "../utils/requestUtils";
4
4
  import { supabase } from "../utils/supabase";
@@ -1,4 +1,4 @@
1
- import { NextResponse } from "next/server";
1
+ import { NextResponse } from "../utils/response";
2
2
  import z from "zod";
3
3
  import { protectedApi, validUser } from "../utils/requestUtils";
4
4
  import { supabase } from "../utils/supabase";
@@ -1,4 +1,4 @@
1
- import { NextResponse } from "next/server";
1
+ import { NextResponse } from "../utils/response";
2
2
  import z from "zod";
3
3
  import { protectedApi } from "../utils/requestUtils";
4
4
  import { supabase } from "../utils/supabase";
@@ -1,4 +1,4 @@
1
- import { NextResponse } from "next/server";
1
+ import { NextResponse } from "../utils/response";
2
2
  import z from "zod";
3
3
  import { protectedApi, validUser } from "../utils/requestUtils";
4
4
  import { supabase } from "../utils/supabase";
@@ -63,7 +63,7 @@ export async function handler({
63
63
  });
64
64
  }
65
65
 
66
- if (contentLength > 50000000) {
66
+ if (contentLength > 100000000) {
67
67
  return NextResponse.json({
68
68
  error: "Max content length exceeded.",
69
69
  });
@@ -1,4 +1,4 @@
1
- import { NextResponse } from "next/server";
1
+ import { NextResponse } from "../utils/response";
2
2
  import z from "zod";
3
3
  import { protectedApi } from "../utils/requestUtils";
4
4
  import { supabase } from "../utils/supabase";
@@ -1,4 +1,4 @@
1
- import { NextResponse } from "next/server";
1
+ import { NextResponse } from "../utils/response";
2
2
  import z from "zod";
3
3
  import { Database } from "../types/database";
4
4
  import { protectedApi, validUser } from "../utils/requestUtils";
package/api/getProfile.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { geolocation } from "@vercel/edge";
2
- import { NextResponse } from "next/server";
1
+ import { geolocation } from "../utils/requestGeo";
2
+ import { NextResponse } from "../utils/response";
3
3
  import z from "zod";
4
4
  import { Database } from "../types/database";
5
5
  import { protectedApi } from "../utils/requestUtils";
@@ -33,15 +33,16 @@ export const Schema = {
33
33
  verified: z.boolean().nullable(),
34
34
  verificationDeadline: z.number().nullable(),
35
35
  play_count: z.number().nullable(),
36
- skill_points: z.number().nullable(),
37
- squares_hit: z.number().nullable(),
38
- total_score: z.number().nullable(),
39
- position: z.number().nullable(),
40
- activity_status: z.enum(["active", "inactive"]),
41
- is_online: z.boolean(),
42
- clans: z
43
- .object({
44
- id: z.number(),
36
+ skill_points: z.number().nullable(),
37
+ squares_hit: z.number().nullable(),
38
+ total_score: z.number().nullable(),
39
+ position: z.number().nullable(),
40
+ country_position: z.number().nullable(),
41
+ activity_status: z.enum(["active", "inactive"]),
42
+ is_online: z.boolean(),
43
+ clans: z
44
+ .object({
45
+ id: z.number(),
45
46
  acronym: z.string(),
46
47
  })
47
48
  .optional()
@@ -131,19 +132,37 @@ export async function handler(
131
132
  if (activityData && activityData.last_activity) {
132
133
  isOnline = Date.now() - activityData.last_activity < 1800000;
133
134
  }
134
-
135
- let position: number | null = null;
136
- if (activityStatus === "active") {
137
- const cutoffIso = getScoreActivityCutoffIso();
138
- const { count: playersWithMorePoints } = await supabase
139
- .from("profiles")
140
- .select("id,scores!inner(id)", { count: "exact", head: true })
141
- .neq("ban", "excluded")
142
- .gte("scores.created_at", cutoffIso)
143
- .gt("skill_points", user.skill_points);
144
-
145
- position = (playersWithMorePoints || 0) + 1;
146
- }
135
+
136
+ let position: number | null = null;
137
+ let countryPosition: number | null = null;
138
+ if (activityStatus === "active") {
139
+ const cutoffIso = getScoreActivityCutoffIso();
140
+ const globalPositionQuery = supabase
141
+ .from("profiles")
142
+ .select("id,scores!inner(id)", { count: "exact", head: true })
143
+ .neq("ban", "excluded")
144
+ .gte("scores.created_at", cutoffIso)
145
+ .gt("skill_points", user.skill_points);
146
+
147
+ const countryPositionQuery = user.flag
148
+ ? supabase
149
+ .from("profiles")
150
+ .select("id,scores!inner(id)", { count: "exact", head: true })
151
+ .neq("ban", "excluded")
152
+ .eq("flag", user.flag)
153
+ .gte("scores.created_at", cutoffIso)
154
+ .gt("skill_points", user.skill_points)
155
+ : Promise.resolve({ count: null });
156
+
157
+ const [{ count: playersWithMorePoints }, { count: countryPlayersWithMorePoints }] =
158
+ await Promise.all([globalPositionQuery, countryPositionQuery]);
159
+
160
+ position = (playersWithMorePoints || 0) + 1;
161
+ countryPosition =
162
+ user.flag && countryPlayersWithMorePoints !== null
163
+ ? (countryPlayersWithMorePoints || 0) + 1
164
+ : null;
165
+ }
147
166
 
148
167
  if (user.verificationDeadline < Date.now()) {
149
168
  await supabase
@@ -157,10 +176,11 @@ export async function handler(
157
176
 
158
177
  return NextResponse.json({
159
178
  user: {
160
- ...user,
161
- position,
162
- activity_status: activityStatus,
163
- is_online: isOnline,
164
- },
165
- });
166
- }
179
+ ...user,
180
+ position,
181
+ country_position: countryPosition,
182
+ activity_status: activityStatus,
183
+ is_online: isOnline,
184
+ },
185
+ });
186
+ }
@@ -1,4 +1,4 @@
1
- import { NextResponse } from "next/server";
1
+ import { NextResponse } from "../utils/response";
2
2
  import z from "zod";
3
3
  import { protectedApi } from "../utils/requestUtils";
4
4
  import { supabase } from "../utils/supabase";
@@ -69,11 +69,11 @@ export async function POST(request: Request) {
69
69
  export async function handler(data: (typeof Schema)["input"]["_type"]) {
70
70
  const countProfilesQuery = await supabase
71
71
  .from("profiles")
72
- .select("*", { count: "exact", head: true });
72
+ .select("*", { count: "estimated", head: true });
73
73
 
74
74
  const countBeatmapsQuery = await supabase
75
75
  .from("beatmaps")
76
- .select("*", { count: "exact", head: true });
76
+ .select("*", { count: "estimated", head: true });
77
77
 
78
78
  let { data: beatmapPage, error: errorlast } = await supabase
79
79
  .from("beatmapPages")
@@ -128,12 +128,12 @@ export async function handler(data: (typeof Schema)["input"]["_type"]) {
128
128
 
129
129
  const countScoresQuery = await supabase
130
130
  .from("scores")
131
- .select("id", { count: "exact", head: true });
131
+ .select("id", { count: "estimated", head: true });
132
132
 
133
133
  // 30 minutes activity
134
134
  const countOnline = await supabase
135
135
  .from("profileActivities")
136
- .select("*", { count: "exact", head: true })
136
+ .select("*", { count: "estimated", head: true })
137
137
  .gt("last_activity", Date.now() - 1800000);
138
138
 
139
139
  const countChart = await supabase
@@ -1,4 +1,4 @@
1
- import { NextResponse } from "next/server";
1
+ import { NextResponse } from "../utils/response";
2
2
  import z from "zod";
3
3
  import { protectedApi } from "../utils/requestUtils";
4
4
  import { rateMapNotes } from "../utils/star-calc";
package/api/getScore.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { NextResponse } from "next/server";
1
+ import { NextResponse } from "../utils/response";
2
2
  import z from "zod";
3
3
  import { protectedApi } from "../utils/requestUtils";
4
4
  import { supabase } from "../utils/supabase";
@@ -1,4 +1,4 @@
1
- import { NextResponse } from "next/server";
1
+ import { NextResponse } from "../utils/response";
2
2
  import z from "zod";
3
3
  import { protectedApi } from "../utils/requestUtils";
4
4
  import { supabase } from "../utils/supabase";
@@ -1,4 +1,4 @@
1
- import { NextResponse } from "next/server";
1
+ import { NextResponse } from "../utils/response";
2
2
  import z from "zod";
3
3
  import { protectedApi } from "../utils/requestUtils";
4
4
 
@@ -1,4 +1,4 @@
1
- import { NextResponse } from "next/server";
1
+ import { NextResponse } from "../utils/response";
2
2
  import z from "zod";
3
3
  import { getCacheValue, setCacheValue } from "../utils/cache";
4
4
  import { protectedApi } from "../utils/requestUtils";
@@ -18,12 +18,12 @@ export const Schema = {
18
18
  awarded_sp: z.number().nullable(),
19
19
  beatmapHash: z.string().nullable(),
20
20
  created_at: z.string(),
21
- id: z.number(),
22
- misses: z.number().nullable(),
23
- passed: z.boolean().nullable(),
24
- replay_url: z.string().nullable().optional(),
25
- songId: z.string().nullable(),
26
- userId: z.number().nullable(),
21
+ id: z.number(),
22
+ misses: z.number().nullable(),
23
+ passed: z.boolean().nullable(),
24
+ replay_url: z.string().nullable().optional(),
25
+ songId: z.string().nullable(),
26
+ userId: z.number().nullable(),
27
27
  beatmapDifficulty: z.number().optional().nullable(),
28
28
  beatmapNotes: z.number().optional().nullable(),
29
29
  beatmapTitle: z.string().optional().nullable(),
@@ -38,12 +38,12 @@ export const Schema = {
38
38
  id: z.number(),
39
39
  awarded_sp: z.number().nullable(),
40
40
  created_at: z.string(),
41
- misses: z.number().nullable(),
42
- mods: z.record(z.unknown()),
43
- passed: z.boolean().nullable(),
44
- replay_url: z.string().nullable().optional(),
45
- songId: z.string().nullable(),
46
- speed: z.number().nullable(),
41
+ misses: z.number().nullable(),
42
+ mods: z.record(z.unknown()),
43
+ passed: z.boolean().nullable(),
44
+ replay_url: z.string().nullable().optional(),
45
+ songId: z.string().nullable(),
46
+ speed: z.number().nullable(),
47
47
  spin: z.boolean(),
48
48
  beatmapHash: z.string().nullable(),
49
49
  beatmapTitle: z.string().nullable(),
@@ -58,12 +58,12 @@ export const Schema = {
58
58
  awarded_sp: z.number().nullable(),
59
59
  beatmapHash: z.string().nullable(),
60
60
  created_at: z.string(),
61
- id: z.number(),
62
- misses: z.number().nullable(),
63
- passed: z.boolean().nullable(),
64
- replay_url: z.string().nullable().optional(),
65
- rank: z.string().nullable(),
66
- songId: z.string().nullable(),
61
+ id: z.number(),
62
+ misses: z.number().nullable(),
63
+ passed: z.boolean().nullable(),
64
+ replay_url: z.string().nullable().optional(),
65
+ rank: z.string().nullable(),
66
+ songId: z.string().nullable(),
67
67
  userId: z.number().nullable(),
68
68
  beatmapDifficulty: z.number().optional().nullable(),
69
69
  beatmapNotes: z.number().optional().nullable(),
@@ -1,4 +1,4 @@
1
- import { NextResponse } from "next/server";
1
+ import { NextResponse } from "../utils/response";
2
2
  import z from "zod";
3
3
  import { protectedApi, validUser } from "../utils/requestUtils";
4
4
  import { supabase } from "../utils/supabase";
@@ -1,4 +1,4 @@
1
- import { NextResponse } from "next/server";
1
+ import { NextResponse } from "../utils/response";
2
2
  import z from "zod";
3
3
  import { protectedApi, validUser } from "../utils/requestUtils";
4
4
  import { supabase } from "../utils/supabase";
@@ -1,4 +1,4 @@
1
- import { NextResponse } from "next/server";
1
+ import { NextResponse } from "../utils/response";
2
2
  import z from "zod";
3
3
  import { protectedApi, validUser } from "../utils/requestUtils";
4
4
  import { supabase } from "../utils/supabase";