@web-ts-toolkit/message-service 0.40.1 → 0.41.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 +161 -29
- package/index.d.mts +1118 -36
- package/index.d.ts +1118 -36
- package/index.js +1153 -157
- package/index.mjs +1139 -157
- package/package.json +8 -4
package/index.js
CHANGED
|
@@ -30,24 +30,38 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
ActionConflictError: () => ActionConflictError,
|
|
33
34
|
ActionNotAllowedError: () => ActionNotAllowedError,
|
|
34
35
|
ActionNotFoundError: () => ActionNotFoundError,
|
|
36
|
+
ActionNotificationPendingError: () => ActionNotificationPendingError,
|
|
37
|
+
ActionRetryableError: () => ActionRetryableError,
|
|
38
|
+
ActionTemplateMismatchError: () => ActionTemplateMismatchError,
|
|
35
39
|
BaseMessageFields: () => BaseMessageFields,
|
|
40
|
+
ClientRequestFailedError: () => ClientRequestFailedError,
|
|
41
|
+
ClientRequestInconsistentStateError: () => ClientRequestInconsistentStateError,
|
|
42
|
+
ClientRequestPendingError: () => ClientRequestPendingError,
|
|
36
43
|
GENERIC_NOTIFICATION_TEMPLATE_CD: () => GENERIC_NOTIFICATION_TEMPLATE_CD,
|
|
44
|
+
InvalidClientRequestIdError: () => InvalidClientRequestIdError,
|
|
45
|
+
InvalidMessageUserError: () => InvalidMessageUserError,
|
|
46
|
+
InvalidPaginationValueError: () => InvalidPaginationValueError,
|
|
37
47
|
MESSAGE_ARCHIVE_MODEL_NAME: () => MESSAGE_ARCHIVE_MODEL_NAME,
|
|
38
48
|
MESSAGE_MODEL_NAME: () => MESSAGE_MODEL_NAME,
|
|
39
49
|
MESSAGE_REQUEST_MODEL_NAME: () => MESSAGE_REQUEST_MODEL_NAME,
|
|
40
50
|
MessageArchiveSchema: () => MessageArchiveSchema,
|
|
41
51
|
MessageArchivedError: () => MessageArchivedError,
|
|
42
52
|
MessageContentSchema: () => MessageContentSchema,
|
|
53
|
+
MessageModelResolutionError: () => MessageModelResolutionError,
|
|
43
54
|
MessageNotFoundError: () => MessageNotFoundError,
|
|
44
55
|
MessageRequestSchema: () => MessageRequestSchema,
|
|
45
56
|
MessageSchema: () => MessageSchema,
|
|
46
57
|
MessageService: () => MessageService,
|
|
58
|
+
MessageTransactionRequiredError: () => MessageTransactionRequiredError,
|
|
47
59
|
NoopEmailProvider: () => NoopEmailProvider,
|
|
48
60
|
NoopPaymentProvider: () => NoopPaymentProvider,
|
|
61
|
+
PaymentSessionCompensationError: () => PaymentSessionCompensationError,
|
|
49
62
|
TemplateNotFoundError: () => TemplateNotFoundError,
|
|
50
63
|
TemplateRegistry: () => TemplateRegistry,
|
|
64
|
+
TemplateRegistryValidationError: () => TemplateRegistryValidationError,
|
|
51
65
|
buildMessageArchiveSchema: () => buildMessageArchiveSchema,
|
|
52
66
|
buildMessageRequestSchema: () => buildMessageRequestSchema,
|
|
53
67
|
buildMessageSchema: () => buildMessageSchema,
|
|
@@ -90,13 +104,105 @@ var BaseMessageFields = {
|
|
|
90
104
|
payload: { type: import_mongoose.default.Schema.Types.Mixed, default: {} },
|
|
91
105
|
display: { type: import_mongoose.default.Schema.Types.Mixed, default: {} },
|
|
92
106
|
clientRequestId: { type: String, default: null },
|
|
93
|
-
|
|
107
|
+
clientRequestOwnerId: { type: String, default: null },
|
|
108
|
+
clientRequestItemIndex: { type: Number, default: null },
|
|
109
|
+
actionState: { type: String, enum: ["active", "processing", "retryable"], default: "active" },
|
|
110
|
+
actionCd: { type: String, default: null },
|
|
111
|
+
actionAttemptId: { type: String, default: null },
|
|
112
|
+
actionClaimedBy: { type: String, default: null },
|
|
113
|
+
actionClaimedAt: { type: Date, default: null },
|
|
114
|
+
actionLeaseExpiresAt: { type: Date, default: null },
|
|
115
|
+
actionFailureMessage: { type: String, default: null }
|
|
94
116
|
};
|
|
95
117
|
|
|
96
118
|
// src/schemas/message.ts
|
|
97
119
|
var import_mongoose2 = __toESM(require("mongoose"));
|
|
98
120
|
|
|
121
|
+
// src/runtime-contract.ts
|
|
122
|
+
var ERROR_BRAND_PREFIX = "@web-ts-toolkit/message-service/error/";
|
|
123
|
+
function markRuntimeError(error, name) {
|
|
124
|
+
Object.defineProperty(error, /* @__PURE__ */ Symbol.for(`${ERROR_BRAND_PREFIX}${name}`), {
|
|
125
|
+
configurable: false,
|
|
126
|
+
enumerable: false,
|
|
127
|
+
value: true
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function isRuntimeError(value, name) {
|
|
131
|
+
if (!value || typeof value !== "object") return false;
|
|
132
|
+
return value.name === name && value[/* @__PURE__ */ Symbol.for(`${ERROR_BRAND_PREFIX}${name}`)] === true;
|
|
133
|
+
}
|
|
134
|
+
|
|
99
135
|
// src/template-registry.ts
|
|
136
|
+
var TemplateRegistryValidationError = class extends Error {
|
|
137
|
+
static [Symbol.hasInstance](value) {
|
|
138
|
+
return isRuntimeError(value, "TemplateRegistryValidationError");
|
|
139
|
+
}
|
|
140
|
+
constructor(message) {
|
|
141
|
+
super(message);
|
|
142
|
+
this.name = "TemplateRegistryValidationError";
|
|
143
|
+
markRuntimeError(this, this.name);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
function requireNonEmptyString(value, field, templateCd) {
|
|
147
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
148
|
+
throw new TemplateRegistryValidationError(
|
|
149
|
+
`${templateCd ? `template "${templateCd}" ` : ""}${field} must be a non-empty string`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
return value;
|
|
153
|
+
}
|
|
154
|
+
function validateTemplate(template) {
|
|
155
|
+
const templateCd = requireNonEmptyString(template.templateCd, "templateCd");
|
|
156
|
+
if (!Array.isArray(template.actions)) {
|
|
157
|
+
throw new TemplateRegistryValidationError(`template "${templateCd}" actions must be an array`);
|
|
158
|
+
}
|
|
159
|
+
const seenActionCodes = /* @__PURE__ */ new Set();
|
|
160
|
+
const defaultByUsertype = /* @__PURE__ */ new Map();
|
|
161
|
+
for (const action of template.actions) {
|
|
162
|
+
const actionCd = requireNonEmptyString(action.actionCd, "actionCd", templateCd);
|
|
163
|
+
if (seenActionCodes.has(actionCd)) {
|
|
164
|
+
throw new TemplateRegistryValidationError(`template "${templateCd}" has duplicate actionCd "${actionCd}"`);
|
|
165
|
+
}
|
|
166
|
+
seenActionCodes.add(actionCd);
|
|
167
|
+
requireNonEmptyString(action.name, `action "${actionCd}" name`, templateCd);
|
|
168
|
+
requireNonEmptyString(action.variant, `action "${actionCd}" variant`, templateCd);
|
|
169
|
+
if (!action.sender && !action.receiver) {
|
|
170
|
+
throw new TemplateRegistryValidationError(
|
|
171
|
+
`template "${templateCd}" action "${actionCd}" must be available to sender or receiver`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
if (action.isDefault) {
|
|
175
|
+
for (const usertype of ["sender", "receiver"]) {
|
|
176
|
+
if (!action[usertype]) continue;
|
|
177
|
+
const existingDefault = defaultByUsertype.get(usertype);
|
|
178
|
+
if (existingDefault) {
|
|
179
|
+
throw new TemplateRegistryValidationError(
|
|
180
|
+
`template "${templateCd}" has multiple default ${usertype} actions: "${existingDefault}" and "${actionCd}"`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
defaultByUsertype.set(usertype, actionCd);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function snapshotTemplate(template) {
|
|
189
|
+
const snapshot = {
|
|
190
|
+
...template,
|
|
191
|
+
senderContent: Object.freeze({ ...template.senderContent }),
|
|
192
|
+
receiverContent: Object.freeze({ ...template.receiverContent }),
|
|
193
|
+
uiTemplate: typeof template.uiTemplate === "string" ? template.uiTemplate : Object.freeze({ ...template.uiTemplate }),
|
|
194
|
+
actions: Object.freeze(
|
|
195
|
+
template.actions.map(
|
|
196
|
+
(action) => Object.freeze({
|
|
197
|
+
...action,
|
|
198
|
+
confirmation: action.confirmation ? Object.freeze({ ...action.confirmation }) : void 0,
|
|
199
|
+
payload: action.payload ? Object.freeze({ ...action.payload }) : void 0
|
|
200
|
+
})
|
|
201
|
+
)
|
|
202
|
+
)
|
|
203
|
+
};
|
|
204
|
+
return Object.freeze(snapshot);
|
|
205
|
+
}
|
|
100
206
|
var TemplateRegistry = class {
|
|
101
207
|
constructor() {
|
|
102
208
|
this.templates = /* @__PURE__ */ new Map();
|
|
@@ -105,7 +211,8 @@ var TemplateRegistry = class {
|
|
|
105
211
|
* Register a template. Overwrites if templateCd already exists.
|
|
106
212
|
*/
|
|
107
213
|
register(template) {
|
|
108
|
-
|
|
214
|
+
validateTemplate(template);
|
|
215
|
+
this.templates.set(template.templateCd, snapshotTemplate(template));
|
|
109
216
|
}
|
|
110
217
|
/**
|
|
111
218
|
* Register multiple templates at once.
|
|
@@ -152,7 +259,9 @@ var TemplateRegistry = class {
|
|
|
152
259
|
this.templates.clear();
|
|
153
260
|
}
|
|
154
261
|
};
|
|
155
|
-
var
|
|
262
|
+
var DEFAULT_REGISTRY_KEY = /* @__PURE__ */ Symbol.for("@web-ts-toolkit/message-service/defaultRegistry");
|
|
263
|
+
var registryGlobal = globalThis;
|
|
264
|
+
var defaultRegistry = registryGlobal[DEFAULT_REGISTRY_KEY] ??= new TemplateRegistry();
|
|
156
265
|
function includesAction(templateCd, actionCd, registry) {
|
|
157
266
|
const template = registry.find(templateCd);
|
|
158
267
|
if (!template) return false;
|
|
@@ -161,20 +270,53 @@ function includesAction(templateCd, actionCd, registry) {
|
|
|
161
270
|
|
|
162
271
|
// src/schemas/methods.ts
|
|
163
272
|
function isSender(user) {
|
|
164
|
-
|
|
273
|
+
const fromUser = normalizeStoredUserId(this.fromUser);
|
|
274
|
+
const userId = normalizeMessageUserId(user);
|
|
275
|
+
return fromUser !== null && userId !== null && fromUser === userId;
|
|
165
276
|
}
|
|
166
277
|
function isReceiver(user) {
|
|
167
|
-
const
|
|
168
|
-
|
|
278
|
+
const toUser = normalizeStoredUserId(this.toUser);
|
|
279
|
+
const userId = normalizeMessageUserId(user);
|
|
280
|
+
return toUser !== null && userId !== null && toUser === userId || this.toRoles.some((r) => user.roles?.includes(r));
|
|
281
|
+
}
|
|
282
|
+
function normalizeMessageUserId(user) {
|
|
283
|
+
return normalizeStoredUserId(user?._id ?? null);
|
|
284
|
+
}
|
|
285
|
+
function normalizeStoredUserId(value) {
|
|
286
|
+
if (value === null || value === void 0) return null;
|
|
287
|
+
if (typeof value === "string") {
|
|
288
|
+
const trimmed = value.trim();
|
|
289
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
290
|
+
}
|
|
291
|
+
return String(value);
|
|
169
292
|
}
|
|
170
293
|
|
|
171
294
|
// src/schemas/message.ts
|
|
295
|
+
function resolveDocumentModel(document, modelName, role) {
|
|
296
|
+
const model = document.constructor;
|
|
297
|
+
const connection = model.db;
|
|
298
|
+
if (!connection) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
`message-service: cannot resolve ${role} model "${modelName}" because the hydrated document has no owning connection`
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
return connection.model(modelName);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
const connectionName = connection.name || "<unnamed>";
|
|
307
|
+
const resolutionError = new Error(
|
|
308
|
+
`message-service: ${role} model "${modelName}" is not registered on Mongoose connection "${connectionName}"`
|
|
309
|
+
);
|
|
310
|
+
resolutionError.cause = error;
|
|
311
|
+
throw resolutionError;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
172
314
|
function createArchiveMethod(ctx) {
|
|
173
315
|
return function archive(actionCd, archivedBy, registry) {
|
|
174
316
|
if (!includesAction(this.templateCd, actionCd, registry) || !archivedBy) {
|
|
175
317
|
return Promise.resolve();
|
|
176
318
|
}
|
|
177
|
-
const MessageArchive =
|
|
319
|
+
const MessageArchive = resolveDocumentModel(this, ctx.archiveModelName, "archive");
|
|
178
320
|
const data = this.toObject();
|
|
179
321
|
return MessageArchive.create({
|
|
180
322
|
...data,
|
|
@@ -185,60 +327,118 @@ function createArchiveMethod(ctx) {
|
|
|
185
327
|
});
|
|
186
328
|
};
|
|
187
329
|
}
|
|
188
|
-
|
|
330
|
+
var EMAIL_WAS_NEW_LOCAL = "messageServiceEmailWasNew";
|
|
331
|
+
function normalizeEmailTitle(value) {
|
|
332
|
+
return value.trim().toLowerCase();
|
|
333
|
+
}
|
|
334
|
+
function createEmailStateCaptureHook() {
|
|
335
|
+
return function captureEmailState() {
|
|
336
|
+
this.$locals[EMAIL_WAS_NEW_LOCAL] = this.isNew;
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
function createPostSaveEmailHook(ctx) {
|
|
189
340
|
return async function sendNotificationEmail() {
|
|
341
|
+
if (!this.$locals[EMAIL_WAS_NEW_LOCAL]) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (this.$session()) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
190
347
|
if (!this.toUser || !this.receiverContent?.title) {
|
|
191
348
|
return;
|
|
192
349
|
}
|
|
193
350
|
const title = this.receiverContent.title.trim();
|
|
194
|
-
if (ctx.emailNotificationExclusions.includes(title
|
|
351
|
+
if (!title || ctx.emailNotificationExclusions.includes(normalizeEmailTitle(title))) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
let user;
|
|
355
|
+
try {
|
|
356
|
+
const User = resolveDocumentModel(this, ctx.userModelName, "user");
|
|
357
|
+
user = await User.findById(this.toUser).select("email").lean();
|
|
358
|
+
} catch (error) {
|
|
359
|
+
await reportEmailFailure(ctx, "recipientLookup", error, this, title);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (!user?.email) {
|
|
195
363
|
return;
|
|
196
364
|
}
|
|
365
|
+
const long = this.receiverContent.long || "";
|
|
366
|
+
const short = this.receiverContent.short || "";
|
|
367
|
+
const body = long.length > short.length ? long : short;
|
|
197
368
|
try {
|
|
198
|
-
const User = import_mongoose2.default.model(ctx.userModelName);
|
|
199
|
-
const user = await User.findById(this.toUser).select("email").lean();
|
|
200
|
-
if (!user?.email) {
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
const long = this.receiverContent.long || "";
|
|
204
|
-
const short = this.receiverContent.short || "";
|
|
205
|
-
const body = long.length > short.length ? long : short;
|
|
206
369
|
await ctx.emailNotifier(user.email, title, body);
|
|
207
|
-
} catch {
|
|
370
|
+
} catch (error) {
|
|
371
|
+
await reportEmailFailure(ctx, "notifier", error, this, title);
|
|
208
372
|
}
|
|
209
373
|
};
|
|
210
374
|
}
|
|
211
|
-
function
|
|
212
|
-
|
|
375
|
+
async function reportEmailFailure(ctx, stage, error, message, title) {
|
|
376
|
+
try {
|
|
377
|
+
await ctx.onEmailDeliveryFailure?.({
|
|
378
|
+
stage,
|
|
379
|
+
error,
|
|
380
|
+
messageId: message._id,
|
|
381
|
+
recipientId: message.toUser,
|
|
382
|
+
title
|
|
383
|
+
});
|
|
384
|
+
} catch {
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
function assertModelRegistered(connection, name, role) {
|
|
388
|
+
if (!connection.modelNames().includes(name)) {
|
|
389
|
+
const connectionName = "name" in connection && connection.name ? connection.name : "global mongoose";
|
|
213
390
|
throw new Error(
|
|
214
|
-
`message-service: cannot configure schema \u2014 ${role} model "${name}" is not registered
|
|
391
|
+
`message-service: cannot configure schema \u2014 ${role} model "${name}" is not registered on Mongoose connection "${connectionName}". Register the model on that connection before buildMessageSchema().`
|
|
215
392
|
);
|
|
216
393
|
}
|
|
217
394
|
}
|
|
218
395
|
function resolveConfig(config) {
|
|
219
396
|
return {
|
|
220
397
|
emailNotifier: config?.emailNotifier ?? null,
|
|
221
|
-
|
|
398
|
+
onEmailDeliveryFailure: config?.onEmailDeliveryFailure,
|
|
399
|
+
emailNotificationExclusions: (config?.emailNotificationExclusions ?? []).map(normalizeEmailTitle),
|
|
222
400
|
userModelName: config?.userModelName ?? "User",
|
|
223
|
-
archiveModelName: config?.archiveModelName ?? MESSAGE_ARCHIVE_MODEL_NAME
|
|
401
|
+
archiveModelName: config?.archiveModelName ?? MESSAGE_ARCHIVE_MODEL_NAME,
|
|
402
|
+
connection: config?.connection ?? import_mongoose2.default
|
|
224
403
|
};
|
|
225
404
|
}
|
|
226
405
|
function buildMessageSchema(config) {
|
|
227
406
|
const resolved = resolveConfig(config);
|
|
228
407
|
if (resolved.emailNotifier) {
|
|
229
|
-
assertModelRegistered(resolved.userModelName, "user");
|
|
408
|
+
assertModelRegistered(resolved.connection, resolved.userModelName, "user");
|
|
230
409
|
}
|
|
231
410
|
const schema = new import_mongoose2.default.Schema(BaseMessageFields, {
|
|
232
411
|
timestamps: true
|
|
233
412
|
});
|
|
234
|
-
schema.index({ createdAt: 1 });
|
|
235
|
-
schema.index({
|
|
413
|
+
schema.index({ fromUser: 1, createdAt: -1, _id: -1 });
|
|
414
|
+
schema.index({ toUser: 1, createdAt: -1, _id: -1 });
|
|
415
|
+
schema.index({ toRoles: 1, createdAt: -1, _id: -1 });
|
|
416
|
+
schema.index({ actionState: 1, actionLeaseExpiresAt: 1 });
|
|
236
417
|
schema.index(
|
|
237
|
-
{
|
|
418
|
+
{ actionAttemptId: 1 },
|
|
238
419
|
{
|
|
239
420
|
unique: true,
|
|
421
|
+
partialFilterExpression: { actionAttemptId: { $type: "string" } }
|
|
422
|
+
}
|
|
423
|
+
);
|
|
424
|
+
schema.index(
|
|
425
|
+
{ clientRequestOwnerId: 1, templateCd: 1, clientRequestId: 1, createdAt: 1, _id: 1 },
|
|
426
|
+
{
|
|
240
427
|
partialFilterExpression: {
|
|
241
428
|
clientRequestId: { $type: "string" },
|
|
429
|
+
clientRequestOwnerId: { $type: "string" },
|
|
430
|
+
templateCd: { $type: "string" }
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
);
|
|
434
|
+
schema.index(
|
|
435
|
+
{ clientRequestOwnerId: 1, templateCd: 1, clientRequestId: 1, clientRequestItemIndex: 1 },
|
|
436
|
+
{
|
|
437
|
+
unique: true,
|
|
438
|
+
partialFilterExpression: {
|
|
439
|
+
clientRequestId: { $type: "string" },
|
|
440
|
+
clientRequestOwnerId: { $type: "string" },
|
|
441
|
+
templateCd: { $type: "string" },
|
|
242
442
|
clientRequestItemIndex: { $type: "number" }
|
|
243
443
|
}
|
|
244
444
|
}
|
|
@@ -247,10 +447,12 @@ function buildMessageSchema(config) {
|
|
|
247
447
|
schema.methods.isReceiver = isReceiver;
|
|
248
448
|
schema.methods.archive = createArchiveMethod({ archiveModelName: resolved.archiveModelName });
|
|
249
449
|
if (resolved.emailNotifier) {
|
|
250
|
-
schema.pre(
|
|
450
|
+
schema.pre("save", createEmailStateCaptureHook());
|
|
451
|
+
schema.post(
|
|
251
452
|
"save",
|
|
252
|
-
|
|
453
|
+
createPostSaveEmailHook({
|
|
253
454
|
emailNotifier: resolved.emailNotifier,
|
|
455
|
+
onEmailDeliveryFailure: resolved.onEmailDeliveryFailure,
|
|
254
456
|
emailNotificationExclusions: resolved.emailNotificationExclusions,
|
|
255
457
|
userModelName: resolved.userModelName
|
|
256
458
|
})
|
|
@@ -268,7 +470,11 @@ function buildMessageArchiveSchema() {
|
|
|
268
470
|
...BaseMessageFields,
|
|
269
471
|
actionCd: { type: String, default: "" },
|
|
270
472
|
archivedBy: { type: import_mongoose3.default.Schema.Types.ObjectId, default: null, index: true },
|
|
271
|
-
archivedAt: { type: Date, default: Date.now }
|
|
473
|
+
archivedAt: { type: Date, default: Date.now },
|
|
474
|
+
actionAttemptId: { type: String, default: null, index: true },
|
|
475
|
+
actionNotificationState: { type: String, enum: ["none", "pending", "sent", "failed"], default: "none" },
|
|
476
|
+
actionNotificationError: { type: String, default: null },
|
|
477
|
+
actionNotificationAttemptedAt: { type: Date, default: null }
|
|
272
478
|
},
|
|
273
479
|
{
|
|
274
480
|
timestamps: true
|
|
@@ -287,15 +493,23 @@ function buildMessageRequestSchema() {
|
|
|
287
493
|
const schema = new import_mongoose4.default.Schema(
|
|
288
494
|
{
|
|
289
495
|
clientRequestId: { type: String, required: true },
|
|
290
|
-
|
|
291
|
-
|
|
496
|
+
clientRequestOwnerId: { type: String, required: true },
|
|
497
|
+
templateCd: { type: String, required: true },
|
|
498
|
+
state: { type: String, enum: ["pending", "completed", "failed"], default: "pending" },
|
|
499
|
+
itemCount: { type: Number, default: null },
|
|
500
|
+
leaseOwnerId: { type: String, default: null },
|
|
501
|
+
leaseExpiresAt: { type: Date, default: null },
|
|
502
|
+
completedAt: { type: Date, default: null },
|
|
503
|
+
failedAt: { type: Date, default: null },
|
|
504
|
+
failureMessage: { type: String, default: null }
|
|
292
505
|
},
|
|
293
506
|
{
|
|
294
507
|
timestamps: true
|
|
295
508
|
}
|
|
296
509
|
);
|
|
297
|
-
schema.index({ clientRequestId: 1 }, { unique: true });
|
|
510
|
+
schema.index({ clientRequestOwnerId: 1, templateCd: 1, clientRequestId: 1 }, { unique: true });
|
|
298
511
|
schema.index({ updatedAt: 1 });
|
|
512
|
+
schema.index({ state: 1, leaseExpiresAt: 1 });
|
|
299
513
|
return schema;
|
|
300
514
|
}
|
|
301
515
|
var MessageRequestSchema = buildMessageRequestSchema();
|
|
@@ -386,52 +600,254 @@ var NoopPaymentProvider = class {
|
|
|
386
600
|
|
|
387
601
|
// src/message-service.ts
|
|
388
602
|
var import_utils = require("@web-ts-toolkit/utils");
|
|
603
|
+
var import_node_crypto = require("crypto");
|
|
604
|
+
var import_mongoose5 = __toESM(require("mongoose"));
|
|
389
605
|
var DEFAULT_LIST_LIMIT = 50;
|
|
390
606
|
var MAX_LIST_LIMIT = 100;
|
|
391
607
|
var DUPLICATE_KEY_ERROR_CODE = 11e3;
|
|
392
|
-
var
|
|
393
|
-
var
|
|
608
|
+
var CLIENT_REQUEST_LEASE_MS = 3e4;
|
|
609
|
+
var CLIENT_REQUEST_WAIT_MS = 5e3;
|
|
610
|
+
var CLIENT_REQUEST_POLL_MS = 200;
|
|
611
|
+
var ACTION_LEASE_MS = 3e4;
|
|
612
|
+
var MAX_CLIENT_REQUEST_ID_LENGTH = 128;
|
|
613
|
+
var TRANSACTION_REQUIRED_MESSAGE = "message-service requires MongoDB replica set or sharded-cluster transactions for idempotent batch creation";
|
|
614
|
+
var DEFAULT_MODEL_NAMES = {
|
|
615
|
+
active: MESSAGE_MODEL_NAME,
|
|
616
|
+
archive: MESSAGE_ARCHIVE_MODEL_NAME,
|
|
617
|
+
request: MESSAGE_REQUEST_MODEL_NAME,
|
|
618
|
+
user: "User"
|
|
619
|
+
};
|
|
394
620
|
var GENERIC_NOTIFICATION_TEMPLATE_CD = "__generic-notification__";
|
|
395
621
|
var MessageNotFoundError = class extends Error {
|
|
622
|
+
static [Symbol.hasInstance](value) {
|
|
623
|
+
return isRuntimeError(value, "MessageNotFoundError");
|
|
624
|
+
}
|
|
396
625
|
constructor(messageId) {
|
|
397
626
|
super(`message "${messageId}" not found`);
|
|
398
627
|
this.name = "MessageNotFoundError";
|
|
628
|
+
markRuntimeError(this, this.name);
|
|
399
629
|
}
|
|
400
630
|
};
|
|
401
631
|
var MessageArchivedError = class extends Error {
|
|
632
|
+
static [Symbol.hasInstance](value) {
|
|
633
|
+
return isRuntimeError(value, "MessageArchivedError");
|
|
634
|
+
}
|
|
402
635
|
constructor(messageId) {
|
|
403
636
|
super(`message "${messageId}" is archived`);
|
|
404
637
|
this.name = "MessageArchivedError";
|
|
638
|
+
markRuntimeError(this, this.name);
|
|
405
639
|
}
|
|
406
640
|
};
|
|
407
641
|
var TemplateNotFoundError = class extends Error {
|
|
642
|
+
static [Symbol.hasInstance](value) {
|
|
643
|
+
return isRuntimeError(value, "TemplateNotFoundError");
|
|
644
|
+
}
|
|
408
645
|
constructor(templateCd) {
|
|
409
646
|
super(`template "${templateCd}" not found`);
|
|
410
647
|
this.name = "TemplateNotFoundError";
|
|
648
|
+
markRuntimeError(this, this.name);
|
|
411
649
|
}
|
|
412
650
|
};
|
|
413
651
|
var ActionNotFoundError = class extends Error {
|
|
652
|
+
static [Symbol.hasInstance](value) {
|
|
653
|
+
return isRuntimeError(value, "ActionNotFoundError");
|
|
654
|
+
}
|
|
414
655
|
constructor(templateCd, actionCd) {
|
|
415
656
|
super(`action "${actionCd}" not found in template "${templateCd}"`);
|
|
416
657
|
this.name = "ActionNotFoundError";
|
|
658
|
+
markRuntimeError(this, this.name);
|
|
417
659
|
}
|
|
418
660
|
};
|
|
419
661
|
var ActionNotAllowedError = class extends Error {
|
|
662
|
+
static [Symbol.hasInstance](value) {
|
|
663
|
+
return isRuntimeError(value, "ActionNotAllowedError");
|
|
664
|
+
}
|
|
420
665
|
constructor() {
|
|
421
666
|
super("not allowed");
|
|
422
667
|
this.name = "ActionNotAllowedError";
|
|
668
|
+
markRuntimeError(this, this.name);
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
var InvalidMessageUserError = class extends Error {
|
|
672
|
+
static [Symbol.hasInstance](value) {
|
|
673
|
+
return isRuntimeError(value, "InvalidMessageUserError");
|
|
674
|
+
}
|
|
675
|
+
constructor(message = "user._id must be a non-empty string or ObjectId") {
|
|
676
|
+
super(message);
|
|
677
|
+
this.name = "InvalidMessageUserError";
|
|
678
|
+
markRuntimeError(this, this.name);
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
var ActionTemplateMismatchError = class extends Error {
|
|
682
|
+
static [Symbol.hasInstance](value) {
|
|
683
|
+
return isRuntimeError(value, "ActionTemplateMismatchError");
|
|
684
|
+
}
|
|
685
|
+
constructor(expectedTemplateCd, receivedTemplateCd) {
|
|
686
|
+
super(`message template "${expectedTemplateCd}" does not match requested template "${receivedTemplateCd}"`);
|
|
687
|
+
this.name = "ActionTemplateMismatchError";
|
|
688
|
+
markRuntimeError(this, this.name);
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
var ActionConflictError = class extends Error {
|
|
692
|
+
static [Symbol.hasInstance](value) {
|
|
693
|
+
return isRuntimeError(value, "ActionConflictError");
|
|
694
|
+
}
|
|
695
|
+
constructor(messageId) {
|
|
696
|
+
super(`message "${messageId}" already has an action in progress`);
|
|
697
|
+
this.name = "ActionConflictError";
|
|
698
|
+
markRuntimeError(this, this.name);
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
var ActionRetryableError = class extends Error {
|
|
702
|
+
static [Symbol.hasInstance](value) {
|
|
703
|
+
return isRuntimeError(value, "ActionRetryableError");
|
|
704
|
+
}
|
|
705
|
+
constructor(messageId, actionAttemptId, cause) {
|
|
706
|
+
super(`message "${messageId}" action attempt "${actionAttemptId}" failed before commit and may be retried`);
|
|
707
|
+
this.name = "ActionRetryableError";
|
|
708
|
+
markRuntimeError(this, this.name);
|
|
709
|
+
this.actionAttemptId = actionAttemptId;
|
|
710
|
+
if (cause) {
|
|
711
|
+
this.cause = cause;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
var ActionNotificationPendingError = class extends Error {
|
|
716
|
+
static [Symbol.hasInstance](value) {
|
|
717
|
+
return isRuntimeError(value, "ActionNotificationPendingError");
|
|
718
|
+
}
|
|
719
|
+
constructor(messageId, actionAttemptId, result, cause) {
|
|
720
|
+
super(`message "${messageId}" action committed but sender notification is pending`);
|
|
721
|
+
this.name = "ActionNotificationPendingError";
|
|
722
|
+
markRuntimeError(this, this.name);
|
|
723
|
+
this.actionAttemptId = actionAttemptId;
|
|
724
|
+
this.result = result;
|
|
725
|
+
if (cause) {
|
|
726
|
+
this.cause = cause;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
var InvalidClientRequestIdError = class extends Error {
|
|
731
|
+
static [Symbol.hasInstance](value) {
|
|
732
|
+
return isRuntimeError(value, "InvalidClientRequestIdError");
|
|
733
|
+
}
|
|
734
|
+
constructor(message) {
|
|
735
|
+
super(message);
|
|
736
|
+
this.name = "InvalidClientRequestIdError";
|
|
737
|
+
markRuntimeError(this, this.name);
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
var InvalidPaginationValueError = class extends Error {
|
|
741
|
+
static [Symbol.hasInstance](value) {
|
|
742
|
+
return isRuntimeError(value, "InvalidPaginationValueError");
|
|
743
|
+
}
|
|
744
|
+
constructor(message) {
|
|
745
|
+
super(message);
|
|
746
|
+
this.name = "InvalidPaginationValueError";
|
|
747
|
+
markRuntimeError(this, this.name);
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
var ClientRequestPendingError = class extends Error {
|
|
751
|
+
static [Symbol.hasInstance](value) {
|
|
752
|
+
return isRuntimeError(value, "ClientRequestPendingError");
|
|
753
|
+
}
|
|
754
|
+
constructor(clientRequestId) {
|
|
755
|
+
super(
|
|
756
|
+
`clientRequestId "${clientRequestId}" is still pending; retry after the current reservation completes or its lease expires`
|
|
757
|
+
);
|
|
758
|
+
this.name = "ClientRequestPendingError";
|
|
759
|
+
markRuntimeError(this, this.name);
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
var ClientRequestFailedError = class extends Error {
|
|
763
|
+
static [Symbol.hasInstance](value) {
|
|
764
|
+
return isRuntimeError(value, "ClientRequestFailedError");
|
|
765
|
+
}
|
|
766
|
+
constructor(clientRequestId, reason) {
|
|
767
|
+
super(`clientRequestId "${clientRequestId}" previously failed${reason ? `: ${reason}` : ""}`);
|
|
768
|
+
this.name = "ClientRequestFailedError";
|
|
769
|
+
markRuntimeError(this, this.name);
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
var ClientRequestInconsistentStateError = class extends Error {
|
|
773
|
+
static [Symbol.hasInstance](value) {
|
|
774
|
+
return isRuntimeError(value, "ClientRequestInconsistentStateError");
|
|
775
|
+
}
|
|
776
|
+
constructor(clientRequestId, message) {
|
|
777
|
+
super(`clientRequestId "${clientRequestId}" has inconsistent persisted state: ${message}`);
|
|
778
|
+
this.name = "ClientRequestInconsistentStateError";
|
|
779
|
+
markRuntimeError(this, this.name);
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
var MessageTransactionRequiredError = class extends Error {
|
|
783
|
+
static [Symbol.hasInstance](value) {
|
|
784
|
+
return isRuntimeError(value, "MessageTransactionRequiredError");
|
|
785
|
+
}
|
|
786
|
+
constructor(cause) {
|
|
787
|
+
super(TRANSACTION_REQUIRED_MESSAGE);
|
|
788
|
+
this.name = "MessageTransactionRequiredError";
|
|
789
|
+
markRuntimeError(this, this.name);
|
|
790
|
+
if (cause) {
|
|
791
|
+
this.cause = cause;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
var MessageModelResolutionError = class extends Error {
|
|
796
|
+
static [Symbol.hasInstance](value) {
|
|
797
|
+
return isRuntimeError(value, "MessageModelResolutionError");
|
|
798
|
+
}
|
|
799
|
+
constructor(role, modelName, connectionName, cause) {
|
|
800
|
+
super(`message-service could not resolve ${role} model "${modelName}" on ${connectionName}`);
|
|
801
|
+
this.name = "MessageModelResolutionError";
|
|
802
|
+
markRuntimeError(this, this.name);
|
|
803
|
+
this.role = role;
|
|
804
|
+
this.modelName = modelName;
|
|
805
|
+
this.connectionName = connectionName;
|
|
806
|
+
if (cause) {
|
|
807
|
+
this.cause = cause;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
};
|
|
811
|
+
var PaymentSessionCompensationError = class extends Error {
|
|
812
|
+
static [Symbol.hasInstance](value) {
|
|
813
|
+
return isRuntimeError(value, "PaymentSessionCompensationError");
|
|
814
|
+
}
|
|
815
|
+
constructor(sessionId, operation, compensationError, originalError, hookError) {
|
|
816
|
+
super(`payment session compensation failed for session "${sessionId}" during ${operation}`);
|
|
817
|
+
this.name = "PaymentSessionCompensationError";
|
|
818
|
+
markRuntimeError(this, this.name);
|
|
819
|
+
this.sessionId = sessionId;
|
|
820
|
+
this.operation = operation;
|
|
821
|
+
this.compensationError = compensationError;
|
|
822
|
+
this.originalError = originalError;
|
|
823
|
+
this.hookError = hookError;
|
|
824
|
+
this.cause = compensationError;
|
|
423
825
|
}
|
|
424
826
|
};
|
|
425
827
|
var MessageService = class {
|
|
426
828
|
constructor(options) {
|
|
427
829
|
this.getModel = options.getModel;
|
|
830
|
+
this.connection = options.connection;
|
|
831
|
+
this.modelNames = { ...DEFAULT_MODEL_NAMES, ...options.modelNames };
|
|
428
832
|
this.paymentProvider = options.paymentProvider ?? null;
|
|
429
833
|
this.expirePaymentSession = this.paymentProvider?.expireSession.bind(this.paymentProvider);
|
|
430
834
|
this.refundPaymentSession = this.paymentProvider?.refundPayment.bind(this.paymentProvider);
|
|
835
|
+
this.onPaymentCompensationFailure = options.onPaymentCompensationFailure;
|
|
431
836
|
this.adminRoles = options.adminRoles ?? [];
|
|
432
837
|
this.registry = options.registry ?? defaultRegistry;
|
|
433
|
-
this.
|
|
434
|
-
this.
|
|
838
|
+
this.maxListLimit = this.validatePositiveIntegerOption("maxListLimit", options.maxListLimit ?? MAX_LIST_LIMIT);
|
|
839
|
+
this.defaultListLimit = this.validatePositiveIntegerOption(
|
|
840
|
+
"defaultListLimit",
|
|
841
|
+
options.defaultListLimit ?? DEFAULT_LIST_LIMIT
|
|
842
|
+
);
|
|
843
|
+
if (this.defaultListLimit > this.maxListLimit) {
|
|
844
|
+
throw new InvalidPaginationValueError("defaultListLimit must be less than or equal to maxListLimit");
|
|
845
|
+
}
|
|
846
|
+
this.clientRequestLeaseMs = options.clientRequestLeaseMs ?? CLIENT_REQUEST_LEASE_MS;
|
|
847
|
+
this.clientRequestWaitMs = options.clientRequestWaitMs ?? CLIENT_REQUEST_WAIT_MS;
|
|
848
|
+
this.clientRequestPollMs = options.clientRequestPollMs ?? CLIENT_REQUEST_POLL_MS;
|
|
849
|
+
this.clientRequestDelay = options.clientRequestDelay ?? this.delay;
|
|
850
|
+
this.actionLeaseMs = ACTION_LEASE_MS;
|
|
435
851
|
}
|
|
436
852
|
// -------------------------------------------------------------------------
|
|
437
853
|
// Message lookup
|
|
@@ -441,8 +857,8 @@ var MessageService = class {
|
|
|
441
857
|
* the message does not exist in either collection.
|
|
442
858
|
*/
|
|
443
859
|
async findMessage(messageId, options = {}) {
|
|
444
|
-
const Message = this.
|
|
445
|
-
const MessageArchive = this.
|
|
860
|
+
const Message = this.resolveModel("active");
|
|
861
|
+
const MessageArchive = this.resolveModel("archive");
|
|
446
862
|
const message = await this.findByIdWithOptions(Message, messageId, options);
|
|
447
863
|
if (message) return message;
|
|
448
864
|
return await this.findByIdWithOptions(
|
|
@@ -473,7 +889,8 @@ var MessageService = class {
|
|
|
473
889
|
req,
|
|
474
890
|
clientRequestId
|
|
475
891
|
} = params;
|
|
476
|
-
|
|
892
|
+
const normalizedClientRequestId = this.normalizeClientRequestId(clientRequestId);
|
|
893
|
+
if (normalizedClientRequestId) {
|
|
477
894
|
return this.createMessageWithReservation({
|
|
478
895
|
templateCd,
|
|
479
896
|
user,
|
|
@@ -483,7 +900,7 @@ var MessageService = class {
|
|
|
483
900
|
payload,
|
|
484
901
|
payerUser,
|
|
485
902
|
req,
|
|
486
|
-
clientRequestId
|
|
903
|
+
clientRequestId: normalizedClientRequestId
|
|
487
904
|
});
|
|
488
905
|
}
|
|
489
906
|
return this.createPreparedMessageBatch({
|
|
@@ -498,31 +915,38 @@ var MessageService = class {
|
|
|
498
915
|
});
|
|
499
916
|
}
|
|
500
917
|
async createMessageWithReservation(params) {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
const acquired = await this.tryCreateClientRequestReservation(params.clientRequestId);
|
|
507
|
-
if (acquired) {
|
|
508
|
-
break;
|
|
509
|
-
}
|
|
510
|
-
const waited = await this.waitForClientRequestOutcome(params.clientRequestId);
|
|
511
|
-
if (waited !== null) {
|
|
512
|
-
return waited;
|
|
513
|
-
}
|
|
514
|
-
}
|
|
918
|
+
const clientRequestScope = this.buildClientRequestScope(params.clientRequestId, params.user, params.templateCd);
|
|
919
|
+
const replay = await this.findCompletedClientRequestReplay(clientRequestScope);
|
|
920
|
+
if (replay !== null) return replay;
|
|
921
|
+
const start = await this.acquireClientRequestStart(clientRequestScope);
|
|
922
|
+
if (start.kind === "replay") return start.replay;
|
|
515
923
|
try {
|
|
516
|
-
const results = await this.createPreparedMessageBatch(
|
|
517
|
-
|
|
924
|
+
const results = await this.createPreparedMessageBatch({
|
|
925
|
+
...params,
|
|
926
|
+
clientRequestScope,
|
|
927
|
+
clientRequestLease: start.lease
|
|
928
|
+
});
|
|
518
929
|
return results;
|
|
519
930
|
} catch (error) {
|
|
520
|
-
|
|
931
|
+
if (!(error instanceof ClientRequestPendingError)) {
|
|
932
|
+
await this.failClientRequestReservation(clientRequestScope, start.lease, error);
|
|
933
|
+
}
|
|
521
934
|
throw error;
|
|
522
935
|
}
|
|
523
936
|
}
|
|
524
937
|
async createPreparedMessageBatch(params) {
|
|
525
|
-
const {
|
|
938
|
+
const {
|
|
939
|
+
templateCd,
|
|
940
|
+
user,
|
|
941
|
+
roles,
|
|
942
|
+
identity,
|
|
943
|
+
permissions,
|
|
944
|
+
payload,
|
|
945
|
+
payerUser,
|
|
946
|
+
req,
|
|
947
|
+
clientRequestScope,
|
|
948
|
+
clientRequestLease
|
|
949
|
+
} = params;
|
|
526
950
|
const template = this.registry.find(templateCd);
|
|
527
951
|
if (!template) throw new TemplateNotFoundError(templateCd);
|
|
528
952
|
const messageData = await template.prepareMessage({
|
|
@@ -531,24 +955,36 @@ var MessageService = class {
|
|
|
531
955
|
identity,
|
|
532
956
|
permissions,
|
|
533
957
|
payload,
|
|
534
|
-
getModel: this.
|
|
958
|
+
getModel: this.getTemplateModelResolver(),
|
|
535
959
|
req
|
|
536
960
|
});
|
|
537
|
-
if (!messageData)
|
|
961
|
+
if (!messageData) {
|
|
962
|
+
if (clientRequestScope && clientRequestLease) {
|
|
963
|
+
await this.persistPreparedBatchTransaction(clientRequestScope, clientRequestLease, []);
|
|
964
|
+
}
|
|
965
|
+
return [];
|
|
966
|
+
}
|
|
538
967
|
const ctx = { user, roles, identity, permissions, payload, payerUser, req };
|
|
539
968
|
const items = Array.isArray(messageData) ? messageData : [messageData];
|
|
969
|
+
if (clientRequestScope && clientRequestLease) {
|
|
970
|
+
const docs = [];
|
|
971
|
+
for (let index = 0; index < items.length; index++) {
|
|
972
|
+
docs.push(await this.buildMessageDocument(template, items[index], ctx, clientRequestScope, index));
|
|
973
|
+
}
|
|
974
|
+
return this.persistPreparedBatchTransaction(clientRequestScope, clientRequestLease, docs);
|
|
975
|
+
}
|
|
540
976
|
const results = [];
|
|
541
977
|
for (let index = 0; index < items.length; index++) {
|
|
542
978
|
const item = items[index];
|
|
543
|
-
results.push(await this.persistItem(template, item, ctx,
|
|
979
|
+
results.push(await this.persistItem(template, item, ctx, clientRequestScope, clientRequestScope ? index : null));
|
|
544
980
|
}
|
|
545
981
|
return results;
|
|
546
982
|
}
|
|
547
983
|
// -------------------------------------------------------------------------
|
|
548
984
|
// Create generic notification (no template, no actions)
|
|
549
985
|
// -------------------------------------------------------------------------
|
|
550
|
-
async createNotification(params) {
|
|
551
|
-
const Message = this.
|
|
986
|
+
async createNotification(params, sourceDocument) {
|
|
987
|
+
const Message = this.resolveModel("active", sourceDocument);
|
|
552
988
|
return Message.create({
|
|
553
989
|
type: "notification",
|
|
554
990
|
templateCd: GENERIC_NOTIFICATION_TEMPLATE_CD,
|
|
@@ -569,7 +1005,7 @@ var MessageService = class {
|
|
|
569
1005
|
* queries (e.g. with `populate`).
|
|
570
1006
|
*/
|
|
571
1007
|
buildVisibilityFilter(user) {
|
|
572
|
-
const userId =
|
|
1008
|
+
const userId = this.requireUserId(user);
|
|
573
1009
|
return {
|
|
574
1010
|
$or: [{ fromUser: userId }, { toUser: userId }, { toRoles: { $in: user.roles ?? [] } }]
|
|
575
1011
|
};
|
|
@@ -581,11 +1017,11 @@ var MessageService = class {
|
|
|
581
1017
|
*/
|
|
582
1018
|
async listMessages(params) {
|
|
583
1019
|
const { user, limit: rawLimit, skip: rawSkip, populate } = params;
|
|
584
|
-
const limit =
|
|
585
|
-
const skip =
|
|
586
|
-
const Message = this.
|
|
1020
|
+
const limit = this.normalizeListLimit(rawLimit);
|
|
1021
|
+
const skip = this.normalizeListSkip(rawSkip);
|
|
1022
|
+
const Message = this.resolveModel("active");
|
|
587
1023
|
const query = this.applyPopulate(
|
|
588
|
-
Message.find(this.buildVisibilityFilter(user)).sort({ createdAt: -1 }).skip(skip).limit(limit),
|
|
1024
|
+
Message.find(this.buildVisibilityFilter(user)).sort({ createdAt: -1, _id: -1 }).skip(skip).limit(limit),
|
|
589
1025
|
populate
|
|
590
1026
|
);
|
|
591
1027
|
return query;
|
|
@@ -595,7 +1031,7 @@ var MessageService = class {
|
|
|
595
1031
|
* Useful for badge indicators ("3 new messages").
|
|
596
1032
|
*/
|
|
597
1033
|
async countMessages(user) {
|
|
598
|
-
const Message = this.
|
|
1034
|
+
const Message = this.resolveModel("active");
|
|
599
1035
|
return Message.countDocuments(this.buildVisibilityFilter(user));
|
|
600
1036
|
}
|
|
601
1037
|
// -------------------------------------------------------------------------
|
|
@@ -604,8 +1040,10 @@ var MessageService = class {
|
|
|
604
1040
|
async getActions(messageId, usertype, options = {}) {
|
|
605
1041
|
const message = options.message ?? await this.findMessage(messageId, { populate: options.populate });
|
|
606
1042
|
if (!message) return null;
|
|
607
|
-
|
|
608
|
-
|
|
1043
|
+
const user = this.requireUserId(options.user);
|
|
1044
|
+
const authorizedUser = { ...options.user, _id: user };
|
|
1045
|
+
if (!options.isAdmin) {
|
|
1046
|
+
const isAllowedUsertype = usertype === "sender" ? message.isSender(authorizedUser) : message.isReceiver(authorizedUser);
|
|
609
1047
|
if (!isAllowedUsertype) {
|
|
610
1048
|
return null;
|
|
611
1049
|
}
|
|
@@ -620,7 +1058,7 @@ var MessageService = class {
|
|
|
620
1058
|
permissions: options.permissions,
|
|
621
1059
|
message
|
|
622
1060
|
});
|
|
623
|
-
if (options.isAdmin) {
|
|
1061
|
+
if (options.isAdmin || this.isArchivedMessage(message)) {
|
|
624
1062
|
return { uiTemplate: interpolated.uiTemplate, actions: [] };
|
|
625
1063
|
}
|
|
626
1064
|
return { uiTemplate: interpolated.uiTemplate, actions: interpolated.actions };
|
|
@@ -629,68 +1067,387 @@ var MessageService = class {
|
|
|
629
1067
|
// Handle an action on a message
|
|
630
1068
|
// -------------------------------------------------------------------------
|
|
631
1069
|
async handleAction(templateCd, actionCd, data) {
|
|
1070
|
+
const userId = this.requireUserId(data.user);
|
|
1071
|
+
const authorizedUser = { ...data.user, _id: userId };
|
|
632
1072
|
if (this.isArchivedMessage(data.message)) {
|
|
1073
|
+
if (data.message.actionNotificationState === "pending" || data.message.actionNotificationState === "failed") {
|
|
1074
|
+
throw new ActionNotificationPendingError(
|
|
1075
|
+
String(data.message._id),
|
|
1076
|
+
data.message.actionAttemptId ?? "",
|
|
1077
|
+
void 0
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
633
1080
|
throw new MessageArchivedError(String(data.message._id));
|
|
634
1081
|
}
|
|
635
|
-
|
|
636
|
-
|
|
1082
|
+
if (templateCd !== data.message.templateCd) {
|
|
1083
|
+
throw new ActionTemplateMismatchError(data.message.templateCd, templateCd);
|
|
1084
|
+
}
|
|
1085
|
+
const template = this.registry.find(data.message.templateCd);
|
|
1086
|
+
if (!template) throw new TemplateNotFoundError(data.message.templateCd);
|
|
637
1087
|
const action = template.actions.find((a) => a.actionCd === actionCd);
|
|
638
|
-
if (!action) throw new ActionNotFoundError(templateCd, actionCd);
|
|
639
|
-
if (!isActionAllowed(action,
|
|
1088
|
+
if (!action) throw new ActionNotFoundError(data.message.templateCd, actionCd);
|
|
1089
|
+
if (!isActionAllowed(action, authorizedUser, data.message, { permissions: data.permissions })) {
|
|
640
1090
|
throw new ActionNotAllowedError();
|
|
641
1091
|
}
|
|
642
|
-
const
|
|
643
|
-
const
|
|
644
|
-
|
|
645
|
-
|
|
1092
|
+
const claim = await this.claimAction(data.message, actionCd, authorizedUser);
|
|
1093
|
+
const ctx = this.buildActionContext({
|
|
1094
|
+
...data,
|
|
1095
|
+
user: authorizedUser,
|
|
1096
|
+
message: claim.message,
|
|
1097
|
+
actionAttemptId: claim.actionAttemptId
|
|
1098
|
+
});
|
|
1099
|
+
let result;
|
|
1100
|
+
try {
|
|
1101
|
+
result = await action.runHandler(ctx);
|
|
1102
|
+
} catch (error) {
|
|
1103
|
+
await this.markActionRetryable(data.message._id, claim.actionAttemptId, error, claim.message);
|
|
1104
|
+
throw new ActionRetryableError(String(data.message._id), claim.actionAttemptId, error);
|
|
1105
|
+
}
|
|
1106
|
+
await this.archiveClaimedMessage(claim.message, action, authorizedUser, claim.actionAttemptId);
|
|
1107
|
+
if (action.senderNotification) {
|
|
1108
|
+
try {
|
|
1109
|
+
await this.runSenderNotification(action, ctx, claim.message);
|
|
1110
|
+
await this.markActionNotificationState(
|
|
1111
|
+
data.message._id,
|
|
1112
|
+
claim.actionAttemptId,
|
|
1113
|
+
"sent",
|
|
1114
|
+
void 0,
|
|
1115
|
+
claim.message
|
|
1116
|
+
);
|
|
1117
|
+
} catch (error) {
|
|
1118
|
+
await this.markActionNotificationState(data.message._id, claim.actionAttemptId, "failed", error, claim.message);
|
|
1119
|
+
throw new ActionNotificationPendingError(String(data.message._id), claim.actionAttemptId, result, error);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
646
1122
|
return result;
|
|
647
1123
|
}
|
|
648
1124
|
// -------------------------------------------------------------------------
|
|
649
1125
|
// Private helpers
|
|
650
1126
|
// -------------------------------------------------------------------------
|
|
651
|
-
|
|
1127
|
+
resolveModel(role, sourceDocument, sourceModel) {
|
|
1128
|
+
const modelName = this.modelNames[role];
|
|
1129
|
+
const sourceConnection = this.getModelConnection(sourceModel) ?? this.getDocumentConnection(sourceDocument);
|
|
1130
|
+
try {
|
|
1131
|
+
if (this.getModel) {
|
|
1132
|
+
const model = this.getModel(modelName);
|
|
1133
|
+
if (!model) {
|
|
1134
|
+
throw new Error(`resolver returned no model for "${modelName}"`);
|
|
1135
|
+
}
|
|
1136
|
+
const resolvedConnection = this.getModelConnection(model);
|
|
1137
|
+
if (sourceConnection && resolvedConnection && resolvedConnection !== sourceConnection) {
|
|
1138
|
+
throw new Error(
|
|
1139
|
+
`resolver returned model "${modelName}" from connection "${resolvedConnection.name || "<unnamed>"}" instead of source connection "${sourceConnection.name || "<unnamed>"}"`
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
return model;
|
|
1143
|
+
}
|
|
1144
|
+
if (sourceConnection) {
|
|
1145
|
+
return sourceConnection.model(modelName);
|
|
1146
|
+
}
|
|
1147
|
+
if (this.connection) {
|
|
1148
|
+
return this.connection.model(modelName);
|
|
1149
|
+
}
|
|
1150
|
+
return import_mongoose5.default.model(modelName);
|
|
1151
|
+
} catch (error) {
|
|
1152
|
+
throw new MessageModelResolutionError(role, modelName, this.describeModelSource(sourceConnection), error);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
getTemplateModelResolver(sourceDocument) {
|
|
1156
|
+
const sourceConnection = this.getDocumentConnection(sourceDocument);
|
|
1157
|
+
return (name) => {
|
|
1158
|
+
try {
|
|
1159
|
+
if (sourceConnection) return sourceConnection.model(name);
|
|
1160
|
+
if (this.getModel) return this.getModel(name);
|
|
1161
|
+
if (this.connection) return this.connection.model(name);
|
|
1162
|
+
return import_mongoose5.default.model(name);
|
|
1163
|
+
} catch (error) {
|
|
1164
|
+
throw new MessageModelResolutionError("user", name, this.describeModelSource(sourceConnection), error);
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
getDocumentConnection(sourceDocument) {
|
|
1169
|
+
if (!sourceDocument) return void 0;
|
|
1170
|
+
const model = sourceDocument.constructor;
|
|
1171
|
+
return this.getModelConnection(model);
|
|
1172
|
+
}
|
|
1173
|
+
getModelConnection(model) {
|
|
1174
|
+
const connection = model?.db;
|
|
1175
|
+
return connection && typeof connection.model === "function" ? connection : void 0;
|
|
1176
|
+
}
|
|
1177
|
+
describeModelSource(sourceConnection) {
|
|
1178
|
+
const connection = sourceConnection ?? this.connection;
|
|
1179
|
+
if (connection) {
|
|
1180
|
+
return `Mongoose connection "${connection.name || "<unnamed>"}"`;
|
|
1181
|
+
}
|
|
1182
|
+
if (this.getModel) {
|
|
1183
|
+
return "the configured model resolver";
|
|
1184
|
+
}
|
|
1185
|
+
return "global mongoose";
|
|
1186
|
+
}
|
|
1187
|
+
async persistItem(template, m, ctx, clientRequestScope, clientRequestItemIndex) {
|
|
1188
|
+
const Message = this.resolveModel("active");
|
|
1189
|
+
const doc = await this.buildMessageDocument(template, m, ctx, clientRequestScope, clientRequestItemIndex);
|
|
1190
|
+
try {
|
|
1191
|
+
return await Message.create(doc);
|
|
1192
|
+
} catch (error) {
|
|
1193
|
+
await this.compensatePaymentSessions([doc], error, clientRequestScope);
|
|
1194
|
+
throw error;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
async buildMessageDocument(template, m, ctx, clientRequestScope, clientRequestItemIndex) {
|
|
652
1198
|
const toUser = m.toUser ?? null;
|
|
653
1199
|
const toRoles = (m.toRoles && m.toRoles.length > 0 ? m.toRoles : this.adminRoles).slice();
|
|
654
1200
|
const type = m.type || template.type || "notification";
|
|
655
1201
|
const paymentCd = m.paymentCd || template.paymentCd || "";
|
|
656
1202
|
let paymentSession = null;
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
1203
|
+
try {
|
|
1204
|
+
if (paymentCd && this.paymentProvider && (toUser || toRoles.length > 0)) {
|
|
1205
|
+
paymentSession = await this.paymentProvider.createSession(
|
|
1206
|
+
(ctx.payerUser || ctx.user)._id,
|
|
1207
|
+
paymentCd,
|
|
1208
|
+
m.priceArgs
|
|
1209
|
+
);
|
|
1210
|
+
if (!paymentSession) throw new Error("payment session creation failed");
|
|
1211
|
+
}
|
|
1212
|
+
const interpolated = interpolateTemplate(template, m.templateData || {}, "receiver");
|
|
1213
|
+
return {
|
|
1214
|
+
type,
|
|
1215
|
+
templateCd: template.templateCd,
|
|
1216
|
+
fromUser: m.fromUser || ctx.user._id,
|
|
1217
|
+
toUser,
|
|
1218
|
+
toRoles,
|
|
1219
|
+
senderContent: interpolated.senderContent,
|
|
1220
|
+
receiverContent: interpolated.receiverContent,
|
|
1221
|
+
documents: ctx.payload.documents || [],
|
|
1222
|
+
paymentSession,
|
|
660
1223
|
paymentCd,
|
|
661
|
-
m.
|
|
1224
|
+
payload: m.payload || ctx.payload,
|
|
1225
|
+
display: m.display,
|
|
1226
|
+
clientRequestId: clientRequestScope?.clientRequestId ?? null,
|
|
1227
|
+
clientRequestOwnerId: clientRequestScope?.clientRequestOwnerId ?? null,
|
|
1228
|
+
clientRequestItemIndex
|
|
1229
|
+
};
|
|
1230
|
+
} catch (error) {
|
|
1231
|
+
if (paymentSession) {
|
|
1232
|
+
await this.compensatePaymentSession(paymentSession, error, clientRequestScope);
|
|
1233
|
+
}
|
|
1234
|
+
throw error;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
async persistPreparedBatchTransaction(scope, lease, docs) {
|
|
1238
|
+
const Message = this.resolveModel("active");
|
|
1239
|
+
const MessageRequest = this.resolveModel("request", void 0, Message);
|
|
1240
|
+
const operation = async (session2) => {
|
|
1241
|
+
const created = docs.length > 0 ? await Message.create(docs, session2 ? { session: session2, ordered: true } : void 0) : [];
|
|
1242
|
+
const result = await MessageRequest.updateOne(
|
|
1243
|
+
{ ...scope, state: "pending", leaseOwnerId: lease.ownerId },
|
|
1244
|
+
{
|
|
1245
|
+
$set: {
|
|
1246
|
+
state: "completed",
|
|
1247
|
+
itemCount: docs.length,
|
|
1248
|
+
completedAt: /* @__PURE__ */ new Date(),
|
|
1249
|
+
leaseExpiresAt: null
|
|
1250
|
+
}
|
|
1251
|
+
},
|
|
1252
|
+
session2 ? { session: session2 } : void 0
|
|
662
1253
|
);
|
|
663
|
-
if (
|
|
1254
|
+
if ((result.matchedCount ?? result.n ?? 0) !== 1) {
|
|
1255
|
+
throw new ClientRequestPendingError(scope.clientRequestId);
|
|
1256
|
+
}
|
|
1257
|
+
return created;
|
|
1258
|
+
};
|
|
1259
|
+
const startSession = Message.db?.startSession;
|
|
1260
|
+
if (!startSession) {
|
|
1261
|
+
try {
|
|
1262
|
+
return await operation();
|
|
1263
|
+
} catch (error) {
|
|
1264
|
+
await this.compensatePaymentSessions(docs, error, scope);
|
|
1265
|
+
throw error;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
const session = await startSession.call(Message.db);
|
|
1269
|
+
try {
|
|
1270
|
+
let result = [];
|
|
1271
|
+
await session.withTransaction(async () => {
|
|
1272
|
+
result = await operation(session);
|
|
1273
|
+
});
|
|
1274
|
+
return result;
|
|
1275
|
+
} catch (error) {
|
|
1276
|
+
const wrappedError = this.isTransactionSupportError(error) ? new MessageTransactionRequiredError(error) : error;
|
|
1277
|
+
await this.compensatePaymentSessions(docs, wrappedError, scope);
|
|
1278
|
+
if (this.isTransactionSupportError(error)) {
|
|
1279
|
+
throw wrappedError;
|
|
1280
|
+
}
|
|
1281
|
+
throw wrappedError;
|
|
1282
|
+
} finally {
|
|
1283
|
+
await session.endSession();
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
async compensatePaymentSessions(docs, originalError, scope) {
|
|
1287
|
+
const sessionIds = docs.map((doc) => doc.paymentSession).filter((sessionId) => typeof sessionId === "string" && sessionId.length > 0);
|
|
1288
|
+
for (const sessionId of sessionIds) {
|
|
1289
|
+
await this.compensatePaymentSession(sessionId, originalError, scope);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
async compensatePaymentSession(sessionId, originalError, scope) {
|
|
1293
|
+
if (!this.expirePaymentSession) return;
|
|
1294
|
+
try {
|
|
1295
|
+
await this.expirePaymentSession(sessionId);
|
|
1296
|
+
} catch (error) {
|
|
1297
|
+
let hookError;
|
|
1298
|
+
try {
|
|
1299
|
+
await this.onPaymentCompensationFailure?.({
|
|
1300
|
+
operation: "expire",
|
|
1301
|
+
sessionId,
|
|
1302
|
+
error,
|
|
1303
|
+
originalError,
|
|
1304
|
+
clientRequestId: scope?.clientRequestId,
|
|
1305
|
+
clientRequestOwnerId: scope?.clientRequestOwnerId,
|
|
1306
|
+
templateCd: scope?.templateCd
|
|
1307
|
+
});
|
|
1308
|
+
} catch (eventError) {
|
|
1309
|
+
hookError = eventError;
|
|
1310
|
+
}
|
|
1311
|
+
throw new PaymentSessionCompensationError(sessionId, "expire", error, originalError, hookError);
|
|
664
1312
|
}
|
|
665
|
-
const interpolated = interpolateTemplate(template, m.templateData || {}, "receiver");
|
|
666
|
-
const Message = this.getModel(MESSAGE_MODEL_NAME);
|
|
667
|
-
return Message.create({
|
|
668
|
-
type,
|
|
669
|
-
templateCd: template.templateCd,
|
|
670
|
-
fromUser: m.fromUser || ctx.user._id,
|
|
671
|
-
toUser,
|
|
672
|
-
toRoles,
|
|
673
|
-
senderContent: interpolated.senderContent,
|
|
674
|
-
receiverContent: interpolated.receiverContent,
|
|
675
|
-
documents: ctx.payload.documents || [],
|
|
676
|
-
paymentSession,
|
|
677
|
-
paymentCd,
|
|
678
|
-
payload: m.payload || ctx.payload,
|
|
679
|
-
display: m.display,
|
|
680
|
-
clientRequestId: clientRequestId ?? null,
|
|
681
|
-
clientRequestItemIndex
|
|
682
|
-
});
|
|
683
1313
|
}
|
|
684
1314
|
buildActionContext(data) {
|
|
685
1315
|
return {
|
|
686
1316
|
message: data.message,
|
|
687
1317
|
user: data.user,
|
|
688
|
-
|
|
1318
|
+
actionAttemptId: data.actionAttemptId,
|
|
1319
|
+
getModel: this.getTemplateModelResolver(data.message),
|
|
689
1320
|
expireSession: this.expirePaymentSession,
|
|
690
1321
|
refundPayment: this.refundPaymentSession,
|
|
691
1322
|
req: data.req
|
|
692
1323
|
};
|
|
693
1324
|
}
|
|
1325
|
+
async claimAction(message, actionCd, user) {
|
|
1326
|
+
const Message = this.resolveModel("active", message);
|
|
1327
|
+
const messageId = message._id;
|
|
1328
|
+
const now = /* @__PURE__ */ new Date();
|
|
1329
|
+
const claimBase = {
|
|
1330
|
+
actionCd,
|
|
1331
|
+
actionClaimedBy: String(user._id),
|
|
1332
|
+
actionClaimedAt: now,
|
|
1333
|
+
actionLeaseExpiresAt: new Date(now.getTime() + this.actionLeaseMs),
|
|
1334
|
+
actionFailureMessage: null
|
|
1335
|
+
};
|
|
1336
|
+
const firstAttemptId = (0, import_node_crypto.randomUUID)();
|
|
1337
|
+
const firstClaim = await Message.findOneAndUpdate(
|
|
1338
|
+
{ _id: messageId, $or: [{ actionState: "active" }, { actionState: null }, { actionState: { $exists: false } }] },
|
|
1339
|
+
{ $set: { ...claimBase, actionState: "processing", actionAttemptId: firstAttemptId } },
|
|
1340
|
+
{ returnDocument: "after" }
|
|
1341
|
+
);
|
|
1342
|
+
if (firstClaim?.actionAttemptId) {
|
|
1343
|
+
return { message: firstClaim, actionAttemptId: firstClaim.actionAttemptId };
|
|
1344
|
+
}
|
|
1345
|
+
const retryClaim = await Message.findOneAndUpdate(
|
|
1346
|
+
{
|
|
1347
|
+
_id: messageId,
|
|
1348
|
+
actionCd,
|
|
1349
|
+
actionAttemptId: { $type: "string" },
|
|
1350
|
+
$or: [{ actionState: "retryable" }, { actionState: "processing", actionLeaseExpiresAt: { $lte: now } }]
|
|
1351
|
+
},
|
|
1352
|
+
{ $set: { ...claimBase, actionState: "processing" } },
|
|
1353
|
+
{ returnDocument: "after" }
|
|
1354
|
+
);
|
|
1355
|
+
if (retryClaim?.actionAttemptId) {
|
|
1356
|
+
return { message: retryClaim, actionAttemptId: retryClaim.actionAttemptId };
|
|
1357
|
+
}
|
|
1358
|
+
const archived = await this.resolveModel("archive", message, Message).findById(
|
|
1359
|
+
messageId
|
|
1360
|
+
);
|
|
1361
|
+
if (archived) {
|
|
1362
|
+
if (archived.actionNotificationState === "pending" || archived.actionNotificationState === "failed") {
|
|
1363
|
+
throw new ActionNotificationPendingError(String(messageId), archived.actionAttemptId ?? "", void 0);
|
|
1364
|
+
}
|
|
1365
|
+
throw new MessageArchivedError(String(messageId));
|
|
1366
|
+
}
|
|
1367
|
+
throw new ActionConflictError(String(messageId));
|
|
1368
|
+
}
|
|
1369
|
+
async markActionRetryable(messageId, actionAttemptId, error, sourceDocument) {
|
|
1370
|
+
const Message = this.resolveModel("active", sourceDocument);
|
|
1371
|
+
await Message.updateOne(
|
|
1372
|
+
{ _id: messageId, actionState: "processing", actionAttemptId },
|
|
1373
|
+
{
|
|
1374
|
+
$set: {
|
|
1375
|
+
actionState: "retryable",
|
|
1376
|
+
actionFailureMessage: error instanceof Error ? error.message : String(error),
|
|
1377
|
+
actionLeaseExpiresAt: null
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
);
|
|
1381
|
+
}
|
|
1382
|
+
async archiveClaimedMessage(message, action, user, actionAttemptId) {
|
|
1383
|
+
const Message = this.resolveModel("active", message);
|
|
1384
|
+
const MessageArchive = this.resolveModel("archive", message, Message);
|
|
1385
|
+
const messageId = message._id;
|
|
1386
|
+
const data = message.toObject();
|
|
1387
|
+
const notificationState = action.senderNotification ? "pending" : "none";
|
|
1388
|
+
delete data.actionState;
|
|
1389
|
+
delete data.actionClaimedBy;
|
|
1390
|
+
delete data.actionClaimedAt;
|
|
1391
|
+
delete data.actionLeaseExpiresAt;
|
|
1392
|
+
delete data.actionFailureMessage;
|
|
1393
|
+
const operation = async (session2) => {
|
|
1394
|
+
await MessageArchive.create(
|
|
1395
|
+
[
|
|
1396
|
+
{
|
|
1397
|
+
...data,
|
|
1398
|
+
actionCd: action.actionCd,
|
|
1399
|
+
archivedBy: user._id,
|
|
1400
|
+
archivedAt: /* @__PURE__ */ new Date(),
|
|
1401
|
+
actionAttemptId,
|
|
1402
|
+
actionNotificationState: notificationState,
|
|
1403
|
+
actionNotificationError: null,
|
|
1404
|
+
actionNotificationAttemptedAt: null
|
|
1405
|
+
}
|
|
1406
|
+
],
|
|
1407
|
+
session2 ? { session: session2, ordered: true } : void 0
|
|
1408
|
+
);
|
|
1409
|
+
const deleted = await Message.deleteOne(
|
|
1410
|
+
{ _id: messageId, actionState: "processing", actionAttemptId },
|
|
1411
|
+
session2 ? { session: session2 } : void 0
|
|
1412
|
+
);
|
|
1413
|
+
if ((deleted.deletedCount ?? deleted.n ?? 0) !== 1) {
|
|
1414
|
+
throw new ActionConflictError(String(messageId));
|
|
1415
|
+
}
|
|
1416
|
+
};
|
|
1417
|
+
const startSession = Message.db?.startSession;
|
|
1418
|
+
if (!startSession) {
|
|
1419
|
+
await operation();
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
const session = await startSession.call(Message.db);
|
|
1423
|
+
try {
|
|
1424
|
+
await session.withTransaction(async () => {
|
|
1425
|
+
await operation(session);
|
|
1426
|
+
});
|
|
1427
|
+
} catch (error) {
|
|
1428
|
+
if (this.isTransactionSupportError(error)) {
|
|
1429
|
+
await this.markActionRetryable(messageId, actionAttemptId, error, message);
|
|
1430
|
+
throw new MessageTransactionRequiredError(error);
|
|
1431
|
+
}
|
|
1432
|
+
await this.markActionRetryable(messageId, actionAttemptId, error, message);
|
|
1433
|
+
throw error;
|
|
1434
|
+
} finally {
|
|
1435
|
+
await session.endSession();
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
async markActionNotificationState(messageId, actionAttemptId, state, error, sourceDocument) {
|
|
1439
|
+
const MessageArchive = this.resolveModel("archive", sourceDocument);
|
|
1440
|
+
await MessageArchive.updateOne(
|
|
1441
|
+
{ _id: messageId, actionAttemptId },
|
|
1442
|
+
{
|
|
1443
|
+
$set: {
|
|
1444
|
+
actionNotificationState: state,
|
|
1445
|
+
actionNotificationError: state === "failed" ? error instanceof Error ? error.message : String(error) : null,
|
|
1446
|
+
actionNotificationAttemptedAt: /* @__PURE__ */ new Date()
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
694
1451
|
async runSenderNotification(action, ctx, message) {
|
|
695
1452
|
if (!action.senderNotification) return;
|
|
696
1453
|
let content;
|
|
@@ -707,70 +1464,212 @@ var MessageService = class {
|
|
|
707
1464
|
};
|
|
708
1465
|
const documents = !(0, import_utils.isString)(content) ? content.documents || [] : [];
|
|
709
1466
|
if (message.fromUser) {
|
|
710
|
-
await this.createNotification(
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
1467
|
+
await this.createNotification(
|
|
1468
|
+
{
|
|
1469
|
+
toUser: message.fromUser,
|
|
1470
|
+
receiverContent: senderNotificationContent,
|
|
1471
|
+
documents
|
|
1472
|
+
},
|
|
1473
|
+
message
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
normalizeClientRequestId(clientRequestId) {
|
|
1478
|
+
if (clientRequestId === void 0) {
|
|
1479
|
+
return void 0;
|
|
715
1480
|
}
|
|
1481
|
+
if (typeof clientRequestId !== "string") {
|
|
1482
|
+
throw new InvalidClientRequestIdError("clientRequestId must be a string when provided");
|
|
1483
|
+
}
|
|
1484
|
+
const trimmed = clientRequestId.trim();
|
|
1485
|
+
if (trimmed.length === 0) {
|
|
1486
|
+
throw new InvalidClientRequestIdError("clientRequestId must be a non-empty string after trimming whitespace");
|
|
1487
|
+
}
|
|
1488
|
+
if (trimmed.length > MAX_CLIENT_REQUEST_ID_LENGTH) {
|
|
1489
|
+
throw new InvalidClientRequestIdError(
|
|
1490
|
+
`clientRequestId must be at most ${MAX_CLIENT_REQUEST_ID_LENGTH} characters`
|
|
1491
|
+
);
|
|
1492
|
+
}
|
|
1493
|
+
return trimmed;
|
|
716
1494
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
1495
|
+
validatePositiveIntegerOption(name, value) {
|
|
1496
|
+
if (!Number.isFinite(value) || !Number.isInteger(value)) {
|
|
1497
|
+
throw new InvalidPaginationValueError(`${name} must be a finite integer`);
|
|
1498
|
+
}
|
|
1499
|
+
if (value < 1) {
|
|
1500
|
+
throw new InvalidPaginationValueError(`${name} must be at least 1`);
|
|
1501
|
+
}
|
|
1502
|
+
return value;
|
|
721
1503
|
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
1504
|
+
normalizeListLimit(value) {
|
|
1505
|
+
if (value === void 0) {
|
|
1506
|
+
return this.defaultListLimit;
|
|
1507
|
+
}
|
|
1508
|
+
if (!Number.isFinite(value) || !Number.isInteger(value)) {
|
|
1509
|
+
throw new InvalidPaginationValueError("listMessages limit must be a finite integer");
|
|
1510
|
+
}
|
|
1511
|
+
return Math.min(Math.max(value, 1), this.maxListLimit);
|
|
1512
|
+
}
|
|
1513
|
+
normalizeListSkip(value) {
|
|
1514
|
+
if (value === void 0) {
|
|
1515
|
+
return 0;
|
|
1516
|
+
}
|
|
1517
|
+
if (!Number.isFinite(value) || !Number.isInteger(value)) {
|
|
1518
|
+
throw new InvalidPaginationValueError("listMessages skip must be a finite integer");
|
|
1519
|
+
}
|
|
1520
|
+
return Math.max(value, 0);
|
|
1521
|
+
}
|
|
1522
|
+
buildClientRequestScope(clientRequestId, user, templateCd) {
|
|
1523
|
+
return {
|
|
1524
|
+
clientRequestId,
|
|
1525
|
+
clientRequestOwnerId: this.requireUserId(user),
|
|
1526
|
+
templateCd
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
requireUserId(user) {
|
|
1530
|
+
if (!user || user._id === void 0 || user._id === null) {
|
|
1531
|
+
throw new InvalidMessageUserError();
|
|
1532
|
+
}
|
|
1533
|
+
if (typeof user._id === "string") {
|
|
1534
|
+
const id = user._id.trim();
|
|
1535
|
+
if (id.length === 0) {
|
|
1536
|
+
throw new InvalidMessageUserError();
|
|
731
1537
|
}
|
|
732
|
-
|
|
733
|
-
|
|
1538
|
+
return id;
|
|
1539
|
+
}
|
|
1540
|
+
if (user._id instanceof import_mongoose5.default.Types.ObjectId) {
|
|
1541
|
+
return String(user._id);
|
|
1542
|
+
}
|
|
1543
|
+
throw new InvalidMessageUserError();
|
|
1544
|
+
}
|
|
1545
|
+
async findByClientRequestScope(scope) {
|
|
1546
|
+
const Message = this.resolveModel("active");
|
|
1547
|
+
const docs = await Message.find(scope).sort({ clientRequestItemIndex: 1, _id: 1 }).limit(Number.MAX_SAFE_INTEGER);
|
|
1548
|
+
return docs;
|
|
1549
|
+
}
|
|
1550
|
+
async findCompletedClientRequestReplay(scope) {
|
|
1551
|
+
const reservation = await this.findClientRequestReservation(scope);
|
|
1552
|
+
const docs = await this.findByClientRequestScope(scope);
|
|
1553
|
+
if (!reservation) {
|
|
1554
|
+
if (docs.length > 0) {
|
|
1555
|
+
throw new ClientRequestInconsistentStateError(scope.clientRequestId, "messages exist without a reservation");
|
|
734
1556
|
}
|
|
735
|
-
|
|
1557
|
+
return null;
|
|
736
1558
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
return docs;
|
|
1559
|
+
if (reservation.state === "failed") {
|
|
1560
|
+
throw new ClientRequestFailedError(scope.clientRequestId, reservation.failureMessage);
|
|
740
1561
|
}
|
|
741
|
-
|
|
742
|
-
|
|
1562
|
+
if (reservation.state !== "completed") {
|
|
1563
|
+
return null;
|
|
1564
|
+
}
|
|
1565
|
+
if (reservation.itemCount === null || reservation.itemCount < 0 || !Number.isInteger(reservation.itemCount)) {
|
|
1566
|
+
throw new ClientRequestInconsistentStateError(
|
|
1567
|
+
scope.clientRequestId,
|
|
1568
|
+
"completed reservation has an invalid itemCount"
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
if (reservation.itemCount === 0) {
|
|
1572
|
+
if (docs.length > 0) {
|
|
1573
|
+
throw new ClientRequestInconsistentStateError(
|
|
1574
|
+
scope.clientRequestId,
|
|
1575
|
+
"completed zero-item reservation has messages"
|
|
1576
|
+
);
|
|
1577
|
+
}
|
|
743
1578
|
return [];
|
|
744
1579
|
}
|
|
745
|
-
|
|
1580
|
+
const indexes = new Set(docs.map((doc) => doc.clientRequestItemIndex));
|
|
1581
|
+
const hasExpectedIndexes = docs.length === reservation.itemCount && indexes.size === reservation.itemCount && Array.from({ length: reservation.itemCount }, (_, index) => indexes.has(index)).every(Boolean);
|
|
1582
|
+
if (!hasExpectedIndexes) {
|
|
1583
|
+
throw new ClientRequestInconsistentStateError(
|
|
1584
|
+
scope.clientRequestId,
|
|
1585
|
+
`completed reservation expects item indexes 0..${reservation.itemCount - 1} but found ${JSON.stringify(
|
|
1586
|
+
Array.from(indexes)
|
|
1587
|
+
)}`
|
|
1588
|
+
);
|
|
1589
|
+
}
|
|
1590
|
+
return docs;
|
|
746
1591
|
}
|
|
747
|
-
async
|
|
748
|
-
const
|
|
1592
|
+
async acquireClientRequestStart(scope) {
|
|
1593
|
+
const deadline = Date.now() + this.clientRequestWaitMs;
|
|
1594
|
+
while (true) {
|
|
1595
|
+
const lease = await this.tryAcquireClientRequestLease(scope);
|
|
1596
|
+
if (lease) {
|
|
1597
|
+
return { kind: "lease", lease };
|
|
1598
|
+
}
|
|
1599
|
+
const replay = await this.findCompletedClientRequestReplay(scope);
|
|
1600
|
+
if (replay !== null) {
|
|
1601
|
+
return { kind: "replay", replay };
|
|
1602
|
+
}
|
|
1603
|
+
if (Date.now() >= deadline) {
|
|
1604
|
+
throw new ClientRequestPendingError(scope.clientRequestId);
|
|
1605
|
+
}
|
|
1606
|
+
await this.clientRequestDelay(Math.min(this.clientRequestPollMs, Math.max(deadline - Date.now(), 0)));
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
async tryAcquireClientRequestLease(scope) {
|
|
1610
|
+
const created = await this.tryCreateClientRequestReservation(scope);
|
|
1611
|
+
if (created) return created;
|
|
1612
|
+
return this.tryTakeOverStaleClientRequestReservation(scope);
|
|
1613
|
+
}
|
|
1614
|
+
async tryCreateClientRequestReservation(scope) {
|
|
1615
|
+
const MessageRequest = this.resolveModel("request");
|
|
1616
|
+
const ownerId = (0, import_node_crypto.randomUUID)();
|
|
749
1617
|
try {
|
|
750
|
-
await MessageRequest.create({
|
|
751
|
-
|
|
1618
|
+
await MessageRequest.create({
|
|
1619
|
+
...scope,
|
|
1620
|
+
state: "pending",
|
|
1621
|
+
itemCount: null,
|
|
1622
|
+
leaseOwnerId: ownerId,
|
|
1623
|
+
leaseExpiresAt: new Date(Date.now() + this.clientRequestLeaseMs)
|
|
1624
|
+
});
|
|
1625
|
+
return { ownerId };
|
|
752
1626
|
} catch (error) {
|
|
753
1627
|
if (this.isDuplicateKeyError(error)) {
|
|
754
|
-
return
|
|
1628
|
+
return null;
|
|
755
1629
|
}
|
|
756
1630
|
throw error;
|
|
757
1631
|
}
|
|
758
1632
|
}
|
|
759
|
-
async
|
|
760
|
-
const MessageRequest = this.
|
|
761
|
-
|
|
1633
|
+
async tryTakeOverStaleClientRequestReservation(scope) {
|
|
1634
|
+
const MessageRequest = this.resolveModel("request");
|
|
1635
|
+
const ownerId = (0, import_node_crypto.randomUUID)();
|
|
1636
|
+
const reservation = await MessageRequest.findOneAndUpdate(
|
|
1637
|
+
{ ...scope, state: "pending", leaseExpiresAt: { $lte: /* @__PURE__ */ new Date() } },
|
|
1638
|
+
{
|
|
1639
|
+
$set: {
|
|
1640
|
+
leaseOwnerId: ownerId,
|
|
1641
|
+
leaseExpiresAt: new Date(Date.now() + this.clientRequestLeaseMs)
|
|
1642
|
+
}
|
|
1643
|
+
},
|
|
1644
|
+
{ returnDocument: "after" }
|
|
1645
|
+
);
|
|
1646
|
+
return reservation ? { ownerId } : null;
|
|
762
1647
|
}
|
|
763
|
-
async
|
|
764
|
-
const MessageRequest = this.
|
|
765
|
-
await MessageRequest.
|
|
1648
|
+
async findClientRequestReservation(scope) {
|
|
1649
|
+
const MessageRequest = this.resolveModel("request");
|
|
1650
|
+
return await MessageRequest.findOne(scope);
|
|
766
1651
|
}
|
|
767
|
-
async
|
|
768
|
-
const MessageRequest = this.
|
|
769
|
-
await MessageRequest.
|
|
1652
|
+
async failClientRequestReservation(scope, lease, error) {
|
|
1653
|
+
const MessageRequest = this.resolveModel("request");
|
|
1654
|
+
await MessageRequest.updateOne(
|
|
1655
|
+
{ ...scope, state: "pending", leaseOwnerId: lease.ownerId },
|
|
1656
|
+
{
|
|
1657
|
+
$set: {
|
|
1658
|
+
state: "failed",
|
|
1659
|
+
failedAt: /* @__PURE__ */ new Date(),
|
|
1660
|
+
failureMessage: error instanceof Error ? error.message : String(error),
|
|
1661
|
+
leaseExpiresAt: null
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
);
|
|
770
1665
|
}
|
|
771
1666
|
isDuplicateKeyError(error) {
|
|
772
1667
|
return error instanceof Error && "code" in error && error.code === DUPLICATE_KEY_ERROR_CODE;
|
|
773
1668
|
}
|
|
1669
|
+
isTransactionSupportError(error) {
|
|
1670
|
+
if (!(error instanceof Error)) return false;
|
|
1671
|
+
return error.message.includes("Transaction numbers are only allowed") || error.message.includes("Transaction is not supported") || error.message.includes("transactions are not supported");
|
|
1672
|
+
}
|
|
774
1673
|
delay(ms) {
|
|
775
1674
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
776
1675
|
}
|
|
@@ -799,10 +1698,45 @@ var MessageService = class {
|
|
|
799
1698
|
// src/route-factory.ts
|
|
800
1699
|
var import_express_json_router = __toESM(require("@web-ts-toolkit/express-json-router"));
|
|
801
1700
|
var ACTION_CD_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
1701
|
+
var ROUTE_CODE_PATTERN = /^[a-zA-Z0-9_.-]+$/;
|
|
1702
|
+
var OBJECT_ID_PATTERN = /^[a-fA-F0-9]{24}$/;
|
|
1703
|
+
var MAX_ROUTE_CODE_LENGTH = 128;
|
|
1704
|
+
var MAX_CLIENT_REQUEST_ID_LENGTH2 = 128;
|
|
1705
|
+
function assertRouteCode(value, name) {
|
|
1706
|
+
if (typeof value !== "string" || value.length === 0 || value.length > MAX_ROUTE_CODE_LENGTH || !ROUTE_CODE_PATTERN.test(value)) {
|
|
1707
|
+
throw new import_express_json_router.default.clientErrors.BadRequestError(
|
|
1708
|
+
`${name} must be a non-empty string of at most ${MAX_ROUTE_CODE_LENGTH} letters, digits, dots, underscores, and hyphens`
|
|
1709
|
+
);
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
802
1712
|
function assertValidActionCd(actionCd) {
|
|
803
|
-
if (typeof actionCd !== "string" || !ACTION_CD_PATTERN.test(actionCd)) {
|
|
1713
|
+
if (typeof actionCd !== "string" || actionCd.length === 0 || actionCd.length > MAX_ROUTE_CODE_LENGTH || !ACTION_CD_PATTERN.test(actionCd)) {
|
|
804
1714
|
throw new import_express_json_router.default.clientErrors.BadRequestError(
|
|
805
|
-
|
|
1715
|
+
`actionCd must be a non-empty string of at most ${MAX_ROUTE_CODE_LENGTH} letters, digits, underscores, and hyphens`
|
|
1716
|
+
);
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
function assertValidMessageId(id) {
|
|
1720
|
+
if (typeof id !== "string" || !OBJECT_ID_PATTERN.test(id)) {
|
|
1721
|
+
throw new import_express_json_router.default.clientErrors.BadRequestError("id must be a 24-character hex ObjectId");
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
function assertValidClientRequestId(clientRequestId) {
|
|
1725
|
+
if (clientRequestId === void 0) {
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1728
|
+
if (typeof clientRequestId !== "string") {
|
|
1729
|
+
throw new import_express_json_router.default.clientErrors.BadRequestError("clientRequestId must be a string when provided");
|
|
1730
|
+
}
|
|
1731
|
+
const trimmed = clientRequestId.trim();
|
|
1732
|
+
if (trimmed.length === 0) {
|
|
1733
|
+
throw new import_express_json_router.default.clientErrors.BadRequestError(
|
|
1734
|
+
"clientRequestId must be a non-empty string after trimming whitespace"
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
if (trimmed.length > MAX_CLIENT_REQUEST_ID_LENGTH2) {
|
|
1738
|
+
throw new import_express_json_router.default.clientErrors.BadRequestError(
|
|
1739
|
+
`clientRequestId must be at most ${MAX_CLIENT_REQUEST_ID_LENGTH2} characters`
|
|
806
1740
|
);
|
|
807
1741
|
}
|
|
808
1742
|
}
|
|
@@ -811,6 +1745,23 @@ function assertValidUsertype(usertype) {
|
|
|
811
1745
|
throw new import_express_json_router.default.clientErrors.BadRequestError('usertype must be "sender" or "receiver"');
|
|
812
1746
|
}
|
|
813
1747
|
}
|
|
1748
|
+
function assertValidBody(body) {
|
|
1749
|
+
if (body === void 0 || body === null) {
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
if (typeof body !== "object" || Array.isArray(body)) {
|
|
1753
|
+
throw new import_express_json_router.default.clientErrors.BadRequestError("body must be a JSON object");
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
function requireUser(user) {
|
|
1757
|
+
if (!user || user._id === void 0 || user._id === null || String(user._id).trim().length === 0) {
|
|
1758
|
+
throw new import_express_json_router.default.clientErrors.UnauthorizedError("authentication required");
|
|
1759
|
+
}
|
|
1760
|
+
return user;
|
|
1761
|
+
}
|
|
1762
|
+
function isMongooseCastError(error) {
|
|
1763
|
+
return !!error && typeof error === "object" && error.name === "CastError";
|
|
1764
|
+
}
|
|
814
1765
|
function mapServiceError(error) {
|
|
815
1766
|
if (error instanceof MessageNotFoundError) {
|
|
816
1767
|
throw new import_express_json_router.default.clientErrors.NotFoundError("message not found");
|
|
@@ -824,15 +1775,34 @@ function mapServiceError(error) {
|
|
|
824
1775
|
if (error instanceof ActionNotAllowedError) {
|
|
825
1776
|
throw new import_express_json_router.default.clientErrors.ForbiddenError(error.message);
|
|
826
1777
|
}
|
|
1778
|
+
if (error instanceof ActionConflictError) {
|
|
1779
|
+
throw new import_express_json_router.default.clientErrors.ConflictError(error.message);
|
|
1780
|
+
}
|
|
1781
|
+
if (error instanceof ActionRetryableError) {
|
|
1782
|
+
throw new import_express_json_router.default.clientErrors.ConflictError(error.message);
|
|
1783
|
+
}
|
|
827
1784
|
if (error instanceof MessageArchivedError) {
|
|
828
1785
|
throw new import_express_json_router.default.clientErrors.GoneError(error.message);
|
|
829
1786
|
}
|
|
1787
|
+
if (error instanceof InvalidClientRequestIdError) {
|
|
1788
|
+
throw new import_express_json_router.default.clientErrors.BadRequestError(error.message);
|
|
1789
|
+
}
|
|
1790
|
+
if (error instanceof InvalidMessageUserError) {
|
|
1791
|
+
throw new import_express_json_router.default.clientErrors.UnauthorizedError("authentication required");
|
|
1792
|
+
}
|
|
1793
|
+
if (error instanceof ClientRequestPendingError || error instanceof ClientRequestFailedError) {
|
|
1794
|
+
throw new import_express_json_router.default.clientErrors.ConflictError(error.message);
|
|
1795
|
+
}
|
|
1796
|
+
if (isMongooseCastError(error)) {
|
|
1797
|
+
throw new import_express_json_router.default.clientErrors.BadRequestError("id must be a valid ObjectId");
|
|
1798
|
+
}
|
|
830
1799
|
throw error;
|
|
831
1800
|
}
|
|
832
1801
|
function createMessageRoutes(options) {
|
|
833
1802
|
const {
|
|
834
1803
|
getModel,
|
|
835
1804
|
paymentProvider = null,
|
|
1805
|
+
onPaymentCompensationFailure,
|
|
836
1806
|
adminRoles,
|
|
837
1807
|
registry,
|
|
838
1808
|
authMiddleware = [],
|
|
@@ -841,17 +1811,20 @@ function createMessageRoutes(options) {
|
|
|
841
1811
|
getIdentity = defaultGetIdentity,
|
|
842
1812
|
adminPermissionKey = "is.admin"
|
|
843
1813
|
} = options;
|
|
844
|
-
const service = new MessageService({ getModel, paymentProvider, adminRoles, registry });
|
|
1814
|
+
const service = new MessageService({ getModel, paymentProvider, onPaymentCompensationFailure, adminRoles, registry });
|
|
845
1815
|
const router = new import_express_json_router.default("", authMiddleware);
|
|
846
1816
|
router.post("/new/:templateCd", async (req) => {
|
|
847
1817
|
const templateCd = req.params.templateCd;
|
|
848
|
-
const user = getUser(req)
|
|
1818
|
+
const user = requireUser(getUser(req));
|
|
1819
|
+
assertRouteCode(templateCd, "templateCd");
|
|
1820
|
+
assertValidBody(req.body);
|
|
849
1821
|
const roles = user.roles || [];
|
|
850
1822
|
const identity = getIdentity(req);
|
|
851
1823
|
const permissions = getPermissions(req);
|
|
852
1824
|
const body = req.body || {};
|
|
853
1825
|
const { clientRequestId, ...payload } = body;
|
|
854
|
-
const
|
|
1826
|
+
const hasClientRequestId = Object.prototype.hasOwnProperty.call(body, "clientRequestId");
|
|
1827
|
+
assertValidClientRequestId(hasClientRequestId ? clientRequestId : void 0);
|
|
855
1828
|
try {
|
|
856
1829
|
return await service.createMessage({
|
|
857
1830
|
templateCd,
|
|
@@ -862,7 +1835,7 @@ function createMessageRoutes(options) {
|
|
|
862
1835
|
payload,
|
|
863
1836
|
payerUser: user,
|
|
864
1837
|
req,
|
|
865
|
-
clientRequestId:
|
|
1838
|
+
clientRequestId: hasClientRequestId ? clientRequestId : void 0
|
|
866
1839
|
});
|
|
867
1840
|
} catch (error) {
|
|
868
1841
|
mapServiceError(error);
|
|
@@ -871,11 +1844,17 @@ function createMessageRoutes(options) {
|
|
|
871
1844
|
router.get("/:id/actions/:usertype", async (req) => {
|
|
872
1845
|
const id = req.params.id;
|
|
873
1846
|
const usertype = req.params.usertype;
|
|
1847
|
+
const user = requireUser(getUser(req));
|
|
1848
|
+
assertValidMessageId(id);
|
|
874
1849
|
assertValidUsertype(usertype);
|
|
875
1850
|
const permissions = getPermissions(req);
|
|
876
1851
|
const isAdmin = !!permissions[adminPermissionKey];
|
|
877
|
-
|
|
878
|
-
|
|
1852
|
+
let result;
|
|
1853
|
+
try {
|
|
1854
|
+
result = await service.getActions(id, usertype, { permissions, user, isAdmin });
|
|
1855
|
+
} catch (error) {
|
|
1856
|
+
mapServiceError(error);
|
|
1857
|
+
}
|
|
879
1858
|
if (!result) {
|
|
880
1859
|
throw new import_express_json_router.default.clientErrors.NotFoundError("message not found");
|
|
881
1860
|
}
|
|
@@ -884,6 +1863,8 @@ function createMessageRoutes(options) {
|
|
|
884
1863
|
async function handleAction(req) {
|
|
885
1864
|
const id = req.params.id;
|
|
886
1865
|
const actionCd = req.params.actionCd;
|
|
1866
|
+
const user = requireUser(getUser(req));
|
|
1867
|
+
assertValidMessageId(id);
|
|
887
1868
|
assertValidActionCd(actionCd);
|
|
888
1869
|
let message;
|
|
889
1870
|
try {
|
|
@@ -891,15 +1872,16 @@ function createMessageRoutes(options) {
|
|
|
891
1872
|
} catch (error) {
|
|
892
1873
|
mapServiceError(error);
|
|
893
1874
|
}
|
|
894
|
-
const user = getUser(req) || { _id: "" };
|
|
895
1875
|
const permissions = getPermissions(req);
|
|
896
1876
|
try {
|
|
897
1877
|
return await service.handleAction(message.templateCd, actionCd, { message, user, permissions, req });
|
|
898
1878
|
} catch (error) {
|
|
1879
|
+
if (error instanceof ActionNotificationPendingError) {
|
|
1880
|
+
return new import_express_json_router.default.success.Accepted({ message: error.message, actionAttemptId: error.actionAttemptId });
|
|
1881
|
+
}
|
|
899
1882
|
mapServiceError(error);
|
|
900
1883
|
}
|
|
901
1884
|
}
|
|
902
|
-
router.get("/:id/action/:actionCd", handleAction);
|
|
903
1885
|
router.post("/:id/action/:actionCd", handleAction);
|
|
904
1886
|
return { router, service };
|
|
905
1887
|
}
|
|
@@ -915,24 +1897,38 @@ function defaultGetIdentity(req) {
|
|
|
915
1897
|
}
|
|
916
1898
|
// Annotate the CommonJS export names for ESM import in node:
|
|
917
1899
|
0 && (module.exports = {
|
|
1900
|
+
ActionConflictError,
|
|
918
1901
|
ActionNotAllowedError,
|
|
919
1902
|
ActionNotFoundError,
|
|
1903
|
+
ActionNotificationPendingError,
|
|
1904
|
+
ActionRetryableError,
|
|
1905
|
+
ActionTemplateMismatchError,
|
|
920
1906
|
BaseMessageFields,
|
|
1907
|
+
ClientRequestFailedError,
|
|
1908
|
+
ClientRequestInconsistentStateError,
|
|
1909
|
+
ClientRequestPendingError,
|
|
921
1910
|
GENERIC_NOTIFICATION_TEMPLATE_CD,
|
|
1911
|
+
InvalidClientRequestIdError,
|
|
1912
|
+
InvalidMessageUserError,
|
|
1913
|
+
InvalidPaginationValueError,
|
|
922
1914
|
MESSAGE_ARCHIVE_MODEL_NAME,
|
|
923
1915
|
MESSAGE_MODEL_NAME,
|
|
924
1916
|
MESSAGE_REQUEST_MODEL_NAME,
|
|
925
1917
|
MessageArchiveSchema,
|
|
926
1918
|
MessageArchivedError,
|
|
927
1919
|
MessageContentSchema,
|
|
1920
|
+
MessageModelResolutionError,
|
|
928
1921
|
MessageNotFoundError,
|
|
929
1922
|
MessageRequestSchema,
|
|
930
1923
|
MessageSchema,
|
|
931
1924
|
MessageService,
|
|
1925
|
+
MessageTransactionRequiredError,
|
|
932
1926
|
NoopEmailProvider,
|
|
933
1927
|
NoopPaymentProvider,
|
|
1928
|
+
PaymentSessionCompensationError,
|
|
934
1929
|
TemplateNotFoundError,
|
|
935
1930
|
TemplateRegistry,
|
|
1931
|
+
TemplateRegistryValidationError,
|
|
936
1932
|
buildMessageArchiveSchema,
|
|
937
1933
|
buildMessageRequestSchema,
|
|
938
1934
|
buildMessageSchema,
|