30 lines
3.5 KiB
JavaScript
30 lines
3.5 KiB
JavaScript
import pg from 'pg';
|
|
export const schema=`
|
|
CREATE TABLE IF NOT EXISTS character_sheets(owner text NOT NULL,id text NOT NULL,character jsonb,revision integer NOT NULL DEFAULT 1,deleted boolean NOT NULL DEFAULT false,PRIMARY KEY(owner,id));
|
|
CREATE TABLE IF NOT EXISTS mcp_clients(id text PRIMARY KEY,metadata jsonb NOT NULL);
|
|
CREATE TABLE IF NOT EXISTS mcp_grants(id text PRIMARY KEY,payload jsonb NOT NULL,expires_at bigint NOT NULL,revoked boolean NOT NULL DEFAULT false);
|
|
CREATE TABLE IF NOT EXISTS mcp_codes(id text PRIMARY KEY,payload jsonb NOT NULL,expires_at bigint NOT NULL);
|
|
`;
|
|
export class Conflict extends Error{constructor(){super('Character changed elsewhere. Reload its current revision before saving.');this.status=409}}
|
|
export class Storage{
|
|
constructor(pool){this.pool=pool}
|
|
async init(){await this.pool.query(schema)}
|
|
async list(owner){return (await this.pool.query('SELECT id,character,revision,deleted FROM character_sheets WHERE owner=$1 ORDER BY id',[owner])).rows}
|
|
async get(owner,id){return (await this.pool.query('SELECT id,character,revision,deleted FROM character_sheets WHERE owner=$1 AND id=$2',[owner,id])).rows[0]}
|
|
async put(owner,id,character,revision,deleted=false){
|
|
// Preserve additive collections omitted by older clients, under the same account/revision guard.
|
|
if(character&&!deleted&&revision>0&&(character.inventoryItems===undefined||character.currency===undefined||character.companions===undefined)){const prior=await this.get(owner,id);for(const key of ['inventoryItems','currency','companions'])if(character[key]===undefined&&prior?.character?.[key]!==undefined)character={...character,[key]:prior.character[key]}}
|
|
|
|
const result=revision===0?await this.pool.query('INSERT INTO character_sheets(owner,id,character,deleted) VALUES($1,$2,$3,$4) ON CONFLICT(owner,id) DO NOTHING RETURNING id,character,revision,deleted',[owner,id,character,deleted]):await this.pool.query('UPDATE character_sheets SET character=$3,deleted=$4,revision=revision+1 WHERE owner=$1 AND id=$2 AND revision=$5 RETURNING id,character,revision,deleted',[owner,id,character,deleted,revision]);
|
|
if(!result.rows[0])throw new Conflict();return result.rows[0];
|
|
}
|
|
async client(id){return (await this.pool.query('SELECT metadata FROM mcp_clients WHERE id=$1',[id])).rows[0]?.metadata}
|
|
async register(id,metadata){await this.pool.query('INSERT INTO mcp_clients(id,metadata) VALUES($1,$2)',[id,metadata]);return metadata}
|
|
async grant(id,payload,expires){await this.pool.query('INSERT INTO mcp_grants(id,payload,expires_at) VALUES($1,$2,$3)',[id,payload,expires])}
|
|
async readGrant(id){const r=(await this.pool.query('SELECT payload,expires_at,revoked FROM mcp_grants WHERE id=$1',[id])).rows[0];return r&&!r.revoked&&Number(r.expires_at)>Date.now()?r.payload:undefined}
|
|
async revoke(id){await this.pool.query('UPDATE mcp_grants SET revoked=true WHERE id=$1',[id])}
|
|
async code(id,payload,seconds=300){await this.pool.query('DELETE FROM mcp_codes WHERE expires_at<$1',[Date.now()]);await this.pool.query('INSERT INTO mcp_codes(id,payload,expires_at) VALUES($1,$2,$3)',[id,payload,Date.now()+seconds*1000])}
|
|
async consume(id){const r=await this.pool.query('DELETE FROM mcp_codes WHERE id=$1 RETURNING payload,expires_at',[id]);const row=r.rows[0];return row&&Number(row.expires_at)>Date.now()?row.payload:undefined}
|
|
}
|
|
export async function productionStorage(){if(!process.env.DATABASE_URL)throw new Error('DATABASE_URL is required');const storage=new Storage(new pg.Pool({connectionString:process.env.DATABASE_URL,max:5}));await storage.init();return storage}
|