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.
- package/README.md +182 -67
- package/SETUP_CHECKLIST.md +773 -67
- package/dist/api/index.d.ts +17 -2
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +16 -1
- package/dist/api/index.js.map +1 -1
- package/dist/api/messages.d.ts +34 -1
- package/dist/api/messages.d.ts.map +1 -1
- package/dist/api/messages.js +340 -47
- package/dist/api/messages.js.map +1 -1
- package/dist/api/types.d.ts +50 -2
- package/dist/api/types.d.ts.map +1 -1
- package/dist/api/unread_count.d.ts +19 -10
- package/dist/api/unread_count.d.ts.map +1 -1
- package/dist/api/unread_count.js +54 -30
- package/dist/api/unread_count.js.map +1 -1
- package/dist/components/hazo_chat/hazo_chat.d.ts.map +1 -1
- package/dist/components/hazo_chat/hazo_chat.js +23 -15
- package/dist/components/hazo_chat/hazo_chat.js.map +1 -1
- package/dist/components/hazo_chat/hazo_chat_header.d.ts.map +1 -1
- package/dist/components/hazo_chat/hazo_chat_header.js +17 -4
- package/dist/components/hazo_chat/hazo_chat_header.js.map +1 -1
- package/dist/components/hazo_chat/hazo_chat_messages.d.ts +5 -4
- package/dist/components/hazo_chat/hazo_chat_messages.d.ts.map +1 -1
- package/dist/components/hazo_chat/hazo_chat_messages.js +48 -8
- package/dist/components/hazo_chat/hazo_chat_messages.js.map +1 -1
- package/dist/hooks/use_chat_messages.d.ts +5 -5
- package/dist/hooks/use_chat_messages.d.ts.map +1 -1
- package/dist/hooks/use_chat_messages.js +247 -148
- package/dist/hooks/use_chat_messages.js.map +1 -1
- package/dist/types/index.d.ts +162 -7
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/api/index.d.ts
CHANGED
|
@@ -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
|
package/dist/api/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA
|
|
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
|
package/dist/api/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA
|
|
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"}
|
package/dist/api/messages.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Messages API Handler Factory
|
|
3
3
|
*
|
|
4
|
-
* Creates GET and
|
|
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,
|
|
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"}
|
package/dist/api/messages.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Messages API Handler Factory
|
|
3
3
|
*
|
|
4
|
-
* Creates GET and
|
|
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
|
-
//
|
|
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
|
-
* -
|
|
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
|
|
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
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
159
|
+
chat_group_id,
|
|
78
160
|
reference_id,
|
|
79
161
|
reference_type,
|
|
162
|
+
cursor,
|
|
163
|
+
direction,
|
|
164
|
+
limit,
|
|
80
165
|
});
|
|
81
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
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
|
-
* -
|
|
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
|
|
256
|
+
return createErrorResponse('User not authenticated', 401, 'UNAUTHENTICATED');
|
|
139
257
|
}
|
|
140
258
|
// Parse request body
|
|
141
259
|
const body = await request.json();
|
|
142
|
-
const {
|
|
260
|
+
const { chat_group_id, message_text, reference_id, reference_type } = body;
|
|
143
261
|
// Validate required fields
|
|
144
|
-
if (!
|
|
145
|
-
console.error('[hazo_chat/messages POST] Missing
|
|
146
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
165
|
-
message_text:
|
|
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
|
-
|
|
321
|
+
chat_group_id,
|
|
176
322
|
reference_id: reference_id || '',
|
|
177
323
|
reference_type: reference_type || 'chat',
|
|
178
|
-
message_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
|
-
|
|
340
|
+
chat_group_id,
|
|
195
341
|
reference_id: reference_id || '',
|
|
196
342
|
reference_type: reference_type || 'chat',
|
|
197
|
-
message_text:
|
|
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
|
|
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
|
|
272
|
-
|
|
273
|
-
|
|
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: '
|
|
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
|