diff --git a/apps/api/src/database/migrations/2026-06-03.ts b/apps/api/src/database/migrations/2026-06-03.ts index e0a0bc2..371fb43 100644 --- a/apps/api/src/database/migrations/2026-06-03.ts +++ b/apps/api/src/database/migrations/2026-06-03.ts @@ -31,7 +31,7 @@ export async function up(db: Kysely): Promise { await db.schema.createTable("calculator_configs") .$call(addDefaultColumns) .addColumn("standards_config", "jsonb", (cb) => cb.notNull()) - .addColumn("is_selected", "boolean", (cb) => cb.notNull()) + .addColumn("is_selected", "boolean", (cb) => cb.notNull().defaultTo(false)) .execute(); // INDEX: ONE_SELECTED_CALCULATOR_CONFIG diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts index a44b175..29418a8 100644 --- a/apps/api/src/server.ts +++ b/apps/api/src/server.ts @@ -18,6 +18,7 @@ import { CommerceService } from "./services/commerce"; import cluster from "node:cluster"; import { randomUUIDv7 } from "bun"; import { CalculatorService } from "./services/calculator"; +import { StandardsConfigSchema } from "@blade-and-brawn/calculator"; // SERVICES // ----------- @@ -100,6 +101,7 @@ export const app = new Elysia() // CALCULATOR .group("/calculator", (app) => app + // Non-authenticated .post("/calculate", async ({ body }) => { return { levels: calculatorService.calculate(body.player, body.activityPerformances) }; }, { @@ -108,15 +110,30 @@ export const app = new Elysia() activityPerformances: t.Array(ActivityPerformanceSchema) }), }) + // Authenticated .resolve(async ({ jwt, cookie: { auth } }) => { const token = auth.value && await jwt.verify(auth.value); if (!token || !token.sessionId) throw status(401, "Unauthorized"); return { sessionId: token.sessionId.toString() }; }) - .post("/config", () => { }) + .put("/config/:id", async ({ params: { id }, body: { standardsConfig } }) => { + await calculatorService.setConfig(id, standardsConfig); + }, { + params: t.Object({ id: t.String() }), + body: t.Object({ standardsConfig: StandardsConfigSchema }) + }) + .post("/config", async ({ body: { standardsConfig } }) => { + await calculatorService.createConfig(standardsConfig); + }, { + body: t.Object({ standardsConfig: StandardsConfigSchema }) + }) + .post("/config/:id/select", async ({ params: { id } }) => { + await calculatorService.selectConfig(id); + }, { + params: t.Object({ id: t.String() }) + }) .get("/config", async () => { - const config = await calculatorService.getConfig(); - return config; + return await calculatorService.getSelectedConfig(); }) ) @@ -150,8 +167,8 @@ export const app = new Elysia() }); }, { params: t.Object({ printfulProductId: t.Optional(t.Numeric()) }) }), ) - .get("/:printfulProductId", async ({ params }) => { - const printfulProduct = await commerceService.printful.Products.get(params.printfulProductId); + .get("/:printfulProductId", async ({ params: { printfulProductId } }) => { + const printfulProduct = await commerceService.printful.Products.get(printfulProductId); if (!printfulProduct) throw new NotFoundError("Missing printful product"); const webflowProductId = printfulProduct.sync_product.external_id.split("-")[0]; diff --git a/apps/api/src/services/calculator.ts b/apps/api/src/services/calculator.ts index 62610a2..b34ea4a 100644 --- a/apps/api/src/services/calculator.ts +++ b/apps/api/src/services/calculator.ts @@ -21,13 +21,41 @@ export class CalculatorService { return this.levelCalculator.calculate(player, activityPerformances); } - async getConfig(opt: { skipCache?: boolean } = {}): Promise { + async getSelectedConfig(opt: { skipCache?: boolean } = {}): Promise { if (!opt.skipCache) return this.levelCalculator.standards.cfg; await this.refreshConfig(); return this.levelCalculator.standards.cfg; } + async selectConfig(id: string) { + await db.transaction().execute(async (trx) => { + await trx.updateTable("calculator_configs") + .set({ is_selected: false }) + .where("is_selected", "=", true) + .execute(); + const result = await trx.updateTable("calculator_configs") + .set({ is_selected: true }) + .where("id", "=", id) + .execute(); + if (result[0]?.numUpdatedRows === 0n) throw new Error(`No config with id "${id}"`); + }); + } + + async setConfig(id: string, standardsConfig: StandardsConfig) { + const result = await db.updateTable("calculator_configs") + .set({ standards_config: standardsConfig }) + .where("id", "=", id) + .execute(); + if (result[0]?.numUpdatedRows === 0n) throw new Error(`No config with id "${id}"`); + } + + async createConfig(standardsConfig: StandardsConfig) { + await db.insertInto("calculator_configs") + .values({ standards_config: standardsConfig }) + .execute(); + } + async refreshConfig(opt: { onlyIfStale?: boolean } = {}) { if (opt.onlyIfStale) if (Date.now() - this.lastRefreshedAt < CalculatorService.CONFIG_REFRESH_INTERVAL_MS) return; diff --git a/packages/calculator/src/default-config.json b/packages/calculator/src/default-config.json new file mode 100644 index 0000000..a71140c --- /dev/null +++ b/packages/calculator/src/default-config.json @@ -0,0 +1,79 @@ +{ + "global": { + "maxLevel": 100 + }, + "activity": { + "BackSquat": { + "enableGeneration": true, + "weightModifier": 0, + "weightSkew": 0, + "ageModifier": 0, + "difficultyModifier": 0.3, + "peakAge": 0, + "stretch": { + "upper": 0, + "lower": 1 + } + }, + "BenchPress": { + "enableGeneration": true, + "weightModifier": 0, + "weightSkew": 0, + "ageModifier": 0, + "difficultyModifier": 0.05, + "peakAge": 0, + "stretch": { + "upper": 1, + "lower": 0 + } + }, + "Deadlift": { + "enableGeneration": true, + "weightModifier": 0, + "weightSkew": 0, + "ageModifier": 0, + "difficultyModifier": -0.05, + "peakAge": 0, + "stretch": { + "upper": 1, + "lower": 0 + } + }, + "Run": { + "enableGeneration": true, + "weightModifier": 0.1, + "weightSkew": 0, + "ageModifier": 0, + "difficultyModifier": 0.35, + "peakAge": 0, + "stretch": { + "upper": 1, + "lower": 1 + } + }, + "BroadJump": { + "enableGeneration": true, + "weightModifier": -0.1, + "weightSkew": 0, + "ageModifier": -0.25, + "difficultyModifier": -0.15, + "peakAge": 23, + "stretch": { + "upper": 3, + "lower": 1 + } + }, + "ConeDrill": { + "enableGeneration": true, + "weightModifier": -0.1, + "weightSkew": 0, + "ageModifier": -0.25, + "difficultyModifier": 0.15, + "peakAge": 23, + "stretch": { + "upper": 0, + "lower": 2 + } + } + } +}