@monorise/core 1.0.2 → 1.0.4-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/dist/mock/monorise/chapter.d.ts +1 -1
- package/dist/mock/monorise/course.d.ts +1 -1
- package/dist/mock/monorise/learning-journey-config.d.ts +1 -1
- package/dist/mock/monorise/module.d.ts +1 -1
- package/dist/mock/monorise/organization.d.ts +1 -1
- package/dist/services/entity.service.d.ts +2 -2
- package/dist/services/entity.service.d.ts.map +1 -1
- package/package.json +4 -1
- package/configs/service.config.ts +0 -14
- package/constants/table.ts +0 -3
- package/controllers/entity/create-entity.controller.ts +0 -51
- package/controllers/entity/delete-entity.controller.ts +0 -35
- package/controllers/entity/entity.http +0 -62
- package/controllers/entity/get-entity.controller.ts +0 -33
- package/controllers/entity/list-entities.controller.ts +0 -69
- package/controllers/entity/update-entity.controller.ts +0 -56
- package/controllers/entity/upsert-entity.controller.ts +0 -97
- package/controllers/mutual/create-mutual.controller.ts +0 -89
- package/controllers/mutual/delete-mutual.controller.ts +0 -40
- package/controllers/mutual/get-mutual.controller.ts +0 -38
- package/controllers/mutual/list-entities-by-entity.controller.ts +0 -76
- package/controllers/mutual/mutual.http +0 -88
- package/controllers/mutual/update-mutual.controller.ts +0 -50
- package/controllers/setupRoutes.ts +0 -73
- package/controllers/tag/list-tags.controller.ts +0 -57
- package/data/DbUtils.ts +0 -40
- package/data/Entity.ts +0 -499
- package/data/EventUtils.ts +0 -47
- package/data/FileObject.ts +0 -16
- package/data/Mutual.ts +0 -779
- package/data/ProjectionExpression.ts +0 -8
- package/data/Tag.ts +0 -470
- package/data/abstract/Item.base.ts +0 -19
- package/data/abstract/Repository.base.ts +0 -92
- package/errors/api-error.ts +0 -39
- package/errors/extendable-error.ts +0 -35
- package/errors/standard-error.ts +0 -29
- package/helpers/dependencies.ts +0 -10
- package/helpers/event.ts +0 -85
- package/helpers/fromLastKeyQuery.ts +0 -11
- package/helpers/sleep.ts +0 -1
- package/helpers/toLastKeyResponse.ts +0 -11
- package/index.ts +0 -23
- package/middlewares/entity-type-check.ts +0 -20
- package/middlewares/mutual-type-check.ts +0 -26
- package/mock/entity.ts +0 -12
- package/mock/monorise/admin.ts +0 -35
- package/mock/monorise/chapter.ts +0 -94
- package/mock/monorise/course.ts +0 -149
- package/mock/monorise/index.ts +0 -143
- package/mock/monorise/learner.ts +0 -66
- package/mock/monorise/learning-activity.ts +0 -62
- package/mock/monorise/learning-journey-config.ts +0 -34
- package/mock/monorise/module.ts +0 -108
- package/mock/monorise/organization.ts +0 -63
- package/mock/monorise/reference.ts +0 -28
- package/mock/monorise/video.ts +0 -36
- package/processors/create-entity-processor.ts +0 -55
- package/processors/mutual-processor.ts +0 -262
- package/processors/prejoin-processor.ts +0 -264
- package/processors/replication-processor.ts +0 -261
- package/processors/tag-processor.ts +0 -174
- package/services/DependencyContainer.ts +0 -208
- package/services/entity-service-lifecycle.ts +0 -41
- package/services/entity.service.ts +0 -201
- package/services/mutual.service.ts +0 -285
- package/tsconfig.json +0 -116
- package/types/entity.type.ts +0 -62
- package/types/event.ts +0 -84
package/data/Mutual.ts
DELETED
|
@@ -1,779 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type AttributeValue,
|
|
3
|
-
BatchStatementErrorCodeEnum,
|
|
4
|
-
type DynamoDB,
|
|
5
|
-
type QueryCommandInput,
|
|
6
|
-
type TransactWriteItem,
|
|
7
|
-
TransactionCanceledException,
|
|
8
|
-
} from '@aws-sdk/client-dynamodb';
|
|
9
|
-
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb';
|
|
10
|
-
import type { Entity, EntitySchemaMap } from '@monorise/base';
|
|
11
|
-
import { ulid } from 'ulid';
|
|
12
|
-
import type { DbUtils } from '../data/DbUtils';
|
|
13
|
-
import { StandardError } from '../errors/standard-error';
|
|
14
|
-
import { sleep } from '../helpers/sleep';
|
|
15
|
-
import {
|
|
16
|
-
PROJECTION_EXPRESSION,
|
|
17
|
-
type ProjectionExpressionValues,
|
|
18
|
-
} from './ProjectionExpression';
|
|
19
|
-
import { Repository } from './abstract/Repository.base';
|
|
20
|
-
|
|
21
|
-
export class Mutual<
|
|
22
|
-
B extends Entity,
|
|
23
|
-
T extends Entity,
|
|
24
|
-
M extends Record<string, unknown>,
|
|
25
|
-
> {
|
|
26
|
-
constructor(
|
|
27
|
-
public byEntityType: B,
|
|
28
|
-
public byEntityId: string,
|
|
29
|
-
public byData: Partial<EntitySchemaMap[B]>,
|
|
30
|
-
public entityType: T,
|
|
31
|
-
public entityId: string,
|
|
32
|
-
public data: Partial<EntitySchemaMap[T]>,
|
|
33
|
-
public mutualData: M,
|
|
34
|
-
public mutualId?: string,
|
|
35
|
-
private _createdAt?: Date,
|
|
36
|
-
private _updatedAt?: Date,
|
|
37
|
-
private _mutualUpdatedAt?: Date,
|
|
38
|
-
private _expiresAt?: Date,
|
|
39
|
-
) {}
|
|
40
|
-
|
|
41
|
-
static fromItem<
|
|
42
|
-
B extends Entity,
|
|
43
|
-
T extends Entity,
|
|
44
|
-
M extends Record<string, unknown>,
|
|
45
|
-
>(item?: Record<string, AttributeValue>): Mutual<B, T, M> {
|
|
46
|
-
if (!item)
|
|
47
|
-
throw new StandardError('MUTUAL_IS_UNDEFINED', 'Mutual item empty');
|
|
48
|
-
|
|
49
|
-
const parsedItem = unmarshall(item);
|
|
50
|
-
|
|
51
|
-
return new Mutual<B, T, M>(
|
|
52
|
-
parsedItem.byEntityType,
|
|
53
|
-
parsedItem.byEntityId,
|
|
54
|
-
parsedItem.byData,
|
|
55
|
-
parsedItem.entityType,
|
|
56
|
-
parsedItem.entityId,
|
|
57
|
-
parsedItem.data,
|
|
58
|
-
parsedItem.mutualData,
|
|
59
|
-
parsedItem.mutualId,
|
|
60
|
-
parsedItem.createdAt ? new Date(parsedItem.createdAt) : undefined,
|
|
61
|
-
parsedItem.updatedAt ? new Date(parsedItem.updatedAt) : undefined,
|
|
62
|
-
parsedItem.mutualUpdatedAt
|
|
63
|
-
? new Date(parsedItem.mutualUpdatedAt)
|
|
64
|
-
: undefined,
|
|
65
|
-
parsedItem.expiresAt ? new Date(parsedItem.expiresAt) : undefined,
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
public mainKeys(): Record<string, AttributeValue> {
|
|
70
|
-
return {
|
|
71
|
-
PK: { S: this.mainPk },
|
|
72
|
-
SK: { S: this.mainSk },
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
public subKeys(): Record<string, AttributeValue> {
|
|
77
|
-
return {
|
|
78
|
-
PK: { S: this.byFullEntityId },
|
|
79
|
-
SK: { S: this.fullEntityId },
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
get byFullEntityId(): string {
|
|
84
|
-
return `${this.byEntityType}#${this.byEntityId}`;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
get fullEntityId(): string {
|
|
88
|
-
return `${this.entityType}#${this.entityId}`;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
get listEntitySK(): string {
|
|
92
|
-
return this.entityType as unknown as string;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
get mainPk(): string {
|
|
96
|
-
return `MUTUAL#${this.mutualId}`;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
get mainSk(): string {
|
|
100
|
-
return '#METADATA#';
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
get createdAt(): string | undefined {
|
|
104
|
-
return this._createdAt?.toISOString();
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
get updatedAt(): string | undefined {
|
|
108
|
-
return this._updatedAt?.toISOString();
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
get mutualUpdatedAt(): string | undefined {
|
|
112
|
-
return this._mutualUpdatedAt?.toISOString();
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
get expiresAt(): string | undefined {
|
|
116
|
-
return this._expiresAt?.toISOString();
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
toMutualItemOnly(): Record<string, AttributeValue> {
|
|
120
|
-
// Mutual #METADATA# has `mutualData` only
|
|
121
|
-
return {
|
|
122
|
-
...marshall(
|
|
123
|
-
{ ...this.toJSON(), data: {} },
|
|
124
|
-
{ removeUndefinedValues: true },
|
|
125
|
-
),
|
|
126
|
-
...this.mainKeys(),
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
toItem(): Record<string, AttributeValue> {
|
|
131
|
-
return {
|
|
132
|
-
...marshall(this.toJSON(), { removeUndefinedValues: true }),
|
|
133
|
-
...this.mainKeys(),
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
toReversedItem(): Record<string, AttributeValue> {
|
|
138
|
-
const item = this.toJSON();
|
|
139
|
-
|
|
140
|
-
const reversedMutual: Record<string, unknown> = {
|
|
141
|
-
...item,
|
|
142
|
-
byEntityType: item.entityType,
|
|
143
|
-
byEntityId: item.entityId,
|
|
144
|
-
entityType: item.byEntityType,
|
|
145
|
-
entityId: item.byEntityId,
|
|
146
|
-
data: this.byData,
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
return {
|
|
150
|
-
...marshall(reversedMutual, { removeUndefinedValues: true }),
|
|
151
|
-
...this.mainKeys(),
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
toJSON(): Record<string, unknown> {
|
|
156
|
-
return {
|
|
157
|
-
byEntityType: this.byEntityType,
|
|
158
|
-
byEntityId: this.byEntityId,
|
|
159
|
-
entityType: this.entityType,
|
|
160
|
-
entityId: this.entityId,
|
|
161
|
-
mutualId: this.mutualId,
|
|
162
|
-
data: this.data,
|
|
163
|
-
mutualData: this.mutualData,
|
|
164
|
-
createdAt: this.createdAt,
|
|
165
|
-
updatedAt: this.updatedAt,
|
|
166
|
-
mutualUpdatedAt: this.mutualUpdatedAt,
|
|
167
|
-
expiresAt: this.expiresAt,
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
export class MutualRepository extends Repository {
|
|
173
|
-
constructor(
|
|
174
|
-
private readonly TABLE_NAME: string,
|
|
175
|
-
private readonly dynamodbClient: DynamoDB,
|
|
176
|
-
private readonly ddbUtils: DbUtils,
|
|
177
|
-
) {
|
|
178
|
-
super();
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
async listEntitiesByEntity<
|
|
182
|
-
B extends Entity,
|
|
183
|
-
T extends Entity,
|
|
184
|
-
M extends Record<string, unknown>,
|
|
185
|
-
>(
|
|
186
|
-
byEntityType: B,
|
|
187
|
-
byEntityId: string,
|
|
188
|
-
entityType: T,
|
|
189
|
-
opts: {
|
|
190
|
-
lastKey?: Record<string, AttributeValue>;
|
|
191
|
-
ProjectionExpression?: ProjectionExpressionValues;
|
|
192
|
-
limit?: number; // if this is not set, retrieve all items
|
|
193
|
-
} = {},
|
|
194
|
-
): Promise<{
|
|
195
|
-
items: Mutual<B, T, M>[];
|
|
196
|
-
lastKey?: Record<string, AttributeValue>;
|
|
197
|
-
}> {
|
|
198
|
-
const mutual = new Mutual(
|
|
199
|
-
byEntityType,
|
|
200
|
-
byEntityId,
|
|
201
|
-
{},
|
|
202
|
-
entityType,
|
|
203
|
-
'list_by_only',
|
|
204
|
-
{},
|
|
205
|
-
{},
|
|
206
|
-
);
|
|
207
|
-
|
|
208
|
-
const listAssociationsQuery: QueryCommandInput = {
|
|
209
|
-
TableName: this.TABLE_NAME,
|
|
210
|
-
KeyConditionExpression: '#PK = :PK and begins_with(#SK, :SK)',
|
|
211
|
-
FilterExpression:
|
|
212
|
-
'attribute_not_exists(#expiresAt) or attribute_type(#expiresAt, :nullType)',
|
|
213
|
-
ExpressionAttributeNames: {
|
|
214
|
-
'#PK': 'PK',
|
|
215
|
-
'#SK': 'SK',
|
|
216
|
-
'#expiresAt': 'expiresAt',
|
|
217
|
-
},
|
|
218
|
-
ExpressionAttributeValues: {
|
|
219
|
-
':PK': {
|
|
220
|
-
S: mutual.byFullEntityId,
|
|
221
|
-
},
|
|
222
|
-
':SK': {
|
|
223
|
-
S: mutual.listEntitySK,
|
|
224
|
-
},
|
|
225
|
-
':nullType': { S: 'NULL' },
|
|
226
|
-
},
|
|
227
|
-
ProjectionExpression: opts.ProjectionExpression,
|
|
228
|
-
};
|
|
229
|
-
let lastKey = opts.lastKey;
|
|
230
|
-
let items: Mutual<B, T, M>[] = [];
|
|
231
|
-
let remainingCount = opts.limit ?? 0;
|
|
232
|
-
do {
|
|
233
|
-
const resp = await this.dynamodbClient.query({
|
|
234
|
-
...listAssociationsQuery,
|
|
235
|
-
...(remainingCount && { Limit: remainingCount }),
|
|
236
|
-
...(lastKey && {
|
|
237
|
-
ExclusiveStartKey: lastKey,
|
|
238
|
-
}),
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
items = items.concat(
|
|
242
|
-
resp.Items?.map((item) => Mutual.fromItem(item)) || [],
|
|
243
|
-
);
|
|
244
|
-
|
|
245
|
-
lastKey = resp.LastEvaluatedKey;
|
|
246
|
-
if (opts.limit) {
|
|
247
|
-
remainingCount = remainingCount - (resp.Items?.length ?? 0);
|
|
248
|
-
}
|
|
249
|
-
} while (
|
|
250
|
-
// limit is given, haven't reach limit, and there are still items to retrieve
|
|
251
|
-
(opts.limit && remainingCount && lastKey) ||
|
|
252
|
-
// no limit is given and there are still items to retrieve
|
|
253
|
-
(!opts.limit && lastKey)
|
|
254
|
-
);
|
|
255
|
-
|
|
256
|
-
return {
|
|
257
|
-
items,
|
|
258
|
-
lastKey,
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
async getMutual<
|
|
263
|
-
B extends Entity,
|
|
264
|
-
T extends Entity,
|
|
265
|
-
M extends Record<string, unknown>,
|
|
266
|
-
>(
|
|
267
|
-
byEntityType: B,
|
|
268
|
-
byEntityId: string,
|
|
269
|
-
entityType: T,
|
|
270
|
-
entityId: string,
|
|
271
|
-
opts?: {
|
|
272
|
-
// isFromMetadata to prevent race condition by querying #METADATA#
|
|
273
|
-
isFromMetadata?: boolean;
|
|
274
|
-
ProjectionExpression?: string;
|
|
275
|
-
},
|
|
276
|
-
): Promise<Mutual<B, T, M>> {
|
|
277
|
-
const mutual = new Mutual(
|
|
278
|
-
byEntityType,
|
|
279
|
-
byEntityId,
|
|
280
|
-
{},
|
|
281
|
-
entityType,
|
|
282
|
-
entityId,
|
|
283
|
-
{},
|
|
284
|
-
{},
|
|
285
|
-
);
|
|
286
|
-
|
|
287
|
-
const resp = await this.dynamodbClient.getItem({
|
|
288
|
-
TableName: this.TABLE_NAME,
|
|
289
|
-
Key: mutual.subKeys(),
|
|
290
|
-
ProjectionExpression: opts?.ProjectionExpression,
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
let mutualMetadata: Mutual<B, T, M> | null = null;
|
|
294
|
-
if (opts?.isFromMetadata) {
|
|
295
|
-
const tempMutual = Mutual.fromItem<B, T, M>(resp.Item);
|
|
296
|
-
const respMetadataMutual = await this.dynamodbClient.getItem({
|
|
297
|
-
TableName: this.TABLE_NAME,
|
|
298
|
-
Key: tempMutual.mainKeys(),
|
|
299
|
-
ProjectionExpression: opts?.ProjectionExpression,
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
mutualMetadata = Mutual.fromItem(respMetadataMutual.Item);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
return mutualMetadata || Mutual.fromItem<B, T, M>(resp.Item);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
async checkMutualExist<B extends Entity, T extends Entity>(
|
|
309
|
-
byEntityType: B,
|
|
310
|
-
byEntityId: string,
|
|
311
|
-
entityType: T,
|
|
312
|
-
entityId: string,
|
|
313
|
-
): Promise<void> {
|
|
314
|
-
const mutual = new Mutual(
|
|
315
|
-
byEntityType,
|
|
316
|
-
byEntityId,
|
|
317
|
-
{},
|
|
318
|
-
entityType,
|
|
319
|
-
entityId,
|
|
320
|
-
{},
|
|
321
|
-
{},
|
|
322
|
-
);
|
|
323
|
-
const resp = await this.dynamodbClient.getItem({
|
|
324
|
-
TableName: this.TABLE_NAME,
|
|
325
|
-
Key: mutual.subKeys(),
|
|
326
|
-
ProjectionExpression: 'PK, SK, expiresAt',
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
if (resp.Item && !resp.Item?.expiresAt) {
|
|
330
|
-
throw new StandardError('MUTUAL_EXISTS', 'Entities are already linked');
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
createMutualTransactItems<
|
|
337
|
-
B extends Entity,
|
|
338
|
-
T extends Entity,
|
|
339
|
-
M extends Record<string, unknown>,
|
|
340
|
-
>(
|
|
341
|
-
mutual: Mutual<B, T, M>,
|
|
342
|
-
opts?: {
|
|
343
|
-
ConditionExpression?: string;
|
|
344
|
-
ExpressionAttributeNames?: Record<string, string>;
|
|
345
|
-
ExpressionAttributeValues?: Record<string, AttributeValue>;
|
|
346
|
-
},
|
|
347
|
-
): TransactWriteItem[] {
|
|
348
|
-
const TransactItems: TransactWriteItem[] = [
|
|
349
|
-
{
|
|
350
|
-
Put: {
|
|
351
|
-
TableName: this.TABLE_NAME,
|
|
352
|
-
ConditionExpression:
|
|
353
|
-
opts?.ConditionExpression ||
|
|
354
|
-
'attribute_not_exists(PK) OR attribute_exists(expiresAt)',
|
|
355
|
-
ExpressionAttributeNames: opts?.ExpressionAttributeNames,
|
|
356
|
-
ExpressionAttributeValues: opts?.ExpressionAttributeValues,
|
|
357
|
-
Item: mutual.toItem(),
|
|
358
|
-
},
|
|
359
|
-
},
|
|
360
|
-
{
|
|
361
|
-
Put: {
|
|
362
|
-
TableName: this.TABLE_NAME,
|
|
363
|
-
ConditionExpression:
|
|
364
|
-
opts?.ConditionExpression ||
|
|
365
|
-
'attribute_not_exists(PK) OR attribute_exists(expiresAt)',
|
|
366
|
-
ExpressionAttributeNames: opts?.ExpressionAttributeNames,
|
|
367
|
-
ExpressionAttributeValues: opts?.ExpressionAttributeValues,
|
|
368
|
-
Item: {
|
|
369
|
-
...mutual.toItem(),
|
|
370
|
-
PK: { S: mutual.byFullEntityId },
|
|
371
|
-
SK: { S: mutual.fullEntityId },
|
|
372
|
-
R1PK: { S: mutual.fullEntityId },
|
|
373
|
-
R1SK: { S: mutual.byFullEntityId },
|
|
374
|
-
R2PK: { S: mutual.mainPk },
|
|
375
|
-
R2SK: { S: mutual.byFullEntityId },
|
|
376
|
-
},
|
|
377
|
-
},
|
|
378
|
-
},
|
|
379
|
-
{
|
|
380
|
-
Put: {
|
|
381
|
-
TableName: this.TABLE_NAME,
|
|
382
|
-
ConditionExpression:
|
|
383
|
-
opts?.ConditionExpression ||
|
|
384
|
-
'attribute_not_exists(PK) OR attribute_exists(expiresAt)',
|
|
385
|
-
ExpressionAttributeNames: opts?.ExpressionAttributeNames,
|
|
386
|
-
ExpressionAttributeValues: opts?.ExpressionAttributeValues,
|
|
387
|
-
Item: {
|
|
388
|
-
...mutual.toReversedItem(),
|
|
389
|
-
PK: { S: mutual.fullEntityId },
|
|
390
|
-
SK: { S: mutual.byFullEntityId },
|
|
391
|
-
R1PK: { S: mutual.byFullEntityId },
|
|
392
|
-
R1SK: { S: mutual.fullEntityId },
|
|
393
|
-
R2PK: { S: mutual.mainPk },
|
|
394
|
-
R2SK: { S: mutual.fullEntityId },
|
|
395
|
-
},
|
|
396
|
-
},
|
|
397
|
-
},
|
|
398
|
-
];
|
|
399
|
-
|
|
400
|
-
return TransactItems;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
async createMutual<
|
|
404
|
-
B extends Entity,
|
|
405
|
-
T extends Entity,
|
|
406
|
-
M extends Record<string, unknown>,
|
|
407
|
-
>(
|
|
408
|
-
byEntityType: B,
|
|
409
|
-
byEntityId: string,
|
|
410
|
-
byData: EntitySchemaMap[B],
|
|
411
|
-
entityType: T,
|
|
412
|
-
entityId: string,
|
|
413
|
-
data: EntitySchemaMap[T],
|
|
414
|
-
mutualData: M = {} as M,
|
|
415
|
-
opts?: {
|
|
416
|
-
ConditionExpression?: string;
|
|
417
|
-
ExpressionAttributeNames?: Record<string, string>;
|
|
418
|
-
ExpressionAttributeValues?: Record<string, AttributeValue>;
|
|
419
|
-
createAndUpdateDatetime?: Date;
|
|
420
|
-
},
|
|
421
|
-
): Promise<Mutual<B, T, M>> {
|
|
422
|
-
const errorContext: Record<string, unknown> = {};
|
|
423
|
-
const currentDatetime = opts?.createAndUpdateDatetime || new Date();
|
|
424
|
-
const mutual = new Mutual<B, T, M>(
|
|
425
|
-
byEntityType,
|
|
426
|
-
byEntityId,
|
|
427
|
-
byData,
|
|
428
|
-
entityType,
|
|
429
|
-
entityId,
|
|
430
|
-
data,
|
|
431
|
-
mutualData,
|
|
432
|
-
ulid(),
|
|
433
|
-
currentDatetime,
|
|
434
|
-
currentDatetime,
|
|
435
|
-
currentDatetime,
|
|
436
|
-
);
|
|
437
|
-
const TransactItems = this.createMutualTransactItems(mutual, {
|
|
438
|
-
ConditionExpression: opts?.ConditionExpression,
|
|
439
|
-
ExpressionAttributeNames: opts?.ExpressionAttributeNames,
|
|
440
|
-
ExpressionAttributeValues: opts?.ExpressionAttributeValues,
|
|
441
|
-
});
|
|
442
|
-
errorContext.TransactItems = TransactItems;
|
|
443
|
-
|
|
444
|
-
await this.dynamodbClient.transactWriteItems({ TransactItems });
|
|
445
|
-
|
|
446
|
-
return mutual;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
async updateMutual<
|
|
450
|
-
B extends Entity,
|
|
451
|
-
T extends Entity,
|
|
452
|
-
M extends Record<string, unknown>,
|
|
453
|
-
>(
|
|
454
|
-
byEntityType: B,
|
|
455
|
-
byEntityId: string,
|
|
456
|
-
entityType: T,
|
|
457
|
-
entityId: string,
|
|
458
|
-
toUpdate: {
|
|
459
|
-
mutualData: Record<string, unknown>;
|
|
460
|
-
mutualUpdatedAt?: string;
|
|
461
|
-
updatedAt?: string;
|
|
462
|
-
},
|
|
463
|
-
opts?: {
|
|
464
|
-
ConditionExpression?: string;
|
|
465
|
-
ExpressionAttributeNames?: Record<string, string>;
|
|
466
|
-
ExpressionAttributeValues?: Record<string, AttributeValue>;
|
|
467
|
-
returnUpdatedValue?: boolean;
|
|
468
|
-
maxObjectUpdateLevel?: number;
|
|
469
|
-
},
|
|
470
|
-
): Promise<Mutual<B, T, M> | undefined> {
|
|
471
|
-
const returnUpdatedValue = opts?.returnUpdatedValue ?? false;
|
|
472
|
-
const errorContext: Record<string, unknown> = {};
|
|
473
|
-
|
|
474
|
-
try {
|
|
475
|
-
const mutual = await this.getMutual<B, T, M>(
|
|
476
|
-
byEntityType,
|
|
477
|
-
byEntityId,
|
|
478
|
-
entityType,
|
|
479
|
-
entityId,
|
|
480
|
-
{ ProjectionExpression: PROJECTION_EXPRESSION.NO_DATA },
|
|
481
|
-
);
|
|
482
|
-
|
|
483
|
-
const currentDatetime = new Date().toISOString();
|
|
484
|
-
const toUpdateExpressions = this.toUpdate(
|
|
485
|
-
{
|
|
486
|
-
mutualUpdatedAt: currentDatetime,
|
|
487
|
-
...toUpdate,
|
|
488
|
-
},
|
|
489
|
-
{ maxLevel: opts?.maxObjectUpdateLevel },
|
|
490
|
-
);
|
|
491
|
-
const updateExpression = {
|
|
492
|
-
ConditionExpression:
|
|
493
|
-
opts?.ConditionExpression || 'attribute_exists(PK)',
|
|
494
|
-
UpdateExpression: toUpdateExpressions.UpdateExpression,
|
|
495
|
-
ExpressionAttributeNames: {
|
|
496
|
-
...toUpdateExpressions.ExpressionAttributeNames,
|
|
497
|
-
...opts?.ExpressionAttributeNames,
|
|
498
|
-
},
|
|
499
|
-
ExpressionAttributeValues: {
|
|
500
|
-
...toUpdateExpressions.ExpressionAttributeValues,
|
|
501
|
-
...opts?.ExpressionAttributeValues,
|
|
502
|
-
},
|
|
503
|
-
};
|
|
504
|
-
|
|
505
|
-
const TransactItems: TransactWriteItem[] = [
|
|
506
|
-
{
|
|
507
|
-
Update: {
|
|
508
|
-
TableName: this.TABLE_NAME,
|
|
509
|
-
Key: mutual.mainKeys(),
|
|
510
|
-
...updateExpression,
|
|
511
|
-
},
|
|
512
|
-
},
|
|
513
|
-
{
|
|
514
|
-
Update: {
|
|
515
|
-
TableName: this.TABLE_NAME,
|
|
516
|
-
Key: {
|
|
517
|
-
PK: { S: mutual.byFullEntityId },
|
|
518
|
-
SK: { S: mutual.fullEntityId },
|
|
519
|
-
},
|
|
520
|
-
...updateExpression,
|
|
521
|
-
},
|
|
522
|
-
},
|
|
523
|
-
{
|
|
524
|
-
Update: {
|
|
525
|
-
TableName: this.TABLE_NAME,
|
|
526
|
-
Key: {
|
|
527
|
-
PK: { S: mutual.fullEntityId },
|
|
528
|
-
SK: { S: mutual.byFullEntityId },
|
|
529
|
-
},
|
|
530
|
-
...updateExpression,
|
|
531
|
-
},
|
|
532
|
-
},
|
|
533
|
-
];
|
|
534
|
-
errorContext.TransactItems = TransactItems;
|
|
535
|
-
|
|
536
|
-
await this.ddbUtils.executeTransactWrite({ TransactItems });
|
|
537
|
-
|
|
538
|
-
if (!returnUpdatedValue) {
|
|
539
|
-
return;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const updatedMutual = await this.getMutual<B, T, M>(
|
|
543
|
-
byEntityType,
|
|
544
|
-
byEntityId,
|
|
545
|
-
entityType,
|
|
546
|
-
entityId,
|
|
547
|
-
);
|
|
548
|
-
|
|
549
|
-
return updatedMutual;
|
|
550
|
-
} catch (err) {
|
|
551
|
-
if (
|
|
552
|
-
err instanceof StandardError &&
|
|
553
|
-
err.code === 'CONDITIONAL_CHECK_FAILED'
|
|
554
|
-
) {
|
|
555
|
-
throw new StandardError('MUTUAL_NOT_FOUND', 'Mutual not found', err, {
|
|
556
|
-
errorContext,
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
throw err;
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
async deleteMutual<
|
|
565
|
-
B extends Entity,
|
|
566
|
-
T extends Entity,
|
|
567
|
-
M extends Record<string, unknown>,
|
|
568
|
-
>(
|
|
569
|
-
byEntityType: B,
|
|
570
|
-
byEntityId: string,
|
|
571
|
-
entityType: T,
|
|
572
|
-
entityId: string,
|
|
573
|
-
opts?: {
|
|
574
|
-
ConditionExpression?: string;
|
|
575
|
-
ExpressionAttributeNames?: Record<string, string>;
|
|
576
|
-
ExpressionAttributeValues?: Record<string, AttributeValue>;
|
|
577
|
-
},
|
|
578
|
-
): Promise<Mutual<B, T, M>> {
|
|
579
|
-
const errorContext: Record<string, unknown> = {
|
|
580
|
-
byEntityType,
|
|
581
|
-
byEntityId,
|
|
582
|
-
entityType,
|
|
583
|
-
entityId,
|
|
584
|
-
};
|
|
585
|
-
|
|
586
|
-
try {
|
|
587
|
-
const mutual = await this.getMutual<B, T, M>(
|
|
588
|
-
byEntityType,
|
|
589
|
-
byEntityId,
|
|
590
|
-
entityType,
|
|
591
|
-
entityId,
|
|
592
|
-
{ ProjectionExpression: PROJECTION_EXPRESSION.NO_DATA },
|
|
593
|
-
);
|
|
594
|
-
|
|
595
|
-
const tenMinsLater = Math.floor(new Date().getTime() / 1000 + 10 * 60);
|
|
596
|
-
const expressions = {
|
|
597
|
-
UpdateExpression:
|
|
598
|
-
'SET #expiresAt = :expiresAt, #mutualUpdatedAt = :mutualUpdatedAt, #updatedAt = :mutualUpdatedAt',
|
|
599
|
-
ConditionExpression:
|
|
600
|
-
opts?.ConditionExpression ||
|
|
601
|
-
'attribute_exists(PK) AND attribute_not_exists(#expiresAt)',
|
|
602
|
-
ExpressionAttributeNames: {
|
|
603
|
-
'#expiresAt': 'expiresAt',
|
|
604
|
-
'#mutualUpdatedAt': 'mutualUpdatedAt',
|
|
605
|
-
'#updatedAt': 'updatedAt',
|
|
606
|
-
...opts?.ExpressionAttributeNames,
|
|
607
|
-
},
|
|
608
|
-
ExpressionAttributeValues: {
|
|
609
|
-
':expiresAt': { N: String(tenMinsLater) },
|
|
610
|
-
':mutualUpdatedAt': { S: new Date().toISOString() },
|
|
611
|
-
...opts?.ExpressionAttributeValues,
|
|
612
|
-
},
|
|
613
|
-
};
|
|
614
|
-
|
|
615
|
-
const TransactItems: TransactWriteItem[] = [
|
|
616
|
-
{
|
|
617
|
-
Update: {
|
|
618
|
-
TableName: this.TABLE_NAME,
|
|
619
|
-
Key: mutual.mainKeys(),
|
|
620
|
-
...expressions,
|
|
621
|
-
},
|
|
622
|
-
},
|
|
623
|
-
{
|
|
624
|
-
Update: {
|
|
625
|
-
TableName: this.TABLE_NAME,
|
|
626
|
-
Key: {
|
|
627
|
-
PK: { S: mutual.byFullEntityId },
|
|
628
|
-
SK: { S: mutual.fullEntityId },
|
|
629
|
-
},
|
|
630
|
-
...expressions,
|
|
631
|
-
},
|
|
632
|
-
},
|
|
633
|
-
{
|
|
634
|
-
Update: {
|
|
635
|
-
TableName: this.TABLE_NAME,
|
|
636
|
-
Key: {
|
|
637
|
-
PK: { S: mutual.fullEntityId },
|
|
638
|
-
SK: { S: mutual.byFullEntityId },
|
|
639
|
-
},
|
|
640
|
-
...expressions,
|
|
641
|
-
},
|
|
642
|
-
},
|
|
643
|
-
];
|
|
644
|
-
errorContext.TransactItems = TransactItems;
|
|
645
|
-
|
|
646
|
-
await this.dynamodbClient.transactWriteItems({ TransactItems });
|
|
647
|
-
|
|
648
|
-
return mutual;
|
|
649
|
-
} catch (err) {
|
|
650
|
-
const isConditionalCheckFailed =
|
|
651
|
-
err instanceof TransactionCanceledException &&
|
|
652
|
-
err.CancellationReasons?.some(
|
|
653
|
-
(reason) =>
|
|
654
|
-
reason.Code === BatchStatementErrorCodeEnum.ConditionalCheckFailed,
|
|
655
|
-
);
|
|
656
|
-
|
|
657
|
-
const isMutualIsUndefined =
|
|
658
|
-
err instanceof StandardError && err.code === 'MUTUAL_IS_UNDEFINED';
|
|
659
|
-
|
|
660
|
-
if (isConditionalCheckFailed || isMutualIsUndefined) {
|
|
661
|
-
throw new StandardError('MUTUAL_NOT_FOUND', 'Mutual not found', err, {
|
|
662
|
-
errorContext,
|
|
663
|
-
});
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
throw err;
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
async createMutualLock<B extends Entity, T extends Entity>({
|
|
671
|
-
byEntityType,
|
|
672
|
-
byEntityId,
|
|
673
|
-
entityType,
|
|
674
|
-
version,
|
|
675
|
-
}: {
|
|
676
|
-
byEntityType: B;
|
|
677
|
-
byEntityId: string;
|
|
678
|
-
entityType: T;
|
|
679
|
-
version: string;
|
|
680
|
-
}): Promise<void> {
|
|
681
|
-
let retryCount = 2;
|
|
682
|
-
|
|
683
|
-
const itemKey = {
|
|
684
|
-
PK: {
|
|
685
|
-
S: `MUTUAL#${byEntityType}#${byEntityId}#${entityType}`,
|
|
686
|
-
},
|
|
687
|
-
SK: { S: '#LOCK#' },
|
|
688
|
-
};
|
|
689
|
-
|
|
690
|
-
do {
|
|
691
|
-
try {
|
|
692
|
-
const fiveMinsLater = Math.floor(new Date().getTime() / 1000 + 5 * 60);
|
|
693
|
-
|
|
694
|
-
await this.dynamodbClient.putItem({
|
|
695
|
-
TableName: this.TABLE_NAME,
|
|
696
|
-
Item: {
|
|
697
|
-
...itemKey,
|
|
698
|
-
version: { S: version },
|
|
699
|
-
status: { S: 'LOCK' },
|
|
700
|
-
expiresAt: {
|
|
701
|
-
// auto release lock in case the mutual logic gone wrong to prevent dead lock
|
|
702
|
-
N: `${fiveMinsLater}`,
|
|
703
|
-
},
|
|
704
|
-
},
|
|
705
|
-
ConditionExpression:
|
|
706
|
-
'attribute_not_exists(PK) OR version < :version AND #status <> :status',
|
|
707
|
-
ExpressionAttributeNames: { '#status': 'status' },
|
|
708
|
-
ExpressionAttributeValues: {
|
|
709
|
-
':version': { S: version },
|
|
710
|
-
':status': { S: 'LOCK' },
|
|
711
|
-
},
|
|
712
|
-
});
|
|
713
|
-
|
|
714
|
-
return;
|
|
715
|
-
} catch (err) {
|
|
716
|
-
console.log('=====CATCHED_MUTUAL_LOCK_CONFLICT=====');
|
|
717
|
-
|
|
718
|
-
const lock = await this.dynamodbClient.getItem({
|
|
719
|
-
TableName: this.TABLE_NAME,
|
|
720
|
-
Key: itemKey,
|
|
721
|
-
});
|
|
722
|
-
|
|
723
|
-
// if version is lower, throw not retryable error to skip
|
|
724
|
-
const existingVersion = lock.Item?.version?.S ?? '';
|
|
725
|
-
const isExistingVersionGreaterThanNewVersion =
|
|
726
|
-
existingVersion >= version;
|
|
727
|
-
if (isExistingVersionGreaterThanNewVersion) {
|
|
728
|
-
throw new StandardError(
|
|
729
|
-
'MUTUAL_LOCK_CONFLICT',
|
|
730
|
-
'Lock conflict',
|
|
731
|
-
err,
|
|
732
|
-
{ lock: lock.Item },
|
|
733
|
-
);
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
// default behaviour
|
|
737
|
-
// if version is higher, retry
|
|
738
|
-
// if lock not found, retry
|
|
739
|
-
|
|
740
|
-
await sleep(2000);
|
|
741
|
-
console.log('=====RETRY_MUTUAL_LOCK=====');
|
|
742
|
-
}
|
|
743
|
-
} while (retryCount-- > 0);
|
|
744
|
-
|
|
745
|
-
// catch real unhandled error, so it can reach DLQ for inspection
|
|
746
|
-
throw new StandardError(
|
|
747
|
-
'RETRYABLE_MUTUAL_LOCK_CONFLICT',
|
|
748
|
-
'Retryable lock conflict',
|
|
749
|
-
);
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
async deleteMutualLock<B extends Entity, T extends Entity>({
|
|
753
|
-
byEntityType,
|
|
754
|
-
byEntityId,
|
|
755
|
-
entityType,
|
|
756
|
-
}: {
|
|
757
|
-
byEntityType: B;
|
|
758
|
-
byEntityId: string;
|
|
759
|
-
entityType: T;
|
|
760
|
-
}): Promise<void> {
|
|
761
|
-
try {
|
|
762
|
-
await this.dynamodbClient.updateItem({
|
|
763
|
-
TableName: this.TABLE_NAME,
|
|
764
|
-
Key: {
|
|
765
|
-
PK: {
|
|
766
|
-
S: `MUTUAL#${byEntityType}#${byEntityId}#${entityType}`,
|
|
767
|
-
},
|
|
768
|
-
SK: { S: '#LOCK#' },
|
|
769
|
-
},
|
|
770
|
-
UpdateExpression: 'REMOVE #status',
|
|
771
|
-
ExpressionAttributeNames: { '#status': 'status' },
|
|
772
|
-
});
|
|
773
|
-
|
|
774
|
-
return;
|
|
775
|
-
} catch (error) {
|
|
776
|
-
// if lock is not found, it's okay
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
}
|