feat: add single-item upsert/delete endpoints and container/slot columns
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
664bd50388
commit
749652d534
2 changed files with 232 additions and 6 deletions
|
|
@ -37,6 +37,10 @@ class Item(Base):
|
||||||
# Equipment status
|
# Equipment status
|
||||||
current_wielded_location = Column(Integer, default=0, index=True) # 0 = not equipped
|
current_wielded_location = Column(Integer, default=0, index=True) # 0 = not equipped
|
||||||
|
|
||||||
|
# Container/position tracking
|
||||||
|
container_id = Column(BigInteger, default=0) # Game container object ID (0 = character)
|
||||||
|
slot = Column(Integer, default=-1) # Slot position within container (-1 = unknown)
|
||||||
|
|
||||||
# Item state
|
# Item state
|
||||||
bonded = Column(Integer, default=0) # 0=Normal, 1=Bonded, 2=Sticky, 4=Destroy on drop
|
bonded = Column(Integer, default=0) # 0=Normal, 1=Bonded, 2=Sticky, 4=Destroy on drop
|
||||||
attuned = Column(Integer, default=0) # 0=Normal, 1=Attuned
|
attuned = Column(Integer, default=0) # 0=Normal, 1=Attuned
|
||||||
|
|
|
||||||
|
|
@ -1405,6 +1405,9 @@ async def process_inventory(inventory: InventoryItem):
|
||||||
# Equipment status
|
# Equipment status
|
||||||
current_wielded_location=basic['current_wielded_location'],
|
current_wielded_location=basic['current_wielded_location'],
|
||||||
|
|
||||||
|
# Container/position tracking
|
||||||
|
container_id=item_data.get('ContainerId', 0),
|
||||||
|
|
||||||
# Item state
|
# Item state
|
||||||
bonded=basic['bonded'],
|
bonded=basic['bonded'],
|
||||||
attuned=basic['attuned'],
|
attuned=basic['attuned'],
|
||||||
|
|
@ -1536,6 +1539,225 @@ async def process_inventory(inventory: InventoryItem):
|
||||||
errors=processing_errors if processing_errors else None
|
errors=processing_errors if processing_errors else None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/inventory/{character_name}/item",
|
||||||
|
summary="Upsert a single inventory item",
|
||||||
|
tags=["Data Processing"])
|
||||||
|
async def upsert_inventory_item(character_name: str, item: Dict[str, Any]):
|
||||||
|
"""Process and upsert a single item for a character's inventory."""
|
||||||
|
|
||||||
|
item_game_id = item.get('Id') or item.get('id')
|
||||||
|
if item_game_id is None:
|
||||||
|
raise HTTPException(status_code=400, detail="Item must have an 'Id' or 'id' field")
|
||||||
|
|
||||||
|
processed_count = 0
|
||||||
|
error_count = 0
|
||||||
|
|
||||||
|
async with database.transaction():
|
||||||
|
# Delete existing item with this character_name + item_id from all related tables
|
||||||
|
existing = await database.fetch_all(
|
||||||
|
"SELECT id FROM items WHERE character_name = :character_name AND item_id = :item_id",
|
||||||
|
{"character_name": character_name, "item_id": item_game_id}
|
||||||
|
)
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
id_list = ','.join(str(row['id']) for row in existing)
|
||||||
|
await database.execute(f"DELETE FROM item_raw_data WHERE item_id IN ({id_list})")
|
||||||
|
await database.execute(f"DELETE FROM item_combat_stats WHERE item_id IN ({id_list})")
|
||||||
|
await database.execute(f"DELETE FROM item_requirements WHERE item_id IN ({id_list})")
|
||||||
|
await database.execute(f"DELETE FROM item_enhancements WHERE item_id IN ({id_list})")
|
||||||
|
await database.execute(f"DELETE FROM item_ratings WHERE item_id IN ({id_list})")
|
||||||
|
await database.execute(f"DELETE FROM item_spells WHERE item_id IN ({id_list})")
|
||||||
|
await database.execute(
|
||||||
|
"DELETE FROM items WHERE character_name = :character_name AND item_id = :item_id",
|
||||||
|
{"character_name": character_name, "item_id": item_game_id}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Process and insert the single item using the same logic as process_inventory
|
||||||
|
try:
|
||||||
|
properties = extract_item_properties(item)
|
||||||
|
basic = properties['basic']
|
||||||
|
|
||||||
|
timestamp = datetime.utcnow()
|
||||||
|
|
||||||
|
item_stmt = sa.insert(Item).values(
|
||||||
|
character_name=character_name,
|
||||||
|
item_id=item_game_id,
|
||||||
|
timestamp=timestamp,
|
||||||
|
name=basic['name'],
|
||||||
|
icon=basic['icon'],
|
||||||
|
object_class=basic['object_class'],
|
||||||
|
value=basic['value'],
|
||||||
|
burden=basic['burden'],
|
||||||
|
has_id_data=basic['has_id_data'],
|
||||||
|
last_id_time=item.get('LastIdTime', 0),
|
||||||
|
|
||||||
|
# Equipment status
|
||||||
|
current_wielded_location=basic['current_wielded_location'],
|
||||||
|
|
||||||
|
# Container/position tracking
|
||||||
|
container_id=item.get('ContainerId', 0),
|
||||||
|
|
||||||
|
# Item state
|
||||||
|
bonded=basic['bonded'],
|
||||||
|
attuned=basic['attuned'],
|
||||||
|
unique=basic['unique'],
|
||||||
|
|
||||||
|
# Stack/Container properties
|
||||||
|
stack_size=basic['stack_size'],
|
||||||
|
max_stack_size=basic['max_stack_size'],
|
||||||
|
items_capacity=basic['items_capacity'] if basic['items_capacity'] != -1 else None,
|
||||||
|
containers_capacity=basic['containers_capacity'] if basic['containers_capacity'] != -1 else None,
|
||||||
|
|
||||||
|
# Durability
|
||||||
|
structure=basic['structure'] if basic['structure'] != -1 else None,
|
||||||
|
max_structure=basic['max_structure'] if basic['max_structure'] != -1 else None,
|
||||||
|
|
||||||
|
# Special item flags
|
||||||
|
rare_id=basic['rare_id'] if basic['rare_id'] != -1 else None,
|
||||||
|
lifespan=basic['lifespan'] if basic['lifespan'] != -1 else None,
|
||||||
|
remaining_lifespan=basic['remaining_lifespan'] if basic['remaining_lifespan'] != -1 else None,
|
||||||
|
).returning(Item.id)
|
||||||
|
|
||||||
|
result = await database.fetch_one(item_stmt)
|
||||||
|
db_item_id = result['id']
|
||||||
|
|
||||||
|
# Store combat stats if applicable
|
||||||
|
combat = properties['combat']
|
||||||
|
if any(v != -1 and v != -1.0 for v in combat.values()):
|
||||||
|
combat_stmt = sa.dialects.postgresql.insert(ItemCombatStats).values(
|
||||||
|
item_id=db_item_id,
|
||||||
|
**{k: v if v != -1 and v != -1.0 else None for k, v in combat.items()}
|
||||||
|
).on_conflict_do_update(
|
||||||
|
index_elements=['item_id'],
|
||||||
|
set_=dict(**{k: v if v != -1 and v != -1.0 else None for k, v in combat.items()})
|
||||||
|
)
|
||||||
|
await database.execute(combat_stmt)
|
||||||
|
|
||||||
|
# Store requirements if applicable
|
||||||
|
requirements = properties['requirements']
|
||||||
|
if any(v not in [-1, None, ''] for v in requirements.values()):
|
||||||
|
req_stmt = sa.dialects.postgresql.insert(ItemRequirements).values(
|
||||||
|
item_id=db_item_id,
|
||||||
|
**{k: v if v not in [-1, None, ''] else None for k, v in requirements.items()}
|
||||||
|
).on_conflict_do_update(
|
||||||
|
index_elements=['item_id'],
|
||||||
|
set_=dict(**{k: v if v not in [-1, None, ''] else None for k, v in requirements.items()})
|
||||||
|
)
|
||||||
|
await database.execute(req_stmt)
|
||||||
|
|
||||||
|
# Store enhancements
|
||||||
|
enhancements = properties['enhancements']
|
||||||
|
enh_stmt = sa.dialects.postgresql.insert(ItemEnhancements).values(
|
||||||
|
item_id=db_item_id,
|
||||||
|
**{k: v if v not in [-1, -1.0, None, ''] else None for k, v in enhancements.items()}
|
||||||
|
).on_conflict_do_update(
|
||||||
|
index_elements=['item_id'],
|
||||||
|
set_=dict(**{k: v if v not in [-1, -1.0, None, ''] else None for k, v in enhancements.items()})
|
||||||
|
)
|
||||||
|
await database.execute(enh_stmt)
|
||||||
|
|
||||||
|
# Store ratings if applicable
|
||||||
|
ratings = properties['ratings']
|
||||||
|
if any(v not in [-1, -1.0, None] for v in ratings.values()):
|
||||||
|
rat_stmt = sa.dialects.postgresql.insert(ItemRatings).values(
|
||||||
|
item_id=db_item_id,
|
||||||
|
**{k: v if v not in [-1, -1.0, None] else None for k, v in ratings.items()}
|
||||||
|
).on_conflict_do_update(
|
||||||
|
index_elements=['item_id'],
|
||||||
|
set_=dict(**{k: v if v not in [-1, -1.0, None] else None for k, v in ratings.items()})
|
||||||
|
)
|
||||||
|
await database.execute(rat_stmt)
|
||||||
|
|
||||||
|
# Store spell data if applicable
|
||||||
|
spells = item.get('Spells', [])
|
||||||
|
active_spells = item.get('ActiveSpells', [])
|
||||||
|
all_spells = set(spells + active_spells)
|
||||||
|
|
||||||
|
if all_spells:
|
||||||
|
await database.execute(
|
||||||
|
"DELETE FROM item_spells WHERE item_id = :item_id",
|
||||||
|
{"item_id": db_item_id}
|
||||||
|
)
|
||||||
|
for spell_id in all_spells:
|
||||||
|
is_active = spell_id in active_spells
|
||||||
|
spell_stmt = sa.dialects.postgresql.insert(ItemSpells).values(
|
||||||
|
item_id=db_item_id,
|
||||||
|
spell_id=spell_id,
|
||||||
|
is_active=is_active
|
||||||
|
).on_conflict_do_nothing()
|
||||||
|
await database.execute(spell_stmt)
|
||||||
|
|
||||||
|
# Store raw data
|
||||||
|
raw_stmt = sa.dialects.postgresql.insert(ItemRawData).values(
|
||||||
|
item_id=db_item_id,
|
||||||
|
int_values=item.get('IntValues', {}),
|
||||||
|
double_values=item.get('DoubleValues', {}),
|
||||||
|
string_values=item.get('StringValues', {}),
|
||||||
|
bool_values=item.get('BoolValues', {}),
|
||||||
|
original_json=item
|
||||||
|
).on_conflict_do_update(
|
||||||
|
index_elements=['item_id'],
|
||||||
|
set_=dict(
|
||||||
|
int_values=item.get('IntValues', {}),
|
||||||
|
double_values=item.get('DoubleValues', {}),
|
||||||
|
string_values=item.get('StringValues', {}),
|
||||||
|
bool_values=item.get('BoolValues', {}),
|
||||||
|
original_json=item
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await database.execute(raw_stmt)
|
||||||
|
|
||||||
|
processed_count = 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Error processing item {item_game_id}: {e}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
error_count = 1
|
||||||
|
raise HTTPException(status_code=500, detail=error_msg)
|
||||||
|
|
||||||
|
logger.info(f"Single item upsert for {character_name}: item_id={item_game_id}, processed={processed_count}")
|
||||||
|
return {"status": "ok", "processed": processed_count}
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/inventory/{character_name}/item/{item_id}",
|
||||||
|
summary="Delete a single inventory item",
|
||||||
|
tags=["Data Processing"])
|
||||||
|
async def delete_inventory_item(character_name: str, item_id: int):
|
||||||
|
"""Delete a single item from a character's inventory."""
|
||||||
|
|
||||||
|
deleted_count = 0
|
||||||
|
|
||||||
|
async with database.transaction():
|
||||||
|
# Find all DB rows for this character + game item_id
|
||||||
|
existing = await database.fetch_all(
|
||||||
|
"SELECT id FROM items WHERE character_name = :character_name AND item_id = :item_id",
|
||||||
|
{"character_name": character_name, "item_id": item_id}
|
||||||
|
)
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
id_list = ','.join(str(row['id']) for row in existing)
|
||||||
|
|
||||||
|
# Delete from all related tables first
|
||||||
|
await database.execute(f"DELETE FROM item_raw_data WHERE item_id IN ({id_list})")
|
||||||
|
await database.execute(f"DELETE FROM item_combat_stats WHERE item_id IN ({id_list})")
|
||||||
|
await database.execute(f"DELETE FROM item_requirements WHERE item_id IN ({id_list})")
|
||||||
|
await database.execute(f"DELETE FROM item_enhancements WHERE item_id IN ({id_list})")
|
||||||
|
await database.execute(f"DELETE FROM item_ratings WHERE item_id IN ({id_list})")
|
||||||
|
await database.execute(f"DELETE FROM item_spells WHERE item_id IN ({id_list})")
|
||||||
|
|
||||||
|
# Delete from main items table
|
||||||
|
await database.execute(
|
||||||
|
"DELETE FROM items WHERE character_name = :character_name AND item_id = :item_id",
|
||||||
|
{"character_name": character_name, "item_id": item_id}
|
||||||
|
)
|
||||||
|
|
||||||
|
deleted_count = len(existing)
|
||||||
|
|
||||||
|
logger.info(f"Single item delete for {character_name}: item_id={item_id}, deleted={deleted_count}")
|
||||||
|
return {"status": "ok", "deleted": deleted_count}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/inventory/{character_name}",
|
@app.get("/inventory/{character_name}",
|
||||||
summary="Get Character Inventory",
|
summary="Get Character Inventory",
|
||||||
description="Retrieve processed inventory data for a specific character with normalized item properties.",
|
description="Retrieve processed inventory data for a specific character with normalized item properties.",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue