This repository has been archived by the owner on Sep 17, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
b5c0823
commit 69afaab
Showing
16 changed files
with
750 additions
and
2 deletions.
There are no files selected for viewing
26 changes: 26 additions & 0 deletions
26
modules/leaderboard/db/migrations/20240327183615_rename_ownerid_to_owner_id/migration.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
-- CreateTable | ||
CREATE TABLE "Entry" ( | ||
"ownerId" UUID NOT NULL, | ||
"score" DOUBLE PRECISION NOT NULL, | ||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
"updatedAt" TIMESTAMP(3) NOT NULL, | ||
"removedAt" TIMESTAMP(3), | ||
"leaderboardKey" TEXT NOT NULL, | ||
|
||
CONSTRAINT "Entry_pkey" PRIMARY KEY ("ownerId","leaderboardKey") | ||
); | ||
|
||
-- CreateTable | ||
CREATE TABLE "Leaderboard" ( | ||
"key" TEXT NOT NULL, | ||
"name" TEXT NOT NULL, | ||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
"updatedAt" TIMESTAMP(3) NOT NULL, | ||
"removedAt" TIMESTAMP(3), | ||
"locked" BOOLEAN NOT NULL DEFAULT false, | ||
|
||
CONSTRAINT "Leaderboard_pkey" PRIMARY KEY ("key") | ||
); | ||
|
||
-- AddForeignKey | ||
ALTER TABLE "Entry" ADD CONSTRAINT "Entry_leaderboardKey_fkey" FOREIGN KEY ("leaderboardKey") REFERENCES "Leaderboard"("key") ON DELETE RESTRICT ON UPDATE CASCADE; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# Please do not edit this file manually | ||
# It should be added in your version-control system (i.e. Git) | ||
provider = "postgresql" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
// Do not modify this `datasource` block | ||
datasource db { | ||
provider = "postgresql" | ||
url = env("DATABASE_URL") | ||
} | ||
|
||
model Entry { | ||
ownerId String @db.Uuid | ||
score Float | ||
createdAt DateTime @default(now()) | ||
updatedAt DateTime @updatedAt | ||
removedAt DateTime? | ||
leaderboard Leaderboard @relation(fields: [leaderboardKey], references: [key]) | ||
leaderboardKey String | ||
@@id([ownerId, leaderboardKey]) | ||
} | ||
|
||
// An overarching leaderboard | ||
model Leaderboard { | ||
key String @id | ||
name String | ||
entries Entry[] | ||
createdAt DateTime @default(now()) | ||
updatedAt DateTime @updatedAt | ||
removedAt DateTime? | ||
locked Boolean @default(false) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
scripts: | ||
init_board: {} | ||
list_boards: {} | ||
delete_board: {} | ||
|
||
lock_board: {} | ||
unlock_board: {} | ||
|
||
set: {} | ||
get_range: {} | ||
get_entry: {} | ||
errors: | ||
leaderboard_not_found: | ||
name: Leaderboard Not Found | ||
description: A leaderboard was not found with provided key | ||
entry_not_found: | ||
name: Entry Not Found | ||
description: An entry with the specified owner ID was not found for the leaderboard with the provided key | ||
leaderboard_locked: | ||
name: Leaderboard Locked | ||
description: Leaderboard and associated entries cannot be edited because it is already locked | ||
leaderboard_unlocked: | ||
name: Leaderboard Unocked | ||
description: Leaderboard cannot be unlocked because it is already unlocked | ||
leaderboard_already_exists: | ||
name: Leaderboard Already Exists | ||
description: A leaderboard with the provided key already exists and `overwrite` was not specified |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { ScriptContext, RuntimeError } from "../_gen/scripts/delete_board.ts"; | ||
|
||
export interface Request { | ||
key: string; | ||
} | ||
|
||
export type Response = Record<string, never>; | ||
|
||
export async function run( | ||
ctx: ScriptContext, | ||
req: Request, | ||
): Promise<Response> { | ||
await ctx.db.$transaction(async (db) => { | ||
const existingLeaderboard = await db.leaderboard.findFirst({ | ||
where: { | ||
key: req.key, | ||
removedAt: null, | ||
} | ||
}); | ||
|
||
if (!existingLeaderboard) { | ||
throw new RuntimeError( | ||
"leaderboard_not_found", | ||
{ cause: `Leaderboard with key ${req.key} not found` }, | ||
); | ||
} | ||
|
||
await db.leaderboard.update({ | ||
where: { | ||
key: req.key, | ||
}, | ||
data: { | ||
removedAt: new Date().toISOString(), | ||
} | ||
}); | ||
|
||
await db.entry.updateMany({ | ||
where: { | ||
leaderboardKey: req.key, | ||
}, | ||
data: { | ||
removedAt: new Date().toISOString(), | ||
} | ||
}); | ||
}); | ||
|
||
return { }; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import { ScriptContext, RuntimeError } from "../_gen/scripts/get_entry.ts"; | ||
import { fromPrismaEntry } from "../utils/types.ts"; | ||
import { LeaderboardEntry } from "../utils/types.ts"; | ||
|
||
export interface Request { | ||
key: string; | ||
ownerId: string; | ||
} | ||
|
||
export interface Response { | ||
entry: LeaderboardEntry; | ||
index: number; | ||
} | ||
|
||
export async function run( | ||
ctx: ScriptContext, | ||
req: Request, | ||
): Promise<Response> { | ||
const { entry, index } = await ctx.db.$transaction(async (db) => { | ||
const existingLeaderboard = await db.leaderboard.findFirst({ | ||
where: { | ||
key: req.key, | ||
removedAt: null, | ||
} | ||
}); | ||
|
||
if (!existingLeaderboard) { | ||
throw new RuntimeError( | ||
"leaderboard_not_found", | ||
{ cause: `Leaderboard with key ${req.key} not found` }, | ||
); | ||
} | ||
|
||
|
||
const entry = await db.entry.findFirst({ | ||
where: { | ||
leaderboardKey: req.key, | ||
ownerId: req.ownerId, | ||
removedAt: null, | ||
}, | ||
}); | ||
|
||
if (!entry) { | ||
throw new RuntimeError( | ||
"entry_not_found", | ||
{ cause: `${req.ownerId} does not have an entry on leaderboard with key ${req.key}` }, | ||
); | ||
} | ||
|
||
const index = await db.entry.count({ | ||
where: { | ||
leaderboardKey: req.key, | ||
score: { | ||
gt: entry.score, | ||
}, | ||
}, | ||
}); | ||
|
||
return { entry, index }; | ||
}); | ||
|
||
return { | ||
entry: fromPrismaEntry(entry), | ||
index, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { ScriptContext, RuntimeError } from "../_gen/scripts/get_range.ts"; | ||
import { fromPrismaEntry } from "../utils/types.ts"; | ||
import { LeaderboardEntry } from "../utils/types.ts"; | ||
|
||
export interface Request { | ||
key: string; | ||
range: [number, number]; | ||
} | ||
|
||
export interface Response { | ||
entries: LeaderboardEntry[]; | ||
} | ||
|
||
export async function run( | ||
ctx: ScriptContext, | ||
req: Request, | ||
): Promise<Response> { | ||
const { entries } = await ctx.db.$transaction(async (db) => { | ||
const existingLeaderboard = await db.leaderboard.findFirst({ | ||
where: { | ||
key: req.key, | ||
removedAt: null, | ||
} | ||
}); | ||
|
||
if (!existingLeaderboard) { | ||
throw new RuntimeError( | ||
"leaderboard_not_found", | ||
{ cause: `Leaderboard with key ${req.key} not found` }, | ||
); | ||
} | ||
|
||
const entries = await db.entry.findMany({ | ||
orderBy: { | ||
score: 'desc', | ||
}, | ||
skip: req.range[0], | ||
take: req.range[1] - req.range[0], | ||
where: { | ||
leaderboardKey: req.key, | ||
} | ||
}); | ||
|
||
return { entries }; | ||
}); | ||
|
||
return { | ||
entries: entries.map(fromPrismaEntry), | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import { ScriptContext, RuntimeError } from "../_gen/scripts/init_board.ts"; | ||
import { fromPrisma } from "../utils/types.ts"; | ||
import { Leaderboard } from "../utils/types.ts"; | ||
|
||
export interface Request { | ||
options: Omit<Leaderboard, "createdAt" | "updatedAt" | "locked">, | ||
overwrite?: boolean; | ||
} | ||
|
||
export interface Response { | ||
leaderboard: Leaderboard; | ||
} | ||
|
||
export async function run( | ||
ctx: ScriptContext, | ||
req: Request, | ||
): Promise<Response> { | ||
const { leaderboard } = await ctx.db.$transaction(async (db) => { | ||
const existingLeaderboard = await db.leaderboard.findFirst({ | ||
where: { | ||
key: req.options.key, | ||
} | ||
}); | ||
|
||
if (existingLeaderboard) { | ||
if (req.overwrite || existingLeaderboard.removedAt) { | ||
// Set the previous leaderboard as deleted and locked to keep | ||
// other requests from overwriting it. | ||
await db.leaderboard.update({ | ||
where: { | ||
key: req.options.key, | ||
}, | ||
data: { | ||
locked: true, | ||
removedAt: new Date().toISOString(), | ||
} | ||
}); | ||
|
||
// Set the previous leaderboard's entries as deleted so that | ||
// they don't show up on the new one. | ||
await db.entry.updateMany({ | ||
where: { | ||
leaderboardKey: req.options.key, | ||
}, | ||
data: { | ||
removedAt: new Date().toISOString(), | ||
} | ||
}); | ||
} else { | ||
throw new RuntimeError( | ||
"leaderboard_already_exists", | ||
{ cause: `Leaderboard with key ${req.options.key} already exists` }, | ||
); | ||
} | ||
} | ||
|
||
const newLeaderboardPayload = { | ||
key: req.options.key, | ||
name: req.options.name, | ||
locked: false, | ||
|
||
createdAt: new Date().toISOString(), | ||
updatedAt: new Date().toISOString(), | ||
removedAt: null, | ||
}; | ||
|
||
const newLeaderboard = await db.leaderboard.upsert({ | ||
where: { | ||
key: req.options.key, | ||
}, | ||
update: newLeaderboardPayload, | ||
create: newLeaderboardPayload, | ||
}); | ||
|
||
return { | ||
leaderboard: newLeaderboard, | ||
}; | ||
}); | ||
|
||
return { leaderboard: fromPrisma(leaderboard) }; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { ScriptContext } from "../_gen/scripts/list_boards.ts"; | ||
import { fromPrisma } from "../utils/types.ts"; | ||
import { Leaderboard } from "../utils/types.ts"; | ||
|
||
export interface Request { | ||
keyContains?: string; | ||
} | ||
|
||
export interface Response { | ||
leaderboards: Leaderboard[]; | ||
} | ||
|
||
export async function run( | ||
ctx: ScriptContext, | ||
req: Request, | ||
): Promise<Response> { | ||
const existingLeaderboards = await ctx.db.leaderboard.findMany({ | ||
where: { | ||
key: { | ||
contains: req.keyContains, | ||
}, | ||
removedAt: null, | ||
} | ||
}); | ||
|
||
return { leaderboards: existingLeaderboards.map(fromPrisma) }; | ||
} |
Oops, something went wrong.