We’re going to build a simple leaderboard module from scratch. Users will be able to authenticate, submit leaderboard scores, and retrieve the top scores.
This will teach you how to build a simple leaderboard module from scratch & all related Open Game Backend concepts including:
Create a module
Setup your database
Write scripts
Write a test
Make sure you’ve installed Open Game Backend as described here.
This will create a directory modules/my_leaderboard and updated
backend.json to include the module. The important files we’ll look at are
modules/my_leaderboard/db/schema.prisma, modules/my_leaderboard/scripts/,
and modules/my_leaderboard/tests/.
Edit your modules/my_leaderboard/db/schema.prisma file to look like this:
modules/my_leaderboard/db/schema.prisma
datasource db { provider = "postgresql" url = env("DATABASE_URL")}model Scores { id String @id @default(uuid()) @db.Uuid createdAt DateTime @default(now()) @db.Timestamp userId String @db.Uuid score Int @@index([score])}
model Scores creates a new table that will store all of the scores on the leadboard
id is the unique identifier for each score.
String is the type of the column
@id indicates that this is the primary key of the table
@default(uuid()) indicates that this column will be automatically set to a UUID when a new row is created
@db.Uuid indicates that this is stored as UUID type. UUIDs are universally unique (unlike integers) and take a small amount of storage (unlike strings).
createdAt is the time the score was created
DateTime is the type of the column
@default(now()) indicates that this column will be automatically set to the current time when a new row is created
@db.Timestamp indicates that this is stored as a timestamp type. This is a good way to store dates and times in a database.
userId is the user who submitted the score
String is the type of the column
@db.Uuid indicates that this is stored as UUID type.
score is the score itself
Int is the type of the column
@@index([score]) creates an index on the score column.
This means the database will keep track of the scores in order so we can query them efficiently.
import { ScriptContext } from "../module.gen.ts"; imports the ScriptContext.
The module.gen.ts file is generated by Open Game Backend to provide a type-safe interface for the database (see schema.prisma), calling other modules, and reading the module config.
export interface Request {} and export interface Response {} define the input and output of the script.
Open Game Backend will automatically generate strict schema validation for these types. This reduces unintentional errors and potential exploits/crashes.
export async function run(...) is the main function of the script.
This is where you write the code that will be executed when the script is called.
2
Add dependencies & make public
Update the modules/my_leaderboard/module.json file to look like this:
This will rate limit the script to 1 request every 15 seconds. If the rate limit is exceeded, a rate_limit_exceeded error will be thrown. See the rate limit docs for more details.
You will be prompted to apply your schema changes to the database. Name the migration init (this name doesn’t matter):
Migrate Database develop (my_leaderboard)Prisma schema loaded from schema.prismaDatasource "db": PostgreSQL database "my_leaderboard", schema "public" at "host.docker.internal:5432"? Enter a name for the new migration: › init
Tests are helpful for validating your module works as expected before running in to the issue down the road. Testing is optional, but strongly encouraged.
All modules provided in the default registry are thoroughly tested.
1
Create test
opengb create test my_leaderboard e2e
e2e stands for “end to end” test. E2E tests simulate real-world scenarios involving multiple parts of a system. These tend to be comprehensive and catch the most bugs.
2
Write test
Update modules/my_leaderboard/tests/e2e.ts to look like this:
modules/my_leaderboard/tests/e2e.ts
import { test, TestContext } from "../module.gen.ts";import { assertEquals } from "https://deno.land/[email protected]/assert/mod.ts";import { faker } from "https://deno.land/x/[email protected]/mod.ts";test("e2e", async (ctx: TestContext) => { // Create user & token to authenticate with const { user } = await ctx.modules.users.createUser({}); const { token } = await ctx.modules.users.createUserToken({ userId: user.id, }); // Create some scores const scores = []; for (let i = 0; i < 10; i++) { const score = faker.random.number({ min: 0, max: 100 }); await ctx.modules.myLeaderboard.submitScore({ userToken: token.token, score: score, }); scores.push(score); } // Get top scores scores.sort((a, b) => b - a); const topScores = await ctx.modules.myLeaderboard.getTopScores({ count: 5 }); assertEquals(topScores.scores.length, 5); for (let i = 0; i < 5; i++) { assertEquals(topScores.scores[i].score, scores[i]); }});
3
Run test
In the same terminal, run:
opengb test
You should see this output once complete:
...test logs...----- output end -----e2e ... ok (269ms)ok | 1 passed | 0 failed (280ms)