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
|
|
@ -36,7 +36,11 @@ class Item(Base):
|
|||
|
||||
# Equipment status
|
||||
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
|
||||
bonded = Column(Integer, default=0) # 0=Normal, 1=Bonded, 2=Sticky, 4=Destroy on drop
|
||||
attuned = Column(Integer, default=0) # 0=Normal, 1=Attuned
|
||||
|
|
|
|||
|
|
@ -1401,25 +1401,28 @@ async def process_inventory(inventory: InventoryItem):
|
|||
burden=basic['burden'],
|
||||
has_id_data=basic['has_id_data'],
|
||||
last_id_time=item_data.get('LastIdTime', 0),
|
||||
|
||||
|
||||
# Equipment status
|
||||
current_wielded_location=basic['current_wielded_location'],
|
||||
|
||||
|
||||
# Container/position tracking
|
||||
container_id=item_data.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,
|
||||
|
|
@ -1536,6 +1539,225 @@ async def process_inventory(inventory: InventoryItem):
|
|||
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}",
|
||||
summary="Get Character Inventory",
|
||||
description="Retrieve processed inventory data for a specific character with normalized item properties.",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue