127 lines
4.3 KiB
Python
127 lines
4.3 KiB
Python
from __future__ import annotations
|
||
|
||
from flask import Blueprint, jsonify, request, abort
|
||
from urllib.parse import urlencode
|
||
|
||
from crudkit.api._cursor import encode_cursor, decode_cursor
|
||
from crudkit.core.service import _is_truthy
|
||
|
||
|
||
def _bool_param(d: dict[str, str], key: str, default: bool) -> bool:
|
||
return _is_truthy(d.get(key, "1" if default else "0"))
|
||
|
||
|
||
def _safe_int(value: str | None, default: int) -> int:
|
||
try:
|
||
return int(value) if value is not None else default
|
||
except Exception:
|
||
return default
|
||
|
||
|
||
def _link_with_params(base_url: str, **params) -> str:
|
||
# Filter out None, encode safely
|
||
q = {k: v for k, v in params.items() if v is not None}
|
||
return f"{base_url}?{urlencode(q)}"
|
||
|
||
|
||
def generate_crud_blueprint(model, service):
|
||
bp = Blueprint(model.__name__.lower(), __name__)
|
||
|
||
@bp.get("/")
|
||
def list_items():
|
||
# Work from a copy so we don't mutate request.args
|
||
args = request.args.to_dict(flat=True)
|
||
|
||
legacy_offset = "offset" in args or "page" in args
|
||
|
||
limit = _safe_int(args.get("limit"), 50)
|
||
args["limit"] = limit
|
||
|
||
if legacy_offset:
|
||
# Old behavior: honor limit/offset, same CRUDSpec goodies
|
||
items = service.list(args)
|
||
return jsonify([obj.as_dict() for obj in items])
|
||
|
||
# New behavior: keyset pagination with cursors
|
||
cursor_token = args.get("cursor")
|
||
key, desc_from_cursor, backward = decode_cursor(cursor_token)
|
||
|
||
window = service.seek_window(
|
||
args,
|
||
key=key,
|
||
backward=backward,
|
||
include_total=_bool_param(args, "include_total", True),
|
||
)
|
||
|
||
# Prefer the order actually used by the window; fall back to desc_from_cursor if needed.
|
||
try:
|
||
desc_flags = list(window.order.desc)
|
||
except Exception:
|
||
desc_flags = desc_from_cursor or []
|
||
|
||
body = {
|
||
"items": [obj.as_dict() for obj in window.items],
|
||
"limit": window.limit,
|
||
"next_cursor": encode_cursor(window.last_key, desc_flags, backward=False),
|
||
"prev_cursor": encode_cursor(window.first_key, desc_flags, backward=True),
|
||
"total": window.total,
|
||
}
|
||
|
||
resp = jsonify(body)
|
||
|
||
# Preserve user’s other query params like include_total, filters, sorts, etc.
|
||
base_url = request.base_url
|
||
base_params = {k: v for k, v in args.items() if k not in {"cursor"}}
|
||
link_parts = []
|
||
if body["next_cursor"]:
|
||
link_parts.append(
|
||
f'<{_link_with_params(base_url, **base_params, cursor=body["next_cursor"])}>; rel="next"'
|
||
)
|
||
if body["prev_cursor"]:
|
||
link_parts.append(
|
||
f'<{_link_with_params(base_url, **base_params, cursor=body["prev_cursor"])}>; rel="prev"'
|
||
)
|
||
if link_parts:
|
||
resp.headers["Link"] = ", ".join(link_parts)
|
||
return resp
|
||
|
||
@bp.get("/<int:id>")
|
||
def get_item(id):
|
||
try:
|
||
item = service.get(id, request.args)
|
||
if item is None:
|
||
abort(404)
|
||
return jsonify(item.as_dict())
|
||
except Exception as e:
|
||
# Could be validation, auth, or just you forgetting an index again
|
||
return jsonify({"status": "error", "error": str(e)}), 400
|
||
|
||
@bp.post("/")
|
||
def create_item():
|
||
payload = request.get_json(silent=True) or {}
|
||
try:
|
||
obj = service.create(payload)
|
||
return jsonify(obj.as_dict()), 201
|
||
except Exception as e:
|
||
return jsonify({"status": "error", "error": str(e)}), 400
|
||
|
||
@bp.patch("/<int:id>")
|
||
def update_item(id):
|
||
payload = request.get_json(silent=True) or {}
|
||
try:
|
||
obj = service.update(id, payload)
|
||
return jsonify(obj.as_dict())
|
||
except Exception as e:
|
||
# 404 if not found, 400 if validation. Your service can throw specific exceptions if you ever feel like being professional.
|
||
return jsonify({"status": "error", "error": str(e)}), 400
|
||
|
||
@bp.delete("/<int:id>")
|
||
def delete_item(id):
|
||
try:
|
||
service.delete(id)
|
||
# 204 means "no content" so don't send any.
|
||
return ("", 204)
|
||
except Exception as e:
|
||
return jsonify({"status": "error", "error": str(e)}), 400
|
||
|
||
return bp
|