diff --git a/scripts/diagnose-join.ts b/scripts/diagnose-join.ts new file mode 100644 index 0000000..446a92e --- /dev/null +++ b/scripts/diagnose-join.ts @@ -0,0 +1,33 @@ +import 'dotenv/config'; +import chalk from 'chalk'; +import util from 'node:util'; +import { client } from '../src/db/typesense.ts'; + +const variants = [ + '$skus(*, $cards(*))', + '$skus(*,$cards(*))', + '$skus(*, card_id, $cards(*))', + '$skus(*, $cards(*, strategy:nest))', + '$skus(*, $cards(*, strategy:merge))', +]; + +const debug = await client.debug.retrieve(); +console.log(chalk.cyan(`Typesense server version: ${debug.version}`)); +console.log(); + +for (const include of variants) { + console.log(chalk.yellow(`include_fields: ${include}`)); + try { + const res: any = await client.collections('inventories').documents().search({ + q: '*', + query_by: 'content', + per_page: 1, + include_fields: include, + }); + const doc = res.hits?.[0]?.document; + console.log(util.inspect(doc, { depth: null, colors: false })); + } catch (err: any) { + console.log(chalk.red(` ERROR: ${err.message ?? err}`)); + } + console.log(); +} diff --git a/scripts/pokemon-helper.ts b/scripts/pokemon-helper.ts index 1ba7db0..6de5701 100644 --- a/scripts/pokemon-helper.ts +++ b/scripts/pokemon-helper.ts @@ -4,6 +4,8 @@ import type { DBInstance } from '../src/db/index.ts'; import fs from "node:fs/promises"; import { sql } from 'drizzle-orm' +import * as util from 'util'; + const DollarToInt = (dollar: any) => { if (dollar === null) return null; @@ -83,7 +85,7 @@ export const createSkuCollection = async () => { { name: 'highestPrice', type: 'int32', optional: true }, { name: 'lowestPrice', type: 'int32', optional: true }, { name: 'marketPrice', type: 'int32', optional: true }, - { name: 'card_id', type: 'string', reference: 'cards.id' }, + { name: 'card_id', type: 'string', reference: 'cards.id', async_reference: true }, ] }); console.log(chalk.green('Collection "skus" created successfully.')); @@ -102,7 +104,15 @@ export const createInventoryCollection = async () => { { name: 'id', type: 'string' }, { name: 'userId', type: 'string' }, { name: 'catalogName', type: 'string' }, - { name: 'sku_id', type: 'string', reference: 'skus.id' }, + { name: 'card_id', type: 'string', reference: 'cards.id', async_reference: true }, + { name: 'sku_id', type: 'string', reference: 'skus.id', async_reference: true }, + // content,setName,productLineName,rarityName,energyType,cardType from cards for searching + { name: 'content', type: 'string', token_separators: ['/'] }, + { name: 'setName', type: 'string' }, + { name: 'productLineName', type: 'string' }, + { name: 'rarityName', type: 'string' }, + { name: 'energyType', type: 'string' }, + { name: 'cardType', type: 'string' }, ] }); console.log(chalk.green('Collection "inventories" created successfully.')); @@ -148,17 +158,33 @@ export const upsertSkuCollection = async (db:DBInstance) => { lowestPrice: DollarToInt(sku.lowestPrice), marketPrice: DollarToInt(sku.marketPrice), card_id: sku.cardId.toString(), - })), { action: 'upsert' }); + })), { action: 'upsert' }); console.log(chalk.green('Collection "skus" indexed successfully.')); } export const upsertInventoryCollection = async (db:DBInstance) => { - const inv = await db.query.inventory.findMany(); + const inv = await db.query.inventory.findMany({ + with: { sku: { with: { card: { with: { set: true } } } } } + }); await client.collections('inventories').documents().import(inv.map(i => ({ id: i.inventoryId, userId: i.userId, catalogName: i.catalogName, + card_id: i.sku?.cardId.toString(), sku_id: i.skuId.toString(), + productLineName: i.sku?.card?.productLineName, + rarityName: i.sku?.card?.rarityName, + setName: i.sku?.card?.set?.setName || "", + cardType: i.sku?.card?.cardType || "", + energyType: i.sku?.card?.energyType || "", + content: [ + i.sku?.card?.productName, + i.sku?.card?.productLineName, + i.sku?.card?.set?.setName || "", + i.sku?.card?.number, + i.sku?.card?.rarityName, + i.sku?.card?.artist || "" + ].join(' '), })), { action: 'upsert' }); console.log(chalk.green('Collection "inventories" indexed successfully.')); } diff --git a/scripts/reindex.ts b/scripts/reindex.ts index 71e4eed..22fc3ab 100644 --- a/scripts/reindex.ts +++ b/scripts/reindex.ts @@ -3,12 +3,12 @@ import { db, ClosePool } from '../src/db/index.ts'; import * as Indexing from './pokemon-helper.ts'; -//await Indexing.createCardCollection(); -//await Indexing.createSkuCollection(); +// await Indexing.createCardCollection(); +await Indexing.createSkuCollection(); await Indexing.createInventoryCollection(); -//await Indexing.upsertCardCollection(db); -//await Indexing.upsertSkuCollection(db); +// await Indexing.upsertCardCollection(db); +await Indexing.upsertSkuCollection(db); await Indexing.upsertInventoryCollection(db); await ClosePool(); console.log(chalk.green('Pokémon reindex complete.')); diff --git a/src/db/relations.ts b/src/db/relations.ts index 73157bd..867180c 100644 --- a/src/db/relations.ts +++ b/src/db/relations.ts @@ -24,7 +24,7 @@ export const relations = defineRelations(schema, (r) => ({ inventories: r.many.inventory(), }, inventory: { - card: r.one.skus({ + sku: r.one.skus({ from: r.inventory.skuId, to: r.skus.skuId, }), diff --git a/src/pages/api/inventory.ts b/src/pages/api/inventory.ts index bc313be..c055968 100644 --- a/src/pages/api/inventory.ts +++ b/src/pages/api/inventory.ts @@ -85,7 +85,7 @@ const getInventory = async (userId: string, cardId: number) => { } -const addToInventory = async (userId: string, skuId: number, purchasePrice: number, quantity: number, note: string, catalogName: string) => { +const addToInventory = async (userId: string, cardId: number, skuId: number, purchasePrice: number, quantity: number, note: string, catalogName: string) => { // First add to database const inv = await db.insert(inventory).values({ userId: userId, @@ -95,13 +95,37 @@ const addToInventory = async (userId: string, skuId: number, purchasePrice: numb quantity: quantity, note: note, }).returning(); + // Get card details from the database to add to Typesense + const card = await db.query.cards.findFirst({ + where: { cardId: cardId }, + with: { set: true }, + }); + + try { // And then add to Typesense for searching await client.collections('inventories').documents().import(inv.map(i => ({ id: i.inventoryId, userId: i.userId, catalogName: i.catalogName, sku_id: i.skuId.toString(), + productLineName: card?.productLineName, + rarityName: card?.rarityName, + setName: card?.set?.setName || "", + cardType: card?.cardType || "", + energyType: card?.energyType || "", + card_id: card?.cardId.toString() || "", + content: [ + card?.productName, + card?.productLineName, + card?.set?.setName || "", + card?.number, + card?.rarityName, + card?.artist || "" + ].join(' '), }))); + } catch (error) { + console.error('Error adding inventory to Typesense:', error); + } } const removeFromInventory = async (inventoryId: string) => { @@ -141,7 +165,7 @@ export const POST: APIRoute = async ({ request, locals }) => { if (!skuId) { return new Response('SKU not found for card', { status: 404 }); } - await addToInventory(userId!, skuId, purchasePrice, quantity, note, catalogName); + await addToInventory(userId!, cardId, skuId, purchasePrice, quantity, note, catalogName); break; case 'remove': diff --git a/src/pages/dashboard.astro b/src/pages/dashboard.astro index 1ebb508..bcb2696 100644 --- a/src/pages/dashboard.astro +++ b/src/pages/dashboard.astro @@ -5,304 +5,14 @@ import NavItems from "../components/NavItems.astro"; import Footer from "../components/Footer.astro"; import FirstEditionIcon from "../components/FirstEditionIcon.astro"; -// Mock inventory using the same schema as the Typesense cards collection. -// skus mirror the real shape: marketPrice is in cents (÷100 = dollars). -const inventory = [ - { - productId: "42382", - productName: "Charizard", - setName: "Base Set", - setCode: "BS", - number: "4/102", - rarityName: "Rare Holo", - energyType: "Fire", - variant: "1st Edition", - qty: 2, - purchasePrice: 32000, - skus: [ - { condition: "Near Mint", marketPrice: 40000 }, - { condition: "Lightly Played", marketPrice: 31000 }, - { condition: "Moderately Played", marketPrice: 22000 }, - { condition: "Heavily Played", marketPrice: 14000 }, - { condition: "Damaged", marketPrice: 8500 }, - ], - }, - { - productId: "146682", - productName: "Pikachu", - setName: "Shining Legends", - setCode: "SLG", - number: "SM70", - rarityName: "Promo", - energyType: "Lightning", - variant: "Normal", - qty: 5, - purchasePrice: 1500, - skus: [ - { condition: "Near Mint", marketPrice: 2000 }, - { condition: "Lightly Played", marketPrice: 1500 }, - { condition: "Moderately Played", marketPrice: 1100 }, - { condition: "Heavily Played", marketPrice: 700 }, - { condition: "Damaged", marketPrice: 400 }, - ], - }, - { - productId: "246723", - productName: "Umbreon VMAX", - setName: "Evolving Skies", - setCode: "EVS", - number: "215/203", - rarityName: "Secret Rare", - energyType: "Darkness", - variant: "Alternate Art", - qty: 1, - purchasePrice: 8500, - skus: [ - { condition: "Near Mint", marketPrice: 11500 }, - { condition: "Lightly Played", marketPrice: 9000 }, - { condition: "Moderately Played", marketPrice: 6500 }, - { condition: "Heavily Played", marketPrice: 4000 }, - { condition: "Damaged", marketPrice: 2000 }, - ], - }, - { - productId: "197660", - productName: "Gyarados", - setName: "Hidden Fates", - setCode: "HIF", - number: "SV19/SV94", - rarityName: "Shiny Holo Rare", - energyType: "Water", - variant: "Shiny", - qty: 3, - purchasePrice: 2500, - skus: [ - { condition: "Near Mint", marketPrice: 3000 }, - { condition: "Lightly Played", marketPrice: 2300 }, - { condition: "Moderately Played", marketPrice: 1600 }, - { condition: "Heavily Played", marketPrice: 900 }, - { condition: "Damaged", marketPrice: 500 }, - ], - }, - { - productId: "246733", - productName: "Rayquaza VMAX", - setName: "Evolving Skies", - setCode: "EVS", - number: "218/203", - rarityName: "Secret Rare", - energyType: "Dragon", - variant: "Alternate Art", - qty: 2, - purchasePrice: 6500, - skus: [ - { condition: "Near Mint", marketPrice: 8800 }, - { condition: "Lightly Played", marketPrice: 7000 }, - { condition: "Moderately Played", marketPrice: 5000 }, - { condition: "Heavily Played", marketPrice: 3200 }, - { condition: "Damaged", marketPrice: 1800 }, - ], - }, - { - productId: "264218", - productName: "Eevee", - setName: "Sword & Shield", - setCode: "SSH", - number: "TG07/TG30", - rarityName: "Trainer Gallery", - energyType: "Colorless", - variant: "Normal", - qty: 10, - purchasePrice: 800, - skus: [ - { condition: "Near Mint", marketPrice: 900 }, - { condition: "Lightly Played", marketPrice: 700 }, - { condition: "Moderately Played", marketPrice: 500 }, - { condition: "Heavily Played", marketPrice: 300 }, - { condition: "Damaged", marketPrice: 150 }, - ], - }, - { - productId: "451834", - productName: "Lugia V", - setName: "Silver Tempest", - setCode: "SIT", - number: "186/195", - rarityName: "Ultra Rare", - energyType: "Colorless", - variant: "Alternate Art", - qty: 1, - purchasePrice: 4500, - skus: [ - { condition: "Near Mint", marketPrice: 5800 }, - { condition: "Lightly Played", marketPrice: 4600 }, - { condition: "Moderately Played", marketPrice: 3200 }, - { condition: "Heavily Played", marketPrice: 2000 }, - { condition: "Damaged", marketPrice: 1000 }, - ], - }, - { - productId: "106997", - productName: "Blastoise", - setName: "Base Set", - setCode: "BS", - number: "2/102", - rarityName: "Rare Holo", - energyType: "Water", - variant: "Shadowless", - qty: 1, - purchasePrice: 18000, - skus: [ - { condition: "Near Mint", marketPrice: 24000 }, - { condition: "Lightly Played", marketPrice: 18500 }, - { condition: "Moderately Played", marketPrice: 13000 }, - { condition: "Heavily Played", marketPrice: 8000 }, - { condition: "Damaged", marketPrice: 4500 }, - ], - }, - { - productId: "253265", - productName: "Espeon VMAX", - setName: "Evolving Skies", - setCode: "EVS", - number: "205/203", - rarityName: "Secret Rare", - energyType: "Psychic", - variant: "Alternate Art", - qty: 2, - purchasePrice: 7000, - skus: [ - { condition: "Near Mint", marketPrice: 9200 }, - { condition: "Lightly Played", marketPrice: 7300 }, - { condition: "Moderately Played", marketPrice: 5200 }, - { condition: "Heavily Played", marketPrice: 3300 }, - { condition: "Damaged", marketPrice: 1600 }, - ], - }, - { - productId: "253266", - productName: "Gengar VMAX", - setName: "Fusion Strike", - setCode: "FST", - number: "271/264", - rarityName: "Secret Rare", - energyType: "Psychic", - variant: "Alternate Art", - qty: 1, - purchasePrice: 5500, - skus: [ - { condition: "Near Mint", marketPrice: 4800 }, - { condition: "Lightly Played", marketPrice: 3800 }, - { condition: "Moderately Played", marketPrice: 2700 }, - { condition: "Heavily Played", marketPrice: 1700 }, - { condition: "Damaged", marketPrice: 900 }, - ], - }, - { - productId: "226432", - productName: "Pikachu VMAX", - setName: "Vivid Voltage", - setCode: "VIV", - number: "188/185", - rarityName: "Secret Rare", - energyType: "Lightning", - variant: "Rainbow Rare", - qty: 3, - purchasePrice: 3200, - skus: [ - { condition: "Near Mint", marketPrice: 4100 }, - { condition: "Lightly Played", marketPrice: 3200 }, - { condition: "Moderately Played", marketPrice: 2300 }, - { condition: "Heavily Played", marketPrice: 1400 }, - { condition: "Damaged", marketPrice: 750 }, - ], - }, - { - productId: "253275", - productName: "Mew VMAX", - setName: "Fusion Strike", - setCode: "FST", - number: "269/264", - rarityName: "Secret Rare", - energyType: "Psychic", - variant: "Alternate Art", - qty: 2, - purchasePrice: 4200, - skus: [ - { condition: "Near Mint", marketPrice: 5600 }, - { condition: "Lightly Played", marketPrice: 4400 }, - { condition: "Moderately Played", marketPrice: 3100 }, - { condition: "Heavily Played", marketPrice: 2000 }, - { condition: "Damaged", marketPrice: 1000 }, - ], - }, - { - productId: "478077", - productName: "Darkrai VSTAR", - setName: "Astral Radiance", - setCode: "ASR", - number: "189/189", - rarityName: "Secret Rare", - energyType: "Darkness", - variant: "Gold", - qty: 1, - purchasePrice: 3800, - skus: [ - { condition: "Near Mint", marketPrice: 3200 }, - { condition: "Lightly Played", marketPrice: 2500 }, - { condition: "Moderately Played", marketPrice: 1800 }, - { condition: "Heavily Played", marketPrice: 1100 }, - { condition: "Damaged", marketPrice: 600 }, - ], - }, - { - productId: "477060", - productName: "Leafeon VSTAR", - setName: "Pokémon GO", - setCode: "PGO", - number: "076/078", - rarityName: "Ultra Rare", - energyType: "Grass", - variant: "Normal", - qty: 4, - purchasePrice: 1200, - skus: [ - { condition: "Near Mint", marketPrice: 1800 }, - { condition: "Lightly Played", marketPrice: 1400 }, - { condition: "Moderately Played", marketPrice: 1000 }, - { condition: "Heavily Played", marketPrice: 600 }, - { condition: "Damaged", marketPrice: 300 }, - ], - }, - { - productId: "478100", - productName: "Giratina VSTAR", - setName: "Lost Origin", - setCode: "LOR", - number: "196/196", - rarityName: "Secret Rare", - energyType: "Dragon", - variant: "Alternate Art", - qty: 1, - purchasePrice: 5200, - skus: [ - { condition: "Near Mint", marketPrice: 7800 }, - { condition: "Lightly Played", marketPrice: 6100 }, - { condition: "Moderately Played", marketPrice: 4400 }, - { condition: "Heavily Played", marketPrice: 2800 }, - { condition: "Damaged", marketPrice: 1400 }, - ], - }, -]; -// Helpers -const nmPrice = (card: typeof inventory[0]) => (card.skus[0]?.marketPrice ?? 0) / 100; -const nmPurchase = (card: typeof inventory[0]) => card.purchasePrice / 100; -const gain = (card: typeof inventory[0]) => nmPrice(card) - nmPurchase(card); -const totalQty = inventory.reduce((s, c) => s + c.qty, 0); -const totalValue = inventory.reduce((s, c) => s + nmPrice(c) * c.qty, 0); -const totalGain = inventory.reduce((s, c) => s + gain(c) * c.qty, 0); +// const totalQty = inventory.reduce((s, c) => s + c.qty, 0); +// const totalValue = inventory.reduce((s, c) => s + nmPrice(c) * c.qty, 0); +// const totalGain = inventory.reduce((s, c) => s + gain(c) * c.qty, 0); +const totalQty = 1234; +const totalValue = 5678.90; +const totalGain = 1234.56; --- @@ -406,72 +116,10 @@ const totalGain = inventory.reduce((s, c) => s + gain(c) * c.qty, 0);
-
- {inventory.map(card => { - const market = nmPrice(card); - const purchase = nmPurchase(card); - const diff = market - purchase; - const pct = purchase > 0 ? (diff / purchase) * 100 : 0; - const isGain = diff >= 0; - - return ( -
-
-
-
- {card.productName} - - - -
-
-
- -
- - -
-
-
-
{card.setName}
-
-
-
{card.productName}
-
-
-
- {isGain ? "▲" : "▼"} - ${market.toFixed(2)} -
-
- {isGain ? "+" : "-"}${Math.abs(diff).toFixed(2)}
{isGain ? "+" : "-"}{Math.abs(pct).toFixed(2)}% -
-
-
-
- ); - })} +
-
-