track owned inventions

This commit is contained in:
Devin Zuczek
2026-08-04 22:33:47 -04:00
parent dfb1e9ab21
commit 9f4ce07aca
10 changed files with 170 additions and 16 deletions
+8 -2
View File
@@ -2,7 +2,14 @@ import { Hono } from 'hono'
import { describeRoute, openAPIRouteHandler } from 'hono-openapi'
import { useWorkersLogger } from 'workers-tagged-logger'
import { consumeGift, createGift, getGift, getPendingGifts } from '@repo/domain'
import {
consumeGift,
createGift,
getGift,
getPendingGifts,
grantInvention,
ownsInvention,
} from '@repo/domain'
import { intVar, logger, withCleanSpec, withNotFound, withOnError } from '@repo/hono-helpers'
import { validateAndGetAccountId } from '@repo/jwt'
@@ -35,7 +42,6 @@ import {
} from './consumables-db'
import { getEquipment, grantEquipment, setEquipmentFavorited } from './equipment-db'
import { getInventory, grantItem } from './inventory-db'
import { grantInvention, ownsInvention } from './inventory-invention-db'
import {
AUTHED,
AvatarV2Dto,
-72
View File
@@ -1,72 +0,0 @@
/**
* Owned inventions on the shared `recflare` D1 database — the inventions a player has
* bought. One row per (account, invention), written at purchase time by
* `GET /api/storefronts/v2/buyInvention`.
*
* Only the invention id is stored: the invention record itself lives in the `invention`
* table, whose schema the `api` worker owns (apps/api/migrations/0002_invention.sql) on
* this same database, and copying its DTO here would leave two rows to keep in step. A
* creator is not listed here either — they own their invention through its
* `CreatorPlayerId`, and the buy path refuses to sell an invention to its own creator.
*
* This worker (`econ`) owns the table and its migration — see apps/econ/migrations/
* 0008_inventory_invention.sql.
*/
/** Schema DDL (mirror of migrations 0008_inventory_invention.sql) — also builds the table in tests. */
export const INVENTORY_INVENTION_SCHEMA_DDL: string[] = [
`CREATE TABLE IF NOT EXISTS inventory_invention (
account_id INTEGER NOT NULL,
invention_id INTEGER NOT NULL,
acquired_at TEXT NOT NULL,
PRIMARY KEY (account_id, invention_id)
)`,
]
/**
* Grant an invention to a player. INSERT OR IGNORE on the (account, invention) primary
* key: owning an invention is boolean, so a second grant keeps the original
* `acquired_at` rather than back-dating the purchase to now.
*/
export async function grantInvention(
db: D1Database,
accountId: number,
inventionId: number
): Promise<void> {
await db
.prepare(
'INSERT OR IGNORE INTO inventory_invention (account_id, invention_id, acquired_at) VALUES (?1, ?2, ?3)'
)
.bind(accountId, inventionId, new Date().toISOString())
.run()
}
/**
* Whether a player has bought an invention. This answers for BOUGHT inventions only —
* the creator of an invention owns it without a row here, so callers that mean "may use
* this invention" must check `CreatorPlayerId` as well.
*/
export async function ownsInvention(
db: D1Database,
accountId: number,
inventionId: number
): Promise<boolean> {
const row = await db
.prepare(
'SELECT 1 AS owned FROM inventory_invention WHERE account_id = ?1 AND invention_id = ?2'
)
.bind(accountId, inventionId)
.first<{ owned: number }>()
return row !== null
}
/** The ids of every invention a player has bought, oldest purchase first. */
export async function getOwnedInventionIds(db: D1Database, accountId: number): Promise<number[]> {
const { results } = await db
.prepare(
'SELECT invention_id FROM inventory_invention WHERE account_id = ?1 ORDER BY acquired_at, invention_id'
)
.bind(accountId)
.all<{ invention_id: number }>()
return results.map((r) => r.invention_id)
}
+5 -2
View File
@@ -4,7 +4,11 @@ import { beforeAll, describe, expect, test } from 'vitest'
import '../../econ.app'
import { RECEIVED_GIFT_SCHEMA_DDL } from '@repo/domain'
import {
getOwnedInventionIds,
INVENTORY_INVENTION_SCHEMA_DDL,
RECEIVED_GIFT_SCHEMA_DDL,
} from '@repo/domain'
// The `invention` table belongs to the `api` worker; buyInvention reads it, so its DDL
// is built here too (see the same cross-worker import in econ.app.ts).
@@ -20,7 +24,6 @@ import {
import { CONSUMABLE_SCHEMA_DDL, grantConsumable } from '../../consumables-db'
import { EQUIPMENT_SCHEMA_DDL } from '../../equipment-db'
import { INVENTORY_SCHEMA_DDL } from '../../inventory-db'
import { getOwnedInventionIds, INVENTORY_INVENTION_SCHEMA_DDL } from '../../inventory-invention-db'
import { OUTFIT_SCHEMA_DDL } from '../../outfit-db'
import type { Env } from '../../context'