docs: improve doucmentation

improve doucmentation and fix small store changes
This commit is contained in:
2026-04-30 01:16:48 +02:00
parent 3f92e74350
commit 0a714428f6
31 changed files with 4161 additions and 23 deletions

View File

@@ -104,6 +104,37 @@ function clampInt(value: unknown, min: number, max: number, fallback: number): n
return Math.max(min, Math.min(max, Math.floor(parsed)));
}
function getTrailingPathParam(pathname: string, pattern: RegExp, name: string): string {
const value = pattern.exec(pathname)?.[1];
if (!value) {
throw new HttpError(400, `${name} is required`, 'INVALID_REQUEST');
}
return decodeURIComponent(value);
}
function getRequiredQueryParam(ctx: RouteContext, name: string): string {
const value = ctx.request.url.searchParams.get(name)?.trim() ?? '';
if (!value) {
throw new HttpError(400, `${name} is required`, 'INVALID_REQUEST');
}
return value;
}
function getRequiredTimestamp(ctx: RouteContext, name: string): number {
const raw = getRequiredQueryParam(ctx, name);
const value = Number(raw);
if (!Number.isFinite(value) || value < 0) {
throw new HttpError(400, `${name} must be a non-negative timestamp`, 'INVALID_REQUEST');
}
return Math.floor(value);
}
const ROUTES: RouteDefinition[] = [
defineRoute('GET', '/api/health', async (ctx): Promise<RouteResponse> => ({
status: 200,
@@ -276,20 +307,165 @@ const ROUTES: RouteDefinition[] = [
return { status: 200, body: rooms ?? [] };
}, true),
defineRoute('GET', '/api/rooms/{roomId}', async (ctx): Promise<RouteResponse> => {
const roomId = getTrailingPathParam(ctx.request.pathname, /\/api\/rooms\/([^/]+)$/u, 'roomId');
const room = await runQuery<unknown>(requireDataSource(ctx.dataSource), {
type: QueryType.GetRoom,
payload: { roomId }
});
if (!room) {
throw new HttpError(404, 'Room not found on this device', 'ROOM_NOT_FOUND');
}
return { status: 200, body: room };
}, true),
defineRoute('GET', '/api/rooms/{roomId}/users', async (ctx): Promise<RouteResponse> => {
const roomId = getTrailingPathParam(ctx.request.pathname, /\/api\/rooms\/([^/]+)\/users$/u, 'roomId');
const users = await runQuery<unknown[]>(requireDataSource(ctx.dataSource), {
type: QueryType.GetUsersByRoom,
payload: { roomId }
});
return { status: 200, body: users ?? [] };
}, true),
defineRoute('GET', '/api/rooms/{roomId}/messages', async (ctx): Promise<RouteResponse> => {
const roomId = ctx.request.url.pathname.match(/\/api\/rooms\/([^/]+)\/messages$/u)?.[1];
if (!roomId)
throw new HttpError(400, 'roomId is required', 'INVALID_REQUEST');
const roomId = getTrailingPathParam(ctx.request.pathname, /\/api\/rooms\/([^/]+)\/messages$/u, 'roomId');
const limit = clampInt(ctx.request.url.searchParams.get('limit'), 1, 500, 100);
const offset = clampInt(ctx.request.url.searchParams.get('offset'), 0, Number.MAX_SAFE_INTEGER, 0);
const messages = await runQuery<unknown[]>(requireDataSource(ctx.dataSource), {
type: QueryType.GetMessages,
payload: { roomId: decodeURIComponent(roomId), limit, offset }
payload: { roomId, limit, offset }
});
return { status: 200, body: messages ?? [] };
}, true),
defineRoute('GET', '/api/rooms/{roomId}/messages/since', async (ctx): Promise<RouteResponse> => {
const roomId = getTrailingPathParam(ctx.request.pathname, /\/api\/rooms\/([^/]+)\/messages\/since$/u, 'roomId');
const sinceTimestamp = getRequiredTimestamp(ctx, 'sinceTimestamp');
const messages = await runQuery<unknown[]>(requireDataSource(ctx.dataSource), {
type: QueryType.GetMessagesSince,
payload: { roomId, sinceTimestamp }
});
return { status: 200, body: messages ?? [] };
}, true),
defineRoute('GET', '/api/rooms/{roomId}/bans', async (ctx): Promise<RouteResponse> => {
const roomId = getTrailingPathParam(ctx.request.pathname, /\/api\/rooms\/([^/]+)\/bans$/u, 'roomId');
const bans = await runQuery<unknown[]>(requireDataSource(ctx.dataSource), {
type: QueryType.GetBansForRoom,
payload: { roomId }
});
return { status: 200, body: bans ?? [] };
}, true),
defineRoute('GET', '/api/rooms/{roomId}/bans/{userId}', async (ctx): Promise<RouteResponse> => {
const match = /\/api\/rooms\/([^/]+)\/bans\/([^/]+)$/u.exec(ctx.request.pathname);
if (!match) {
throw new HttpError(400, 'roomId and userId are required', 'INVALID_REQUEST');
}
const isBanned = await runQuery<boolean>(requireDataSource(ctx.dataSource), {
type: QueryType.IsUserBanned,
payload: { roomId: decodeURIComponent(match[1]), userId: decodeURIComponent(match[2]) }
});
return { status: 200, body: { isBanned } };
}, true),
defineRoute('GET', '/api/messages/{messageId}', async (ctx): Promise<RouteResponse> => {
const messageId = getTrailingPathParam(ctx.request.pathname, /\/api\/messages\/([^/]+)$/u, 'messageId');
const message = await runQuery<unknown>(requireDataSource(ctx.dataSource), {
type: QueryType.GetMessageById,
payload: { messageId }
});
if (!message) {
throw new HttpError(404, 'Message not found on this device', 'MESSAGE_NOT_FOUND');
}
return { status: 200, body: message };
}, true),
defineRoute('GET', '/api/messages/{messageId}/reactions', async (ctx): Promise<RouteResponse> => {
const messageId = getTrailingPathParam(ctx.request.pathname, /\/api\/messages\/([^/]+)\/reactions$/u, 'messageId');
const reactions = await runQuery<unknown[]>(requireDataSource(ctx.dataSource), {
type: QueryType.GetReactionsForMessage,
payload: { messageId }
});
return { status: 200, body: reactions ?? [] };
}, true),
defineRoute('GET', '/api/messages/{messageId}/attachments', async (ctx): Promise<RouteResponse> => {
const messageId = getTrailingPathParam(ctx.request.pathname, /\/api\/messages\/([^/]+)\/attachments$/u, 'messageId');
const attachments = await runQuery<unknown[]>(requireDataSource(ctx.dataSource), {
type: QueryType.GetAttachmentsForMessage,
payload: { messageId }
});
return { status: 200, body: attachments ?? [] };
}, true),
defineRoute('GET', '/api/users/{userId}', async (ctx): Promise<RouteResponse> => {
const userId = getTrailingPathParam(ctx.request.pathname, /\/api\/users\/([^/]+)$/u, 'userId');
const user = await runQuery<unknown>(requireDataSource(ctx.dataSource), {
type: QueryType.GetUser,
payload: { userId }
});
if (!user) {
throw new HttpError(404, 'User not found on this device', 'USER_NOT_FOUND');
}
return { status: 200, body: user };
}, true),
defineRoute('GET', '/api/attachments', async (ctx): Promise<RouteResponse> => {
const attachments = await runQuery<unknown[]>(requireDataSource(ctx.dataSource), {
type: QueryType.GetAllAttachments,
payload: {}
});
return { status: 200, body: attachments ?? [] };
}, true),
defineRoute('GET', '/api/plugin-data', async (ctx): Promise<RouteResponse> => {
const pluginId = getRequiredQueryParam(ctx, 'pluginId');
const key = getRequiredQueryParam(ctx, 'key');
const scope = getRequiredQueryParam(ctx, 'scope');
if (scope !== 'local' && scope !== 'server') {
throw new HttpError(400, 'scope must be local or server', 'INVALID_REQUEST');
}
const value = await runQuery<unknown>(requireDataSource(ctx.dataSource), {
type: QueryType.GetPluginData,
payload: {
key,
pluginId,
scope,
serverId: ctx.request.url.searchParams.get('serverId') ?? undefined
}
});
return { status: 200, body: { value } };
}, true),
defineRoute('GET', '/api/meta/{key}', async (ctx): Promise<RouteResponse> => {
const key = getTrailingPathParam(ctx.request.pathname, /\/api\/meta\/([^/]+)$/u, 'key');
const value = await runQuery<string | null>(requireDataSource(ctx.dataSource), {
type: QueryType.GetMeta,
payload: { key }
});
return { status: 200, body: { key, value } };
}, true)
];