hazo_chat 2.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +182 -67
  2. package/SETUP_CHECKLIST.md +773 -67
  3. package/dist/api/index.d.ts +17 -2
  4. package/dist/api/index.d.ts.map +1 -1
  5. package/dist/api/index.js +16 -1
  6. package/dist/api/index.js.map +1 -1
  7. package/dist/api/messages.d.ts +34 -1
  8. package/dist/api/messages.d.ts.map +1 -1
  9. package/dist/api/messages.js +340 -47
  10. package/dist/api/messages.js.map +1 -1
  11. package/dist/api/types.d.ts +50 -2
  12. package/dist/api/types.d.ts.map +1 -1
  13. package/dist/api/unread_count.d.ts +19 -10
  14. package/dist/api/unread_count.d.ts.map +1 -1
  15. package/dist/api/unread_count.js +54 -30
  16. package/dist/api/unread_count.js.map +1 -1
  17. package/dist/components/hazo_chat/hazo_chat.d.ts.map +1 -1
  18. package/dist/components/hazo_chat/hazo_chat.js +23 -15
  19. package/dist/components/hazo_chat/hazo_chat.js.map +1 -1
  20. package/dist/components/hazo_chat/hazo_chat_header.d.ts.map +1 -1
  21. package/dist/components/hazo_chat/hazo_chat_header.js +17 -4
  22. package/dist/components/hazo_chat/hazo_chat_header.js.map +1 -1
  23. package/dist/components/hazo_chat/hazo_chat_messages.d.ts +5 -4
  24. package/dist/components/hazo_chat/hazo_chat_messages.d.ts.map +1 -1
  25. package/dist/components/hazo_chat/hazo_chat_messages.js +48 -8
  26. package/dist/components/hazo_chat/hazo_chat_messages.js.map +1 -1
  27. package/dist/hooks/use_chat_messages.d.ts +5 -5
  28. package/dist/hooks/use_chat_messages.d.ts.map +1 -1
  29. package/dist/hooks/use_chat_messages.js +247 -148
  30. package/dist/hooks/use_chat_messages.js.map +1 -1
  31. package/dist/types/index.d.ts +162 -7
  32. package/dist/types/index.d.ts.map +1 -1
  33. package/package.json +1 -1
@@ -18,9 +18,24 @@
18
18
  *
19
19
  * export { GET, POST };
20
20
  * ```
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * // app/api/hazo_chat/messages/[id]/route.ts
25
+ * import { createDeleteHandler } from 'hazo_chat/api';
26
+ * import { getHazoConnectSingleton } from 'hazo_connect/nextjs/setup';
27
+ *
28
+ * export const dynamic = 'force-dynamic';
29
+ *
30
+ * const { DELETE } = createDeleteHandler({
31
+ * getHazoConnect: () => getHazoConnectSingleton()
32
+ * });
33
+ *
34
+ * export { DELETE };
35
+ * ```
21
36
  */
22
- export { createMessagesHandler, createMarkAsReadHandler } from './messages.js';
37
+ export { createMessagesHandler, createMarkAsReadHandler, createDeleteHandler, } from './messages.js';
23
38
  export { createUnreadCountFunction } from './unread_count.js';
24
- export type { MessagesHandlerOptions, ChatMessageInput, ChatMessageRecord } from './types.js';
39
+ export type { MessagesHandlerOptions, ChatMessageInput, ChatMessageRecord, ApiErrorResponse, ApiSuccessResponse, PaginationMeta, } from './types.js';
25
40
  export type { UnreadCountFunctionOptions, UnreadCountResult } from './unread_count.js';
26
41
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAC/E,OAAO,EAAE,yBAAyB,EAAE,MAAM,mBAAmB,CAAC;AAG9D,YAAY,EACV,sBAAsB,EACtB,gBAAgB,EAChB,iBAAiB,EAClB,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,0BAA0B,EAC1B,iBAAiB,EAClB,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAGH,OAAO,EACL,qBAAqB,EACrB,uBAAuB,EACvB,mBAAmB,GACpB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,yBAAyB,EAAE,MAAM,mBAAmB,CAAC;AAG9D,YAAY,EACV,sBAAsB,EACtB,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,GACf,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,0BAA0B,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC"}
package/dist/api/index.js CHANGED
@@ -18,8 +18,23 @@
18
18
  *
19
19
  * export { GET, POST };
