Restructuring calculator data in database and improve api surface

This commit is contained in:
Dominic Ferrando
2026-07-08 12:11:25 -04:00
parent 91a14252cf
commit cec0d17312
8 changed files with 199 additions and 87 deletions
+22 -39
View File
@@ -1,8 +1,8 @@
import { LevelCalculator, Standards, type StandardsParams, StandardsParamsSchema, StandardsDataSchema, type StandardsConfig, type LevelCalculatorOutput } from "@blade-and-brawn/calculator";
import { LevelCalculator, Standards, type StandardsParams, StandardsParamsSchema, StandardsDataSchema, type StandardsData, type StandardsConfig, type LevelCalculatorOutput } from "@blade-and-brawn/calculator";
import type { ActivityPerformance, Player } from "@blade-and-brawn/domain";
import { log } from "../util";
import { db } from "../database/db";
import { Value } from "@sinclair/typebox/value";
import { log } from "../util";
export class CalculatorUnavailableError extends Error {
status: number = 503
@@ -10,9 +10,10 @@ export class CalculatorUnavailableError extends Error {
export class CalculatorService {
private calculator: LevelCalculator | null = null;
Standards: StandardsService = new StandardsService();
Standards: CalculatorStandardsService;
constructor() {
constructor(private name: string) {
this.Standards = new CalculatorStandardsService(this.name);
// eagerly load
this.refresh().catch((err) => log.error({ err }, "failed to load initial standards config"));
}
@@ -38,15 +39,15 @@ export class CalculatorService {
}
}
class StandardsService {
Config: StandardsConfigService;
class CalculatorStandardsService {
Config: CalculatorStandardsConfigService;
constructor() {
this.Config = new StandardsConfigService();
constructor(private name: string) {
this.Config = new CalculatorStandardsConfigService(this.name);
}
}
class StandardsConfigService {
class CalculatorStandardsConfigService {
private cache = {
refreshInterval: 30_000,
lastRefreshedAt: 0,
@@ -62,39 +63,27 @@ class StandardsConfigService {
},
}
async create(datasetId: string, params: StandardsParams) {
await db.insertInto("standards_configs")
.values({ params, dataset_id: datasetId })
.execute();
}
constructor(private name: string) { }
/** Retrieves the currently selected configuration */
async get(): Promise<StandardsConfig> {
await this.refresh({ onlyIfStale: true });
if (!this.cache.data.config) throw new CalculatorUnavailableError("Unable to determine the selected standards configuration");
if (!this.cache.data.config) throw new CalculatorUnavailableError("Unable to determine the calculator's standards configuration");
return this.cache.data.config;
}
async update(id: string, datasetId: string, params: StandardsParams) {
const result = await db.updateTable("standards_configs")
.set({ params, dataset_id: datasetId })
.where("id", "=", id)
.execute();
if (result[0]?.numUpdatedRows === 0n) throw new Error(`No config with id "${id}"`);
}
async refresh(opt: { onlyIfStale?: boolean } = {}): Promise<void> {
if (!this.cache.isIncomplete() && (opt.onlyIfStale && !this.cache.isStale())) return;
try {
const result = await db.selectFrom("standards_configs")
const result = await db.selectFrom("calculators")
.innerJoin("standards_configs", "standards_configs.id", "calculators.standards_config_id")
.innerJoin("standards_datasets", "standards_datasets.id", "standards_configs.dataset_id")
.select([
"standards_configs.dataset_id as datasetId",
"standards_configs.params as params",
"standards_datasets.data as data",
])
.where("standards_configs.is_selected", "=", true)
.where("calculators.name", "=", this.name)
.limit(1)
.executeTakeFirstOrThrow({ errorConstructor: () => new Error("No standards config selected") });
Value.Assert(StandardsParamsSchema, result.params);
@@ -109,21 +98,15 @@ class StandardsConfigService {
// config; a failed attempt must not make stale data look fresh
this.cache.lastRefreshedAt = Date.now();
} catch (err) {
throw new CalculatorUnavailableError("Unable to determine the selected standards configuration", { cause: err });
throw new CalculatorUnavailableError("Unable to determine the calculator's standards configuration", { cause: err });
}
}
async switch(id: string) {
await db.transaction().execute(async (trx) => {
await trx.updateTable("standards_configs")
.set({ is_selected: false })
.where("is_selected", "=", true)
.execute();
const result = await trx.updateTable("standards_configs")
.set({ is_selected: true })
.where("id", "=", id)
.execute();
if (result[0]?.numUpdatedRows === 0n) throw new Error(`No standards config with id "${id}"`);
});
async switch(standardsConfigId: string) {
const result = await db.updateTable("calculators")
.set({ standards_config_id: standardsConfigId })
.where("name", "=", this.name)
.execute();
if (result[0]?.numUpdatedRows === 0n) throw new Error(`No calculator with name "${this.name}"`);
}
}
+82
View File
@@ -0,0 +1,82 @@
import { StandardsDataSchema, StandardsParamsSchema, type StandardsConfig, type StandardsData, type StandardsParams } from "@blade-and-brawn/calculator";
import { db } from "../database/db";
import { Value } from "@sinclair/typebox/value";
export class StandardsService {
Configs: StandardsConfigService;
Datasets: StandardsDatasetService;
constructor() {
this.Configs = new StandardsConfigService();
this.Datasets = new StandardsDatasetService();
}
}
class StandardsConfigService {
async create(name: string, datasetId: string, params: StandardsParams): Promise<{ id: string }> {
return await db.insertInto("standards_configs")
.values({ name, params, dataset_id: datasetId })
.returning("id")
.executeTakeFirstOrThrow();
}
async list(): Promise<{ id: string, name: string, datasetId: string }[]> {
return await db.selectFrom("standards_configs")
.select([
"id",
"name",
"dataset_id as datasetId",
])
.orderBy("name")
.execute();
}
async get(id: string): Promise<StandardsConfig & { id: string, name: string, datasetId: string }> {
const result = await db.selectFrom("standards_configs")
.innerJoin("standards_datasets", "standards_datasets.id", "standards_configs.dataset_id")
.select([
"standards_configs.id as id",
"standards_configs.name as name",
"standards_configs.dataset_id as datasetId",
"standards_configs.params as params",
"standards_datasets.data as data",
])
.where("standards_configs.id", "=", id)
.executeTakeFirstOrThrow({ errorConstructor: () => new Error(`No config with id "${id}"`) });
Value.Assert(StandardsParamsSchema, result.params);
Value.Assert(StandardsDataSchema, result.data);
return {
id: result.id,
name: result.name,
datasetId: result.datasetId,
params: result.params,
data: result.data,
};
}
async update(id: string, name: string, datasetId: string, params: StandardsParams) {
const result = await db.updateTable("standards_configs")
.set({ name, params, dataset_id: datasetId })
.where("id", "=", id)
.execute();
if (result[0]?.numUpdatedRows === 0n) throw new Error(`No config with id "${id}"`);
}
}
class StandardsDatasetService {
async list(): Promise<{ id: string, name: string }[]> {
return await db.selectFrom("standards_datasets")
.select(["id", "name"])
.orderBy("name")
.execute();
}
async get(id: string): Promise<{ id: string, name: string, data: StandardsData }> {
const result = await db.selectFrom("standards_datasets")
.select(["id", "name", "data"])
.where("id", "=", id)
.executeTakeFirstOrThrow({ errorConstructor: () => new Error(`No dataset with id "${id}"`) });
Value.Assert(StandardsDataSchema, result.data);
return { id: result.id, name: result.name, data: result.data };
}
}