refactor(契约): 契约闸门铺到其余学生端接口,并清掉与契约等价的重复类型
Some checks failed
Deploy / deploy (push) Has been cancelled

## 闸门覆盖

从 7 条扩到 20 条:竞赛列表/详情/口令/榜单、题单列表/详情/题目/徽章/进度、
全服榜与活跃榜、公告列表/详情、教程、用户度量、流程图历史/当前/统计、
提交统计与统计明细、相似题目、题目 AC 榜。

## 顺手把两处形状收进契约

- `contestRankItemSchema.submissionInfo` 原来只是 `Record<string, unknown>`,
  前端被迫再声明一份 `SubmissionInfo` 去覆盖它。现在 JSONB 的 snake_case 形状
  (is_ac / ac_time / is_first_ac / error_number / checked)进了契约,那层覆盖随之消失。
- 竞赛题目列表用 `problemListItemSchema.array()` 而不是契约里的
  `contestProblemsSchema` —— 后者是 `array(union([列表项, 详情]))`,联合类型会让
  `filterResult` 的类型收窄落到详情分支,而且学生侧这条接口只下发列表项。

## 清掉两处确证重复的派生

用类型探针(双向可赋值)逐对验证,`Problem` 与 `Message` 的本地派生与契约**完全等价**,
是没有内容的重复:`Problem` 的 languages/template 收窄、`Message` 的 submission 收窄
都已经在前两轮搬进契约。其余七处派生(Submission 的 result|9、Profile、AdminProblem、
Exercise、ContestRank、BlankProblem、TutorialEdit 等)探针确认**不等价**,都有真实理由,
一律保留。

`Problem` 原来还挂着 hasAstRules / visible / answers 三个可选字段,探针证明它们对
赋值没有影响(契约里本来就有 hasAstRules),去掉后没有任何调用点报错。

## 验证

- vue-tsc 与 tsc -p apps/api 均 exit 0;vite build 通过;check:routes 无遮蔽;
- 契约 schema 直跑真实接口:学生会话 13 通过 / 0 失败,管理员会话 15 通过 / 0 失败;
  跳过的三条都是预期的权限或空数据(/submissions/statistics 与 /flowcharts/statistics
  要教师权限、/users/:id/metrics 只统计公开提交)。
This commit is contained in:
2026-09-10 04:23:41 -06:00
parent aab0404ed7
commit de34a47996
3 changed files with 249 additions and 128 deletions

View File

