@generazioneai/events 0.0.1 → 0.1.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/CHANGELOG.md +21 -0
- package/README.md +106 -1
- package/dist/connection.d.ts +23 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +113 -0
- package/dist/connection.js.map +1 -0
- package/dist/consumers.service.d.ts +54 -0
- package/dist/consumers.service.d.ts.map +1 -0
- package/dist/consumers.service.js +346 -0
- package/dist/consumers.service.js.map +1 -0
- package/dist/decorators.d.ts +12 -0
- package/dist/decorators.d.ts.map +1 -0
- package/dist/decorators.js +14 -0
- package/dist/decorators.js.map +1 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +24 -0
- package/dist/errors.js.map +1 -0
- package/dist/events.module.d.ts +20 -0
- package/dist/events.module.d.ts.map +1 -0
- package/dist/events.module.js +98 -0
- package/dist/events.module.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics.d.ts +18 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +64 -0
- package/dist/metrics.js.map +1 -0
- package/dist/outbox.service.d.ts +24 -0
- package/dist/outbox.service.d.ts.map +1 -0
- package/dist/outbox.service.js +88 -0
- package/dist/outbox.service.js.map +1 -0
- package/dist/prisma-resolver.d.ts +11 -0
- package/dist/prisma-resolver.d.ts.map +1 -0
- package/dist/prisma-resolver.js +46 -0
- package/dist/prisma-resolver.js.map +1 -0
- package/dist/relay.service.d.ts +32 -0
- package/dist/relay.service.d.ts.map +1 -0
- package/dist/relay.service.js +153 -0
- package/dist/relay.service.js.map +1 -0
- package/dist/tokens.d.ts +4 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +7 -0
- package/dist/tokens.js.map +1 -0
- package/dist/types.d.ts +116 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +34 -0
- package/dist/types.js.map +1 -0
- package/dist/uuid.d.ts +7 -0
- package/dist/uuid.d.ts.map +1 -0
- package/dist/uuid.js +32 -0
- package/dist/uuid.js.map +1 -0
- package/package.json +65 -4
- package/prisma/events.prisma +30 -0
- package/sql/001-events-outbox-inbox-rientro.sql +3 -0
- package/sql/001-events-outbox-inbox.sql +28 -0
package/dist/types.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_RETRY_SCHEDULE_MS = void 0;
|
|
4
|
+
exports.resolveOptions = resolveOptions;
|
|
5
|
+
exports.DEFAULT_RETRY_SCHEDULE_MS = [10_000, 60_000, 300_000, 1_800_000, 7_200_000, 21_600_000, 43_200_000];
|
|
6
|
+
function resolveOptions(o, env = process.env) {
|
|
7
|
+
const mode = (o.mode ?? env.EVENTS_MODE ?? 'off');
|
|
8
|
+
const sig = (o.signatureMode ?? env.EVENTS_SIGNATURE ?? 'off');
|
|
9
|
+
if (!['off', 'outbox', 'on'].includes(mode))
|
|
10
|
+
throw new Error(`EVENTS_MODE non valido: ${mode}`);
|
|
11
|
+
if (!['off', 'shadow', 'enforce'].includes(sig))
|
|
12
|
+
throw new Error(`EVENTS_SIGNATURE non valido: ${sig}`);
|
|
13
|
+
return {
|
|
14
|
+
service: o.service,
|
|
15
|
+
stream: o.stream ?? `EVT_${o.service.toUpperCase()}`,
|
|
16
|
+
mode,
|
|
17
|
+
signatureMode: sig,
|
|
18
|
+
createStream: o.createStream ?? true,
|
|
19
|
+
streamReplicas: o.streamReplicas ?? (env.EVENTS_STREAM_REPLICAS ? Number(env.EVENTS_STREAM_REPLICAS) : 3),
|
|
20
|
+
streamMaxBytes: o.streamMaxBytes ?? (env.EVENTS_STREAM_MAX_BYTES ? Number(env.EVENTS_STREAM_MAX_BYTES) : 5 * 1024 ** 3),
|
|
21
|
+
retryScheduleMs: o.retryScheduleMs ?? exports.DEFAULT_RETRY_SCHEDULE_MS,
|
|
22
|
+
relayIntervalMs: o.relayIntervalMs ?? 500,
|
|
23
|
+
relayBatchSize: o.relayBatchSize ?? 100,
|
|
24
|
+
pollMs: o.pollMs ?? 5_000,
|
|
25
|
+
outboxRetentionDays: o.outboxRetentionDays ?? 7,
|
|
26
|
+
inboxRetentionDays: o.inboxRetentionDays ?? 30,
|
|
27
|
+
natsUrl: o.natsUrl ?? env.EVENTS_NATS_URL ?? env.NATS_URL,
|
|
28
|
+
signer: o.signer,
|
|
29
|
+
verifier: o.verifier,
|
|
30
|
+
connection: o.connection,
|
|
31
|
+
traceparent: o.traceparent,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";;;AA0IA,wCAyBC;AArDY,QAAA,yBAAyB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC;AA4BjH,SAAgB,cAAc,CAAC,CAAsB,EAAE,MAAyB,OAAO,CAAC,GAAG;IACzF,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,IAAK,GAAG,CAAC,WAA0B,IAAI,KAAK,CAAe,CAAC;IAChF,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,aAAa,IAAK,GAAG,CAAC,gBAAwC,IAAI,KAAK,CAAwB,CAAC;IAC/G,IAAI,CAAC,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,2BAA2B,IAAI,EAAE,CAAC,CAAC;IAChG,IAAI,CAAC,CAAC,KAAK,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,gCAAgC,GAAG,EAAE,CAAC,CAAC;IACxG,OAAO;QACL,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE;QACpD,IAAI;QACJ,aAAa,EAAE,GAAG;QAClB,YAAY,EAAE,CAAC,CAAC,YAAY,IAAI,IAAI;QACpC,cAAc,EAAE,CAAC,CAAC,cAAc,IAAI,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACzG,cAAc,EAAE,CAAC,CAAC,cAAc,IAAI,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC;QACvH,eAAe,EAAE,CAAC,CAAC,eAAe,IAAI,iCAAyB;QAC/D,eAAe,EAAE,CAAC,CAAC,eAAe,IAAI,GAAG;QACzC,cAAc,EAAE,CAAC,CAAC,cAAc,IAAI,GAAG;QACvC,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,KAAK;QACzB,mBAAmB,EAAE,CAAC,CAAC,mBAAmB,IAAI,CAAC;QAC/C,kBAAkB,EAAE,CAAC,CAAC,kBAAkB,IAAI,EAAE;QAC9C,OAAO,EAAE,CAAC,CAAC,OAAO,IAAI,GAAG,CAAC,eAAe,IAAI,GAAG,CAAC,QAAQ;QACzD,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,UAAU,EAAE,CAAC,CAAC,UAAU;QACxB,WAAW,EAAE,CAAC,CAAC,WAAW;KAC3B,CAAC;AACJ,CAAC"}
|
package/dist/uuid.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UUID v7 (RFC 9562): 48 bit di millisecondi + 12 bit di contatore monotono nello stesso millisecondo + 62 bit casuali.
|
|
3
|
+
* Gli id si ordinano come testo nell'ordine in cui sono stati generati (da questo processo).
|
|
4
|
+
*/
|
|
5
|
+
export declare function uuidv7(now?: number): string;
|
|
6
|
+
export declare const UUID_RE: RegExp;
|
|
7
|
+
//# sourceMappingURL=uuid.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uuid.d.ts","sourceRoot":"","sources":["../src/uuid.ts"],"names":[],"mappings":"AAKA;;;GAGG;AACH,wBAAgB,MAAM,CAAC,GAAG,GAAE,MAAmB,GAAG,MAAM,CAgBvD;AAED,eAAO,MAAM,OAAO,QAAoE,CAAC"}
|
package/dist/uuid.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UUID_RE = void 0;
|
|
4
|
+
exports.uuidv7 = uuidv7;
|
|
5
|
+
const crypto_1 = require("crypto");
|
|
6
|
+
let lastMs = 0;
|
|
7
|
+
let seq = 0;
|
|
8
|
+
/**
|
|
9
|
+
* UUID v7 (RFC 9562): 48 bit di millisecondi + 12 bit di contatore monotono nello stesso millisecondo + 62 bit casuali.
|
|
10
|
+
* Gli id si ordinano come testo nell'ordine in cui sono stati generati (da questo processo).
|
|
11
|
+
*/
|
|
12
|
+
function uuidv7(now = Date.now()) {
|
|
13
|
+
if (now <= lastMs) {
|
|
14
|
+
now = lastMs;
|
|
15
|
+
seq = (seq + 1) & 0xfff;
|
|
16
|
+
if (seq === 0)
|
|
17
|
+
now = ++lastMs; // contatore esaurito: avanza il millisecondo
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
seq = (0, crypto_1.randomBytes)(2).readUInt16BE(0) & 0x1ff; // parte bassa, lascia spazio al contatore
|
|
21
|
+
}
|
|
22
|
+
lastMs = now;
|
|
23
|
+
const b = (0, crypto_1.randomBytes)(16);
|
|
24
|
+
b.writeUIntBE(now, 0, 6);
|
|
25
|
+
b[6] = 0x70 | ((seq >> 8) & 0x0f);
|
|
26
|
+
b[7] = seq & 0xff;
|
|
27
|
+
b[8] = (b[8] & 0x3f) | 0x80;
|
|
28
|
+
const h = b.toString('hex');
|
|
29
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
30
|
+
}
|
|
31
|
+
exports.UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
32
|
+
//# sourceMappingURL=uuid.js.map
|
package/dist/uuid.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uuid.js","sourceRoot":"","sources":["../src/uuid.ts"],"names":[],"mappings":";;;AASA,wBAgBC;AAzBD,mCAAqC;AAErC,IAAI,MAAM,GAAG,CAAC,CAAC;AACf,IAAI,GAAG,GAAG,CAAC,CAAC;AAEZ;;;GAGG;AACH,SAAgB,MAAM,CAAC,MAAc,IAAI,CAAC,GAAG,EAAE;IAC7C,IAAI,GAAG,IAAI,MAAM,EAAE,CAAC;QAClB,GAAG,GAAG,MAAM,CAAC;QACb,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC;QACxB,IAAI,GAAG,KAAK,CAAC;YAAE,GAAG,GAAG,EAAE,MAAM,CAAC,CAAC,6CAA6C;IAC9E,CAAC;SAAM,CAAC;QACN,GAAG,GAAG,IAAA,oBAAW,EAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,0CAA0C;IAC1F,CAAC;IACD,MAAM,GAAG,GAAG,CAAC;IACb,MAAM,CAAC,GAAG,IAAA,oBAAW,EAAC,EAAE,CAAC,CAAC;IAC1B,CAAC,CAAC,WAAW,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IACzB,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAClC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,GAAG,IAAI,CAAC;IAClB,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;IAC5B,MAAM,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC5B,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC;AACnG,CAAC;AAEY,QAAA,OAAO,GAAG,iEAAiE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,69 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@generazioneai/events",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Eventi durevoli fra i servizi (ADR-009): outbox transazionale, relay su NATS JetStream, consumer durevoli con inbox idempotente, riprove con attesa crescente e dead letter comune.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"sql",
|
|
10
|
+
"prisma",
|
|
11
|
+
"README.md",
|
|
12
|
+
"CHANGELOG.md"
|
|
13
|
+
],
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"registry": "https://registry.npmjs.org",
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/GenerazioneAI-SRL/authz.git",
|
|
21
|
+
"directory": "packages/events"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=22.0.0"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "npm run clean && tsc -p tsconfig.json",
|
|
28
|
+
"clean": "rm -rf dist",
|
|
29
|
+
"test": "jest --maxWorkers=2",
|
|
30
|
+
"typecheck": "tsc -p tsconfig.spec.json",
|
|
31
|
+
"test:prepare": "prisma generate --config test/prisma/prisma.config.ts"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@nats-io/jetstream": "^3.4.0",
|
|
35
|
+
"@nats-io/transport-node": "^3.4.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@generazioneai/authz": "^0.7.0",
|
|
39
|
+
"@nestjs/common": "^11.0.0 || ^12.0.0",
|
|
40
|
+
"@nestjs/core": "^11.0.0 || ^12.0.0",
|
|
41
|
+
"reflect-metadata": "^0.2.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"@generazioneai/authz": {
|
|
45
|
+
"optional": true
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@nestjs/common": "^11.2.7",
|
|
50
|
+
"@nestjs/core": "^11.2.7",
|
|
51
|
+
"@nestjs/testing": "^11.2.7",
|
|
52
|
+
"@prisma/adapter-pg": "^7.8.0",
|
|
53
|
+
"@prisma/client": "^7.8.0",
|
|
54
|
+
"@types/node": "^22.0.0",
|
|
55
|
+
"@types/pg": "^8.15.0",
|
|
56
|
+
"pg": "^8.16.0",
|
|
57
|
+
"prisma": "^7.8.0",
|
|
58
|
+
"rxjs": "^7.8.1",
|
|
59
|
+
"typescript": "^5.1.3"
|
|
60
|
+
},
|
|
61
|
+
"keywords": [
|
|
62
|
+
"jetstream",
|
|
63
|
+
"nats",
|
|
64
|
+
"outbox",
|
|
65
|
+
"events"
|
|
66
|
+
],
|
|
5
67
|
"license": "UNLICENSED",
|
|
6
|
-
"
|
|
7
|
-
"files": ["README.md"]
|
|
68
|
+
"homepage": "https://github.com/GenerazioneAI-SRL/authz/tree/main/packages/events#readme"
|
|
8
69
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// @generazioneai/events 0.1.0 — da copiare nello schema del servizio (con prismaSchemaFolder: prisma/schema/events.prisma).
|
|
2
|
+
// Poi `prisma migrate dev --name events_outbox` (o l'SQL di sql/001-events-outbox-inbox.sql nel registro, per HR).
|
|
3
|
+
// I nomi delle tabelle e delle colonne sono quelli che la libreria usa nelle sue query: non cambiarli.
|
|
4
|
+
model OutboxEvent {
|
|
5
|
+
id String @id @db.Uuid // UUID v7: ordinato nel tempo
|
|
6
|
+
subject String // evt.id.employment.changed
|
|
7
|
+
aggregateType String
|
|
8
|
+
aggregateId String
|
|
9
|
+
aggregateVersion Int
|
|
10
|
+
juridicalId String? // l'azienda, anche in Gen-Tenant
|
|
11
|
+
payload Json // `data` della busta: solo id e stati
|
|
12
|
+
headers Json? // { occurredAt, traceparent? }
|
|
13
|
+
createdAt DateTime @default(now())
|
|
14
|
+
publishedAt DateTime?
|
|
15
|
+
attempts Int @default(0)
|
|
16
|
+
lastError String?
|
|
17
|
+
|
|
18
|
+
@@index([publishedAt, id])
|
|
19
|
+
@@map("outbox_events")
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
model InboxProcessed {
|
|
23
|
+
consumer String
|
|
24
|
+
eventId String @db.Uuid
|
|
25
|
+
processedAt DateTime @default(now())
|
|
26
|
+
|
|
27
|
+
@@id([consumer, eventId])
|
|
28
|
+
@@index([processedAt])
|
|
29
|
+
@@map("inbox_processed")
|
|
30
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
-- @generazioneai/events 0.1.0: outbox e inbox. Solo aggiunte; il rientro è 001-events-outbox-inbox-rientro.sql.
|
|
2
|
+
-- Equivalente SQL di prisma/events.prisma (stesse colonne e tipi di quelli che Prisma 7 genera): copiare nella migrazione
|
|
3
|
+
-- del servizio (Prisma: `prisma migrate`; HR: nel registro SQL con `-- db:` e il suo -rientro).
|
|
4
|
+
CREATE TABLE IF NOT EXISTS "outbox_events" (
|
|
5
|
+
"id" UUID NOT NULL,
|
|
6
|
+
"subject" TEXT NOT NULL,
|
|
7
|
+
"aggregateType" TEXT NOT NULL,
|
|
8
|
+
"aggregateId" TEXT NOT NULL,
|
|
9
|
+
"aggregateVersion" INTEGER NOT NULL,
|
|
10
|
+
"juridicalId" TEXT,
|
|
11
|
+
"payload" JSONB NOT NULL,
|
|
12
|
+
"headers" JSONB,
|
|
13
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
14
|
+
"publishedAt" TIMESTAMP(3),
|
|
15
|
+
"attempts" INTEGER NOT NULL DEFAULT 0,
|
|
16
|
+
"lastError" TEXT,
|
|
17
|
+
CONSTRAINT "outbox_events_pkey" PRIMARY KEY ("id")
|
|
18
|
+
);
|
|
19
|
+
-- il relay legge solo le righe non pubblicate, in ordine di id (UUID v7 = ordine di scrittura)
|
|
20
|
+
CREATE INDEX IF NOT EXISTS "outbox_events_publishedAt_id_idx" ON "outbox_events" ("publishedAt", "id");
|
|
21
|
+
|
|
22
|
+
CREATE TABLE IF NOT EXISTS "inbox_processed" (
|
|
23
|
+
"consumer" TEXT NOT NULL,
|
|
24
|
+
"eventId" UUID NOT NULL,
|
|
25
|
+
"processedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
26
|
+
CONSTRAINT "inbox_processed_pkey" PRIMARY KEY ("consumer", "eventId")
|
|
27
|
+
);
|
|
28
|
+
CREATE INDEX IF NOT EXISTS "inbox_processed_processedAt_idx" ON "inbox_processed" ("processedAt");
|