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