Task #511: Fully remove Chat feature

Destructive removal per user confirmation ("حذف نهائي ما يرجع").

Removed:
- API: routes/conversations.ts, schema/conversations.ts, all chat
  socket handlers in src/index.ts, /admin/users/:id/dependents/
  conversations+messages endpoints, conversation/message dependency
  counts in users/stats routes.
- Web: pages/chat.tsx, /chat route, dock chat filter, MessageSquare
  icon and messages StatCard on home, all chat-related UI in
  notifications + admin (dependency badges, delete-dialog rows,
  UserDependentConversations/Messages sections, count map keys).
- Locales: nav.chat, home.stats.messages, full chat.* block,
  admin.deleteUser conv/msgCount, admin.users.counts.conv/msg,
  admin.audit.unit.conversation_*/message_*, admin.dependents.user*.
- OpenAPI spec: tags, all /conversations/* paths, conv/msg dependent
  paths, related schemas (ConversationWithDetails, MessageWithSender,
  UserDependentConversation/MessageItem+Page, etc.), UserProfile and
  UserDeletionConflict conv/msg fields, HomeStats.unreadMessages.
  Regenerated client via orval.
- Database: dropped message_reads, messages,
  conversation_participants, conversations (CASCADE); deleted
  notifications with related_type='conversation' or type='chat';
  deleted apps row with slug='chat'; ran drizzle push-force.
- Seed: removed chat:access permission + user-role assignment +
  seeded chat app entry from scripts/src/seed.ts.
- Tests: deleted conversations-leave.test.mjs; cleaned chat refs from
  list-dependency-counts, delete-force-warnings, audit-log-coverage,
  and admin-inline-dependency-counts (e2e) — replaced chat dependents
  with note dependents where needed for force-delete coverage.

Notes preserved: notes.tsx noConversationsYet/conversationWith refer
to NOTE THREADS (not chat) and were intentionally NOT touched.

executive-meetings.ts not modified per replit.md restriction.
Pre-existing flaky test failures in executive-meetings/group/etc
suites remain unrelated to this task.
This commit is contained in:
riyadhafraa
2026-05-12 10:51:31 +00:00
parent e4ebaffe73
commit 84398de390
27 changed files with 22 additions and 6492 deletions
@@ -197,14 +197,6 @@ Omitted from single-user responses (GET /users/:id, PATCH, etc).
admin list endpoint (GET /users).
*/
orderCount?: number;
/** Number of conversations created by this user. Populated only by the
admin list endpoint (GET /users).
*/
conversationCount?: number;
/** Number of messages authored by this user. Populated only by the
admin list endpoint (GET /users).
*/
messageCount?: number;
}
export interface UpdateUserBody {
@@ -371,8 +363,6 @@ export interface UserDeletionConflict {
error: string;
noteCount: number;
orderCount: number;
conversationCount: number;
messageCount: number;
}
export interface AppDeletionConflict {
@@ -643,96 +633,6 @@ export interface BulkDeleteServiceOrdersResponse {
failedIds: number[];
}
export interface ParticipantInfo {
id: number;
username: string;
/** @nullable */
displayNameAr?: string | null;
/** @nullable */
displayNameEn?: string | null;
/** @nullable */
avatarUrl?: string | null;
isAdmin: boolean;
}
export type MessageWithSenderKind =
(typeof MessageWithSenderKind)[keyof typeof MessageWithSenderKind];
export const MessageWithSenderKind = {
user: "user",
group_renamed: "group_renamed",
members_added: "members_added",
member_removed: "member_removed",
member_left: "member_left",
admin_promoted: "admin_promoted",
} as const;
export type MessageWithSenderMeta = { [key: string]: unknown } | null;
export interface MessageWithSender {
id: number;
conversationId: number;
senderId: number;
content: string;
kind: MessageWithSenderKind;
meta?: MessageWithSenderMeta;
sender: ParticipantInfo;
createdAt: string;
updatedAt: string;
}
export interface ConversationWithDetails {
id: number;
/** @nullable */
nameAr?: string | null;
/** @nullable */
nameEn?: string | null;
/** @nullable */
avatarUrl?: string | null;
isGroup: boolean;
createdBy: number;
participants: ParticipantInfo[];
lastMessage?: MessageWithSender | null;
unreadCount: number;
isMuted: boolean;
isArchived: boolean;
createdAt: string;
updatedAt: string;
}
export interface CreateConversationBody {
participantIds: number[];
/** @nullable */
nameAr?: string | null;
/** @nullable */
nameEn?: string | null;
/** @nullable */
avatarUrl?: string | null;
isGroup?: boolean;
}
export interface UpdateConversationBody {
/** @nullable */
avatarUrl?: string | null;
/** @nullable */
nameAr?: string | null;
/** @nullable */
nameEn?: string | null;
}
export interface UpdateConversationStateBody {
isMuted?: boolean;
isArchived?: boolean;
}
export interface AddParticipantsBody {
userIds: number[];
}
export interface SendMessageBody {
content: string;
}
export interface Notification {
id: number;
userId: number;
@@ -808,7 +708,6 @@ export interface HomeStats {
totalApps: number;
totalServices: number;
unreadNotifications: number;
unreadMessages: number;
totalUsers: number;
}
@@ -1224,47 +1123,6 @@ export interface UserDependentOrdersPage {
nextOffset: number | null;
}
export interface UserDependentConversationItem {
id: number;
/** @nullable */
nameAr: string | null;
/** @nullable */
nameEn: string | null;
isGroup: boolean;
createdAt: string;
}
export interface UserDependentConversationsPage {
items: UserDependentConversationItem[];
totalCount: number;
limit: number;
offset: number;
/** @nullable */
nextOffset: number | null;
}
export interface UserDependentMessageItem {
id: number;
content: string;
kind: string;
createdAt: string;
conversationId: number;
/** @nullable */
conversationNameAr: string | null;
/** @nullable */
conversationNameEn: string | null;
conversationIsGroup: boolean;
}
export interface UserDependentMessagesPage {
items: UserDependentMessageItem[];
totalCount: number;
limit: number;
offset: number;
/** @nullable */
nextOffset: number | null;
}
export interface AuditLogActor {
id: number;
username: string;
@@ -1381,14 +1239,6 @@ export type DeleteServiceParams = {
force?: boolean;
};
export type LeaveConversationBody = {
/** When the leaver is the only admin, optionally name a specific
remaining member to promote. Omit (or send null) to keep the
automatic behavior of promoting the longest-tenured member.
*/
successorId?: number | null;
};
export type ListUsersParams = {
/**
* Free-text search across username, email, and display names
@@ -1822,27 +1672,3 @@ export type GetAdminUserDependentOrdersParams = {
*/
offset?: number;
};
export type GetAdminUserDependentConversationsParams = {
/**
* @minimum 1
* @maximum 200
*/
limit?: number;
/**
* @minimum 0
*/
offset?: number;
};
export type GetAdminUserDependentMessagesParams = {
/**
* @minimum 1
* @maximum 200
*/
limit?: number;
/**
* @minimum 0
*/
offset?: number;
};
File diff suppressed because it is too large Load Diff
-539
View File
@@ -18,10 +18,6 @@ tags:
description: Internal services (khidmati)
- name: orders
description: Service orders workflow
- name: conversations
description: Chat conversations
- name: messages
description: Chat messages
- name: notifications
description: Notifications
- name: users
@@ -928,254 +924,6 @@ paths:
items:
$ref: "#/components/schemas/ServiceCategory"
# Conversations
/conversations:
get:
operationId: listConversations
tags: [conversations]
summary: List conversations for current user
responses:
"200":
description: Conversations
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/ConversationWithDetails"
post:
operationId: createConversation
tags: [conversations]
summary: Create a conversation
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateConversationBody"
responses:
"201":
description: Created
content:
application/json:
schema:
$ref: "#/components/schemas/ConversationWithDetails"
/conversations/{id}:
get:
operationId: getConversation
tags: [conversations]
summary: Get conversation by ID
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
"200":
description: Conversation
content:
application/json:
schema:
$ref: "#/components/schemas/ConversationWithDetails"
patch:
operationId: updateConversation
tags: [conversations]
summary: Update conversation properties (admin only)
parameters:
- name: id
in: path
required: true
schema:
type: integer
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateConversationBody"
responses:
"200":
description: Updated conversation
content:
application/json:
schema:
$ref: "#/components/schemas/ConversationWithDetails"
/conversations/{id}/participants:
post:
operationId: addConversationParticipants
tags: [conversations]
summary: Add participants to a group conversation (admin only)
parameters:
- name: id
in: path
required: true
schema:
type: integer
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/AddParticipantsBody"
responses:
"200":
description: Updated conversation
content:
application/json:
schema:
$ref: "#/components/schemas/ConversationWithDetails"
/conversations/{id}/participants/{userId}:
delete:
operationId: removeConversationParticipant
tags: [conversations]
summary: Remove a participant from a group conversation (admin only)
parameters:
- name: id
in: path
required: true
schema:
type: integer
- name: userId
in: path
required: true
schema:
type: integer
responses:
"200":
description: Updated conversation
content:
application/json:
schema:
$ref: "#/components/schemas/ConversationWithDetails"
/conversations/{id}/state:
patch:
operationId: updateConversationState
tags: [conversations]
summary: Update per-user state (mute, archive)
parameters:
- name: id
in: path
required: true
schema:
type: integer
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateConversationStateBody"
responses:
"200":
description: Updated conversation
content:
application/json:
schema:
$ref: "#/components/schemas/ConversationWithDetails"
/conversations/{id}/leave:
post:
operationId: leaveConversation
tags: [conversations]
summary: Leave a group conversation
parameters:
- name: id
in: path
required: true
schema:
type: integer
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
successorId:
type: integer
description: |
When the leaver is the only admin, optionally name a specific
remaining member to promote. Omit (or send null) to keep the
automatic behavior of promoting the longest-tenured member.
nullable: true
responses:
"200":
description: Left conversation
content:
application/json:
schema:
$ref: "#/components/schemas/SuccessResponse"
/conversations/{id}/messages:
get:
operationId: listMessages
tags: [messages]
summary: List messages in a conversation
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
"200":
description: Messages
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/MessageWithSender"
post:
operationId: sendMessage
tags: [messages]
summary: Send a message
parameters:
- name: id
in: path
required: true
schema:
type: integer
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/SendMessageBody"
responses:
"201":
description: Message sent
content:
application/json:
schema:
$ref: "#/components/schemas/MessageWithSender"
/conversations/{id}/read:
post:
operationId: markConversationRead
tags: [messages]
summary: Mark all messages in conversation as read
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
"200":
description: Marked as read
content:
application/json:
schema:
$ref: "#/components/schemas/SuccessResponse"
# Notifications
/notifications:
get:
@@ -3074,70 +2822,6 @@ paths:
schema:
$ref: "#/components/schemas/ErrorResponse"
/admin/users/{id}/dependents/conversations:
get:
operationId: getAdminUserDependentConversations
tags: [users]
summary: List conversations created by a user
parameters:
- in: path
name: id
required: true
schema: { type: integer }
- in: query
name: limit
required: false
schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
- in: query
name: offset
required: false
schema: { type: integer, minimum: 0, default: 0 }
responses:
"200":
description: Paged list of conversations
content:
application/json:
schema:
$ref: "#/components/schemas/UserDependentConversationsPage"
"404":
description: User not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/admin/users/{id}/dependents/messages:
get:
operationId: getAdminUserDependentMessages
tags: [users]
summary: List messages sent by a user
parameters:
- in: path
name: id
required: true
schema: { type: integer }
- in: query
name: limit
required: false
schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
- in: query
name: offset
required: false
schema: { type: integer, minimum: 0, default: 0 }
responses:
"200":
description: Paged list of messages
content:
application/json:
schema:
$ref: "#/components/schemas/UserDependentMessagesPage"
"404":
description: User not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
components:
schemas:
HealthStatus:
@@ -3463,16 +3147,6 @@ components:
description: |
Number of service orders owned by this user. Populated only by the
admin list endpoint (GET /users).
conversationCount:
type: integer
description: |
Number of conversations created by this user. Populated only by the
admin list endpoint (GET /users).
messageCount:
type: integer
description: |
Number of messages authored by this user. Populated only by the
admin list endpoint (GET /users).
required:
- id
- username
@@ -3817,16 +3491,10 @@ components:
type: integer
orderCount:
type: integer
conversationCount:
type: integer
messageCount:
type: integer
required:
- error
- noteCount
- orderCount
- conversationCount
- messageCount
AppDeletionConflict:
type: object
@@ -4304,161 +3972,6 @@ components:
- deletedIds
- failedIds
ConversationWithDetails:
type: object
properties:
id:
type: integer
nameAr:
type: ["string", "null"]
nameEn:
type: ["string", "null"]
avatarUrl:
type: ["string", "null"]
isGroup:
type: boolean
createdBy:
type: integer
participants:
type: array
items:
$ref: "#/components/schemas/ParticipantInfo"
lastMessage:
$ref: "#/components/schemas/MessageWithSender"
nullable: true
unreadCount:
type: integer
isMuted:
type: boolean
isArchived:
type: boolean
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time
required:
- id
- isGroup
- createdBy
- participants
- unreadCount
- isMuted
- isArchived
- createdAt
- updatedAt
ParticipantInfo:
type: object
properties:
id:
type: integer
username:
type: string
displayNameAr:
type: ["string", "null"]
displayNameEn:
type: ["string", "null"]
avatarUrl:
type: ["string", "null"]
isAdmin:
type: boolean
required:
- id
- username
- isAdmin
CreateConversationBody:
type: object
properties:
participantIds:
type: array
items:
type: integer
nameAr:
type: ["string", "null"]
nameEn:
type: ["string", "null"]
avatarUrl:
type: ["string", "null"]
isGroup:
type: boolean
required:
- participantIds
UpdateConversationBody:
type: object
properties:
avatarUrl:
type: ["string", "null"]
nameAr:
type: ["string", "null"]
nameEn:
type: ["string", "null"]
UpdateConversationStateBody:
type: object
properties:
isMuted:
type: boolean
isArchived:
type: boolean
AddParticipantsBody:
type: object
properties:
userIds:
type: array
items:
type: integer
required:
- userIds
MessageWithSender:
type: object
properties:
id:
type: integer
conversationId:
type: integer
senderId:
type: integer
content:
type: string
kind:
type: string
enum: [user, group_renamed, members_added, member_removed, member_left, admin_promoted]
meta:
type: object
nullable: true
additionalProperties: true
sender:
$ref: "#/components/schemas/ParticipantInfo"
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time
required:
- id
- conversationId
- senderId
- content
- kind
- sender
- createdAt
- updatedAt
SendMessageBody:
type: object
properties:
content:
type: string
required:
- content
Notification:
type: object
properties:
@@ -4579,15 +4092,12 @@ components:
type: integer
unreadNotifications:
type: integer
unreadMessages:
type: integer
totalUsers:
type: integer
required:
- totalApps
- totalServices
- unreadNotifications
- unreadMessages
- totalUsers
AdminStats:
@@ -5179,55 +4689,6 @@ components:
nextOffset: { type: ["integer", "null"] }
required: [items, totalCount, limit, offset, nextOffset]
UserDependentConversationItem:
type: object
properties:
id: { type: integer }
nameAr: { type: ["string", "null"] }
nameEn: { type: ["string", "null"] }
isGroup: { type: boolean }
createdAt: { type: string, format: date-time }
required: [id, nameAr, nameEn, isGroup, createdAt]
UserDependentConversationsPage:
type: object
properties:
items:
type: array
items:
$ref: "#/components/schemas/UserDependentConversationItem"
totalCount: { type: integer }
limit: { type: integer }
offset: { type: integer }
nextOffset: { type: ["integer", "null"] }
required: [items, totalCount, limit, offset, nextOffset]
UserDependentMessageItem:
type: object
properties:
id: { type: integer }
content: { type: string }
kind: { type: string }
createdAt: { type: string, format: date-time }
conversationId: { type: integer }
conversationNameAr: { type: ["string", "null"] }
conversationNameEn: { type: ["string", "null"] }
conversationIsGroup: { type: boolean }
required: [id, content, kind, createdAt, conversationId, conversationNameAr, conversationNameEn, conversationIsGroup]
UserDependentMessagesPage:
type: object
properties:
items:
type: array
items:
$ref: "#/components/schemas/UserDependentMessageItem"
totalCount: { type: integer }
limit: { type: integer }
offset: { type: integer }
nextOffset: { type: ["integer", "null"] }
required: [items, totalCount, limit, offset, nextOffset]
TopUserItem:
type: object
properties:
-596
View File
@@ -1332,456 +1332,6 @@ export const ListServiceCategoriesResponse = zod.array(
ListServiceCategoriesResponseItem,
);
/**
* @summary List conversations for current user
*/
export const ListConversationsResponseItem = zod.object({
id: zod.number(),
nameAr: zod.string().nullish(),
nameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
isGroup: zod.boolean(),
createdBy: zod.number(),
participants: zod.array(
zod.object({
id: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
isAdmin: zod.boolean(),
}),
),
lastMessage: zod
.object({
id: zod.number(),
conversationId: zod.number(),
senderId: zod.number(),
content: zod.string(),
kind: zod.enum([
"user",
"group_renamed",
"members_added",
"member_removed",
"member_left",
"admin_promoted",
]),
meta: zod.record(zod.string(), zod.unknown()).nullish(),
sender: zod.object({
id: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
isAdmin: zod.boolean(),
}),
createdAt: zod.coerce.date(),
updatedAt: zod.coerce.date(),
})
.nullish(),
unreadCount: zod.number(),
isMuted: zod.boolean(),
isArchived: zod.boolean(),
createdAt: zod.coerce.date(),
updatedAt: zod.coerce.date(),
});
export const ListConversationsResponse = zod.array(
ListConversationsResponseItem,
);
/**
* @summary Create a conversation
*/
export const CreateConversationBody = zod.object({
participantIds: zod.array(zod.number()),
nameAr: zod.string().nullish(),
nameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
isGroup: zod.boolean().optional(),
});
/**
* @summary Get conversation by ID
*/
export const GetConversationParams = zod.object({
id: zod.coerce.number(),
});
export const GetConversationResponse = zod.object({
id: zod.number(),
nameAr: zod.string().nullish(),
nameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
isGroup: zod.boolean(),
createdBy: zod.number(),
participants: zod.array(
zod.object({
id: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
isAdmin: zod.boolean(),
}),
),
lastMessage: zod
.object({
id: zod.number(),
conversationId: zod.number(),
senderId: zod.number(),
content: zod.string(),
kind: zod.enum([
"user",
"group_renamed",
"members_added",
"member_removed",
"member_left",
"admin_promoted",
]),
meta: zod.record(zod.string(), zod.unknown()).nullish(),
sender: zod.object({
id: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
isAdmin: zod.boolean(),
}),
createdAt: zod.coerce.date(),
updatedAt: zod.coerce.date(),
})
.nullish(),
unreadCount: zod.number(),
isMuted: zod.boolean(),
isArchived: zod.boolean(),
createdAt: zod.coerce.date(),
updatedAt: zod.coerce.date(),
});
/**
* @summary Update conversation properties (admin only)
*/
export const UpdateConversationParams = zod.object({
id: zod.coerce.number(),
});
export const UpdateConversationBody = zod.object({
avatarUrl: zod.string().nullish(),
nameAr: zod.string().nullish(),
nameEn: zod.string().nullish(),
});
export const UpdateConversationResponse = zod.object({
id: zod.number(),
nameAr: zod.string().nullish(),
nameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
isGroup: zod.boolean(),
createdBy: zod.number(),
participants: zod.array(
zod.object({
id: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
isAdmin: zod.boolean(),
}),
),
lastMessage: zod
.object({
id: zod.number(),
conversationId: zod.number(),
senderId: zod.number(),
content: zod.string(),
kind: zod.enum([
"user",
"group_renamed",
"members_added",
"member_removed",
"member_left",
"admin_promoted",
]),
meta: zod.record(zod.string(), zod.unknown()).nullish(),
sender: zod.object({
id: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
isAdmin: zod.boolean(),
}),
createdAt: zod.coerce.date(),
updatedAt: zod.coerce.date(),
})
.nullish(),
unreadCount: zod.number(),
isMuted: zod.boolean(),
isArchived: zod.boolean(),
createdAt: zod.coerce.date(),
updatedAt: zod.coerce.date(),
});
/**
* @summary Add participants to a group conversation (admin only)
*/
export const AddConversationParticipantsParams = zod.object({
id: zod.coerce.number(),
});
export const AddConversationParticipantsBody = zod.object({
userIds: zod.array(zod.number()),
});
export const AddConversationParticipantsResponse = zod.object({
id: zod.number(),
nameAr: zod.string().nullish(),
nameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
isGroup: zod.boolean(),
createdBy: zod.number(),
participants: zod.array(
zod.object({
id: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
isAdmin: zod.boolean(),
}),
),
lastMessage: zod
.object({
id: zod.number(),
conversationId: zod.number(),
senderId: zod.number(),
content: zod.string(),
kind: zod.enum([
"user",
"group_renamed",
"members_added",
"member_removed",
"member_left",
"admin_promoted",
]),
meta: zod.record(zod.string(), zod.unknown()).nullish(),
sender: zod.object({
id: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
isAdmin: zod.boolean(),
}),
createdAt: zod.coerce.date(),
updatedAt: zod.coerce.date(),
})
.nullish(),
unreadCount: zod.number(),
isMuted: zod.boolean(),
isArchived: zod.boolean(),
createdAt: zod.coerce.date(),
updatedAt: zod.coerce.date(),
});
/**
* @summary Remove a participant from a group conversation (admin only)
*/
export const RemoveConversationParticipantParams = zod.object({
id: zod.coerce.number(),
userId: zod.coerce.number(),
});
export const RemoveConversationParticipantResponse = zod.object({
id: zod.number(),
nameAr: zod.string().nullish(),
nameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
isGroup: zod.boolean(),
createdBy: zod.number(),
participants: zod.array(
zod.object({
id: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
isAdmin: zod.boolean(),
}),
),
lastMessage: zod
.object({
id: zod.number(),
conversationId: zod.number(),
senderId: zod.number(),
content: zod.string(),
kind: zod.enum([
"user",
"group_renamed",
"members_added",
"member_removed",
"member_left",
"admin_promoted",
]),
meta: zod.record(zod.string(), zod.unknown()).nullish(),
sender: zod.object({
id: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
isAdmin: zod.boolean(),
}),
createdAt: zod.coerce.date(),
updatedAt: zod.coerce.date(),
})
.nullish(),
unreadCount: zod.number(),
isMuted: zod.boolean(),
isArchived: zod.boolean(),
createdAt: zod.coerce.date(),
updatedAt: zod.coerce.date(),
});
/**
* @summary Update per-user state (mute, archive)
*/
export const UpdateConversationStateParams = zod.object({
id: zod.coerce.number(),
});
export const UpdateConversationStateBody = zod.object({
isMuted: zod.boolean().optional(),
isArchived: zod.boolean().optional(),
});
export const UpdateConversationStateResponse = zod.object({
id: zod.number(),
nameAr: zod.string().nullish(),
nameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
isGroup: zod.boolean(),
createdBy: zod.number(),
participants: zod.array(
zod.object({
id: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
isAdmin: zod.boolean(),
}),
),
lastMessage: zod
.object({
id: zod.number(),
conversationId: zod.number(),
senderId: zod.number(),
content: zod.string(),
kind: zod.enum([
"user",
"group_renamed",
"members_added",
"member_removed",
"member_left",
"admin_promoted",
]),
meta: zod.record(zod.string(), zod.unknown()).nullish(),
sender: zod.object({
id: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
isAdmin: zod.boolean(),
}),
createdAt: zod.coerce.date(),
updatedAt: zod.coerce.date(),
})
.nullish(),
unreadCount: zod.number(),
isMuted: zod.boolean(),
isArchived: zod.boolean(),
createdAt: zod.coerce.date(),
updatedAt: zod.coerce.date(),
});
/**
* @summary Leave a group conversation
*/
export const LeaveConversationParams = zod.object({
id: zod.coerce.number(),
});
export const LeaveConversationBody = zod.object({
successorId: zod
.number()
.nullish()
.describe(
"When the leaver is the only admin, optionally name a specific\nremaining member to promote. Omit (or send null) to keep the\nautomatic behavior of promoting the longest-tenured member.\n",
),
});
export const LeaveConversationResponse = zod.object({
success: zod.boolean(),
});
/**
* @summary List messages in a conversation
*/
export const ListMessagesParams = zod.object({
id: zod.coerce.number(),
});
export const ListMessagesResponseItem = zod.object({
id: zod.number(),
conversationId: zod.number(),
senderId: zod.number(),
content: zod.string(),
kind: zod.enum([
"user",
"group_renamed",
"members_added",
"member_removed",
"member_left",
"admin_promoted",
]),
meta: zod.record(zod.string(), zod.unknown()).nullish(),
sender: zod.object({
id: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
isAdmin: zod.boolean(),
}),
createdAt: zod.coerce.date(),
updatedAt: zod.coerce.date(),
});
export const ListMessagesResponse = zod.array(ListMessagesResponseItem);
/**
* @summary Send a message
*/
export const SendMessageParams = zod.object({
id: zod.coerce.number(),
});
export const SendMessageBody = zod.object({
content: zod.string(),
});
/**
* @summary Mark all messages in conversation as read
*/
export const MarkConversationReadParams = zod.object({
id: zod.coerce.number(),
});
export const MarkConversationReadResponse = zod.object({
success: zod.boolean(),
});
/**
* @summary List notifications for current user
*/
@@ -1888,18 +1438,6 @@ export const ListUsersResponseItem = zod.object({
.describe(
"Number of service orders owned by this user. Populated only by the\nadmin list endpoint (GET \/users).\n",
),
conversationCount: zod
.number()
.optional()
.describe(
"Number of conversations created by this user. Populated only by the\nadmin list endpoint (GET \/users).\n",
),
messageCount: zod
.number()
.optional()
.describe(
"Number of messages authored by this user. Populated only by the\nadmin list endpoint (GET \/users).\n",
),
});
export const ListUsersResponse = zod.array(ListUsersResponseItem);
@@ -1972,18 +1510,6 @@ export const GetUserResponse = zod.object({
.describe(
"Number of service orders owned by this user. Populated only by the\nadmin list endpoint (GET \/users).\n",
),
conversationCount: zod
.number()
.optional()
.describe(
"Number of conversations created by this user. Populated only by the\nadmin list endpoint (GET \/users).\n",
),
messageCount: zod
.number()
.optional()
.describe(
"Number of messages authored by this user. Populated only by the\nadmin list endpoint (GET \/users).\n",
),
});
/**
@@ -2044,18 +1570,6 @@ export const UpdateUserResponse = zod.object({
.describe(
"Number of service orders owned by this user. Populated only by the\nadmin list endpoint (GET \/users).\n",
),
conversationCount: zod
.number()
.optional()
.describe(
"Number of conversations created by this user. Populated only by the\nadmin list endpoint (GET \/users).\n",
),
messageCount: zod
.number()
.optional()
.describe(
"Number of messages authored by this user. Populated only by the\nadmin list endpoint (GET \/users).\n",
),
});
/**
@@ -2169,18 +1683,6 @@ export const AddUserRoleResponse = zod.object({
.describe(
"Number of service orders owned by this user. Populated only by the\nadmin list endpoint (GET \/users).\n",
),
conversationCount: zod
.number()
.optional()
.describe(
"Number of conversations created by this user. Populated only by the\nadmin list endpoint (GET \/users).\n",
),
messageCount: zod
.number()
.optional()
.describe(
"Number of messages authored by this user. Populated only by the\nadmin list endpoint (GET \/users).\n",
),
});
/**
@@ -2231,18 +1733,6 @@ export const RemoveUserRoleResponse = zod.object({
.describe(
"Number of service orders owned by this user. Populated only by the\nadmin list endpoint (GET \/users).\n",
),
conversationCount: zod
.number()
.optional()
.describe(
"Number of conversations created by this user. Populated only by the\nadmin list endpoint (GET \/users).\n",
),
messageCount: zod
.number()
.optional()
.describe(
"Number of messages authored by this user. Populated only by the\nadmin list endpoint (GET \/users).\n",
),
});
/**
@@ -2878,7 +2368,6 @@ export const GetHomeStatsResponse = zod.object({
totalApps: zod.number(),
totalServices: zod.number(),
unreadNotifications: zod.number(),
unreadMessages: zod.number(),
totalUsers: zod.number(),
});
@@ -3830,88 +3319,3 @@ export const GetAdminUserDependentOrdersResponse = zod.object({
offset: zod.number(),
nextOffset: zod.number().nullable(),
});
/**
* @summary List conversations created by a user
*/
export const GetAdminUserDependentConversationsParams = zod.object({
id: zod.coerce.number(),
});
export const getAdminUserDependentConversationsQueryLimitDefault = 50;
export const getAdminUserDependentConversationsQueryLimitMax = 200;
export const getAdminUserDependentConversationsQueryOffsetDefault = 0;
export const getAdminUserDependentConversationsQueryOffsetMin = 0;
export const GetAdminUserDependentConversationsQueryParams = zod.object({
limit: zod.coerce
.number()
.min(1)
.max(getAdminUserDependentConversationsQueryLimitMax)
.default(getAdminUserDependentConversationsQueryLimitDefault),
offset: zod.coerce
.number()
.min(getAdminUserDependentConversationsQueryOffsetMin)
.default(getAdminUserDependentConversationsQueryOffsetDefault),
});
export const GetAdminUserDependentConversationsResponse = zod.object({
items: zod.array(
zod.object({
id: zod.number(),
nameAr: zod.string().nullable(),
nameEn: zod.string().nullable(),
isGroup: zod.boolean(),
createdAt: zod.coerce.date(),
}),
),
totalCount: zod.number(),
limit: zod.number(),
offset: zod.number(),
nextOffset: zod.number().nullable(),
});
/**
* @summary List messages sent by a user
*/
export const GetAdminUserDependentMessagesParams = zod.object({
id: zod.coerce.number(),
});
export const getAdminUserDependentMessagesQueryLimitDefault = 50;
export const getAdminUserDependentMessagesQueryLimitMax = 200;
export const getAdminUserDependentMessagesQueryOffsetDefault = 0;
export const getAdminUserDependentMessagesQueryOffsetMin = 0;
export const GetAdminUserDependentMessagesQueryParams = zod.object({
limit: zod.coerce
.number()
.min(1)
.max(getAdminUserDependentMessagesQueryLimitMax)
.default(getAdminUserDependentMessagesQueryLimitDefault),
offset: zod.coerce
.number()
.min(getAdminUserDependentMessagesQueryOffsetMin)
.default(getAdminUserDependentMessagesQueryOffsetDefault),
});
export const GetAdminUserDependentMessagesResponse = zod.object({
items: zod.array(
zod.object({
id: zod.number(),
content: zod.string(),
kind: zod.string(),
createdAt: zod.coerce.date(),
conversationId: zod.number(),
conversationNameAr: zod.string().nullable(),
conversationNameEn: zod.string().nullable(),
conversationIsGroup: zod.boolean(),
}),
),
totalCount: zod.number(),
limit: zod.number(),
offset: zod.number(),
nextOffset: zod.number().nullable(),
});
-47
View File
@@ -1,47 +0,0 @@
import { pgTable, text, serial, timestamp, integer, varchar, boolean, primaryKey, jsonb } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { z } from "zod/v4";
import { usersTable } from "./users";
export const conversationsTable = pgTable("conversations", {
id: serial("id").primaryKey(),
nameAr: varchar("name_ar", { length: 300 }),
nameEn: varchar("name_en", { length: 300 }),
avatarUrl: text("avatar_url"),
isGroup: boolean("is_group").notNull().default(false),
createdBy: integer("created_by").notNull().references(() => usersTable.id),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow().$onUpdate(() => new Date()),
});
export const conversationParticipantsTable = pgTable("conversation_participants", {
conversationId: integer("conversation_id").notNull().references(() => conversationsTable.id, { onDelete: "cascade" }),
userId: integer("user_id").notNull().references(() => usersTable.id, { onDelete: "cascade" }),
joinedAt: timestamp("joined_at", { withTimezone: true }).notNull().defaultNow(),
isAdmin: boolean("is_admin").notNull().default(false),
isMuted: boolean("is_muted").notNull().default(false),
isArchived: boolean("is_archived").notNull().default(false),
}, (t) => [primaryKey({ columns: [t.conversationId, t.userId] })]);
export const messagesTable = pgTable("messages", {
id: serial("id").primaryKey(),
conversationId: integer("conversation_id").notNull().references(() => conversationsTable.id, { onDelete: "cascade" }),
senderId: integer("sender_id").notNull().references(() => usersTable.id),
content: text("content").notNull(),
kind: varchar("kind", { length: 32 }).notNull().default("user"),
meta: jsonb("meta"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow().$onUpdate(() => new Date()),
});
export const messageReadsTable = pgTable("message_reads", {
messageId: integer("message_id").notNull().references(() => messagesTable.id, { onDelete: "cascade" }),
userId: integer("user_id").notNull().references(() => usersTable.id, { onDelete: "cascade" }),
readAt: timestamp("read_at", { withTimezone: true }).notNull().defaultNow(),
}, (t) => [primaryKey({ columns: [t.messageId, t.userId] })]);
export const insertMessageSchema = createInsertSchema(messagesTable).omit({ id: true, createdAt: true, updatedAt: true });
export const insertConversationSchema = createInsertSchema(conversationsTable).omit({ id: true, createdAt: true, updatedAt: true });
export type InsertMessage = z.infer<typeof insertMessageSchema>;
export type Message = typeof messagesTable.$inferSelect;
export type Conversation = typeof conversationsTable.$inferSelect;
-1
View File
@@ -3,7 +3,6 @@ export * from "./roles";
export * from "./apps";
export * from "./services";
export * from "./service-orders";
export * from "./conversations";
export * from "./notifications";
export * from "./settings";
export * from "./user-app-orders";