@friggframework/core 2.0.0--canary.658.93c8e07.0 → 2.0.0--canary.658.164d92b.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/CLAUDE.md +13 -54
- package/core/invocation-deadline.js +10 -19
- package/core/invocation-scope.js +1 -9
- package/errors/fetch-error.js +7 -9
- package/errors/rate-limit-error.js +0 -4
- package/integrations/integration-base.js +3 -23
- package/integrations/repositories/integration-repository-documentdb.js +9 -15
- package/integrations/repositories/integration-repository-interface.js +6 -9
- package/integrations/repositories/integration-repository-mongo.js +10 -17
- package/integrations/repositories/integration-repository-postgres.js +10 -17
- package/integrations/repositories/message-item-shared.js +3 -23
- package/integrations/tests/doubles/prisma-message-row.js +24 -0
- package/integrations/tests/doubles/test-integration-repository.js +3 -3
- package/integrations/use-cases/record-rate-limit-message.js +46 -21
- package/integrations/use-cases/update-integration-messages.js +21 -4
- package/logs/serialize.js +15 -0
- package/modules/module.js +6 -9
- package/modules/requester/rate-limit/parsers.js +1 -24
- package/modules/requester/rate-limit/policy.js +21 -52
- package/modules/requester/requester.js +38 -72
- package/package.json +5 -5
- package/types/core/index.d.ts +0 -9
- package/types/errors/index.d.ts +0 -15
- package/types/integrations/index.d.ts +0 -5
- package/types/module-plugin/index.d.ts +1 -15
package/CLAUDE.md
CHANGED
|
@@ -370,49 +370,11 @@ class MyIntegration extends IntegrationBase {
|
|
|
370
370
|
- `BasicAuthRequester` - Basic authentication
|
|
371
371
|
|
|
372
372
|
**Rate limits** (ADR-049): an API module declares `static rateLimit` on its
|
|
373
|
-
`Requester` subclass.
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
| 1 | `classify({ status, headers, body })` of the module. It recognises a limit that is not a plain 429 (a 403 limit code, a `policyName` in the body) and may name only a `reason` |
|
|
379
|
-
| 2 | The header parsers in `parsers` order. Default: `retryAfter`, `resetHeaders`, `ietf` |
|
|
380
|
-
| 3 | The static policy: `minRetryAfterMs`, or the `perMs` of the window named by the `reason` |
|
|
381
|
-
| 4 | The fixed `backOff` ladder (1, 3, 10, 30, 60, 180 s) |
|
|
382
|
-
|
|
383
|
-
- A 429 with no hint from steps 1 to 3 keeps today's ladder: the same calls,
|
|
384
|
-
the same delays, then a plain `FetchError`. Nothing budgets it. A response
|
|
385
|
-
that `classify` names as a limit but that has no time follows the same
|
|
386
|
-
ladder, and the last error is a `FetchError` flagged `isRateLimited` with
|
|
387
|
-
the `reason`, so the queue worker does not halt it.
|
|
388
|
-
- With a hint, the wait is `max(hint, minRetryAfterMs, 1 s)` plus at most 10 %
|
|
389
|
-
jitter. The total sleep of one request is capped at `maxInProcessWaitMs`
|
|
390
|
-
(default 5 minutes) and at the time left in the invocation less one request
|
|
391
|
-
timeout (`remainingInvocationMs()`).
|
|
392
|
-
- A wait that does not fit throws `RateLimitError` (`isRateLimited`, `retryAt`,
|
|
393
|
-
`waitMs`, `reason`, `policy`, `source`, `module`, `scopeKey`). The queue
|
|
394
|
-
worker does not halt it, whatever its status.
|
|
395
|
-
- `Retry-After` is read on a 429 only. Another status counts only when
|
|
396
|
-
`classify` recognises it. A 5xx keeps the 5xx ladder.
|
|
397
|
-
- `maxInProcessWaitMs: 0` turns off sleeping: every hinted wait throws.
|
|
398
|
-
- A module that does not use the Requester (for example one on jsforce) calls
|
|
399
|
-
`classifyRateLimit(policy, { status, headers, body })` around its own client
|
|
400
|
-
and throws `RateLimitError` itself.
|
|
401
|
-
|
|
402
|
-
```javascript
|
|
403
|
-
class Api extends OAuth2Requester {
|
|
404
|
-
static rateLimit = {
|
|
405
|
-
scope: 'entity',
|
|
406
|
-
minRetryAfterMs: 1_000,
|
|
407
|
-
classify({ status, body }) {
|
|
408
|
-
if (status === 403 && body?.code === 'DAILY_LIMIT') {
|
|
409
|
-
return { reason: 'daily', waitMs: 60 * 60_000 };
|
|
410
|
-
}
|
|
411
|
-
return null;
|
|
412
|
-
},
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
```
|
|
373
|
+
`Requester` subclass. A hinted wait (from `classify()`, the header parsers or
|
|
374
|
+
the static policy) sleeps within a budget, or throws `RateLimitError`
|
|
375
|
+
(`isRateLimited`, `retryAt`) when it does not fit. A 429 with no hint keeps the
|
|
376
|
+
fixed `backOff` ladder. See
|
|
377
|
+
[Rate Limits](../../docs/reference/api-module-definition-and-functions.md#rate-limits-static-ratelimit).
|
|
416
378
|
|
|
417
379
|
**Module Factory**:
|
|
418
380
|
- `ModuleFactory` - Creates and configures API module instances
|
|
@@ -568,10 +530,11 @@ if (!userId) {
|
|
|
568
530
|
}
|
|
569
531
|
```
|
|
570
532
|
|
|
571
|
-
**Rate-limit message** (ADR-049): when a wait is
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
533
|
+
**Rate-limit message** (ADR-049): when a wait is longer than the in-process cap
|
|
534
|
+
(`maxInProcessWaitMs`, default 5 min), the Requester tells its delegate
|
|
535
|
+
(`RATE_LIMITED`, with the `RateLimitError`) and then throws. A shorter wait
|
|
536
|
+
that throws sends no notification. The notification takes the path of
|
|
537
|
+
`CREDENTIAL_INVALIDATED`: Requester, `Module`, `IntegrationBase`.
|
|
575
538
|
|
|
576
539
|
- The `Module` sends `{ moduleName, reason, retryAt, policy, statusCode, links }`.
|
|
577
540
|
`links` is `static rateLimit.userHints[reason].links`, or `[]`. The message,
|
|
@@ -607,13 +570,9 @@ Requester, `Module`, `IntegrationBase`.
|
|
|
607
570
|
|
|
608
571
|
The text is fixed. It holds the module name and the reset time in UTC, rounded
|
|
609
572
|
up to the minute, and nothing from the request.
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
`findIntegrationMessages(id, type)` reads the stored items of one type. The
|
|
614
|
-
`messages` of the record from `findIntegrationById` is not filled on Postgres
|
|
615
|
-
and Mongo, because the schema keeps the messages in the `errors`, `warnings`,
|
|
616
|
-
`info` and `logs` columns.
|
|
573
|
+
|
|
574
|
+
Integration messages (the item form, `findIntegrationMessages`, the newest 50
|
|
575
|
+
per type): see "Integration Messages" in `docs/guides/INTEGRATION-PATTERNS.md`.
|
|
617
576
|
|
|
618
577
|
### 9. Logging System (`/logs`)
|
|
619
578
|
|
|
@@ -9,11 +9,6 @@ function storage() {
|
|
|
9
9
|
return globalThis[STORE_KEY];
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
* Runs fn with the time the invocation ends, as epoch milliseconds. A nested
|
|
14
|
-
* scope can shorten the deadline and never extend it. A deadline that is not
|
|
15
|
-
* a finite number runs fn with no limit.
|
|
16
|
-
*/
|
|
17
12
|
function runWithInvocationDeadline(deadlineAt, fn) {
|
|
18
13
|
if (typeof deadlineAt !== 'number' || !Number.isFinite(deadlineAt)) {
|
|
19
14
|
return fn();
|
|
@@ -25,25 +20,20 @@ function runWithInvocationDeadline(deadlineAt, fn) {
|
|
|
25
20
|
return storage().run(Object.freeze({ deadlineAt: effective }), fn);
|
|
26
21
|
}
|
|
27
22
|
|
|
28
|
-
|
|
29
|
-
* The end of a Lambda invocation, from its context. Undefined when the
|
|
30
|
-
* context cannot tell.
|
|
31
|
-
*/
|
|
32
|
-
function deadlineFromContext(context) {
|
|
23
|
+
function remainingTimeMs(context) {
|
|
33
24
|
try {
|
|
34
25
|
const remaining = context?.getRemainingTimeInMillis?.();
|
|
35
|
-
return typeof remaining === 'number'
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
} catch {
|
|
39
|
-
return undefined;
|
|
26
|
+
return typeof remaining === 'number' ? remaining : Infinity;
|
|
27
|
+
} catch (_) {
|
|
28
|
+
return Infinity;
|
|
40
29
|
}
|
|
41
30
|
}
|
|
42
31
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
32
|
+
function deadlineFromContext(context) {
|
|
33
|
+
const remaining = remainingTimeMs(context);
|
|
34
|
+
return Number.isFinite(remaining) ? Date.now() + remaining : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
47
37
|
function remainingInvocationMs(now = Date.now()) {
|
|
48
38
|
const store = storage().getStore();
|
|
49
39
|
return store ? Math.max(0, store.deadlineAt - now) : Infinity;
|
|
@@ -52,5 +42,6 @@ function remainingInvocationMs(now = Date.now()) {
|
|
|
52
42
|
module.exports = {
|
|
53
43
|
deadlineFromContext,
|
|
54
44
|
remainingInvocationMs,
|
|
45
|
+
remainingTimeMs,
|
|
55
46
|
runWithInvocationDeadline,
|
|
56
47
|
};
|
package/core/invocation-scope.js
CHANGED
|
@@ -3,6 +3,7 @@ const { summarizeMessageBody } = require('../logs/summarize-event');
|
|
|
3
3
|
const { flushSinks, hasFlushableSinks } = require('../logs/logger-runtime');
|
|
4
4
|
const {
|
|
5
5
|
deadlineFromContext,
|
|
6
|
+
remainingTimeMs,
|
|
6
7
|
runWithInvocationDeadline,
|
|
7
8
|
} = require('./invocation-deadline');
|
|
8
9
|
|
|
@@ -115,15 +116,6 @@ function withDeadline(ms, fn) {
|
|
|
115
116
|
});
|
|
116
117
|
}
|
|
117
118
|
|
|
118
|
-
function remainingTimeMs(context) {
|
|
119
|
-
try {
|
|
120
|
-
const remaining = context?.getRemainingTimeInMillis?.();
|
|
121
|
-
return typeof remaining === 'number' ? remaining : Infinity;
|
|
122
|
-
} catch (_) {
|
|
123
|
-
return Infinity;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
119
|
async function flushInvocation({
|
|
128
120
|
telemetry,
|
|
129
121
|
usageRollup,
|
package/errors/fetch-error.js
CHANGED
|
@@ -55,15 +55,13 @@ class FetchError extends BaseError {
|
|
|
55
55
|
|
|
56
56
|
static async create(options = {}) {
|
|
57
57
|
const { response } = options;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
) {
|
|
66
|
-
responseBody = (await response.text()) || provided;
|
|
58
|
+
let { responseBody } = options;
|
|
59
|
+
if (responseBody === undefined || responseBody === null) {
|
|
60
|
+
const canRead =
|
|
61
|
+
response &&
|
|
62
|
+
!response.bodyUsed &&
|
|
63
|
+
typeof response.text === 'function';
|
|
64
|
+
responseBody = (canRead && (await response.text())) || options.body;
|
|
67
65
|
}
|
|
68
66
|
const ErrorClass = typeof this === 'function' ? this : FetchError;
|
|
69
67
|
return new ErrorClass({ ...options, responseBody });
|
|
@@ -14,10 +14,6 @@ function resolveTiming({ hint, waitMs, now }) {
|
|
|
14
14
|
return { waitMs: fallbackWaitMs, retryAt: new Date(now + fallbackWaitMs) };
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
* A FetchError for a response that said a limit was hit and told us, or the
|
|
19
|
-
* module's policy told us, when to call again. `retryAt` is that time.
|
|
20
|
-
*/
|
|
21
17
|
class RateLimitError extends FetchError {
|
|
22
18
|
constructor({
|
|
23
19
|
hint,
|
|
@@ -871,9 +871,7 @@ class IntegrationBase {
|
|
|
871
871
|
* something integration-level needs attention. Today this catches the
|
|
872
872
|
* `CREDENTIAL_INVALIDATED` event Module fires from `markCredentialsInvalid`
|
|
873
873
|
* and flips this integration's status to ERROR so the queue worker
|
|
874
|
-
* stops processing further webhooks until the user re-authorizes.
|
|
875
|
-
* catches `RATE_LIMITED`, which records one warning for the user and
|
|
876
|
-
* changes no status: a rate limit is not an error.
|
|
874
|
+
* stops processing further webhooks until the user re-authorizes.
|
|
877
875
|
*
|
|
878
876
|
* Modules are wired to this delegate in `_appendModules()`, which runs
|
|
879
877
|
* during `setIntegrationRecord()` — this covers every construction path
|
|
@@ -918,14 +916,13 @@ class IntegrationBase {
|
|
|
918
916
|
}
|
|
919
917
|
|
|
920
918
|
if (delegateString === 'RATE_LIMITED') {
|
|
921
|
-
const moduleName = object?.moduleName ?? notifier?.name;
|
|
922
919
|
this.logger.warn('Module reported a rate limit', {
|
|
923
920
|
eventName: `${this.logger.name}.rate_limited`,
|
|
924
|
-
moduleName,
|
|
921
|
+
moduleName: object?.moduleName,
|
|
925
922
|
reason: object?.reason,
|
|
926
923
|
retryAt: object?.retryAt,
|
|
927
924
|
});
|
|
928
|
-
await this.
|
|
925
|
+
await this.recordRateLimitMessage.execute(this.id, object);
|
|
929
926
|
}
|
|
930
927
|
}
|
|
931
928
|
|
|
@@ -952,23 +949,6 @@ class IntegrationBase {
|
|
|
952
949
|
});
|
|
953
950
|
}
|
|
954
951
|
}
|
|
955
|
-
|
|
956
|
-
/**
|
|
957
|
-
* Best-effort: the module's request is about to throw a RateLimitError,
|
|
958
|
-
* and a failed warning must not change that. A rate limit is not an ERROR,
|
|
959
|
-
* so this never touches the integration status.
|
|
960
|
-
* @param {Object} payload - The RATE_LIMITED payload of the module.
|
|
961
|
-
*/
|
|
962
|
-
async _recordRateLimitMessage(payload) {
|
|
963
|
-
try {
|
|
964
|
-
await this.recordRateLimitMessage.execute(this.id, payload);
|
|
965
|
-
} catch (error) {
|
|
966
|
-
this.logger.error('Failed to record rate limit message', {
|
|
967
|
-
eventName: `${this.logger.name}.rate_limit_message_record_failed`,
|
|
968
|
-
error,
|
|
969
|
-
});
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
952
|
}
|
|
973
953
|
|
|
974
954
|
module.exports = { IntegrationBase };
|
|
@@ -14,7 +14,7 @@ const {
|
|
|
14
14
|
IntegrationRepositoryInterface,
|
|
15
15
|
} = require('./integration-repository-interface');
|
|
16
16
|
const { validateConfigPatch } = require('./config-patch-shared');
|
|
17
|
-
const {
|
|
17
|
+
const { messagesOfType } = require('./message-item-shared');
|
|
18
18
|
|
|
19
19
|
class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
|
|
20
20
|
constructor() {
|
|
@@ -95,9 +95,8 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
|
|
|
95
95
|
async updateIntegrationMessages(
|
|
96
96
|
integrationId,
|
|
97
97
|
messageType,
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
messageTimestamp
|
|
98
|
+
item,
|
|
99
|
+
{ keepLast }
|
|
101
100
|
) {
|
|
102
101
|
const objectId = toObjectId(integrationId);
|
|
103
102
|
if (!objectId) {
|
|
@@ -108,14 +107,12 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
|
|
|
108
107
|
throw new Error(`Integration ${integrationId} not found`);
|
|
109
108
|
}
|
|
110
109
|
const messages = this._extractMessages(existing);
|
|
111
|
-
const list =
|
|
112
|
-
const item = toMessageItem(
|
|
113
|
-
messageTitleOrItem,
|
|
114
|
-
messageBody,
|
|
115
|
-
messageTimestamp
|
|
116
|
-
);
|
|
110
|
+
const list = [...messagesOfType(messages, messageType)];
|
|
117
111
|
list.push({ ...item, title: item.title ?? null });
|
|
118
|
-
const updatedMessages = {
|
|
112
|
+
const updatedMessages = {
|
|
113
|
+
...messages,
|
|
114
|
+
[messageType]: list.slice(-keepLast),
|
|
115
|
+
};
|
|
119
116
|
await updateOne(
|
|
120
117
|
this.prisma,
|
|
121
118
|
'Integration',
|
|
@@ -145,10 +142,7 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
|
|
|
145
142
|
if (!existing) {
|
|
146
143
|
throw new Error(`Integration ${integrationId} not found`);
|
|
147
144
|
}
|
|
148
|
-
|
|
149
|
-
return Array.isArray(messages[messageType])
|
|
150
|
-
? messages[messageType]
|
|
151
|
-
: [];
|
|
145
|
+
return messagesOfType(this._extractMessages(existing), messageType);
|
|
152
146
|
}
|
|
153
147
|
|
|
154
148
|
async createIntegration(entities, userId, config) {
|
|
@@ -97,24 +97,21 @@ class IntegrationRepositoryInterface {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
|
-
* Append a message to the stored messages of one type
|
|
101
|
-
* positional form (title, body, timestamp) or one message item object,
|
|
102
|
-
* whose keys are stored as they are.
|
|
100
|
+
* Append a message item to the stored messages of one type
|
|
103
101
|
*
|
|
104
102
|
* @param {string|number} integrationId - Integration ID
|
|
105
103
|
* @param {string} messageType - Type of message (errors, warnings, info, logs)
|
|
106
|
-
* @param {
|
|
107
|
-
* @param {
|
|
108
|
-
* @param {
|
|
104
|
+
* @param {Object} item - Message item, stored as it is
|
|
105
|
+
* @param {Object} options
|
|
106
|
+
* @param {number} options.keepLast - How many of the newest items of the type to keep
|
|
109
107
|
* @returns {Promise<boolean>} Success indicator
|
|
110
108
|
* @abstract
|
|
111
109
|
*/
|
|
112
110
|
async updateIntegrationMessages(
|
|
113
111
|
integrationId,
|
|
114
112
|
messageType,
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
messageTimestamp
|
|
113
|
+
item,
|
|
114
|
+
{ keepLast }
|
|
118
115
|
) {
|
|
119
116
|
throw new Error('Method updateIntegrationMessages must be implemented by subclass');
|
|
120
117
|
}
|
|
@@ -3,7 +3,7 @@ const {
|
|
|
3
3
|
IntegrationRepositoryInterface,
|
|
4
4
|
} = require('./integration-repository-interface');
|
|
5
5
|
const { validateConfigPatch } = require('./config-patch-shared');
|
|
6
|
-
const {
|
|
6
|
+
const { messagesOfType } = require('./message-item-shared');
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* MongoDB Integration Repository Adapter
|
|
@@ -230,17 +230,16 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
|
|
|
230
230
|
*
|
|
231
231
|
* @param {string} integrationId - Integration ID
|
|
232
232
|
* @param {string} messageType - Type of message (errors, warnings, info, logs)
|
|
233
|
-
* @param {
|
|
234
|
-
* @param {
|
|
235
|
-
* @param {
|
|
233
|
+
* @param {Object} item - Message item, stored as it is
|
|
234
|
+
* @param {Object} options
|
|
235
|
+
* @param {number} options.keepLast - How many of the newest items of the type to keep
|
|
236
236
|
* @returns {Promise<boolean>} Success indicator
|
|
237
237
|
*/
|
|
238
238
|
async updateIntegrationMessages(
|
|
239
239
|
integrationId,
|
|
240
240
|
messageType,
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
messageTimestamp
|
|
241
|
+
item,
|
|
242
|
+
{ keepLast }
|
|
244
243
|
) {
|
|
245
244
|
// Get current integration
|
|
246
245
|
const integration = await this.prisma.integration.findUnique({
|
|
@@ -252,20 +251,16 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
|
|
|
252
251
|
}
|
|
253
252
|
|
|
254
253
|
// Parse existing messages (JSON field)
|
|
255
|
-
const messageArray =
|
|
256
|
-
? integration[messageType]
|
|
257
|
-
: [];
|
|
254
|
+
const messageArray = messagesOfType(integration, messageType);
|
|
258
255
|
|
|
259
256
|
// Add new message
|
|
260
|
-
messageArray.push(
|
|
261
|
-
toMessageItem(messageTitleOrItem, messageBody, messageTimestamp)
|
|
262
|
-
);
|
|
257
|
+
messageArray.push(item);
|
|
263
258
|
|
|
264
259
|
// Update messages
|
|
265
260
|
await this.prisma.integration.update({
|
|
266
261
|
where: { id: integrationId },
|
|
267
262
|
data: {
|
|
268
|
-
[messageType]: messageArray,
|
|
263
|
+
[messageType]: messageArray.slice(-keepLast),
|
|
269
264
|
},
|
|
270
265
|
});
|
|
271
266
|
|
|
@@ -289,9 +284,7 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
|
|
|
289
284
|
throw new Error(`Integration ${integrationId} not found`);
|
|
290
285
|
}
|
|
291
286
|
|
|
292
|
-
return
|
|
293
|
-
? integration[messageType]
|
|
294
|
-
: [];
|
|
287
|
+
return messagesOfType(integration, messageType);
|
|
295
288
|
}
|
|
296
289
|
|
|
297
290
|
/**
|
|
@@ -3,7 +3,7 @@ const {
|
|
|
3
3
|
IntegrationRepositoryInterface,
|
|
4
4
|
} = require('./integration-repository-interface');
|
|
5
5
|
const { validateConfigPatch } = require('./config-patch-shared');
|
|
6
|
-
const {
|
|
6
|
+
const { messagesOfType } = require('./message-item-shared');
|
|
7
7
|
const { strictIntId } = require('./report-id');
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -274,17 +274,16 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
274
274
|
*
|
|
275
275
|
* @param {string} integrationId - Integration ID (string from application layer)
|
|
276
276
|
* @param {string} messageType - Type of message (errors, warnings, info, logs)
|
|
277
|
-
* @param {
|
|
278
|
-
* @param {
|
|
279
|
-
* @param {
|
|
277
|
+
* @param {Object} item - Message item, stored as it is
|
|
278
|
+
* @param {Object} options
|
|
279
|
+
* @param {number} options.keepLast - How many of the newest items of the type to keep
|
|
280
280
|
* @returns {Promise<boolean>} Success indicator
|
|
281
281
|
*/
|
|
282
282
|
async updateIntegrationMessages(
|
|
283
283
|
integrationId,
|
|
284
284
|
messageType,
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
messageTimestamp
|
|
285
|
+
item,
|
|
286
|
+
{ keepLast }
|
|
288
287
|
) {
|
|
289
288
|
const intId = this._convertId(integrationId);
|
|
290
289
|
|
|
@@ -298,20 +297,16 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
298
297
|
}
|
|
299
298
|
|
|
300
299
|
// Parse existing messages (JSON field)
|
|
301
|
-
const messageArray =
|
|
302
|
-
? integration[messageType]
|
|
303
|
-
: [];
|
|
300
|
+
const messageArray = messagesOfType(integration, messageType);
|
|
304
301
|
|
|
305
302
|
// Add new message
|
|
306
|
-
messageArray.push(
|
|
307
|
-
toMessageItem(messageTitleOrItem, messageBody, messageTimestamp)
|
|
308
|
-
);
|
|
303
|
+
messageArray.push(item);
|
|
309
304
|
|
|
310
305
|
// Update messages
|
|
311
306
|
await this.prisma.integration.update({
|
|
312
307
|
where: { id: intId },
|
|
313
308
|
data: {
|
|
314
|
-
[messageType]: messageArray,
|
|
309
|
+
[messageType]: messageArray.slice(-keepLast),
|
|
315
310
|
},
|
|
316
311
|
});
|
|
317
312
|
|
|
@@ -336,9 +331,7 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
336
331
|
throw new Error(`Integration ${integrationId} not found`);
|
|
337
332
|
}
|
|
338
333
|
|
|
339
|
-
return
|
|
340
|
-
? integration[messageType]
|
|
341
|
-
: [];
|
|
334
|
+
return messagesOfType(integration, messageType);
|
|
342
335
|
}
|
|
343
336
|
|
|
344
337
|
/**
|
|
@@ -1,24 +1,4 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
1
|
+
const messagesOfType = (record, messageType) =>
|
|
2
|
+
Array.isArray(record[messageType]) ? record[messageType] : [];
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
* The stored shape of a message, for IntegrationRepository
|
|
6
|
-
* .updateIntegrationMessages(). The method takes the positional form (title,
|
|
7
|
-
* body, timestamp) or one item object. The keys of an item object are stored
|
|
8
|
-
* as they are, so a caller can attach what a client acts on, like a code or
|
|
9
|
-
* a list of actions.
|
|
10
|
-
* @param {string|Object} titleOrItem - The title, or the whole item.
|
|
11
|
-
* @param {string} [messageBody] - Ignored when an item is given.
|
|
12
|
-
* @param {number|Date} [messageTimestamp] - Ignored when an item is given.
|
|
13
|
-
* @returns {Object} A new object; the given item is never returned itself.
|
|
14
|
-
*/
|
|
15
|
-
function toMessageItem(titleOrItem, messageBody, messageTimestamp) {
|
|
16
|
-
if (isItem(titleOrItem)) return { ...titleOrItem };
|
|
17
|
-
return {
|
|
18
|
-
title: titleOrItem,
|
|
19
|
-
message: messageBody,
|
|
20
|
-
timestamp: messageTimestamp,
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
module.exports = { toMessageItem };
|
|
4
|
+
module.exports = { messagesOfType };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const copy = (value) => JSON.parse(JSON.stringify(value));
|
|
2
|
+
|
|
3
|
+
function withMessageRow(repo, id, stored = {}) {
|
|
4
|
+
const row = {
|
|
5
|
+
id,
|
|
6
|
+
errors: [],
|
|
7
|
+
warnings: [],
|
|
8
|
+
info: [],
|
|
9
|
+
logs: [],
|
|
10
|
+
...stored,
|
|
11
|
+
};
|
|
12
|
+
repo.prisma = {
|
|
13
|
+
integration: {
|
|
14
|
+
findUnique: jest.fn(async () => copy(row)),
|
|
15
|
+
update: jest.fn(async ({ data }) => {
|
|
16
|
+
Object.assign(row, copy(data));
|
|
17
|
+
return copy(row);
|
|
18
|
+
}),
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
return { repo, row };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { withMessageRow };
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const { v4: uuid } = require('uuid');
|
|
2
2
|
const { validateConfigPatch } = require('../../repositories/config-patch-shared');
|
|
3
|
-
const { toMessageItem } = require('../../repositories/message-item-shared');
|
|
4
3
|
|
|
5
4
|
class TestIntegrationRepository {
|
|
6
5
|
constructor() {
|
|
@@ -73,14 +72,15 @@ class TestIntegrationRepository {
|
|
|
73
72
|
return results;
|
|
74
73
|
}
|
|
75
74
|
|
|
76
|
-
async updateIntegrationMessages(id, type,
|
|
75
|
+
async updateIntegrationMessages(id, type, item, { keepLast }) {
|
|
77
76
|
const rec = this.store.get(id);
|
|
78
77
|
if (!rec) {
|
|
79
78
|
this.operationHistory.push({ operation: 'updateMessages', id, success: false });
|
|
80
79
|
return false;
|
|
81
80
|
}
|
|
82
81
|
if (!rec.messages[type]) rec.messages[type] = [];
|
|
83
|
-
rec.messages[type].push(
|
|
82
|
+
rec.messages[type].push(item);
|
|
83
|
+
rec.messages[type] = rec.messages[type].slice(-keepLast);
|
|
84
84
|
this.operationHistory.push({ operation: 'updateMessages', id, type, success: true });
|
|
85
85
|
return true;
|
|
86
86
|
}
|