@friggframework/core 2.0.0--canary.658.93c8e07.0 → 2.0.0--canary.656.4fb07b4.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 +5 -90
- 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 +1 -37
- package/integrations/repositories/integration-repository-documentdb.js +6 -25
- package/integrations/repositories/integration-repository-interface.js +5 -22
- package/integrations/repositories/integration-repository-mongo.js +12 -32
- package/integrations/repositories/integration-repository-postgres.js +12 -33
- package/integrations/tests/doubles/test-integration-repository.js +2 -15
- package/integrations/use-cases/update-integration-messages.js +6 -8
- package/logs/serialize.js +15 -0
- package/modules/module.js +0 -30
- package/modules/requester/rate-limit/parsers.js +1 -24
- package/modules/requester/rate-limit/policy.js +21 -52
- package/modules/requester/requester.js +26 -88
- 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 +4 -25
- package/types/module-plugin/index.d.ts +2 -20
- package/integrations/repositories/message-item-shared.js +0 -24
- package/integrations/use-cases/record-rate-limit-message.js +0 -85
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,53 +530,6 @@ if (!userId) {
|
|
|
568
530
|
}
|
|
569
531
|
```
|
|
570
532
|
|
|
571
|
-
**Rate-limit message** (ADR-049): when a wait is too long to sleep, the
|
|
572
|
-
Requester tells its delegate (`RATE_LIMITED`, with the `RateLimitError`) and
|
|
573
|
-
then throws. The notification takes the path of `CREDENTIAL_INVALIDATED`:
|
|
574
|
-
Requester, `Module`, `IntegrationBase`.
|
|
575
|
-
|
|
576
|
-
- The `Module` sends `{ moduleName, reason, retryAt, policy, statusCode, links }`.
|
|
577
|
-
`links` is `static rateLimit.userHints[reason].links`, or `[]`. The message,
|
|
578
|
-
url, body and headers of the error are never sent: the message reaches end
|
|
579
|
-
users.
|
|
580
|
-
- `IntegrationBase` writes one `WARN` (`integration.<name>.rate_limited`), and
|
|
581
|
-
`RecordRateLimitMessage` appends one item to the stored `warnings`. It
|
|
582
|
-
changes no status. A rate limit is not an `ERROR`, and an `ERROR` status
|
|
583
|
-
makes the queue worker drop every webhook.
|
|
584
|
-
- A report for the same module whose reset time is within 60 s of a stored one
|
|
585
|
-
is skipped. The check reads the stored `warnings` with
|
|
586
|
-
`findIntegrationMessages`, not `this.messages`, which is per instance and
|
|
587
|
-
empty on Postgres. It is a read then a write, so workers that report at the
|
|
588
|
-
same instant can still store a few copies.
|
|
589
|
-
- Every hop is best effort. A failure is logged, and the caller still gets the
|
|
590
|
-
`RateLimitError`.
|
|
591
|
-
|
|
592
|
-
```javascript
|
|
593
|
-
{
|
|
594
|
-
title: 'Rate limit reached',
|
|
595
|
-
message: 'The hubspot API rate limit was reached and resets at 2026-09-28 14:31 UTC.',
|
|
596
|
-
timestamp: 1790000000000,
|
|
597
|
-
code: 'RATE_LIMITED',
|
|
598
|
-
module: 'hubspot',
|
|
599
|
-
reason: 'daily',
|
|
600
|
-
retryAt: '2026-09-28T14:30:15.000Z',
|
|
601
|
-
actions: [
|
|
602
|
-
{ type: 'RETRY_WHEN_READY' },
|
|
603
|
-
{ type: 'LINK', label: 'API usage limits', url: 'https://example.com/limits' },
|
|
604
|
-
],
|
|
605
|
-
}
|
|
606
|
-
```
|
|
607
|
-
|
|
608
|
-
The text is fixed. It holds the module name and the reset time in UTC, rounded
|
|
609
|
-
up to the minute, and nothing from the request.
|
|
610
|
-
`updateIntegrationMessages(id, type, title, body, timestamp)` still works, and
|
|
611
|
-
it also takes one item object, `updateIntegrationMessages(id, type, item)`,
|
|
612
|
-
whose keys are stored as they are. Every call appends.
|
|
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.
|
|
617
|
-
|
|
618
533
|
### 9. Logging System (`/logs`)
|
|
619
534
|
|
|
620
535
|
**Purpose**: One redacted JSON record per line to stdout (ADR-048). See `docs/guides/LOGGING.md`.
|
|
@@ -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,
|
|
@@ -17,9 +17,6 @@ const {
|
|
|
17
17
|
const {
|
|
18
18
|
UpdateIntegrationConfig,
|
|
19
19
|
} = require('./use-cases/update-integration-config');
|
|
20
|
-
const {
|
|
21
|
-
RecordRateLimitMessage,
|
|
22
|
-
} = require('./use-cases/record-rate-limit-message');
|
|
23
20
|
const { validateExtensionBinding } = require('./extension');
|
|
24
21
|
const { getTelemetry } = require('../telemetry/telemetry-runtime');
|
|
25
22
|
const { instrumentHandler } = require('../telemetry/instrument-handler');
|
|
@@ -62,9 +59,6 @@ class IntegrationBase {
|
|
|
62
59
|
updateIntegrationConfig = new UpdateIntegrationConfig({
|
|
63
60
|
integrationRepository: this.integrationRepository,
|
|
64
61
|
});
|
|
65
|
-
recordRateLimitMessage = new RecordRateLimitMessage({
|
|
66
|
-
integrationRepository: this.integrationRepository,
|
|
67
|
-
});
|
|
68
62
|
|
|
69
63
|
// this.telemetry.count('records.synced', n, { entity: 'contact' })
|
|
70
64
|
telemetry = bindTelemetryContext(getTelemetry(), () =>
|
|
@@ -871,9 +865,7 @@ class IntegrationBase {
|
|
|
871
865
|
* something integration-level needs attention. Today this catches the
|
|
872
866
|
* `CREDENTIAL_INVALIDATED` event Module fires from `markCredentialsInvalid`
|
|
873
867
|
* 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.
|
|
868
|
+
* stops processing further webhooks until the user re-authorizes.
|
|
877
869
|
*
|
|
878
870
|
* Modules are wired to this delegate in `_appendModules()`, which runs
|
|
879
871
|
* during `setIntegrationRecord()` — this covers every construction path
|
|
@@ -916,17 +908,6 @@ class IntegrationBase {
|
|
|
916
908
|
});
|
|
917
909
|
await this.persistStatus('ENABLED');
|
|
918
910
|
}
|
|
919
|
-
|
|
920
|
-
if (delegateString === 'RATE_LIMITED') {
|
|
921
|
-
const moduleName = object?.moduleName ?? notifier?.name;
|
|
922
|
-
this.logger.warn('Module reported a rate limit', {
|
|
923
|
-
eventName: `${this.logger.name}.rate_limited`,
|
|
924
|
-
moduleName,
|
|
925
|
-
reason: object?.reason,
|
|
926
|
-
retryAt: object?.retryAt,
|
|
927
|
-
});
|
|
928
|
-
await this._recordRateLimitMessage({ ...object, moduleName });
|
|
929
|
-
}
|
|
930
911
|
}
|
|
931
912
|
|
|
932
913
|
/**
|
|
@@ -952,23 +933,6 @@ class IntegrationBase {
|
|
|
952
933
|
});
|
|
953
934
|
}
|
|
954
935
|
}
|
|
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
936
|
}
|
|
973
937
|
|
|
974
938
|
module.exports = { IntegrationBase };
|
|
@@ -14,7 +14,6 @@ const {
|
|
|
14
14
|
IntegrationRepositoryInterface,
|
|
15
15
|
} = require('./integration-repository-interface');
|
|
16
16
|
const { validateConfigPatch } = require('./config-patch-shared');
|
|
17
|
-
const { toMessageItem } = require('./message-item-shared');
|
|
18
17
|
|
|
19
18
|
class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
|
|
20
19
|
constructor() {
|
|
@@ -95,7 +94,7 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
|
|
|
95
94
|
async updateIntegrationMessages(
|
|
96
95
|
integrationId,
|
|
97
96
|
messageType,
|
|
98
|
-
|
|
97
|
+
messageTitle,
|
|
99
98
|
messageBody,
|
|
100
99
|
messageTimestamp
|
|
101
100
|
) {
|
|
@@ -109,12 +108,11 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
|
|
|
109
108
|
}
|
|
110
109
|
const messages = this._extractMessages(existing);
|
|
111
110
|
const list = Array.isArray(messages[messageType]) ? [...messages[messageType]] : [];
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
messageBody,
|
|
115
|
-
messageTimestamp
|
|
116
|
-
);
|
|
117
|
-
list.push({ ...item, title: item.title ?? null });
|
|
111
|
+
list.push({
|
|
112
|
+
title: messageTitle ?? null,
|
|
113
|
+
message: messageBody,
|
|
114
|
+
timestamp: messageTimestamp,
|
|
115
|
+
});
|
|
118
116
|
const updatedMessages = { ...messages, [messageType]: list };
|
|
119
117
|
await updateOne(
|
|
120
118
|
this.prisma,
|
|
@@ -134,23 +132,6 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
|
|
|
134
132
|
return true;
|
|
135
133
|
}
|
|
136
134
|
|
|
137
|
-
async findIntegrationMessages(integrationId, messageType) {
|
|
138
|
-
const objectId = toObjectId(integrationId);
|
|
139
|
-
if (!objectId) {
|
|
140
|
-
throw new Error(`Integration ${integrationId} not found`);
|
|
141
|
-
}
|
|
142
|
-
const existing = await findOne(this.prisma, 'Integration', {
|
|
143
|
-
_id: objectId,
|
|
144
|
-
});
|
|
145
|
-
if (!existing) {
|
|
146
|
-
throw new Error(`Integration ${integrationId} not found`);
|
|
147
|
-
}
|
|
148
|
-
const messages = this._extractMessages(existing);
|
|
149
|
-
return Array.isArray(messages[messageType])
|
|
150
|
-
? messages[messageType]
|
|
151
|
-
: [];
|
|
152
|
-
}
|
|
153
|
-
|
|
154
135
|
async createIntegration(entities, userId, config) {
|
|
155
136
|
const now = new Date();
|
|
156
137
|
const document = {
|
|
@@ -97,43 +97,26 @@ class IntegrationRepositoryInterface {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
|
-
*
|
|
101
|
-
* positional form (title, body, timestamp) or one message item object,
|
|
102
|
-
* whose keys are stored as they are.
|
|
100
|
+
* Update integration messages
|
|
103
101
|
*
|
|
104
102
|
* @param {string|number} integrationId - Integration ID
|
|
105
103
|
* @param {string} messageType - Type of message (errors, warnings, info, logs)
|
|
106
|
-
* @param {string
|
|
107
|
-
* @param {string}
|
|
108
|
-
* @param {Date}
|
|
104
|
+
* @param {string} messageTitle - Message title
|
|
105
|
+
* @param {string} messageBody - Message body
|
|
106
|
+
* @param {Date} messageTimestamp - Message timestamp
|
|
109
107
|
* @returns {Promise<boolean>} Success indicator
|
|
110
108
|
* @abstract
|
|
111
109
|
*/
|
|
112
110
|
async updateIntegrationMessages(
|
|
113
111
|
integrationId,
|
|
114
112
|
messageType,
|
|
115
|
-
|
|
113
|
+
messageTitle,
|
|
116
114
|
messageBody,
|
|
117
115
|
messageTimestamp
|
|
118
116
|
) {
|
|
119
117
|
throw new Error('Method updateIntegrationMessages must be implemented by subclass');
|
|
120
118
|
}
|
|
121
119
|
|
|
122
|
-
/**
|
|
123
|
-
* Find the stored messages of one type. Reads the stored items, not the
|
|
124
|
-
* `messages` of an integration record, which some adapters do not fill.
|
|
125
|
-
*
|
|
126
|
-
* @param {string|number} integrationId - Integration ID
|
|
127
|
-
* @param {string} messageType - Type of message (errors, warnings, info, logs)
|
|
128
|
-
* @returns {Promise<Array>} The stored message items of that type, oldest first
|
|
129
|
-
* @abstract
|
|
130
|
-
*/
|
|
131
|
-
async findIntegrationMessages(integrationId, messageType) {
|
|
132
|
-
throw new Error(
|
|
133
|
-
'Method findIntegrationMessages must be implemented by subclass'
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
120
|
/**
|
|
138
121
|
* Create a new integration
|
|
139
122
|
*
|
|
@@ -3,7 +3,6 @@ const {
|
|
|
3
3
|
IntegrationRepositoryInterface,
|
|
4
4
|
} = require('./integration-repository-interface');
|
|
5
5
|
const { validateConfigPatch } = require('./config-patch-shared');
|
|
6
|
-
const { toMessageItem } = require('./message-item-shared');
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* MongoDB Integration Repository Adapter
|
|
@@ -230,15 +229,15 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
|
|
|
230
229
|
*
|
|
231
230
|
* @param {string} integrationId - Integration ID
|
|
232
231
|
* @param {string} messageType - Type of message (errors, warnings, info, logs)
|
|
233
|
-
* @param {string
|
|
234
|
-
* @param {string}
|
|
235
|
-
* @param {Date}
|
|
232
|
+
* @param {string} messageTitle - Message title
|
|
233
|
+
* @param {string} messageBody - Message body
|
|
234
|
+
* @param {Date} messageTimestamp - Message timestamp
|
|
236
235
|
* @returns {Promise<boolean>} Success indicator
|
|
237
236
|
*/
|
|
238
237
|
async updateIntegrationMessages(
|
|
239
238
|
integrationId,
|
|
240
239
|
messageType,
|
|
241
|
-
|
|
240
|
+
messageTitle,
|
|
242
241
|
messageBody,
|
|
243
242
|
messageTimestamp
|
|
244
243
|
) {
|
|
@@ -252,14 +251,17 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
|
|
|
252
251
|
}
|
|
253
252
|
|
|
254
253
|
// Parse existing messages (JSON field)
|
|
255
|
-
const
|
|
256
|
-
|
|
254
|
+
const messages = integration.messages || {};
|
|
255
|
+
const messageArray = Array.isArray(messages[messageType])
|
|
256
|
+
? messages[messageType]
|
|
257
257
|
: [];
|
|
258
258
|
|
|
259
259
|
// Add new message
|
|
260
|
-
messageArray.push(
|
|
261
|
-
|
|
262
|
-
|
|
260
|
+
messageArray.push({
|
|
261
|
+
title: messageTitle,
|
|
262
|
+
message: messageBody,
|
|
263
|
+
timestamp: messageTimestamp,
|
|
264
|
+
});
|
|
263
265
|
|
|
264
266
|
// Update messages
|
|
265
267
|
await this.prisma.integration.update({
|
|
@@ -272,28 +274,6 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
|
|
|
272
274
|
return true; // Mongoose compatibility
|
|
273
275
|
}
|
|
274
276
|
|
|
275
|
-
/**
|
|
276
|
-
* Find the stored messages of one type
|
|
277
|
-
*
|
|
278
|
-
* @param {string} integrationId - Integration ID
|
|
279
|
-
* @param {string} messageType - Type of message (errors, warnings, info, logs)
|
|
280
|
-
* @returns {Promise<Array>} The stored message items of that type, oldest first
|
|
281
|
-
*/
|
|
282
|
-
async findIntegrationMessages(integrationId, messageType) {
|
|
283
|
-
const integration = await this.prisma.integration.findUnique({
|
|
284
|
-
where: { id: integrationId },
|
|
285
|
-
select: { [messageType]: true },
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
if (!integration) {
|
|
289
|
-
throw new Error(`Integration ${integrationId} not found`);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return Array.isArray(integration[messageType])
|
|
293
|
-
? integration[messageType]
|
|
294
|
-
: [];
|
|
295
|
-
}
|
|
296
|
-
|
|
297
277
|
/**
|
|
298
278
|
* Find all integrations whose entity set includes the given entity ID.
|
|
299
279
|
*
|
|
@@ -3,7 +3,6 @@ const {
|
|
|
3
3
|
IntegrationRepositoryInterface,
|
|
4
4
|
} = require('./integration-repository-interface');
|
|
5
5
|
const { validateConfigPatch } = require('./config-patch-shared');
|
|
6
|
-
const { toMessageItem } = require('./message-item-shared');
|
|
7
6
|
const { strictIntId } = require('./report-id');
|
|
8
7
|
|
|
9
8
|
/**
|
|
@@ -274,15 +273,15 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
274
273
|
*
|
|
275
274
|
* @param {string} integrationId - Integration ID (string from application layer)
|
|
276
275
|
* @param {string} messageType - Type of message (errors, warnings, info, logs)
|
|
277
|
-
* @param {string
|
|
278
|
-
* @param {string}
|
|
279
|
-
* @param {Date}
|
|
276
|
+
* @param {string} messageTitle - Message title
|
|
277
|
+
* @param {string} messageBody - Message body
|
|
278
|
+
* @param {Date} messageTimestamp - Message timestamp
|
|
280
279
|
* @returns {Promise<boolean>} Success indicator
|
|
281
280
|
*/
|
|
282
281
|
async updateIntegrationMessages(
|
|
283
282
|
integrationId,
|
|
284
283
|
messageType,
|
|
285
|
-
|
|
284
|
+
messageTitle,
|
|
286
285
|
messageBody,
|
|
287
286
|
messageTimestamp
|
|
288
287
|
) {
|
|
@@ -298,14 +297,17 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
298
297
|
}
|
|
299
298
|
|
|
300
299
|
// Parse existing messages (JSON field)
|
|
301
|
-
const
|
|
302
|
-
|
|
300
|
+
const messages = integration.messages || {};
|
|
301
|
+
const messageArray = Array.isArray(messages[messageType])
|
|
302
|
+
? messages[messageType]
|
|
303
303
|
: [];
|
|
304
304
|
|
|
305
305
|
// Add new message
|
|
306
|
-
messageArray.push(
|
|
307
|
-
|
|
308
|
-
|
|
306
|
+
messageArray.push({
|
|
307
|
+
title: messageTitle,
|
|
308
|
+
message: messageBody,
|
|
309
|
+
timestamp: messageTimestamp,
|
|
310
|
+
});
|
|
309
311
|
|
|
310
312
|
// Update messages
|
|
311
313
|
await this.prisma.integration.update({
|
|
@@ -318,29 +320,6 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
318
320
|
return true; // Mongoose compatibility
|
|
319
321
|
}
|
|
320
322
|
|
|
321
|
-
/**
|
|
322
|
-
* Find the stored messages of one type
|
|
323
|
-
*
|
|
324
|
-
* @param {string} integrationId - Integration ID (string from application layer)
|
|
325
|
-
* @param {string} messageType - Type of message (errors, warnings, info, logs)
|
|
326
|
-
* @returns {Promise<Array>} The stored message items of that type, oldest first
|
|
327
|
-
*/
|
|
328
|
-
async findIntegrationMessages(integrationId, messageType) {
|
|
329
|
-
const intId = this._convertId(integrationId);
|
|
330
|
-
const integration = await this.prisma.integration.findUnique({
|
|
331
|
-
where: { id: intId },
|
|
332
|
-
select: { [messageType]: true },
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
if (!integration) {
|
|
336
|
-
throw new Error(`Integration ${integrationId} not found`);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
return Array.isArray(integration[messageType])
|
|
340
|
-
? integration[messageType]
|
|
341
|
-
: [];
|
|
342
|
-
}
|
|
343
|
-
|
|
344
323
|
/**
|
|
345
324
|
* Create a new integration
|
|
346
325
|
*
|
|
@@ -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,30 +72,18 @@ class TestIntegrationRepository {
|
|
|
73
72
|
return results;
|
|
74
73
|
}
|
|
75
74
|
|
|
76
|
-
async updateIntegrationMessages(id, type,
|
|
75
|
+
async updateIntegrationMessages(id, type, title, body, timestamp) {
|
|
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({ title, message: body, timestamp });
|
|
84
83
|
this.operationHistory.push({ operation: 'updateMessages', id, type, success: true });
|
|
85
84
|
return true;
|
|
86
85
|
}
|
|
87
86
|
|
|
88
|
-
async findIntegrationMessages(id, type) {
|
|
89
|
-
const rec = this.store.get(id);
|
|
90
|
-
this.operationHistory.push({
|
|
91
|
-
operation: 'findMessages',
|
|
92
|
-
id,
|
|
93
|
-
type,
|
|
94
|
-
found: !!rec,
|
|
95
|
-
});
|
|
96
|
-
if (!rec) throw new Error(`Integration ${id} not found`);
|
|
97
|
-
return [...(rec.messages[type] ?? [])];
|
|
98
|
-
}
|
|
99
|
-
|
|
100
87
|
async updateIntegrationConfig(id, config) {
|
|
101
88
|
if (config === null || config === undefined) {
|
|
102
89
|
throw new Error('Config parameter is required');
|
|
@@ -13,21 +13,19 @@ class UpdateIntegrationMessages {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Executes the integration messages update.
|
|
17
|
-
* (title, body, timestamp) or one message item object; the keys of an item
|
|
18
|
-
* object are stored as they are.
|
|
16
|
+
* Executes the integration messages update.
|
|
19
17
|
* @async
|
|
20
18
|
* @param {string} integrationId - ID of the integration to update.
|
|
21
19
|
* @param {string} messageType - Type of message: 'errors', 'warnings', 'info', or 'logs'.
|
|
22
|
-
* @param {string
|
|
23
|
-
* @param {string}
|
|
24
|
-
* @param {string}
|
|
20
|
+
* @param {string} messageTitle - Title of the message.
|
|
21
|
+
* @param {string} messageBody - Body content of the message.
|
|
22
|
+
* @param {string} messageTimestamp - Timestamp when the message was created.
|
|
25
23
|
* @returns {Promise<Object>} The updated integration record.
|
|
26
24
|
*/
|
|
27
25
|
async execute(
|
|
28
26
|
integrationId,
|
|
29
27
|
messageType,
|
|
30
|
-
|
|
28
|
+
messageTitle,
|
|
31
29
|
messageBody,
|
|
32
30
|
messageTimestamp
|
|
33
31
|
) {
|
|
@@ -35,7 +33,7 @@ class UpdateIntegrationMessages {
|
|
|
35
33
|
await this.integrationRepository.updateIntegrationMessages(
|
|
36
34
|
integrationId,
|
|
37
35
|
messageType,
|
|
38
|
-
|
|
36
|
+
messageTitle,
|
|
39
37
|
messageBody,
|
|
40
38
|
messageTimestamp
|
|
41
39
|
);
|
package/logs/serialize.js
CHANGED
|
@@ -235,6 +235,20 @@ function errorCode(err) {
|
|
|
235
235
|
return undefined;
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
function rateLimitFields(err) {
|
|
239
|
+
const retryAt = attempt(
|
|
240
|
+
() => (err.isRateLimited ? err.retryAt : undefined),
|
|
241
|
+
undefined
|
|
242
|
+
);
|
|
243
|
+
if (!(retryAt instanceof Date) || Number.isNaN(retryAt.getTime())) {
|
|
244
|
+
return {};
|
|
245
|
+
}
|
|
246
|
+
const reason = attempt(() => err.reason, undefined);
|
|
247
|
+
return typeof reason === 'string'
|
|
248
|
+
? { retryAt: retryAt.toISOString(), reason: cleanString(reason) }
|
|
249
|
+
: { retryAt: retryAt.toISOString() };
|
|
250
|
+
}
|
|
251
|
+
|
|
238
252
|
function errorStack(err, type, message) {
|
|
239
253
|
const stack = attempt(() => err.stack, undefined);
|
|
240
254
|
if (typeof stack !== 'string') return undefined;
|
|
@@ -279,6 +293,7 @@ function serializeErrorAt(err, depth, seen) {
|
|
|
279
293
|
if (code !== undefined) out.code = code;
|
|
280
294
|
const status = errorStatus(err);
|
|
281
295
|
if (status !== undefined) out.status = status;
|
|
296
|
+
Object.assign(out, rateLimitFields(err));
|
|
282
297
|
const stack = errorStack(err, type, message);
|
|
283
298
|
if (stack !== undefined) out.stack = stack;
|
|
284
299
|
|