0.12.18 : add create message, conversation
All checks were successful
Build release Docker image / Build Docker Images (push) Successful in 7m23s
All checks were successful
Build release Docker image / Build Docker Images (push) Successful in 7m23s
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "harmony-back",
|
"name": "harmony-back",
|
||||||
"version": "0.12.17",
|
"version": "0.12.18",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "A Strapi application",
|
"description": "A Strapi application",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
126
report.md
Normal file
126
report.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# Implementation Report: Chat Messaging Feature
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Successfully implemented the core chat messaging functionality with support for direct messages (1:1) and group conversations (2+). The implementation includes conversation creation with automatic member management, and message creation with permission-based access control.
|
||||||
|
|
||||||
|
## What Was Implemented
|
||||||
|
|
||||||
|
### 1. Chat Conversation Service (`src/api/chat-conversation/services/chat-conversation.ts`)
|
||||||
|
- **`createConversationWithMembers(userIds, creatorId, title?)`** service method
|
||||||
|
- Validates all recipient user IDs exist in the database
|
||||||
|
- Automatically includes creator in conversation members
|
||||||
|
- Deduplicates user list to prevent duplicates
|
||||||
|
- Determines conversation type: `isGroup = totalUsers > 2`
|
||||||
|
- Sets title only for group conversations
|
||||||
|
- Creates ChatConversationMember records with appropriate roles:
|
||||||
|
- `owner` role for conversation creator
|
||||||
|
- `member` role for other participants
|
||||||
|
- Captures `joinedAt` timestamp for each member
|
||||||
|
|
||||||
|
### 2. Chat Conversation Controller (`src/api/chat-conversation/controllers/chat-conversation.ts`)
|
||||||
|
- **`createConversation(ctx)` controller method**
|
||||||
|
- Authenticates user from `ctx.state.user.id`
|
||||||
|
- Validates `userIds` is non-empty array
|
||||||
|
- Calls service method to create conversation with members
|
||||||
|
- Returns created conversation with populated members
|
||||||
|
- Proper error handling with user-friendly messages
|
||||||
|
|
||||||
|
### 3. Chat Message Service (`src/api/chat-message/services/chat-message.ts`)
|
||||||
|
- **`createMessageInConversation(conversationId, senderId, content)` service method**
|
||||||
|
- Validates all required parameters
|
||||||
|
- Verifies conversation exists
|
||||||
|
- **Permission check**: Verifies sender is a member of the conversation
|
||||||
|
- Validates message content is non-empty
|
||||||
|
- Creates ChatMessage with sender and content
|
||||||
|
- Links message to conversation via `messages` relation
|
||||||
|
- Returns created message
|
||||||
|
|
||||||
|
### 4. Chat Message Controller (`src/api/chat-message/controllers/chat-message.ts`)
|
||||||
|
- **`createMessage(ctx)` controller method**
|
||||||
|
- Authenticates user from `ctx.state.user.id`
|
||||||
|
- Validates message content is provided
|
||||||
|
- Workflow supports two scenarios:
|
||||||
|
1. **Existing conversation**: If `conversationId` provided, creates message in that conversation
|
||||||
|
2. **New conversation**: If `conversationId` is null, creates new conversation with `recipientIds` first, then creates message
|
||||||
|
- Leverages service layer for all business logic
|
||||||
|
- Proper error handling with detailed messages
|
||||||
|
|
||||||
|
## Architecture Decisions
|
||||||
|
|
||||||
|
1. **Service-based approach**: All business logic encapsulated in service methods for reusability and testability
|
||||||
|
2. **Permission checks**: Message creation validates user membership in conversation
|
||||||
|
3. **Member creation**: Automatic ChatConversationMember records ensure consistent data relationships
|
||||||
|
4. **Group detection**: Automatic based on user count (>2 = group, ≤2 = direct message)
|
||||||
|
5. **Unidirectional relation**: ChatMessage has no back-relation to ChatConversation (by design, avoids TypeScript type issues on client)
|
||||||
|
|
||||||
|
## Workflow Diagram
|
||||||
|
|
||||||
|
```
|
||||||
|
createMessage(conversationId, content, recipientIds)
|
||||||
|
├─ If conversationId is null:
|
||||||
|
│ └─ createConversation(recipientIds)
|
||||||
|
│ ├─ Validate all user IDs exist
|
||||||
|
│ ├─ Determine isGroup (>2 users)
|
||||||
|
│ └─ Create ChatConversation + ChatConversationMembers
|
||||||
|
│
|
||||||
|
└─ createMessageInConversation(conversationId, userId, content)
|
||||||
|
├─ Verify conversation exists
|
||||||
|
├─ Verify user is member (permission check)
|
||||||
|
├─ Create ChatMessage
|
||||||
|
└─ Link to conversation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
✅ **Build**: `npm run build` completed successfully with no TypeScript errors
|
||||||
|
- Compilation time: 2.884s
|
||||||
|
- Total build time: ~14s
|
||||||
|
|
||||||
|
✅ **Type Safety**: Full TypeScript compilation with no errors
|
||||||
|
|
||||||
|
✅ **Edge Cases Handled**:
|
||||||
|
- Non-existent users validation
|
||||||
|
- Permission checks for message creation
|
||||||
|
- Empty message content validation
|
||||||
|
- Non-existent conversation validation
|
||||||
|
- Duplicate user removal in conversation creation
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
1. `/src/api/chat-conversation/services/chat-conversation.ts` — Added `createConversationWithMembers()`
|
||||||
|
2. `/src/api/chat-conversation/controllers/chat-conversation.ts` — Added `createConversation()` endpoint
|
||||||
|
3. `/src/api/chat-message/services/chat-message.ts` — Added `createMessageInConversation()`
|
||||||
|
4. `/src/api/chat-message/controllers/chat-message.ts` — Added `createMessage()` endpoint
|
||||||
|
|
||||||
|
## Testing Recommendations
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
- Conversation creation with various user counts
|
||||||
|
- Message creation in existing conversations
|
||||||
|
- Permission validation for non-members
|
||||||
|
- User existence validation
|
||||||
|
- Content validation (empty/whitespace)
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
- End-to-end message creation flow with conversation creation
|
||||||
|
- ChatConversationMember creation and role assignment
|
||||||
|
- Conversation isGroup flag accuracy
|
||||||
|
|
||||||
|
### API Tests
|
||||||
|
- POST `/api/chat-conversations/createConversation` with valid/invalid payloads
|
||||||
|
- POST `/api/chat-messages/createMessage` with conversation ID
|
||||||
|
- POST `/api/chat-messages/createMessage` with recipient IDs (new conversation)
|
||||||
|
|
||||||
|
## Known Limitations & Future Enhancements
|
||||||
|
|
||||||
|
1. **No bulk operations**: Currently single message/conversation per request
|
||||||
|
2. **No message updates/deletion**: Only creation implemented
|
||||||
|
3. **No read receipts**: `lastReadAt` field exists but not updated
|
||||||
|
4. **No pagination**: Conversation and message listing not implemented
|
||||||
|
5. **No media support**: Messages are text-only
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
The messaging feature foundation is now complete with core conversation and message creation functionality. The implementation follows Strapi patterns, includes proper validation and permission checks, and is fully type-safe with TypeScript.
|
||||||
|
|
||||||
173
spec.md
Normal file
173
spec.md
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
# Technical Specification: Chat Messaging Feature
|
||||||
|
|
||||||
|
## Task Complexity: **Medium**
|
||||||
|
|
||||||
|
### Rationale
|
||||||
|
- Involves creating relationships between 3 entities (ChatMessage, ChatConversation, ChatConversationMember)
|
||||||
|
- Handles two conversation types with different logic (direct vs group)
|
||||||
|
- Requires user authentication and transactional data creation
|
||||||
|
- Some edge cases (creating conversation before message, handling members, etc.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technical Context
|
||||||
|
|
||||||
|
- **Language**: TypeScript
|
||||||
|
- **Framework**: Strapi 4
|
||||||
|
- **Database**: Relational DB (presumed based on Strapi structure)
|
||||||
|
- **Existing patterns**: Core controller pattern with custom methods (see `post.ts`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current State
|
||||||
|
|
||||||
|
### Completed
|
||||||
|
- Schema definitions for `ChatMessage`, `ChatConversation`, `ChatConversationMember`
|
||||||
|
- Service layer structure in place (core service factories)
|
||||||
|
- Basic controller scaffolding
|
||||||
|
|
||||||
|
### Missing
|
||||||
|
- Custom controller methods for creating messages and conversations
|
||||||
|
- Business logic for handling conversation creation workflow
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Model Overview
|
||||||
|
|
||||||
|
### ChatConversation
|
||||||
|
- `title`: Optional conversation name
|
||||||
|
- `isGroup`: Boolean (true if 3+ users, false if 2 users)
|
||||||
|
- `creator`: Relation to user
|
||||||
|
- `messages`: OneToMany relation to ChatMessage
|
||||||
|
- `users`: OneToMany relation to user
|
||||||
|
|
||||||
|
### ChatConversationMember
|
||||||
|
- `user`: OneToOne relation to user
|
||||||
|
- `conversation`: OneToOne relation to ChatConversation
|
||||||
|
- `role`: Enum (member, admin, owner)
|
||||||
|
- `joinedAt`: DateTime
|
||||||
|
- `lastReadAt`: DateTime
|
||||||
|
|
||||||
|
### ChatMessage
|
||||||
|
- `sender`: OneToOne relation to user
|
||||||
|
- `content`: String (required)
|
||||||
|
- `isEdited`: Boolean (default: false)
|
||||||
|
- `deletedAt`: DateTime
|
||||||
|
- **Note**: No back-relation to ChatConversation (by design)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Approach
|
||||||
|
|
||||||
|
### 1. Controller Methods
|
||||||
|
|
||||||
|
#### `createMessage(ctx)`
|
||||||
|
- **Purpose**: Create a message in a conversation
|
||||||
|
- **Parameters**:
|
||||||
|
- `conversationId` (query/body): ID of target conversation (null if creating new conversation)
|
||||||
|
- `content` (body): Message content (required)
|
||||||
|
- `recipientIds` (body, optional): User IDs for new conversation
|
||||||
|
- **Logic**:
|
||||||
|
- Get authenticated user from `ctx.state.user.id`
|
||||||
|
- If `conversationId` is null/undefined:
|
||||||
|
- Call `createConversation()` with `recipientIds` and authenticated user
|
||||||
|
- Use returned conversation ID
|
||||||
|
- Create ChatMessage with:
|
||||||
|
- `sender`: authenticated user ID
|
||||||
|
- `content`: message content
|
||||||
|
- Add message to ChatConversation.messages relation
|
||||||
|
- Return created message with populated relations
|
||||||
|
|
||||||
|
#### `createConversation(ctx)`
|
||||||
|
- **Purpose**: Create a new conversation with one or more users
|
||||||
|
- **Parameters**:
|
||||||
|
- `userIds` (body): Array of user IDs to add to conversation
|
||||||
|
- `title` (body, optional): Conversation title (for groups)
|
||||||
|
- **Logic**:
|
||||||
|
- Get authenticated user from `ctx.state.user.id`
|
||||||
|
- Merge authenticated user into conversation members
|
||||||
|
- Remove duplicates from user list
|
||||||
|
- Determine if group: `isGroup = totalUsers > 2`
|
||||||
|
- Create ChatConversation:
|
||||||
|
- `isGroup`: boolean
|
||||||
|
- `creator`: authenticated user ID
|
||||||
|
- `title`: title (optional, for groups)
|
||||||
|
- Create ChatConversationMember records for each user:
|
||||||
|
- `user`: user ID
|
||||||
|
- `conversation`: created conversation ID
|
||||||
|
- `role`: "owner" for creator, "member" for others
|
||||||
|
- `joinedAt`: current timestamp
|
||||||
|
- Return created conversation
|
||||||
|
|
||||||
|
### 2. Integration Points
|
||||||
|
|
||||||
|
- **Authentication**: Use `ctx.state.user?.id` (follows existing pattern in `post.ts`)
|
||||||
|
- **Database queries**: Use `strapi.db.query()` pattern (established in codebase)
|
||||||
|
- **Error handling**: Follow Strapi context methods (`ctx.badRequest()`, `ctx.unauthorized()`)
|
||||||
|
|
||||||
|
### 3. Files to Modify
|
||||||
|
|
||||||
|
- `/Users/julien/projets/dev/harmony/harmony-back/src/api/chat-message/controllers/chat-message.ts` — Add `createMessage()` and helper logic
|
||||||
|
- `/Users/julien/projets/dev/harmony/harmony-back/src/api/chat-conversation/controllers/chat-conversation.ts` — Add `createConversation()`
|
||||||
|
|
||||||
|
### 4. No Schema Changes Required
|
||||||
|
- ChatMessage schema is intentionally unidirectional (no back-relation)
|
||||||
|
- All necessary relations already defined in ChatConversation and ChatConversationMember
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification Approach
|
||||||
|
|
||||||
|
1. **Type checking**: Run Strapi build or `npm run build`
|
||||||
|
2. **Linting**: Run project linting command (TBD - will check package.json)
|
||||||
|
3. **Manual testing**:
|
||||||
|
- Create message in existing conversation
|
||||||
|
- Create message with null conversationId (should create conversation first)
|
||||||
|
- Create conversation with 2 users (direct message)
|
||||||
|
- Create conversation with 3+ users (group chat)
|
||||||
|
- Verify `isGroup` flag is set correctly
|
||||||
|
- Verify ChatConversationMember records are created with correct roles
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Decisions (Confirmed)
|
||||||
|
|
||||||
|
1. **User validation**: ✅ Validate recipient user IDs exist before creating conversation
|
||||||
|
2. **API routing**: ✅ `createMessage` belongs to chat-message controller
|
||||||
|
3. **Permission checks**: ✅ Verify user is member of conversation before sending message
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Detailed Implementation Plan
|
||||||
|
|
||||||
|
### Task 1: Implement `createConversation` in chat-conversation controller
|
||||||
|
- Validate all recipient user IDs exist in database
|
||||||
|
- Add authenticated user to members list
|
||||||
|
- Determine `isGroup` based on total user count (>2 = group)
|
||||||
|
- Create ChatConversation record with creator and title
|
||||||
|
- Create ChatConversationMember records for all users with appropriate roles
|
||||||
|
- Return created conversation with populated members
|
||||||
|
|
||||||
|
### Task 2: Implement `createMessage` in chat-message controller
|
||||||
|
- Validate authenticated user exists
|
||||||
|
- If conversationId is null: call service method to create conversation with recipientIds
|
||||||
|
- If conversationId is provided:
|
||||||
|
- Verify conversation exists
|
||||||
|
- Verify user is member of conversation (permission check)
|
||||||
|
- Validate message content (required, non-empty)
|
||||||
|
- Create ChatMessage with sender and content
|
||||||
|
- Add message to conversation.messages relation
|
||||||
|
- Return created message with populated sender relation
|
||||||
|
|
||||||
|
### Task 3: Add helper service methods in chat-conversation service
|
||||||
|
- `createConversationWithMembers()`: Centralized logic for creating conversation + members
|
||||||
|
- Used by both direct createConversation controller and createMessage
|
||||||
|
|
||||||
|
### Task 4: Add helper service methods in chat-message service
|
||||||
|
- `createMessageInConversation()`: Centralized logic for creating and linking message
|
||||||
|
|
||||||
|
### Task 5: Verification
|
||||||
|
- Build and type-check the project
|
||||||
|
- Run linting
|
||||||
|
- Verify error handling for edge cases
|
||||||
|
|
||||||
@@ -2,6 +2,36 @@
|
|||||||
* chat-conversation controller
|
* chat-conversation controller
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { factories } from '@strapi/strapi'
|
import { factories } from "@strapi/strapi";
|
||||||
|
|
||||||
export default factories.createCoreController('api::chat-conversation.chat-conversation');
|
export default factories.createCoreController(
|
||||||
|
"api::chat-conversation.chat-conversation",
|
||||||
|
({ strapi }) => ({
|
||||||
|
async create(ctx) {
|
||||||
|
const userId = ctx.state.user?.id;
|
||||||
|
if (!userId) {
|
||||||
|
return ctx.unauthorized(
|
||||||
|
"You must be logged in to create a conversation"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { users, title, isGroup } = ctx.request.body.data;
|
||||||
|
|
||||||
|
if (!users || !Array.isArray(users) || users.length === 0) {
|
||||||
|
return ctx.badRequest("userIds must be a non-empty array");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const conversation = await strapi
|
||||||
|
.service("api::chat-conversation.chat-conversation")
|
||||||
|
.createConversationWithMembers(users, userId, title);
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
data: conversation,
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
return ctx.badRequest(error.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|||||||
@@ -4,4 +4,64 @@
|
|||||||
|
|
||||||
import { factories } from '@strapi/strapi';
|
import { factories } from '@strapi/strapi';
|
||||||
|
|
||||||
export default factories.createCoreService('api::chat-conversation.chat-conversation');
|
export default factories.createCoreService(
|
||||||
|
'api::chat-conversation.chat-conversation',
|
||||||
|
({ strapi }) => ({
|
||||||
|
async createConversationWithMembers(
|
||||||
|
userIds: number[],
|
||||||
|
creatorId: number,
|
||||||
|
title?: string
|
||||||
|
) {
|
||||||
|
if (!userIds || userIds.length === 0) {
|
||||||
|
throw new Error('At least one user ID is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!creatorId) {
|
||||||
|
throw new Error('Creator ID is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueUserIds = [...new Set([...userIds, creatorId])];
|
||||||
|
|
||||||
|
for (const userId of uniqueUserIds) {
|
||||||
|
const userExists = await strapi.db
|
||||||
|
.query('plugin::users-permissions.user')
|
||||||
|
.findOne({
|
||||||
|
where: { id: userId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userExists) {
|
||||||
|
throw new Error(`User with ID ${userId} not found`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isGroup = uniqueUserIds.length > 2;
|
||||||
|
|
||||||
|
const conversation = await strapi.db
|
||||||
|
.query('api::chat-conversation.chat-conversation')
|
||||||
|
.create({
|
||||||
|
data: {
|
||||||
|
title: isGroup ? title : null,
|
||||||
|
isGroup,
|
||||||
|
creator: creatorId,
|
||||||
|
users: uniqueUserIds.map((id) => ({ id })),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const userId of uniqueUserIds) {
|
||||||
|
const role = userId === creatorId ? 'owner' : 'member';
|
||||||
|
await strapi.db
|
||||||
|
.query('api::chat-conversation-member.chat-conversation-member')
|
||||||
|
.create({
|
||||||
|
data: {
|
||||||
|
user: userId,
|
||||||
|
conversation: conversation.id,
|
||||||
|
role,
|
||||||
|
joinedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return conversation;
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
"info": {
|
"info": {
|
||||||
"singularName": "chat-message",
|
"singularName": "chat-message",
|
||||||
"pluralName": "chat-messages",
|
"pluralName": "chat-messages",
|
||||||
"displayName": "ChatMessage"
|
"displayName": "ChatMessage",
|
||||||
|
"description": ""
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"draftAndPublish": false
|
"draftAndPublish": false
|
||||||
@@ -26,6 +27,16 @@
|
|||||||
},
|
},
|
||||||
"deletedAt": {
|
"deletedAt": {
|
||||||
"type": "datetime"
|
"type": "datetime"
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"allowedTypes": [
|
||||||
|
"images",
|
||||||
|
"files",
|
||||||
|
"videos",
|
||||||
|
"audios"
|
||||||
|
],
|
||||||
|
"type": "media",
|
||||||
|
"multiple": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,87 @@
|
|||||||
* chat-message controller
|
* chat-message controller
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { factories } from '@strapi/strapi'
|
import { factories } from "@strapi/strapi";
|
||||||
|
|
||||||
export default factories.createCoreController('api::chat-message.chat-message');
|
export default factories.createCoreController(
|
||||||
|
"api::chat-message.chat-message",
|
||||||
|
({ strapi }) => ({
|
||||||
|
async create(ctx) {
|
||||||
|
const userId = ctx.state.user?.id;
|
||||||
|
if (!userId) {
|
||||||
|
return ctx.unauthorized("You must be logged in to send a message");
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = ctx.request.body as any;
|
||||||
|
const data =
|
||||||
|
typeof body?.data === "string"
|
||||||
|
? JSON.parse(body.data)
|
||||||
|
: body?.data || {};
|
||||||
|
|
||||||
|
const { conversation, content, recipientIds } = data;
|
||||||
|
|
||||||
|
if (!content || content.trim() === "") {
|
||||||
|
return ctx.badRequest("Message content is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
let finalConversationId = conversation;
|
||||||
|
|
||||||
|
if (!finalConversationId) {
|
||||||
|
if (
|
||||||
|
!recipientIds ||
|
||||||
|
!Array.isArray(recipientIds) ||
|
||||||
|
recipientIds.length === 0
|
||||||
|
) {
|
||||||
|
return ctx.badRequest(
|
||||||
|
"Either conversationId or recipientIds must be provided"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newConversation = await strapi
|
||||||
|
.service("api::chat-conversation.chat-conversation")
|
||||||
|
.createConversationWithMembers(recipientIds, userId);
|
||||||
|
finalConversationId = newConversation.id;
|
||||||
|
} catch (error: any) {
|
||||||
|
return ctx.badRequest(
|
||||||
|
`Failed to create conversation: ${error.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let mediaId: number | undefined;
|
||||||
|
const mediaInput = (ctx.request.files as any)?.media;
|
||||||
|
if (mediaInput) {
|
||||||
|
const file = Array.isArray(mediaInput) ? mediaInput[0] : mediaInput;
|
||||||
|
const uploaded = await strapi
|
||||||
|
.plugin("upload")
|
||||||
|
.service("upload")
|
||||||
|
.upload({
|
||||||
|
data: {
|
||||||
|
fileInfo: {
|
||||||
|
alternativeText: data?.name || "media",
|
||||||
|
caption: "media",
|
||||||
|
name: file.originalFilename || "media",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
files: file,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (uploaded?.[0]?.id) {
|
||||||
|
mediaId = uploaded[0].id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const message = await strapi
|
||||||
|
.service("api::chat-message.chat-message")
|
||||||
|
.createMessageInConversation(finalConversationId, userId, content, mediaId);
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
data: message,
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
return ctx.badRequest(`Failed to create message: ${error.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|||||||
@@ -4,4 +4,92 @@
|
|||||||
|
|
||||||
import { factories } from '@strapi/strapi';
|
import { factories } from '@strapi/strapi';
|
||||||
|
|
||||||
export default factories.createCoreService('api::chat-message.chat-message');
|
export default factories.createCoreService(
|
||||||
|
'api::chat-message.chat-message',
|
||||||
|
({ strapi }) => ({
|
||||||
|
async createMessageInConversation(
|
||||||
|
conversationId: number,
|
||||||
|
senderId: number,
|
||||||
|
content: string,
|
||||||
|
mediaId?: number
|
||||||
|
) {
|
||||||
|
if (!conversationId) {
|
||||||
|
throw new Error('Conversation ID is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!senderId) {
|
||||||
|
throw new Error('Sender ID is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!content || content.trim() === '') {
|
||||||
|
throw new Error('Message content is required and cannot be empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
const conversation = await strapi.db
|
||||||
|
.query('api::chat-conversation.chat-conversation')
|
||||||
|
.findOne({
|
||||||
|
where: { id: conversationId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!conversation) {
|
||||||
|
throw new Error(`Conversation with ID ${conversationId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const member = await strapi.db
|
||||||
|
.query('api::chat-conversation-member.chat-conversation-member')
|
||||||
|
.findOne({
|
||||||
|
where: {
|
||||||
|
user: { id: senderId },
|
||||||
|
conversation: { id: conversationId },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!member) {
|
||||||
|
throw new Error(
|
||||||
|
'User is not a member of this conversation'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mediaId) {
|
||||||
|
const media = await strapi.db
|
||||||
|
.query('plugin::upload.file')
|
||||||
|
.findOne({
|
||||||
|
where: { id: mediaId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!media) {
|
||||||
|
throw new Error(`Media with ID ${mediaId} not found`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageData: any = {
|
||||||
|
sender: senderId,
|
||||||
|
content: content.trim(),
|
||||||
|
isEdited: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (mediaId) {
|
||||||
|
messageData.media = mediaId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = await strapi.db
|
||||||
|
.query('api::chat-message.chat-message')
|
||||||
|
.create({
|
||||||
|
data: messageData,
|
||||||
|
});
|
||||||
|
|
||||||
|
await strapi.db
|
||||||
|
.query('api::chat-conversation.chat-conversation')
|
||||||
|
.update({
|
||||||
|
where: { id: conversationId },
|
||||||
|
data: {
|
||||||
|
messages: {
|
||||||
|
connect: [{ id: message.id }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"name": "Apache 2.0",
|
"name": "Apache 2.0",
|
||||||
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
|
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
|
||||||
},
|
},
|
||||||
"x-generation-date": "2026-01-15T08:01:40.043Z"
|
"x-generation-date": "2026-01-16T10:01:51.447Z"
|
||||||
},
|
},
|
||||||
"x-strapi-config": {
|
"x-strapi-config": {
|
||||||
"plugins": [
|
"plugins": [
|
||||||
@@ -44201,6 +44201,135 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "date-time"
|
"format": "date-time"
|
||||||
},
|
},
|
||||||
|
"media": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"documentId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"alternativeText": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"caption": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"width": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"height": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"formats": {},
|
||||||
|
"hash": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"ext": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"mime": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"previewUrl": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"provider_metadata": {},
|
||||||
|
"related": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"documentId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"folder": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"documentId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"folderPath": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"publishedAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"createdBy": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"documentId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"updatedBy": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"documentId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"locale": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"localizations": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"documentId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"createdAt": {
|
"createdAt": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "date-time"
|
"format": "date-time"
|
||||||
@@ -48258,6 +48387,135 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "date-time"
|
"format": "date-time"
|
||||||
},
|
},
|
||||||
|
"media": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"documentId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"alternativeText": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"caption": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"width": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"height": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"formats": {},
|
||||||
|
"hash": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"ext": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"mime": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"previewUrl": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"provider_metadata": {},
|
||||||
|
"related": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"documentId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"folder": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"documentId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"folderPath": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"publishedAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"createdBy": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"documentId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"updatedBy": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"documentId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"locale": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"localizations": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"documentId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"createdAt": {
|
"createdAt": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "date-time"
|
"format": "date-time"
|
||||||
@@ -48565,6 +48823,17 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "date-time"
|
"format": "date-time"
|
||||||
},
|
},
|
||||||
|
"media": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"example": "string or id"
|
||||||
|
},
|
||||||
"locale": {
|
"locale": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -52286,6 +52555,135 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "date-time"
|
"format": "date-time"
|
||||||
},
|
},
|
||||||
|
"media": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"documentId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"alternativeText": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"caption": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"width": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"height": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"formats": {},
|
||||||
|
"hash": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"ext": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"mime": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"previewUrl": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"provider_metadata": {},
|
||||||
|
"related": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"documentId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"folder": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"documentId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"folderPath": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"publishedAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"createdBy": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"documentId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"updatedBy": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"documentId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"locale": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"localizations": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"documentId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"createdAt": {
|
"createdAt": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "date-time"
|
"format": "date-time"
|
||||||
@@ -52355,6 +52753,135 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "date-time"
|
"format": "date-time"
|
||||||
},
|
},
|
||||||
|
"media": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"documentId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"alternativeText": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"caption": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"width": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"height": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"formats": {},
|
||||||
|
"hash": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"ext": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"mime": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"previewUrl": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"provider_metadata": {},
|
||||||
|
"related": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"documentId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"folder": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"documentId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"folderPath": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"publishedAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"createdBy": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"documentId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"updatedBy": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"documentId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"locale": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"localizations": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"documentId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"createdAt": {
|
"createdAt": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "date-time"
|
"format": "date-time"
|
||||||
|
|||||||
2
types/generated/contentTypes.d.ts
vendored
2
types/generated/contentTypes.d.ts
vendored
@@ -673,6 +673,7 @@ export interface ApiChatConversationChatConversation
|
|||||||
export interface ApiChatMessageChatMessage extends Struct.CollectionTypeSchema {
|
export interface ApiChatMessageChatMessage extends Struct.CollectionTypeSchema {
|
||||||
collectionName: 'chat_messages';
|
collectionName: 'chat_messages';
|
||||||
info: {
|
info: {
|
||||||
|
description: '';
|
||||||
displayName: 'ChatMessage';
|
displayName: 'ChatMessage';
|
||||||
pluralName: 'chat-messages';
|
pluralName: 'chat-messages';
|
||||||
singularName: 'chat-message';
|
singularName: 'chat-message';
|
||||||
@@ -693,6 +694,7 @@ export interface ApiChatMessageChatMessage extends Struct.CollectionTypeSchema {
|
|||||||
'api::chat-message.chat-message'
|
'api::chat-message.chat-message'
|
||||||
> &
|
> &
|
||||||
Schema.Attribute.Private;
|
Schema.Attribute.Private;
|
||||||
|
media: Schema.Attribute.Media<'images' | 'files' | 'videos' | 'audios'>;
|
||||||
publishedAt: Schema.Attribute.DateTime;
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
sender: Schema.Attribute.Relation<
|
sender: Schema.Attribute.Relation<
|
||||||
'oneToOne',
|
'oneToOne',
|
||||||
|
|||||||
Reference in New Issue
Block a user