@remit/drizzle-service 0.0.102 → 0.0.104
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 +4 -3
- package/src/batch.sqlite.test.ts +4 -4
- package/src/batch.ts +47 -21
- package/src/databases.ts +24 -6
- package/src/db.ts +20 -5
- package/src/index.ts +12 -0
- package/src/repair/junk-only-address.sqlite.test.ts +7 -2
- package/src/repos/account.conformance.d1.test.ts +20 -0
- package/src/repos/account.conformance.sqlite.test.ts +6 -2
- package/src/repos/calendar-collection.conformance.d1.test.ts +20 -0
- package/src/repos/calendar-collection.ts +2 -2
- package/src/repos/calendar-default.d1.test.ts +4 -0
- package/src/repos/calendar-default.sqlite.test.ts +3 -72
- package/src/repos/calendar-default.suite.ts +68 -0
- package/src/repos/calendar-event-index.conformance.d1.test.ts +20 -0
- package/src/repos/calendar-event-index.ts +2 -2
- package/src/repos/calendar-feed-token.conformance.d1.test.ts +20 -0
- package/src/repos/calendar-feed-token.ts +2 -2
- package/src/repos/calendar-object.conformance.d1.test.ts +20 -0
- package/src/repos/calendar-object.ts +2 -2
- package/src/repos/calendar-put.d1.test.ts +4 -0
- package/src/repos/calendar-put.sqlite.test.ts +3 -205
- package/src/repos/calendar-put.suite.ts +210 -0
- package/src/repos/calendar-store.ts +36 -40
- package/src/repos/calendar-suggestion.ts +2 -2
- package/src/repos/cascade-delete.test.ts +3 -2
- package/src/repos/cascade-delete.ts +12 -8
- package/src/repos/envelope.sqlite.test.ts +3 -2
- package/src/repos/envelope.test.ts +3 -2
- package/src/repos/envelope.ts +14 -9
- package/src/repos/filter.d1.test.ts +4 -0
- package/src/repos/filter.suite.ts +476 -0
- package/src/repos/filter.test.ts +3 -472
- package/src/repos/filter.ts +2 -2
- package/src/repos/i4-account-config.d1.test.ts +4 -0
- package/src/repos/i4-account-config.suite.ts +131 -0
- package/src/repos/i4-account-config.test.ts +3 -123
- package/src/repos/i4-account-config.ts +8 -10
- package/src/repos/i4-account-export-request.d1.test.ts +4 -0
- package/src/repos/i4-account-export-request.suite.ts +44 -0
- package/src/repos/i4-account-export-request.test.ts +3 -40
- package/src/repos/i4-account-export-request.ts +2 -2
- package/src/repos/i4-account-setting.d1.test.ts +4 -0
- package/src/repos/i4-account-setting.suite.ts +100 -0
- package/src/repos/i4-account-setting.test.ts +3 -93
- package/src/repos/i4-account-setting.ts +2 -2
- package/src/repos/i4-account.test.ts +12 -8
- package/src/repos/i4-account.ts +6 -10
- package/src/repos/i4-address-junk-move.sqlite.test.ts +7 -2
- package/src/repos/i4-address.ts +12 -2
- package/src/repos/i4-config-import.ts +2 -2
- package/src/repos/i4-mailbox-lock.ts +2 -2
- package/src/repos/i4-mailbox-special-use.sqlite.test.ts +12 -10
- package/src/repos/i4-mailbox-special-use.ts +29 -40
- package/src/repos/i4-mailbox.sqlite.test.ts +12 -10
- package/src/repos/i4-mailbox.test.ts +15 -14
- package/src/repos/i4-mailbox.ts +13 -7
- package/src/repos/i4-message-flag-push.ts +2 -2
- package/src/repos/i4-message-placement-move.ts +2 -2
- package/src/repos/i4-organize-job-request.ts +2 -2
- package/src/repos/i4-outbox-attachment.test.ts +7 -2
- package/src/repos/i4-outbox-attachment.ts +11 -5
- package/src/repos/i4-outbox-message.ts +2 -2
- package/src/repos/i4-sender-signer-standing.ts +2 -2
- package/src/repos/label.conformance.d1.test.ts +20 -0
- package/src/repos/label.ts +2 -2
- package/src/repos/message-flag.ts +3 -6
- package/src/repos/message-label.ts +2 -2
- package/src/repos/message.sqlite.test.ts +57 -3
- package/src/repos/message.test.ts +6 -5
- package/src/repos/message.ts +81 -10
- package/src/repos/quarantine.ts +2 -2
- package/src/repos/sync-warning.ts +2 -2
- package/src/repos/thread-message-body-muted.sqlite.test.ts +30 -22
- package/src/repos/thread-message-field-terms.sqlite.test.ts +16 -11
- package/src/repos/thread-message.sqlite.test.ts +13 -7
- package/src/repos/thread-message.test.ts +43 -27
- package/src/repos/thread-message.ts +26 -14
- package/src/repos/thread-search-predicates.ts +36 -63
- package/src/schema/message-data.ts +3 -0
- package/src/schema-sets/mail.ts +1 -0
- package/src/search/thread-message-search.sqlite.test.ts +57 -11
- package/src/search/thread-message-search.ts +26 -12
- package/src/sqlite-repositories.ts +48 -32
- package/src/test-d1.ts +40 -0
- package/src/test-db-sqlite.ts +1 -1
- package/src/test-shipped-sqlite-schema.ts +8 -6
- package/src/test-stores.ts +30 -0
package/src/repos/filter.test.ts
CHANGED
|
@@ -1,473 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
FilterClauseField,
|
|
5
|
-
FilterDisabledReason,
|
|
6
|
-
FilterScope,
|
|
7
|
-
FilterState,
|
|
8
|
-
} from "@remit/domain-enums";
|
|
9
|
-
import { NotFoundError } from "../error.js";
|
|
10
|
-
import { createTestDb, randomId, type TestDb } from "../test-db.js";
|
|
11
|
-
import { FilterRepo } from "./filter.js";
|
|
1
|
+
import { openSqliteTestStores } from "../test-stores.js";
|
|
2
|
+
import { filterRepoSuite } from "./filter.suite.js";
|
|
12
3
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
describe("FilterRepo", () => {
|
|
16
|
-
let db: TestDb;
|
|
17
|
-
let close: () => Promise<void>;
|
|
18
|
-
let repo: FilterRepo;
|
|
19
|
-
|
|
20
|
-
before(async () => {
|
|
21
|
-
({ db, close } = await createTestDb());
|
|
22
|
-
repo = new FilterRepo(db as never);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
after(async () => {
|
|
26
|
-
await close();
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test("create fills in every defaulted field, never leaving it absent", async () => {
|
|
30
|
-
const accountConfigId = randomId();
|
|
31
|
-
const filter = await repo.create({
|
|
32
|
-
accountConfigId,
|
|
33
|
-
name: "Booking confirmations",
|
|
34
|
-
scope: FilterScope.Standing,
|
|
35
|
-
literalClauses: CLAUSES,
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
assert.equal(filter.state, FilterState.Active);
|
|
39
|
-
assert.equal(filter.disabledReason, FilterDisabledReason.None);
|
|
40
|
-
assert.deepEqual(filter.literalClauses, CLAUSES);
|
|
41
|
-
assert.equal(filter.actionLabelId, "None");
|
|
42
|
-
assert.equal(filter.actionMailboxId, "None");
|
|
43
|
-
assert.ok(
|
|
44
|
-
filter.ruleChangedAt > 0,
|
|
45
|
-
"ruleChangedAt should be set on create",
|
|
46
|
-
);
|
|
47
|
-
assert.ok(
|
|
48
|
-
filter.actionChangedAt > 0,
|
|
49
|
-
"actionChangedAt should be set on create",
|
|
50
|
-
);
|
|
51
|
-
assert.equal(filter.expiresAt, undefined);
|
|
52
|
-
assert.equal(filter.ttl, undefined);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test("a disabled filter keeps its reason and drops out of the Active listing (#1103)", async () => {
|
|
56
|
-
const accountConfigId = randomId();
|
|
57
|
-
const filter = await repo.create({
|
|
58
|
-
accountConfigId,
|
|
59
|
-
name: "Invoices",
|
|
60
|
-
scope: FilterScope.Standing,
|
|
61
|
-
literalClauses: CLAUSES,
|
|
62
|
-
state: FilterState.Disabled,
|
|
63
|
-
disabledReason: FilterDisabledReason.AwaitingFolder,
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
assert.equal(filter.state, FilterState.Disabled);
|
|
67
|
-
assert.equal(filter.disabledReason, FilterDisabledReason.AwaitingFolder);
|
|
68
|
-
assert.deepEqual(
|
|
69
|
-
await repo.listByAccountAndState(accountConfigId, FilterState.Active),
|
|
70
|
-
[],
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
const enabled = await repo.update(accountConfigId, filter.filterId, {
|
|
74
|
-
state: FilterState.Active,
|
|
75
|
-
disabledReason: FilterDisabledReason.None,
|
|
76
|
-
});
|
|
77
|
-
assert.equal(enabled.disabledReason, FilterDisabledReason.None);
|
|
78
|
-
assert.deepEqual(
|
|
79
|
-
(
|
|
80
|
-
await repo.listByAccountAndState(accountConfigId, FilterState.Active)
|
|
81
|
-
).map((item) => item.filterId),
|
|
82
|
-
[filter.filterId],
|
|
83
|
-
);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test("a Temporary filter round-trips expiresAt and ttl", async () => {
|
|
87
|
-
const accountConfigId = randomId();
|
|
88
|
-
const expiresAt = "2026-08-01T00:00:00+02:00";
|
|
89
|
-
const ttl = Math.floor(new Date(expiresAt).getTime() / 1000);
|
|
90
|
-
|
|
91
|
-
const filter = await repo.create({
|
|
92
|
-
accountConfigId,
|
|
93
|
-
name: "Until my trip",
|
|
94
|
-
scope: FilterScope.Temporary,
|
|
95
|
-
literalClauses: CLAUSES,
|
|
96
|
-
expiresAt,
|
|
97
|
-
ttl,
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
assert.equal(filter.scope, FilterScope.Temporary);
|
|
101
|
-
assert.equal(filter.expiresAt, expiresAt);
|
|
102
|
-
assert.equal(filter.ttl, ttl);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test("create round-trips literal clauses", async () => {
|
|
106
|
-
const accountConfigId = randomId();
|
|
107
|
-
const literalClauses = [
|
|
108
|
-
{ field: "From", value: "billing@example.com" },
|
|
109
|
-
{ field: "Subject", value: "invoice" },
|
|
110
|
-
] as const;
|
|
111
|
-
|
|
112
|
-
const filter = await repo.create({
|
|
113
|
-
accountConfigId,
|
|
114
|
-
name: "Invoices",
|
|
115
|
-
scope: FilterScope.Standing,
|
|
116
|
-
literalClauses: [...literalClauses],
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
assert.deepEqual(filter.literalClauses, literalClauses);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test("get throws NotFoundError for a missing filter", async () => {
|
|
123
|
-
await assert.rejects(repo.get(randomId(), randomId()), NotFoundError);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
test("update does not bump ruleChangedAt on a cosmetic rename", async () => {
|
|
127
|
-
const accountConfigId = randomId();
|
|
128
|
-
const filter = await repo.create({
|
|
129
|
-
accountConfigId,
|
|
130
|
-
name: "Newsletter admin",
|
|
131
|
-
scope: FilterScope.Standing,
|
|
132
|
-
literalClauses: CLAUSES,
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
const renamed = await repo.update(accountConfigId, filter.filterId, {
|
|
136
|
-
name: "Newsletter admin (renamed)",
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
assert.equal(renamed.name, "Newsletter admin (renamed)");
|
|
140
|
-
assert.equal(
|
|
141
|
-
renamed.ruleChangedAt,
|
|
142
|
-
filter.ruleChangedAt,
|
|
143
|
-
"a name-only edit must not move ruleChangedAt (RFC 034 Decision 3.2)",
|
|
144
|
-
);
|
|
145
|
-
assert.equal(
|
|
146
|
-
renamed.actionChangedAt,
|
|
147
|
-
filter.actionChangedAt,
|
|
148
|
-
"a name-only edit must not move actionChangedAt either",
|
|
149
|
-
);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test("update bumps both ruleChangedAt and actionChangedAt when the action changes", async () => {
|
|
153
|
-
const accountConfigId = randomId();
|
|
154
|
-
const filter = await repo.create({
|
|
155
|
-
accountConfigId,
|
|
156
|
-
name: "Move to archive",
|
|
157
|
-
scope: FilterScope.Standing,
|
|
158
|
-
literalClauses: CLAUSES,
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
await new Promise((resolve) => setTimeout(resolve, 1100));
|
|
162
|
-
|
|
163
|
-
const updated = await repo.update(accountConfigId, filter.filterId, {
|
|
164
|
-
actionMailboxId: randomId(),
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
assert.ok(
|
|
168
|
-
updated.ruleChangedAt > filter.ruleChangedAt,
|
|
169
|
-
"an action edit must move ruleChangedAt forward",
|
|
170
|
-
);
|
|
171
|
-
assert.ok(
|
|
172
|
-
updated.actionChangedAt > filter.actionChangedAt,
|
|
173
|
-
"an action edit must move actionChangedAt forward too — it is what a matching message's exclusive move goes by",
|
|
174
|
-
);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test("update bumps ruleChangedAt but NOT actionChangedAt when scope changes (reader #266, #384)", async () => {
|
|
178
|
-
const accountConfigId = randomId();
|
|
179
|
-
const filter = await repo.create({
|
|
180
|
-
accountConfigId,
|
|
181
|
-
name: "Trip out-of-office",
|
|
182
|
-
scope: FilterScope.Standing,
|
|
183
|
-
literalClauses: CLAUSES,
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
await new Promise((resolve) => setTimeout(resolve, 1100));
|
|
187
|
-
|
|
188
|
-
const expiresAt = "2027-01-01T00:00:00+00:00";
|
|
189
|
-
const updated = await repo.update(accountConfigId, filter.filterId, {
|
|
190
|
-
scope: FilterScope.Temporary,
|
|
191
|
-
expiresAt,
|
|
192
|
-
ttl: Math.floor(new Date(expiresAt).getTime() / 1000),
|
|
193
|
-
state: FilterState.Active,
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
assert.equal(updated.scope, FilterScope.Temporary);
|
|
197
|
-
assert.equal(updated.expiresAt, expiresAt);
|
|
198
|
-
assert.ok(
|
|
199
|
-
updated.ruleChangedAt > filter.ruleChangedAt,
|
|
200
|
-
"a scope edit must move ruleChangedAt forward, so a lapsed filter's back-application can be offered again",
|
|
201
|
-
);
|
|
202
|
-
assert.equal(
|
|
203
|
-
updated.actionChangedAt,
|
|
204
|
-
filter.actionChangedAt,
|
|
205
|
-
"a scope edit changes the filter's lifecycle, not what it matches or does — it must not move actionChangedAt, or it would wrongly promote the filter to move-winner",
|
|
206
|
-
);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
test("update bumps ruleChangedAt but NOT actionChangedAt when only expiresAt changes (reader #266, #384)", async () => {
|
|
210
|
-
const accountConfigId = randomId();
|
|
211
|
-
const initialExpiresAt = "2026-08-01T00:00:00+00:00";
|
|
212
|
-
const filter = await repo.create({
|
|
213
|
-
accountConfigId,
|
|
214
|
-
name: "Until my trip",
|
|
215
|
-
scope: FilterScope.Temporary,
|
|
216
|
-
literalClauses: CLAUSES,
|
|
217
|
-
expiresAt: initialExpiresAt,
|
|
218
|
-
ttl: Math.floor(new Date(initialExpiresAt).getTime() / 1000),
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
await new Promise((resolve) => setTimeout(resolve, 1100));
|
|
222
|
-
|
|
223
|
-
const extendedExpiresAt = "2026-09-01T00:00:00+00:00";
|
|
224
|
-
const updated = await repo.update(accountConfigId, filter.filterId, {
|
|
225
|
-
expiresAt: extendedExpiresAt,
|
|
226
|
-
ttl: Math.floor(new Date(extendedExpiresAt).getTime() / 1000),
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
assert.equal(updated.expiresAt, extendedExpiresAt);
|
|
230
|
-
assert.ok(
|
|
231
|
-
updated.ruleChangedAt > filter.ruleChangedAt,
|
|
232
|
-
"an expiresAt edit must move ruleChangedAt forward",
|
|
233
|
-
);
|
|
234
|
-
assert.equal(
|
|
235
|
-
updated.actionChangedAt,
|
|
236
|
-
filter.actionChangedAt,
|
|
237
|
-
"extending a filter's expiry alone must not promote it to move-winner (reader #384) — actionChangedAt must not move",
|
|
238
|
-
);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
test("update to Standing clears expiresAt and ttl at the storage layer, even if the caller didn't null them (reader #266)", async () => {
|
|
242
|
-
const accountConfigId = randomId();
|
|
243
|
-
const expiresAt = "2026-08-01T00:00:00+00:00";
|
|
244
|
-
const filter = await repo.create({
|
|
245
|
-
accountConfigId,
|
|
246
|
-
name: "Until my trip",
|
|
247
|
-
scope: FilterScope.Temporary,
|
|
248
|
-
literalClauses: CLAUSES,
|
|
249
|
-
expiresAt,
|
|
250
|
-
ttl: Math.floor(new Date(expiresAt).getTime() / 1000),
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
const updated = await repo.update(accountConfigId, filter.filterId, {
|
|
254
|
-
scope: FilterScope.Standing,
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
assert.equal(updated.scope, FilterScope.Standing);
|
|
258
|
-
assert.equal(updated.expiresAt, undefined);
|
|
259
|
-
assert.equal(updated.ttl, undefined);
|
|
260
|
-
|
|
261
|
-
const reread = await repo.get(accountConfigId, filter.filterId);
|
|
262
|
-
assert.equal(reread.expiresAt, undefined);
|
|
263
|
-
assert.equal(reread.ttl, undefined);
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
test("update throws NotFoundError for a missing filter", async () => {
|
|
267
|
-
await assert.rejects(
|
|
268
|
-
repo.update(randomId(), randomId(), { name: "x" }),
|
|
269
|
-
NotFoundError,
|
|
270
|
-
);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
test("listByAccountConfig and listByAccountAndState scope to the account", async () => {
|
|
274
|
-
const accountConfigId = randomId();
|
|
275
|
-
const other = randomId();
|
|
276
|
-
|
|
277
|
-
const standing = await repo.create({
|
|
278
|
-
accountConfigId,
|
|
279
|
-
name: "Standing",
|
|
280
|
-
scope: FilterScope.Standing,
|
|
281
|
-
literalClauses: CLAUSES,
|
|
282
|
-
});
|
|
283
|
-
await repo.create({
|
|
284
|
-
accountConfigId: other,
|
|
285
|
-
name: "Foreign",
|
|
286
|
-
scope: FilterScope.Standing,
|
|
287
|
-
literalClauses: CLAUSES,
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
const all = await repo.listByAccountConfig(accountConfigId);
|
|
291
|
-
assert.equal(all.length, 1);
|
|
292
|
-
assert.equal(all[0]?.filterId, standing.filterId);
|
|
293
|
-
|
|
294
|
-
const active = await repo.listByAccountAndState(
|
|
295
|
-
accountConfigId,
|
|
296
|
-
FilterState.Active,
|
|
297
|
-
);
|
|
298
|
-
assert.equal(active.length, 1);
|
|
299
|
-
assert.equal(active[0]?.filterId, standing.filterId);
|
|
300
|
-
|
|
301
|
-
const expired = await repo.listByAccountAndState(
|
|
302
|
-
accountConfigId,
|
|
303
|
-
FilterState.Expired,
|
|
304
|
-
);
|
|
305
|
-
assert.equal(expired.length, 0);
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
test("refreshExpiry leaves a Standing filter untouched", async () => {
|
|
309
|
-
const accountConfigId = randomId();
|
|
310
|
-
const filter = await repo.create({
|
|
311
|
-
accountConfigId,
|
|
312
|
-
name: "Standing",
|
|
313
|
-
scope: FilterScope.Standing,
|
|
314
|
-
literalClauses: CLAUSES,
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
const result = await repo.refreshExpiry(filter);
|
|
318
|
-
assert.equal(result.state, FilterState.Active);
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
test("refreshExpiry leaves a not-yet-expired Temporary filter Active", async () => {
|
|
322
|
-
const accountConfigId = randomId();
|
|
323
|
-
const future = new Date(Date.now() + 86_400_000).toISOString();
|
|
324
|
-
const filter = await repo.create({
|
|
325
|
-
accountConfigId,
|
|
326
|
-
name: "Until tomorrow",
|
|
327
|
-
scope: FilterScope.Temporary,
|
|
328
|
-
literalClauses: CLAUSES,
|
|
329
|
-
expiresAt: future,
|
|
330
|
-
ttl: Math.floor(new Date(future).getTime() / 1000),
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
const result = await repo.refreshExpiry(filter);
|
|
334
|
-
assert.equal(result.state, FilterState.Active);
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
test("refreshExpiry leaves a disabled filter and its reason alone (#1103)", async () => {
|
|
338
|
-
const accountConfigId = randomId();
|
|
339
|
-
const past = new Date(Date.now() - 86_400_000).toISOString();
|
|
340
|
-
const filter = await repo.create({
|
|
341
|
-
accountConfigId,
|
|
342
|
-
name: "Until yesterday, awaiting its folder",
|
|
343
|
-
scope: FilterScope.Temporary,
|
|
344
|
-
literalClauses: CLAUSES,
|
|
345
|
-
expiresAt: past,
|
|
346
|
-
ttl: Math.floor(new Date(past).getTime() / 1000) + 172_800,
|
|
347
|
-
state: FilterState.Disabled,
|
|
348
|
-
disabledReason: FilterDisabledReason.AwaitingFolder,
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
const result = await repo.refreshExpiry(filter);
|
|
352
|
-
assert.equal(result.state, FilterState.Disabled);
|
|
353
|
-
assert.equal(result.disabledReason, FilterDisabledReason.AwaitingFolder);
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
test("refreshExpiry lazily patches a past-expiresAt Temporary filter to Expired", async () => {
|
|
357
|
-
const accountConfigId = randomId();
|
|
358
|
-
const past = new Date(Date.now() - 86_400_000).toISOString();
|
|
359
|
-
const filter = await repo.create({
|
|
360
|
-
accountConfigId,
|
|
361
|
-
name: "Until yesterday",
|
|
362
|
-
scope: FilterScope.Temporary,
|
|
363
|
-
literalClauses: CLAUSES,
|
|
364
|
-
expiresAt: past,
|
|
365
|
-
ttl: Math.floor(new Date(past).getTime() / 1000) + 172_800,
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
assert.equal(filter.state, FilterState.Active, "starts Active on create");
|
|
369
|
-
|
|
370
|
-
const result = await repo.refreshExpiry(filter);
|
|
371
|
-
assert.equal(result.state, FilterState.Expired);
|
|
372
|
-
|
|
373
|
-
const reread = await repo.get(accountConfigId, filter.filterId);
|
|
374
|
-
assert.equal(
|
|
375
|
-
reread.state,
|
|
376
|
-
FilterState.Expired,
|
|
377
|
-
"the patch is persisted, not just returned",
|
|
378
|
-
);
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
test("delete removes the row", async () => {
|
|
382
|
-
const accountConfigId = randomId();
|
|
383
|
-
const filter = await repo.create({
|
|
384
|
-
accountConfigId,
|
|
385
|
-
name: "To delete",
|
|
386
|
-
scope: FilterScope.Standing,
|
|
387
|
-
literalClauses: CLAUSES,
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
await repo.delete(accountConfigId, filter.filterId);
|
|
391
|
-
|
|
392
|
-
await assert.rejects(
|
|
393
|
-
repo.get(accountConfigId, filter.filterId),
|
|
394
|
-
NotFoundError,
|
|
395
|
-
);
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
test("listPageByAccountConfig round-trips a continuationToken across pages", async () => {
|
|
399
|
-
const accountConfigId = randomId();
|
|
400
|
-
const created = await Promise.all([
|
|
401
|
-
repo.create({
|
|
402
|
-
accountConfigId,
|
|
403
|
-
name: "One",
|
|
404
|
-
scope: FilterScope.Standing,
|
|
405
|
-
literalClauses: CLAUSES,
|
|
406
|
-
}),
|
|
407
|
-
repo.create({
|
|
408
|
-
accountConfigId,
|
|
409
|
-
name: "Two",
|
|
410
|
-
scope: FilterScope.Standing,
|
|
411
|
-
literalClauses: CLAUSES,
|
|
412
|
-
}),
|
|
413
|
-
repo.create({
|
|
414
|
-
accountConfigId,
|
|
415
|
-
name: "Three",
|
|
416
|
-
scope: FilterScope.Standing,
|
|
417
|
-
literalClauses: CLAUSES,
|
|
418
|
-
}),
|
|
419
|
-
]);
|
|
420
|
-
|
|
421
|
-
const first = await repo.listPageByAccountConfig(accountConfigId, {
|
|
422
|
-
limit: 2,
|
|
423
|
-
});
|
|
424
|
-
assert.equal(first.items.length, 2);
|
|
425
|
-
assert.ok(
|
|
426
|
-
first.continuationToken,
|
|
427
|
-
"a full page returns a continuationToken",
|
|
428
|
-
);
|
|
429
|
-
|
|
430
|
-
const second = await repo.listPageByAccountConfig(accountConfigId, {
|
|
431
|
-
limit: 2,
|
|
432
|
-
continuationToken: first.continuationToken,
|
|
433
|
-
});
|
|
434
|
-
assert.equal(second.items.length, 1);
|
|
435
|
-
assert.equal(
|
|
436
|
-
second.continuationToken,
|
|
437
|
-
undefined,
|
|
438
|
-
"the last page carries no continuationToken",
|
|
439
|
-
);
|
|
440
|
-
|
|
441
|
-
const seen = new Set([
|
|
442
|
-
...first.items.map((f) => f.filterId),
|
|
443
|
-
...second.items.map((f) => f.filterId),
|
|
444
|
-
]);
|
|
445
|
-
assert.equal(
|
|
446
|
-
seen.size,
|
|
447
|
-
created.length,
|
|
448
|
-
"every filter is paged exactly once",
|
|
449
|
-
);
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
describe("continuation token rejection (#172)", () => {
|
|
453
|
-
for (const [label, token] of [
|
|
454
|
-
["an unparseable", "not-a-cursor"],
|
|
455
|
-
["a bare number", Buffer.from("123").toString("base64url")],
|
|
456
|
-
["a JSON array", Buffer.from("[1,2]").toString("base64url")],
|
|
457
|
-
] as const) {
|
|
458
|
-
test(`${label} token is rejected as a 400`, async () => {
|
|
459
|
-
await assert.rejects(
|
|
460
|
-
() =>
|
|
461
|
-
repo.listPageByAccountConfig(randomId(), {
|
|
462
|
-
continuationToken: token,
|
|
463
|
-
}),
|
|
464
|
-
(error: unknown) => {
|
|
465
|
-
assert.equal((error as { statusCode?: number }).statusCode, 400);
|
|
466
|
-
assert.equal((error as Error).name, "BadRequestError");
|
|
467
|
-
return true;
|
|
468
|
-
},
|
|
469
|
-
);
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
});
|
|
473
|
-
});
|
|
4
|
+
filterRepoSuite(openSqliteTestStores);
|
package/src/repos/filter.ts
CHANGED
|
@@ -12,13 +12,13 @@ import {
|
|
|
12
12
|
FilterState,
|
|
13
13
|
} from "@remit/domain-enums";
|
|
14
14
|
import { and, asc, eq, gt, or } from "drizzle-orm";
|
|
15
|
-
import type {
|
|
15
|
+
import type { RepositoryDb } from "../db.js";
|
|
16
16
|
import { NotFoundError } from "../error.js";
|
|
17
17
|
import { randomId } from "../id.js";
|
|
18
18
|
import { decodeToken, resultList } from "../pagination.js";
|
|
19
19
|
import { filterTable } from "../schema.js";
|
|
20
20
|
|
|
21
|
-
type DB =
|
|
21
|
+
type DB = RepositoryDb<Record<string, unknown>>;
|
|
22
22
|
|
|
23
23
|
const RULE_ASSERTION_FIELDS = [
|
|
24
24
|
"matchOperator",
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { after, before, describe, test } from "node:test";
|
|
3
|
+
import { randomId } from "../id.js";
|
|
4
|
+
import {
|
|
5
|
+
composeSqliteRepositories,
|
|
6
|
+
type SqliteRepositories,
|
|
7
|
+
} from "../sqlite-repositories.js";
|
|
8
|
+
import type { OpenTestStores } from "../test-stores.js";
|
|
9
|
+
import type { AccountConfigRepo } from "./i4-account-config.js";
|
|
10
|
+
|
|
11
|
+
export const accountConfigRepoSuite = (open: OpenTestStores): void => {
|
|
12
|
+
describe("AccountConfigRepo", () => {
|
|
13
|
+
let close: () => Promise<void>;
|
|
14
|
+
let repos: SqliteRepositories;
|
|
15
|
+
let repo: AccountConfigRepo;
|
|
16
|
+
|
|
17
|
+
before(async () => {
|
|
18
|
+
const opened = await open();
|
|
19
|
+
close = opened.close;
|
|
20
|
+
repos = composeSqliteRepositories(opened.stores);
|
|
21
|
+
repo = repos.accountConfig;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
after(async () => {
|
|
25
|
+
await close();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("create and get", async () => {
|
|
29
|
+
const userId = randomId();
|
|
30
|
+
const cfg = await repo.create({ userId, name: "My Config" });
|
|
31
|
+
|
|
32
|
+
assert.ok(cfg.accountConfigId);
|
|
33
|
+
assert.equal(cfg.userId, userId);
|
|
34
|
+
assert.equal(cfg.name, "My Config");
|
|
35
|
+
assert.equal(cfg.state, "active");
|
|
36
|
+
|
|
37
|
+
const fetched = await repo.get(cfg.accountConfigId);
|
|
38
|
+
assert.equal(fetched.accountConfigId, cfg.accountConfigId);
|
|
39
|
+
|
|
40
|
+
await repo.delete(cfg.accountConfigId);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("batchGet: WHERE id = ANY($1)", async () => {
|
|
44
|
+
const userId = randomId();
|
|
45
|
+
const c1 = await repo.create({ userId });
|
|
46
|
+
const c2 = await repo.create({ userId });
|
|
47
|
+
|
|
48
|
+
const results = await repo.get([c1.accountConfigId, c2.accountConfigId]);
|
|
49
|
+
assert.equal(results.length, 2);
|
|
50
|
+
|
|
51
|
+
await repo.deleteMany([c1.accountConfigId, c2.accountConfigId]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("batchGet empty array returns []", async () => {
|
|
55
|
+
const results = await repo.get([]);
|
|
56
|
+
assert.deepEqual(results, []);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("get throws for missing config", async () => {
|
|
60
|
+
await assert.rejects(repo.get(randomId()), /not found/i);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("describe assembles collection (accountConfig + account + address)", async () => {
|
|
64
|
+
const userId = randomId();
|
|
65
|
+
const cfg = await repo.create({ userId });
|
|
66
|
+
|
|
67
|
+
const accountRepo = repos.account;
|
|
68
|
+
const addrRepo = repos.address;
|
|
69
|
+
|
|
70
|
+
await accountRepo.create({
|
|
71
|
+
accountConfigId: cfg.accountConfigId,
|
|
72
|
+
username: "u",
|
|
73
|
+
email: "u@test.com",
|
|
74
|
+
isActive: true,
|
|
75
|
+
imapHost: "imap.test.com",
|
|
76
|
+
imapPort: 993,
|
|
77
|
+
imapTls: true,
|
|
78
|
+
imapStartTls: false,
|
|
79
|
+
connectionState: "not_authenticated",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await addrRepo.createAddress({
|
|
83
|
+
addressId: randomId(),
|
|
84
|
+
accountConfigId: cfg.accountConfigId,
|
|
85
|
+
localPart: "u",
|
|
86
|
+
domain: "test.com",
|
|
87
|
+
normalizedEmail: "u@test.com",
|
|
88
|
+
normalizedCompound: "u@test.com:u",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const desc = await repo.describe(cfg.accountConfigId);
|
|
92
|
+
assert.equal(desc.accountConfig.length, 1);
|
|
93
|
+
assert.equal(desc.account.length, 1);
|
|
94
|
+
assert.equal(desc.address.length, 1);
|
|
95
|
+
|
|
96
|
+
await repo.delete(cfg.accountConfigId);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("listAll returns all configs", async () => {
|
|
100
|
+
const userId = randomId();
|
|
101
|
+
const c1 = await repo.create({ userId });
|
|
102
|
+
const c2 = await repo.create({ userId });
|
|
103
|
+
|
|
104
|
+
const all = await repo.listAll();
|
|
105
|
+
const ids = new Set(all.map((c) => c.accountConfigId));
|
|
106
|
+
assert.ok(ids.has(c1.accountConfigId));
|
|
107
|
+
assert.ok(ids.has(c2.accountConfigId));
|
|
108
|
+
|
|
109
|
+
await repo.deleteMany([c1.accountConfigId, c2.accountConfigId]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("continuation token rejection (#172)", () => {
|
|
113
|
+
for (const [label, token] of [
|
|
114
|
+
["an unparseable", "not-a-cursor"],
|
|
115
|
+
["a bare number", Buffer.from("123").toString("base64url")],
|
|
116
|
+
["a JSON array", Buffer.from("[1,2]").toString("base64url")],
|
|
117
|
+
] as const) {
|
|
118
|
+
test(`${label} token is rejected as a 400`, async () => {
|
|
119
|
+
await assert.rejects(
|
|
120
|
+
() => repo.listByUser(randomId(), { continuationToken: token }),
|
|
121
|
+
(error: unknown) => {
|
|
122
|
+
assert.equal((error as { statusCode?: number }).statusCode, 400);
|
|
123
|
+
assert.equal((error as Error).name, "BadRequestError");
|
|
124
|
+
return true;
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
};
|