Add the API import surface for legacy migrations (contract 0.8.0)
Goal: an LLM or script can migrate an entire legacy database using only the HTTP API - original history preserved, safely re-runnable. - X-Import-Mode header (admin only): create/update endpoints across 15 timestamped entity types accept original createddate/modifieddate; helper exposed via shopdb.api (contract 0.7.0 -> 0.8.0). - Exact-match natural-key lookup filters on 13 list endpoints for the lookup-then-upsert recipe. - Selfhosted USB checkout/checkin accept backdated event times in import mode. - docs/IMPORT-API.md: operator manual grounded in the real legacy schema - order of operations, full table-by-table mapping including the machines fan-out, idempotent Python importer with dry-run, parity checks, and decided dispositions for unmigrated tables (DNC config stays live-fed via the collector; supportteams/appowners map to the upcoming supportteams model). 635 tests pass; naming green; frontend untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@ from datetime import datetime, timezone
|
||||
from shopdb.api import (
|
||||
db, success_response, error_response, ErrorCodes,
|
||||
get_pagination_params, paginated_response,
|
||||
apply_import_timestamps, import_mode_active, parse_import_datetime,
|
||||
)
|
||||
|
||||
from ..models import USBDevice, USBCheckout
|
||||
@@ -147,6 +148,7 @@ def create_device(data):
|
||||
storagelocation=data.get('locker_location'),
|
||||
ischeckedout=False, isactive=True)
|
||||
db.session.add(device)
|
||||
apply_import_timestamps(device, data)
|
||||
db.session.commit()
|
||||
return success_response(_device_dict(device), message='Device created', http_code=201)
|
||||
|
||||
@@ -189,13 +191,20 @@ def checkout_device(device_id, data):
|
||||
return error_response(ErrorCodes.CONFLICT, 'Device is already checked out', http_code=409)
|
||||
name = _resolve_name(badge)
|
||||
now = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||
# Backdated import: an admin import request may pass the historical
|
||||
# checkouttime so migrated usbcheckouts rows keep their real event time.
|
||||
eventtime = now
|
||||
if import_mode_active():
|
||||
override = parse_import_datetime(data.get('checkouttime'))
|
||||
if override is not None:
|
||||
eventtime = override
|
||||
db.session.add(USBCheckout(usbdeviceid=device.usbdeviceid, machineid=0, sso=badge,
|
||||
checkoutname=name, checkouttime=now,
|
||||
checkoutname=name, checkouttime=eventtime,
|
||||
checkoutreason=data.get('reason')))
|
||||
device.ischeckedout = True
|
||||
device.currentuserid = badge
|
||||
device.currentusername = name
|
||||
device.currentcheckoutdate = now
|
||||
device.currentcheckoutdate = eventtime
|
||||
if data.get('locker_location'):
|
||||
device.storagelocation = data['locker_location']
|
||||
db.session.commit()
|
||||
@@ -215,7 +224,14 @@ def checkin_device(device_id, data):
|
||||
.filter_by(usbdeviceid=device.usbdeviceid, checkintime=None)
|
||||
.order_by(USBCheckout.checkouttime.desc()).first())
|
||||
if open_checkout:
|
||||
open_checkout.checkintime = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||
checkintime = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||
# Backdated import: accept the historical checkintime from an admin
|
||||
# import request so returned checkouts keep their real return time.
|
||||
if import_mode_active():
|
||||
override = parse_import_datetime(data.get('checkintime'))
|
||||
if override is not None:
|
||||
checkintime = override
|
||||
open_checkout.checkintime = checkintime
|
||||
open_checkout.waswiped = bool(data.get('sanitized'))
|
||||
open_checkout.checkinnotes = data.get('notes')
|
||||
device.ischeckedout = False
|
||||
|
||||
Reference in New Issue
Block a user