@remit/config-transfer 0.0.11 → 0.0.12
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/export.test.ts +8 -38
- package/src/export.ts +2 -25
- package/src/fixtures.ts +1 -20
- package/src/import-repositories.ts +0 -29
- package/src/import.test.ts +29 -155
- package/src/import.ts +21 -122
- package/src/index.ts +0 -2
- package/src/repositories.ts +0 -2
package/package.json
CHANGED
package/src/export.test.ts
CHANGED
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
makeAccount,
|
|
17
17
|
makeAccountConfig,
|
|
18
18
|
makeAddress,
|
|
19
|
-
makeAnchor,
|
|
20
19
|
makeFilter,
|
|
21
20
|
makeJunkOnlyFlaggedAddress,
|
|
22
21
|
makeLabel,
|
|
@@ -158,14 +157,12 @@ const fullFixture = (): ConfigFixture => ({
|
|
|
158
157
|
actionMailboxId: INVOICES_ID,
|
|
159
158
|
}),
|
|
160
159
|
makeFilter({
|
|
161
|
-
filterId: "flt-
|
|
162
|
-
name: "
|
|
160
|
+
filterId: "flt-release",
|
|
161
|
+
name: "Release notes",
|
|
163
162
|
scope: "Temporary",
|
|
164
163
|
expiresAt: "2026-12-31T23:59:59+01:00",
|
|
165
|
-
hasAnchor: true,
|
|
166
164
|
}),
|
|
167
165
|
],
|
|
168
|
-
anchors: [makeAnchor({ filterId: "flt-semantic" })],
|
|
169
166
|
addresses: [
|
|
170
167
|
makeAddress({
|
|
171
168
|
addressId: "adr-bank",
|
|
@@ -195,12 +192,12 @@ const fullFixture = (): ConfigFixture => ({
|
|
|
195
192
|
const exportFixture = (fixture: ConfigFixture) =>
|
|
196
193
|
readConfigForExport(asRepositories(fixture), ACCOUNT_CONFIG_ID, IDENTITY);
|
|
197
194
|
|
|
198
|
-
test("a configuration exports as a document the
|
|
195
|
+
test("a configuration exports as a document the current schema accepts", async () => {
|
|
199
196
|
const document = await exportFixture(fullFixture());
|
|
200
197
|
|
|
201
198
|
assert.equal(ReaderConfigDocumentSchema.safeParse(document).success, true);
|
|
202
199
|
assert.equal(document.kind, "reader.config");
|
|
203
|
-
assert.equal(document.schemaVersion,
|
|
200
|
+
assert.equal(document.schemaVersion, 2);
|
|
204
201
|
assert.deepEqual(document.provenance, {
|
|
205
202
|
accountConfigId: ACCOUNT_CONFIG_ID,
|
|
206
203
|
instance: "reader.ischen.nl",
|
|
@@ -379,7 +376,7 @@ test("an override on a folder that no longer exists is dropped", async () => {
|
|
|
379
376
|
|
|
380
377
|
test("a filter names its label and its folder, never their ids", async () => {
|
|
381
378
|
const document = await exportFixture(fullFixture());
|
|
382
|
-
const [invoices,
|
|
379
|
+
const [invoices, release] = document.filters;
|
|
383
380
|
|
|
384
381
|
assert.equal(invoices?.actionLabelName, "Facturen");
|
|
385
382
|
assert.equal(invoices?.matchOperator, "Or");
|
|
@@ -388,11 +385,10 @@ test("a filter names its label and its folder, never their ids", async () => {
|
|
|
388
385
|
{ field: "FromDomain", value: "moneybird.com" },
|
|
389
386
|
]);
|
|
390
387
|
assert.equal(invoices?.expiresAt, null);
|
|
391
|
-
assert.equal(invoices?.anchor, null);
|
|
392
388
|
|
|
393
|
-
assert.equal(
|
|
394
|
-
assert.equal(
|
|
395
|
-
assert.equal(
|
|
389
|
+
assert.equal(release?.actionLabelName, null);
|
|
390
|
+
assert.equal(release?.actionFolder, null);
|
|
391
|
+
assert.equal(release?.expiresAt, "2026-12-31T23:59:59+01:00");
|
|
396
392
|
});
|
|
397
393
|
|
|
398
394
|
test("a filter action naming something that is gone exports as no action", async () => {
|
|
@@ -408,31 +404,6 @@ test("a filter action naming something that is gone exports as no action", async
|
|
|
408
404
|
assert.equal(document.filters[0]?.actionFolder, null);
|
|
409
405
|
});
|
|
410
406
|
|
|
411
|
-
test("an anchor exports its text and its provenance, never its vector", async () => {
|
|
412
|
-
const document = await exportFixture(fullFixture());
|
|
413
|
-
|
|
414
|
-
assert.deepEqual(document.filters[1]?.anchor, {
|
|
415
|
-
sourceText: "the release note this filter was drawn from",
|
|
416
|
-
embeddingId: "amazon.titan-embed-text-v2:0@1024",
|
|
417
|
-
sourceMessageId: "2c9e4b117d5a4f609a315b8c0",
|
|
418
|
-
});
|
|
419
|
-
assert.equal(JSON.stringify(document).includes("anchorEmbedding"), false);
|
|
420
|
-
assert.equal(JSON.stringify(document).includes("0.22"), false);
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
test("a filter claiming an anchor it has no row for exports without one", async () => {
|
|
424
|
-
// hasAnchor is a cache of whether the sibling row exists, and a write that
|
|
425
|
-
// failed after the filter landed leaves it saying yes over nothing. The
|
|
426
|
-
// export answers with the rows it can read, never with the flag.
|
|
427
|
-
const fixture = fullFixture();
|
|
428
|
-
fixture.anchors = [];
|
|
429
|
-
|
|
430
|
-
const document = await exportFixture(fixture);
|
|
431
|
-
|
|
432
|
-
assert.equal(document.filters[1]?.anchor, null);
|
|
433
|
-
assert.equal(ReaderConfigDocumentSchema.safeParse(document).success, true);
|
|
434
|
-
});
|
|
435
|
-
|
|
436
407
|
test("labels export by name and colour", async () => {
|
|
437
408
|
const document = await exportFixture(fullFixture());
|
|
438
409
|
|
|
@@ -625,7 +596,6 @@ test("a configuration with nothing in it still exports a valid document", async
|
|
|
625
596
|
mailboxes: [],
|
|
626
597
|
labels: [],
|
|
627
598
|
filters: [],
|
|
628
|
-
anchors: [],
|
|
629
599
|
addresses: [],
|
|
630
600
|
});
|
|
631
601
|
|
package/src/export.ts
CHANGED
|
@@ -14,7 +14,6 @@ import type {
|
|
|
14
14
|
AccountItem,
|
|
15
15
|
AddressFlags,
|
|
16
16
|
AddressItem,
|
|
17
|
-
FilterAnchorItem,
|
|
18
17
|
FilterItem,
|
|
19
18
|
LabelItem,
|
|
20
19
|
MailboxItem,
|
|
@@ -245,7 +244,6 @@ const toConfigLabel = (label: LabelItem): ConfigLabel => ({
|
|
|
245
244
|
|
|
246
245
|
const toConfigFilter = (
|
|
247
246
|
filter: FilterItem,
|
|
248
|
-
anchor: FilterAnchorItem | undefined,
|
|
249
247
|
labelNames: ReadonlyMap<string, string>,
|
|
250
248
|
index: MailboxIndex,
|
|
251
249
|
): ConfigFilter => {
|
|
@@ -274,16 +272,6 @@ const toConfigFilter = (
|
|
|
274
272
|
actionAccountId === undefined || actionPath === undefined
|
|
275
273
|
? null
|
|
276
274
|
: { accountId: actionAccountId, folderPath: actionPath },
|
|
277
|
-
// The stored vector is a function of the model that produced it, so it
|
|
278
|
-
// means nothing on an instance running another one. The source text is
|
|
279
|
-
// what travels; import re-embeds from it.
|
|
280
|
-
anchor: anchor
|
|
281
|
-
? {
|
|
282
|
-
sourceText: anchor.anchorSourceText,
|
|
283
|
-
embeddingId: anchor.anchorEmbeddingId,
|
|
284
|
-
sourceMessageId: anchor.anchorMessageId,
|
|
285
|
-
}
|
|
286
|
-
: null,
|
|
287
275
|
};
|
|
288
276
|
};
|
|
289
277
|
|
|
@@ -378,7 +366,7 @@ export const readConfigForExport = async (
|
|
|
378
366
|
accountConfigId: string,
|
|
379
367
|
identity: ConfigExportIdentity,
|
|
380
368
|
): Promise<ReaderConfigDocument> => {
|
|
381
|
-
const [accountConfig, accounts, settingRows, labels, filters
|
|
369
|
+
const [accountConfig, accounts, settingRows, labels, filters] =
|
|
382
370
|
await Promise.all([
|
|
383
371
|
// A configuration nobody has added an account to yet holds no row. That
|
|
384
372
|
// is an empty export, not a failure: the file is the migration safety
|
|
@@ -393,7 +381,6 @@ export const readConfigForExport = async (
|
|
|
393
381
|
repositories.accountSetting.listByAccountConfig(accountConfigId),
|
|
394
382
|
repositories.label.listByAccountConfig(accountConfigId),
|
|
395
383
|
repositories.filter.listByAccountConfig(accountConfigId),
|
|
396
|
-
repositories.filterAnchor.listByAccountConfig(accountConfigId),
|
|
397
384
|
]);
|
|
398
385
|
|
|
399
386
|
const liveAccounts = accounts.filter(
|
|
@@ -415,9 +402,6 @@ export const readConfigForExport = async (
|
|
|
415
402
|
const labelNames = new Map(
|
|
416
403
|
labels.map((label) => [label.labelId, label.name] as const),
|
|
417
404
|
);
|
|
418
|
-
const anchorsByFilter = new Map(
|
|
419
|
-
anchors.map((anchor) => [anchor.filterId, anchor] as const),
|
|
420
|
-
);
|
|
421
405
|
const addresses = await readAddresses(repositories, accountConfigId);
|
|
422
406
|
|
|
423
407
|
const document: ReaderConfigDocument = {
|
|
@@ -448,14 +432,7 @@ export const readConfigForExport = async (
|
|
|
448
432
|
),
|
|
449
433
|
),
|
|
450
434
|
labels: labels.map(toConfigLabel),
|
|
451
|
-
filters: filters.map((filter) =>
|
|
452
|
-
toConfigFilter(
|
|
453
|
-
filter,
|
|
454
|
-
anchorsByFilter.get(filter.filterId),
|
|
455
|
-
labelNames,
|
|
456
|
-
index,
|
|
457
|
-
),
|
|
458
|
-
),
|
|
435
|
+
filters: filters.map((filter) => toConfigFilter(filter, labelNames, index)),
|
|
459
436
|
addressFlags: addresses
|
|
460
437
|
.filter((address) => carriesUserFlag(address.flags))
|
|
461
438
|
.map(toConfigAddressFlags),
|
package/src/fixtures.ts
CHANGED
|
@@ -3,7 +3,6 @@ import type {
|
|
|
3
3
|
AccountItem,
|
|
4
4
|
AccountSettingItem,
|
|
5
5
|
AddressItem,
|
|
6
|
-
FilterAnchorItem,
|
|
7
6
|
FilterItem,
|
|
8
7
|
IAddressRepository,
|
|
9
8
|
LabelItem,
|
|
@@ -24,7 +23,6 @@ export interface ConfigFixture {
|
|
|
24
23
|
mailboxes: MailboxItem[];
|
|
25
24
|
labels: LabelItem[];
|
|
26
25
|
filters: FilterItem[];
|
|
27
|
-
anchors: FilterAnchorItem[];
|
|
28
26
|
addresses: AddressItem[];
|
|
29
27
|
/** Page size for the address listing, so paging is exercised. */
|
|
30
28
|
addressPageSize?: number;
|
|
@@ -130,11 +128,10 @@ export const makeFilter = (
|
|
|
130
128
|
scope: "Standing",
|
|
131
129
|
state: "Active",
|
|
132
130
|
disabledReason: "None",
|
|
133
|
-
hasAnchor: false,
|
|
134
131
|
ruleChangedAt: NOW,
|
|
135
132
|
actionChangedAt: NOW,
|
|
136
133
|
matchOperator: "And",
|
|
137
|
-
literalClauses: [],
|
|
134
|
+
literalClauses: [{ field: "Subject", value: "release notes" }],
|
|
138
135
|
actionLabelId: "None",
|
|
139
136
|
actionMailboxId: "None",
|
|
140
137
|
createdAt: NOW,
|
|
@@ -142,19 +139,6 @@ export const makeFilter = (
|
|
|
142
139
|
...overrides,
|
|
143
140
|
});
|
|
144
141
|
|
|
145
|
-
export const makeAnchor = (
|
|
146
|
-
overrides: Partial<FilterAnchorItem> & Pick<FilterAnchorItem, "filterId">,
|
|
147
|
-
): FilterAnchorItem => ({
|
|
148
|
-
accountConfigId: ACCOUNT_CONFIG_ID,
|
|
149
|
-
anchorEmbedding: [0.11, 0.22, 0.33],
|
|
150
|
-
anchorEmbeddingId: "amazon.titan-embed-text-v2:0@1024",
|
|
151
|
-
anchorSourceText: "the release note this filter was drawn from",
|
|
152
|
-
anchorMessageId: "2c9e4b117d5a4f609a315b8c0",
|
|
153
|
-
createdAt: NOW,
|
|
154
|
-
updatedAt: NOW,
|
|
155
|
-
...overrides,
|
|
156
|
-
});
|
|
157
|
-
|
|
158
142
|
export const makeAddress = (
|
|
159
143
|
overrides: Partial<AddressItem> &
|
|
160
144
|
Pick<AddressItem, "addressId" | "normalizedEmail">,
|
|
@@ -287,8 +271,5 @@ export const asRepositories = (
|
|
|
287
271
|
filter: {
|
|
288
272
|
listByAccountConfig: async () => fixture.filters,
|
|
289
273
|
},
|
|
290
|
-
filterAnchor: {
|
|
291
|
-
listByAccountConfig: async () => fixture.anchors,
|
|
292
|
-
},
|
|
293
274
|
address: asAddressRepository(fixture),
|
|
294
275
|
});
|
|
@@ -5,7 +5,6 @@ import type {
|
|
|
5
5
|
IAccountSettingRepository,
|
|
6
6
|
IAddressRepository,
|
|
7
7
|
IConfigImportRepository,
|
|
8
|
-
IFilterAnchorRepository,
|
|
9
8
|
IFilterRepository,
|
|
10
9
|
ILabelRepository,
|
|
11
10
|
IMailboxRepository,
|
|
@@ -38,7 +37,6 @@ export interface ConfigImportRepositories {
|
|
|
38
37
|
mailbox: Pick<IMailboxRepository, "listAllByAccount">;
|
|
39
38
|
label: Pick<ILabelRepository, "listByAccountConfig" | "create" | "update">;
|
|
40
39
|
filter: Pick<IFilterRepository, "listByAccountConfig" | "create" | "update">;
|
|
41
|
-
filterAnchor: Pick<IFilterAnchorRepository, "put">;
|
|
42
40
|
/**
|
|
43
41
|
* Not the suggest listing, for the reason the export refuses it too: what a
|
|
44
42
|
* configuration already holds is decided over every stored row, junk-only
|
|
@@ -54,21 +52,6 @@ export interface ConfigImportRepositories {
|
|
|
54
52
|
>;
|
|
55
53
|
}
|
|
56
54
|
|
|
57
|
-
/**
|
|
58
|
-
* A filter anchor, re-embedded from the source text the file carries. The
|
|
59
|
-
* stored vector is a function of the model that produced it, so it never
|
|
60
|
-
* travels; this is the one thing an import has to compute rather than copy.
|
|
61
|
-
*
|
|
62
|
-
* Optional, and free to answer `undefined`, because a configuration file has to
|
|
63
|
-
* import on a deployment that ships no embedding model at all. Either way the
|
|
64
|
-
* anchor lands stamped `ANCHOR_EMBEDDING_PENDING`, which the existing lazy
|
|
65
|
-
* repair (`refreshAnchorForEmbedder`) treats as needing a fresh vector the
|
|
66
|
-
* first time the filter is matched.
|
|
67
|
-
*/
|
|
68
|
-
export type EmbedAnchor = (
|
|
69
|
-
sourceText: string,
|
|
70
|
-
) => Promise<{ embedding: number[]; embeddingId: string } | undefined>;
|
|
71
|
-
|
|
72
55
|
/**
|
|
73
56
|
* Persist one role's folder appointment. Injected rather than composed here:
|
|
74
57
|
* an appointment row means a person decided, and it keeps that meaning only
|
|
@@ -86,17 +69,5 @@ export type AppointFolderRole = (
|
|
|
86
69
|
export interface ConfigImportDeps {
|
|
87
70
|
repositories: ConfigImportRepositories;
|
|
88
71
|
appointFolderRole: AppointFolderRole;
|
|
89
|
-
/**
|
|
90
|
-
* Runs the whole apply as one write set, rolling back on any throw.
|
|
91
|
-
*
|
|
92
|
-
* Optional, and its presence is the fact the report words itself from: a
|
|
93
|
-
* backend that supplies this rolls a failed apply back to nothing, and one
|
|
94
|
-
* that does not leaves behind whatever landed before the failure. Only the
|
|
95
|
-
* caller knows which, so it says so by passing this or omitting it rather
|
|
96
|
-
* than by handing over a pass-through that would claim atomicity it has not
|
|
97
|
-
* got.
|
|
98
|
-
*/
|
|
99
|
-
transaction?: <T>(run: () => Promise<T>) => Promise<T>;
|
|
100
|
-
embedAnchor?: EmbedAnchor;
|
|
101
72
|
now?: () => number;
|
|
102
73
|
}
|
package/src/import.test.ts
CHANGED
|
@@ -6,7 +6,6 @@ import type {
|
|
|
6
6
|
AccountSettingItem,
|
|
7
7
|
AddressItem,
|
|
8
8
|
ConfigImportItem,
|
|
9
|
-
FilterAnchorItem,
|
|
10
9
|
FilterItem,
|
|
11
10
|
LabelItem,
|
|
12
11
|
MailboxItem,
|
|
@@ -30,7 +29,6 @@ import {
|
|
|
30
29
|
makeAccount,
|
|
31
30
|
makeAccountConfig,
|
|
32
31
|
makeAddress,
|
|
33
|
-
makeAnchor,
|
|
34
32
|
makeFilter,
|
|
35
33
|
makeJunkOnlyFlaggedAddress,
|
|
36
34
|
makeLabel,
|
|
@@ -41,7 +39,7 @@ import {
|
|
|
41
39
|
PASSWORD_ACCOUNT_ID,
|
|
42
40
|
suggestable,
|
|
43
41
|
} from "./fixtures.js";
|
|
44
|
-
import {
|
|
42
|
+
import { importConfig } from "./import.js";
|
|
45
43
|
import type { ConfigImportDeps } from "./import-repositories.js";
|
|
46
44
|
|
|
47
45
|
const IDENTITY = {
|
|
@@ -53,7 +51,6 @@ const IDENTITY = {
|
|
|
53
51
|
|
|
54
52
|
const TARGET_CONFIG_ID = "1a2b3c4d5e6f7a8b9c0d1e2f3";
|
|
55
53
|
const TARGET_USER_ID = "9f8e7d6c-5b4a-4938-8271-605f4e3d2c1b";
|
|
56
|
-
const EMBEDDING_ID = "amazon.titan-embed-text-v2:0@1024";
|
|
57
54
|
|
|
58
55
|
const INBOX_ID = "mbx-inbox";
|
|
59
56
|
const SENT_ID = "mbx-sent";
|
|
@@ -74,7 +71,6 @@ interface Store {
|
|
|
74
71
|
mailboxes: MailboxItem[];
|
|
75
72
|
labels: LabelItem[];
|
|
76
73
|
filters: FilterItem[];
|
|
77
|
-
anchors: FilterAnchorItem[];
|
|
78
74
|
addresses: AddressItem[];
|
|
79
75
|
imports: ConfigImportItem[];
|
|
80
76
|
/** Set to make the next write of this kind fail, for the abort path. */
|
|
@@ -92,7 +88,6 @@ const emptyStore = (): Store => ({
|
|
|
92
88
|
mailboxes: [],
|
|
93
89
|
labels: [],
|
|
94
90
|
filters: [],
|
|
95
|
-
anchors: [],
|
|
96
91
|
addresses: [],
|
|
97
92
|
imports: [],
|
|
98
93
|
});
|
|
@@ -108,7 +103,6 @@ const storeFromFixture = (fixture: ConfigFixture): Store => ({
|
|
|
108
103
|
mailboxes: [...fixture.mailboxes],
|
|
109
104
|
labels: [...fixture.labels],
|
|
110
105
|
filters: [...fixture.filters],
|
|
111
|
-
anchors: [...fixture.anchors],
|
|
112
106
|
addresses: [...fixture.addresses],
|
|
113
107
|
imports: [],
|
|
114
108
|
});
|
|
@@ -244,19 +238,6 @@ const repositoriesOf = (store: Store, accountConfigId: string): any => {
|
|
|
244
238
|
return next;
|
|
245
239
|
},
|
|
246
240
|
},
|
|
247
|
-
filterAnchor: {
|
|
248
|
-
put: async (input: Partial<FilterAnchorItem> & { filterId: string }) => {
|
|
249
|
-
const row = makeAnchor({ ...input, accountConfigId });
|
|
250
|
-
store.anchors = [
|
|
251
|
-
...store.anchors.filter(
|
|
252
|
-
(anchor) => anchor.filterId !== input.filterId,
|
|
253
|
-
),
|
|
254
|
-
row,
|
|
255
|
-
];
|
|
256
|
-
return row;
|
|
257
|
-
},
|
|
258
|
-
listByAccountConfig: async () => [...store.anchors],
|
|
259
|
-
},
|
|
260
241
|
address: {
|
|
261
242
|
// Both reads, so the stub can tell them apart: the suggest listing
|
|
262
243
|
// hides a junk-only row, and the import must not be reading it (#1029).
|
|
@@ -392,50 +373,15 @@ const settle = (
|
|
|
392
373
|
return next;
|
|
393
374
|
};
|
|
394
375
|
|
|
395
|
-
|
|
396
|
-
* A transaction that actually rolls back. The backend's `writeSet` discards
|
|
397
|
-
* every write on a throw, so a fake that merely called through would let an
|
|
398
|
-
* atomicity test pass over a store that had kept half a document — which is the
|
|
399
|
-
* one thing these tests exist to catch. Each collection is restored to the
|
|
400
|
-
* instance it had on entry.
|
|
401
|
-
*/
|
|
402
|
-
const transactionOver =
|
|
403
|
-
(store: Store) =>
|
|
404
|
-
async <T>(run: () => Promise<T>): Promise<T> => {
|
|
405
|
-
const before = {
|
|
406
|
-
accountConfigs: new Map(store.accountConfigs),
|
|
407
|
-
accounts: [...store.accounts],
|
|
408
|
-
settings: new Map(store.settings),
|
|
409
|
-
mailboxes: [...store.mailboxes],
|
|
410
|
-
labels: [...store.labels],
|
|
411
|
-
filters: [...store.filters],
|
|
412
|
-
anchors: [...store.anchors],
|
|
413
|
-
addresses: [...store.addresses],
|
|
414
|
-
imports: [...store.imports],
|
|
415
|
-
};
|
|
416
|
-
return run().catch((error: unknown) => {
|
|
417
|
-
Object.assign(store, before);
|
|
418
|
-
throw error;
|
|
419
|
-
});
|
|
420
|
-
};
|
|
421
|
-
|
|
422
|
-
const depsOf = (
|
|
423
|
-
store: Store,
|
|
424
|
-
accountConfigId: string,
|
|
425
|
-
embed = true,
|
|
426
|
-
): ConfigImportDeps => ({
|
|
376
|
+
const depsOf = (store: Store, accountConfigId: string): ConfigImportDeps => ({
|
|
427
377
|
repositories: repositoriesOf(store, accountConfigId),
|
|
428
378
|
appointFolderRole: appointFolderRoleInto(store),
|
|
429
|
-
transaction: transactionOver(store),
|
|
430
|
-
embedAnchor: embed
|
|
431
|
-
? async () => ({ embedding: [0.11, 0.22, 0.33], embeddingId: EMBEDDING_ID })
|
|
432
|
-
: undefined,
|
|
433
379
|
now: () => NOW,
|
|
434
380
|
});
|
|
435
381
|
|
|
436
382
|
// ---------------------------------------------------------------------------
|
|
437
383
|
// A configuration worth carrying across: two accounts, both auth types, folder
|
|
438
|
-
// roles, per-folder overrides, labels,
|
|
384
|
+
// roles, per-folder overrides, labels, two filters, and two
|
|
439
385
|
// flagged senders.
|
|
440
386
|
// ---------------------------------------------------------------------------
|
|
441
387
|
|
|
@@ -543,14 +489,12 @@ const sourceFixture = (): ConfigFixture => ({
|
|
|
543
489
|
actionMailboxId: INVOICES_ID,
|
|
544
490
|
}),
|
|
545
491
|
makeFilter({
|
|
546
|
-
filterId: "flt-
|
|
547
|
-
name: "
|
|
492
|
+
filterId: "flt-release",
|
|
493
|
+
name: "Release notes",
|
|
548
494
|
scope: "Temporary",
|
|
549
495
|
expiresAt: "2026-12-31T23:59:59+01:00",
|
|
550
|
-
hasAnchor: true,
|
|
551
496
|
}),
|
|
552
497
|
],
|
|
553
|
-
anchors: [makeAnchor({ filterId: "flt-semantic" })],
|
|
554
498
|
addresses: [
|
|
555
499
|
makeAddress({
|
|
556
500
|
addressId: "adr-bank",
|
|
@@ -1570,67 +1514,6 @@ test("a filter filing into an account the file does not carry is refused", async
|
|
|
1570
1514
|
assert.equal(report.errors[0]?.code, "unknown_account");
|
|
1571
1515
|
});
|
|
1572
1516
|
|
|
1573
|
-
test("an anchor this deployment cannot embed is stored as text for the repair to pick up", async () => {
|
|
1574
|
-
const document = await exportSource();
|
|
1575
|
-
const store = emptyStore();
|
|
1576
|
-
|
|
1577
|
-
const outcome = await importConfig(depsOf(store, TARGET_CONFIG_ID, false), {
|
|
1578
|
-
accountConfigId: TARGET_CONFIG_ID,
|
|
1579
|
-
userId: TARGET_USER_ID,
|
|
1580
|
-
document,
|
|
1581
|
-
mode: "apply",
|
|
1582
|
-
onExisting: "abort",
|
|
1583
|
-
});
|
|
1584
|
-
const report = reportOf(outcome);
|
|
1585
|
-
|
|
1586
|
-
const anchor = store.anchors[0];
|
|
1587
|
-
assert.equal(anchor.anchorEmbeddingId, ANCHOR_EMBEDDING_PENDING);
|
|
1588
|
-
assert.deepEqual(anchor.anchorEmbedding, []);
|
|
1589
|
-
assert.equal(
|
|
1590
|
-
anchor.anchorSourceText,
|
|
1591
|
-
"the release note this filter was drawn from",
|
|
1592
|
-
);
|
|
1593
|
-
assert.equal(
|
|
1594
|
-
report.warnings.some((warning) => warning.code === "anchor_not_embedded"),
|
|
1595
|
-
true,
|
|
1596
|
-
);
|
|
1597
|
-
});
|
|
1598
|
-
|
|
1599
|
-
test("an embedder that fails costs the anchor its vector, not the import", async () => {
|
|
1600
|
-
const document = await exportSource();
|
|
1601
|
-
const store = emptyStore();
|
|
1602
|
-
const deps = depsOf(store, TARGET_CONFIG_ID);
|
|
1603
|
-
|
|
1604
|
-
const report = reportOf(
|
|
1605
|
-
await importConfig(
|
|
1606
|
-
{
|
|
1607
|
-
...deps,
|
|
1608
|
-
embedAnchor: () => Promise.reject(new Error("embedder unavailable")),
|
|
1609
|
-
},
|
|
1610
|
-
{
|
|
1611
|
-
accountConfigId: TARGET_CONFIG_ID,
|
|
1612
|
-
userId: TARGET_USER_ID,
|
|
1613
|
-
document,
|
|
1614
|
-
mode: "apply",
|
|
1615
|
-
onExisting: "abort",
|
|
1616
|
-
},
|
|
1617
|
-
),
|
|
1618
|
-
);
|
|
1619
|
-
|
|
1620
|
-
assert.equal(report.applied, true);
|
|
1621
|
-
assert.deepEqual(report.errors, []);
|
|
1622
|
-
assert.equal(
|
|
1623
|
-
report.warnings.some((warning) => warning.code === "anchor_not_embedded"),
|
|
1624
|
-
true,
|
|
1625
|
-
);
|
|
1626
|
-
assert.equal(store.anchors[0]?.anchorEmbeddingId, ANCHOR_EMBEDDING_PENDING);
|
|
1627
|
-
assert.equal(
|
|
1628
|
-
store.anchors[0]?.anchorSourceText,
|
|
1629
|
-
"the release note this filter was drawn from",
|
|
1630
|
-
);
|
|
1631
|
-
assert.equal(store.accounts.length > 0, true);
|
|
1632
|
-
});
|
|
1633
|
-
|
|
1634
1517
|
test("the dry run names the same folders and credentials the apply does", async () => {
|
|
1635
1518
|
const document = await exportSource();
|
|
1636
1519
|
const planStore = emptyStore();
|
|
@@ -1659,7 +1542,7 @@ test("the dry run names the same folders and credentials the apply does", async
|
|
|
1659
1542
|
assert.deepEqual(planStore.accounts, []);
|
|
1660
1543
|
});
|
|
1661
1544
|
|
|
1662
|
-
test("a write that fails
|
|
1545
|
+
test("a write that fails part-way reports what it did leave behind and names where it stopped", async () => {
|
|
1663
1546
|
const document = await exportSource();
|
|
1664
1547
|
const store = emptyStore();
|
|
1665
1548
|
store.failOn = "filter";
|
|
@@ -1667,40 +1550,8 @@ test("a write that fails after validation leaves nothing behind and names where
|
|
|
1667
1550
|
const report = reportOf(await apply(store, document));
|
|
1668
1551
|
|
|
1669
1552
|
assert.equal(report.valid, true);
|
|
1670
|
-
assert.equal(report.applied, false);
|
|
1671
1553
|
assert.equal(report.errors[0]?.code, "import_write_failed");
|
|
1672
1554
|
assert.equal(report.errors[0]?.details?.section, "filters");
|
|
1673
|
-
assert.match(report.errors[0]?.message ?? "", /Nothing was written/);
|
|
1674
|
-
assert.equal(report.items[0]?.verdict, "rejected");
|
|
1675
|
-
|
|
1676
|
-
// Filters fail after accounts and labels have been written, so these are the
|
|
1677
|
-
// assertions that make the claim: the rollback undid them too.
|
|
1678
|
-
assert.deepEqual(store.imports, []);
|
|
1679
|
-
assert.deepEqual(store.accounts, []);
|
|
1680
|
-
assert.deepEqual(store.labels, []);
|
|
1681
|
-
assert.deepEqual(store.filters, []);
|
|
1682
|
-
assert.equal(store.accountConfigs.size, 0);
|
|
1683
|
-
});
|
|
1684
|
-
|
|
1685
|
-
test("a write that fails without a transaction reports what it did leave behind", async () => {
|
|
1686
|
-
const document = await exportSource();
|
|
1687
|
-
const store = emptyStore();
|
|
1688
|
-
store.failOn = "filter";
|
|
1689
|
-
|
|
1690
|
-
const deps = depsOf(store, TARGET_CONFIG_ID);
|
|
1691
|
-
const report = reportOf(
|
|
1692
|
-
await importConfig(
|
|
1693
|
-
{ ...deps, transaction: undefined },
|
|
1694
|
-
{
|
|
1695
|
-
accountConfigId: TARGET_CONFIG_ID,
|
|
1696
|
-
userId: TARGET_USER_ID,
|
|
1697
|
-
document,
|
|
1698
|
-
mode: "apply",
|
|
1699
|
-
onExisting: "abort",
|
|
1700
|
-
},
|
|
1701
|
-
),
|
|
1702
|
-
);
|
|
1703
|
-
|
|
1704
1555
|
assert.equal(report.applied, true);
|
|
1705
1556
|
assert.match(report.errors[0]?.message ?? "", /were written and remain/);
|
|
1706
1557
|
assert.equal(
|
|
@@ -1712,6 +1563,29 @@ test("a write that fails without a transaction reports what it did leave behind"
|
|
|
1712
1563
|
assert.equal(store.accounts.length > 0, true);
|
|
1713
1564
|
});
|
|
1714
1565
|
|
|
1566
|
+
test("a stopped import imported again with merge reaches what one clean import holds", async () => {
|
|
1567
|
+
const document = await exportSource();
|
|
1568
|
+
const clean = emptyStore();
|
|
1569
|
+
await apply(clean, document);
|
|
1570
|
+
|
|
1571
|
+
const store = emptyStore();
|
|
1572
|
+
store.failOn = "filter";
|
|
1573
|
+
await apply(store, document);
|
|
1574
|
+
store.failOn = undefined;
|
|
1575
|
+
const report = reportOf(await apply(store, document, "merge"));
|
|
1576
|
+
|
|
1577
|
+
assert.deepEqual(report.errors, []);
|
|
1578
|
+
const held = (target: Store) => ({
|
|
1579
|
+
accounts: target.accounts.map((account) => account.email).sort(),
|
|
1580
|
+
labels: target.labels.map((label) => label.name).sort(),
|
|
1581
|
+
filters: target.filters.length,
|
|
1582
|
+
addresses: target.addresses.length,
|
|
1583
|
+
settings: [...target.settings.keys()].sort(),
|
|
1584
|
+
});
|
|
1585
|
+
assert.deepEqual(held(store), held(clean));
|
|
1586
|
+
assert.ok(held(clean).filters > 0, "the file carries filters to land");
|
|
1587
|
+
});
|
|
1588
|
+
|
|
1715
1589
|
test("the configuration row is created when the database holds none, and named from the file", async () => {
|
|
1716
1590
|
const document = await exportSource();
|
|
1717
1591
|
const store = emptyStore();
|
package/src/import.ts
CHANGED
|
@@ -39,17 +39,6 @@ import {
|
|
|
39
39
|
ConfigImportVerdict,
|
|
40
40
|
} from "./report.js";
|
|
41
41
|
|
|
42
|
-
/**
|
|
43
|
-
* The `<modelId>@<dimensions>` stamp an anchor carries when this deployment
|
|
44
|
-
* could not embed its source text at import time. It matches no real embedder,
|
|
45
|
-
* which is what makes the existing lazy repair (`refreshAnchorForEmbedder`)
|
|
46
|
-
* treat the anchor as needing a fresh vector the first time it is matched —
|
|
47
|
-
* the same route a model migration takes. Deliberately the same literal as
|
|
48
|
-
* `UNKNOWN_CHUNK_EMBEDDING_ID` in `@remit/search-service`, which config
|
|
49
|
-
* transfer cannot import without dragging the vector stack into the CLI.
|
|
50
|
-
*/
|
|
51
|
-
export const ANCHOR_EMBEDDING_PENDING = "unknown";
|
|
52
|
-
|
|
53
42
|
/** The named sentinel a Filter carries for "no action of this kind". */
|
|
54
43
|
const NO_ACTION = "None";
|
|
55
44
|
|
|
@@ -270,9 +259,9 @@ const landingState = (
|
|
|
270
259
|
|
|
271
260
|
/**
|
|
272
261
|
* What the apply has decided so far. Owned by `importConfig` rather than by
|
|
273
|
-
* `applyDocument` so a write that fails part-way still has it:
|
|
274
|
-
*
|
|
275
|
-
*
|
|
262
|
+
* `applyDocument` so a write that fails part-way still has it: those items are
|
|
263
|
+
* what actually landed, and a report that threw them away would name a stopping
|
|
264
|
+
* point without naming the state.
|
|
276
265
|
*/
|
|
277
266
|
interface ApplyProgress {
|
|
278
267
|
items: ConfigImportItemReport[];
|
|
@@ -288,56 +277,6 @@ const emptyProgress = (): ApplyProgress => ({
|
|
|
288
277
|
unresolvedRefs: [],
|
|
289
278
|
});
|
|
290
279
|
|
|
291
|
-
interface AnchorVector {
|
|
292
|
-
embedding: number[];
|
|
293
|
-
embeddingId: string;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Every anchor's vector, computed before the transaction opens.
|
|
298
|
-
*
|
|
299
|
-
* Two reasons it happens here and not at the point of write. An embedder is a
|
|
300
|
-
* network round trip and the sqlite write queue is process-wide, so embedding
|
|
301
|
-
* inside the transaction holds that queue against every other writer for the
|
|
302
|
-
* length of the call. And a failed embed is not a failed import: it degrades
|
|
303
|
-
* exactly the way a deployment that ships no embedder does, landing the anchor
|
|
304
|
-
* stamped `ANCHOR_EMBEDDING_PENDING` for the lazy repair to rebuild the first
|
|
305
|
-
* time the filter runs. A 503 from the embedder must not cost a person their
|
|
306
|
-
* configuration.
|
|
307
|
-
*/
|
|
308
|
-
const embedAnchors = async (
|
|
309
|
-
deps: ConfigImportDeps,
|
|
310
|
-
document: ReaderConfigDocument,
|
|
311
|
-
warnings: ConfigImportProblem[],
|
|
312
|
-
): Promise<Map<string, AnchorVector>> => {
|
|
313
|
-
const vectors = new Map<string, AnchorVector>();
|
|
314
|
-
const { embedAnchor } = deps;
|
|
315
|
-
|
|
316
|
-
for (const filter of document.filters) {
|
|
317
|
-
const { anchor } = filter;
|
|
318
|
-
if (anchor === null) continue;
|
|
319
|
-
|
|
320
|
-
const embedded = embedAnchor
|
|
321
|
-
? await embedAnchor(anchor.sourceText).catch(() => undefined)
|
|
322
|
-
: undefined;
|
|
323
|
-
|
|
324
|
-
if (embedded) {
|
|
325
|
-
vectors.set(normalize(filter.name), embedded);
|
|
326
|
-
continue;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
warnings.push(
|
|
330
|
-
problem(
|
|
331
|
-
"anchor_not_embedded",
|
|
332
|
-
`The example behind filter "${filter.name}" is carried as text; its vector is rebuilt the first time the filter runs.`,
|
|
333
|
-
{ filter: filter.name },
|
|
334
|
-
),
|
|
335
|
-
);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
return vectors;
|
|
339
|
-
};
|
|
340
|
-
|
|
341
280
|
/**
|
|
342
281
|
* The id this instance will hold each account in the file under: the one it
|
|
343
282
|
* already has for that id or email, or the file's own, which `create` preserves
|
|
@@ -378,8 +317,7 @@ const foldersFor = (
|
|
|
378
317
|
|
|
379
318
|
/**
|
|
380
319
|
* A write that failed after validation. It carries the item it stopped on so
|
|
381
|
-
* the report can name it
|
|
382
|
-
* stopped is the only fact worth recovering from.
|
|
320
|
+
* the report can name it beside what landed before it.
|
|
383
321
|
*/
|
|
384
322
|
class ImportWriteError extends Error {
|
|
385
323
|
constructor(
|
|
@@ -399,7 +337,6 @@ const applyDocument = async (
|
|
|
399
337
|
input: ImportConfigInput,
|
|
400
338
|
document: ReaderConfigDocument,
|
|
401
339
|
existing: ExistingConfig,
|
|
402
|
-
anchorVectors: ReadonlyMap<string, AnchorVector>,
|
|
403
340
|
progress: ApplyProgress,
|
|
404
341
|
): Promise<string> => {
|
|
405
342
|
const { repositories } = deps;
|
|
@@ -485,8 +422,7 @@ const applyDocument = async (
|
|
|
485
422
|
account.email,
|
|
486
423
|
() =>
|
|
487
424
|
repositories.account.create({
|
|
488
|
-
// Verbatim: a resynced message id hashes from it
|
|
489
|
-
// what keeps every filter anchor's provenance valid across the drop.
|
|
425
|
+
// Verbatim: a resynced message id hashes from it.
|
|
490
426
|
accountId: account.accountId,
|
|
491
427
|
accountConfigId,
|
|
492
428
|
...endpoints,
|
|
@@ -545,7 +481,6 @@ const applyDocument = async (
|
|
|
545
481
|
const existingFilterByName = new Map(
|
|
546
482
|
existing.filters.map((filter) => [normalize(filter.name), filter] as const),
|
|
547
483
|
);
|
|
548
|
-
const filterIdByName = new Map<string, string>();
|
|
549
484
|
|
|
550
485
|
const resolveFilterFolder = (
|
|
551
486
|
filter: ConfigFilter,
|
|
@@ -574,7 +509,6 @@ const applyDocument = async (
|
|
|
574
509
|
? NO_ACTION
|
|
575
510
|
: (labelIdByName.get(normalize(filter.actionLabelName)) ?? NO_ACTION),
|
|
576
511
|
actionMailboxId: folder.actionMailboxId,
|
|
577
|
-
hasAnchor: filter.anchor !== null,
|
|
578
512
|
...(folder.pending
|
|
579
513
|
? {
|
|
580
514
|
state: FilterState.Disabled,
|
|
@@ -603,7 +537,6 @@ const applyDocument = async (
|
|
|
603
537
|
)
|
|
604
538
|
).filterId;
|
|
605
539
|
|
|
606
|
-
filterIdByName.set(key, filterId);
|
|
607
540
|
items.push({
|
|
608
541
|
section: ConfigImportSection.Filters,
|
|
609
542
|
key: filter.name,
|
|
@@ -636,34 +569,6 @@ const applyDocument = async (
|
|
|
636
569
|
}
|
|
637
570
|
}
|
|
638
571
|
|
|
639
|
-
// ---- anchors --------------------------------------------------------
|
|
640
|
-
// Vectors were computed before the transaction opened; a missing one is an
|
|
641
|
-
// anchor carried as text, already warned about there.
|
|
642
|
-
for (const filter of document.filters) {
|
|
643
|
-
const { anchor } = filter;
|
|
644
|
-
if (anchor === null) continue;
|
|
645
|
-
const key = normalize(filter.name);
|
|
646
|
-
const filterId = filterIdByName.get(key);
|
|
647
|
-
if (filterId === undefined) {
|
|
648
|
-
throw new Error(
|
|
649
|
-
`Filter "${filter.name}" carries an anchor but the filter loop minted no id for it. The two loops read the same documents.filters, so this cannot happen without one of them changing.`,
|
|
650
|
-
);
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
const embedded = anchorVectors.get(key);
|
|
654
|
-
|
|
655
|
-
await write(ConfigImportSection.Filters, filter.name, () =>
|
|
656
|
-
repositories.filterAnchor.put({
|
|
657
|
-
accountConfigId,
|
|
658
|
-
filterId,
|
|
659
|
-
anchorEmbedding: embedded?.embedding ?? [],
|
|
660
|
-
anchorEmbeddingId: embedded?.embeddingId ?? ANCHOR_EMBEDDING_PENDING,
|
|
661
|
-
anchorSourceText: anchor.sourceText,
|
|
662
|
-
anchorMessageId: anchor.sourceMessageId,
|
|
663
|
-
}),
|
|
664
|
-
);
|
|
665
|
-
}
|
|
666
|
-
|
|
667
572
|
// ---- address flags --------------------------------------------------
|
|
668
573
|
for (const entry of document.addressFlags) {
|
|
669
574
|
await write(ConfigImportSection.AddressFlags, entry.normalizedEmail, () =>
|
|
@@ -939,8 +844,8 @@ class SettingWriter {
|
|
|
939
844
|
*
|
|
940
845
|
* The order is fixed: parse and migrate, check every reference and every
|
|
941
846
|
* natural key against what this configuration already holds, and only then
|
|
942
|
-
* write —
|
|
943
|
-
*
|
|
847
|
+
* write — accounts before labels before filters before
|
|
848
|
+
* addresses before settings, because each of those reads
|
|
944
849
|
* an id the one before it minted.
|
|
945
850
|
*
|
|
946
851
|
* A document this reader will not import is the operation's expected outcome,
|
|
@@ -1004,19 +909,16 @@ export const importConfig = async (
|
|
|
1004
909
|
};
|
|
1005
910
|
}
|
|
1006
911
|
|
|
1007
|
-
// Before the transaction: an embed is a network round trip, and the write
|
|
1008
|
-
// queue it would otherwise hold is process-wide.
|
|
1009
|
-
const embedWarnings: ConfigImportProblem[] = [];
|
|
1010
|
-
const anchorVectors = await embedAnchors(deps, document, embedWarnings);
|
|
1011
|
-
|
|
1012
|
-
const { transaction } = deps;
|
|
1013
|
-
const atomic = transaction !== undefined;
|
|
1014
912
|
const progress = emptyProgress();
|
|
1015
|
-
const apply = (): Promise<string> =>
|
|
1016
|
-
applyDocument(deps, input, document, existing, anchorVectors, progress);
|
|
1017
913
|
|
|
1018
914
|
try {
|
|
1019
|
-
const importId = await (
|
|
915
|
+
const importId = await applyDocument(
|
|
916
|
+
deps,
|
|
917
|
+
input,
|
|
918
|
+
document,
|
|
919
|
+
existing,
|
|
920
|
+
progress,
|
|
921
|
+
);
|
|
1020
922
|
return {
|
|
1021
923
|
outcome: "report",
|
|
1022
924
|
report: {
|
|
@@ -1026,16 +928,17 @@ export const importConfig = async (
|
|
|
1026
928
|
applied: true,
|
|
1027
929
|
items: progress.items,
|
|
1028
930
|
errors: [],
|
|
1029
|
-
warnings:
|
|
931
|
+
warnings: progress.warnings,
|
|
1030
932
|
accountsNeedingCredentials: progress.accountsNeedingCredentials,
|
|
1031
933
|
},
|
|
1032
934
|
};
|
|
1033
935
|
} catch (error) {
|
|
1034
936
|
if (!(error instanceof ImportWriteError)) throw error;
|
|
1035
937
|
// What the report owes the reader is where it stopped and what is now in
|
|
1036
|
-
// the store
|
|
1037
|
-
//
|
|
1038
|
-
|
|
938
|
+
// the store: everything recorded up to the failure. Every write is keyed on
|
|
939
|
+
// a natural key, so importing the same file again with merge folds the
|
|
940
|
+
// rest in over what landed.
|
|
941
|
+
const landed = progress;
|
|
1039
942
|
return {
|
|
1040
943
|
outcome: "report",
|
|
1041
944
|
report: {
|
|
@@ -1054,15 +957,11 @@ export const importConfig = async (
|
|
|
1054
957
|
errors: [
|
|
1055
958
|
problem(
|
|
1056
959
|
"import_write_failed",
|
|
1057
|
-
`${error.message}
|
|
1058
|
-
atomic
|
|
1059
|
-
? "Nothing was written; the import stopped here."
|
|
1060
|
-
: "The items above it were written and remain; the import stopped here."
|
|
1061
|
-
}`,
|
|
960
|
+
`${error.message} The items above it were written and remain; import the same file again with merge to finish.`,
|
|
1062
961
|
{ section: error.section, key: error.key },
|
|
1063
962
|
),
|
|
1064
963
|
],
|
|
1065
|
-
warnings:
|
|
964
|
+
warnings: landed.warnings,
|
|
1066
965
|
accountsNeedingCredentials: landed.accountsNeedingCredentials,
|
|
1067
966
|
},
|
|
1068
967
|
};
|
package/src/index.ts
CHANGED
|
@@ -13,7 +13,6 @@ export {
|
|
|
13
13
|
readConfigForExport,
|
|
14
14
|
} from "./export.js";
|
|
15
15
|
export {
|
|
16
|
-
ANCHOR_EMBEDDING_PENDING,
|
|
17
16
|
type ImportConfigInput,
|
|
18
17
|
importConfig,
|
|
19
18
|
} from "./import.js";
|
|
@@ -21,7 +20,6 @@ export type {
|
|
|
21
20
|
AppointFolderRole,
|
|
22
21
|
ConfigImportDeps,
|
|
23
22
|
ConfigImportRepositories,
|
|
24
|
-
EmbedAnchor,
|
|
25
23
|
} from "./import-repositories.js";
|
|
26
24
|
export {
|
|
27
25
|
type ConfigImportConflict,
|
package/src/repositories.ts
CHANGED
|
@@ -3,7 +3,6 @@ import type {
|
|
|
3
3
|
IAccountRepository,
|
|
4
4
|
IAccountSettingRepository,
|
|
5
5
|
IAddressRepository,
|
|
6
|
-
IFilterAnchorRepository,
|
|
7
6
|
IFilterRepository,
|
|
8
7
|
ILabelRepository,
|
|
9
8
|
IMailboxRepository,
|
|
@@ -24,7 +23,6 @@ export interface ConfigExportRepositories {
|
|
|
24
23
|
mailbox: Pick<IMailboxRepository, "listAllByAccount">;
|
|
25
24
|
label: Pick<ILabelRepository, "listByAccountConfig">;
|
|
26
25
|
filter: Pick<IFilterRepository, "listByAccountConfig">;
|
|
27
|
-
filterAnchor: Pick<IFilterAnchorRepository, "listByAccountConfig">;
|
|
28
26
|
/**
|
|
29
27
|
* Not the suggest listing: that one hides an address the machine marked
|
|
30
28
|
* `junkOnly` unless the account has corresponded with it, which is right for
|