@primocaredentgroup/chat-backend-component 0.1.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 (38) hide show
  1. package/README.md +48 -0
  2. package/dist/client/index.d.ts +55 -0
  3. package/dist/client/index.js +79 -0
  4. package/dist/client/index.js.map +1 -0
  5. package/dist/component/_generated/api.d.ts +33 -0
  6. package/dist/component/_generated/api.js +31 -0
  7. package/dist/component/_generated/api.js.map +1 -0
  8. package/dist/component/_generated/component.d.ts +107 -0
  9. package/dist/component/_generated/component.js +11 -0
  10. package/dist/component/_generated/component.js.map +1 -0
  11. package/dist/component/_generated/dataModel.d.ts +45 -0
  12. package/dist/component/_generated/dataModel.js +11 -0
  13. package/dist/component/_generated/dataModel.js.map +1 -0
  14. package/dist/component/_generated/server.d.ts +120 -0
  15. package/dist/component/_generated/server.js +78 -0
  16. package/dist/component/_generated/server.js.map +1 -0
  17. package/dist/component/convex.config.d.ts +2 -0
  18. package/dist/component/convex.config.js +3 -0
  19. package/dist/component/convex.config.js.map +1 -0
  20. package/dist/component/lib.d.ts +158 -0
  21. package/dist/component/lib.js +348 -0
  22. package/dist/component/lib.js.map +1 -0
  23. package/dist/component/schema.d.ts +65 -0
  24. package/dist/component/schema.js +43 -0
  25. package/dist/component/schema.js.map +1 -0
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.js +2 -0
  28. package/dist/index.js.map +1 -0
  29. package/package.json +54 -0
  30. package/src/client/index.ts +98 -0
  31. package/src/component/_generated/api.ts +50 -0
  32. package/src/component/_generated/component.ts +179 -0
  33. package/src/component/_generated/dataModel.ts +60 -0
  34. package/src/component/_generated/server.ts +156 -0
  35. package/src/component/convex.config.ts +3 -0
  36. package/src/component/lib.ts +399 -0
  37. package/src/component/schema.ts +45 -0
  38. package/src/index.ts +1 -0
