@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.
- package/README.md +48 -0
- package/dist/client/index.d.ts +55 -0
- package/dist/client/index.js +79 -0
- package/dist/client/index.js.map +1 -0
- package/dist/component/_generated/api.d.ts +33 -0
- package/dist/component/_generated/api.js +31 -0
- package/dist/component/_generated/api.js.map +1 -0
- package/dist/component/_generated/component.d.ts +107 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +45 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +120 -0
- package/dist/component/_generated/server.js +78 -0
- package/dist/component/_generated/server.js.map +1 -0
- package/dist/component/convex.config.d.ts +2 -0
- package/dist/component/convex.config.js +3 -0
- package/dist/component/convex.config.js.map +1 -0
- package/dist/component/lib.d.ts +158 -0
- package/dist/component/lib.js +348 -0
- package/dist/component/lib.js.map +1 -0
- package/dist/component/schema.d.ts +65 -0
- package/dist/component/schema.js +43 -0
- package/dist/component/schema.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
- package/src/client/index.ts +98 -0
- package/src/component/_generated/api.ts +50 -0
- package/src/component/_generated/component.ts +179 -0
- package/src/component/_generated/dataModel.ts +60 -0
- package/src/component/_generated/server.ts +156 -0
- package/src/component/convex.config.ts +3 -0
- package/src/component/lib.ts +399 -0
- package/src/component/schema.ts +45 -0
- 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'
|