20
20
  * ```
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * // app/api/hazo_chat/messages/[id]/route.ts
25
+ * import { createDeleteHandler } from 'hazo_chat/api';
26
+ * import { getHazoConnectSingleton } from 'hazo_connect/nextjs/setup';
27
+ *
28
+ * export const dynamic = 'force-dynamic';
29
+ *
30
+ * const { DELETE } = createDeleteHandler({
31
+ * getHazoConnect: () => getHazoConnectSingleton()
32
+ * });
33
+ *
34
+ * export { DELETE };
35
+ * ```
21
36
  */
22
37
  // Export handler factories
23
- export { createMessagesHandler, createMarkAsReadHandler } from './messages.js';
38
+ export { createMessagesHandler, createMarkAsReadHandler, createDeleteHandler, } from './messages.js';
24
39
  export { createUnreadCountFunction } from './unread_count.js';
25
40
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,2BAA2B;AAC3B,OAAO,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAC/E,OAAO,EAAE,yBAAyB,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,2BAA2B;AAC3B,OAAO,EACL,qBAAqB,EACrB,uBAAuB,EACvB,mBAAmB,GACpB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,yBAAyB,EAAE,MAAM,mBAAmB,CAAC"}
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Messages API Handler Factory
3
3
  *
4
- * Creates GET and POST handlers for the /api/hazo_chat/messages endpoint.
4
+ * Creates GET, POST, and DELETE handlers for the /api/hazo_chat/messages endpoint.
5
5
  * These handlers should be used in a Next.js API route.
6
6
  *
7
7
  * @example
@@ -49,4 +49,37 @@ export declare function createMarkAsReadHandler(options: MessagesHandlerOptions)
49
49
  }>;
50
50
  }) => Promise<NextResponse>;
51
51
  };
52
+ /**
53
+ * Creates a DELETE handler for soft-deleting a message
54
+ *
55
+ * This handler should be used in a Next.js API route like:
56
+ * /api/hazo_chat/messages/[id]/route.ts
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * // app/api/hazo_chat/messages/[id]/route.ts
61
+ * import { createDeleteHandler } from 'hazo_chat/api';
62
+ * import { getHazoConnectSingleton } from 'hazo_connect/nextjs/setup';
63
+ *
64
+ * export const dynamic = 'force-dynamic';
65
+ *
66
+ * const { DELETE } = createDeleteHandler({
67
+ * getHazoConnect: () => getHazoConnectSingleton()
68
+ * });
69
+ *
70
+ * export { DELETE };
71
+ * ```
72
+ *
73
+ * @param options - Configuration options
74
+ * @returns DELETE handler function
75
+ */
76
+ export declare function createDeleteHandler(options: MessagesHandlerOptions): {
77
+ DELETE: (request: NextRequest, context: {
78
+ params: {
79
+ id: string;
80
+ } | Promise<{
81
+ id: string;
82
+ }>;
83
+ }) => Promise<NextResponse>;
84
+ };
52
85
  //# sourceMappingURL=messages.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/api/messages.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAIxD,OAAO,KAAK,EAAE,sBAAsB,EAAuC,MAAM,YAAY,CAAC;AAmB9F;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,sBAAsB;mBAWvC,WAAW,KAAG,OAAO,CAAC,YAAY,CAAC;oBA+FlC,WAAW,KAAG,OAAO,CAAC,YAAY,CAAC;EA0GjE;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,sBAAsB;qBAY1D,WAAW,WACX;QAAE,MAAM,EAAE;YAAE,EAAE,EAAE,MAAM,CAAA;SAAE,GAAG,OAAO,CAAC;YAAE,EAAE,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,KAC5D,OAAO,CAAC,YAAY,CAAC;EA8HzB"}
