Skip to content
This repository has been archived by the owner on Sep 17, 2024. It is now read-only.

Commit

Permalink
feat: Create leaderboard module
Browse files Browse the repository at this point in the history
  • Loading branch information
Blckbrry-Pi committed Mar 27, 2024
1 parent b5c0823 commit 69afaab
Show file tree
Hide file tree
Showing 16 changed files with 750 additions and 2 deletions.
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;
3 changes: 3 additions & 0 deletions modules/leaderboard/db/migrations/migration_lock.toml
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"
30 changes: 30 additions & 0 deletions modules/leaderboard/db/schema.prisma
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)
}
27 changes: 27 additions & 0 deletions modules/leaderboard/module.yaml
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
48 changes: 48 additions & 0 deletions modules/leaderboard/scripts/delete_board.ts
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 { };
}
66 changes: 66 additions & 0 deletions modules/leaderboard/scripts/get_entry.ts
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,
};
}
50 changes: 50 additions & 0 deletions modules/leaderboard/scripts/get_range.ts
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),
};
}
81 changes: 81 additions & 0 deletions modules/leaderboard/scripts/init_board.ts
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) };
}
27 changes: 27 additions & 0 deletions modules/leaderboard/scripts/list_boards.ts
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) };
}
Loading

0 comments on commit 69afaab

Please sign in to comment.