@@ -1,54 +1,55 @@
import { import {
type AiAnalysisRecord, type AiAnalysisRecord,
type Contest as OjContest,
type ContestAccess,
type ContestList,
type ActivityRankItem,
type FormatCodeResponse, type FormatCodeResponse,
type Metrics,
type TutorialSummary, type TutorialSummary,
type ClassComparisonResponse, type ClassComparisonResponse,
type ClassRankItem, type ClassRankItem,
type ClassUserRank, type ClassUserRank,
type UserRank,
type ProblemRank,
type CreateSubmissionResponse, type CreateSubmissionResponse,
type ProblemAuthor, type ProblemAuthor,
type ProblemListItem,
type YearlyAc, type YearlyAc,
type CreateFlowchartResponse, type CreateFlowchartResponse,
type FlowchartCurrent,
type FlowchartDetail,
type FlowchartList,
type FlowchartSubmission, type FlowchartSubmission,
type AiDetail, type AiDetail,
type DurationData, type DurationData,
type HeatmapItem, type HeatmapItem,
type LoginSummary, type LoginSummary,
type SolvedList, type SolvedList,
type ProblemSet,
type ProblemSetBadge,
type ProblemSetList,
type ProblemSetProblem,
type ProblemSetProgressList,
type UserBadge,
problemDetailSchema, problemDetailSchema,
problemListSchema, problemListSchema,
problemListItemSchema,
submissionDetailSchema, submissionDetailSchema,
submissionListSchema, submissionListSchema,
submissionStatisticsSchema,
submissionStatisticsItemsSchema,
onlineCountSchema, onlineCountSchema,
websiteConfigSchema, websiteConfigSchema,
type FlowchartStatistics, contestListSchema,
type SubmissionStatistics, contestSchema,
type SubmissionStatisticsItems, contestAccessSchema,
contestRankSchema,
announcementListSchema,
announcementSchema,
problemSetListSchema,
problemSetSchema,
problemSetBadgeSchema,
userBadgeSchema,
tutorialSchema,
metricsSchema,
activityRankItemSchema,
problemRankSchema,
userRankSchema,
flowchartListSchema,
flowchartDetailSchema,
flowchartCurrentSchema,
flowchartStatisticsSchema,
problemSetProgressListSchema,
problemSetProblemSchema,
} from "@oj2/contract" } from "@oj2/contract"
import api from "utils/api" import api from "utils/api"
import { contract } from "utils/contract" import { contract } from "utils/contract"
import { filterResult } from "oj/transforms" import { filterResult } from "oj/transforms"
import type { import type {
Announcement,
AnnouncementListItem,
ContestRank,
Profile, Profile,
Message, Message,
Exercise, Exercise,
@@ -57,7 +58,6 @@ import type {
ReactionState, ReactionState,
SubmissionListPayload, SubmissionListPayload,
SubmitCodePayload, SubmitCodePayload,
Tutorial,
TutorialProgress, TutorialProgress,
} from "utils/types" } from "utils/types"
@@ -183,8 +183,13 @@ async function getSubmissionPage(
) )
} }
export function getRankOfProblem(problemId: string) { export async function getRankOfProblem(problemId: string) {
return api.get<ProblemRank>(`problems/${encodeURIComponent(problemId)}/rank`) const endpoint = `problems/${encodeURIComponent(problemId)}/rank`
return contract(
"GET /problems/:id/rank",
problemRankSchema,
await api.get<unknown>(endpoint),
)
} }
export function getTodaySubmissionCount(language?: string) { export function getTodaySubmissionCount(language?: string) {
@@ -201,38 +206,56 @@ export function adminRejudge(id: string) {
* 统计面板展开一行时拉这个人的明细。username 这里要**精确**到人, * 统计面板展开一行时拉这个人的明细。username 这里要**精确**到人,
* 和上面那个按班级模糊匹配的不是一回事。 * 和上面那个按班级模糊匹配的不是一回事。
*/ */
export function getSubmissionStatisticsItems( export async function getSubmissionStatisticsItems(
duration: { start?: string; end: string }, duration: { start?: string; end: string },
username: string, username: string,
problemID?: string, problemID?: string,
) { ) {
return api.get<SubmissionStatisticsItems>("submissions/statistics/items", { const endpoint = "submissions/statistics/items"
params: { ...duration, problemId: problemID, username }, return contract(
}) "GET /submissions/statistics/items",
submissionStatisticsItemsSchema,
await api.get<unknown>(endpoint, {
params: { ...duration, problemId: problemID, username },
}),
)
} }
export function getSubmissionStatistics( export async function getSubmissionStatistics(
duration: { start?: string; end: string }, duration: { start?: string; end: string },
problemID?: string, problemID?: string,
username?: string, username?: string,
) { ) {
return api.get<SubmissionStatistics>("submissions/statistics", { const endpoint = "submissions/statistics"
params: { ...duration, problemId: problemID, username }, return contract(
}) "GET /submissions/statistics",
submissionStatisticsSchema,
await api.get<unknown>(endpoint, {
params: { ...duration, problemId: problemID, username },
}),
)
} }
/** /**
* 全服榜单。上限100 名)由服务端定,调用方只管翻页 —— * 全服榜单。上限100 名)由服务端定,调用方只管翻页 ——
* 「全服 Top10」就是这个榜的第一页取 limit=10 即可,不需要另一个上限参数。 * 「全服 Top10」就是这个榜的第一页取 limit=10 即可,不需要另一个上限参数。
*/ */
export function getRank(offset: number, limit: number) { export async function getRank(offset: number, limit: number) {
return api.get<UserRank>("rankings/users", { params: { offset, limit } }) const endpoint = "rankings/users"
return contract(
"GET /rankings/users",
userRankSchema,
await api.get<unknown>(endpoint, { params: { offset, limit } }),
)
} }
export function getActivityRank(start: string) { export async function getActivityRank(start: string) {
return api.get<ActivityRankItem[]>("rankings/activity", { const endpoint = "rankings/activity"
params: { start }, return contract(
}) "GET /rankings/activity",
activityRankItemSchema.array(),
await api.get<unknown>(endpoint, { params: { start } }),
)
} }
export function getClassRank(grade?: number | null) { export function getClassRank(grade?: number | null) {
@@ -261,22 +284,37 @@ export function getClassPK(
}) })
} }
export function getContestList(query: { export async function getContestList(query: {
offset: number offset: number
limit: number limit: number
keyword: string keyword: string
status: string status: string
tag: string tag: string
}) { }) {
return api.get<ContestList>("contests", { params: query }) const endpoint = "contests"
return contract(
"GET /contests",
contestListSchema,
await api.get<unknown>(endpoint, { params: query }),
)
} }
export function getContest(id: string) { export async function getContest(id: string) {
return api.get<OjContest>(`contests/${encodeURIComponent(id)}`) const endpoint = `contests/${encodeURIComponent(id)}`
return contract(
"GET /contests/:id",
contestSchema,
await api.get<unknown>(endpoint),
)
} }
export function getContestAccess(id: string) { export async function getContestAccess(id: string) {
return api.get<ContestAccess>(`contests/${encodeURIComponent(id)}/access`) const endpoint = `contests/${encodeURIComponent(id)}/access`
return contract(
"GET /contests/:id/access",
contestAccessSchema,
await api.get<unknown>(endpoint),
)
} }
// 注意和 GET /access 不一样:这个返回裸 true密码错是 403 走 catch // 注意和 GET /access 不一样:这个返回裸 true密码错是 403 走 catch
@@ -290,21 +328,29 @@ export function checkContestPassword(contestID: string, password: string) {
} }
export async function getContestProblems(contestID: string) { export async function getContestProblems(contestID: string) {
const res = await api.get<ProblemListItem[]>( const endpoint = `contests/${encodeURIComponent(contestID)}/problems`
`contests/${encodeURIComponent(contestID)}/problems`, // 用 problemListItemSchema.array(),不是契约的 contestProblemsSchema ——
// 后者是 `array(union([列表项, 详情]))`,联合类型会让 filterResult 的类型收窄
// 落到详情分支上,而且学生侧这条接口只下发列表项。
const res = contract(
"GET /contests/:id/problems",
problemListItemSchema.array(),
await api.get<unknown>(endpoint),
) )
return res.map(filterResult) return res.map(filterResult)
} }
export function getContestRank( export async function getContestRank(
contestID: string, contestID: string,
query: { limit: number; offset: number }, query: { limit: number; offset: number },
) { ) {
// submissionInfo 在契约里是 Record<string, unknown>JSONB 原文), // submissionInfo 在契约里是 Record<string, unknown>JSONB 原文),
// 前端在这里收窄成 SubmissionInfo见 utils/types 的 ContestRank // 前端在这里收窄成 SubmissionInfo见 utils/types 的 ContestRank
return api.get<{ results: ContestRank[]; total: number }>( const endpoint = `contests/${encodeURIComponent(contestID)}/rank`
`contests/${encodeURIComponent(contestID)}/rank`, return contract(
{ params: query }, "GET /contests/:id/rank",
contestRankSchema,
await api.get<unknown>(endpoint, { params: query }),
) )
} }
@@ -320,14 +366,22 @@ export function updateProfile(data: { realName: string; mood: string }) {
return api.put<Profile>("me/profile", data) return api.put<Profile>("me/profile", data)
} }
export function getAnnouncementList(offset = 0, limit = 10) { export async function getAnnouncementList(offset = 0, limit = 10) {
return api.get<{ results: AnnouncementListItem[]; total: number }>("announcements", { const endpoint = "announcements"
params: { limit, offset }, return contract(
}) "GET /announcements",
announcementListSchema,
await api.get<unknown>(endpoint, { params: { limit, offset } }),
)
} }
export function getAnnouncement(id: number) { export async function getAnnouncement(id: number) {
return api.get<Announcement>(`announcements/${id}`) const endpoint = `announcements/${id}`
return contract(
"GET /announcements/:id",
announcementSchema,
await api.get<unknown>(endpoint),
)
} }
export function getMessageList(offset = 0, limit = 10) { export function getMessageList(offset = 0, limit = 10) {
@@ -345,12 +399,22 @@ export function setReaction(problemID: number, type: ReactionKey) {
return api.post<ReactionState>(`problems/${problemID}/reaction`, { type }) return api.post<ReactionState>(`problems/${problemID}/reaction`, { type })
} }
export function getMetrics(userid: number) { export async function getMetrics(userid: number) {
return api.get<Metrics>(`users/${userid}/metrics`) const endpoint = `users/${userid}/metrics`
return contract(
"GET /users/:id/metrics",
metricsSchema,
await api.get<unknown>(endpoint),
)
} }
export function getTutorial(id: number) { export async function getTutorial(id: number) {
return api.get<Tutorial>(`tutorials/${id}`) const endpoint = `tutorials/${id}`
return contract(
"GET /tutorials/:id",
tutorialSchema,
await api.get<unknown>(endpoint),
)
} }
export function getTutorials(type: "python" | "c") { export function getTutorials(type: "python" | "c") {
@@ -399,10 +463,14 @@ export function getAIPinnedReport() {
// ==================== 相似题目推荐 ==================== // ==================== 相似题目推荐 ====================
export function getSimilarProblems(problemId: string) { export async function getSimilarProblems(problemId: string) {
return api const endpoint = `problems/${encodeURIComponent(problemId)}/similar`
.get<ProblemListItem[]>(`problems/${encodeURIComponent(problemId)}/similar`) const res = contract(
.then((response) => response.map(filterResult)) "GET /problems/:id/similar",
problemListItemSchema.array(),
await api.get<unknown>(endpoint),
)
return res.map(filterResult)
} }
export type { YearlyAc as YearlyACData } from "@oj2/contract" export type { YearlyAc as YearlyACData } from "@oj2/contract"
@@ -427,7 +495,7 @@ export function getFlowchartSubmission(id: string) {
return api.get<FlowchartSubmission>(`flowcharts/${encodeURIComponent(id)}`) return api.get<FlowchartSubmission>(`flowcharts/${encodeURIComponent(id)}`)
} }
export function getFlowchartSubmissions(params: { export async function getFlowchartSubmissions(params: {
username?: string username?: string
problemId?: string problemId?: string
myself?: string myself?: string
@@ -436,17 +504,27 @@ export function getFlowchartSubmissions(params: {
today?: string today?: string
grade?: string grade?: string
}) { }) {
return api.get<FlowchartList>("flowcharts", { params }) const endpoint = "flowcharts"
return contract(
"GET /flowcharts",
flowchartListSchema,
await api.get<unknown>(endpoint, { params }),
)
} }
export function getFlowchartStatistics( export async function getFlowchartStatistics(
duration: { start?: string; end: string }, duration: { start?: string; end: string },
problemID?: string, problemID?: string,
username?: string, username?: string,
) { ) {
return api.get<FlowchartStatistics>("flowcharts/statistics", { const endpoint = "flowcharts/statistics"
params: { ...duration, problemId: problemID, username }, return contract(
}) "GET /flowcharts/statistics",
flowchartStatisticsSchema,
await api.get<unknown>(endpoint, {
params: { ...duration, problemId: problemID, username },
}),
)
} }
export function retryFlowchartSubmission(submissionId: string) { export function retryFlowchartSubmission(submissionId: string) {
@@ -455,36 +533,59 @@ export function retryFlowchartSubmission(submissionId: string) {
) )
} }
export function getCurrentProblemFlowchartSubmission(problemId: number) { export async function getCurrentProblemFlowchartSubmission(problemId: number) {
return api.get<FlowchartCurrent>(`problems/${problemId}/flowchart/current`) const endpoint = `problems/${problemId}/flowchart/current`
return contract(
"GET /problems/:id/flowchart/current",
flowchartCurrentSchema,
await api.get<unknown>(endpoint),
)
} }
export function getFlowchartSubmissionDetail(problemId: number, page = 0) { export async function getFlowchartSubmissionDetail(problemId: number, page = 0) {
return api.get<FlowchartDetail>(`problems/${problemId}/flowchart/history`, { const endpoint = `problems/${problemId}/flowchart/history`
params: { page }, return contract(
}) "GET /problems/:id/flowchart/history",
flowchartDetailSchema,
await api.get<unknown>(endpoint, { params: { page } }),
)
} }
// ==================== 题单相关API ==================== // ==================== 题单相关API ====================
export function getProblemSetList( export async function getProblemSetList(
offset = 0, offset = 0,
limit = 10, limit = 10,
keyword = "", keyword = "",
difficulty = "", difficulty = "",
status = "", status = "",
) { ) {
return api.get<ProblemSetList>("problem-sets", { const endpoint = "problem-sets"
params: { offset, limit, keyword, difficulty, status }, return contract(
}) "GET /problem-sets",
problemSetListSchema,
await api.get<unknown>(endpoint, {
params: { offset, limit, keyword, difficulty, status },
}),
)
} }
export function getProblemSetDetail(id: number) { export async function getProblemSetDetail(id: number) {
return api.get<ProblemSet>(`problem-sets/${id}`) const endpoint = `problem-sets/${id}`
return contract(
"GET /problem-sets/:id",
problemSetSchema,
await api.get<unknown>(endpoint),
)
} }
export function getProblemSetProblems(problemSetId: number) { export async function getProblemSetProblems(problemSetId: number) {
return api.get<ProblemSetProblem[]>(`problem-sets/${problemSetId}/problems`) const endpoint = `problem-sets/${problemSetId}/problems`
return contract(
"GET /problem-sets/:id/problems",
problemSetProblemSchema.array(),
await api.get<unknown>(endpoint),
)
} }
export function joinProblemSet(problemSetId: number) { export function joinProblemSet(problemSetId: number) {
@@ -503,17 +604,25 @@ export function updateProblemSetProgress(
}) })
} }
export function getUserBadges(username?: string) { export async function getUserBadges(username?: string) {
return api.get<UserBadge[]>( const endpoint = `users/${encodeURIComponent(username ?? "me")}/badges`
`users/${encodeURIComponent(username ?? "me")}/badges`, return contract(
"GET /users/:username/badges",
userBadgeSchema.array(),
await api.get<unknown>(endpoint),
) )
} }
export function getProblemSetBadges(problemSetId: number) { export async function getProblemSetBadges(problemSetId: number) {
return api.get<ProblemSetBadge[]>(`problem-sets/${problemSetId}/badges`) const endpoint = `problem-sets/${problemSetId}/badges`
return contract(
"GET /problem-sets/:id/badges",
problemSetBadgeSchema.array(),
await api.get<unknown>(endpoint),
)
} }
export function getProblemSetUserProgress( export async function getProblemSetUserProgress(
problemSetId: number, problemSetId: number,
params?: { params?: {
limit?: number limit?: number
@@ -522,9 +631,11 @@ export function getProblemSetUserProgress(
completionStatus?: "" | "completed" | "in_progress" | "not_started" completionStatus?: "" | "completed" | "in_progress" | "not_started"
}, },
) { ) {
return api.get<ProblemSetProgressList>( const endpoint = `problem-sets/${problemSetId}/user-progress`
`problem-sets/${problemSetId}/user-progress`, return contract(
{ params }, "GET /problem-sets/:id/user-progress",
problemSetProgressListSchema,
await api.get<unknown>(endpoint, { params }),
) )
} }

View File

@@ -113,17 +113,18 @@ export type { UploadTestCaseResponse } from "@oj2/contract"
export type { ProblemTestCaseScore as Testcase } from "@oj2/contract" export type { ProblemTestCaseScore as Testcase } from "@oj2/contract"
/** /**
* 题目详情。以契约的 ProblemDetail 为准,只在这里补两处前端自己的窄化: * 题目详情。**直接取契约** —— `languages` / `template` 的收窄已经搬进
* - `languages` / `template` 的键窄化成 LANGUAGE组件按语言查模板要靠它 * `problemDetailSchema``z.array(problemLanguageSchema)` 与
* `z.partialRecord(problemLanguageSchema, …)`),这里原来那份
* `Omit<ProblemDetail, "languages" | "template"> & {...}` 与契约**双向可赋值**
* 即完全等价,是一层没有内容的重复(已用类型探针验证)。
*
* 原来它还多挂三个可选字段(`hasAstRules` / `visible` / `answers`)。它们在
* 契约里**本来就有**`hasAstRules` 在题目列表项上、`ProblemDetail` 带的是
* `astRequirements`),少数管理端调用点需要补充时应该就地声明自己的类型,
* 不该让一个全局别名对所有调用方声称这些字段存在。
*/ */
export type Problem = Omit<ProblemDetail, "languages" | "template"> & { export type Problem = ProblemDetail
languages: LANGUAGE[]
template: { [key in LANGUAGE]?: string }
// oj 侧不下发 astRules 原文,只有 astRequirements契约里就有
hasAstRules?: boolean
visible?: boolean
answers?: { language: LANGUAGE; code: string }[]
}
/** /**
* AST 代码要求。原来这里手抄的那份少了 label / exact / outer / inner —— * AST 代码要求。原来这里手抄的那份少了 label / exact / outer / inner ——
@@ -327,27 +328,16 @@ export type BlankContest = Omit<
> >
/** /**
* acm_contest_rank.submission_info 的 JSONB 内容。**键名保持 snake_case** —— * `acm_contest_rank.submission_info` 的 JSONB 内容。**形状已搬进契约**
* 判题按这套键名写进去,历史比赛的榜单行也是这个形状,不能跟着响应字段一起改名 * `contestSubmissionInfoSchema`),这里保留别名给既有调用点
*/ */
export interface SubmissionInfo { export type { ContestSubmissionInfo as SubmissionInfo } from "@oj2/contract"
is_ac: boolean
ac_time: number
is_first_ac: boolean
error_number: number
checked?: boolean
}
/** /**
* 榜单行。`submissionInfo` 的**内容**仍是 snake_case —— 它是 acm_contest_rank * 榜单行。`submissionInfo` 的收窄JSONB 原文的 snake_case 形状)已经搬进
* 表的 JSONB 原文,见 SubmissionInfo * `contestRankItemSchema`,这里不再需要 Omit + 覆盖
*/ */
export type ContestRank = Omit< export type ContestRank = import("@oj2/contract").ContestRankItem
import("@oj2/contract").ContestRankItem,
"submissionInfo"
> & {
submissionInfo: { [key: string]: SubmissionInfo }
}
export type { WebsiteConfig, OnlineCount } from "@oj2/contract" export type { WebsiteConfig, OnlineCount } from "@oj2/contract"
@@ -399,12 +389,12 @@ export type {
export type AnnouncementEdit = CreateAnnouncementRequest & { id: number } export type AnnouncementEdit = CreateAnnouncementRequest & { id: number }
/** /**
* 站内信。取契约的形状,只把 `submission` 换成前端窄化过的那个 * 站内信。**直接取契约** —— 原来这里把 `submission` 换成前端窄化过的
* statisticInfo / language 在契约里是 unknown见 EmbeddedSubmission)。 * `EmbeddedSubmission`,但那个窄化已经搬进 `embeddedSubmissionSchema`
* statisticInfo / language 在契约里不再是 unknown两者双向可赋值、
* 完全等价,这层 Omit 已经没有内容。
*/ */
export type Message = Omit<ContractMessage, "submission"> & { export type Message = ContractMessage
submission: EmbeddedSubmission
}
/** /**
* 题目表情。三个类型都直接取自契约 —— 语义 key 必须与后端 reaction/models.py * 题目表情。三个类型都直接取自契约 —— 语义 key 必须与后端 reaction/models.py

View File

@@ -29,13 +29,32 @@ export const contestPasswordRequestSchema = z.object({
export const contestAccessSchema = z.object({ access: z.boolean() }) export const contestAccessSchema = z.object({ access: z.boolean() })
export const contestProblemsSchema = z.array(z.union([problemListItemSchema, problemDetailSchema])) export const contestProblemsSchema = z.array(z.union([problemListItemSchema, problemDetailSchema]))
/**
* `acm_contest_rank.submission_info` 的 JSONB 原文。
*
* 键名是**判题链路写进去的 snake_case**(历史比赛的榜单行也是这个形状),
* 不要跟着响应字段一起改成 camelCase。字段全部可选只有真正提交过的题目键
* 才会出现,`checked` 更是前端在本地标「已看」时补的。
*
* 原来契约这里是 `z.record(z.string(), z.unknown())`,于是前端不得不
* 自己再声明一份 `SubmissionInfo` 去覆盖它utils/types 的 ContestRank
* 形状搬进来之后那个覆盖就没有内容了。
*/
export const contestSubmissionInfoSchema = z.object({
is_ac: z.boolean(),
ac_time: z.number(),
is_first_ac: z.boolean(),
error_number: z.number().int(),
checked: z.boolean().optional(),
})
export const contestRankItemSchema = z.object({ export const contestRankItemSchema = z.object({
id: z.number().int(), id: z.number().int(),
user: sampleUserSchema, user: sampleUserSchema,
submissionNumber: z.number().int(), submissionNumber: z.number().int(),
acceptedNumber: z.number().int(), acceptedNumber: z.number().int(),
totalTime: z.number().int(), totalTime: z.number().int(),
submissionInfo: z.record(z.string(), z.unknown()), submissionInfo: z.record(z.string(), contestSubmissionInfoSchema),
contestId: z.number().int(), contestId: z.number().int(),
}) })
@@ -46,6 +65,7 @@ export type ContestList = z.infer<typeof contestListSchema>
export type ContestRankItem = z.infer<typeof contestRankItemSchema> export type ContestRankItem = z.infer<typeof contestRankItemSchema>
export type ContestRank = z.infer<typeof contestRankSchema> export type ContestRank = z.infer<typeof contestRankSchema>
export type ContestAccess = z.infer<typeof contestAccessSchema> export type ContestAccess = z.infer<typeof contestAccessSchema>
export type ContestSubmissionInfo = z.infer<typeof contestSubmissionInfoSchema>
export type ContestStatus = z.infer<typeof contestStatusSchema> export type ContestStatus = z.infer<typeof contestStatusSchema>
export type ContestPasswordRequest = z.infer<typeof contestPasswordRequestSchema> export type ContestPasswordRequest = z.infer<typeof contestPasswordRequestSchema>