feat(guild-messaging): add idempotency-key support for write endpoints
This commit is contained in:
@@ -3,6 +3,7 @@ import {
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Headers,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Patch,
|
||||
@@ -14,6 +15,7 @@ import { DataSource, Repository } from 'typeorm';
|
||||
import { CreateMessageDto } from './dto.create-message.dto';
|
||||
import { Channel } from '../entities/channel.entity';
|
||||
import { Message } from '../entities/message.entity';
|
||||
import { IdempotencyRecord } from '../entities/idempotency-record.entity';
|
||||
|
||||
const EDIT_WINDOW_MS = 15 * 60 * 1000;
|
||||
const DEFAULT_PAGE_LIMIT = 50;
|
||||
@@ -27,8 +29,33 @@ export class MessagingController {
|
||||
private readonly channelRepo: Repository<Channel>,
|
||||
@InjectRepository(Message)
|
||||
private readonly messageRepo: Repository<Message>,
|
||||
@InjectRepository(IdempotencyRecord)
|
||||
private readonly idemRepo: Repository<IdempotencyRecord>,
|
||||
) {}
|
||||
|
||||
private async getIdempotentResponse(
|
||||
scope: string,
|
||||
idempotencyKey?: string,
|
||||
): Promise<Record<string, unknown> | null> {
|
||||
if (!idempotencyKey) return null;
|
||||
const row = await this.idemRepo.findOne({ where: { scope, idempotencyKey } });
|
||||
return row?.responseBody ?? null;
|
||||
}
|
||||
|
||||
private async saveIdempotentResponse(
|
||||
scope: string,
|
||||
idempotencyKey: string | undefined,
|
||||
responseBody: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
if (!idempotencyKey) return;
|
||||
const row = this.idemRepo.create({
|
||||
scope,
|
||||
idempotencyKey,
|
||||
responseBody,
|
||||
});
|
||||
await this.idemRepo.save(row);
|
||||
}
|
||||
|
||||
private toView(m: Message) {
|
||||
return {
|
||||
messageId: m.messageId,
|
||||
@@ -46,7 +73,15 @@ export class MessagingController {
|
||||
}
|
||||
|
||||
@Post()
|
||||
async create(@Param('id') channelId: string, @Body() body: CreateMessageDto) {
|
||||
async create(
|
||||
@Param('id') channelId: string,
|
||||
@Body() body: CreateMessageDto,
|
||||
@Headers('idempotency-key') idempotencyKey?: string,
|
||||
) {
|
||||
const scope = `POST:/channels/${channelId}/messages`;
|
||||
const existed = await this.getIdempotentResponse(scope, idempotencyKey);
|
||||
if (existed) return existed;
|
||||
|
||||
const message = await this.dataSource.transaction(async (manager) => {
|
||||
const channel = await manager.findOne(Channel, {
|
||||
where: { id: channelId },
|
||||
@@ -78,7 +113,9 @@ export class MessagingController {
|
||||
return manager.save(Message, row);
|
||||
});
|
||||
|
||||
return this.toView(message);
|
||||
const responseBody = this.toView(message) as Record<string, unknown>;
|
||||
await this.saveIdempotentResponse(scope, idempotencyKey, responseBody);
|
||||
return responseBody;
|
||||
}
|
||||
|
||||
@Patch(':messageId')
|
||||
@@ -86,7 +123,12 @@ export class MessagingController {
|
||||
@Param('id') channelId: string,
|
||||
@Param('messageId') messageId: string,
|
||||
@Body() body: { content?: string },
|
||||
@Headers('idempotency-key') idempotencyKey?: string,
|
||||
) {
|
||||
const scope = `PATCH:/channels/${channelId}/messages/${messageId}`;
|
||||
const existed = await this.getIdempotentResponse(scope, idempotencyKey);
|
||||
if (existed) return existed;
|
||||
|
||||
const item = await this.messageRepo.findOne({ where: { channelId, messageId } });
|
||||
if (!item) return { status: 'not_found' };
|
||||
|
||||
@@ -99,11 +141,21 @@ export class MessagingController {
|
||||
item.content = body.content ?? item.content;
|
||||
item.editedAt = new Date();
|
||||
const saved = await this.messageRepo.save(item);
|
||||
return this.toView(saved);
|
||||
const responseBody = this.toView(saved) as Record<string, unknown>;
|
||||
await this.saveIdempotentResponse(scope, idempotencyKey, responseBody);
|
||||
return responseBody;
|
||||
}
|
||||
|
||||
@Delete(':messageId')
|
||||
async remove(@Param('id') channelId: string, @Param('messageId') messageId: string) {
|
||||
async remove(
|
||||
@Param('id') channelId: string,
|
||||
@Param('messageId') messageId: string,
|
||||
@Headers('idempotency-key') idempotencyKey?: string,
|
||||
) {
|
||||
const scope = `DELETE:/channels/${channelId}/messages/${messageId}`;
|
||||
const existed = await this.getIdempotentResponse(scope, idempotencyKey);
|
||||
if (existed) return existed;
|
||||
|
||||
const item = await this.messageRepo.findOne({ where: { channelId, messageId } });
|
||||
if (!item) return { status: 'not_found' };
|
||||
|
||||
@@ -114,7 +166,13 @@ export class MessagingController {
|
||||
item.attachments = [];
|
||||
await this.messageRepo.save(item);
|
||||
|
||||
return { status: 'deleted', mode: 'soft', messageId };
|
||||
const responseBody = {
|
||||
status: 'deleted',
|
||||
mode: 'soft',
|
||||
messageId,
|
||||
} as Record<string, unknown>;
|
||||
await this.saveIdempotentResponse(scope, idempotencyKey, responseBody);
|
||||
return responseBody;
|
||||
}
|
||||
|
||||
@Get()
|
||||
|
||||
Reference in New Issue
Block a user