1
+ {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/api/messages.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAIxD,OAAO,KAAK,EAAE,sBAAsB,EAAkG,MAAM,YAAY,CAAC;AA0FzJ;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,sBAAsB;mBAcvC,WAAW,KAAG,OAAO,CAAC,YAAY,CAAC;oBA2KlC,WAAW,KAAG,OAAO,CAAC,YAAY,CAAC;EAiJjE;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,sBAAsB;qBAY1D,WAAW,WACX;QAAE,MAAM,EAAE;YAAE,EAAE,EAAE,MAAM,CAAA;SAAE,GAAG,OAAO,CAAC;YAAE,EAAE,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,KAC5D,OAAO,CAAC,YAAY,CAAC;EA2IzB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,sBAAsB;sBAatD,WAAW,WACX;QAAE,MAAM,EAAE;YAAE,EAAE,EAAE,MAAM,CAAA;SAAE,GAAG,OAAO,CAAC;YAAE,EAAE,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,KAC5D,OAAO,CAAC,YAAY,CAAC;EAuIzB"}
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Messages API Handler Factory
3
3
  *
4
- * Creates GET and POST handlers for the /api/hazo_chat/messages endpoint.
4
+ * Creates GET, POST, and DELETE handlers for the /api/hazo_chat/messages endpoint.
5
5
  * These handlers should be used in a Next.js API route.
6
6
  *
7
7
  * @example
@@ -22,14 +22,45 @@
22
22
  import { NextResponse } from 'next/server';
23
23
  import { cookies } from 'next/headers';
24
24
  import { createCrudService, getSqliteAdminService } from 'hazo_connect/server';
25
- // UUID generation for message IDs
25
+ // ============================================================================
26
+ // Constants for validation
27
+ // ============================================================================
28
+ /** Maximum message length in characters */
29
+ const MAX_MESSAGE_LENGTH = 5000;
30
+ /** Maximum length for reference_id */
31
+ const MAX_REFERENCE_ID_LENGTH = 255;
32
+ /** Maximum length for reference_type */
33
+ const MAX_REFERENCE_TYPE_LENGTH = 100;
34
+ /** Default messages per page */
35
+ const DEFAULT_LIMIT = 50;
36
+ /** Maximum messages per page */
37
+ const MAX_LIMIT = 100;
38
+ // ============================================================================
39
+ // Helper Functions
40
+ // ============================================================================
41
+ /** Generate a UUID v4 */
26
42
  function generateUUID() {
27
43
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
28
- const r = Math.random() * 16 | 0;
29
- const v = c === 'x' ? r : (r & 0x3 | 0x8);
44
+ const r = (Math.random() * 16) | 0;
45
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
30
46
  return v.toString(16);
31
47
  });
32
48
  }
49
+ /** Create a standardized error response */
50
+ function createErrorResponse(error, status, error_code) {
51
+ return NextResponse.json({
52
+ success: false,
53
+ error,
54
+ error_code,
55
+ }, { status });
56
+ }
57
+ /** Create a standardized success response */
58
+ function createSuccessResponse(data) {
59
+ return NextResponse.json({
60
+ success: true,
61
+ data,
62
+ });
63
+ }
33
64
  /**
34
65
  * Default function to get user ID from request cookies
35
66
  */
@@ -37,6 +68,23 @@ async function defaultGetUserIdFromRequest() {
37
68
  const cookieStore = await cookies();
38
69
  return cookieStore.get('hazo_auth_user_id')?.value || null;
39
70
  }
71
+ /**
72
+ * Verify that a user is a member of a chat group
73
+ * @returns The membership record if found, null otherwise
74
+ */
75
+ async function verifyGroupMembership(hazoConnect, user_id, chat_group_id) {
76
+ const membershipService = createCrudService(hazoConnect, 'hazo_chat_group_users');
77
+ try {
78
+ const memberships = await membershipService.list((qb) => qb.select('*')
79
+ .where('chat_group_id', 'eq', chat_group_id)
80
+ .where('user_id', 'eq', user_id));
81
+ return memberships[0] || null;
82
+ }
83
+ catch (error) {
84
+ console.error('[hazo_chat] Error verifying group membership:', error);
85
+ return null;
86
+ }
87
+ }
40
88
  /**
41
89
  * Creates GET and POST handlers for chat messages
42
90
  *
@@ -46,12 +94,15 @@ async function defaultGetUserIdFromRequest() {
46
94
  export function createMessagesHandler(options) {
47
95
  const { getHazoConnect, getUserIdFromRequest } = options;
48
96
  /**
49
- * GET handler - Fetch chat messages
97
+ * GET handler - Fetch chat messages with pagination
50
98
  *
51
99
  * Query params:
52
- * - receiver_user_id (required): The other user in the conversation
100
+ * - chat_group_id (required): The chat group to fetch messages from
53
101
  * - reference_id (optional): Filter by reference ID
54
102
  * - reference_type (optional): Filter by reference type
103
+ * - limit (optional): Number of messages per page (default: 50, max: 100)
104
+ * - cursor (optional): Cursor for pagination (created_at timestamp of last message)
105
+ * - direction (optional): 'older' or 'newer' relative to cursor (default: 'older')
55
106
  */
