@remit/drizzle-service 0.0.87 → 0.0.89

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.
@@ -40,6 +40,7 @@ export function rowToAccount(
40
40
  email: row.email,
41
41
  authType: row.authType as AccountItem["authType"],
42
42
  syncedServices: row.syncedServices as AccountItem["syncedServices"],
43
+ grantedScopes: row.grantedScopes as AccountItem["grantedScopes"],
43
44
  passwordHash: row.passwordHash ?? undefined,
44
45
  oauthRefreshTokenHash: row.oauthRefreshTokenHash ?? undefined,
45
46
  oauthTokenUpdatedAt: row.oauthTokenUpdatedAt ?? undefined,
@@ -83,6 +84,7 @@ export class AccountRepo implements IAccountRepository {
83
84
  email: input.email,
84
85
  authType: input.authType ?? "password",
85
86
  syncedServices: input.syncedServices ?? [AccountService.Mail],
87
+ grantedScopes: input.grantedScopes ?? [],
86
88
  passwordHash: input.passwordHash,
87
89
  oauthRefreshTokenHash: input.oauthRefreshTokenHash,
88
90
  oauthTokenUpdatedAt: input.oauthTokenUpdatedAt,
@@ -148,6 +150,8 @@ export class AccountRepo implements IAccountRepository {
148
150
  if (input.authType !== undefined) updates.authType = input.authType;
149
151
  if (input.syncedServices !== undefined)
150
152
  updates.syncedServices = input.syncedServices;
153
+ if (input.grantedScopes !== undefined)
154
+ updates.grantedScopes = input.grantedScopes;
151
155
  if (input.passwordHash !== undefined)
152
156
  updates.passwordHash = input.passwordHash;
153
157
  if (input.oauthRefreshTokenHash !== undefined)
@@ -16,6 +16,7 @@ import { createSqliteTestDb } from "../test-db-sqlite.js";
16
16
  import { AccountSettingRepo } from "./i4-account-setting.js";
17
17
  import { MailboxRepo } from "./i4-mailbox.js";
18
18
  import { MailboxSpecialUseRepo } from "./i4-mailbox-special-use.js";
19
+ import { mailboxCreated } from "./test-helpers.js";
19
20
 
20
21
  const makeMailboxInput = (
21
22
  accountId: string,
@@ -103,11 +104,15 @@ describe("MailboxSpecialUseRepo role lookups (sqlite)", () => {
103
104
 
104
105
  test("the appointment beats the folder the server flagged", async () => {
105
106
  const { accountId, accountConfigId } = await makeAccount();
106
- await mailboxes.create(makeMailboxInput(accountId, "INBOX"));
107
- const flagged = await mailboxes.create(makeMailboxInput(accountId, "Junk"));
107
+ mailboxCreated(
108
+ await mailboxes.create(makeMailboxInput(accountId, "INBOX")),
109
+ );
110
+ const flagged = mailboxCreated(
111
+ await mailboxes.create(makeMailboxInput(accountId, "Junk")),
112
+ );
108
113
  await repo.create(flagged.mailboxId, "Junk");
109
- const chosen = await mailboxes.create(
110
- makeMailboxInput(accountId, "INBOX/Rubbish"),
114
+ const chosen = mailboxCreated(
115
+ await mailboxes.create(makeMailboxInput(accountId, "INBOX/Rubbish")),
111
116
  );
112
117
  await appoint(
113
118
  accountConfigId,
@@ -126,10 +131,14 @@ describe("MailboxSpecialUseRepo role lookups (sqlite)", () => {
126
131
  // the user keeps mail in. `bin` is no longer a hint, and an appointment
127
132
  // overrides the proposal either way.
128
133
  const { accountId, accountConfigId } = await makeAccount();
129
- await mailboxes.create(makeMailboxInput(accountId, "INBOX"));
130
- const bin = await mailboxes.create(makeMailboxInput(accountId, "Bin"));
131
- const gmailTrash = await mailboxes.create(
132
- makeMailboxInput(accountId, "[Gmail]/Trash"),
134
+ mailboxCreated(
135
+ await mailboxes.create(makeMailboxInput(accountId, "INBOX")),
136
+ );
137
+ const bin = mailboxCreated(
138
+ await mailboxes.create(makeMailboxInput(accountId, "Bin")),
139
+ );
140
+ const gmailTrash = mailboxCreated(
141
+ await mailboxes.create(makeMailboxInput(accountId, "[Gmail]/Trash")),
133
142
  );
134
143
 
135
144
  assert.equal(
@@ -151,9 +160,11 @@ describe("MailboxSpecialUseRepo role lookups (sqlite)", () => {
151
160
 
152
161
  test("an appointment on a mailbox that is gone falls back rather than resolving to nothing", async () => {
153
162
  const { accountId, accountConfigId } = await makeAccount();
154
- await mailboxes.create(makeMailboxInput(accountId, "INBOX"));
155
- const archive = await mailboxes.create(
156
- makeMailboxInput(accountId, "INBOX/Archive"),
163
+ mailboxCreated(
164
+ await mailboxes.create(makeMailboxInput(accountId, "INBOX")),
165
+ );
166
+ const archive = mailboxCreated(
167
+ await mailboxes.create(makeMailboxInput(accountId, "INBOX/Archive")),
157
168
  );
158
169
  await appoint(
159
170
  accountConfigId,
@@ -171,12 +182,14 @@ describe("MailboxSpecialUseRepo role lookups (sqlite)", () => {
171
182
  // `Deleted` matches the name proposal, and emptying it would destroy mail
172
183
  // the user never put in a trash folder (audit #841).
173
184
  const { accountId, accountConfigId } = await makeAccount();
174
- await mailboxes.create(makeMailboxInput(accountId, "INBOX"));
175
- const keepsakes = await mailboxes.create(
176
- makeMailboxInput(accountId, "Deleted"),
185
+ mailboxCreated(
186
+ await mailboxes.create(makeMailboxInput(accountId, "INBOX")),
187
+ );
188
+ const keepsakes = mailboxCreated(
189
+ await mailboxes.create(makeMailboxInput(accountId, "Deleted")),
177
190
  );
178
- const trash = await mailboxes.create(
179
- makeMailboxInput(accountId, "[Gmail]/Trash"),
191
+ const trash = mailboxCreated(
192
+ await mailboxes.create(makeMailboxInput(accountId, "[Gmail]/Trash")),
180
193
  );
181
194
 
182
195
  const proposal = await repo.resolveTrashRole(accountId);
@@ -201,9 +214,11 @@ describe("MailboxSpecialUseRepo role lookups (sqlite)", () => {
201
214
  // flagged folder both hide that; the resolution names the id the user chose
202
215
  // so a refusal can offer the repair (#887).
203
216
  const { accountId, accountConfigId } = await makeAccount();
204
- await mailboxes.create(makeMailboxInput(accountId, "INBOX"));
205
- const flagged = await mailboxes.create(
206
- makeMailboxInput(accountId, "[Gmail]/Trash"),
217
+ mailboxCreated(
218
+ await mailboxes.create(makeMailboxInput(accountId, "INBOX")),
219
+ );
220
+ const flagged = mailboxCreated(
221
+ await mailboxes.create(makeMailboxInput(accountId, "[Gmail]/Trash")),
207
222
  );
208
223
  await repo.create(flagged.mailboxId, "Trash");
209
224
  const appointed = randomUUID();
@@ -237,9 +252,11 @@ describe("MailboxSpecialUseRepo role lookups (sqlite)", () => {
237
252
 
238
253
  test("resolves an INBOX-nested Junk folder that advertises \\Junk", async () => {
239
254
  const { accountId } = await makeAccount();
240
- await mailboxes.create(makeMailboxInput(accountId, "INBOX"));
241
- const spam = await mailboxes.create(
242
- makeMailboxInput(accountId, "INBOX/Spam"),
255
+ mailboxCreated(
256
+ await mailboxes.create(makeMailboxInput(accountId, "INBOX")),
257
+ );
258
+ const spam = mailboxCreated(
259
+ await mailboxes.create(makeMailboxInput(accountId, "INBOX/Spam")),
243
260
  );
244
261
  await repo.create(spam.mailboxId, "Junk");
245
262
 
@@ -252,9 +269,11 @@ describe("MailboxSpecialUseRepo role lookups (sqlite)", () => {
252
269
  // bare names, so `INBOX/Spam` resolved to nothing on a server that
253
270
  // advertises no \Junk.
254
271
  const { accountId } = await makeAccount();
255
- await mailboxes.create(makeMailboxInput(accountId, "INBOX"));
256
- const spam = await mailboxes.create(
257
- makeMailboxInput(accountId, "INBOX/Spam"),
272
+ mailboxCreated(
273
+ await mailboxes.create(makeMailboxInput(accountId, "INBOX")),
274
+ );
275
+ const spam = mailboxCreated(
276
+ await mailboxes.create(makeMailboxInput(accountId, "INBOX/Spam")),
258
277
  );
259
278
 
260
279
  const found = await repo.findJunkMailbox(accountId);
@@ -266,9 +285,11 @@ describe("MailboxSpecialUseRepo role lookups (sqlite)", () => {
266
285
  // whole-path rule, so `INBOX/Trash` resolved to nothing and a delete
267
286
  // refused on an account that plainly has a Trash folder.
268
287
  const { accountId } = await makeAccount();
269
- await mailboxes.create(makeMailboxInput(accountId, "INBOX"));
270
- const trash = await mailboxes.create(
271
- makeMailboxInput(accountId, "INBOX/Trash"),
288
+ mailboxCreated(
289
+ await mailboxes.create(makeMailboxInput(accountId, "INBOX")),
290
+ );
291
+ const trash = mailboxCreated(
292
+ await mailboxes.create(makeMailboxInput(accountId, "INBOX/Trash")),
272
293
  );
273
294
 
274
295
  assert.equal(
@@ -286,9 +307,11 @@ describe("MailboxSpecialUseRepo role lookups (sqlite)", () => {
286
307
 
287
308
  test("resolves an INBOX-nested Archive folder that advertises no special use", async () => {
288
309
  const { accountId } = await makeAccount();
289
- await mailboxes.create(makeMailboxInput(accountId, "INBOX"));
290
- const archive = await mailboxes.create(
291
- makeMailboxInput(accountId, "INBOX/Archive"),
310
+ mailboxCreated(
311
+ await mailboxes.create(makeMailboxInput(accountId, "INBOX")),
312
+ );
313
+ const archive = mailboxCreated(
314
+ await mailboxes.create(makeMailboxInput(accountId, "INBOX/Archive")),
292
315
  );
293
316
 
294
317
  assert.equal(
@@ -299,12 +322,14 @@ describe("MailboxSpecialUseRepo role lookups (sqlite)", () => {
299
322
 
300
323
  test("splits the leaf on the account's own delimiter, not on a slash", async () => {
301
324
  const { accountId } = await makeAccount();
302
- await mailboxes.create(makeMailboxInput(accountId, "INBOX", "."));
303
- const trash = await mailboxes.create(
304
- makeMailboxInput(accountId, "INBOX.Trash", "."),
325
+ mailboxCreated(
326
+ await mailboxes.create(makeMailboxInput(accountId, "INBOX", ".")),
305
327
  );
306
- const archive = await mailboxes.create(
307
- makeMailboxInput(accountId, "INBOX.Archive", "."),
328
+ const trash = mailboxCreated(
329
+ await mailboxes.create(makeMailboxInput(accountId, "INBOX.Trash", ".")),
330
+ );
331
+ const archive = mailboxCreated(
332
+ await mailboxes.create(makeMailboxInput(accountId, "INBOX.Archive", ".")),
308
333
  );
309
334
 
310
335
  assert.equal(
@@ -319,8 +344,12 @@ describe("MailboxSpecialUseRepo role lookups (sqlite)", () => {
319
344
 
320
345
  test("answers null when the account has no Junk folder at all", async () => {
321
346
  const { accountId } = await makeAccount();
322
- await mailboxes.create(makeMailboxInput(accountId, "INBOX"));
323
- await mailboxes.create(makeMailboxInput(accountId, "INBOX/Work"));
347
+ mailboxCreated(
348
+ await mailboxes.create(makeMailboxInput(accountId, "INBOX")),
349
+ );
350
+ mailboxCreated(
351
+ await mailboxes.create(makeMailboxInput(accountId, "INBOX/Work")),
352
+ );
324
353
 
325
354
  assert.equal(await repo.findJunkMailbox(accountId), null);
326
355
  });
@@ -11,6 +11,7 @@ import {
11
11
  shippedTableDdl,
12
12
  } from "../test-shipped-sqlite-schema.js";
13
13
  import { MailboxRepo } from "./i4-mailbox.js";
14
+ import { mailboxCreated } from "./test-helpers.js";
14
15
 
15
16
  function makeMailboxInput(accountId: string, fullPath = "INBOX") {
16
17
  return {
@@ -50,10 +51,12 @@ describe("MailboxRepo (sqlite)", () => {
50
51
  const accountId = randomUUID();
51
52
  const modseq = "18446744073709551615";
52
53
 
53
- const created = await repo.create({
54
- ...makeMailboxInput(accountId),
55
- highestModseq: modseq,
56
- });
54
+ const created = mailboxCreated(
55
+ await repo.create({
56
+ ...makeMailboxInput(accountId),
57
+ highestModseq: modseq,
58
+ }),
59
+ );
57
60
  assert.equal(created.highestModseq, modseq);
58
61
 
59
62
  const fetched = await repo.get(accountId, created.mailboxId);
@@ -70,8 +73,8 @@ describe("MailboxRepo (sqlite)", () => {
70
73
 
71
74
  test("a row written without a state reads back synced (D1)", async () => {
72
75
  const accountId = randomUUID();
73
- const created = await repo.create(
74
- makeMailboxInput(accountId, "Discovered"),
76
+ const created = mailboxCreated(
77
+ await repo.create(makeMailboxInput(accountId, "Discovered")),
75
78
  );
76
79
  assert.equal(created.syncStatus, MailboxSyncStatus.synced);
77
80
  const fetched = await repo.get(accountId, created.mailboxId);
@@ -80,7 +83,9 @@ describe("MailboxRepo (sqlite)", () => {
80
83
 
81
84
  test("pendingPath round-trips and clears", async () => {
82
85
  const accountId = randomUUID();
83
- const created = await repo.create(makeMailboxInput(accountId, "Archive"));
86
+ const created = mailboxCreated(
87
+ await repo.create(makeMailboxInput(accountId, "Archive")),
88
+ );
84
89
  assert.equal(created.pendingPath, undefined);
85
90
 
86
91
  const claimed = await repo.transition(accountId, created.mailboxId, {
@@ -102,6 +107,53 @@ describe("MailboxRepo (sqlite)", () => {
102
107
  assert.equal(settled?.pendingPath, undefined);
103
108
  assert.equal(settled?.fullPath, "Records");
104
109
  });
110
+
111
+ test("a second row at a path the account holds is refused as PathTaken", async () => {
112
+ const accountId = randomUUID();
113
+ const first = mailboxCreated(
114
+ await repo.create(makeMailboxInput(accountId, "Notifications")),
115
+ );
116
+
117
+ const second = await repo.create(
118
+ makeMailboxInput(accountId, "Notifications"),
119
+ );
120
+
121
+ assert.deepEqual(second, { outcome: "PathTaken" });
122
+ assert.equal(
123
+ (await repo.findByPath(accountId, "Notifications"))?.mailboxId,
124
+ first.mailboxId,
125
+ );
126
+ });
127
+
128
+ test("another account may hold the same path", async () => {
129
+ const created = await repo.create(
130
+ makeMailboxInput(randomUUID(), "Notifications"),
131
+ );
132
+ assert.equal(created.outcome, "Created");
133
+ });
134
+
135
+ test("a write that moves a row onto a path another row holds is rejected", async () => {
136
+ const accountId = randomUUID();
137
+ mailboxCreated(
138
+ await repo.create(makeMailboxInput(accountId, "INBOX/Notifications")),
139
+ );
140
+ const recreated = mailboxCreated(
141
+ await repo.create({
142
+ ...makeMailboxInput(accountId, "Notifications"),
143
+ syncStatus: MailboxSyncStatus.pending,
144
+ }),
145
+ );
146
+
147
+ await assert.rejects(
148
+ repo.transition(accountId, recreated.mailboxId, {
149
+ from: [MailboxSyncStatus.pending],
150
+ wherePendingPath: null,
151
+ to: MailboxSyncStatus.synced,
152
+ set: { fullPath: "INBOX/Notifications" },
153
+ }),
154
+ /UNIQUE constraint failed: mailbox\.account_id, mailbox\.full_path/,
155
+ );
156
+ });
105
157
  });
106
158
 
107
159
  /**
@@ -126,6 +178,7 @@ describe("MailboxRepo (sqlite, shipped migrations)", () => {
126
178
  applyMigration(sqlite, "0026_mailbox_sync_status_backfill");
127
179
  applyMigration(sqlite, "0027_mailbox_sync_status_total");
128
180
  applyMigration(sqlite, "0028_mailbox_pending_path");
181
+ applyMigration(sqlite, "0031_mailbox_path_unique");
129
182
  const db = drizzle(sqlite, { schema: { mailbox: mailboxTable } });
130
183
  repo = new MailboxRepo(db as never);
131
184
  close = async () => {
@@ -146,10 +199,12 @@ describe("MailboxRepo (sqlite, shipped migrations)", () => {
146
199
 
147
200
  test("reads a plain-digit cursor back as a string", async () => {
148
201
  const accountId = randomUUID();
149
- const created = await repo.create({
150
- ...makeMailboxInput(accountId),
151
- highestModseq: "900",
152
- });
202
+ const created = mailboxCreated(
203
+ await repo.create({
204
+ ...makeMailboxInput(accountId),
205
+ highestModseq: "900",
206
+ }),
207
+ );
153
208
 
154
209
  assert.strictEqual(created.highestModseq, "900");
155
210
 
@@ -159,10 +214,12 @@ describe("MailboxRepo (sqlite, shipped migrations)", () => {
159
214
 
160
215
  test("keeps a resumable cursor intact", async () => {
161
216
  const accountId = randomUUID();
162
- const created = await repo.create({
163
- ...makeMailboxInput(accountId, "Archive"),
164
- highestModseq: "900:149",
165
- });
217
+ const created = mailboxCreated(
218
+ await repo.create({
219
+ ...makeMailboxInput(accountId, "Archive"),
220
+ highestModseq: "900:149",
221
+ }),
222
+ );
166
223
 
167
224
  const fetched = await repo.get(accountId, created.mailboxId);
168
225
  assert.strictEqual(fetched.highestModseq, "900:149");
@@ -170,13 +227,17 @@ describe("MailboxRepo (sqlite, shipped migrations)", () => {
170
227
 
171
228
  test("a row inserted without a state reads back synced", async () => {
172
229
  const accountId = randomUUID();
173
- const created = await repo.create(makeMailboxInput(accountId, "Notes"));
230
+ const created = mailboxCreated(
231
+ await repo.create(makeMailboxInput(accountId, "Notes")),
232
+ );
174
233
  assert.equal(created.syncStatus, MailboxSyncStatus.synced);
175
234
  });
176
235
 
177
236
  test("pendingPath round-trips and clears on the shipped shape", async () => {
178
237
  const accountId = randomUUID();
179
- const created = await repo.create(makeMailboxInput(accountId, "Receipts"));
238
+ const created = mailboxCreated(
239
+ await repo.create(makeMailboxInput(accountId, "Receipts")),
240
+ );
180
241
 
181
242
  const claimed = await repo.transition(accountId, created.mailboxId, {
182
243
  from: [MailboxSyncStatus.synced],
@@ -196,10 +257,12 @@ describe("MailboxRepo (sqlite, shipped migrations)", () => {
196
257
  test("round-trips a cursor above 2^53 with its exact digits", async () => {
197
258
  const accountId = randomUUID();
198
259
  const modseq = "18446744073709551615";
199
- const created = await repo.create({
200
- ...makeMailboxInput(accountId, "Sent"),
201
- highestModseq: modseq,
202
- });
260
+ const created = mailboxCreated(
261
+ await repo.create({
262
+ ...makeMailboxInput(accountId, "Sent"),
263
+ highestModseq: modseq,
264
+ }),
265
+ );
203
266
 
204
267
  assert.strictEqual(created.highestModseq, modseq);
205
268
  const fetched = await repo.get(accountId, created.mailboxId);