@@ -0,0 +1,399 @@
1
+ import { v } from 'convex/values'
2
+ import { mutation, query } from './_generated/server.js'
3
+
4
+ const channelKindValidator = v.union(v.literal('general'), v.literal('custom'))
5
+
6
+ /**
7
+ * CHANNEL CRUD
8
+ */
9
+ export const createChannel = mutation({
10
+ args: {
11
+ externalId: v.string(),
12
+ name: v.string(),
13
+ metadata: v.optional(v.any())
14
+ },
15
+ returns: v.id('channels'),
16
+ handler: async (ctx, args) => {
17
+ const now = Date.now()
18
+ return await ctx.db.insert('channels', {
19
+ externalId: args.externalId,
20
+ name: args.name,
21
+ archived: false,
22
+ metadata: args.metadata,
23
+ createdAt: now,
24
+ updatedAt: now
25
+ })
26
+ }
27
+ })
28
+
29
+ /**
30
+ * Ensure channel exists for external ID (e.g. patientId),
31
+ * and auto-create "General" subchannel on first creation.
32
+ */
33
+ export const getOrCreateChannel = mutation({
34
+ args: {
35
+ externalId: v.string(),
36
+ name: v.string(),
37
+ metadata: v.optional(v.any())
38
+ },
39
+ returns: v.id('channels'),
40
+ handler: async (ctx, args) => {
41
+ const existing = await ctx.db
42
+ .query('channels')
43
+ .withIndex('by_external_id', (q) => q.eq('externalId', args.externalId))
44
+ .first()
45
+
46
+ if (existing) {
47
+ if (existing.archived || existing.name !== args.name) {
48
+ await ctx.db.patch(existing._id, {
49
+ archived: false,
50
+ name: args.name,
51
+ metadata: args.metadata,
52
+ updatedAt: Date.now()
53
+ })
54
+ }
55
+ return existing._id
56
+ }
57
+
58
+ const now = Date.now()
59
+ const channelId = await ctx.db.insert('channels', {
60
+ externalId: args.externalId,
61
+ name: args.name,
62
+ archived: false,
63
+ metadata: args.metadata,
64
+ createdAt: now,
65
+ updatedAt: now
66
+ })
67
+
68
+ await ctx.db.insert('subchannels', {
69
+ channelId,
70
+ name: 'Generale',
71
+ kind: 'general',
72
+ archived: false,
73
+ createdAt: now,
74
+ updatedAt: now
75
+ })
76
+
77
+ return channelId
78
+ }
79
+ })
80
+
81
+ export const getChannel = query({
82
+ args: { channelId: v.id('channels') },
83
+ handler: async (ctx, args) => await ctx.db.get(args.channelId)
84
+ })
85
+
86
+ export const listChannels = query({
87
+ args: { includeArchived: v.optional(v.boolean()) },
88
+ handler: async (ctx, args) => {
89
+ const channels = await ctx.db.query('channels').collect()
90
+ if (args.includeArchived) return channels
91
+ return channels.filter((channel) => !channel.archived)
92
+ }
93
+ })
94
+
95
+ export const updateChannel = mutation({
96
+ args: {
97
+ channelId: v.id('channels'),
98
+ name: v.optional(v.string()),
99
+ metadata: v.optional(v.any())
100
+ },
101
+ returns: v.null(),
102
+ handler: async (ctx, args) => {
103
+ const patch: {
104
+ updatedAt: number
105
+ name?: string
106
+ metadata?: unknown
107
+ } = { updatedAt: Date.now() }
108
+ if (args.name !== undefined) patch.name = args.name
109
+ if (args.metadata !== undefined) patch.metadata = args.metadata
110
+ await ctx.db.patch(args.channelId, patch)
111
+ return null
112
+ }
113
+ })
114
+
115
+ export const archiveChannel = mutation({
116
+ args: {
117
+ channelId: v.id('channels'),
118
+ archived: v.optional(v.boolean())
119
+ },
120
+ returns: v.null(),
121
+ handler: async (ctx, args) => {
122
+ const archived = args.archived ?? true
123
+ const now = Date.now()
124
+ await ctx.db.patch(args.channelId, { archived, updatedAt: now })
125
+
126
+ const subchannels = await ctx.db
127
+ .query('subchannels')
128
+ .withIndex('by_channel', (q) => q.eq('channelId', args.channelId))
129
+ .collect()
130
+
131
+ await Promise.all(
132
+ subchannels.map((subchannel) =>
133
+ ctx.db.patch(subchannel._id, { archived, updatedAt: now })
134
+ )
135
+ )
136
+
137
+ return null
138
+ }
139
+ })
140
+
141
+ export const deleteChannel = mutation({
142
+ args: { channelId: v.id('channels') },
143
+ returns: v.null(),
144
+ handler: async (ctx, args) => {
145
+ const subchannels = await ctx.db
146
+ .query('subchannels')
147
+ .withIndex('by_channel', (q) => q.eq('channelId', args.channelId))
148
+ .collect()
149
+
150
+ for (const subchannel of subchannels) {
151
+ const messages = await ctx.db
152
+ .query('messages')
153
+ .withIndex('by_subchannel_and_time', (q) =>
154
+ q.eq('subchannelId', subchannel._id)
155
+ )
156
+ .collect()
157
+
158
+ for (const message of messages) {
159
+ await ctx.db.delete(message._id)
160
+ }
161
+
162
+ await ctx.db.delete(subchannel._id)
163
+ }
164
+
165
+ await ctx.db.delete(args.channelId)
166
+ return null
167
+ }
168
+ })
169
+
170
+ /**
171
+ * SUBCHANNEL CRUD
172
+ */
173
+ export const createSubchannel = mutation({
174
+ args: {
175
+ channelId: v.id('channels'),
176
+ name: v.string(),
177
+ kind: v.optional(channelKindValidator),
178
+ externalReferenceId: v.optional(v.string()),
179
+ metadata: v.optional(v.any())
180
+ },
181
+ returns: v.id('subchannels'),
182
+ handler: async (ctx, args) => {
183
+ const channel = await ctx.db.get(args.channelId)
184
+ if (!channel) {
185
+ throw new Error('Channel not found')
186
+ }
187
+
188
+ const now = Date.now()
189
+ return await ctx.db.insert('subchannels', {
190
+ channelId: args.channelId,
191
+ name: args.name,
192
+ kind: args.kind ?? 'custom',
193
+ externalReferenceId: args.externalReferenceId,
194
+ metadata: args.metadata,
195
+ archived: false,
196
+ createdAt: now,
197
+ updatedAt: now
198
+ })
199
+ }
200
+ })
201
+
202
+ export const getOrCreateSubchannel = mutation({
203
+ args: {
204
+ channelId: v.id('channels'),
205
+ name: v.string(),
206
+ kind: v.optional(channelKindValidator),
207
+ externalReferenceId: v.string(),
208
+ metadata: v.optional(v.any())
209
+ },
210
+ returns: v.id('subchannels'),
211
+ handler: async (ctx, args) => {
212
+ const existing = await ctx.db
213
+ .query('subchannels')
214
+ .withIndex('by_channel_and_external_reference', (q) =>
215
+ q
216
+ .eq('channelId', args.channelId)
217
+ .eq('externalReferenceId', args.externalReferenceId)
218
+ )
219
+ .first()
220
+
221
+ if (existing) {
222
+ return existing._id
223
+ }
224
+
225
+ const now = Date.now()
226
+ return await ctx.db.insert('subchannels', {
227
+ channelId: args.channelId,
228
+ name: args.name,
229
+ kind: args.kind ?? 'custom',
230
+ externalReferenceId: args.externalReferenceId,
231
+ metadata: args.metadata,
232
+ archived: false,
233
+ createdAt: now,
234
+ updatedAt: now
235
+ })
236
+ }
237
+ })
238
+
239
+ export const getSubchannel = query({
240
+ args: { subchannelId: v.id('subchannels') },
241
+ handler: async (ctx, args) => await ctx.db.get(args.subchannelId)
242
+ })
243
+
244
+ export const listSubchannels = query({
245
+ args: {
246
+ channelId: v.id('channels'),
247
+ includeArchived: v.optional(v.boolean())
248
+ },
249
+ handler: async (ctx, args) => {
250
+ const subchannels = await ctx.db
251
+ .query('subchannels')
252
+ .withIndex('by_channel', (q) => q.eq('channelId', args.channelId))
253
+ .collect()
254
+
255
+ const visible = args.includeArchived
256
+ ? subchannels
257
+ : subchannels.filter((subchannel) => !subchannel.archived)
258
+
259
+ return visible.sort((a, b) => {
260
+ if (a.kind === 'general' && b.kind !== 'general') return -1
261
+ if (a.kind !== 'general' && b.kind === 'general') return 1
262
+ return a.createdAt - b.createdAt
263
+ })
264
+ }
265
+ })
266
+
267
+ export const updateSubchannel = mutation({
268
+ args: {
269
+ subchannelId: v.id('subchannels'),
270
+ name: v.optional(v.string()),
271
+ metadata: v.optional(v.any())
272
+ },
273
+ returns: v.null(),
274
+ handler: async (ctx, args) => {
275
+ const patch: {
276
+ updatedAt: number
277
+ name?: string
278
+ metadata?: unknown
279
+ } = { updatedAt: Date.now() }
280
+ if (args.name !== undefined) patch.name = args.name
281
+ if (args.metadata !== undefined) patch.metadata = args.metadata
282
+ await ctx.db.patch(args.subchannelId, patch)
283
+ return null
284
+ }
285
+ })
286
+
287
+ export const archiveSubchannel = mutation({
288
+ args: {
289
+ subchannelId: v.id('subchannels'),
290
+ archived: v.optional(v.boolean())
291
+ },
292
+ returns: v.null(),
293
+ handler: async (ctx, args) => {
294
+ await ctx.db.patch(args.subchannelId, {
295
+ archived: args.archived ?? true,
296
+ updatedAt: Date.now()
297
+ })
298
+ return null
299
+ }
300
+ })
301
+
302
+ export const deleteSubchannel = mutation({
303
+ args: { subchannelId: v.id('subchannels') },
304
+ returns: v.null(),
305
+ handler: async (ctx, args) => {
306
+ const messages = await ctx.db
307
+ .query('messages')
308
+ .withIndex('by_subchannel_and_time', (q) =>
309
+ q.eq('subchannelId', args.subchannelId)
310
+ )
311
+ .collect()
312
+
313
+ for (const message of messages) {
314
+ await ctx.db.delete(message._id)
315
+ }
316
+
317
+ await ctx.db.delete(args.subchannelId)
318
+ return null
319
+ }
320
+ })
321
+
322
+ /**
323
+ * MESSAGE CRUD
324
+ */
325
+ export const sendMessage = mutation({
326
+ args: {
327
+ subchannelId: v.id('subchannels'),
328
+ authorId: v.string(),
329
+ authorName: v.optional(v.string()),
330
+ body: v.string(),
331
+ metadata: v.optional(v.any())
332
+ },
333
+ returns: v.id('messages'),
334
+ handler: async (ctx, args) => {
335
+ const subchannel = await ctx.db.get(args.subchannelId)
336
+ if (!subchannel) {
337
+ throw new Error('Subchannel not found')
338
+ }
339
+
340
+ const now = Date.now()
341
+ return await ctx.db.insert('messages', {
342
+ channelId: subchannel.channelId,
343
+ subchannelId: args.subchannelId,
344
+ authorId: args.authorId,
345
+ authorName: args.authorName,
346
+ body: args.body,
347
+ metadata: args.metadata,
348
+ createdAt: now,
349
+ updatedAt: now
350
+ })
351
+ }
352
+ })
353
+
354
+ export const getMessage = query({
355
+ args: { messageId: v.id('messages') },
356
+ handler: async (ctx, args) => await ctx.db.get(args.messageId)
357
+ })
358
+
359
+ export const listMessages = query({
360
+ args: {
361
+ subchannelId: v.id('subchannels'),
362
+ limit: v.optional(v.number())
363
+ },
364
+ handler: async (ctx, args) => {
365
+ return await ctx.db
366
+ .query('messages')
367
+ .withIndex('by_subchannel_and_time', (q) =>
368
+ q.eq('subchannelId', args.subchannelId)
369
+ )
370
+ .order('asc')
371
+ .take(args.limit ?? 200)
372
+ }
373
+ })
374
+
375
+ export const updateMessage = mutation({
376
+ args: {
377
+ messageId: v.id('messages'),
378
+ body: v.string(),
379
+ metadata: v.optional(v.any())
380
+ },
381
+ returns: v.null(),
382
+ handler: async (ctx, args) => {
383
+ await ctx.db.patch(args.messageId, {
384
+ body: args.body,
385
+ metadata: args.metadata,
386
+ updatedAt: Date.now()
387
+ })
388
+ return null
389
+ }
390
+ })
391
+
392
+ export const deleteMessage = mutation({
393
+ args: { messageId: v.id('messages') },
394
+ returns: v.null(),
395
+ handler: async (ctx, args) => {
396
+ await ctx.db.delete(args.messageId)
397
+ return null
398
+ }
399
+ })
@@ -0,0 +1,45 @@
1
+ import { defineSchema, defineTable } from 'convex/server'
2
+ import { v } from 'convex/values'
3
+
4
+ export default defineSchema({
5
+ channels: defineTable({
6
+ // External key from host domain (e.g. patientId in PrimoUp)
7
+ externalId: v.string(),
8
+ name: v.string(),
9
+ archived: v.boolean(),
10
+ metadata: v.optional(v.any()),
11
+ createdAt: v.number(),
12
+ updatedAt: v.number()
13
+ })
14
+ .index('by_external_id', ['externalId'])
15
+ .index('by_archived', ['archived']),
16
+
17
+ subchannels: defineTable({
18
+ channelId: v.id('channels'),
19
+ name: v.string(),
20
+ kind: v.union(v.literal('general'), v.literal('custom')),
21
+ externalReferenceId: v.optional(v.string()),
22
+ archived: v.boolean(),
23
+ metadata: v.optional(v.any()),
24
+ createdAt: v.number(),
25
+ updatedAt: v.number()
26
+ })
27
+ .index('by_channel', ['channelId'])
28
+ .index('by_channel_and_external_reference', [
29
+ 'channelId',
30
+ 'externalReferenceId'
31
+ ]),
32
+
33
+ messages: defineTable({
34
+ channelId: v.id('channels'),
35
+ subchannelId: v.id('subchannels'),
36
+ authorId: v.string(),
37
+ authorName: v.optional(v.string()),
38
+ body: v.string(),
39
+ metadata: v.optional(v.any()),
40
+ createdAt: v.number(),
41
+ updatedAt: v.number()
42
+ })
43
+ .index('by_subchannel_and_time', ['subchannelId', 'createdAt'])
44
+ .index('by_channel_and_time', ['channelId', 'createdAt'])
45
+ })
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { exposeApi } from './client/index.js'