56
107
  async function GET(request) {
57
108
  try {
@@ -61,69 +112,136 @@ export function createMessagesHandler(options) {
61
112
  : await defaultGetUserIdFromRequest();
62
113
  if (!current_user_id) {
63
114
  console.error('[hazo_chat/messages GET] No user ID - not authenticated');
64
- return NextResponse.json({ success: false, error: 'User not authenticated', messages: [] }, { status: 401 });
115
+ return createErrorResponse('User not authenticated', 401, 'UNAUTHENTICATED');
65
116
  }
66
117
  // Get query params
67
118
  const { searchParams } = new URL(request.url);
68
- const receiver_user_id = searchParams.get('receiver_user_id');
119
+ const chat_group_id = searchParams.get('chat_group_id');
69
120
  const reference_id = searchParams.get('reference_id') || '';
70
121
  const reference_type = searchParams.get('reference_type') || '';
71
- if (!receiver_user_id) {
72
- console.error('[hazo_chat/messages GET] Missing receiver_user_id');
73
- return NextResponse.json({ success: false, error: 'receiver_user_id is required', messages: [] }, { status: 400 });
122
+ const cursor = searchParams.get('cursor') || '';
123
+ const direction = searchParams.get('direction') || 'older';
124
+ const limit_param = searchParams.get('limit');
125
+ // Validate required params
126
+ if (!chat_group_id) {
127
+ console.error('[hazo_chat/messages GET] Missing chat_group_id');
128
+ return createErrorResponse('chat_group_id is required', 400, 'MISSING_CHAT_GROUP');
129
+ }
130
+ // Get hazo_connect instance early for membership check
131
+ const hazoConnect = getHazoConnect();
132
+ // Verify user is a member of the chat group
133
+ const membership = await verifyGroupMembership(hazoConnect, current_user_id, chat_group_id);
134
+ if (!membership) {
135
+ console.error('[hazo_chat/messages GET] User is not a member of chat group:', {
136
+ current_user_id,
137
+ chat_group_id,
138
+ });
139
+ return createErrorResponse('Access denied - not a member of this chat group', 403, 'FORBIDDEN');
140
+ }
141
+ // Validate input lengths
142
+ if (reference_id && reference_id.length > MAX_REFERENCE_ID_LENGTH) {
143
+ return createErrorResponse(`reference_id exceeds maximum length of ${MAX_REFERENCE_ID_LENGTH}`, 400, 'INVALID_REFERENCE_ID');
144
+ }
145
+ if (reference_type && reference_type.length > MAX_REFERENCE_TYPE_LENGTH) {
146
+ return createErrorResponse(`reference_type exceeds maximum length of ${MAX_REFERENCE_TYPE_LENGTH}`, 400, 'INVALID_REFERENCE_TYPE');
147
+ }
148
+ // Parse and validate limit
149
+ let limit = DEFAULT_LIMIT;
150
+ if (limit_param) {
151
+ const parsed_limit = parseInt(limit_param, 10);
152
+ if (isNaN(parsed_limit) || parsed_limit < 1) {
153
+ return createErrorResponse('limit must be a positive integer', 400, 'INVALID_LIMIT');
154
+ }
155
+ limit = Math.min(parsed_limit, MAX_LIMIT);
74
156
  }
75
157
  console.log('[hazo_chat/messages GET] Fetching messages:', {
76
158
  current_user_id,
77
- receiver_user_id,
159
+ chat_group_id,
78
160
  reference_id,
79
161
  reference_type,
162
+ cursor,
163
+ direction,
164
+ limit,
80
165
  });
81
- // Get hazo_connect instance and create CRUD service
82
- const hazoConnect = getHazoConnect();
166
+ // Create CRUD service (hazoConnect already obtained above)
83
167
  const chatService = createCrudService(hazoConnect, 'hazo_chat');
84
168
  let messages = [];
85
169
  try {
86
- // Fetch all messages with reference filters
170
+ // Build query with proper filtering
87
171
  const all_messages = await chatService.list((qb) => {
88
172
  let builder = qb.select('*');
173
+ // Filter by chat group - this is the primary filter
174
+ builder = builder.where('chat_group_id', 'eq', chat_group_id);
175
+ // Filter by reference if provided
89
176
  if (reference_id) {
90
177
  builder = builder.where('reference_id', 'eq', reference_id);
91
178
  }
92
179
  if (reference_type) {
93
180
  builder = builder.where('reference_type', 'eq', reference_type);
94
181
  }
95
- return builder.order('created_at', 'asc');
96
- });
97
- // Filter to only messages between current user and receiver
98
- messages = all_messages.filter((msg) => {
99
- const is_sent_by_me = msg.sender_user_id === current_user_id && msg.receiver_user_id === receiver_user_id;
100
- const is_sent_to_me = msg.sender_user_id === receiver_user_id && msg.receiver_user_id === current_user_id;
101
- return is_sent_by_me || is_sent_to_me;
182
+ // Apply cursor-based pagination
183
+ if (cursor) {
184
+ if (direction === 'newer') {
185
+ builder = builder.where('created_at', 'gt', cursor);
186
+ }
187
+ else {
188
+ builder = builder.where('created_at', 'lt', cursor);
189
+ }
190
+ }
191
+ // Order by created_at
192
+ // For 'older' direction, we want desc to get the most recent first before cursor
193
+ // For 'newer' or initial load, we want asc
194
+ if (direction === 'older' && cursor) {
195
+ builder = builder.order('created_at', 'desc');
196
+ }
197
+ else {
198
+ builder = builder.order('created_at', 'asc');
199
+ }
200
+ return builder;
102
201
  });
202
+ // Filter out deleted messages (membership check already done above)
203
+ const filtered_messages = all_messages.filter((msg) => !msg.deleted_at);
204
+ // Apply limit after filtering
205
+ messages = filtered_messages.slice(0, limit);
206
+ // If we fetched in desc order, reverse to return in asc order
207
+ if (direction === 'older' && cursor) {
208
+ messages.reverse();
209
+ }
103
210
  }
104
211
  catch (dbError) {
105
212
  console.error('[hazo_chat/messages GET] Database error:', dbError);
106
213
  throw dbError;
107
214
  }
108
215
  console.log('[hazo_chat/messages GET] Found messages:', messages.length);
216
+ // Determine if there are more messages
217
+ const has_more = messages.length === limit;
218
+ // Get cursors for next/prev page
219
+ const next_cursor = messages.length > 0 ? messages[messages.length - 1].created_at : null;
220
+ const prev_cursor = messages.length > 0 ? messages[0].created_at : null;
109
221
  return NextResponse.json({
110
222
  success: true,
111
223
  messages,
112
224
  current_user_id,
225
+ pagination: {
226
+ limit,
227
+ has_more,
228
+ next_cursor,
229
+ prev_cursor,
230
+ },
113
231
  });
114
232
  }
115
233
  catch (error) {
116
234
  const error_message = error instanceof Error ? error.message : 'Unknown error';
117
235
  console.error('[hazo_chat/messages GET] Error:', error_message, error);
118
- return NextResponse.json({ success: false, error: 'Failed to fetch messages', messages: [] }, { status: 500 });
236
+ return createErrorResponse('Failed to fetch messages', 500, 'INTERNAL_ERROR');
119
237
  }
120
238
  }
121
239
  /**
122
240
  * POST handler - Create a new chat message
123
241
  *
124
242
  * Request body:
125
- * - receiver_user_id (required): The recipient user ID
126
- * - message_text (required): The message content
243
+ * - chat_group_id (required): The chat group to send the message to
244
+ * - message_text (required): The message content (max 5000 chars)
127
245
  * - reference_id (optional): Reference ID for context grouping
128
246
  * - reference_type (optional): Reference type (default: 'chat')
129
247
  */
@@ -135,22 +253,50 @@ export function createMessagesHandler(options) {
135
253
  : await defaultGetUserIdFromRequest();
136
254
  if (!sender_user_id) {
137
255
  console.error('[hazo_chat/messages POST] No user ID - not authenticated');
138
- return NextResponse.json({ success: false, error: 'User not authenticated' }, { status: 401 });
256
+ return createErrorResponse('User not authenticated', 401, 'UNAUTHENTICATED');
139
257
  }
140
258
  // Parse request body
141
259
  const body = await request.json();
142
- const { receiver_user_id, message_text, reference_id, reference_type } = body;
260
+ const { chat_group_id, message_text, reference_id, reference_type } = body;
143
261
  // Validate required fields
144
- if (!receiver_user_id) {
145
- console.error('[hazo_chat/messages POST] Missing receiver_user_id');
146
- return NextResponse.json({ success: false, error: 'receiver_user_id is required' }, { status: 400 });
262
+ if (!chat_group_id) {
263
+ console.error('[hazo_chat/messages POST] Missing chat_group_id');
264
+ return createErrorResponse('chat_group_id is required', 400, 'MISSING_CHAT_GROUP');
147
265
  }
148
- if (!message_text || message_text.trim() === '') {
149
- console.error('[hazo_chat/messages POST] Missing or empty message_text');
150
- return NextResponse.json({ success: false, error: 'message_text is required' }, { status: 400 });
151
- }
152
- // Get hazo_connect instance and create CRUD service
266
+ // Get hazo_connect instance early for membership check
153
267
  const hazoConnect = getHazoConnect();
268
+ // Verify user is a member of the chat group
269
+ const membership = await verifyGroupMembership(hazoConnect, sender_user_id, chat_group_id);
270
+ if (!membership) {
271
+ console.error('[hazo_chat/messages POST] User is not a member of chat group:', {
272
+ sender_user_id,
273
+ chat_group_id,
274
+ });
275
+ return createErrorResponse('Access denied - not a member of this chat group', 403, 'FORBIDDEN');
276
+ }
277
+ // Validate message_text
278
+ if (!message_text || typeof message_text !== 'string') {
279
+ console.error('[hazo_chat/messages POST] Missing message_text');
280
+ return createErrorResponse('message_text is required', 400, 'MISSING_MESSAGE');
281
+ }
282
+ const trimmed_message = message_text.trim();
283
+ if (trimmed_message === '') {
284
+ console.error('[hazo_chat/messages POST] Empty message_text');
285
+ return createErrorResponse('message_text cannot be empty or whitespace-only', 400, 'EMPTY_MESSAGE');
286
+ }
287
+ if (trimmed_message.length > MAX_MESSAGE_LENGTH) {
288
+ console.error('[hazo_chat/messages POST] Message too long:', trimmed_message.length);
289
+ return createErrorResponse(`Message exceeds maximum length of ${MAX_MESSAGE_LENGTH} characters`, 400, 'MESSAGE_TOO_LONG');
290
+ }
291
+ // Validate reference_id length
292
+ if (reference_id && reference_id.length > MAX_REFERENCE_ID_LENGTH) {
293
+ return createErrorResponse(`reference_id exceeds maximum length of ${MAX_REFERENCE_ID_LENGTH}`, 400, 'INVALID_REFERENCE_ID');
294
+ }
295
+ // Validate reference_type length
296
+ if (reference_type && reference_type.length > MAX_REFERENCE_TYPE_LENGTH) {
297
+ return createErrorResponse(`reference_type exceeds maximum length of ${MAX_REFERENCE_TYPE_LENGTH}`, 400, 'INVALID_REFERENCE_TYPE');
298
+ }
299
+ // Create CRUD service (hazoConnect already obtained above)
154
300
  const chatService = createCrudService(hazoConnect, 'hazo_chat');
155
301
  // Generate message ID and timestamps
156
302
  const message_id = generateUUID();
@@ -161,8 +307,8 @@ export function createMessagesHandler(options) {
161
307
  reference_id: reference_id || '',
162
308
  reference_type: reference_type || 'chat',
163
309
  sender_user_id,
164
- receiver_user_id,
165
- message_text: message_text.trim(),
310
+ chat_group_id,
311
+ message_text: trimmed_message,
166
312
  reference_list: null,
167
313
  read_at: null,
168
314
  deleted_at: null,
@@ -172,10 +318,10 @@ export function createMessagesHandler(options) {
172
318
  console.log('[hazo_chat/messages POST] Saving message:', {
173
319
  id: message_id,
174
320
  sender_user_id,
175
- receiver_user_id,
321
+ chat_group_id,
176
322
  reference_id: reference_id || '',
177
323
  reference_type: reference_type || 'chat',
178
- message_length: message_text.length,
324
+ message_length: trimmed_message.length,
179
325
  });
180
326
  // Save to database
181
327
  try {
@@ -191,10 +337,10 @@ export function createMessagesHandler(options) {
191
337
  message: {
192
338
  id: message_id,
193
339
  sender_user_id,
194
- receiver_user_id,
340
+ chat_group_id,
195
341
  reference_id: reference_id || '',
196
342
  reference_type: reference_type || 'chat',
197
- message_text: message_text.trim(),
343
+ message_text: trimmed_message,
198
344
  reference_list: null,
199
345
  read_at: null,
200
346
  deleted_at: null,
@@ -206,7 +352,7 @@ export function createMessagesHandler(options) {
206
352
  catch (error) {
207
353
  const error_message = error instanceof Error ? error.message : 'Unknown error';
208
354
  console.error('[hazo_chat/messages POST] Error:', error_message, error);
209
- return NextResponse.json({ success: false, error: 'Failed to save message' }, { status: 500 });
355
+ return createErrorResponse('Failed to save message', 500, 'INTERNAL_ERROR');
210
356
  }
211
357
  }
212
358
  return { GET, POST };
@@ -268,14 +414,23 @@ export function createMarkAsReadHandler(options) {
268
414
  console.error('[hazo_chat/messages/[id]/read PATCH] Message not found:', message_id);
269
415
  return NextResponse.json({ success: false, error: 'Message not found' }, { status: 404 });
270
416
  }
271
- // Verify that the current user is the receiver (only receivers can mark as read)
272
- if (message.receiver_user_id !== current_user_id) {
273
- console.error('[hazo_chat/messages/[id]/read PATCH] User is not the receiver:', {
417
+ // Verify that the current user is a member of the chat group
418
+ const membership = await verifyGroupMembership(hazoConnect, current_user_id, message.chat_group_id);
419
+ if (!membership) {
420
+ console.error('[hazo_chat/messages/[id]/read PATCH] User is not a member of chat group:', {
421
+ message_id,
422
+ current_user_id,
423
+ chat_group_id: message.chat_group_id,
424
+ });
425
+ return NextResponse.json({ success: false, error: 'Unauthorized - not a member of this chat group' }, { status: 403 });
426
+ }
427
+ // Cannot mark your own messages as read
428
+ if (message.sender_user_id === current_user_id) {
429
+ console.error('[hazo_chat/messages/[id]/read PATCH] User cannot mark own message as read:', {
274
430
  message_id,
275
431
  current_user_id,
276
- receiver_user_id: message.receiver_user_id,
277
432
  });
278
- return NextResponse.json({ success: false, error: 'Unauthorized - only the receiver can mark messages as read' }, { status: 403 });
433
+ return NextResponse.json({ success: false, error: 'Cannot mark your own messages as read' }, { status: 400 });
279
434
  }
280
435
  // Don't update if already read
281
436
  if (message.read_at) {
@@ -327,4 +482,142 @@ export function createMarkAsReadHandler(options) {
327
482
  }
328
483
  return { PATCH };
329
484
  }
485
+ /**
486
+ * Creates a DELETE handler for soft-deleting a message
487
+ *
488
+ * This handler should be used in a Next.js API route like:
489
+ * /api/hazo_chat/messages/[id]/route.ts
490
+ *
491
+ * @example
492
+ * ```typescript
493
+ * // app/api/hazo_chat/messages/[id]/route.ts
494
+ * import { createDeleteHandler } from 'hazo_chat/api';
495
+ * import { getHazoConnectSingleton } from 'hazo_connect/nextjs/setup';
496
+ *
497
+ * export const dynamic = 'force-dynamic';
498
+ *
499
+ * const { DELETE } = createDeleteHandler({
500
+ * getHazoConnect: () => getHazoConnectSingleton()
501
+ * });
502
+ *
503
+ * export { DELETE };
504
+ * ```
505
+ *
506
+ * @param options - Configuration options
507
+ * @returns DELETE handler function
508
+ */
509
+ export function createDeleteHandler(options) {
510
+ const { getHazoConnect, getUserIdFromRequest } = options;
511
+ /**
512
+ * DELETE handler - Soft delete a message
513
+ *
514
+ * Route params:
515
+ * - id (required): The message ID to delete
516
+ *
517
+ * Note: This performs a soft delete by setting deleted_at timestamp.
518
+ * Only the sender can delete their own messages.
519
+ */
520
+ async function DELETE(request, context) {
521
+ try {
522
+ // Get current user ID
523
+ const current_user_id = getUserIdFromRequest
524
+ ? await getUserIdFromRequest(request)
525
+ : await defaultGetUserIdFromRequest();
526
+ if (!current_user_id) {
527
+ console.error('[hazo_chat/messages/[id] DELETE] No user ID - not authenticated');
528
+ return createErrorResponse('User not authenticated', 401, 'UNAUTHENTICATED');
529
+ }
530
+ // Handle params as Promise (Next.js 15+) or direct object (Next.js 13-14)
531
+ const params = context.params instanceof Promise ? await context.params : context.params;
532
+ const message_id = params.id;
533
+ if (!message_id) {
534
+ console.error('[hazo_chat/messages/[id] DELETE] Missing message ID');
535
+ return createErrorResponse('Message ID is required', 400, 'MISSING_MESSAGE_ID');
536
+ }
537
+ console.log('[hazo_chat/messages/[id] DELETE] Deleting message:', {
538
+ message_id,
539
+ current_user_id,
540
+ });
541
+ // Get hazo_connect instance and create CRUD service
542
+ const hazoConnect = getHazoConnect();
543
+ const chatService = createCrudService(hazoConnect, 'hazo_chat');
544
+ // Fetch the message to verify ownership
545
+ let message = null;
546
+ try {
547
+ const messages = await chatService.list((qb) => qb.select('*').where('id', 'eq', message_id));
548
+ message = messages[0] || null;
549
+ }
550
+ catch (dbError) {
551
+ console.error('[hazo_chat/messages/[id] DELETE] Database error fetching message:', dbError);
552
+ throw dbError;
553
+ }
554
+ if (!message) {
555
+ console.error('[hazo_chat/messages/[id] DELETE] Message not found:', message_id);
556
+ return createErrorResponse('Message not found', 404, 'MESSAGE_NOT_FOUND');
557
+ }
558
+ // Verify that the current user is a member of the chat group
559
+ const membership = await verifyGroupMembership(hazoConnect, current_user_id, message.chat_group_id);
560
+ if (!membership) {
561
+ console.error('[hazo_chat/messages/[id] DELETE] User is not a member of chat group:', {
562
+ message_id,
563
+ current_user_id,
564
+ chat_group_id: message.chat_group_id,
565
+ });
566
+ return createErrorResponse('Unauthorized - not a member of this chat group', 403, 'FORBIDDEN');
567
+ }
568
+ // Verify that the current user is the sender (only senders can delete their messages)
569
+ if (message.sender_user_id !== current_user_id) {
570
+ console.error('[hazo_chat/messages/[id] DELETE] User is not the sender:', {
571
+ message_id,
572
+ current_user_id,
573
+ sender_user_id: message.sender_user_id,
574
+ });
575
+ return createErrorResponse('Unauthorized - only the sender can delete their messages', 403, 'UNAUTHORIZED');
576
+ }
577
+ // Check if already deleted
578
+ if (message.deleted_at) {
579
+ console.log('[hazo_chat/messages/[id] DELETE] Message already deleted:', message_id);
580
+ return NextResponse.json({
581
+ success: true,
582
+ message: {
583
+ ...message,
584
+ message_text: null,
585
+ },
586
+ });
587
+ }
588
+ // Soft delete: set deleted_at timestamp and clear message_text
589
+ const now = new Date().toISOString();
590
+ try {
591
+ const sqliteService = getSqliteAdminService();
592
+ const updated_rows = await sqliteService.updateRows('hazo_chat', { id: message_id }, { deleted_at: now, message_text: null, changed_at: now });
593
+ if (updated_rows.length === 0) {
594
+ console.warn('[hazo_chat/messages/[id] DELETE] No rows updated - message may not exist:', message_id);
595
+ }
596
+ else {
597
+ console.log('[hazo_chat/messages/[id] DELETE] Successfully deleted', updated_rows.length, 'row(s)');
598
+ }
599
+ }
600
+ catch (dbError) {
601
+ console.error('[hazo_chat/messages/[id] DELETE] Database error deleting message:', dbError);
602
+ throw dbError;
603
+ }
604
+ console.log('[hazo_chat/messages/[id] DELETE] Message deleted successfully:', message_id);
605
+ return NextResponse.json({
606
+ success: true,
607
+ message: {
608
+ ...message,
609
+ deleted_at: now,
610
+ message_text: null,
611
+ changed_at: now,
612
+ },
613
+ });
614
+ }
615
+ catch (error) {
616
+ const error_message = error instanceof Error ? error.message : 'Unknown error';
617
+ console.error('[hazo_chat/messages/[id] DELETE] Error:', error_message, error);
618
+ return createErrorResponse('Failed to delete message', 500, 'INTERNAL_ERROR');
619
+ }
620
+ }
621
+ return { DELETE };
622
+ }
330
623
  //# sourceMappingURL=messages.js.map