Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 167 additions & 5 deletions apps/api/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -2020,7 +2020,8 @@
"items": {
"enum": [
"read_members",
"check_permissions"
"check_permissions",
"read_inbound_submissions"
],
"type": "string"
},
Expand All @@ -2036,6 +2037,59 @@
],
"type": "object"
},
"ProjectSubmissionDto": {
"additionalProperties": false,
"properties": {
"id": {
"example": "0b6f5c1e-7d2a-4c8e-9f1a-3e5b7c9d1f20",
"type": "string"
},
"origin": {
"example": "event-platform",
"nullable": true,
"type": "string"
},
"payload": {
"additionalProperties": true,
"example": {
"name": "Sara B."
},
"type": "object"
},
"receivedAt": {
"example": "2026-11-20T09:58:00.000Z",
"type": "string"
}
},
"required": [
"id",
"payload",
"receivedAt",
"origin"
],
"type": "object"
},
"ProjectSubmissionPageDto": {
"additionalProperties": false,
"properties": {
"items": {
"items": {
"$ref": "#/components/schemas/ProjectSubmissionDto"
},
"type": "array"
},
"nextCursor": {
"description": "Pass as `cursor` for the next page. Null at the end.",
"nullable": true,
"type": "string"
}
},
"required": [
"items",
"nextCursor"
],
"type": "object"
},
"RateLimitSettingsDto": {
"additionalProperties": false,
"properties": {
Expand Down Expand Up @@ -2449,7 +2503,8 @@
"items": {
"enum": [
"read_members",
"check_permissions"
"check_permissions",
"read_inbound_submissions"
],
"type": "string"
},
Expand Down Expand Up @@ -5464,7 +5519,8 @@
"schema": {
"enum": [
"read_members",
"check_permissions"
"check_permissions",
"read_inbound_submissions"
],
"type": "string"
}
Expand Down Expand Up @@ -5552,7 +5608,8 @@
"schema": {
"enum": [
"read_members",
"check_permissions"
"check_permissions",
"read_inbound_submissions"
],
"type": "string"
}
Expand Down Expand Up @@ -6027,7 +6084,8 @@
"schema": {
"enum": [
"read_members",
"check_permissions"
"check_permissions",
"read_inbound_submissions"
],
"type": "string"
}
Expand Down Expand Up @@ -9241,6 +9299,110 @@
]
}
},
"/api/project/inbound-webhooks/{id}/submissions": {
"get": {
"description": "Oldest first, keyset-paged on (receivedAt, id), so rows arriving between pages are neither skipped nor repeated. Every read is audited. Requires the `read_inbound_submissions` scope on the `serverId` given in the query.",
"operationId": "InboundWebhookProjectController_listSubmissions",
"parameters": [
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "Discord server on which the project holds the `read_inbound_submissions` scope.",
"in": "query",
"name": "serverId",
"required": true,
"schema": {
"example": "123456789012345678",
"type": "string"
}
},
{
"description": "Opaque `nextCursor` of the previous page.",
"in": "query",
"name": "cursor",
"required": false,
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "limit",
"required": false,
"schema": {
"default": 50,
"maximum": 200,
"minimum": 1,
"type": "number"
}
},
{
"description": "ISO 8601 - submissions received at or after it.",
"in": "query",
"name": "dateFrom",
"required": false,
"schema": {
"example": "2026-03-01T00:00:00Z",
"format": "date-time",
"type": "string"
}
},
{
"description": "ISO 8601 - submissions received at or before it.",
"in": "query",
"name": "dateTo",
"required": false,
"schema": {
"example": "2026-04-01T00:00:00Z",
"format": "date-time",
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProjectSubmissionPageDto"
}
}
},
"description": ""
},
"400": {
"description": "Malformed cursor or query."
},
"401": {
"description": "Missing or invalid API key."
},
"403": {
"description": "The project lacks the `read_inbound_submissions` scope or read access on `serverId`."
},
"404": {
"description": "No such webhook for this project."
},
"429": {
"description": "Project rate limit."
}
},
"security": [
{
"api-key": []
}
],
"summary": "List a webhook's submissions after a cursor",
"tags": [
"Inbound Webhooks (Project)"
]
}
},
"/api/projects/{projectId}/webhooks": {
"get": {
"description": "Returns the webhooks owned by the requesting project. Channel and server names are resolved from the bot cache and may be null.",
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/common/filters/drizzle.filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ export class PostgresExceptionFilter extends BaseExceptionFilter {
// 22001 = string data right truncation
// 22021 = invalid byte sequence for encoding "UTF8" (e.g. \x00 null bytes)
// 22P02 = invalid text representation (e.g. invalid uuid format)
if (['22001', '22021', '22P02'].includes(pgCode)) {
// 22007 / 22008 = invalid datetime format / field out of range
if (['22001', '22021', '22P02', '22007', '22008'].includes(pgCode)) {
const response = host.switchToHttp().getResponse<Response>();
return response.status(HttpStatus.BAD_REQUEST).json({
statusCode: HttpStatus.BAD_REQUEST,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const inboundWebhookSubmissions = pgTable(
webhookIdx: index('idx_inbound_webhook_submissions_webhook').on(
t.webhookId,
t.receivedAt,
t.id,
),
projectIdx: index('idx_inbound_webhook_submissions_project').on(
t.projectId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- Re-run on every boot: rebuild only while the index still lacks "id".
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_inbound_webhook_submissions_webhook' AND indexdef LIKE '%(webhook_id, received_at, id)') THEN
DROP INDEX IF EXISTS "idx_inbound_webhook_submissions_webhook";
CREATE INDEX "idx_inbound_webhook_submissions_webhook" ON "inbound_webhook_submissions" USING btree ("webhook_id","received_at","id");
END IF;
END $$;
Loading
Loading