@molecule/api-resource-message 1.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 (56) hide show
  1. package/LICENSE +115 -0
  2. package/dist/authorizers/index.d.ts +12 -0
  3. package/dist/authorizers/index.d.ts.map +1 -0
  4. package/dist/authorizers/index.js +11 -0
  5. package/dist/browser-guard.d.ts +2 -0
  6. package/dist/browser-guard.d.ts.map +1 -0
  7. package/dist/browser-guard.js +18 -0
  8. package/dist/handlers/createThread.d.ts +15 -0
  9. package/dist/handlers/createThread.d.ts.map +1 -0
  10. package/dist/handlers/createThread.js +54 -0
  11. package/dist/handlers/deleteMessage.d.ts +14 -0
  12. package/dist/handlers/deleteMessage.d.ts.map +1 -0
  13. package/dist/handlers/deleteMessage.js +56 -0
  14. package/dist/handlers/editMessage.d.ts +14 -0
  15. package/dist/handlers/editMessage.d.ts.map +1 -0
  16. package/dist/handlers/editMessage.js +63 -0
  17. package/dist/handlers/index.d.ts +9 -0
  18. package/dist/handlers/index.d.ts.map +1 -0
  19. package/dist/handlers/index.js +8 -0
  20. package/dist/handlers/listThreads.d.ts +14 -0
  21. package/dist/handlers/listThreads.d.ts.map +1 -0
  22. package/dist/handlers/listThreads.js +39 -0
  23. package/dist/handlers/markRead.d.ts +16 -0
  24. package/dist/handlers/markRead.d.ts.map +1 -0
  25. package/dist/handlers/markRead.js +68 -0
  26. package/dist/handlers/messages.d.ts +23 -0
  27. package/dist/handlers/messages.d.ts.map +1 -0
  28. package/dist/handlers/messages.js +135 -0
  29. package/dist/handlers/readThread.d.ts +15 -0
  30. package/dist/handlers/readThread.d.ts.map +1 -0
  31. package/dist/handlers/readThread.js +66 -0
  32. package/dist/handlers/unreadCount.d.ts +14 -0
  33. package/dist/handlers/unreadCount.d.ts.map +1 -0
  34. package/dist/handlers/unreadCount.js +37 -0
  35. package/dist/i18n.d.ts +10 -0
  36. package/dist/i18n.d.ts.map +1 -0
  37. package/dist/i18n.js +17 -0
  38. package/dist/index.d.ts +82 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +81 -0
  41. package/dist/requestHandlerMap.d.ts +28 -0
  42. package/dist/requestHandlerMap.d.ts.map +1 -0
  43. package/dist/requestHandlerMap.js +27 -0
  44. package/dist/routes.d.ts +58 -0
  45. package/dist/routes.d.ts.map +1 -0
  46. package/dist/routes.js +67 -0
  47. package/dist/service.d.ts +117 -0
  48. package/dist/service.d.ts.map +1 -0
  49. package/dist/service.js +301 -0
  50. package/dist/types.d.ts +102 -0
  51. package/dist/types.d.ts.map +1 -0
  52. package/dist/types.js +32 -0
  53. package/dist/validation.d.ts +36 -0
  54. package/dist/validation.d.ts.map +1 -0
  55. package/dist/validation.js +38 -0
  56. package/package.json +74 -0
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Message resource business logic.
3
+ *
4
+ * Uses the abstract DataStore from `@molecule/api-database` for all
5
+ * persistence — never raw SQL. Realtime broadcast is delegated to
6
+ * `@molecule/api-realtime` and is no-op when no realtime provider is
7
+ * bonded.
8
+ *
9
+ * @module
10
+ */
11
+ import { type ListMessagesOptions, type Message, type MessageAttachment, type Thread } from './types.js';
12
+ /**
13
+ * Canonicalises a (a, b) participant pair so that the lexicographically
14
+ * smaller value is always returned first. This guarantees `(x, y)` and
15
+ * `(y, x)` resolve to the same {@link Thread} row.
16
+ *
17
+ * @param a - First participant ID.
18
+ * @param b - Second participant ID.
19
+ * @returns A tuple `[participantAId, participantBId]` in canonical order.
20
+ * @throws {Error} If `a` and `b` refer to the same participant.
21
+ */
22
+ export declare function canonicaliseParticipants(a: string, b: string): [string, string];
23
+ /**
24
+ * Looks up the canonical thread between two participants, creating it
25
+ * lazily if none exists.
26
+ *
27
+ * @param a - First participant user ID.
28
+ * @param b - Second participant user ID.
29
+ * @returns The existing or newly-created thread.
30
+ */
31
+ export declare function getOrCreateThread(a: string, b: string): Promise<Thread>;
32
+ /**
33
+ * Retrieves a thread by ID.
34
+ *
35
+ * @param threadId - The thread ID.
36
+ * @returns The thread, or `null` if not found.
37
+ */
38
+ export declare function getThreadById(threadId: string): Promise<Thread | null>;
39
+ /**
40
+ * Lists threads a participant is part of, ordered by most recent activity.
41
+ *
42
+ * @param participantId - The user ID to list threads for.
43
+ * @param options - Pagination options.
44
+ * @returns Array of threads in descending `lastMessageAt` order.
45
+ */
46
+ export declare function listThreadsForParticipant(participantId: string, options?: {
47
+ limit?: number;
48
+ offset?: number;
49
+ }): Promise<Thread[]>;
50
+ /**
51
+ * Sends a message in a thread. Updates `lastMessageAt` and bumps the
52
+ * recipient's `unreadCount*` counter, then broadcasts the new message
53
+ * over the bonded realtime provider (if any).
54
+ *
55
+ * @param threadId - The target thread ID.
56
+ * @param senderId - The sending participant's user ID.
57
+ * @param body - Message body. Must be non-empty after trimming.
58
+ * @param attachments - Optional attachment list.
59
+ * @returns The persisted message.
60
+ * @throws {Error} If the thread does not exist, the sender is not a
61
+ * participant, or the body is empty.
62
+ */
63
+ export declare function sendMessage(threadId: string, senderId: string, body: string, attachments?: MessageAttachment[]): Promise<Message>;
64
+ /**
65
+ * Marks a thread as read for the given participant and resets their
66
+ * unread counter to zero. Broadcasts a `message:read` event.
67
+ *
68
+ * @param threadId - The thread ID.
69
+ * @param readerId - The participant user ID marking the thread as read.
70
+ * @throws {Error} If the thread does not exist or the reader is not a
71
+ * participant.
72
+ */
73
+ export declare function markRead(threadId: string, readerId: string): Promise<void>;
74
+ /**
75
+ * Lists messages in a thread, newest first. Soft-deleted messages are
76
+ * still returned (with `deletedAt` set) so clients can render
77
+ * "this message was deleted" placeholders.
78
+ *
79
+ * @param threadId - The thread ID.
80
+ * @param options - Pagination options. `before` returns messages strictly
81
+ * older than the given ISO timestamp; `limit` is clamped to 200.
82
+ * @returns Array of messages, newest first.
83
+ */
84
+ export declare function listMessages(threadId: string, options?: ListMessagesOptions): Promise<Message[]>;
85
+ /**
86
+ * Edits an existing message body. Only the original sender may edit.
87
+ *
88
+ * @param messageId - The message ID.
89
+ * @param senderId - The user ID claiming to be the message author.
90
+ * @param body - New non-empty message body.
91
+ * @returns The updated message, or `null` if not found / not authorised.
92
+ */
93
+ export declare function editMessage(messageId: string, senderId: string, body: string): Promise<Message | null>;
94
+ /**
95
+ * Soft-deletes a message. Only the original sender may delete.
96
+ *
97
+ * @param messageId - The message ID.
98
+ * @param senderId - The user ID claiming to be the message author.
99
+ * @returns `true` if the message was soft-deleted, `false` if not found
100
+ * or not authorised.
101
+ */
102
+ export declare function deleteMessage(messageId: string, senderId: string): Promise<boolean>;
103
+ /**
104
+ * Counts unread messages for a participant across all of their threads.
105
+ *
106
+ * @param participantId - The participant user ID.
107
+ * @returns Total unread message count across every thread.
108
+ */
109
+ export declare function getTotalUnreadCount(participantId: string): Promise<number>;
110
+ /**
111
+ * Counts the messages persisted in a thread (including soft-deleted).
112
+ *
113
+ * @param threadId - The thread ID.
114
+ * @returns Total stored message count.
115
+ */
116
+ export declare function countMessages(threadId: string): Promise<number>;
117
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAYH,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,OAAO,EAEZ,KAAK,iBAAiB,EACtB,KAAK,MAAM,EAEZ,MAAM,YAAY,CAAA;AAQnB;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAK/E;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAsB7E;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAE5E;AAED;;;;;;GAMG;AACH,wBAAsB,yBAAyB,CAC7C,aAAa,EAAE,MAAM,EACrB,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GAChD,OAAO,CAAC,MAAM,EAAE,CAAC,CA2BnB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,WAAW,GAAE,iBAAiB,EAAO,GACpC,OAAO,CAAC,OAAO,CAAC,CAkDlB;AAED;;;;;;;;GAQG;AACH,wBAAsB,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA4BhF;AAED;;;;;;;;;GASG;AACH,wBAAsB,YAAY,CAChC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,OAAO,EAAE,CAAC,CAepB;AAED;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAC/B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAgBzB;AAED;;;;;;;GAOG;AACH,wBAAsB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAazF;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAiBhF;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAErE"}
@@ -0,0 +1,301 @@
1
+ /**
2
+ * Message resource business logic.
3
+ *
4
+ * Uses the abstract DataStore from `@molecule/api-database` for all
5
+ * persistence — never raw SQL. Realtime broadcast is delegated to
6
+ * `@molecule/api-realtime` and is no-op when no realtime provider is
7
+ * bonded.
8
+ *
9
+ * @module
10
+ */
11
+ import { count, create as dbCreate, findById, findMany, findOne, updateById, } from '@molecule/api-database';
12
+ import { broadcast, hasProvider as hasRealtimeProvider } from '@molecule/api-realtime';
13
+ import { MESSAGE_REALTIME_EVENTS, threadRoomId, } from './types.js';
14
+ const THREADS_TABLE = 'message_threads';
15
+ const MESSAGES_TABLE = 'messages';
16
+ const DEFAULT_PAGE_SIZE = 50;
17
+ const MAX_PAGE_SIZE = 200;
18
+ /**
19
+ * Canonicalises a (a, b) participant pair so that the lexicographically
20
+ * smaller value is always returned first. This guarantees `(x, y)` and
21
+ * `(y, x)` resolve to the same {@link Thread} row.
22
+ *
23
+ * @param a - First participant ID.
24
+ * @param b - Second participant ID.
25
+ * @returns A tuple `[participantAId, participantBId]` in canonical order.
26
+ * @throws {Error} If `a` and `b` refer to the same participant.
27
+ */
28
+ export function canonicaliseParticipants(a, b) {
29
+ if (a === b) {
30
+ throw new Error('Cannot create a 1:1 thread with the same participant on both sides');
31
+ }
32
+ return a < b ? [a, b] : [b, a];
33
+ }
34
+ /**
35
+ * Looks up the canonical thread between two participants, creating it
36
+ * lazily if none exists.
37
+ *
38
+ * @param a - First participant user ID.
39
+ * @param b - Second participant user ID.
40
+ * @returns The existing or newly-created thread.
41
+ */
42
+ export async function getOrCreateThread(a, b) {
43
+ const [participantAId, participantBId] = canonicaliseParticipants(a, b);
44
+ const existing = await findOne(THREADS_TABLE, [
45
+ { field: 'participantAId', operator: '=', value: participantAId },
46
+ { field: 'participantBId', operator: '=', value: participantBId },
47
+ ]);
48
+ if (existing)
49
+ return existing;
50
+ const result = await dbCreate(THREADS_TABLE, {
51
+ participantAId,
52
+ participantBId,
53
+ lastMessageAt: null,
54
+ unreadCountA: 0,
55
+ unreadCountB: 0,
56
+ });
57
+ if (!result.data) {
58
+ throw new Error('Failed to create message thread');
59
+ }
60
+ return result.data;
61
+ }
62
+ /**
63
+ * Retrieves a thread by ID.
64
+ *
65
+ * @param threadId - The thread ID.
66
+ * @returns The thread, or `null` if not found.
67
+ */
68
+ export async function getThreadById(threadId) {
69
+ return findById(THREADS_TABLE, threadId);
70
+ }
71
+ /**
72
+ * Lists threads a participant is part of, ordered by most recent activity.
73
+ *
74
+ * @param participantId - The user ID to list threads for.
75
+ * @param options - Pagination options.
76
+ * @returns Array of threads in descending `lastMessageAt` order.
77
+ */
78
+ export async function listThreadsForParticipant(participantId, options = {}) {
79
+ const limit = clampLimit(options.limit, DEFAULT_PAGE_SIZE);
80
+ const offset = options.offset ?? 0;
81
+ // Two queries unioned in memory because most DataStore backends do not
82
+ // expose OR filters on the abstract interface.
83
+ const [asA, asB] = await Promise.all([
84
+ findMany(THREADS_TABLE, {
85
+ where: [{ field: 'participantAId', operator: '=', value: participantId }],
86
+ orderBy: [{ field: 'lastMessageAt', direction: 'desc' }],
87
+ limit: limit + offset,
88
+ }),
89
+ findMany(THREADS_TABLE, {
90
+ where: [{ field: 'participantBId', operator: '=', value: participantId }],
91
+ orderBy: [{ field: 'lastMessageAt', direction: 'desc' }],
92
+ limit: limit + offset,
93
+ }),
94
+ ]);
95
+ const merged = [...asA, ...asB].sort((x, y) => {
96
+ const xt = x.lastMessageAt ?? '';
97
+ const yt = y.lastMessageAt ?? '';
98
+ if (xt === yt)
99
+ return x.id < y.id ? 1 : -1;
100
+ return xt < yt ? 1 : -1;
101
+ });
102
+ return merged.slice(offset, offset + limit);
103
+ }
104
+ /**
105
+ * Sends a message in a thread. Updates `lastMessageAt` and bumps the
106
+ * recipient's `unreadCount*` counter, then broadcasts the new message
107
+ * over the bonded realtime provider (if any).
108
+ *
109
+ * @param threadId - The target thread ID.
110
+ * @param senderId - The sending participant's user ID.
111
+ * @param body - Message body. Must be non-empty after trimming.
112
+ * @param attachments - Optional attachment list.
113
+ * @returns The persisted message.
114
+ * @throws {Error} If the thread does not exist, the sender is not a
115
+ * participant, or the body is empty.
116
+ */
117
+ export async function sendMessage(threadId, senderId, body, attachments = []) {
118
+ if (!body || !body.trim()) {
119
+ throw new Error('Message body must be non-empty');
120
+ }
121
+ const thread = await findById(THREADS_TABLE, threadId);
122
+ if (!thread) {
123
+ throw new Error(`Thread not found: ${threadId}`);
124
+ }
125
+ if (senderId !== thread.participantAId && senderId !== thread.participantBId) {
126
+ throw new Error('Sender is not a participant in this thread');
127
+ }
128
+ const now = new Date().toISOString();
129
+ const result = await dbCreate(MESSAGES_TABLE, {
130
+ threadId,
131
+ senderId,
132
+ body,
133
+ attachments,
134
+ createdAt: now,
135
+ editedAt: null,
136
+ deletedAt: null,
137
+ });
138
+ if (!result.data) {
139
+ throw new Error('Failed to persist message');
140
+ }
141
+ // Bump the recipient's unread counter and refresh lastMessageAt.
142
+ const recipientField = senderId === thread.participantAId ? 'unreadCountB' : 'unreadCountA';
143
+ const currentRecipientUnread = senderId === thread.participantAId ? thread.unreadCountB : thread.unreadCountA;
144
+ await updateById(THREADS_TABLE, threadId, {
145
+ lastMessageAt: now,
146
+ [recipientField]: currentRecipientUnread + 1,
147
+ updatedAt: now,
148
+ });
149
+ // Optional realtime broadcast — silently no-ops without a bonded provider.
150
+ if (hasRealtimeProvider()) {
151
+ try {
152
+ await broadcast(threadRoomId(threadId), MESSAGE_REALTIME_EVENTS.messageSent, result.data);
153
+ }
154
+ catch (_error) {
155
+ // Realtime delivery is best-effort; persistence already succeeded.
156
+ }
157
+ }
158
+ return result.data;
159
+ }
160
+ /**
161
+ * Marks a thread as read for the given participant and resets their
162
+ * unread counter to zero. Broadcasts a `message:read` event.
163
+ *
164
+ * @param threadId - The thread ID.
165
+ * @param readerId - The participant user ID marking the thread as read.
166
+ * @throws {Error} If the thread does not exist or the reader is not a
167
+ * participant.
168
+ */
169
+ export async function markRead(threadId, readerId) {
170
+ const thread = await findById(THREADS_TABLE, threadId);
171
+ if (!thread) {
172
+ throw new Error(`Thread not found: ${threadId}`);
173
+ }
174
+ if (readerId !== thread.participantAId && readerId !== thread.participantBId) {
175
+ throw new Error('Reader is not a participant in this thread');
176
+ }
177
+ const now = new Date().toISOString();
178
+ const counterField = readerId === thread.participantAId ? 'unreadCountA' : 'unreadCountB';
179
+ await updateById(THREADS_TABLE, threadId, {
180
+ [counterField]: 0,
181
+ updatedAt: now,
182
+ });
183
+ if (hasRealtimeProvider()) {
184
+ try {
185
+ await broadcast(threadRoomId(threadId), MESSAGE_REALTIME_EVENTS.messageRead, {
186
+ threadId,
187
+ readerId,
188
+ readAt: now,
189
+ });
190
+ }
191
+ catch (_error) {
192
+ // Best-effort; read-state is updated, broadcast failure is non-critical.
193
+ }
194
+ }
195
+ }
196
+ /**
197
+ * Lists messages in a thread, newest first. Soft-deleted messages are
198
+ * still returned (with `deletedAt` set) so clients can render
199
+ * "this message was deleted" placeholders.
200
+ *
201
+ * @param threadId - The thread ID.
202
+ * @param options - Pagination options. `before` returns messages strictly
203
+ * older than the given ISO timestamp; `limit` is clamped to 200.
204
+ * @returns Array of messages, newest first.
205
+ */
206
+ export async function listMessages(threadId, options = {}) {
207
+ const limit = clampLimit(options.limit, DEFAULT_PAGE_SIZE);
208
+ const where = [
209
+ { field: 'threadId', operator: '=', value: threadId },
210
+ ];
211
+ if (options.before) {
212
+ where.push({ field: 'createdAt', operator: '<', value: options.before });
213
+ }
214
+ return findMany(MESSAGES_TABLE, {
215
+ where,
216
+ orderBy: [{ field: 'createdAt', direction: 'desc' }],
217
+ limit,
218
+ });
219
+ }
220
+ /**
221
+ * Edits an existing message body. Only the original sender may edit.
222
+ *
223
+ * @param messageId - The message ID.
224
+ * @param senderId - The user ID claiming to be the message author.
225
+ * @param body - New non-empty message body.
226
+ * @returns The updated message, or `null` if not found / not authorised.
227
+ */
228
+ export async function editMessage(messageId, senderId, body) {
229
+ if (!body || !body.trim()) {
230
+ throw new Error('Message body must be non-empty');
231
+ }
232
+ const existing = await findById(MESSAGES_TABLE, messageId);
233
+ if (!existing || existing.senderId !== senderId || existing.deletedAt) {
234
+ return null;
235
+ }
236
+ const now = new Date().toISOString();
237
+ const result = await updateById(MESSAGES_TABLE, messageId, {
238
+ body,
239
+ editedAt: now,
240
+ });
241
+ return result.data;
242
+ }
243
+ /**
244
+ * Soft-deletes a message. Only the original sender may delete.
245
+ *
246
+ * @param messageId - The message ID.
247
+ * @param senderId - The user ID claiming to be the message author.
248
+ * @returns `true` if the message was soft-deleted, `false` if not found
249
+ * or not authorised.
250
+ */
251
+ export async function deleteMessage(messageId, senderId) {
252
+ const existing = await findById(MESSAGES_TABLE, messageId);
253
+ if (!existing || existing.senderId !== senderId || existing.deletedAt) {
254
+ return false;
255
+ }
256
+ const now = new Date().toISOString();
257
+ await updateById(MESSAGES_TABLE, messageId, {
258
+ deletedAt: now,
259
+ body: '',
260
+ attachments: [],
261
+ });
262
+ return true;
263
+ }
264
+ /**
265
+ * Counts unread messages for a participant across all of their threads.
266
+ *
267
+ * @param participantId - The participant user ID.
268
+ * @returns Total unread message count across every thread.
269
+ */
270
+ export async function getTotalUnreadCount(participantId) {
271
+ const [asA, asB] = await Promise.all([
272
+ findMany(THREADS_TABLE, {
273
+ where: [{ field: 'participantAId', operator: '=', value: participantId }],
274
+ limit: 1000,
275
+ }),
276
+ findMany(THREADS_TABLE, {
277
+ where: [{ field: 'participantBId', operator: '=', value: participantId }],
278
+ limit: 1000,
279
+ }),
280
+ ]);
281
+ const total = asA.reduce((sum, t) => sum + (t.unreadCountA ?? 0), 0) +
282
+ asB.reduce((sum, t) => sum + (t.unreadCountB ?? 0), 0);
283
+ return total;
284
+ }
285
+ /**
286
+ * Counts the messages persisted in a thread (including soft-deleted).
287
+ *
288
+ * @param threadId - The thread ID.
289
+ * @returns Total stored message count.
290
+ */
291
+ export async function countMessages(threadId) {
292
+ return count(MESSAGES_TABLE, [{ field: 'threadId', operator: '=', value: threadId }]);
293
+ }
294
+ /**
295
+ * Clamps a caller-supplied page-size to `[1, MAX_PAGE_SIZE]`, falling back to `fallback` when absent or invalid.
296
+ */
297
+ function clampLimit(value, fallback) {
298
+ if (!value || value <= 0)
299
+ return fallback;
300
+ return Math.min(value, MAX_PAGE_SIZE);
301
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Direct-message resource type definitions.
3
+ *
4
+ * Models 1:1 messaging between two participants. A `Thread` is the
5
+ * canonical conversation between participants A and B. Participants are
6
+ * canonicalised on insert so `(a, b)` and `(b, a)` resolve to the same
7
+ * thread. `unread_count_a` / `unread_count_b` track unread messages for
8
+ * each side.
9
+ *
10
+ * @module
11
+ */
12
+ /**
13
+ * A 1:1 messaging thread between two participants.
14
+ *
15
+ * `participantAId` and `participantBId` are stored canonically — the
16
+ * smaller value (lexicographically) is always `participantAId`. Use
17
+ * {@link getOrCreateThread} to look up or create a thread between two
18
+ * users without worrying about ordering.
19
+ */
20
+ export interface Thread {
21
+ /** Unique thread identifier. */
22
+ id: string;
23
+ /** Lexicographically-smaller participant ID. */
24
+ participantAId: string;
25
+ /** Lexicographically-larger participant ID. */
26
+ participantBId: string;
27
+ /** Timestamp of the most recent message, or `null` if no messages exist. */
28
+ lastMessageAt: string | null;
29
+ /** Number of messages participant A has not yet read. */
30
+ unreadCountA: number;
31
+ /** Number of messages participant B has not yet read. */
32
+ unreadCountB: number;
33
+ /** When the thread was created (ISO 8601). */
34
+ createdAt: string;
35
+ /** When the thread was last updated (ISO 8601). */
36
+ updatedAt: string;
37
+ }
38
+ /**
39
+ * A message attachment reference.
40
+ */
41
+ export interface MessageAttachment {
42
+ /** Public or signed URL to the attachment payload. */
43
+ url: string;
44
+ /** MIME type of the attachment (e.g. `image/png`). */
45
+ mime: string;
46
+ }
47
+ /**
48
+ * A single message within a {@link Thread}.
49
+ *
50
+ * `editedAt` and `deletedAt` are `null` until the message is mutated. A
51
+ * soft-deleted message (`deletedAt !== null`) is preserved for audit /
52
+ * thread continuity but should not be rendered as content.
53
+ */
54
+ export interface Message {
55
+ /** Unique message identifier. */
56
+ id: string;
57
+ /** Thread this message belongs to. */
58
+ threadId: string;
59
+ /** ID of the user who sent the message. */
60
+ senderId: string;
61
+ /** Message body text. */
62
+ body: string;
63
+ /** Optional file/image attachments. */
64
+ attachments: MessageAttachment[];
65
+ /** When the message was created (ISO 8601). */
66
+ createdAt: string;
67
+ /** When the message was last edited, or `null`. */
68
+ editedAt: string | null;
69
+ /** When the message was soft-deleted, or `null`. */
70
+ deletedAt: string | null;
71
+ }
72
+ /**
73
+ * Options for paginating messages within a thread.
74
+ */
75
+ export interface ListMessagesOptions {
76
+ /** Return messages strictly older than this timestamp (ISO 8601). */
77
+ before?: string;
78
+ /** Maximum number of messages to return. Defaults to 50, max 200. */
79
+ limit?: number;
80
+ }
81
+ /**
82
+ * Realtime event names emitted by this package.
83
+ *
84
+ * Subscribers should listen on the room ID returned from
85
+ * {@link threadRoomId} for the thread they care about.
86
+ */
87
+ export declare const MESSAGE_REALTIME_EVENTS: {
88
+ /** A new message was sent in the thread. Payload: {@link Message}. */
89
+ readonly messageSent: "message:sent";
90
+ /** A participant marked the thread as read. Payload: `{ threadId, readerId, readAt }`. */
91
+ readonly messageRead: "message:read";
92
+ };
93
+ /** Union of realtime event names. */
94
+ export type MessageRealtimeEvent = (typeof MESSAGE_REALTIME_EVENTS)[keyof typeof MESSAGE_REALTIME_EVENTS];
95
+ /**
96
+ * Returns the realtime room ID for a given thread.
97
+ *
98
+ * @param threadId - The thread ID.
99
+ * @returns The room ID consumers should subscribe to for thread events.
100
+ */
101
+ export declare function threadRoomId(threadId: string): string;
102
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;;;;;;;GAOG;AACH,MAAM,WAAW,MAAM;IACrB,gCAAgC;IAChC,EAAE,EAAE,MAAM,CAAA;IACV,gDAAgD;IAChD,cAAc,EAAE,MAAM,CAAA;IACtB,+CAA+C;IAC/C,cAAc,EAAE,MAAM,CAAA;IACtB,4EAA4E;IAC5E,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,yDAAyD;IACzD,YAAY,EAAE,MAAM,CAAA;IACpB,yDAAyD;IACzD,YAAY,EAAE,MAAM,CAAA;IACpB,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAA;IACjB,mDAAmD;IACnD,SAAS,EAAE,MAAM,CAAA;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,sDAAsD;IACtD,GAAG,EAAE,MAAM,CAAA;IACX,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAA;CACb;AAED;;;;;;GAMG;AACH,MAAM,WAAW,OAAO;IACtB,iCAAiC;IACjC,EAAE,EAAE,MAAM,CAAA;IACV,sCAAsC;IACtC,QAAQ,EAAE,MAAM,CAAA;IAChB,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAA;IAChB,yBAAyB;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,uCAAuC;IACvC,WAAW,EAAE,iBAAiB,EAAE,CAAA;IAChC,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAA;IACjB,mDAAmD;IACnD,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,oDAAoD;IACpD,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,qEAAqE;IACrE,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB;IAClC,sEAAsE;;IAEtE,0FAA0F;;CAElF,CAAA;AAEV,qCAAqC;AACrC,MAAM,MAAM,oBAAoB,GAC9B,CAAC,OAAO,uBAAuB,CAAC,CAAC,MAAM,OAAO,uBAAuB,CAAC,CAAA;AAExE;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAErD"}
package/dist/types.js ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Direct-message resource type definitions.
3
+ *
4
+ * Models 1:1 messaging between two participants. A `Thread` is the
5
+ * canonical conversation between participants A and B. Participants are
6
+ * canonicalised on insert so `(a, b)` and `(b, a)` resolve to the same
7
+ * thread. `unread_count_a` / `unread_count_b` track unread messages for
8
+ * each side.
9
+ *
10
+ * @module
11
+ */
12
+ /**
13
+ * Realtime event names emitted by this package.
14
+ *
15
+ * Subscribers should listen on the room ID returned from
16
+ * {@link threadRoomId} for the thread they care about.
17
+ */
18
+ export const MESSAGE_REALTIME_EVENTS = {
19
+ /** A new message was sent in the thread. Payload: {@link Message}. */
20
+ messageSent: 'message:sent',
21
+ /** A participant marked the thread as read. Payload: `{ threadId, readerId, readAt }`. */
22
+ messageRead: 'message:read',
23
+ };
24
+ /**
25
+ * Returns the realtime room ID for a given thread.
26
+ *
27
+ * @param threadId - The thread ID.
28
+ * @returns The room ID consumers should subscribe to for thread events.
29
+ */
30
+ export function threadRoomId(threadId) {
31
+ return `thread:${threadId}`;
32
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Message resource input validation schemas.
3
+ *
4
+ * @module
5
+ */
6
+ import { z } from 'zod';
7
+ /**
8
+ * Schema for an individual message attachment.
9
+ */
10
+ export declare const messageAttachmentSchema: z.ZodObject<{
11
+ url: z.ZodString;
12
+ mime: z.ZodString;
13
+ }, z.core.$strip>;
14
+ /**
15
+ * Schema for `getOrCreateThread` request input.
16
+ */
17
+ export declare const getOrCreateThreadSchema: z.ZodObject<{
18
+ participantId: z.ZodString;
19
+ }, z.core.$strip>;
20
+ /**
21
+ * Schema for `sendMessage` request input.
22
+ */
23
+ export declare const sendMessageSchema: z.ZodObject<{
24
+ body: z.ZodString;
25
+ attachments: z.ZodOptional<z.ZodArray<z.ZodObject<{
26
+ url: z.ZodString;
27
+ mime: z.ZodString;
28
+ }, z.core.$strip>>>;
29
+ }, z.core.$strip>;
30
+ /**
31
+ * Schema for `editMessage` request input.
32
+ */
33
+ export declare const editMessageSchema: z.ZodObject<{
34
+ body: z.ZodString;
35
+ }, z.core.$strip>;
36
+ //# sourceMappingURL=validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB;;GAEG;AACH,eAAO,MAAM,uBAAuB;;;iBAKlC,CAAA;AAEF;;GAEG;AACH,eAAO,MAAM,uBAAuB;;iBAGlC,CAAA;AAEF;;GAEG;AACH,eAAO,MAAM,iBAAiB;;;;;;iBAK5B,CAAA;AAEF;;GAEG;AACH,eAAO,MAAM,iBAAiB;;iBAG5B,CAAA"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Message resource input validation schemas.
3
+ *
4
+ * @module
5
+ */
6
+ import { z } from 'zod';
7
+ /**
8
+ * Schema for an individual message attachment.
9
+ */
10
+ export const messageAttachmentSchema = z.object({
11
+ /** Public or signed URL to the attachment. */
12
+ url: z.string().url().max(2048),
13
+ /** MIME type of the attachment. */
14
+ mime: z.string().min(1).max(255),
15
+ });
16
+ /**
17
+ * Schema for `getOrCreateThread` request input.
18
+ */
19
+ export const getOrCreateThreadSchema = z.object({
20
+ /** The other participant's user ID. */
21
+ participantId: z.string().min(1).max(255),
22
+ });
23
+ /**
24
+ * Schema for `sendMessage` request input.
25
+ */
26
+ export const sendMessageSchema = z.object({
27
+ /** Message body. Must be non-empty and at most 10,000 characters. */
28
+ body: z.string().min(1).max(10_000),
29
+ /** Optional attachments. */
30
+ attachments: z.array(messageAttachmentSchema).max(20).optional(),
31
+ });
32
+ /**
33
+ * Schema for `editMessage` request input.
34
+ */
35
+ export const editMessageSchema = z.object({
36
+ /** Updated message body. Must be non-empty and at most 10,000 characters. */
37
+ body: z.string().min(1).max(10_000),
38
+ });