Plugin Database
Overview
UrsaMU’s database layer is built on Deno KV. The generic class DBO<T>
provides a typed CRUD interface for any collection. Each plugin creates its own
named collections that live in the shared KV store, isolated by key prefix.
Defining a Collection
Create a db.ts file in your plugin directory:
// src/plugins/my-plugin/db.ts
import { DBO } from "../../services/Database/database.ts";
export interface IMyRecord {
id: string;
playerId: string;
text: string;
createdAt: number; // ms timestamp
}
// The string key must be globally unique — prefix with "server." by convention
export const myRecords = new DBO<IMyRecord>("server.my-plugin-records");
Import the collection wherever you need it:
import { myRecords } from "./db.ts";
DBO Methods
| Method | Signature | Description |
|---|---|---|
create |
create(record: T): Promise<T> |
Insert a record, returns the created object |
queryOne |
queryOne(query: Partial<T>): Promise<T | undefined> |
First match or undefined |
find |
find(query: Partial<T>): Promise<T[]> |
All matching records |
update |
update(query: Partial<T>, record: T): Promise<void> |
Replace a record (matched by id) |
modify |
modify(query: Partial<T>, op: "$set", data: Partial<T>): Promise<void> |
Partial field update |
delete |
delete(query: Partial<T>): Promise<void> |
Delete all matching records |
all |
all(): Promise<T[]> |
Return every record in the collection |
Update vs Modify:
update({}, record)replaces the full document.
modify({ id }, "$set", { field: value })updates only the listed fields.
Querying
Pass any subset of fields as the query object. All fields in the query must
match (logical AND). An empty object {} matches everything.
// Find all records for a specific player
const mine = await myRecords.find({ playerId: "p123" });
// Find the first record with a specific id
const one = await myRecords.queryOne({ id: "rec-7" });
// All records
const all = await myRecords.all();
Built-in Collections
The core engine exposes these pre-defined collections from
src/services/Database/index.ts:
| Export | KV Key | Contents |
|---|---|---|
dbojs |
server.objects |
All game objects (players, rooms, exits, things) |
counters |
server.counters |
Auto-increment sequences |
chans |
server.chans |
Communication channels |
mail |
server.mail |
In-game mail messages |
bboard |
server.bboard |
Bulletin board posts |
scenes |
server.scenes |
Scene logs |
import { dbojs, counters } from "../../services/Database/index.ts";
// Find a player by name
const player = await dbojs.queryOne({ data: { name: "Alice" } });
// Find all objects in a room
const contents = await dbojs.find({ location: roomId });
Sequential IDs
Use the counters collection to generate monotonically increasing numbers
(e.g., ticket #1, event #2):
import { counters } from "../../services/Database/index.ts";
export async function getNextId(namespace: string): Promise<number> {
const row = await counters.queryOne({ id: namespace });
if (!row) {
await counters.create({ id: namespace, seq: 1 });
return 1;
}
const next = row.seq + 1;
await counters.modify({ id: namespace }, "$set", { seq: next });
return next;
}
Call it with a unique namespace per plugin:
const num = await getNextId("my-plugin.tickets");
Full Example
A plugin that lets players save short bookmarks:
// db.ts
import { DBO } from "../../services/Database/database.ts";
export interface IBookmark {
id: string;
playerId: string;
playerName: string;
label: string;
url: string;
createdAt: number;
}
export const bookmarks = new DBO<IBookmark>("server.bookmarks");
// commands.ts
import { addCmd } from "../../services/commands/cmdParser.ts";
import type { IUrsamuSDK } from "../../@types/UrsamuSDK.ts";
import { bookmarks } from "./db.ts";
addCmd({
name: "+bookmark",
pattern: /^\+bookmark(?:\/(\S+))?\s*(.*)/i,
lock: "connected",
exec: async (u: IUrsamuSDK) => {
const sw = (u.cmd.args[0] || "").toLowerCase();
const arg = (u.cmd.args[1] || "").trim();
// +bookmark/list
if (sw === "list") {
const mine = await bookmarks.find({ playerId: u.me.id });
if (!mine.length) { u.send("No bookmarks saved."); return; }
for (const b of mine) u.send(`[${b.id.slice(-4)}] ${b.label} — ${b.url}`);
return;
}
// +bookmark/delete <id>
if (sw === "delete") {
const b = await bookmarks.queryOne({ id: arg, playerId: u.me.id });
if (!b) { u.send("Bookmark not found."); return; }
await bookmarks.delete({ id: b.id });
u.send("Deleted.");
return;
}
// +bookmark <label>=<url>
const eq = arg.indexOf("=");
if (eq === -1) { u.send("Usage: +bookmark <label>=<url>"); return; }
const label = arg.slice(0, eq).trim();
const url = arg.slice(eq + 1).trim();
if (!label || !url) { u.send("Usage: +bookmark <label>=<url>"); return; }
await bookmarks.create({
id: crypto.randomUUID(),
playerId: u.me.id,
playerName: u.me.name || u.me.id,
label,
url,
createdAt: Date.now(),
});
u.send(`Bookmark "${label}" saved.`);
},
});