diff --git a/app/(admin)/(activity-management)/events/[id]/_components/event-details-card.tsx b/app/(admin)/(activity-management)/events/[id]/_components/event-details-card.tsx new file mode 100644 index 0000000..a1df8bf --- /dev/null +++ b/app/(admin)/(activity-management)/events/[id]/_components/event-details-card.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { useSuspenseQuery } from "@apollo/client/react"; +import { EVENT_BY_ID_QUERY } from "./query"; + +interface EventDetailsCardProps { + id: string; +} + +export function EventDetailsCard({ id }: EventDetailsCardProps) { + const { data } = useSuspenseQuery(EVENT_BY_ID_QUERY, { + variables: { id }, + }); + + const event = data?.event; + + if (!event) { + return ( + + + 事件詳情 + 查看事件的詳細資訊和負載資料 + + +

找不到事件記錄

+
+
+ ); + } + + let payloadData = null; + try { + payloadData = event.payload ? JSON.parse(event.payload) : null; + } catch { + // If payload is not valid JSON, treat as string + payloadData = event.payload; + } + + return ( + + + 事件詳情 + 查看事件的詳細資訊和負載資料 + + +
+

事件類型

+

{event.type}

+
+ +
+

觸發時間

+

+ {new Date(event.triggeredAt).toLocaleString("zh-tw")} +

+
+ + {payloadData && ( +
+

負載資料

+
+              
+                {typeof payloadData === "string"
+                  ? payloadData
+                  : JSON.stringify(payloadData, null, 2)}
+              
+            
+
+ )} +
+
+ ); +} diff --git a/app/(admin)/(activity-management)/events/[id]/_components/header.tsx b/app/(admin)/(activity-management)/events/[id]/_components/header.tsx new file mode 100644 index 0000000..f77fee6 --- /dev/null +++ b/app/(admin)/(activity-management)/events/[id]/_components/header.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { Badge } from "@/components/ui/badge"; +import { useSuspenseQuery } from "@apollo/client/react"; +import { EVENT_BY_ID_QUERY } from "./query"; + +interface HeaderProps { + id: string; +} + +export function Header({ id }: HeaderProps) { + const { data } = useSuspenseQuery(EVENT_BY_ID_QUERY, { + variables: { id }, + }); + + const event = data.event; + + return ( +
+

+ 事件 #{event.id} +

+
+ {event.type} + + 觸發時間:{new Date(event.triggeredAt).toLocaleString("zh-tw")} + +
+
+ ); +} diff --git a/app/(admin)/(activity-management)/events/[id]/_components/query.ts b/app/(admin)/(activity-management)/events/[id]/_components/query.ts new file mode 100644 index 0000000..32abdf4 --- /dev/null +++ b/app/(admin)/(activity-management)/events/[id]/_components/query.ts @@ -0,0 +1,16 @@ +import { graphql } from "@/gql"; + +export const EVENT_BY_ID_QUERY = graphql(` + query EventById($id: ID!) { + event(id: $id) { + id + user { + id + name + } + type + payload + triggeredAt + } + } +`); diff --git a/app/(admin)/(activity-management)/events/[id]/_components/user-card.tsx b/app/(admin)/(activity-management)/events/[id]/_components/user-card.tsx new file mode 100644 index 0000000..fe1fe2b --- /dev/null +++ b/app/(admin)/(activity-management)/events/[id]/_components/user-card.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { StyledLink } from "@/components/ui/link"; +import { useSuspenseQuery } from "@apollo/client/react"; +import { EVENT_BY_ID_QUERY } from "./query"; + +interface UserCardProps { + id: string; +} + +export function UserCard({ id }: UserCardProps) { + const { data } = useSuspenseQuery(EVENT_BY_ID_QUERY, { + variables: { id }, + }); + + const event = data.event; + + return ( + + + 使用者資訊 + 查看觸發此事件的使用者 + + +
+ {event.user.name} (#{event.user.id}) +
+
+ + 檢視使用者資訊 → + +
+
+
+ ); +} diff --git a/app/(admin)/(activity-management)/events/[id]/page.tsx b/app/(admin)/(activity-management)/events/[id]/page.tsx new file mode 100644 index 0000000..717323c --- /dev/null +++ b/app/(admin)/(activity-management)/events/[id]/page.tsx @@ -0,0 +1,40 @@ +import { SiteHeader } from "@/components/site-header"; +import { Suspense } from "react"; +import { EventDetailsCard } from "./_components/event-details-card"; +import { Header } from "./_components/header"; +import { UserCard } from "./_components/user-card"; + +export default async function EventPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + + return ( + <> + +
+
+
+
+
+ + + + +
+
+ + ); +} diff --git a/app/(admin)/(activity-management)/events/_components/data-table-columns.tsx b/app/(admin)/(activity-management)/events/_components/data-table-columns.tsx new file mode 100644 index 0000000..fc99058 --- /dev/null +++ b/app/(admin)/(activity-management)/events/_components/data-table-columns.tsx @@ -0,0 +1,91 @@ +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { StyledLink } from "@/components/ui/link"; +import type { ColumnDef } from "@tanstack/react-table"; +import { MoreHorizontal } from "lucide-react"; +import Link from "next/link"; + +export interface Event { + id: string; + user: { id: string; name: string }; + type: string; + triggeredAt: string; +} + +export const columns: ColumnDef[] = [ + { + accessorKey: "id", + header: "事件 ID", + cell: ({ row }) => { + const event = row.original; + return ( + + {event.id} + + ); + }, + }, + { + accessorKey: "user.id", + header: "使用者", + cell: ({ row }) => { + const userId = row.original.user.id; + const userName = row.original.user.name; + + return ( + + {userName} (#{userId}) + + ); + }, + }, + { + accessorKey: "type", + header: "事件類型", + cell: ({ row }) => { + const type = row.original.type; + return {type}; + }, + }, + { + accessorKey: "triggeredAt", + header: "觸發時間", + cell: ({ row }) => { + const triggeredAt = new Date(row.original.triggeredAt); + return
{triggeredAt.toLocaleString("zh-tw")}
; + }, + }, + { + id: "actions", + cell: ({ row }) => { + return ( + + + + + + 動作 + + 檢視事件詳情 + + + + 檢視使用者 + + + + ); + }, + }, +]; diff --git a/app/(admin)/(activity-management)/events/_components/data-table.tsx b/app/(admin)/(activity-management)/events/_components/data-table.tsx new file mode 100644 index 0000000..4bdfece --- /dev/null +++ b/app/(admin)/(activity-management)/events/_components/data-table.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { CursorDataTable } from "@/components/data-table/cursor"; +import type { Direction } from "@/components/data-table/pagination"; +import { useSuspenseQuery } from "@apollo/client/react"; +import { useState } from "react"; +import { columns, type Event } from "./data-table-columns"; +import { EVENTS_TABLE_QUERY } from "./query"; + +export function EventsDataTable() { + const PAGE_SIZE = 10; + const [after, setAfter] = useState(null); + const [before, setBefore] = useState(null); + const [direction, setDirection] = useState("backward"); + + const variables = direction === "backward" + ? { first: PAGE_SIZE, after, last: undefined, before: undefined } + : { last: PAGE_SIZE, before, first: undefined, after: undefined }; + + const { data } = useSuspenseQuery(EVENTS_TABLE_QUERY, { + variables, + }); + + const eventList = data?.events.edges + ?.map((edge) => { + const event = edge?.node; + if (!event) return null; + return { + id: event.id, + user: { + id: event.user.id, + name: event.user.name, + }, + type: event.type, + triggeredAt: event.triggeredAt, + } satisfies Event; + }) + .filter((event) => event !== null) ?? []; + + const pageInfo = data?.events.pageInfo; + + const handlePageChange = (direction: Direction) => { + if (!pageInfo) return; + if (direction === "forward" && pageInfo.hasNextPage) { + setAfter(pageInfo.endCursor ?? null); + setBefore(null); + setDirection("forward"); + } else if (direction === "backward" && pageInfo.hasPreviousPage) { + setBefore(pageInfo.startCursor ?? null); + setAfter(null); + setDirection("backward"); + } + }; + + return ( + + ); +} diff --git a/app/(admin)/(activity-management)/events/_components/query.ts b/app/(admin)/(activity-management)/events/_components/query.ts new file mode 100644 index 0000000..8c0db5f --- /dev/null +++ b/app/(admin)/(activity-management)/events/_components/query.ts @@ -0,0 +1,31 @@ +import { graphql } from "@/gql"; + +export const EVENTS_TABLE_QUERY = graphql(` + query EventsTable( + $first: Int + $after: Cursor + $last: Int + $before: Cursor + ) { + events(first: $first, after: $after, last: $last, before: $before, orderBy: { field: TRIGGERED_AT, direction: DESC }) { + edges { + node { + id + user { + id + name + } + type + triggeredAt + } + } + totalCount + pageInfo { + hasNextPage + hasPreviousPage + endCursor + startCursor + } + } + } +`); diff --git a/app/(admin)/(activity-management)/events/page.tsx b/app/(admin)/(activity-management)/events/page.tsx new file mode 100644 index 0000000..ae36676 --- /dev/null +++ b/app/(admin)/(activity-management)/events/page.tsx @@ -0,0 +1,26 @@ +import { SiteHeader } from "@/components/site-header"; +import { EventsDataTable } from "./_components/data-table"; + +export default function Page() { + return ( + <> + +
+
+
+

事件管理

+

查看和管理系統事件記錄。

+
+
+
+ +
+
+ + ); +} diff --git a/app/(admin)/(activity-management)/points/[id]/_components/header.tsx b/app/(admin)/(activity-management)/points/[id]/_components/header.tsx new file mode 100644 index 0000000..0213362 --- /dev/null +++ b/app/(admin)/(activity-management)/points/[id]/_components/header.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { Badge } from "@/components/ui/badge"; +import { useSuspenseQuery } from "@apollo/client/react"; +import { POINT_BY_ID_QUERY } from "./query"; + +interface HeaderProps { + id: string; +} + +export function Header({ id }: HeaderProps) { + const { data } = useSuspenseQuery(POINT_BY_ID_QUERY, { + variables: { id }, + }); + + const point = data.pointGrant; + + const isPositive = point.points >= 0; + + return ( +
+

+ 積分記錄 #{point.id} +

+
+ + {isPositive ? "+" : ""} + {point.points} 積分 + + + 獲得時間:{new Date(point.grantedAt).toLocaleString("zh-tw")} + +
+
+ ); +} diff --git a/app/(admin)/(activity-management)/points/[id]/_components/point-details-card.tsx b/app/(admin)/(activity-management)/points/[id]/_components/point-details-card.tsx new file mode 100644 index 0000000..088b6c8 --- /dev/null +++ b/app/(admin)/(activity-management)/points/[id]/_components/point-details-card.tsx @@ -0,0 +1,54 @@ +"use client"; + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { useSuspenseQuery } from "@apollo/client/react"; +import { POINT_BY_ID_QUERY } from "./query"; + +interface PointDetailsCardProps { + id: string; +} + +export function PointDetailsCard({ id }: PointDetailsCardProps) { + const { data } = useSuspenseQuery(POINT_BY_ID_QUERY, { + variables: { id }, + }); + + const point = data.pointGrant; + + const isPositive = point.points >= 0; + + return ( + + + 積分詳情 + 查看積分獲得的詳細資訊 + + +
+

積分數量

+
+ {isPositive ? "+" : ""} + {point.points} +
+
+ +
+

描述

+

{point.description}

+
+ +
+

獲得時間

+

+ {new Date(point.grantedAt).toLocaleString("zh-tw")} +

+
+
+
+ ); +} diff --git a/app/(admin)/(activity-management)/points/[id]/_components/query.ts b/app/(admin)/(activity-management)/points/[id]/_components/query.ts new file mode 100644 index 0000000..04c539b --- /dev/null +++ b/app/(admin)/(activity-management)/points/[id]/_components/query.ts @@ -0,0 +1,16 @@ +import { graphql } from "@/gql"; + +export const POINT_BY_ID_QUERY = graphql(` + query PointById($id: ID!) { + pointGrant(id: $id) { + id + user { + id + name + } + points + description + grantedAt + } + } +`); diff --git a/app/(admin)/(activity-management)/points/[id]/_components/user-card.tsx b/app/(admin)/(activity-management)/points/[id]/_components/user-card.tsx new file mode 100644 index 0000000..12828ef --- /dev/null +++ b/app/(admin)/(activity-management)/points/[id]/_components/user-card.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { StyledLink } from "@/components/ui/link"; +import { useSuspenseQuery } from "@apollo/client/react"; +import { POINT_BY_ID_QUERY } from "./query"; + +interface UserCardProps { + id: string; +} + +export function UserCard({ id }: UserCardProps) { + const { data } = useSuspenseQuery(POINT_BY_ID_QUERY, { + variables: { id }, + }); + + const point = data.pointGrant; + + return ( + + + 使用者資訊 + 查看獲得此積分的使用者 + + +
+ {point.user.name} (#{point.user.id}) +
+
+ + 檢視使用者資訊 → + +
+
+
+ ); +} diff --git a/app/(admin)/(activity-management)/points/[id]/page.tsx b/app/(admin)/(activity-management)/points/[id]/page.tsx new file mode 100644 index 0000000..fd85db1 --- /dev/null +++ b/app/(admin)/(activity-management)/points/[id]/page.tsx @@ -0,0 +1,40 @@ +import { SiteHeader } from "@/components/site-header"; +import { Suspense } from "react"; +import { Header } from "./_components/header"; +import { PointDetailsCard } from "./_components/point-details-card"; +import { UserCard } from "./_components/user-card"; + +export default async function PointPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + + return ( + <> + +
+
+
+
+
+ + + + +
+
+ + ); +} diff --git a/app/(admin)/(activity-management)/points/_components/data-table-columns.tsx b/app/(admin)/(activity-management)/points/_components/data-table-columns.tsx new file mode 100644 index 0000000..3b464f4 --- /dev/null +++ b/app/(admin)/(activity-management)/points/_components/data-table-columns.tsx @@ -0,0 +1,111 @@ +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { StyledLink } from "@/components/ui/link"; +import type { ColumnDef } from "@tanstack/react-table"; +import { MoreHorizontal } from "lucide-react"; +import Link from "next/link"; + +export interface Point { + id: string; + user: { id: string; name: string }; + points: number; + description: string; + grantedAt: string; +} + +export const columns: ColumnDef[] = [ + { + accessorKey: "id", + header: "記錄 ID", + cell: ({ row }) => { + const point = row.original; + return ( + + {point.id} + + ); + }, + }, + { + accessorKey: "user.id", + header: "使用者", + cell: ({ row }) => { + const userId = row.original.user.id; + const userName = row.original.user.name; + return ( + + {userName} (#{userId}) + + ); + }, + }, + { + accessorKey: "points", + header: "積分", + cell: ({ row }) => { + const points = row.original.points; + const isPositive = points >= 0; + return ( + + {isPositive ? "+" : ""} + {points} + + ); + }, + }, + { + accessorKey: "description", + header: "描述", + cell: ({ row }) => { + const description = row.original.description; + return ( +
+
+ {description} +
+
+ ); + }, + }, + { + accessorKey: "grantedAt", + header: "獲得時間", + cell: ({ row }) => { + const grantedAt = new Date(row.original.grantedAt); + return
{grantedAt.toLocaleString("zh-tw")}
; + }, + }, + { + id: "actions", + cell: ({ row }) => { + return ( + + + + + + 動作 + + 檢視積分記錄 + + + + 檢視使用者 + + + + ); + }, + }, +]; diff --git a/app/(admin)/(activity-management)/points/_components/data-table.tsx b/app/(admin)/(activity-management)/points/_components/data-table.tsx new file mode 100644 index 0000000..5df8679 --- /dev/null +++ b/app/(admin)/(activity-management)/points/_components/data-table.tsx @@ -0,0 +1,66 @@ +"use client"; + +import { CursorDataTable } from "@/components/data-table/cursor"; +import type { Direction } from "@/components/data-table/pagination"; +import { useSuspenseQuery } from "@apollo/client/react"; +import { useState } from "react"; +import { columns, type Point } from "./data-table-columns"; +import { POINTS_TABLE_QUERY } from "./query"; + +export function PointsDataTable() { + const PAGE_SIZE = 10; + const [after, setAfter] = useState(null); + const [before, setBefore] = useState(null); + const [direction, setDirection] = useState("backward"); + + const variables = direction === "backward" + ? { first: PAGE_SIZE, after, last: undefined, before: undefined } + : { last: PAGE_SIZE, before, first: undefined, after: undefined }; + + const { data } = useSuspenseQuery(POINTS_TABLE_QUERY, { + variables, + }); + + const pointsList = data?.points.edges + ?.map((edge) => { + const point = edge?.node; + if (!point) return null; + return { + id: point.id, + user: { + id: point.user.id, + name: point.user.name, + }, + points: point.points, + description: point.description ?? "", + grantedAt: point.grantedAt, + } satisfies Point; + }) + .filter((point) => point !== null) ?? []; + + const pageInfo = data?.points.pageInfo; + + const handlePageChange = (direction: Direction) => { + if (!pageInfo) return; + if (direction === "forward" && pageInfo.hasNextPage) { + setAfter(pageInfo.endCursor ?? null); + setBefore(null); + setDirection("forward"); + } else if (direction === "backward" && pageInfo.hasPreviousPage) { + setBefore(pageInfo.startCursor ?? null); + setAfter(null); + setDirection("backward"); + } + }; + + return ( + + ); +} diff --git a/app/(admin)/(activity-management)/points/_components/query.ts b/app/(admin)/(activity-management)/points/_components/query.ts new file mode 100644 index 0000000..948b2e9 --- /dev/null +++ b/app/(admin)/(activity-management)/points/_components/query.ts @@ -0,0 +1,32 @@ +import { graphql } from "@/gql"; + +export const POINTS_TABLE_QUERY = graphql(` + query PointsTable( + $first: Int + $after: Cursor + $last: Int + $before: Cursor + ) { + points(first: $first, after: $after, last: $last, before: $before, orderBy: { field: GRANTED_AT, direction: DESC }) { + edges { + node { + id + user { + id + name + } + points + description + grantedAt + } + } + totalCount + pageInfo { + hasNextPage + hasPreviousPage + endCursor + startCursor + } + } + } +`); diff --git a/app/(admin)/(activity-management)/points/page.tsx b/app/(admin)/(activity-management)/points/page.tsx new file mode 100644 index 0000000..6368de7 --- /dev/null +++ b/app/(admin)/(activity-management)/points/page.tsx @@ -0,0 +1,26 @@ +import { SiteHeader } from "@/components/site-header"; +import { PointsDataTable } from "./_components/data-table"; + +export default function Page() { + return ( + <> + +
+
+
+

積分管理

+

查看和管理使用者的積分獲得記錄。

+
+
+
+ +
+
+ + ); +} diff --git a/app/(admin)/(activity-management)/submissions/[id]/_components/header.tsx b/app/(admin)/(activity-management)/submissions/[id]/_components/header.tsx new file mode 100644 index 0000000..d2781a1 --- /dev/null +++ b/app/(admin)/(activity-management)/submissions/[id]/_components/header.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { Badge } from "@/components/ui/badge"; +import { SubmissionStatus } from "@/gql/graphql"; +import { useSuspenseQuery } from "@apollo/client/react"; +import { SUBMISSION_BY_ID_QUERY } from "./query"; + +interface HeaderProps { + id: string; +} + +const statusMap: Record< + SubmissionStatus, + { label: string; variant: "default" | "secondary" | "destructive" | "outline" } +> = { + [SubmissionStatus.Success]: { label: "成功", variant: "default" }, + [SubmissionStatus.Failed]: { label: "錯誤", variant: "destructive" }, + [SubmissionStatus.Pending]: { label: "處理中", variant: "secondary" }, +}; + +export function Header({ id }: HeaderProps) { + const { data } = useSuspenseQuery(SUBMISSION_BY_ID_QUERY, { + variables: { id }, + }); + + const submission = data.submission; + const statusInfo = statusMap[submission.status] || { label: submission.status, variant: "outline" as const }; + + return ( +
+

+ 提交記錄 #{submission.id} +

+
+ {statusInfo.label} + + 提交時間:{new Date(submission.submittedAt).toLocaleString("zh-tw")} + +
+
+ ); +} diff --git a/app/(admin)/(activity-management)/submissions/[id]/_components/query.ts b/app/(admin)/(activity-management)/submissions/[id]/_components/query.ts new file mode 100644 index 0000000..d16051d --- /dev/null +++ b/app/(admin)/(activity-management)/submissions/[id]/_components/query.ts @@ -0,0 +1,25 @@ +import { graphql } from "@/gql"; + +export const SUBMISSION_BY_ID_QUERY = graphql(` + query SubmissionById($id: ID!) { + submission(id: $id) { + id + user { + id + name + } + queryResult { + columns + rows + matchAnswer + } + question { + id + } + error + submittedCode + status + submittedAt + } + } +`); diff --git a/app/(admin)/(activity-management)/submissions/[id]/_components/result-card.tsx b/app/(admin)/(activity-management)/submissions/[id]/_components/result-card.tsx new file mode 100644 index 0000000..2f3b0c8 --- /dev/null +++ b/app/(admin)/(activity-management)/submissions/[id]/_components/result-card.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { StyledLink } from "@/components/ui/link"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { useSuspenseQuery } from "@apollo/client/react"; +import { AlertTriangle } from "lucide-react"; +import { SUBMISSION_BY_ID_QUERY } from "./query"; + +interface ResultCardProps { + id: string; +} + +export function ResultCard({ id }: ResultCardProps) { + const { data } = useSuspenseQuery(SUBMISSION_BY_ID_QUERY, { + variables: { id }, + }); + + const submission = data.submission; + const question = submission.question; + const queryResult = submission.queryResult; + + if (!queryResult) { + return null; + } + + const { columns, rows } = queryResult; + + return ( + + + 查詢結果 + 查看查詢執行的結果 + + + {!queryResult.matchAnswer && ( + + + + 和正確答案不一致 + + + 您可以到原始問題中取得正確答案應該輸出的結果。 + 原始問題 → + + + )} + + {columns.length === 0 || rows.length === 0 + ?

查詢沒有回傳結果

+ : ( +
+ + + + {columns.map((column, index) => {column})} + + + + {rows.map((row, rowIndex) => ( + + {row.map((cell, cellIndex) => ( + + {cell == null + ? ( + + NULL + + ) + : ( + String(cell) + )} + + ))} + + ))} + +
+
+ )} +
+
+ ); +} diff --git a/app/(admin)/(activity-management)/submissions/[id]/_components/submission-details-card.tsx b/app/(admin)/(activity-management)/submissions/[id]/_components/submission-details-card.tsx new file mode 100644 index 0000000..e16f480 --- /dev/null +++ b/app/(admin)/(activity-management)/submissions/[id]/_components/submission-details-card.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { useSuspenseQuery } from "@apollo/client/react"; +import { SUBMISSION_BY_ID_QUERY } from "./query"; + +interface SubmissionDetailsCardProps { + id: string; +} + +export function SubmissionDetailsCard({ id }: SubmissionDetailsCardProps) { + const { data } = useSuspenseQuery(SUBMISSION_BY_ID_QUERY, { + variables: { id }, + }); + + const submission = data.submission; + + return ( + + + 提交詳情 + 查看提交的程式碼和錯誤資訊 + + +
+

提交的程式碼

+
+            {submission.submittedCode}
+          
+
+ + {submission.error && ( +
+

錯誤訊息

+
+              {submission.error}
+            
+
+ )} +
+
+ ); +} diff --git a/app/(admin)/(activity-management)/submissions/[id]/_components/user-card.tsx b/app/(admin)/(activity-management)/submissions/[id]/_components/user-card.tsx new file mode 100644 index 0000000..1cc78fd --- /dev/null +++ b/app/(admin)/(activity-management)/submissions/[id]/_components/user-card.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { StyledLink } from "@/components/ui/link"; +import { useSuspenseQuery } from "@apollo/client/react"; +import { SUBMISSION_BY_ID_QUERY } from "./query"; + +interface UserCardProps { + id: string; +} + +export function UserCard({ id }: UserCardProps) { + const { data } = useSuspenseQuery(SUBMISSION_BY_ID_QUERY, { + variables: { id }, + }); + + const submission = data.submission; + + return ( + + + 使用者資訊 + 查看提交此查詢的使用者 + + +
+ {submission.user.name} (#{submission.user.id}) +
+
+ + 檢視使用者資訊 → + +
+
+
+ ); +} diff --git a/app/(admin)/(activity-management)/submissions/[id]/page.tsx b/app/(admin)/(activity-management)/submissions/[id]/page.tsx new file mode 100644 index 0000000..e6ef914 --- /dev/null +++ b/app/(admin)/(activity-management)/submissions/[id]/page.tsx @@ -0,0 +1,42 @@ +import { SiteHeader } from "@/components/site-header"; +import { Suspense } from "react"; +import { Header } from "./_components/header"; +import { ResultCard } from "./_components/result-card"; +import { SubmissionDetailsCard } from "./_components/submission-details-card"; +import { UserCard } from "./_components/user-card"; + +export default async function SubmissionPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + + return ( + <> + +
+
+
+
+
+ + + + + +
+
+ + ); +} diff --git a/app/(admin)/(activity-management)/submissions/_components/data-table-columns.tsx b/app/(admin)/(activity-management)/submissions/_components/data-table-columns.tsx new file mode 100644 index 0000000..053c664 --- /dev/null +++ b/app/(admin)/(activity-management)/submissions/_components/data-table-columns.tsx @@ -0,0 +1,123 @@ +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { StyledLink } from "@/components/ui/link"; +import { SubmissionStatus } from "@/gql/graphql"; +import type { ColumnDef } from "@tanstack/react-table"; +import { MoreHorizontal } from "lucide-react"; +import Link from "next/link"; + +export interface Submission { + id: string; + submittedCode: string; + status: SubmissionStatus; + user: { + id: string; + name: string; + }; + question: { + id: string; + title: string; + }; +} + +const statusMap: Record< + SubmissionStatus, + { label: string; variant: "default" | "secondary" | "destructive" | "outline" } +> = { + [SubmissionStatus.Success]: { label: "成功", variant: "default" }, + [SubmissionStatus.Failed]: { label: "錯誤", variant: "destructive" }, + [SubmissionStatus.Pending]: { label: "處理中", variant: "secondary" }, +}; + +export const columns: ColumnDef[] = [ + { + accessorKey: "id", + header: "ID", + cell: ({ row }) => { + const submission = row.original; + return ( + + {submission.id} + + ); + }, + }, + { + accessorKey: "user.id", + header: "使用者", + cell: ({ row }) => { + const userId = row.original.user.id; + const userName = row.original.user.name; + return ( + + {userName} (#{userId}) + + ); + }, + }, + { + accessorKey: "question.title", + header: "題目", + cell: ({ row }) => { + const question = row.original.question; + return {question.title}; + }, + }, + { + accessorKey: "status", + header: "狀態", + cell: ({ row }) => { + const status = row.original.status; + const statusInfo = statusMap[status] || { label: status, variant: "outline" as const }; + return {statusInfo.label}; + }, + }, + { + accessorKey: "submittedCode", + header: "提交程式碼", + cell: ({ row }) => { + const code = row.original.submittedCode; + return ( +
+ {code} +
+ ); + }, + }, + { + id: "actions", + cell: ({ row }) => { + return ( + + + + + + 動作 + + 檢視提交記錄 + + + + 檢視使用者 + + + 檢視題目 + + + + ); + }, + }, +]; diff --git a/app/(admin)/(activity-management)/submissions/_components/data-table.tsx b/app/(admin)/(activity-management)/submissions/_components/data-table.tsx new file mode 100644 index 0000000..76b7b44 --- /dev/null +++ b/app/(admin)/(activity-management)/submissions/_components/data-table.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { CursorDataTable } from "@/components/data-table/cursor"; +import type { Direction } from "@/components/data-table/pagination"; +import { useSuspenseQuery } from "@apollo/client/react"; +import { useState } from "react"; +import { columns, type Submission } from "./data-table-columns"; +import { SUBMISSIONS_TABLE_QUERY } from "./query"; + +export function SubmissionsDataTable() { + const PAGE_SIZE = 10; + const [after, setAfter] = useState(null); + const [before, setBefore] = useState(null); + const [direction, setDirection] = useState("backward"); + + const variables = direction === "backward" + ? { first: PAGE_SIZE, after, last: undefined, before: undefined } + : { last: PAGE_SIZE, before, first: undefined, after: undefined }; + + const { data } = useSuspenseQuery(SUBMISSIONS_TABLE_QUERY, { + variables, + }); + + const submissionList = data?.submissions.edges + ?.map((edge) => { + const submission = edge?.node; + if (!submission) return null; + return { + id: submission.id, + submittedCode: submission.submittedCode, + status: submission.status, + user: { + id: submission.user.id, + name: submission.user.name, + }, + question: { + id: submission.question.id, + title: submission.question.title, + }, + } satisfies Submission; + }) + .filter((submission) => submission !== null) ?? []; + + const pageInfo = data?.submissions.pageInfo; + + const handlePageChange = (direction: Direction) => { + if (!pageInfo) return; + if (direction === "forward" && pageInfo.hasNextPage) { + setAfter(pageInfo.endCursor ?? null); + setBefore(null); + setDirection("forward"); + } else if (direction === "backward" && pageInfo.hasPreviousPage) { + setBefore(pageInfo.startCursor ?? null); + setAfter(null); + setDirection("backward"); + } + }; + + return ( + + ); +} diff --git a/app/(admin)/(activity-management)/submissions/_components/query.ts b/app/(admin)/(activity-management)/submissions/_components/query.ts new file mode 100644 index 0000000..e75ca41 --- /dev/null +++ b/app/(admin)/(activity-management)/submissions/_components/query.ts @@ -0,0 +1,35 @@ +import { graphql } from "@/gql"; + +export const SUBMISSIONS_TABLE_QUERY = graphql(` + query SubmissionsTable( + $first: Int + $after: Cursor + $last: Int + $before: Cursor + ) { + submissions(first: $first, after: $after, last: $last, before: $before, orderBy: { field: SUBMITTED_AT, direction: DESC }) { + edges { + node { + id + submittedCode + status + user { + id + name + } + question { + id + title + } + } + } + totalCount + pageInfo { + hasNextPage + hasPreviousPage + endCursor + startCursor + } + } + } +`); diff --git a/app/(admin)/(activity-management)/submissions/page.tsx b/app/(admin)/(activity-management)/submissions/page.tsx new file mode 100644 index 0000000..7e5fe9f --- /dev/null +++ b/app/(admin)/(activity-management)/submissions/page.tsx @@ -0,0 +1,26 @@ +import { SiteHeader } from "@/components/site-header"; +import { SubmissionsDataTable } from "./_components/data-table"; + +export default function Page() { + return ( + <> + +
+
+
+

提交記錄管理

+

查看和管理使用者的查詢提交記錄。

+
+
+
+ +
+
+ + ); +} diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx index 85304d0..8e8a691 100644 --- a/components/app-sidebar.tsx +++ b/components/app-sidebar.tsx @@ -1,6 +1,6 @@ "use client"; -import { Book, Code, LibrarySquare, type LucideIcon, SquareUser } from "lucide-react"; +import { Activity, Book, Code, Coins, LibrarySquare, type LucideIcon, Send, SquareUser } from "lucide-react"; import * as React from "react"; import { NavMain } from "@/components/nav-main"; @@ -50,48 +50,76 @@ const isUserManagement = (pathname: string) => const buildNavbar = ( pathname: string, ): { - navMain: NavItem[]; + navMain: { group: string; items: NavItem[] }[]; navSecondary: NavItem[]; } => ({ navMain: [ { - title: "使用者管理", - url: "/users", - icon: SquareUser, - isActive: isUserManagement(pathname), + group: "資料管理", items: [ { - title: "使用者", + title: "使用者管理", url: "/users", - isActive: pathname.startsWith("/users"), + icon: SquareUser, + isActive: isUserManagement(pathname), + items: [ + { + title: "使用者", + url: "/users", + isActive: pathname.startsWith("/users"), + }, + { + title: "群組", + url: "/groups", + isActive: pathname.startsWith("/groups"), + }, + { + title: "權限集", + url: "/scopesets", + isActive: pathname.startsWith("/scopesets"), + }, + ], }, { - title: "群組", - url: "/groups", - isActive: pathname.startsWith("/groups"), - }, - { - title: "權限集", - url: "/scopesets", - isActive: pathname.startsWith("/scopesets"), + title: "題庫管理", + url: "/questions", + icon: LibrarySquare, + isActive: pathname.startsWith("/questions") || pathname.startsWith("/database"), + items: [ + { + title: "題庫", + url: "/questions", + isActive: pathname.startsWith("/questions"), + }, + { + title: "資料庫", + url: "/database", + isActive: pathname.startsWith("/database"), + }, + ], }, ], }, { - title: "題庫管理", - url: "/questions", - icon: LibrarySquare, - isActive: pathname.startsWith("/questions") || pathname.startsWith("/database"), + group: "系統操作動態", items: [ { - title: "題庫", - url: "/questions", - isActive: pathname.startsWith("/questions"), + title: "提交記錄", + url: "/submissions", + icon: Send, + isActive: pathname.startsWith("/submissions"), + }, + { + title: "事件記錄", + url: "/events", + icon: Activity, + isActive: pathname.startsWith("/events"), }, { - title: "資料庫", - url: "/database", - isActive: pathname.startsWith("/database"), + title: "積分記錄", + url: "/points", + icon: Coins, + isActive: pathname.startsWith("/points"), }, ], }, @@ -139,7 +167,7 @@ export function AppSidebar({ ...props }: React.ComponentProps) { - + {data.navMain.map((group) => )} diff --git a/components/nav-main.tsx b/components/nav-main.tsx index 7adebc5..b71f307 100644 --- a/components/nav-main.tsx +++ b/components/nav-main.tsx @@ -19,17 +19,19 @@ import type { NavItem } from "./app-sidebar"; export function NavMain({ items, + groupLabel, }: { items: NavItem[]; + groupLabel?: string; }) { return ( - 資料管理 + {groupLabel || "資料管理"} {items.map((item) => ( - + {item.title} diff --git a/gql/gql.ts b/gql/gql.ts index 5a28ea3..57a6d37 100644 --- a/gql/gql.ts +++ b/gql/gql.ts @@ -14,6 +14,12 @@ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document- * Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size */ type Documents = { + "\n query EventById($id: ID!) {\n event(id: $id) {\n id\n user {\n id\n name\n }\n type\n payload\n triggeredAt\n }\n }\n": typeof types.EventByIdDocument, + "\n query EventsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n events(first: $first, after: $after, last: $last, before: $before, orderBy: { field: TRIGGERED_AT, direction: DESC }) {\n edges {\n node {\n id\n user {\n id\n name\n }\n type\n triggeredAt\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n": typeof types.EventsTableDocument, + "\n query PointById($id: ID!) {\n pointGrant(id: $id) {\n id\n user {\n id\n name\n }\n points\n description\n grantedAt\n }\n }\n": typeof types.PointByIdDocument, + "\n query PointsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n points(first: $first, after: $after, last: $last, before: $before, orderBy: { field: GRANTED_AT, direction: DESC }) {\n edges {\n node {\n id\n user {\n id\n name\n }\n points\n description\n grantedAt\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n": typeof types.PointsTableDocument, + "\n query SubmissionById($id: ID!) {\n submission(id: $id) {\n id\n user {\n id\n name\n }\n queryResult {\n columns\n rows\n matchAnswer\n }\n question {\n id\n }\n error\n submittedCode\n status\n submittedAt\n }\n }\n": typeof types.SubmissionByIdDocument, + "\n query SubmissionsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n submissions(first: $first, after: $after, last: $last, before: $before, orderBy: { field: SUBMITTED_AT, direction: DESC }) {\n edges {\n node {\n id\n submittedCode\n status\n user {\n id\n name\n }\n question {\n id\n title\n }\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n": typeof types.SubmissionsTableDocument, "\n query DatabaseDetail($id: ID!) {\n database(id: $id) {\n id\n slug\n description\n schema\n relationFigure\n }\n }\n": typeof types.DatabaseDetailDocument, "\n mutation CreateDatabase($input: CreateDatabaseInput!) {\n createDatabase(input: $input) {\n id\n }\n }\n": typeof types.CreateDatabaseDocument, "\n mutation UpdateDatabase($id: ID!, $input: UpdateDatabaseInput!) {\n updateDatabase(id: $id, input: $input) {\n id\n }\n }\n": typeof types.UpdateDatabaseDocument, @@ -61,6 +67,12 @@ type Documents = { "\n query BasicUserInfo {\n me {\n id\n name\n email\n avatar\n\n group {\n name\n }\n }\n }\n": typeof types.BasicUserInfoDocument, }; const documents: Documents = { + "\n query EventById($id: ID!) {\n event(id: $id) {\n id\n user {\n id\n name\n }\n type\n payload\n triggeredAt\n }\n }\n": types.EventByIdDocument, + "\n query EventsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n events(first: $first, after: $after, last: $last, before: $before, orderBy: { field: TRIGGERED_AT, direction: DESC }) {\n edges {\n node {\n id\n user {\n id\n name\n }\n type\n triggeredAt\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n": types.EventsTableDocument, + "\n query PointById($id: ID!) {\n pointGrant(id: $id) {\n id\n user {\n id\n name\n }\n points\n description\n grantedAt\n }\n }\n": types.PointByIdDocument, + "\n query PointsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n points(first: $first, after: $after, last: $last, before: $before, orderBy: { field: GRANTED_AT, direction: DESC }) {\n edges {\n node {\n id\n user {\n id\n name\n }\n points\n description\n grantedAt\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n": types.PointsTableDocument, + "\n query SubmissionById($id: ID!) {\n submission(id: $id) {\n id\n user {\n id\n name\n }\n queryResult {\n columns\n rows\n matchAnswer\n }\n question {\n id\n }\n error\n submittedCode\n status\n submittedAt\n }\n }\n": types.SubmissionByIdDocument, + "\n query SubmissionsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n submissions(first: $first, after: $after, last: $last, before: $before, orderBy: { field: SUBMITTED_AT, direction: DESC }) {\n edges {\n node {\n id\n submittedCode\n status\n user {\n id\n name\n }\n question {\n id\n title\n }\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n": types.SubmissionsTableDocument, "\n query DatabaseDetail($id: ID!) {\n database(id: $id) {\n id\n slug\n description\n schema\n relationFigure\n }\n }\n": types.DatabaseDetailDocument, "\n mutation CreateDatabase($input: CreateDatabaseInput!) {\n createDatabase(input: $input) {\n id\n }\n }\n": types.CreateDatabaseDocument, "\n mutation UpdateDatabase($id: ID!, $input: UpdateDatabaseInput!) {\n updateDatabase(id: $id, input: $input) {\n id\n }\n }\n": types.UpdateDatabaseDocument, @@ -122,6 +134,30 @@ const documents: Documents = { */ export function graphql(source: string): unknown; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "\n query EventById($id: ID!) {\n event(id: $id) {\n id\n user {\n id\n name\n }\n type\n payload\n triggeredAt\n }\n }\n"): (typeof documents)["\n query EventById($id: ID!) {\n event(id: $id) {\n id\n user {\n id\n name\n }\n type\n payload\n triggeredAt\n }\n }\n"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "\n query EventsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n events(first: $first, after: $after, last: $last, before: $before, orderBy: { field: TRIGGERED_AT, direction: DESC }) {\n edges {\n node {\n id\n user {\n id\n name\n }\n type\n triggeredAt\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n"): (typeof documents)["\n query EventsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n events(first: $first, after: $after, last: $last, before: $before, orderBy: { field: TRIGGERED_AT, direction: DESC }) {\n edges {\n node {\n id\n user {\n id\n name\n }\n type\n triggeredAt\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "\n query PointById($id: ID!) {\n pointGrant(id: $id) {\n id\n user {\n id\n name\n }\n points\n description\n grantedAt\n }\n }\n"): (typeof documents)["\n query PointById($id: ID!) {\n pointGrant(id: $id) {\n id\n user {\n id\n name\n }\n points\n description\n grantedAt\n }\n }\n"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "\n query PointsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n points(first: $first, after: $after, last: $last, before: $before, orderBy: { field: GRANTED_AT, direction: DESC }) {\n edges {\n node {\n id\n user {\n id\n name\n }\n points\n description\n grantedAt\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n"): (typeof documents)["\n query PointsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n points(first: $first, after: $after, last: $last, before: $before, orderBy: { field: GRANTED_AT, direction: DESC }) {\n edges {\n node {\n id\n user {\n id\n name\n }\n points\n description\n grantedAt\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "\n query SubmissionById($id: ID!) {\n submission(id: $id) {\n id\n user {\n id\n name\n }\n queryResult {\n columns\n rows\n matchAnswer\n }\n question {\n id\n }\n error\n submittedCode\n status\n submittedAt\n }\n }\n"): (typeof documents)["\n query SubmissionById($id: ID!) {\n submission(id: $id) {\n id\n user {\n id\n name\n }\n queryResult {\n columns\n rows\n matchAnswer\n }\n question {\n id\n }\n error\n submittedCode\n status\n submittedAt\n }\n }\n"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "\n query SubmissionsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n submissions(first: $first, after: $after, last: $last, before: $before, orderBy: { field: SUBMITTED_AT, direction: DESC }) {\n edges {\n node {\n id\n submittedCode\n status\n user {\n id\n name\n }\n question {\n id\n title\n }\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n"): (typeof documents)["\n query SubmissionsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n submissions(first: $first, after: $after, last: $last, before: $before, orderBy: { field: SUBMITTED_AT, direction: DESC }) {\n edges {\n node {\n id\n submittedCode\n status\n user {\n id\n name\n }\n question {\n id\n title\n }\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/gql/graphql.ts b/gql/graphql.ts index 5a4313c..48e988e 100644 --- a/gql/graphql.ts +++ b/gql/graphql.ts @@ -1,7 +1,7 @@ /* eslint-disable */ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; export type Maybe = T | null; -export type InputMaybe = Maybe; +export type InputMaybe = T | null | undefined; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; @@ -19,6 +19,8 @@ export type Scalars = { * https://relay.dev/graphql/connections.htm#sec-Cursor */ Cursor: { input: any; output: any; } + /** The builtin Map type */ + Map: { input: any; output: any; } /** The builtin Time type */ Time: { input: string; output: string; } }; @@ -61,6 +63,7 @@ export type CreateQuestionInput = { difficulty?: InputMaybe; /** Reference answer */ referenceAnswer: Scalars['String']['input']; + submissionIDs?: InputMaybe>; /** Question title */ title: Scalars['String']['input']; }; @@ -83,8 +86,11 @@ export type CreateScopeSetInput = { export type CreateUserInput = { avatar?: InputMaybe; email: Scalars['String']['input']; + eventIDs?: InputMaybe>; groupID: Scalars['ID']['input']; name: Scalars['String']['input']; + pointIDs?: InputMaybe>; + submissionIDs?: InputMaybe>; }; export type Database = Node & { @@ -179,6 +185,99 @@ export type DatabaseWhereInput = { slugNotIn?: InputMaybe>; }; +export type Event = Node & { + __typename?: 'Event'; + id: Scalars['ID']['output']; + payload?: Maybe; + triggeredAt: Scalars['Time']['output']; + type: Scalars['String']['output']; + user: User; + userID: Scalars['ID']['output']; +}; + +/** A connection to a list of items. */ +export type EventConnection = { + __typename?: 'EventConnection'; + /** A list of edges. */ + edges?: Maybe>>; + /** Information to aid in pagination. */ + pageInfo: PageInfo; + /** Identifies the total count of items in the connection. */ + totalCount: Scalars['Int']['output']; +}; + +/** An edge in a connection. */ +export type EventEdge = { + __typename?: 'EventEdge'; + /** A cursor for use in pagination. */ + cursor: Scalars['Cursor']['output']; + /** The item at the end of the edge. */ + node?: Maybe; +}; + +/** Ordering options for Event connections */ +export type EventOrder = { + /** The ordering direction. */ + direction?: OrderDirection; + /** The field by which to order Events. */ + field: EventOrderField; +}; + +/** Properties by which Event connections can be ordered. */ +export enum EventOrderField { + TriggeredAt = 'TRIGGERED_AT' +} + +/** + * EventWhereInput is used for filtering Event objects. + * Input was generated by ent. + */ +export type EventWhereInput = { + and?: InputMaybe>; + /** user edge predicates */ + hasUser?: InputMaybe; + hasUserWith?: InputMaybe>; + /** id field predicates */ + id?: InputMaybe; + idGT?: InputMaybe; + idGTE?: InputMaybe; + idIn?: InputMaybe>; + idLT?: InputMaybe; + idLTE?: InputMaybe; + idNEQ?: InputMaybe; + idNotIn?: InputMaybe>; + not?: InputMaybe; + or?: InputMaybe>; + /** triggered_at field predicates */ + triggeredAt?: InputMaybe; + triggeredAtGT?: InputMaybe; + triggeredAtGTE?: InputMaybe; + triggeredAtIn?: InputMaybe>; + triggeredAtLT?: InputMaybe; + triggeredAtLTE?: InputMaybe; + triggeredAtNEQ?: InputMaybe; + triggeredAtNotIn?: InputMaybe>; + /** type field predicates */ + type?: InputMaybe; + typeContains?: InputMaybe; + typeContainsFold?: InputMaybe; + typeEqualFold?: InputMaybe; + typeGT?: InputMaybe; + typeGTE?: InputMaybe; + typeHasPrefix?: InputMaybe; + typeHasSuffix?: InputMaybe; + typeIn?: InputMaybe>; + typeLT?: InputMaybe; + typeLTE?: InputMaybe; + typeNEQ?: InputMaybe; + typeNotIn?: InputMaybe>; + /** user_id field predicates */ + userID?: InputMaybe; + userIDIn?: InputMaybe>; + userIDNEQ?: InputMaybe; + userIDNotIn?: InputMaybe>; +}; + export type Group = Node & { __typename?: 'Group'; createdAt: Scalars['Time']['output']; @@ -302,6 +401,8 @@ export type Mutation = { logoutAll: Scalars['Boolean']['output']; /** Logout a user from all his devices. */ logoutUser: Scalars['Boolean']['output']; + /** Submit your answer to a question. */ + submitAnswer: SubmissionResult; /** Update a database. */ updateDatabase: Database; /** Update a group. */ @@ -374,6 +475,12 @@ export type MutationLogoutUserArgs = { }; +export type MutationSubmitAnswerArgs = { + answer: Scalars['String']['input']; + id: Scalars['ID']['input']; +}; + + export type MutationUpdateDatabaseArgs = { id: Scalars['ID']['input']; input: UpdateDatabaseInput; @@ -441,11 +548,117 @@ export type PageInfo = { startCursor?: Maybe; }; +export type Point = Node & { + __typename?: 'Point'; + description?: Maybe; + grantedAt: Scalars['Time']['output']; + id: Scalars['ID']['output']; + points: Scalars['Int']['output']; + user: User; +}; + +/** A connection to a list of items. */ +export type PointConnection = { + __typename?: 'PointConnection'; + /** A list of edges. */ + edges?: Maybe>>; + /** Information to aid in pagination. */ + pageInfo: PageInfo; + /** Identifies the total count of items in the connection. */ + totalCount: Scalars['Int']['output']; +}; + +/** An edge in a connection. */ +export type PointEdge = { + __typename?: 'PointEdge'; + /** A cursor for use in pagination. */ + cursor: Scalars['Cursor']['output']; + /** The item at the end of the edge. */ + node?: Maybe; +}; + +/** Ordering options for Point connections */ +export type PointOrder = { + /** The ordering direction. */ + direction?: OrderDirection; + /** The field by which to order Points. */ + field: PointOrderField; +}; + +/** Properties by which Point connections can be ordered. */ +export enum PointOrderField { + GrantedAt = 'GRANTED_AT' +} + +/** + * PointWhereInput is used for filtering Point objects. + * Input was generated by ent. + */ +export type PointWhereInput = { + and?: InputMaybe>; + /** description field predicates */ + description?: InputMaybe; + descriptionContains?: InputMaybe; + descriptionContainsFold?: InputMaybe; + descriptionEqualFold?: InputMaybe; + descriptionGT?: InputMaybe; + descriptionGTE?: InputMaybe; + descriptionHasPrefix?: InputMaybe; + descriptionHasSuffix?: InputMaybe; + descriptionIn?: InputMaybe>; + descriptionIsNil?: InputMaybe; + descriptionLT?: InputMaybe; + descriptionLTE?: InputMaybe; + descriptionNEQ?: InputMaybe; + descriptionNotIn?: InputMaybe>; + descriptionNotNil?: InputMaybe; + /** granted_at field predicates */ + grantedAt?: InputMaybe; + grantedAtGT?: InputMaybe; + grantedAtGTE?: InputMaybe; + grantedAtIn?: InputMaybe>; + grantedAtLT?: InputMaybe; + grantedAtLTE?: InputMaybe; + grantedAtNEQ?: InputMaybe; + grantedAtNotIn?: InputMaybe>; + /** user edge predicates */ + hasUser?: InputMaybe; + hasUserWith?: InputMaybe>; + /** id field predicates */ + id?: InputMaybe; + idGT?: InputMaybe; + idGTE?: InputMaybe; + idIn?: InputMaybe>; + idLT?: InputMaybe; + idLTE?: InputMaybe; + idNEQ?: InputMaybe; + idNotIn?: InputMaybe>; + not?: InputMaybe; + or?: InputMaybe>; + /** points field predicates */ + points?: InputMaybe; + pointsGT?: InputMaybe; + pointsGTE?: InputMaybe; + pointsIn?: InputMaybe>; + pointsLT?: InputMaybe; + pointsLTE?: InputMaybe; + pointsNEQ?: InputMaybe; + pointsNotIn?: InputMaybe>; +}; + export type Query = { __typename?: 'Query'; /** Get a database by ID. */ database: Database; databases: Array; + /** + * Get an event by ID. + * + * If you have the "event:read" scope, you can get any event by ID; + * otherwise, you can only get your own events. + */ + event: Event; + events: EventConnection; /** Get a group by ID. */ group: Group; groups: Array; @@ -454,12 +667,28 @@ export type Query = { node?: Maybe; /** Lookup nodes by a list of IDs. */ nodes: Array>; + /** + * Get a point grant by ID. + * + * If you have the "point:read" scope, you can get any point grant by ID; + * otherwise, you can only get your own point grants. + */ + pointGrant: Point; + points: PointConnection; /** Get a question by ID. */ question: Question; questions: QuestionConnection; /** Get a scope set by ID or slug. */ scopeSet: ScopeSet; scopeSets: Array; + /** + * Get a submission by ID. + * + * If you have the "submission:read" scope, you can get any submission by ID; + * otherwise, you can only get your own submissions. + */ + submission: Submission; + submissions: SubmissionConnection; /** Get a user by ID. */ user: User; users: UserConnection; @@ -471,6 +700,21 @@ export type QueryDatabaseArgs = { }; +export type QueryEventArgs = { + id: Scalars['ID']['input']; +}; + + +export type QueryEventsArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + orderBy?: InputMaybe; + where?: InputMaybe; +}; + + export type QueryGroupArgs = { id: Scalars['ID']['input']; }; @@ -486,6 +730,21 @@ export type QueryNodesArgs = { }; +export type QueryPointGrantArgs = { + id: Scalars['ID']['input']; +}; + + +export type QueryPointsArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + orderBy?: InputMaybe; + where?: InputMaybe; +}; + + export type QueryQuestionArgs = { id: Scalars['ID']['input']; }; @@ -506,6 +765,21 @@ export type QueryScopeSetArgs = { }; +export type QuerySubmissionArgs = { + id: Scalars['ID']['input']; +}; + + +export type QuerySubmissionsArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + orderBy?: InputMaybe; + where?: InputMaybe; +}; + + export type QueryUserArgs = { id: Scalars['ID']['input']; }; @@ -532,7 +806,8 @@ export type Question = Node & { id: Scalars['ID']['output']; /** Reference answer */ referenceAnswer: Scalars['String']['output']; - referenceAnswerResult: SqlResponse; + referenceAnswerResult: SqlExecutionResult; + submissions?: Maybe>; /** Question title */ title: Scalars['String']['output']; }; @@ -621,6 +896,9 @@ export type QuestionWhereInput = { /** database edge predicates */ hasDatabase?: InputMaybe; hasDatabaseWith?: InputMaybe>; + /** submissions edge predicates */ + hasSubmissions?: InputMaybe; + hasSubmissionsWith?: InputMaybe>; /** id field predicates */ id?: InputMaybe; idGT?: InputMaybe; @@ -662,6 +940,12 @@ export type QuestionWhereInput = { titleNotIn?: InputMaybe>; }; +export type SqlExecutionResult = { + __typename?: 'SQLExecutionResult'; + columns: Array; + rows: Array>; +}; + export type ScopeSet = Node & { __typename?: 'ScopeSet'; description?: Maybe; @@ -735,10 +1019,135 @@ export type ScopeSetWhereInput = { slugNotIn?: InputMaybe>; }; -export type SqlResponse = { - __typename?: 'SqlResponse'; - columns: Array; - rows: Array>; +export type Submission = Node & { + __typename?: 'Submission'; + error?: Maybe; + id: Scalars['ID']['output']; + queryResult?: Maybe; + question: Question; + status: SubmissionStatus; + submittedAt: Scalars['Time']['output']; + submittedCode: Scalars['String']['output']; + user: User; +}; + +/** A connection to a list of items. */ +export type SubmissionConnection = { + __typename?: 'SubmissionConnection'; + /** A list of edges. */ + edges?: Maybe>>; + /** Information to aid in pagination. */ + pageInfo: PageInfo; + /** Identifies the total count of items in the connection. */ + totalCount: Scalars['Int']['output']; +}; + +/** An edge in a connection. */ +export type SubmissionEdge = { + __typename?: 'SubmissionEdge'; + /** A cursor for use in pagination. */ + cursor: Scalars['Cursor']['output']; + /** The item at the end of the edge. */ + node?: Maybe; +}; + +/** Ordering options for Submission connections */ +export type SubmissionOrder = { + /** The ordering direction. */ + direction?: OrderDirection; + /** The field by which to order Submissions. */ + field: SubmissionOrderField; +}; + +/** Properties by which Submission connections can be ordered. */ +export enum SubmissionOrderField { + SubmittedAt = 'SUBMITTED_AT' +} + +export type SubmissionResult = { + __typename?: 'SubmissionResult'; + error?: Maybe; + result?: Maybe; +}; + +/** SubmissionStatus is enum for the field status */ +export enum SubmissionStatus { + Failed = 'failed', + Pending = 'pending', + Success = 'success' +} + +/** + * SubmissionWhereInput is used for filtering Submission objects. + * Input was generated by ent. + */ +export type SubmissionWhereInput = { + and?: InputMaybe>; + /** error field predicates */ + error?: InputMaybe; + errorContains?: InputMaybe; + errorContainsFold?: InputMaybe; + errorEqualFold?: InputMaybe; + errorGT?: InputMaybe; + errorGTE?: InputMaybe; + errorHasPrefix?: InputMaybe; + errorHasSuffix?: InputMaybe; + errorIn?: InputMaybe>; + errorIsNil?: InputMaybe; + errorLT?: InputMaybe; + errorLTE?: InputMaybe; + errorNEQ?: InputMaybe; + errorNotIn?: InputMaybe>; + errorNotNil?: InputMaybe; + /** question edge predicates */ + hasQuestion?: InputMaybe; + hasQuestionWith?: InputMaybe>; + /** user edge predicates */ + hasUser?: InputMaybe; + hasUserWith?: InputMaybe>; + /** id field predicates */ + id?: InputMaybe; + idGT?: InputMaybe; + idGTE?: InputMaybe; + idIn?: InputMaybe>; + idLT?: InputMaybe; + idLTE?: InputMaybe; + idNEQ?: InputMaybe; + idNotIn?: InputMaybe>; + not?: InputMaybe; + or?: InputMaybe>; + /** status field predicates */ + status?: InputMaybe; + statusIn?: InputMaybe>; + statusNEQ?: InputMaybe; + statusNotIn?: InputMaybe>; + /** submitted_at field predicates */ + submittedAt?: InputMaybe; + submittedAtGT?: InputMaybe; + submittedAtGTE?: InputMaybe; + submittedAtIn?: InputMaybe>; + submittedAtLT?: InputMaybe; + submittedAtLTE?: InputMaybe; + submittedAtNEQ?: InputMaybe; + submittedAtNotIn?: InputMaybe>; + /** submitted_code field predicates */ + submittedCode?: InputMaybe; + submittedCodeContains?: InputMaybe; + submittedCodeContainsFold?: InputMaybe; + submittedCodeEqualFold?: InputMaybe; + submittedCodeGT?: InputMaybe; + submittedCodeGTE?: InputMaybe; + submittedCodeHasPrefix?: InputMaybe; + submittedCodeHasSuffix?: InputMaybe; + submittedCodeIn?: InputMaybe>; + submittedCodeLT?: InputMaybe; + submittedCodeLTE?: InputMaybe; + submittedCodeNEQ?: InputMaybe; + submittedCodeNotIn?: InputMaybe>; +}; + +export type SubmissionsOfQuestionWhereInput = { + status?: InputMaybe; }; /** @@ -775,6 +1184,8 @@ export type UpdateGroupInput = { * Input was generated by ent. */ export type UpdateQuestionInput = { + addSubmissionIDs?: InputMaybe>; + clearSubmissions?: InputMaybe; databaseID?: InputMaybe; /** Question stem */ description?: InputMaybe; @@ -782,6 +1193,7 @@ export type UpdateQuestionInput = { difficulty?: InputMaybe; /** Reference answer */ referenceAnswer?: InputMaybe; + removeSubmissionIDs?: InputMaybe>; /** Question title */ title?: InputMaybe; }; @@ -805,10 +1217,19 @@ export type UpdateScopeSetInput = { * Input was generated by ent. */ export type UpdateUserInput = { + addEventIDs?: InputMaybe>; + addPointIDs?: InputMaybe>; + addSubmissionIDs?: InputMaybe>; avatar?: InputMaybe; clearAvatar?: InputMaybe; + clearEvents?: InputMaybe; + clearPoints?: InputMaybe; + clearSubmissions?: InputMaybe; groupID?: InputMaybe; name?: InputMaybe; + removeEventIDs?: InputMaybe>; + removePointIDs?: InputMaybe>; + removeSubmissionIDs?: InputMaybe>; }; export type User = Node & { @@ -817,14 +1238,62 @@ export type User = Node & { createdAt: Scalars['Time']['output']; deletedAt?: Maybe; email: Scalars['String']['output']; + events: EventConnection; group: Group; id: Scalars['ID']['output']; /** The user who impersonated this user. */ impersonatedBy?: Maybe; name: Scalars['String']['output']; + points: PointConnection; + submissions: SubmissionConnection; + /** Get all submissions of a question. */ + submissionsOfQuestion: SubmissionConnection; + /** The total points of the user. */ + totalPoints: Scalars['Int']['output']; updatedAt: Scalars['Time']['output']; }; + +export type UserEventsArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + orderBy?: InputMaybe; + where?: InputMaybe; +}; + + +export type UserPointsArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + orderBy?: InputMaybe; + where?: InputMaybe; +}; + + +export type UserSubmissionsArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + orderBy?: InputMaybe; + where?: InputMaybe; +}; + + +export type UserSubmissionsOfQuestionArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + orderBy?: InputMaybe; + questionID: Scalars['ID']['input']; + where?: InputMaybe; +}; + /** A connection to a list of items. */ export type UserConnection = { __typename?: 'UserConnection'; @@ -858,6 +1327,13 @@ export enum UserOrderField { Email = 'EMAIL' } +export type UserSqlExecutionResult = { + __typename?: 'UserSQLExecutionResult'; + columns: Array; + matchAnswer: Scalars['Boolean']['output']; + rows: Array>; +}; + /** * UserWhereInput is used for filtering User objects. * Input was generated by ent. @@ -914,9 +1390,18 @@ export type UserWhereInput = { emailLTE?: InputMaybe; emailNEQ?: InputMaybe; emailNotIn?: InputMaybe>; + /** events edge predicates */ + hasEvents?: InputMaybe; + hasEventsWith?: InputMaybe>; /** group edge predicates */ hasGroup?: InputMaybe; hasGroupWith?: InputMaybe>; + /** points edge predicates */ + hasPoints?: InputMaybe; + hasPointsWith?: InputMaybe>; + /** submissions edge predicates */ + hasSubmissions?: InputMaybe; + hasSubmissionsWith?: InputMaybe>; /** id field predicates */ id?: InputMaybe; idGT?: InputMaybe; @@ -953,6 +1438,57 @@ export type UserWhereInput = { updatedAtNotIn?: InputMaybe>; }; +export type EventByIdQueryVariables = Exact<{ + id: Scalars['ID']['input']; +}>; + + +export type EventByIdQuery = { __typename?: 'Query', event: { __typename?: 'Event', id: string, type: string, payload?: any | null, triggeredAt: string, user: { __typename?: 'User', id: string, name: string } } }; + +export type EventsTableQueryVariables = Exact<{ + first?: InputMaybe; + after?: InputMaybe; + last?: InputMaybe; + before?: InputMaybe; +}>; + + +export type EventsTableQuery = { __typename?: 'Query', events: { __typename?: 'EventConnection', totalCount: number, edges?: Array<{ __typename?: 'EventEdge', node?: { __typename?: 'Event', id: string, type: string, triggeredAt: string, user: { __typename?: 'User', id: string, name: string } } | null } | null> | null, pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, hasPreviousPage: boolean, endCursor?: any | null, startCursor?: any | null } } }; + +export type PointByIdQueryVariables = Exact<{ + id: Scalars['ID']['input']; +}>; + + +export type PointByIdQuery = { __typename?: 'Query', pointGrant: { __typename?: 'Point', id: string, points: number, description?: string | null, grantedAt: string, user: { __typename?: 'User', id: string, name: string } } }; + +export type PointsTableQueryVariables = Exact<{ + first?: InputMaybe; + after?: InputMaybe; + last?: InputMaybe; + before?: InputMaybe; +}>; + + +export type PointsTableQuery = { __typename?: 'Query', points: { __typename?: 'PointConnection', totalCount: number, edges?: Array<{ __typename?: 'PointEdge', node?: { __typename?: 'Point', id: string, points: number, description?: string | null, grantedAt: string, user: { __typename?: 'User', id: string, name: string } } | null } | null> | null, pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, hasPreviousPage: boolean, endCursor?: any | null, startCursor?: any | null } } }; + +export type SubmissionByIdQueryVariables = Exact<{ + id: Scalars['ID']['input']; +}>; + + +export type SubmissionByIdQuery = { __typename?: 'Query', submission: { __typename?: 'Submission', id: string, error?: string | null, submittedCode: string, status: SubmissionStatus, submittedAt: string, user: { __typename?: 'User', id: string, name: string }, queryResult?: { __typename?: 'UserSQLExecutionResult', columns: Array, rows: Array>, matchAnswer: boolean } | null, question: { __typename?: 'Question', id: string } } }; + +export type SubmissionsTableQueryVariables = Exact<{ + first?: InputMaybe; + after?: InputMaybe; + last?: InputMaybe; + before?: InputMaybe; +}>; + + +export type SubmissionsTableQuery = { __typename?: 'Query', submissions: { __typename?: 'SubmissionConnection', totalCount: number, edges?: Array<{ __typename?: 'SubmissionEdge', node?: { __typename?: 'Submission', id: string, submittedCode: string, status: SubmissionStatus, user: { __typename?: 'User', id: string, name: string }, question: { __typename?: 'Question', id: string, title: string } } | null } | null> | null, pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, hasPreviousPage: boolean, endCursor?: any | null, startCursor?: any | null } } }; + export type DatabaseDetailQueryVariables = Exact<{ id: Scalars['ID']['input']; }>; @@ -1006,7 +1542,7 @@ export type QuestionReferenceAnswerResultQueryVariables = Exact<{ }>; -export type QuestionReferenceAnswerResultQuery = { __typename?: 'Query', question: { __typename?: 'Question', referenceAnswerResult: { __typename?: 'SqlResponse', columns: Array, rows: Array> } } }; +export type QuestionReferenceAnswerResultQuery = { __typename?: 'Query', question: { __typename?: 'Question', referenceAnswerResult: { __typename?: 'SQLExecutionResult', columns: Array, rows: Array> } } }; export type CreateQuestionMutationVariables = Exact<{ input: CreateQuestionInput; @@ -1262,6 +1798,12 @@ export type BasicUserInfoQueryVariables = Exact<{ [key: string]: never; }>; export type BasicUserInfoQuery = { __typename?: 'Query', me: { __typename?: 'User', id: string, name: string, email: string, avatar?: string | null, group: { __typename?: 'Group', name: string } } }; +export const EventByIdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EventById"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"payload"}},{"kind":"Field","name":{"kind":"Name","value":"triggeredAt"}}]}}]}}]} as unknown as DocumentNode; +export const EventsTableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EventsTable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"last"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"before"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"events"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"last"},"value":{"kind":"Variable","name":{"kind":"Name","value":"last"}}},{"kind":"Argument","name":{"kind":"Name","value":"before"},"value":{"kind":"Variable","name":{"kind":"Name","value":"before"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"field"},"value":{"kind":"EnumValue","value":"TRIGGERED_AT"}},{"kind":"ObjectField","name":{"kind":"Name","value":"direction"},"value":{"kind":"EnumValue","value":"DESC"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"triggeredAt"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}}]}}]}}]}}]} as unknown as DocumentNode; +export const PointByIdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PointById"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"pointGrant"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"points"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"grantedAt"}}]}}]}}]} as unknown as DocumentNode; +export const PointsTableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PointsTable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"last"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"before"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"points"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"last"},"value":{"kind":"Variable","name":{"kind":"Name","value":"last"}}},{"kind":"Argument","name":{"kind":"Name","value":"before"},"value":{"kind":"Variable","name":{"kind":"Name","value":"before"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"field"},"value":{"kind":"EnumValue","value":"GRANTED_AT"}},{"kind":"ObjectField","name":{"kind":"Name","value":"direction"},"value":{"kind":"EnumValue","value":"DESC"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"points"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"grantedAt"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}}]}}]}}]}}]} as unknown as DocumentNode; +export const SubmissionByIdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SubmissionById"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"submission"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"queryResult"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"columns"}},{"kind":"Field","name":{"kind":"Name","value":"rows"}},{"kind":"Field","name":{"kind":"Name","value":"matchAnswer"}}]}},{"kind":"Field","name":{"kind":"Name","value":"question"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"submittedCode"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"submittedAt"}}]}}]}}]} as unknown as DocumentNode; +export const SubmissionsTableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SubmissionsTable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"last"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"before"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"submissions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"last"},"value":{"kind":"Variable","name":{"kind":"Name","value":"last"}}},{"kind":"Argument","name":{"kind":"Name","value":"before"},"value":{"kind":"Variable","name":{"kind":"Name","value":"before"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"field"},"value":{"kind":"EnumValue","value":"SUBMITTED_AT"}},{"kind":"ObjectField","name":{"kind":"Name","value":"direction"},"value":{"kind":"EnumValue","value":"DESC"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"submittedCode"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"question"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}}]}}]}}]}}]} as unknown as DocumentNode; export const DatabaseDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"DatabaseDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"database"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"schema"}},{"kind":"Field","name":{"kind":"Name","value":"relationFigure"}}]}}]}}]} as unknown as DocumentNode; export const CreateDatabaseDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateDatabase"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateDatabaseInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createDatabase"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; export const UpdateDatabaseDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateDatabase"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateDatabaseInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateDatabase"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; diff --git a/schema.graphql b/schema.graphql index 2fdf56f..7a1de4f 100644 --- a/schema.graphql +++ b/schema.graphql @@ -57,6 +57,7 @@ input CreateQuestionInput { difficulty: QuestionDifficulty """Reference answer""" referenceAnswer: String! + submissionIDs: [ID!] """Question title""" title: String! } @@ -79,8 +80,11 @@ Input was generated by ent. input CreateUserInput { avatar: String email: String! + eventIDs: [ID!] groupID: ID! name: String! + pointIDs: [ID!] + submissionIDs: [ID!] } """ @@ -180,6 +184,96 @@ input DatabaseWhereInput { slugNotIn: [String!] } +type Event implements Node { + id: ID! + payload: Map + triggeredAt: Time! + type: String! + user: User! + userID: ID! +} + +"""A connection to a list of items.""" +type EventConnection { + """A list of edges.""" + edges: [EventEdge] + """Information to aid in pagination.""" + pageInfo: PageInfo! + """Identifies the total count of items in the connection.""" + totalCount: Int! +} + +"""An edge in a connection.""" +type EventEdge { + """A cursor for use in pagination.""" + cursor: Cursor! + """The item at the end of the edge.""" + node: Event +} + +"""Ordering options for Event connections""" +input EventOrder { + """The ordering direction.""" + direction: OrderDirection! = ASC + """The field by which to order Events.""" + field: EventOrderField! +} + +"""Properties by which Event connections can be ordered.""" +enum EventOrderField { + TRIGGERED_AT +} + +""" +EventWhereInput is used for filtering Event objects. +Input was generated by ent. +""" +input EventWhereInput { + and: [EventWhereInput!] + """user edge predicates""" + hasUser: Boolean + hasUserWith: [UserWhereInput!] + """id field predicates""" + id: ID + idGT: ID + idGTE: ID + idIn: [ID!] + idLT: ID + idLTE: ID + idNEQ: ID + idNotIn: [ID!] + not: EventWhereInput + or: [EventWhereInput!] + """triggered_at field predicates""" + triggeredAt: Time + triggeredAtGT: Time + triggeredAtGTE: Time + triggeredAtIn: [Time!] + triggeredAtLT: Time + triggeredAtLTE: Time + triggeredAtNEQ: Time + triggeredAtNotIn: [Time!] + """type field predicates""" + type: String + typeContains: String + typeContainsFold: String + typeEqualFold: String + typeGT: String + typeGTE: String + typeHasPrefix: String + typeHasSuffix: String + typeIn: [String!] + typeLT: String + typeLTE: String + typeNEQ: String + typeNotIn: [String!] + """user_id field predicates""" + userID: ID + userIDIn: [ID!] + userIDNEQ: ID + userIDNotIn: [ID!] +} + type Group implements Node { createdAt: Time! deletedAt: Time @@ -271,6 +365,9 @@ input GroupWhereInput { updatedAtNotIn: [Time!] } +"""The builtin Map type""" +scalar Map + type Mutation { """Create a database.""" createDatabase(input: CreateDatabaseInput!): Database! @@ -301,6 +398,8 @@ type Mutation { logoutAll: Boolean! """Logout a user from all his devices.""" logoutUser(userID: ID!): Boolean! + """Submit your answer to a question.""" + submitAnswer(answer: String!, id: ID!): SubmissionResult! """Update a database.""" updateDatabase(id: ID!, input: UpdateDatabaseInput!): Database! """Update a group.""" @@ -351,10 +450,128 @@ type PageInfo { startCursor: Cursor } +type Point implements Node { + description: String + grantedAt: Time! + id: ID! + points: Int! + user: User! +} + +"""A connection to a list of items.""" +type PointConnection { + """A list of edges.""" + edges: [PointEdge] + """Information to aid in pagination.""" + pageInfo: PageInfo! + """Identifies the total count of items in the connection.""" + totalCount: Int! +} + +"""An edge in a connection.""" +type PointEdge { + """A cursor for use in pagination.""" + cursor: Cursor! + """The item at the end of the edge.""" + node: Point +} + +"""Ordering options for Point connections""" +input PointOrder { + """The ordering direction.""" + direction: OrderDirection! = ASC + """The field by which to order Points.""" + field: PointOrderField! +} + +"""Properties by which Point connections can be ordered.""" +enum PointOrderField { + GRANTED_AT +} + +""" +PointWhereInput is used for filtering Point objects. +Input was generated by ent. +""" +input PointWhereInput { + and: [PointWhereInput!] + """description field predicates""" + description: String + descriptionContains: String + descriptionContainsFold: String + descriptionEqualFold: String + descriptionGT: String + descriptionGTE: String + descriptionHasPrefix: String + descriptionHasSuffix: String + descriptionIn: [String!] + descriptionIsNil: Boolean + descriptionLT: String + descriptionLTE: String + descriptionNEQ: String + descriptionNotIn: [String!] + descriptionNotNil: Boolean + """granted_at field predicates""" + grantedAt: Time + grantedAtGT: Time + grantedAtGTE: Time + grantedAtIn: [Time!] + grantedAtLT: Time + grantedAtLTE: Time + grantedAtNEQ: Time + grantedAtNotIn: [Time!] + """user edge predicates""" + hasUser: Boolean + hasUserWith: [UserWhereInput!] + """id field predicates""" + id: ID + idGT: ID + idGTE: ID + idIn: [ID!] + idLT: ID + idLTE: ID + idNEQ: ID + idNotIn: [ID!] + not: PointWhereInput + or: [PointWhereInput!] + """points field predicates""" + points: Int + pointsGT: Int + pointsGTE: Int + pointsIn: [Int!] + pointsLT: Int + pointsLTE: Int + pointsNEQ: Int + pointsNotIn: [Int!] +} + type Query { """Get a database by ID.""" database(id: ID!): Database! databases: [Database!]! + """ + Get an event by ID. + + If you have the "event:read" scope, you can get any event by ID; + otherwise, you can only get your own events. + """ + event(id: ID!): Event! + events( + """Returns the elements in the list that come after the specified cursor.""" + after: Cursor + """ + Returns the elements in the list that come before the specified cursor. + """ + before: Cursor + """Returns the first _n_ elements from the list.""" + first: Int + """Returns the last _n_ elements from the list.""" + last: Int + """Ordering options for Events returned from the connection.""" + orderBy: EventOrder + """Filtering options for Events returned from the connection.""" + where: EventWhereInput + ): EventConnection! """Get a group by ID.""" group(id: ID!): Group! groups: [Group!]! @@ -369,6 +586,29 @@ type Query { """The list of node IDs.""" ids: [ID!]! ): [Node]! + """ + Get a point grant by ID. + + If you have the "point:read" scope, you can get any point grant by ID; + otherwise, you can only get your own point grants. + """ + pointGrant(id: ID!): Point! + points( + """Returns the elements in the list that come after the specified cursor.""" + after: Cursor + """ + Returns the elements in the list that come before the specified cursor. + """ + before: Cursor + """Returns the first _n_ elements from the list.""" + first: Int + """Returns the last _n_ elements from the list.""" + last: Int + """Ordering options for Points returned from the connection.""" + orderBy: PointOrder + """Filtering options for Points returned from the connection.""" + where: PointWhereInput + ): PointConnection! """Get a question by ID.""" question(id: ID!): Question! questions( @@ -390,6 +630,29 @@ type Query { """Get a scope set by ID or slug.""" scopeSet(filter: ScopeSetFilter!): ScopeSet! scopeSets: [ScopeSet!]! + """ + Get a submission by ID. + + If you have the "submission:read" scope, you can get any submission by ID; + otherwise, you can only get your own submissions. + """ + submission(id: ID!): Submission! + submissions( + """Returns the elements in the list that come after the specified cursor.""" + after: Cursor + """ + Returns the elements in the list that come before the specified cursor. + """ + before: Cursor + """Returns the first _n_ elements from the list.""" + first: Int + """Returns the last _n_ elements from the list.""" + last: Int + """Ordering options for Submissions returned from the connection.""" + orderBy: SubmissionOrder + """Filtering options for Submissions returned from the connection.""" + where: SubmissionWhereInput + ): SubmissionConnection! """Get a user by ID.""" user(id: ID!): User! users( @@ -421,7 +684,8 @@ type Question implements Node { id: ID! """Reference answer""" referenceAnswer: String! - referenceAnswerResult: SqlResponse! + referenceAnswerResult: SQLExecutionResult! + submissions: [Submission!] """Question title""" title: String! } @@ -508,6 +772,9 @@ input QuestionWhereInput { """database edge predicates""" hasDatabase: Boolean hasDatabaseWith: [DatabaseWhereInput!] + """submissions edge predicates""" + hasSubmissions: Boolean + hasSubmissionsWith: [SubmissionWhereInput!] """id field predicates""" id: ID idGT: ID @@ -549,6 +816,11 @@ input QuestionWhereInput { titleNotIn: [String!] } +type SQLExecutionResult { + columns: [String!]! + rows: [[String!]!]! +} + type ScopeSet implements Node { description: String groups: [Group!] @@ -621,9 +893,131 @@ input ScopeSetWhereInput { slugNotIn: [String!] } -type SqlResponse { - columns: [String!]! - rows: [[String!]!]! +type Submission implements Node { + error: String + id: ID! + queryResult: UserSQLExecutionResult + question: Question! + status: SubmissionStatus! + submittedAt: Time! + submittedCode: String! + user: User! +} + +"""A connection to a list of items.""" +type SubmissionConnection { + """A list of edges.""" + edges: [SubmissionEdge] + """Information to aid in pagination.""" + pageInfo: PageInfo! + """Identifies the total count of items in the connection.""" + totalCount: Int! +} + +"""An edge in a connection.""" +type SubmissionEdge { + """A cursor for use in pagination.""" + cursor: Cursor! + """The item at the end of the edge.""" + node: Submission +} + +"""Ordering options for Submission connections""" +input SubmissionOrder { + """The ordering direction.""" + direction: OrderDirection! = ASC + """The field by which to order Submissions.""" + field: SubmissionOrderField! +} + +"""Properties by which Submission connections can be ordered.""" +enum SubmissionOrderField { + SUBMITTED_AT +} + +type SubmissionResult { + error: String + result: UserSQLExecutionResult +} + +"""SubmissionStatus is enum for the field status""" +enum SubmissionStatus { + failed + pending + success +} + +""" +SubmissionWhereInput is used for filtering Submission objects. +Input was generated by ent. +""" +input SubmissionWhereInput { + and: [SubmissionWhereInput!] + """error field predicates""" + error: String + errorContains: String + errorContainsFold: String + errorEqualFold: String + errorGT: String + errorGTE: String + errorHasPrefix: String + errorHasSuffix: String + errorIn: [String!] + errorIsNil: Boolean + errorLT: String + errorLTE: String + errorNEQ: String + errorNotIn: [String!] + errorNotNil: Boolean + """question edge predicates""" + hasQuestion: Boolean + hasQuestionWith: [QuestionWhereInput!] + """user edge predicates""" + hasUser: Boolean + hasUserWith: [UserWhereInput!] + """id field predicates""" + id: ID + idGT: ID + idGTE: ID + idIn: [ID!] + idLT: ID + idLTE: ID + idNEQ: ID + idNotIn: [ID!] + not: SubmissionWhereInput + or: [SubmissionWhereInput!] + """status field predicates""" + status: SubmissionStatus + statusIn: [SubmissionStatus!] + statusNEQ: SubmissionStatus + statusNotIn: [SubmissionStatus!] + """submitted_at field predicates""" + submittedAt: Time + submittedAtGT: Time + submittedAtGTE: Time + submittedAtIn: [Time!] + submittedAtLT: Time + submittedAtLTE: Time + submittedAtNEQ: Time + submittedAtNotIn: [Time!] + """submitted_code field predicates""" + submittedCode: String + submittedCodeContains: String + submittedCodeContainsFold: String + submittedCodeEqualFold: String + submittedCodeGT: String + submittedCodeGTE: String + submittedCodeHasPrefix: String + submittedCodeHasSuffix: String + submittedCodeIn: [String!] + submittedCodeLT: String + submittedCodeLTE: String + submittedCodeNEQ: String + submittedCodeNotIn: [String!] +} + +input SubmissionsOfQuestionWhereInput { + status: SubmissionStatus } """The builtin Time type""" @@ -663,6 +1057,8 @@ UpdateQuestionInput is used for update Question object. Input was generated by ent. """ input UpdateQuestionInput { + addSubmissionIDs: [ID!] + clearSubmissions: Boolean databaseID: ID """Question stem""" description: String @@ -670,6 +1066,7 @@ input UpdateQuestionInput { difficulty: QuestionDifficulty """Reference answer""" referenceAnswer: String + removeSubmissionIDs: [ID!] """Question title""" title: String } @@ -693,10 +1090,19 @@ UpdateUserInput is used for update User object. Input was generated by ent. """ input UpdateUserInput { + addEventIDs: [ID!] + addPointIDs: [ID!] + addSubmissionIDs: [ID!] avatar: String clearAvatar: Boolean + clearEvents: Boolean + clearPoints: Boolean + clearSubmissions: Boolean groupID: ID name: String + removeEventIDs: [ID!] + removePointIDs: [ID!] + removeSubmissionIDs: [ID!] } type User implements Node { @@ -704,11 +1110,80 @@ type User implements Node { createdAt: Time! deletedAt: Time email: String! + events( + """Returns the elements in the list that come after the specified cursor.""" + after: Cursor + """ + Returns the elements in the list that come before the specified cursor. + """ + before: Cursor + """Returns the first _n_ elements from the list.""" + first: Int + """Returns the last _n_ elements from the list.""" + last: Int + """Ordering options for Events returned from the connection.""" + orderBy: EventOrder + """Filtering options for Events returned from the connection.""" + where: EventWhereInput + ): EventConnection! group: Group! id: ID! """The user who impersonated this user.""" impersonatedBy: User name: String! + points( + """Returns the elements in the list that come after the specified cursor.""" + after: Cursor + """ + Returns the elements in the list that come before the specified cursor. + """ + before: Cursor + """Returns the first _n_ elements from the list.""" + first: Int + """Returns the last _n_ elements from the list.""" + last: Int + """Ordering options for Points returned from the connection.""" + orderBy: PointOrder + """Filtering options for Points returned from the connection.""" + where: PointWhereInput + ): PointConnection! + submissions( + """Returns the elements in the list that come after the specified cursor.""" + after: Cursor + """ + Returns the elements in the list that come before the specified cursor. + """ + before: Cursor + """Returns the first _n_ elements from the list.""" + first: Int + """Returns the last _n_ elements from the list.""" + last: Int + """Ordering options for Submissions returned from the connection.""" + orderBy: SubmissionOrder + """Filtering options for Submissions returned from the connection.""" + where: SubmissionWhereInput + ): SubmissionConnection! + """Get all submissions of a question.""" + submissionsOfQuestion( + """Returns the elements in the list that come after the specified cursor.""" + after: Cursor + """ + Returns the elements in the list that come before the specified cursor. + """ + before: Cursor + """Returns the first _n_ elements from the list.""" + first: Int + """Returns the last _n_ elements from the list.""" + last: Int + """The order by input to order the submissions.""" + orderBy: SubmissionOrder + """The question ID.""" + questionID: ID! + """The where input to filter the submissions.""" + where: SubmissionsOfQuestionWhereInput + ): SubmissionConnection! + """The total points of the user.""" + totalPoints: Int! updatedAt: Time! } @@ -743,6 +1218,12 @@ enum UserOrderField { EMAIL } +type UserSQLExecutionResult { + columns: [String!]! + matchAnswer: Boolean! + rows: [[String!]!]! +} + """ UserWhereInput is used for filtering User objects. Input was generated by ent. @@ -799,9 +1280,18 @@ input UserWhereInput { emailLTE: String emailNEQ: String emailNotIn: [String!] + """events edge predicates""" + hasEvents: Boolean + hasEventsWith: [EventWhereInput!] """group edge predicates""" hasGroup: Boolean hasGroupWith: [GroupWhereInput!] + """points edge predicates""" + hasPoints: Boolean + hasPointsWith: [PointWhereInput!] + """submissions edge predicates""" + hasSubmissions: Boolean + hasSubmissionsWith: [SubmissionWhereInput!] """id field predicates""" id: ID idGT: ID