mirror of
https://github.com/System-End/nephthys.git
synced 2026-04-19 19:45:12 +00:00
Add a tickets API (#156)
* Add a tickets API * Add more fields to JSON response for /api/ticket and /api/tickets * Prevent returning a massive number of tickets by default * Add an order to the query * Document /api/tickets
This commit is contained in:
parent
7a4546a917
commit
a1f3ebfaf6
4 changed files with 158 additions and 18 deletions
42
docs/api.md
42
docs/api.md
|
|
@ -74,3 +74,45 @@ interface OldestUnansweredTicket {
|
|||
|
||||
Note that some fields can be `null` if there are no tickets (or no open/closed/in-progress tickets)
|
||||
within the time period.
|
||||
|
||||
## `/api/tickets`
|
||||
|
||||
Returns a big list of tickets and their details! Please provide filters using query parameters
|
||||
to avoid overloading Nephthys as it tries to provide 1,000s of tickets at once.
|
||||
|
||||
Parameters available are:
|
||||
|
||||
- `?status=` - filter by ticket status, can be `open`, `closed`, or `in_progress`
|
||||
- `?since=` or `?after=` - filter for tickets created after a certain date/time, in ISO 8601 format (e.g. `2026-01-01`)
|
||||
- `?until=` or `?before=` - filter for tickets created before a certain date/time, in ISO 8601 format (e.g. `2026-01-31T12:00:00`)
|
||||
|
||||
Returns an array of ticket objects. Ticket objects look like this:
|
||||
|
||||
```ts
|
||||
interface Ticket {
|
||||
id: number
|
||||
title: string
|
||||
description: string
|
||||
status: "open" | "closed" | "in_progress"
|
||||
opened_by: User | null
|
||||
closed_by: User | null
|
||||
assigned_to: User | null
|
||||
reopened_by: User | null
|
||||
tags: Array<string>
|
||||
created_at: string
|
||||
message_ts: string
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: number
|
||||
slack_id: string
|
||||
}
|
||||
```
|
||||
|
||||
## `/api/ticket?id=<TICKET_ID>`
|
||||
|
||||
Returns a single ticket! See above for details on the ticket object returned.
|
||||
|
||||
Required parameter:
|
||||
|
||||
- `?id=` - the ID of the ticket to return
|
||||
|
|
|
|||
|
|
@ -2,6 +2,39 @@ from starlette.requests import Request
|
|||
from starlette.responses import JSONResponse
|
||||
|
||||
from nephthys.utils.env import env
|
||||
from prisma.models import Tag
|
||||
from prisma.models import Ticket
|
||||
from prisma.models import User
|
||||
|
||||
|
||||
def user_to_json(user: User | None) -> dict | None:
|
||||
return {"slack_id": user.slackId, "id": user.id} if user else None
|
||||
|
||||
|
||||
def tag_to_json(tag: Tag | None) -> str:
|
||||
if tag is None:
|
||||
raise ValueError("tag is None, did you forget to include the nested relation?")
|
||||
return tag.name
|
||||
|
||||
|
||||
def ticket_to_json(ticket: Ticket) -> dict:
|
||||
if ticket.tagsOnTickets is None:
|
||||
raise ValueError(
|
||||
"ticket.tagsOnTickets is None, did you forget to include the relation?"
|
||||
)
|
||||
return {
|
||||
"id": ticket.id,
|
||||
"title": ticket.title,
|
||||
"description": ticket.description,
|
||||
"status": ticket.status,
|
||||
"opened_by": user_to_json(ticket.openedBy),
|
||||
"closed_by": user_to_json(ticket.closedBy),
|
||||
"assigned_to": user_to_json(ticket.assignedTo),
|
||||
"reopened_by": user_to_json(ticket.reopenedBy),
|
||||
"tags": [tag_to_json(t.tag) for t in ticket.tagsOnTickets],
|
||||
"created_at": ticket.createdAt.isoformat(),
|
||||
"message_ts": ticket.msgTs,
|
||||
}
|
||||
|
||||
|
||||
async def ticket_info(req: Request):
|
||||
|
|
@ -13,26 +46,16 @@ async def ticket_info(req: Request):
|
|||
return JSONResponse({"error": "invalid_ticket_id"}, status_code=400)
|
||||
ticket = await env.db.ticket.find_unique(
|
||||
where={"id": ticket_id},
|
||||
include={"openedBy": True, "closedBy": True, "assignedTo": True},
|
||||
include={
|
||||
"openedBy": True,
|
||||
"closedBy": True,
|
||||
"assignedTo": True,
|
||||
"reopenedBy": True,
|
||||
"tagsOnTickets": {"include": {"tag": True}},
|
||||
},
|
||||
)
|
||||
|
||||
if not ticket:
|
||||
return JSONResponse({"error": "ticket_not_found"}, status_code=404)
|
||||
|
||||
return JSONResponse(
|
||||
{
|
||||
"title": ticket.title,
|
||||
"description": ticket.description,
|
||||
"status": ticket.status,
|
||||
"opened_by": {"slack_id": ticket.openedBy.slackId}
|
||||
if ticket.openedBy
|
||||
else None,
|
||||
"closed_by": {"slack_id": ticket.closedBy.slackId}
|
||||
if ticket.closedBy
|
||||
else None,
|
||||
"assigned_to": {"slack_id": ticket.assignedTo.slackId}
|
||||
if ticket.assignedTo
|
||||
else None,
|
||||
"created_at": ticket.createdAt.isoformat(),
|
||||
}
|
||||
)
|
||||
return JSONResponse(ticket_to_json(ticket))
|
||||
|
|
|
|||
73
nephthys/api/tickets.py
Normal file
73
nephthys/api/tickets.py
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
from datetime import datetime
|
||||
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
from nephthys.api.ticket import ticket_to_json
|
||||
from nephthys.utils.env import env
|
||||
from prisma.enums import TicketStatus
|
||||
from prisma.types import TicketWhereInput
|
||||
|
||||
|
||||
async def tickets_list(req: Request):
|
||||
filter_status = req.query_params.get("status")
|
||||
if filter_status:
|
||||
try:
|
||||
filter_status = TicketStatus(filter_status.upper())
|
||||
except ValueError:
|
||||
return JSONResponse(
|
||||
{"error": f"Invalid status parameter: {filter_status}"}, status_code=400
|
||||
)
|
||||
filter_created_after = req.query_params.get("since") or req.query_params.get(
|
||||
"after"
|
||||
)
|
||||
if filter_created_after:
|
||||
try:
|
||||
filter_created_after = datetime.fromisoformat(filter_created_after)
|
||||
except ValueError:
|
||||
msg = f"created_after parameter is not a valid ISO datetime: {filter_created_after}"
|
||||
return JSONResponse({"error": msg}, status_code=400)
|
||||
filter_created_before = req.query_params.get("until") or req.query_params.get(
|
||||
"before"
|
||||
)
|
||||
if filter_created_before:
|
||||
try:
|
||||
filter_created_before = datetime.fromisoformat(filter_created_before)
|
||||
except ValueError:
|
||||
msg = f"created_before parameter is not a valid ISO datetime: {filter_created_before}"
|
||||
return JSONResponse({"error": msg}, status_code=400)
|
||||
|
||||
if (
|
||||
(not filter_status or filter_status == TicketStatus.CLOSED)
|
||||
and not filter_created_after
|
||||
and not filter_created_before
|
||||
):
|
||||
# Prevent returning a massive number of tickets by default
|
||||
# If they want them, they should set a big date range explicitly
|
||||
# (that may also be disallowed in the future if it impacts performance too much)
|
||||
msg = "Provided filters are too broad"
|
||||
tip = "Please provide a ?since= or ?until= parameter, or filter by ?status=open or ?status=in_progress"
|
||||
return JSONResponse({"error": msg, "tip": tip}, status_code=400)
|
||||
|
||||
db_filter: TicketWhereInput = {}
|
||||
if filter_status:
|
||||
db_filter["status"] = filter_status
|
||||
if filter_created_after or filter_created_before:
|
||||
db_filter["createdAt"] = {}
|
||||
if filter_created_after:
|
||||
db_filter["createdAt"]["gte"] = filter_created_after
|
||||
if filter_created_before:
|
||||
db_filter["createdAt"]["lte"] = filter_created_before
|
||||
|
||||
tickets = await env.db.ticket.find_many(
|
||||
where=db_filter,
|
||||
include={
|
||||
"openedBy": True,
|
||||
"closedBy": True,
|
||||
"assignedTo": True,
|
||||
"reopenedBy": True,
|
||||
"tagsOnTickets": {"include": {"tag": True}},
|
||||
},
|
||||
order={"createdAt": "asc"},
|
||||
)
|
||||
return JSONResponse([ticket_to_json(t) for t in tickets])
|
||||
|
|
@ -17,6 +17,7 @@ from nephthys.__main__ import main
|
|||
from nephthys.api.stats import stats
|
||||
from nephthys.api.stats_v2 import stats_v2
|
||||
from nephthys.api.ticket import ticket_info
|
||||
from nephthys.api.tickets import tickets_list
|
||||
from nephthys.api.user import user_stats
|
||||
from nephthys.utils.env import env
|
||||
from nephthys.utils.slack import app as slack_app
|
||||
|
|
@ -68,6 +69,7 @@ app = Starlette(
|
|||
Route(path="/api/stats", endpoint=stats, methods=["GET"]),
|
||||
Route(path="/api/stats_v2", endpoint=stats_v2, methods=["GET"]),
|
||||
Route(path="/api/user", endpoint=user_stats, methods=["GET"]),
|
||||
Route(path="/api/tickets", endpoint=tickets_list, methods=["GET"]),
|
||||
Route(path="/api/ticket", endpoint=ticket_info, methods=["GET"]),
|
||||
Route(path="/health", endpoint=health, methods=["GET"]),
|
||||
Route(path="/metrics", endpoint=metrics, methods=["GET"]),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue