@web-ts-toolkit/message-service 0.6.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/index.js ADDED
@@ -0,0 +1,802 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ ActionNotAllowedError: () => ActionNotAllowedError,
34
+ ActionNotFoundError: () => ActionNotFoundError,
35
+ BaseMessageFields: () => BaseMessageFields,
36
+ GENERIC_NOTIFICATION_TEMPLATE_CD: () => GENERIC_NOTIFICATION_TEMPLATE_CD,
37
+ MESSAGE_ARCHIVE_MODEL_NAME: () => MESSAGE_ARCHIVE_MODEL_NAME,
38
+ MESSAGE_MODEL_NAME: () => MESSAGE_MODEL_NAME,
39
+ MessageArchiveSchema: () => MessageArchiveSchema,
40
+ MessageArchivedError: () => MessageArchivedError,
41
+ MessageContentSchema: () => MessageContentSchema,
42
+ MessageNotFoundError: () => MessageNotFoundError,
43
+ MessageSchema: () => MessageSchema,
44
+ MessageService: () => MessageService,
45
+ NoopEmailProvider: () => NoopEmailProvider,
46
+ NoopPaymentProvider: () => NoopPaymentProvider,
47
+ TemplateNotFoundError: () => TemplateNotFoundError,
48
+ TemplateRegistry: () => TemplateRegistry,
49
+ buildMessageArchiveSchema: () => buildMessageArchiveSchema,
50
+ buildMessageSchema: () => buildMessageSchema,
51
+ createMessageRoutes: () => createMessageRoutes,
52
+ defaultRegistry: () => defaultRegistry,
53
+ filterActions: () => filterActions,
54
+ includesAction: () => includesAction,
55
+ interpolateTemplate: () => interpolateTemplate,
56
+ isActionAllowed: () => isActionAllowed
57
+ });
58
+ module.exports = __toCommonJS(index_exports);
59
+
60
+ // src/schemas/base.ts
61
+ var import_mongoose = __toESM(require("mongoose"));
62
+ var MESSAGE_MODEL_NAME = "Message";
63
+ var MESSAGE_ARCHIVE_MODEL_NAME = "MessageArchive";
64
+ var MessageContentSchema = new import_mongoose.default.Schema(
65
+ {
66
+ title: { type: String, default: "" },
67
+ long: { type: String, default: "" },
68
+ short: { type: String, default: "" }
69
+ },
70
+ { _id: false }
71
+ );
72
+ function defaultMessageContent() {
73
+ return { title: "", long: "", short: "" };
74
+ }
75
+ var BaseMessageFields = {
76
+ templateCd: { type: String, default: "" },
77
+ type: { type: String, default: "notification" },
78
+ fromUser: { type: import_mongoose.default.Schema.Types.ObjectId, default: null },
79
+ toUser: { type: import_mongoose.default.Schema.Types.ObjectId, default: null },
80
+ toRoles: { type: [String], default: [] },
81
+ senderContent: { type: MessageContentSchema, default: defaultMessageContent },
82
+ receiverContent: { type: MessageContentSchema, default: defaultMessageContent },
83
+ documents: { type: [import_mongoose.default.Schema.Types.ObjectId], default: [] },
84
+ paymentSession: { type: String, default: null },
85
+ paymentCd: { type: String, default: "" },
86
+ payload: { type: import_mongoose.default.Schema.Types.Mixed, default: {} },
87
+ display: { type: import_mongoose.default.Schema.Types.Mixed, default: {} },
88
+ clientRequestId: { type: String, default: null }
89
+ };
90
+
91
+ // src/schemas/message.ts
92
+ var import_mongoose2 = __toESM(require("mongoose"));
93
+
94
+ // src/template-registry.ts
95
+ var TemplateRegistry = class {
96
+ constructor() {
97
+ this.templates = /* @__PURE__ */ new Map();
98
+ }
99
+ /**
100
+ * Register a template. Overwrites if templateCd already exists.
101
+ */
102
+ register(template) {
103
+ this.templates.set(template.templateCd, template);
104
+ }
105
+ /**
106
+ * Register multiple templates at once.
107
+ */
108
+ registerAll(templates) {
109
+ for (const t of templates) {
110
+ this.register(t);
111
+ }
112
+ }
113
+ /**
114
+ * Find a template by its templateCd.
115
+ */
116
+ find(templateCd) {
117
+ return this.templates.get(templateCd);
118
+ }
119
+ /**
120
+ * Check if a template exists.
121
+ */
122
+ has(templateCd) {
123
+ return this.templates.has(templateCd);
124
+ }
125
+ /**
126
+ * Get all registered template codes.
127
+ */
128
+ getTemplateCodes() {
129
+ return Array.from(this.templates.keys());
130
+ }
131
+ /**
132
+ * Get all registered templates.
133
+ */
134
+ getAll() {
135
+ return Array.from(this.templates.values());
136
+ }
137
+ /**
138
+ * Remove a template by templateCd.
139
+ */
140
+ unregister(templateCd) {
141
+ return this.templates.delete(templateCd);
142
+ }
143
+ /**
144
+ * Clear all templates.
145
+ */
146
+ clear() {
147
+ this.templates.clear();
148
+ }
149
+ };
150
+ var defaultRegistry = new TemplateRegistry();
151
+ function includesAction(templateCd, actionCd, registry) {
152
+ const template = registry.find(templateCd);
153
+ if (!template) return false;
154
+ return template.actions.some((a) => a.actionCd === actionCd);
155
+ }
156
+
157
+ // src/schemas/methods.ts
158
+ function isSender(user) {
159
+ return String(this.fromUser) === String(user.id || user._id);
160
+ }
161
+ function isReceiver(user) {
162
+ const id = String(user.id || user._id);
163
+ return String(this.toUser) === id || this.toRoles.some((r) => user.roles?.includes(r));
164
+ }
165
+
166
+ // src/schemas/message.ts
167
+ function createArchiveMethod(ctx) {
168
+ return function archive(actionCd, archivedBy, registry) {
169
+ if (!includesAction(this.templateCd, actionCd, registry) || !archivedBy) {
170
+ return Promise.resolve();
171
+ }
172
+ const MessageArchive = import_mongoose2.default.model(ctx.archiveModelName);
173
+ const data = this.toObject();
174
+ return MessageArchive.create({
175
+ ...data,
176
+ actionCd,
177
+ archivedBy
178
+ }).then(async () => {
179
+ await this.deleteOne();
180
+ });
181
+ };
182
+ }
183
+ function createPreSaveHook(ctx) {
184
+ return async function sendNotificationEmail() {
185
+ if (!this.toUser || !this.receiverContent?.title) {
186
+ return;
187
+ }
188
+ const title = this.receiverContent.title.trim();
189
+ if (ctx.emailNotificationExclusions.includes(title.toLowerCase())) {
190
+ return;
191
+ }
192
+ try {
193
+ const User = import_mongoose2.default.model(ctx.userModelName);
194
+ const user = await User.findById(this.toUser).select("email").lean();
195
+ if (!user?.email) {
196
+ return;
197
+ }
198
+ const long = this.receiverContent.long || "";
199
+ const short = this.receiverContent.short || "";
200
+ const body = long.length > short.length ? long : short;
201
+ await ctx.emailNotifier(user.email, title, body);
202
+ } catch {
203
+ }
204
+ };
205
+ }
206
+ function assertModelRegistered(name, role) {
207
+ if (!import_mongoose2.default.modelNames().includes(name)) {
208
+ throw new Error(
209
+ `message-service: cannot configure schema \u2014 ${role} model "${name}" is not registered with Mongoose. Call mongoose.model("${name}", ...) before buildMessageSchema().`
210
+ );
211
+ }
212
+ }
213
+ function resolveConfig(config) {
214
+ return {
215
+ emailNotifier: config?.emailNotifier ?? null,
216
+ emailNotificationExclusions: (config?.emailNotificationExclusions ?? []).map((e) => e.toLowerCase()),
217
+ userModelName: config?.userModelName ?? "User",
218
+ archiveModelName: config?.archiveModelName ?? MESSAGE_ARCHIVE_MODEL_NAME
219
+ };
220
+ }
221
+ function buildMessageSchema(config) {
222
+ const resolved = resolveConfig(config);
223
+ if (resolved.emailNotifier) {
224
+ assertModelRegistered(resolved.userModelName, "user");
225
+ }
226
+ const schema = new import_mongoose2.default.Schema(BaseMessageFields, {
227
+ timestamps: true
228
+ });
229
+ schema.index({ createdAt: 1 });
230
+ schema.index({ clientRequestId: 1 }, { sparse: true });
231
+ schema.methods.isSender = isSender;
232
+ schema.methods.isReceiver = isReceiver;
233
+ schema.methods.archive = createArchiveMethod({ archiveModelName: resolved.archiveModelName });
234
+ if (resolved.emailNotifier) {
235
+ schema.pre(
236
+ "save",
237
+ createPreSaveHook({
238
+ emailNotifier: resolved.emailNotifier,
239
+ emailNotificationExclusions: resolved.emailNotificationExclusions,
240
+ userModelName: resolved.userModelName
241
+ })
242
+ );
243
+ }
244
+ return schema;
245
+ }
246
+ var MessageSchema = buildMessageSchema();
247
+
248
+ // src/schemas/message-archive.ts
249
+ var import_mongoose3 = __toESM(require("mongoose"));
250
+ function buildMessageArchiveSchema() {
251
+ const schema = new import_mongoose3.default.Schema(
252
+ {
253
+ ...BaseMessageFields,
254
+ actionCd: { type: String, default: "" },
255
+ archivedBy: { type: import_mongoose3.default.Schema.Types.ObjectId, default: null, index: true },
256
+ archivedAt: { type: Date, default: Date.now }
257
+ },
258
+ {
259
+ timestamps: true
260
+ }
261
+ );
262
+ schema.index({ createdAt: 1 });
263
+ schema.methods.isSender = isSender;
264
+ schema.methods.isReceiver = isReceiver;
265
+ return schema;
266
+ }
267
+ var MessageArchiveSchema = buildMessageArchiveSchema();
268
+
269
+ // src/template-engine.ts
270
+ var import_handlebars = __toESM(require("handlebars"));
271
+ var compiledTemplates = /* @__PURE__ */ new Map();
272
+ function compile(template, data) {
273
+ let compiled = compiledTemplates.get(template);
274
+ if (!compiled) {
275
+ compiled = import_handlebars.default.compile(template, { noEscape: true });
276
+ compiledTemplates.set(template, compiled);
277
+ }
278
+ return compiled(data);
279
+ }
280
+ function filterActions(actions, usertype, options = {}) {
281
+ const { permissions = {}, message = {}, data = {} } = options;
282
+ return actions.filter((action) => {
283
+ if (!action[usertype]) return false;
284
+ if (action.permission && !permissions[action.permission]) return false;
285
+ if (action.condition && !action.condition(message)) return false;
286
+ return true;
287
+ }).map((action) => {
288
+ const { confirmation } = action;
289
+ return {
290
+ actionCd: action.actionCd,
291
+ isDefault: action.isDefault,
292
+ name: compile(action.name, data),
293
+ variant: action.variant,
294
+ confirmation: confirmation ? {
295
+ title: compile(confirmation.title, data),
296
+ message: compile(confirmation.message, data),
297
+ ...confirmation.notesLabel ? { notesLabel: compile(confirmation.notesLabel, data) } : {},
298
+ ...confirmation.requireNotes !== void 0 ? { requireNotes: confirmation.requireNotes } : {},
299
+ ...confirmation.documents !== void 0 ? { documents: confirmation.documents } : {}
300
+ } : void 0,
301
+ payload: action.payload
302
+ };
303
+ });
304
+ }
305
+ function isActionAllowed(action, user, message, options = {}) {
306
+ const isReceiver2 = action.receiver && message.isReceiver(user);
307
+ const isSender2 = action.sender && message.isSender(user);
308
+ if (!isReceiver2 && !isSender2) return false;
309
+ if (action.permission && !(options.permissions || {})[action.permission]) return false;
310
+ if (action.condition && !action.condition(message)) return false;
311
+ return true;
312
+ }
313
+ function interpolateTemplate(template, data, usertype, options = {}) {
314
+ const { permissions = {}, message = {} } = options;
315
+ const { senderContent, receiverContent, actions, uiTemplate } = template;
316
+ return {
317
+ senderContent: {
318
+ title: compile(senderContent?.title || "", data),
319
+ long: compile(senderContent?.long || "", data),
320
+ short: compile(senderContent?.short || "", data)
321
+ },
322
+ receiverContent: {
323
+ title: compile(receiverContent?.title || "", data),
324
+ long: compile(receiverContent?.long || "", data),
325
+ short: compile(receiverContent?.short || "", data)
326
+ },
327
+ uiTemplate: resolveUiTemplate(uiTemplate, usertype),
328
+ actions: filterActions(actions, usertype, { permissions, message, data })
329
+ };
330
+ }
331
+ function resolveUiTemplate(uiTemplate, usertype) {
332
+ if (typeof uiTemplate === "string") return uiTemplate;
333
+ return uiTemplate[usertype] || uiTemplate.sender || uiTemplate.receiver || "";
334
+ }
335
+
336
+ // src/providers/email.ts
337
+ var NoopEmailProvider = class {
338
+ async sendNotification(_to, _title, _body) {
339
+ }
340
+ };
341
+
342
+ // src/providers/payment.ts
343
+ var NoopPaymentProvider = class {
344
+ async createSession(_user, _code, _priceArgs) {
345
+ return null;
346
+ }
347
+ async expireSession(_sessionId) {
348
+ }
349
+ async refundPayment(_sessionId) {
350
+ }
351
+ };
352
+
353
+ // src/message-service.ts
354
+ var import_utils = require("@web-ts-toolkit/utils");
355
+ var DEFAULT_LIST_LIMIT = 50;
356
+ var MAX_LIST_LIMIT = 100;
357
+ var GENERIC_NOTIFICATION_TEMPLATE_CD = "__generic-notification__";
358
+ var MessageNotFoundError = class extends Error {
359
+ constructor(messageId) {
360
+ super(`message "${messageId}" not found`);
361
+ this.name = "MessageNotFoundError";
362
+ }
363
+ };
364
+ var MessageArchivedError = class extends Error {
365
+ constructor(messageId) {
366
+ super(`message "${messageId}" is archived`);
367
+ this.name = "MessageArchivedError";
368
+ }
369
+ };
370
+ var TemplateNotFoundError = class extends Error {
371
+ constructor(templateCd) {
372
+ super(`template "${templateCd}" not found`);
373
+ this.name = "TemplateNotFoundError";
374
+ }
375
+ };
376
+ var ActionNotFoundError = class extends Error {
377
+ constructor(templateCd, actionCd) {
378
+ super(`action "${actionCd}" not found in template "${templateCd}"`);
379
+ this.name = "ActionNotFoundError";
380
+ }
381
+ };
382
+ var ActionNotAllowedError = class extends Error {
383
+ constructor() {
384
+ super("not allowed");
385
+ this.name = "ActionNotAllowedError";
386
+ }
387
+ };
388
+ var MessageService = class {
389
+ constructor(options) {
390
+ this.getModel = options.getModel;
391
+ this.paymentProvider = options.paymentProvider ?? null;
392
+ this.expirePaymentSession = this.paymentProvider?.expireSession.bind(this.paymentProvider);
393
+ this.refundPaymentSession = this.paymentProvider?.refundPayment.bind(this.paymentProvider);
394
+ this.adminRoles = options.adminRoles ?? [];
395
+ this.registry = options.registry ?? defaultRegistry;
396
+ this.defaultListLimit = options.defaultListLimit ?? DEFAULT_LIST_LIMIT;
397
+ this.maxListLimit = options.maxListLimit ?? MAX_LIST_LIMIT;
398
+ }
399
+ // -------------------------------------------------------------------------
400
+ // Message lookup
401
+ // -------------------------------------------------------------------------
402
+ /**
403
+ * Find a message by id, falling back to the archive. Returns null if
404
+ * the message does not exist in either collection.
405
+ */
406
+ async findMessage(messageId, options = {}) {
407
+ const Message = this.getModel(MESSAGE_MODEL_NAME);
408
+ const MessageArchive = this.getModel(MESSAGE_ARCHIVE_MODEL_NAME);
409
+ const message = await this.findByIdWithOptions(Message, messageId, options);
410
+ if (message) return message;
411
+ return await this.findByIdWithOptions(
412
+ MessageArchive,
413
+ messageId,
414
+ options
415
+ );
416
+ }
417
+ async findMessageOrThrow(messageId, options = {}) {
418
+ const message = await this.findMessage(messageId, options);
419
+ if (!message) {
420
+ throw new MessageNotFoundError(messageId);
421
+ }
422
+ return message;
423
+ }
424
+ // -------------------------------------------------------------------------
425
+ // Create message from template
426
+ // -------------------------------------------------------------------------
427
+ async createMessage(params) {
428
+ const {
429
+ templateCd,
430
+ user,
431
+ roles = [],
432
+ identity = {},
433
+ permissions = {},
434
+ payload = {},
435
+ payerUser,
436
+ req,
437
+ clientRequestId
438
+ } = params;
439
+ if (clientRequestId) {
440
+ const existing = await this.findByClientRequestId(clientRequestId);
441
+ if (existing) return existing;
442
+ }
443
+ const template = this.registry.find(templateCd);
444
+ if (!template) throw new TemplateNotFoundError(templateCd);
445
+ const messageData = await template.prepareMessage({
446
+ user,
447
+ roles,
448
+ identity,
449
+ permissions,
450
+ payload,
451
+ getModel: this.getModel,
452
+ req
453
+ });
454
+ if (!messageData) return [];
455
+ const ctx = { user, roles, identity, permissions, payload, payerUser, req };
456
+ const items = Array.isArray(messageData) ? messageData : [messageData];
457
+ const results = [];
458
+ for (const m of items) {
459
+ results.push(await this.persistItem(template, m, ctx, clientRequestId));
460
+ }
461
+ return results;
462
+ }
463
+ // -------------------------------------------------------------------------
464
+ // Create generic notification (no template, no actions)
465
+ // -------------------------------------------------------------------------
466
+ async createNotification(params) {
467
+ const Message = this.getModel(MESSAGE_MODEL_NAME);
468
+ return Message.create({
469
+ type: "notification",
470
+ templateCd: GENERIC_NOTIFICATION_TEMPLATE_CD,
471
+ fromUser: params.fromUser ?? null,
472
+ toUser: params.toUser ?? null,
473
+ toRoles: params.toRoles,
474
+ senderContent: params.senderContent,
475
+ receiverContent: params.receiverContent,
476
+ documents: params.documents || []
477
+ });
478
+ }
479
+ // -------------------------------------------------------------------------
480
+ // List messages
481
+ // -------------------------------------------------------------------------
482
+ /**
483
+ * Build a Mongoose filter for messages visible to the given user.
484
+ * Exposed so callers can use the same visibility rules for custom
485
+ * queries (e.g. with `populate`).
486
+ */
487
+ buildVisibilityFilter(user) {
488
+ const userId = user.id || String(user._id);
489
+ return {
490
+ $or: [{ fromUser: userId }, { toUser: userId }, { toRoles: { $in: user.roles ?? [] } }]
491
+ };
492
+ }
493
+ /**
494
+ * List active (non-archived) messages visible to a user.
495
+ * Returns messages where the user is the sender, the receiver,
496
+ * or matches one of the recipient's roles.
497
+ */
498
+ async listMessages(params) {
499
+ const { user, limit: rawLimit, skip: rawSkip, populate } = params;
500
+ const limit = Math.min(Math.max(rawLimit ?? this.defaultListLimit, 1), this.maxListLimit);
501
+ const skip = Math.max(rawSkip ?? 0, 0);
502
+ const Message = this.getModel(MESSAGE_MODEL_NAME);
503
+ const query = this.applyPopulate(
504
+ Message.find(this.buildVisibilityFilter(user)).sort({ createdAt: -1 }).skip(skip).limit(limit),
505
+ populate
506
+ );
507
+ return query;
508
+ }
509
+ /**
510
+ * Count active (non-archived) messages visible to a user.
511
+ * Useful for badge indicators ("3 new messages").
512
+ */
513
+ async countMessages(user) {
514
+ const Message = this.getModel(MESSAGE_MODEL_NAME);
515
+ return Message.countDocuments(this.buildVisibilityFilter(user));
516
+ }
517
+ // -------------------------------------------------------------------------
518
+ // Get actions for a message
519
+ // -------------------------------------------------------------------------
520
+ async getActions(messageId, usertype, options = {}) {
521
+ const message = options.message ?? await this.findMessage(messageId, { populate: options.populate });
522
+ if (!message) return null;
523
+ if (!options.isAdmin && options.user) {
524
+ const isAllowedUsertype = usertype === "sender" ? message.isSender(options.user) : message.isReceiver(options.user);
525
+ if (!isAllowedUsertype) {
526
+ return null;
527
+ }
528
+ }
529
+ if (message.templateCd === GENERIC_NOTIFICATION_TEMPLATE_CD) {
530
+ return { uiTemplate: "notification", actions: [] };
531
+ }
532
+ const template = this.registry.find(message.templateCd);
533
+ if (!template) return null;
534
+ const data = message.payload ?? {};
535
+ const interpolated = interpolateTemplate(template, data, usertype, {
536
+ permissions: options.permissions,
537
+ message
538
+ });
539
+ if (options.isAdmin) {
540
+ return { uiTemplate: interpolated.uiTemplate, actions: [] };
541
+ }
542
+ return { uiTemplate: interpolated.uiTemplate, actions: interpolated.actions };
543
+ }
544
+ // -------------------------------------------------------------------------
545
+ // Handle an action on a message
546
+ // -------------------------------------------------------------------------
547
+ async handleAction(templateCd, actionCd, data) {
548
+ if (this.isArchivedMessage(data.message)) {
549
+ throw new MessageArchivedError(String(data.message._id));
550
+ }
551
+ const template = this.registry.find(templateCd);
552
+ if (!template) throw new TemplateNotFoundError(templateCd);
553
+ const action = template.actions.find((a) => a.actionCd === actionCd);
554
+ if (!action) throw new ActionNotFoundError(templateCd, actionCd);
555
+ if (!isActionAllowed(action, data.user, data.message, { permissions: data.permissions })) {
556
+ throw new ActionNotAllowedError();
557
+ }
558
+ const ctx = this.buildActionContext(data);
559
+ const result = await action.runHandler(ctx);
560
+ await data.message.archive(actionCd, data.user._id, this.registry);
561
+ await this.runSenderNotification(action, ctx, data.message);
562
+ return result;
563
+ }
564
+ // -------------------------------------------------------------------------
565
+ // Private helpers
566
+ // -------------------------------------------------------------------------
567
+ async persistItem(template, m, ctx, clientRequestId) {
568
+ const toUser = m.toUser ?? null;
569
+ const toRoles = (m.toRoles && m.toRoles.length > 0 ? m.toRoles : this.adminRoles).slice();
570
+ const type = m.type || template.type || "notification";
571
+ const paymentCd = m.paymentCd || template.paymentCd || "";
572
+ let paymentSession = null;
573
+ if (paymentCd && this.paymentProvider && (toUser || toRoles.length > 0)) {
574
+ paymentSession = await this.paymentProvider.createSession(
575
+ (ctx.payerUser || ctx.user)._id,
576
+ paymentCd,
577
+ m.priceArgs
578
+ );
579
+ if (!paymentSession) throw new Error("payment session creation failed");
580
+ }
581
+ const interpolated = interpolateTemplate(template, m.templateData || {}, "receiver");
582
+ const Message = this.getModel(MESSAGE_MODEL_NAME);
583
+ return Message.create({
584
+ type,
585
+ templateCd: template.templateCd,
586
+ fromUser: m.fromUser || ctx.user._id,
587
+ toUser,
588
+ toRoles,
589
+ senderContent: interpolated.senderContent,
590
+ receiverContent: interpolated.receiverContent,
591
+ documents: ctx.payload.documents || [],
592
+ paymentSession,
593
+ paymentCd,
594
+ payload: m.payload || ctx.payload,
595
+ display: m.display,
596
+ clientRequestId: clientRequestId ?? null
597
+ });
598
+ }
599
+ buildActionContext(data) {
600
+ return {
601
+ message: data.message,
602
+ user: data.user,
603
+ getModel: this.getModel,
604
+ expireSession: this.expirePaymentSession,
605
+ refundPayment: this.refundPaymentSession,
606
+ req: data.req
607
+ };
608
+ }
609
+ async runSenderNotification(action, ctx, message) {
610
+ if (!action.senderNotification) return;
611
+ let content;
612
+ if (typeof action.senderNotification === "function") {
613
+ content = await action.senderNotification(ctx);
614
+ } else {
615
+ content = action.senderNotification;
616
+ }
617
+ const senderTitle = message.senderContent?.title ?? "";
618
+ const senderNotificationContent = (0, import_utils.isString)(content) ? { title: senderTitle, long: content, short: content } : {
619
+ title: content.title || senderTitle,
620
+ long: content.long,
621
+ short: content.short || content.long
622
+ };
623
+ const documents = !(0, import_utils.isString)(content) ? content.documents || [] : [];
624
+ if (message.fromUser) {
625
+ await this.createNotification({
626
+ toUser: message.fromUser,
627
+ receiverContent: senderNotificationContent,
628
+ documents
629
+ });
630
+ }
631
+ }
632
+ async findByClientRequestId(clientRequestId) {
633
+ const Message = this.getModel(MESSAGE_MODEL_NAME);
634
+ const docs = await Message.find({ clientRequestId }).sort({ createdAt: 1, _id: 1 }).limit(Number.MAX_SAFE_INTEGER);
635
+ return docs.length > 0 ? docs : null;
636
+ }
637
+ isArchivedMessage(message) {
638
+ return "archivedAt" in message;
639
+ }
640
+ applyPopulate(query, populate) {
641
+ if (!populate) {
642
+ return query;
643
+ }
644
+ const items = Array.isArray(populate) ? populate : [populate];
645
+ let current = query;
646
+ for (const item of items) {
647
+ current = current.populate(item);
648
+ }
649
+ return current;
650
+ }
651
+ async findByIdWithOptions(model, id, options) {
652
+ const baseQuery = model.findById(id);
653
+ const selectedQuery = options.select === void 0 ? baseQuery : baseQuery.select(options.select);
654
+ const query = this.applyPopulate(selectedQuery, options.populate);
655
+ return query;
656
+ }
657
+ };
658
+
659
+ // src/route-factory.ts
660
+ var import_express_json_router = __toESM(require("@web-ts-toolkit/express-json-router"));
661
+ var ACTION_CD_PATTERN = /^[a-zA-Z0-9_-]+$/;
662
+ function assertValidActionCd(actionCd) {
663
+ if (typeof actionCd !== "string" || !ACTION_CD_PATTERN.test(actionCd)) {
664
+ throw new import_express_json_router.default.clientErrors.BadRequestError(
665
+ "actionCd must be a string of letters, digits, underscores, and hyphens"
666
+ );
667
+ }
668
+ }
669
+ function assertValidUsertype(usertype) {
670
+ if (usertype !== "sender" && usertype !== "receiver") {
671
+ throw new import_express_json_router.default.clientErrors.BadRequestError('usertype must be "sender" or "receiver"');
672
+ }
673
+ }
674
+ function mapServiceError(error) {
675
+ if (error instanceof MessageNotFoundError) {
676
+ throw new import_express_json_router.default.clientErrors.NotFoundError("message not found");
677
+ }
678
+ if (error instanceof TemplateNotFoundError) {
679
+ throw new import_express_json_router.default.clientErrors.NotFoundError(error.message);
680
+ }
681
+ if (error instanceof ActionNotFoundError) {
682
+ throw new import_express_json_router.default.clientErrors.NotFoundError(error.message);
683
+ }
684
+ if (error instanceof ActionNotAllowedError) {
685
+ throw new import_express_json_router.default.clientErrors.ForbiddenError(error.message);
686
+ }
687
+ if (error instanceof MessageArchivedError) {
688
+ throw new import_express_json_router.default.clientErrors.GoneError(error.message);
689
+ }
690
+ throw error;
691
+ }
692
+ function createMessageRoutes(options) {
693
+ const {
694
+ getModel,
695
+ paymentProvider = null,
696
+ adminRoles,
697
+ registry,
698
+ authMiddleware = [],
699
+ getUser = defaultGetUser,
700
+ getPermissions = defaultGetPermissions,
701
+ getIdentity = defaultGetIdentity,
702
+ adminPermissionKey = "is.admin"
703
+ } = options;
704
+ const service = new MessageService({ getModel, paymentProvider, adminRoles, registry });
705
+ const router = new import_express_json_router.default("", authMiddleware);
706
+ router.post("/new/:templateCd", async (req) => {
707
+ const templateCd = req.params.templateCd;
708
+ const user = getUser(req) || { _id: "" };
709
+ const roles = user.roles || [];
710
+ const identity = getIdentity(req);
711
+ const permissions = getPermissions(req);
712
+ const body = req.body || {};
713
+ const { clientRequestId, ...payload } = body;
714
+ const clientRequestIdStr = typeof clientRequestId === "string" ? clientRequestId : void 0;
715
+ try {
716
+ return await service.createMessage({
717
+ templateCd,
718
+ user,
719
+ roles,
720
+ identity,
721
+ permissions,
722
+ payload,
723
+ payerUser: user,
724
+ req,
725
+ clientRequestId: clientRequestIdStr
726
+ });
727
+ } catch (error) {
728
+ mapServiceError(error);
729
+ }
730
+ });
731
+ router.get("/:id/actions/:usertype", async (req) => {
732
+ const id = req.params.id;
733
+ const usertype = req.params.usertype;
734
+ assertValidUsertype(usertype);
735
+ const permissions = getPermissions(req);
736
+ const isAdmin = !!permissions[adminPermissionKey];
737
+ const user = getUser(req) || { _id: "" };
738
+ const result = await service.getActions(id, usertype, { permissions, user, isAdmin });
739
+ if (!result) {
740
+ throw new import_express_json_router.default.clientErrors.NotFoundError("message not found");
741
+ }
742
+ return result;
743
+ });
744
+ async function handleAction(req) {
745
+ const id = req.params.id;
746
+ const actionCd = req.params.actionCd;
747
+ assertValidActionCd(actionCd);
748
+ let message;
749
+ try {
750
+ message = await service.findMessageOrThrow(id);
751
+ } catch (error) {
752
+ mapServiceError(error);
753
+ }
754
+ const user = getUser(req) || { _id: "" };
755
+ const permissions = getPermissions(req);
756
+ try {
757
+ return await service.handleAction(message.templateCd, actionCd, { message, user, permissions, req });
758
+ } catch (error) {
759
+ mapServiceError(error);
760
+ }
761
+ }
762
+ router.get("/:id/action/:actionCd", handleAction);
763
+ router.post("/:id/action/:actionCd", handleAction);
764
+ return { router, service };
765
+ }
766
+ function defaultGetUser(req) {
767
+ const raw = req._user || req.user;
768
+ return raw;
769
+ }
770
+ function defaultGetPermissions(req) {
771
+ return req._permissions || {};
772
+ }
773
+ function defaultGetIdentity(req) {
774
+ return req._identity || {};
775
+ }
776
+ // Annotate the CommonJS export names for ESM import in node:
777
+ 0 && (module.exports = {
778
+ ActionNotAllowedError,
779
+ ActionNotFoundError,
780
+ BaseMessageFields,
781
+ GENERIC_NOTIFICATION_TEMPLATE_CD,
782
+ MESSAGE_ARCHIVE_MODEL_NAME,
783
+ MESSAGE_MODEL_NAME,
784
+ MessageArchiveSchema,
785
+ MessageArchivedError,
786
+ MessageContentSchema,
787
+ MessageNotFoundError,
788
+ MessageSchema,
789
+ MessageService,
790
+ NoopEmailProvider,
791
+ NoopPaymentProvider,
792
+ TemplateNotFoundError,
793
+ TemplateRegistry,
794
+ buildMessageArchiveSchema,
795
+ buildMessageSchema,
796
+ createMessageRoutes,
797
+ defaultRegistry,
798
+ filterActions,
799
+ includesAction,
800
+ interpolateTemplate,
801
+ isActionAllowed
802
+ });