Messenger/packages/core/src/schemas/entities.ts
Заид Омар Медхат | Zaid Omar Medhat db5c1610b3 Phase 6: media messages + avatars via MinIO presigned uploads
- Media (voice/video/image/file): private bucket, presigned PUT upload,
  reference-only publish, membership-gated presigned GET for viewing
- Avatars: public bucket, presigned PUT + direct public URL, profile.update event
- messages.media_key + media_meta (migration 1720000000005_media)
- Explicit MinIO region on presigning client (avoids getBucketRegion network call)
- Event type values sourced from EventType constants (no raw string literals)
- Web: attach button, MediaView renderer, avatar upload in topbar

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFRyKxkKEjfAgpXygzoNiD
2026-07-10 18:11:25 +05:00

217 lines
5.8 KiB
TypeScript

// Response JSON Schemas mirroring the core types. Shared by the backend for
// OpenAPI docs and response serialization, so the documented contract cannot
// drift from what the API returns.
import { mediaRefSchema } from './media';
export const publicUserSchema = {
type: 'object',
additionalProperties: false,
required: ['id', 'username', 'displayName', 'avatarUrl'],
properties: {
id: { type: 'string', format: 'uuid' },
username: { type: 'string' },
displayName: { type: 'string' },
avatarUrl: { type: ['string', 'null'] },
},
} as const;
export const userSchema = {
type: 'object',
additionalProperties: false,
required: [
'id',
'username',
'displayName',
'avatarUrl',
'email',
'phone',
'createdAt',
'lastSeenAt',
],
properties: {
id: { type: 'string', format: 'uuid' },
username: { type: 'string' },
displayName: { type: 'string' },
avatarUrl: { type: ['string', 'null'] },
email: { type: ['string', 'null'] },
phone: { type: ['string', 'null'] },
createdAt: { type: 'string', format: 'date-time' },
lastSeenAt: { type: ['string', 'null'], format: 'date-time' },
},
} as const;
// The refresh token is delivered as an httpOnly cookie, so it is deliberately
// absent from the response body.
export const authResultSchema = {
type: 'object',
additionalProperties: false,
required: ['user', 'accessToken', 'accessTokenExpiresIn'],
properties: {
user: userSchema,
accessToken: { type: 'string' },
accessTokenExpiresIn: { type: 'integer' },
},
} as const;
export const sessionSchema = {
type: 'object',
additionalProperties: false,
required: ['id', 'userAgent', 'ip', 'createdAt', 'lastUsedAt', 'current'],
properties: {
id: { type: 'string', format: 'uuid' },
userAgent: { type: ['string', 'null'] },
ip: { type: ['string', 'null'] },
createdAt: { type: 'string', format: 'date-time' },
lastUsedAt: { type: 'string', format: 'date-time' },
current: { type: 'boolean' },
},
} as const;
export const sessionListSchema = {
type: 'array',
items: sessionSchema,
} as const;
export const centrifugoTokenSchema = {
type: 'object',
additionalProperties: false,
required: ['token', 'expiresIn'],
properties: {
token: { type: 'string' },
expiresIn: { type: 'integer' },
},
} as const;
export const errorSchema = {
type: 'object',
required: ['error'],
properties: {
error: { type: 'string' },
message: { type: 'string' },
},
} as const;
export const publicUserListSchema = { type: 'array', items: publicUserSchema } as const;
export const conversationSchema = {
type: 'object',
additionalProperties: false,
required: [
'id',
'type',
'title',
'peer',
'createdBy',
'createdAt',
'lastMessageAt',
'unreadCount',
],
properties: {
id: { type: 'string', format: 'uuid' },
type: { type: 'string', enum: ['direct', 'group'] },
title: { type: ['string', 'null'] },
peer: {
oneOf: [publicUserSchema, { type: 'null' }],
},
createdBy: { type: 'string', format: 'uuid' },
createdAt: { type: 'string', format: 'date-time' },
lastMessageAt: { type: 'string', format: 'date-time' },
unreadCount: { type: 'integer' },
},
} as const;
export const reactionSummarySchema = {
type: 'object',
additionalProperties: false,
required: ['emoji', 'count', 'mine'],
properties: {
emoji: { type: 'string' },
count: { type: 'integer' },
mine: { type: 'boolean' },
},
} as const;
export const presenceSchema = {
type: 'object',
additionalProperties: false,
required: ['userId', 'online', 'lastSeenAt'],
properties: {
userId: { type: 'string', format: 'uuid' },
online: { type: 'boolean' },
lastSeenAt: { type: ['string', 'null'], format: 'date-time' },
},
} as const;
export const presenceListSchema = { type: 'array', items: presenceSchema } as const;
export const conversationListSchema = { type: 'array', items: conversationSchema } as const;
export const conversationMemberSchema = {
type: 'object',
additionalProperties: false,
required: ['userId', 'role', 'joinedAt', 'user'],
properties: {
userId: { type: 'string', format: 'uuid' },
role: { type: 'string' },
joinedAt: { type: 'string', format: 'date-time' },
user: publicUserSchema,
},
} as const;
export const conversationMemberListSchema = {
type: 'array',
items: conversationMemberSchema,
} as const;
export const contactSchema = {
type: 'object',
additionalProperties: false,
required: ['userId', 'user', 'createdAt'],
properties: {
userId: { type: 'string', format: 'uuid' },
user: publicUserSchema,
createdAt: { type: 'string', format: 'date-time' },
},
} as const;
export const contactListSchema = { type: 'array', items: contactSchema } as const;
export const messageSchema = {
type: 'object',
additionalProperties: false,
required: [
'id',
'conversationId',
'senderId',
'sender',
'content',
'contentType',
'encryption',
'clientMsgId',
'seq',
'createdAt',
'editedAt',
'deletedAt',
'reactions',
'media',
],
properties: {
id: { type: 'string', format: 'uuid' },
conversationId: { type: 'string', format: 'uuid' },
senderId: { type: 'string', format: 'uuid' },
sender: publicUserSchema,
content: { type: 'string' },
contentType: { type: 'string' },
encryption: { type: ['string', 'null'] },
clientMsgId: { type: 'string' },
seq: { type: 'integer' },
createdAt: { type: 'string', format: 'date-time' },
editedAt: { type: ['string', 'null'], format: 'date-time' },
deletedAt: { type: ['string', 'null'], format: 'date-time' },
reactions: { type: 'array', items: reactionSummarySchema },
media: { oneOf: [mediaRefSchema, { type: 'null' }] },
},
} as const;
export const messageListSchema = { type: 'array', items: messageSchema } as const;