@remit/backend 0.0.45 → 0.0.47
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/package.json +1 -1
- package/src/derive/deriveMuted.test.ts +34 -0
- package/src/derive/deriveMuted.ts +10 -0
- package/src/derive/enrichThreadRows.test.ts +61 -1
- package/src/derive/enrichThreadRows.ts +20 -9
- package/src/derive/filterThreadCriteria.test.ts +1 -0
- package/src/handlers/filter.test.ts +1 -0
- package/src/handlers/filter.ts +1 -0
- package/src/service/organize.test.ts +4 -0
package/package.json
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import type { AddressFlags } from "@remit/api-openapi-types";
|
|
4
|
+
import { deriveMuted } from "./deriveMuted.js";
|
|
5
|
+
|
|
6
|
+
const SET_AT = 1_700_000_000_000;
|
|
7
|
+
|
|
8
|
+
describe("deriveMuted", () => {
|
|
9
|
+
it("returns false when flags is undefined", () => {
|
|
10
|
+
assert.equal(deriveMuted(undefined), false);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("returns false for an empty flags object", () => {
|
|
14
|
+
assert.equal(deriveMuted({}), false);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("returns false when muted.value is false", () => {
|
|
18
|
+
const flags: AddressFlags = { muted: { value: false, setAt: SET_AT } };
|
|
19
|
+
assert.equal(deriveMuted(flags), false);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("returns true when muted.value is true", () => {
|
|
23
|
+
const flags: AddressFlags = { muted: { value: true, setAt: SET_AT } };
|
|
24
|
+
assert.equal(deriveMuted(flags), true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("ignores other flags (orthogonal axis)", () => {
|
|
28
|
+
const flags: AddressFlags = {
|
|
29
|
+
vip: { value: true, setAt: SET_AT },
|
|
30
|
+
wellknown: { value: true, setAt: SET_AT },
|
|
31
|
+
};
|
|
32
|
+
assert.equal(deriveMuted(flags), false);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AddressFlags } from "@remit/api-openapi-types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Derive whether the From address is muted from an Address's flags map.
|
|
5
|
+
*
|
|
6
|
+
* Pure function, no I/O. Frontend never derives this — single source of
|
|
7
|
+
* truth for filtering the daily brief.
|
|
8
|
+
*/
|
|
9
|
+
export const deriveMuted = (flags: AddressFlags | undefined): boolean =>
|
|
10
|
+
flags?.muted?.value === true;
|
|
@@ -7,11 +7,13 @@ import type {
|
|
|
7
7
|
MessageLabelItem,
|
|
8
8
|
ThreadMessageItem,
|
|
9
9
|
} from "@remit/data-ports";
|
|
10
|
+
import { deriveAddressId } from "@remit/data-ports/id";
|
|
10
11
|
import { type EnrichClient, enrichThreadRows } from "./enrichThreadRows.js";
|
|
11
12
|
|
|
12
13
|
const threadRow = (
|
|
13
14
|
threadMessageId: string,
|
|
14
15
|
messageId: string,
|
|
16
|
+
fromEmail?: string,
|
|
15
17
|
): ThreadMessageItem =>
|
|
16
18
|
({
|
|
17
19
|
threadMessageId,
|
|
@@ -19,6 +21,7 @@ const threadRow = (
|
|
|
19
21
|
messageId,
|
|
20
22
|
accountConfigId: "acc-1",
|
|
21
23
|
mailboxId: "mbx-1",
|
|
24
|
+
fromEmail,
|
|
22
25
|
sentDate: 1,
|
|
23
26
|
isRead: true,
|
|
24
27
|
hasAttachment: false,
|
|
@@ -31,9 +34,10 @@ const threadRow = (
|
|
|
31
34
|
const buildClient = (
|
|
32
35
|
messageLabels: MessageLabelItem[],
|
|
33
36
|
labels: LabelItem[],
|
|
37
|
+
addresses: AddressItem[] = [],
|
|
34
38
|
): EnrichClient => ({
|
|
35
39
|
message: { get: async () => [] as MessageItem[] },
|
|
36
|
-
address: { getAddress: async () =>
|
|
40
|
+
address: { getAddress: async () => addresses },
|
|
37
41
|
messageLabel: {
|
|
38
42
|
listByMessageIds: async (messageIds: string[]) =>
|
|
39
43
|
messageLabels.filter((row) => messageIds.includes(row.messageId)),
|
|
@@ -161,3 +165,59 @@ describe("enrichThreadRows — labels", () => {
|
|
|
161
165
|
assert.equal(second?.labels, undefined);
|
|
162
166
|
});
|
|
163
167
|
});
|
|
168
|
+
|
|
169
|
+
describe("enrichThreadRows — muted", () => {
|
|
170
|
+
const SET_AT = 1_700_000_000_000;
|
|
171
|
+
|
|
172
|
+
test("sets muted true from the batch-fetched Address's flags, no extra query", async () => {
|
|
173
|
+
const fromEmail = "muted@example.com";
|
|
174
|
+
const addressId = deriveAddressId("acc-1", fromEmail);
|
|
175
|
+
const rows = [threadRow("tm-1", "msg-1", fromEmail)];
|
|
176
|
+
const addresses = [
|
|
177
|
+
{
|
|
178
|
+
addressId,
|
|
179
|
+
accountConfigId: "acc-1",
|
|
180
|
+
flags: { muted: { value: true, setAt: SET_AT } },
|
|
181
|
+
},
|
|
182
|
+
] as unknown as AddressItem[];
|
|
183
|
+
|
|
184
|
+
let addressCalls = 0;
|
|
185
|
+
const client: EnrichClient = {
|
|
186
|
+
message: { get: async () => [] as MessageItem[] },
|
|
187
|
+
address: {
|
|
188
|
+
getAddress: async () => {
|
|
189
|
+
addressCalls += 1;
|
|
190
|
+
return addresses;
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
messageLabel: { listByMessageIds: async () => [] },
|
|
194
|
+
label: { listByAccountConfig: async () => [] },
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const [result] = await enrichThreadRows(rows, client, "acc-1");
|
|
198
|
+
assert.equal(result?.muted, true);
|
|
199
|
+
assert.equal(addressCalls, 1);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("defaults muted to false when the Address has no muted flag", async () => {
|
|
203
|
+
const fromEmail = "not-muted@example.com";
|
|
204
|
+
const addressId = deriveAddressId("acc-1", fromEmail);
|
|
205
|
+
const rows = [threadRow("tm-1", "msg-1", fromEmail)];
|
|
206
|
+
const addresses = [
|
|
207
|
+
{ addressId, accountConfigId: "acc-1", flags: {} },
|
|
208
|
+
] as unknown as AddressItem[];
|
|
209
|
+
|
|
210
|
+
const [result] = await enrichThreadRows(
|
|
211
|
+
rows,
|
|
212
|
+
buildClient([], [], addresses),
|
|
213
|
+
"acc-1",
|
|
214
|
+
);
|
|
215
|
+
assert.equal(result?.muted, false);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("defaults muted to false when no Address row resolves", async () => {
|
|
219
|
+
const rows = [threadRow("tm-1", "msg-1")];
|
|
220
|
+
const [result] = await enrichThreadRows(rows, buildClient([], []), "acc-1");
|
|
221
|
+
assert.equal(result?.muted, false);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
import { deriveAddressId } from "@remit/data-ports/id";
|
|
10
10
|
import { SenderTrust, StarColor } from "@remit/domain-enums";
|
|
11
11
|
import { deriveAutoMoved } from "./autoMoved.js";
|
|
12
|
+
import { deriveMuted } from "./deriveMuted.js";
|
|
12
13
|
import { deriveSenderTrust } from "./senderTrust.js";
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -54,6 +55,7 @@ const toResponse = (item: ThreadMessageItem): ThreadMessageResponse => ({
|
|
|
54
55
|
createdAt: item.createdAt,
|
|
55
56
|
updatedAt: item.updatedAt,
|
|
56
57
|
senderTrust: SenderTrust.Unknown,
|
|
58
|
+
muted: false,
|
|
57
59
|
});
|
|
58
60
|
|
|
59
61
|
/**
|
|
@@ -98,20 +100,22 @@ export const planBatchFetch = (rows: ThreadMessageItem[]): BatchPlan => {
|
|
|
98
100
|
};
|
|
99
101
|
|
|
100
102
|
/**
|
|
101
|
-
* Enrich a page of ThreadMessage rows with `senderTrust`
|
|
102
|
-
* Address's flags map), `authenticity` and `autoMoved`
|
|
103
|
-
* Message row, see `deriveAutoMoved`).
|
|
103
|
+
* Enrich a page of ThreadMessage rows with `senderTrust` and `muted` (both
|
|
104
|
+
* derived from the From Address's flags map), `authenticity` and `autoMoved`
|
|
105
|
+
* (both projected from the Message row, see `deriveAutoMoved`).
|
|
104
106
|
*
|
|
105
|
-
* `category` is not enriched: it is denormalized onto the ThreadMessage row
|
|
106
|
-
*
|
|
107
|
-
*
|
|
107
|
+
* `category` is not enriched: it is denormalized onto the ThreadMessage row
|
|
108
|
+
* (shared with `Message.category`'s write-once value, see body-sync.ts) and
|
|
109
|
+
* carried straight through by `toResponse`, so the value a client renders is
|
|
110
|
+
* the value the category filter matched.
|
|
108
111
|
*
|
|
109
112
|
* Two BatchGetItem calls per page, regardless of page size — see
|
|
110
113
|
* `planBatchFetch` for the dedup contract.
|
|
111
114
|
*
|
|
112
|
-
* Missing rows fall back gracefully: `senderTrust` defaults to `"unknown"`,
|
|
113
|
-
* `authenticity` / `autoMoved` are omitted
|
|
114
|
-
* the move isn't a real, in-effect
|
|
115
|
+
* Missing rows fall back gracefully: `senderTrust` defaults to `"unknown"`,
|
|
116
|
+
* `muted` defaults to `false`, and `authenticity` / `autoMoved` are omitted
|
|
117
|
+
* whenever the Message row is absent or the move isn't a real, in-effect
|
|
118
|
+
* auto-move.
|
|
115
119
|
*
|
|
116
120
|
* Not annotated `Promise<ThreadMessageResponse[]>`: `labels` is a new field on
|
|
117
121
|
* it in this same PR, and that package publishes separately from this repo —
|
|
@@ -169,6 +173,9 @@ export const enrichThreadRows = async (
|
|
|
169
173
|
const trustByAddressId = new Map(
|
|
170
174
|
addresses.map((a) => [a.addressId, deriveSenderTrust(a.flags)]),
|
|
171
175
|
);
|
|
176
|
+
const mutedByAddressId = new Map(
|
|
177
|
+
addresses.map((a) => [a.addressId, deriveMuted(a.flags)]),
|
|
178
|
+
);
|
|
172
179
|
|
|
173
180
|
return rows.map((row) => {
|
|
174
181
|
const base = toResponse(row);
|
|
@@ -178,6 +185,9 @@ export const enrichThreadRows = async (
|
|
|
178
185
|
const senderTrust = addressId
|
|
179
186
|
? (trustByAddressId.get(addressId) ?? SenderTrust.Unknown)
|
|
180
187
|
: SenderTrust.Unknown;
|
|
188
|
+
const muted = addressId
|
|
189
|
+
? (mutedByAddressId.get(addressId) ?? false)
|
|
190
|
+
: false;
|
|
181
191
|
const labels = labelsByMessageId.get(row.messageId);
|
|
182
192
|
return {
|
|
183
193
|
...base,
|
|
@@ -185,6 +195,7 @@ export const enrichThreadRows = async (
|
|
|
185
195
|
...(autoMoved !== undefined ? { autoMoved } : {}),
|
|
186
196
|
...(labels !== undefined ? { labels } : {}),
|
|
187
197
|
senderTrust,
|
|
198
|
+
muted,
|
|
188
199
|
};
|
|
189
200
|
});
|
|
190
201
|
};
|
|
@@ -199,6 +199,7 @@ describe("createFilterWithAnchor (#351)", () => {
|
|
|
199
199
|
state: FilterState.Active,
|
|
200
200
|
hasAnchor: false,
|
|
201
201
|
ruleChangedAt: 1_700_000_000,
|
|
202
|
+
actionChangedAt: 1_700_000_000,
|
|
202
203
|
matchOperator: FilterMatchOperator.And,
|
|
203
204
|
literalClauses: [],
|
|
204
205
|
actionLabelId: "None",
|
package/src/handlers/filter.ts
CHANGED
|
@@ -187,6 +187,7 @@ const toFilterResponse = (item: FilterItem): FilterResponse => ({
|
|
|
187
187
|
state: item.state,
|
|
188
188
|
hasAnchor: item.hasAnchor,
|
|
189
189
|
ruleChangedAt: item.ruleChangedAt,
|
|
190
|
+
actionChangedAt: item.actionChangedAt,
|
|
190
191
|
matchOperator: item.matchOperator,
|
|
191
192
|
literalClauses: item.literalClauses,
|
|
192
193
|
actionLabelId: item.actionLabelId,
|
|
@@ -79,6 +79,7 @@ const filterItem = (over: Partial<FilterItem> = {}): FilterItem => ({
|
|
|
79
79
|
state: FilterState.Active,
|
|
80
80
|
hasAnchor: false,
|
|
81
81
|
ruleChangedAt: 0,
|
|
82
|
+
actionChangedAt: 0,
|
|
82
83
|
matchOperator: FilterMatchOperator.And,
|
|
83
84
|
literalClauses: [],
|
|
84
85
|
actionLabelId: "None",
|
|
@@ -780,6 +781,7 @@ describe("applyOrganize resolves move precedence against current Active filters
|
|
|
780
781
|
const newerFilter = filterItem({
|
|
781
782
|
filterId: "filter-newer",
|
|
782
783
|
ruleChangedAt: 1_000,
|
|
784
|
+
actionChangedAt: 1_000,
|
|
783
785
|
actionMailboxId: "mbox-new",
|
|
784
786
|
literalClauses: [{ field: "Subject", value: "reservation" }],
|
|
785
787
|
});
|
|
@@ -828,6 +830,7 @@ describe("applyOrganize resolves move precedence against current Active filters
|
|
|
828
830
|
const agreeingFilter = filterItem({
|
|
829
831
|
filterId: "filter-agrees",
|
|
830
832
|
ruleChangedAt: 1_000,
|
|
833
|
+
actionChangedAt: 1_000,
|
|
831
834
|
actionMailboxId: "mbox-target",
|
|
832
835
|
literalClauses: [{ field: "Subject", value: "reservation" }],
|
|
833
836
|
});
|
|
@@ -869,6 +872,7 @@ describe("applyOrganize resolves move precedence against current Active filters
|
|
|
869
872
|
const newerSemanticFilter = filterItem({
|
|
870
873
|
filterId: "filter-newer-semantic",
|
|
871
874
|
ruleChangedAt: 1_000,
|
|
875
|
+
actionChangedAt: 1_000,
|
|
872
876
|
actionMailboxId: "mbox-new",
|
|
873
877
|
hasAnchor: true,
|
|
874
878
|
});
|