@remit/config-transfer 0.0.6 → 0.0.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/config-transfer",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
package/src/binder.ts CHANGED
@@ -18,7 +18,7 @@ import type { AppointFolderRole } from "./import-repositories.js";
18
18
  export interface ConfigBinderRepositories {
19
19
  configImport: Pick<IConfigImportRepository, "listByAccountConfig" | "update">;
20
20
  accountSetting: Pick<IAccountSettingRepository, "upsert">;
21
- filter: Pick<IFilterRepository, "update">;
21
+ filter: Pick<IFilterRepository, "listByAccountConfig" | "update">;
22
22
  mailbox: Pick<IMailboxRepository, "listAllByAccount">;
23
23
  }
24
24
 
@@ -34,9 +34,15 @@ export interface ConfigBinderDeps {
34
34
  */
35
35
  export interface BindResult {
36
36
  bound: number;
37
+ dropped: number;
37
38
  stillPending: number;
38
39
  }
39
40
 
41
+ type RefState =
42
+ | { kind: "TargetGone" }
43
+ | { kind: "Waiting" }
44
+ | { kind: "Ready"; mailboxId: string };
45
+
40
46
  /**
41
47
  * Bind the folder references an import could not resolve, now that discovery
42
48
  * has produced this account's mailboxes.
@@ -62,7 +68,7 @@ export const bindImportedFolders = async (
62
68
  const imports = (
63
69
  await repositories.configImport.listByAccountConfig(accountConfigId)
64
70
  ).filter((row) => row.state === ConfigImportState.Pending);
65
- if (imports.length === 0) return { bound: 0, stillPending: 0 };
71
+ if (imports.length === 0) return { bound: 0, dropped: 0, stillPending: 0 };
66
72
 
67
73
  const byPath = new Map(
68
74
  (await repositories.mailbox.listAllByAccount(accountId)).map(
@@ -70,7 +76,14 @@ export const bindImportedFolders = async (
70
76
  ),
71
77
  );
72
78
 
79
+ const filterIds = new Set(
80
+ (await repositories.filter.listByAccountConfig(accountConfigId)).map(
81
+ (filter) => filter.filterId,
82
+ ),
83
+ );
84
+
73
85
  let bound = 0;
86
+ let dropped = 0;
74
87
  let stillPending = 0;
75
88
 
76
89
  for (const row of imports) {
@@ -78,13 +91,16 @@ export const bindImportedFolders = async (
78
91
  const remaining: ConfigImportUnresolvedRefItem[] = [];
79
92
 
80
93
  for (const ref of row.unresolvedRefs) {
81
- const mailboxId =
82
- ref.accountId === accountId ? byPath.get(ref.folderPath) : undefined;
83
- if (mailboxId === undefined) {
94
+ const state = refStateOf(ref, accountId, byPath, filterIds);
95
+ if (state.kind === "TargetGone") {
96
+ dropped++;
97
+ continue;
98
+ }
99
+ if (state.kind === "Waiting") {
84
100
  remaining.push(ref);
85
101
  continue;
86
102
  }
87
- await bindRef(deps, accountConfigId, ref, mailboxId, document);
103
+ await bindRef(deps, accountConfigId, ref, state.mailboxId, document);
88
104
  bound++;
89
105
  }
90
106
 
@@ -99,11 +115,29 @@ export const bindImportedFolders = async (
99
115
  });
100
116
  }
101
117
 
102
- return { bound, stillPending };
118
+ return { bound, dropped, stillPending };
103
119
  };
104
120
 
105
121
  type BoundDocument = ReturnType<typeof readConfigDocument>;
106
122
 
123
+ const refStateOf = (
124
+ ref: ConfigImportUnresolvedRefItem,
125
+ accountId: string,
126
+ byPath: ReadonlyMap<string, string>,
127
+ filterIds: ReadonlySet<string>,
128
+ ): RefState => {
129
+ if (
130
+ ref.kind === ConfigImportRefKind.FilterAction &&
131
+ !filterIds.has(ref.target)
132
+ ) {
133
+ return { kind: "TargetGone" };
134
+ }
135
+ const mailboxId =
136
+ ref.accountId === accountId ? byPath.get(ref.folderPath) : undefined;
137
+ if (mailboxId === undefined) return { kind: "Waiting" };
138
+ return { kind: "Ready", mailboxId };
139
+ };
140
+
107
141
  const bindRef = async (
108
142
  deps: ConfigBinderDeps,
109
143
  accountConfigId: string,
package/src/fixtures.ts CHANGED
@@ -55,6 +55,7 @@ export const makeAccount = (
55
55
  username: "matthijs@ischen.nl",
56
56
  email: "matthijs@ischen.nl",
57
57
  authType: "password",
58
+ syncedServices: ["Mail"],
58
59
  imapHost: "imap.ischen.nl",
59
60
  imapPort: 993,
60
61
  imapTls: true,
@@ -229,6 +229,9 @@ const repositoriesOf = (store: Store, accountConfigId: string): any => {
229
229
  const at = store.filters.findIndex(
230
230
  (filter) => filter.filterId === filterId,
231
231
  );
232
+ if (at === -1) {
233
+ throw new NotFoundError(`Filter not found: ${filterId}`);
234
+ }
232
235
  const next = { ...store.filters[at], ...patch };
233
236
  store.filters[at] = next;
234
237
  return next;
@@ -1228,6 +1231,93 @@ test("a binder run on a configuration with no import does nothing", async () =>
1228
1231
  TARGET_CONFIG_ID,
1229
1232
  PASSWORD_ACCOUNT_ID,
1230
1233
  );
1231
- assert.deepEqual(result, { bound: 0, stillPending: 0 });
1234
+ assert.deepEqual(result, { bound: 0, dropped: 0, stillPending: 0 });
1232
1235
  assert.equal(pendingImportOf([]), undefined);
1233
1236
  });
1237
+
1238
+ test("a filter deleted before its folder is discovered drops its reference instead of failing the bind", async () => {
1239
+ const document = await exportSource(sourceWithLegacyFlagsFixture());
1240
+ const store = emptyStore();
1241
+ await apply(store, document);
1242
+
1243
+ const invoices = store.filters.find(
1244
+ (filter) => filter.name === "Invoices to Facturen",
1245
+ );
1246
+ assert.ok(invoices);
1247
+ store.filters = store.filters.filter(
1248
+ (filter) => filter.filterId !== invoices.filterId,
1249
+ );
1250
+
1251
+ discover(store, PASSWORD_ACCOUNT_ID, [
1252
+ "INBOX",
1253
+ "INBOX.Sent",
1254
+ "INBOX.Facturen",
1255
+ "INBOX.Lists.dev-null",
1256
+ ]);
1257
+ discover(store, OAUTH_ACCOUNT_ID, ["INBOX"]);
1258
+ const binder = {
1259
+ repositories: repositoriesOf(store, TARGET_CONFIG_ID),
1260
+ appointFolderRole: appointFolderRoleInto(store),
1261
+ now: () => NOW,
1262
+ };
1263
+
1264
+ const first = await bindImportedFolders(
1265
+ binder,
1266
+ TARGET_CONFIG_ID,
1267
+ PASSWORD_ACCOUNT_ID,
1268
+ );
1269
+ await bindImportedFolders(binder, TARGET_CONFIG_ID, OAUTH_ACCOUNT_ID);
1270
+
1271
+ assert.equal(first.dropped, 1);
1272
+ assert.equal(
1273
+ store.imports[0].unresolvedRefs.some(
1274
+ (ref) => ref.target === invoices.filterId,
1275
+ ),
1276
+ false,
1277
+ );
1278
+ assert.equal(store.imports[0].state, "Complete");
1279
+
1280
+ const again = await bindImportedFolders(
1281
+ binder,
1282
+ TARGET_CONFIG_ID,
1283
+ PASSWORD_ACCOUNT_ID,
1284
+ );
1285
+ assert.deepEqual(again, { bound: 0, dropped: 0, stillPending: 0 });
1286
+ });
1287
+
1288
+ test("a deleted filter's reference is dropped even when its folder never appears", async () => {
1289
+ const document = await exportSource(sourceWithLegacyFlagsFixture());
1290
+ const store = emptyStore();
1291
+ await apply(store, document);
1292
+
1293
+ const invoices = store.filters.find(
1294
+ (filter) => filter.name === "Invoices to Facturen",
1295
+ );
1296
+ assert.ok(invoices);
1297
+ store.filters = store.filters.filter(
1298
+ (filter) => filter.filterId !== invoices.filterId,
1299
+ );
1300
+
1301
+ discover(store, PASSWORD_ACCOUNT_ID, ["INBOX"]);
1302
+ const result = await bindImportedFolders(
1303
+ {
1304
+ repositories: repositoriesOf(store, TARGET_CONFIG_ID),
1305
+ appointFolderRole: appointFolderRoleInto(store),
1306
+ now: () => NOW,
1307
+ },
1308
+ TARGET_CONFIG_ID,
1309
+ PASSWORD_ACCOUNT_ID,
1310
+ );
1311
+
1312
+ assert.equal(result.dropped, 1);
1313
+ assert.equal(
1314
+ store.imports[0].unresolvedRefs.some(
1315
+ (ref) => ref.target === invoices.filterId,
1316
+ ),
1317
+ false,
1318
+ );
1319
+ assert.equal(
1320
+ pendingImportOf(store.imports)?.folderPaths.includes("INBOX.Facturen"),
1321
+ false,
1322
+ );
1323
+ });