@remit/drizzle-service 0.0.3 → 0.0.5
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/drizzle-service",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Drizzle ORM service parameterized by dialect (Postgres / SQLite)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"repository": {
|
|
48
48
|
"type": "git",
|
|
49
|
-
"url": "git+https://github.com/remit-mail/
|
|
49
|
+
"url": "git+https://github.com/remit-mail/reader.git",
|
|
50
50
|
"directory": "packages/drizzle-service"
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -135,610 +135,589 @@ describe("clampThreadSearchLimit", () => {
|
|
|
135
135
|
// These six scenarios mirror the canonical DynamoDB test suite in
|
|
136
136
|
// packages/remit-electrodb-service/src/models/thread-message.test.ts.
|
|
137
137
|
|
|
138
|
-
describe(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
after(async () => {
|
|
151
|
-
for (const fn of cleanup.reverse()) {
|
|
152
|
-
await fn();
|
|
153
|
-
}
|
|
154
|
-
await repo.close();
|
|
155
|
-
});
|
|
138
|
+
describe("DrizzleThreadMessageRepository.searchByMailboxWindow / countByMailbox", {
|
|
139
|
+
skip: !RUN_INTEG,
|
|
140
|
+
}, () => {
|
|
141
|
+
let repo: DrizzleThreadMessageRepository;
|
|
142
|
+
const cleanup: Array<() => Promise<void>> = [];
|
|
143
|
+
|
|
144
|
+
before(async () => {
|
|
145
|
+
await setupDb();
|
|
146
|
+
repo = new DrizzleThreadMessageRepository(PG_URL);
|
|
147
|
+
});
|
|
156
148
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
rows: Array<Partial<CreateThreadMessageInput>>,
|
|
161
|
-
): Promise<void> {
|
|
162
|
-
for (const overrides of rows) {
|
|
163
|
-
const created = await repo.create(
|
|
164
|
-
makeInput(accountConfigId, mailboxId, overrides),
|
|
165
|
-
);
|
|
166
|
-
cleanup.push(() =>
|
|
167
|
-
repo.delete(accountConfigId, created.threadMessageId),
|
|
168
|
-
);
|
|
169
|
-
}
|
|
149
|
+
after(async () => {
|
|
150
|
+
for (const fn of cleanup.reverse()) {
|
|
151
|
+
await fn();
|
|
170
152
|
}
|
|
153
|
+
await repo.close();
|
|
154
|
+
});
|
|
171
155
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
{ isRead: true, subject: "read one" },
|
|
181
|
-
]);
|
|
182
|
-
|
|
183
|
-
const unread = await repo.searchByMailboxWindow(
|
|
184
|
-
acct,
|
|
185
|
-
mbx,
|
|
186
|
-
{ unread: true },
|
|
187
|
-
{ excludeDeleted: true },
|
|
188
|
-
);
|
|
189
|
-
assert.equal(unread.items.length, 2);
|
|
190
|
-
assert.ok(unread.items.every((r) => r.isRead === false));
|
|
191
|
-
|
|
192
|
-
const read = await repo.searchByMailboxWindow(
|
|
193
|
-
acct,
|
|
194
|
-
mbx,
|
|
195
|
-
{ unread: false },
|
|
196
|
-
{ excludeDeleted: true },
|
|
197
|
-
);
|
|
198
|
-
assert.equal(read.items.length, 1);
|
|
199
|
-
assert.equal(read.items[0].subject, "read one");
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
// ── Scenario 2 ────────────────────────────────────────────────────────────
|
|
203
|
-
// subject filter matches a substring of the subject.
|
|
204
|
-
test("subject filter matches a substring", async () => {
|
|
205
|
-
const acct = uuid();
|
|
206
|
-
const mbx = uuid();
|
|
207
|
-
await seed(acct, mbx, [
|
|
208
|
-
{ subject: "invoice March" },
|
|
209
|
-
{ subject: "invoice April" },
|
|
210
|
-
{ subject: "newsletter" },
|
|
211
|
-
]);
|
|
212
|
-
|
|
213
|
-
const result = await repo.searchByMailboxWindow(
|
|
214
|
-
acct,
|
|
215
|
-
mbx,
|
|
216
|
-
{ subject: "invoice" },
|
|
217
|
-
{ excludeDeleted: true },
|
|
218
|
-
);
|
|
219
|
-
assert.equal(result.items.length, 2);
|
|
220
|
-
assert.ok(
|
|
221
|
-
result.items.every((r) => r.subject?.includes("invoice") ?? false),
|
|
222
|
-
);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
// ── Scenario 3 ────────────────────────────────────────────────────────────
|
|
226
|
-
// A full page (limit 2 over 5 rows) yields a cursor to resume from.
|
|
227
|
-
test("a full page yields a continuation cursor", async () => {
|
|
228
|
-
const acct = uuid();
|
|
229
|
-
const mbx = uuid();
|
|
230
|
-
const now = Date.now();
|
|
231
|
-
await seed(
|
|
232
|
-
acct,
|
|
233
|
-
mbx,
|
|
234
|
-
Array.from({ length: 5 }, (_, i) => ({
|
|
235
|
-
subject: `row ${i}`,
|
|
236
|
-
sentDate: now - i,
|
|
237
|
-
internalDate: now - i,
|
|
238
|
-
})),
|
|
239
|
-
);
|
|
240
|
-
|
|
241
|
-
const result = await repo.searchByMailboxWindow(
|
|
242
|
-
acct,
|
|
243
|
-
mbx,
|
|
244
|
-
{},
|
|
245
|
-
{ excludeDeleted: true, limit: 2 },
|
|
246
|
-
);
|
|
247
|
-
assert.equal(result.items.length, 2);
|
|
248
|
-
assert.ok(result.continuationToken, "full page must yield a cursor");
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
// ── Scenario 4 ────────────────────────────────────────────────────────────
|
|
252
|
-
// Matching runs over the WHOLE mailbox via the trigram index, so an old
|
|
253
|
-
// match well behind the recent rows is still found and paged (the DynamoDB
|
|
254
|
-
// recent-window bound does not apply on Postgres — the improvement over #443
|
|
255
|
-
// on the DDB path).
|
|
256
|
-
test("an old match behind the recent rows is still found — matching is whole-mailbox, not window-bounded", async () => {
|
|
257
|
-
const acct = uuid();
|
|
258
|
-
const mbx = uuid();
|
|
259
|
-
const now = Date.now();
|
|
260
|
-
// The newest 2 rows do NOT match; the older 4 rows DO — searching "match"
|
|
261
|
-
// (lowercased, as the client sends it) must reach all four.
|
|
262
|
-
const rows = [
|
|
263
|
-
{ subject: "recent plain a", sentDate: now, internalDate: now },
|
|
264
|
-
{ subject: "recent plain b", sentDate: now - 1, internalDate: now - 1 },
|
|
265
|
-
{ subject: "old MATCH c", sentDate: now - 2, internalDate: now - 2 },
|
|
266
|
-
{ subject: "old MATCH d", sentDate: now - 3, internalDate: now - 3 },
|
|
267
|
-
{ subject: "old MATCH e", sentDate: now - 4, internalDate: now - 4 },
|
|
268
|
-
{ subject: "old MATCH f", sentDate: now - 5, internalDate: now - 5 },
|
|
269
|
-
];
|
|
270
|
-
await seed(acct, mbx, rows);
|
|
271
|
-
|
|
272
|
-
const page1 = await repo.searchByMailboxWindow(
|
|
273
|
-
acct,
|
|
274
|
-
mbx,
|
|
275
|
-
{ subject: "match" },
|
|
276
|
-
{ excludeDeleted: true, limit: 2 },
|
|
277
|
-
);
|
|
278
|
-
assert.deepEqual(
|
|
279
|
-
page1.items.map((r) => r.subject),
|
|
280
|
-
["old MATCH c", "old MATCH d"],
|
|
281
|
-
"first page holds the two newest matches, not the recent non-matches",
|
|
282
|
-
);
|
|
283
|
-
assert.ok(
|
|
284
|
-
page1.continuationToken,
|
|
285
|
-
"more matches remain — cursor expected",
|
|
286
|
-
);
|
|
287
|
-
|
|
288
|
-
const page2 = await repo.searchByMailboxWindow(
|
|
289
|
-
acct,
|
|
290
|
-
mbx,
|
|
291
|
-
{ subject: "match" },
|
|
292
|
-
{
|
|
293
|
-
excludeDeleted: true,
|
|
294
|
-
limit: 2,
|
|
295
|
-
continuationToken: page1.continuationToken,
|
|
296
|
-
},
|
|
297
|
-
);
|
|
298
|
-
assert.deepEqual(
|
|
299
|
-
page2.items.map((r) => r.subject),
|
|
300
|
-
["old MATCH e", "old MATCH f"],
|
|
301
|
-
"paging reaches the oldest matches",
|
|
302
|
-
);
|
|
303
|
-
|
|
304
|
-
const count = await repo.countByMailbox(
|
|
305
|
-
acct,
|
|
306
|
-
mbx,
|
|
307
|
-
{ subject: "match" },
|
|
308
|
-
{ excludeDeleted: true, limit: 2 },
|
|
309
|
-
);
|
|
310
|
-
assert.equal(
|
|
311
|
-
count,
|
|
312
|
-
2,
|
|
313
|
-
"count is capped at the page limit when more match",
|
|
156
|
+
async function seed(
|
|
157
|
+
accountConfigId: string,
|
|
158
|
+
mailboxId: string,
|
|
159
|
+
rows: Array<Partial<CreateThreadMessageInput>>,
|
|
160
|
+
): Promise<void> {
|
|
161
|
+
for (const overrides of rows) {
|
|
162
|
+
const created = await repo.create(
|
|
163
|
+
makeInput(accountConfigId, mailboxId, overrides),
|
|
314
164
|
);
|
|
315
|
-
|
|
165
|
+
cleanup.push(() => repo.delete(accountConfigId, created.threadMessageId));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Scenario 1 ────────────────────────────────────────────────────────────
|
|
170
|
+
// The unread boolean narrows results to matching rows only.
|
|
171
|
+
test("unread filter returns only matching rows", async () => {
|
|
172
|
+
const acct = uuid();
|
|
173
|
+
const mbx = uuid();
|
|
174
|
+
await seed(acct, mbx, [
|
|
175
|
+
{ isRead: false, subject: "unread one" },
|
|
176
|
+
{ isRead: false, subject: "unread two" },
|
|
177
|
+
{ isRead: true, subject: "read one" },
|
|
178
|
+
]);
|
|
179
|
+
|
|
180
|
+
const unread = await repo.searchByMailboxWindow(
|
|
181
|
+
acct,
|
|
182
|
+
mbx,
|
|
183
|
+
{ unread: true },
|
|
184
|
+
{ excludeDeleted: true },
|
|
185
|
+
);
|
|
186
|
+
assert.equal(unread.items.length, 2);
|
|
187
|
+
assert.ok(unread.items.every((r) => r.isRead === false));
|
|
188
|
+
|
|
189
|
+
const read = await repo.searchByMailboxWindow(
|
|
190
|
+
acct,
|
|
191
|
+
mbx,
|
|
192
|
+
{ unread: false },
|
|
193
|
+
{ excludeDeleted: true },
|
|
194
|
+
);
|
|
195
|
+
assert.equal(read.items.length, 1);
|
|
196
|
+
assert.equal(read.items[0].subject, "read one");
|
|
197
|
+
});
|
|
316
198
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
199
|
+
// ── Scenario 2 ────────────────────────────────────────────────────────────
|
|
200
|
+
// subject filter matches a substring of the subject.
|
|
201
|
+
test("subject filter matches a substring", async () => {
|
|
202
|
+
const acct = uuid();
|
|
203
|
+
const mbx = uuid();
|
|
204
|
+
await seed(acct, mbx, [
|
|
205
|
+
{ subject: "invoice March" },
|
|
206
|
+
{ subject: "invoice April" },
|
|
207
|
+
{ subject: "newsletter" },
|
|
208
|
+
]);
|
|
209
|
+
|
|
210
|
+
const result = await repo.searchByMailboxWindow(
|
|
211
|
+
acct,
|
|
212
|
+
mbx,
|
|
213
|
+
{ subject: "invoice" },
|
|
214
|
+
{ excludeDeleted: true },
|
|
215
|
+
);
|
|
216
|
+
assert.equal(result.items.length, 2);
|
|
217
|
+
assert.ok(
|
|
218
|
+
result.items.every((r) => r.subject?.includes("invoice") ?? false),
|
|
219
|
+
);
|
|
220
|
+
});
|
|
333
221
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
222
|
+
// ── Scenario 3 ────────────────────────────────────────────────────────────
|
|
223
|
+
// A full page (limit 2 over 5 rows) yields a cursor to resume from.
|
|
224
|
+
test("a full page yields a continuation cursor", async () => {
|
|
225
|
+
const acct = uuid();
|
|
226
|
+
const mbx = uuid();
|
|
227
|
+
const now = Date.now();
|
|
228
|
+
await seed(
|
|
229
|
+
acct,
|
|
230
|
+
mbx,
|
|
231
|
+
Array.from({ length: 5 }, (_, i) => ({
|
|
232
|
+
subject: `row ${i}`,
|
|
233
|
+
sentDate: now - i,
|
|
234
|
+
internalDate: now - i,
|
|
235
|
+
})),
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
const result = await repo.searchByMailboxWindow(
|
|
239
|
+
acct,
|
|
240
|
+
mbx,
|
|
241
|
+
{},
|
|
242
|
+
{ excludeDeleted: true, limit: 2 },
|
|
243
|
+
);
|
|
244
|
+
assert.equal(result.items.length, 2);
|
|
245
|
+
assert.ok(result.continuationToken, "full page must yield a cursor");
|
|
246
|
+
});
|
|
353
247
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
248
|
+
// ── Scenario 4 ────────────────────────────────────────────────────────────
|
|
249
|
+
// Matching runs over the WHOLE mailbox via the trigram index, so an old
|
|
250
|
+
// match well behind the recent rows is still found and paged (the DynamoDB
|
|
251
|
+
// recent-window bound does not apply on Postgres — the improvement over #443
|
|
252
|
+
// on the DDB path).
|
|
253
|
+
test("an old match behind the recent rows is still found — matching is whole-mailbox, not window-bounded", async () => {
|
|
254
|
+
const acct = uuid();
|
|
255
|
+
const mbx = uuid();
|
|
256
|
+
const now = Date.now();
|
|
257
|
+
// The newest 2 rows do NOT match; the older 4 rows DO — searching "match"
|
|
258
|
+
// (lowercased, as the client sends it) must reach all four.
|
|
259
|
+
const rows = [
|
|
260
|
+
{ subject: "recent plain a", sentDate: now, internalDate: now },
|
|
261
|
+
{ subject: "recent plain b", sentDate: now - 1, internalDate: now - 1 },
|
|
262
|
+
{ subject: "old MATCH c", sentDate: now - 2, internalDate: now - 2 },
|
|
263
|
+
{ subject: "old MATCH d", sentDate: now - 3, internalDate: now - 3 },
|
|
264
|
+
{ subject: "old MATCH e", sentDate: now - 4, internalDate: now - 4 },
|
|
265
|
+
{ subject: "old MATCH f", sentDate: now - 5, internalDate: now - 5 },
|
|
266
|
+
];
|
|
267
|
+
await seed(acct, mbx, rows);
|
|
268
|
+
|
|
269
|
+
const page1 = await repo.searchByMailboxWindow(
|
|
270
|
+
acct,
|
|
271
|
+
mbx,
|
|
272
|
+
{ subject: "match" },
|
|
273
|
+
{ excludeDeleted: true, limit: 2 },
|
|
274
|
+
);
|
|
275
|
+
assert.deepEqual(
|
|
276
|
+
page1.items.map((r) => r.subject),
|
|
277
|
+
["old MATCH c", "old MATCH d"],
|
|
278
|
+
"first page holds the two newest matches, not the recent non-matches",
|
|
279
|
+
);
|
|
280
|
+
assert.ok(page1.continuationToken, "more matches remain — cursor expected");
|
|
281
|
+
|
|
282
|
+
const page2 = await repo.searchByMailboxWindow(
|
|
283
|
+
acct,
|
|
284
|
+
mbx,
|
|
285
|
+
{ subject: "match" },
|
|
286
|
+
{
|
|
287
|
+
excludeDeleted: true,
|
|
288
|
+
limit: 2,
|
|
289
|
+
continuationToken: page1.continuationToken,
|
|
290
|
+
},
|
|
291
|
+
);
|
|
292
|
+
assert.deepEqual(
|
|
293
|
+
page2.items.map((r) => r.subject),
|
|
294
|
+
["old MATCH e", "old MATCH f"],
|
|
295
|
+
"paging reaches the oldest matches",
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
const count = await repo.countByMailbox(
|
|
299
|
+
acct,
|
|
300
|
+
mbx,
|
|
301
|
+
{ subject: "match" },
|
|
302
|
+
{ excludeDeleted: true, limit: 2 },
|
|
303
|
+
);
|
|
304
|
+
assert.equal(count, 2, "count is capped at the page limit when more match");
|
|
305
|
+
});
|
|
369
306
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
307
|
+
// ── Scenario 5 ────────────────────────────────────────────────────────────
|
|
308
|
+
// desc default: the first page holds the newest matches; count equals the
|
|
309
|
+
// page length when more rows match than the limit.
|
|
310
|
+
test("desc page holds the newest matches and count equals items.length", async () => {
|
|
311
|
+
const acct = uuid();
|
|
312
|
+
const mbx = uuid();
|
|
313
|
+
const now = Date.now();
|
|
314
|
+
await seed(
|
|
315
|
+
acct,
|
|
316
|
+
mbx,
|
|
317
|
+
Array.from({ length: 5 }, (_, i) => ({
|
|
318
|
+
subject: `alpha ${i}`,
|
|
319
|
+
sentDate: now - i,
|
|
320
|
+
internalDate: now - i,
|
|
321
|
+
})),
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
const window = await repo.searchByMailboxWindow(
|
|
325
|
+
acct,
|
|
326
|
+
mbx,
|
|
327
|
+
{ subject: "alpha" },
|
|
328
|
+
{ excludeDeleted: true, limit: 2 },
|
|
329
|
+
);
|
|
330
|
+
assert.equal(window.items.length, 2);
|
|
331
|
+
const dates = window.items.map((r) => r.sentDate).sort((a, b) => b - a);
|
|
332
|
+
assert.deepEqual(dates, [now, now - 1], "page holds the two newest rows");
|
|
333
|
+
|
|
334
|
+
const count = await repo.countByMailbox(
|
|
335
|
+
acct,
|
|
336
|
+
mbx,
|
|
337
|
+
{ subject: "alpha" },
|
|
338
|
+
{ excludeDeleted: true, limit: 2 },
|
|
339
|
+
);
|
|
340
|
+
assert.equal(count, window.items.length, "count == items.length");
|
|
341
|
+
assert.equal(count, 2);
|
|
342
|
+
});
|
|
383
343
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
344
|
+
// ── Scenario 6 ────────────────────────────────────────────────────────────
|
|
345
|
+
// order:asc returns the OLDEST matches first; count agrees with the page.
|
|
346
|
+
test("order:asc returns the oldest matches first", async () => {
|
|
347
|
+
const acct = uuid();
|
|
348
|
+
const mbx = uuid();
|
|
349
|
+
const now = Date.now();
|
|
350
|
+
await seed(
|
|
351
|
+
acct,
|
|
352
|
+
mbx,
|
|
353
|
+
Array.from({ length: 5 }, (_, i) => ({
|
|
354
|
+
subject: `beta ${i}`,
|
|
355
|
+
sentDate: now - i,
|
|
356
|
+
internalDate: now - i,
|
|
357
|
+
})),
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
const window = await repo.searchByMailboxWindow(
|
|
361
|
+
acct,
|
|
362
|
+
mbx,
|
|
363
|
+
{ subject: "beta" },
|
|
364
|
+
{ excludeDeleted: true, limit: 2, order: "asc" },
|
|
365
|
+
);
|
|
366
|
+
assert.equal(window.items.length, 2);
|
|
367
|
+
const dates = window.items.map((r) => r.sentDate).sort((a, b) => a - b);
|
|
368
|
+
assert.deepEqual(
|
|
369
|
+
dates,
|
|
370
|
+
[now - 4, now - 3],
|
|
371
|
+
"asc page holds the two oldest rows",
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
const count = await repo.countByMailbox(
|
|
375
|
+
acct,
|
|
376
|
+
mbx,
|
|
377
|
+
{ subject: "beta" },
|
|
378
|
+
{ excludeDeleted: true, limit: 2, order: "asc" },
|
|
379
|
+
);
|
|
380
|
+
assert.equal(count, window.items.length, "count == items.length for asc");
|
|
381
|
+
assert.equal(count, 2);
|
|
382
|
+
});
|
|
383
|
+
});
|
|
395
384
|
|
|
396
385
|
// ─── Native text-search semantics ─────────────────────────────────────────────
|
|
397
386
|
// The type-ahead search box lowercases the query before sending it. These tests
|
|
398
387
|
// pin the Postgres-native behaviour: case- and accent-insensitive substring
|
|
399
388
|
// matching over the whole mailbox, scoped to the account/mailbox.
|
|
400
389
|
|
|
401
|
-
describe(
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
const cleanup: Array<() => Promise<void>> = [];
|
|
390
|
+
describe("DrizzleThreadMessageRepository — native text search", {
|
|
391
|
+
skip: !RUN_INTEG,
|
|
392
|
+
}, () => {
|
|
393
|
+
let repo: DrizzleThreadMessageRepository;
|
|
394
|
+
const cleanup: Array<() => Promise<void>> = [];
|
|
407
395
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
after(async () => {
|
|
414
|
-
for (const fn of cleanup.reverse()) {
|
|
415
|
-
await fn();
|
|
416
|
-
}
|
|
417
|
-
await repo.close();
|
|
418
|
-
});
|
|
396
|
+
before(async () => {
|
|
397
|
+
await setupDb();
|
|
398
|
+
repo = new DrizzleThreadMessageRepository(PG_URL);
|
|
399
|
+
});
|
|
419
400
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
rows: Array<Partial<CreateThreadMessageInput>>,
|
|
424
|
-
): Promise<void> {
|
|
425
|
-
for (const overrides of rows) {
|
|
426
|
-
const created = await repo.create(
|
|
427
|
-
makeInput(accountConfigId, mailboxId, overrides),
|
|
428
|
-
);
|
|
429
|
-
cleanup.push(() =>
|
|
430
|
-
repo.delete(accountConfigId, created.threadMessageId),
|
|
431
|
-
);
|
|
432
|
-
}
|
|
401
|
+
after(async () => {
|
|
402
|
+
for (const fn of cleanup.reverse()) {
|
|
403
|
+
await fn();
|
|
433
404
|
}
|
|
405
|
+
await repo.close();
|
|
406
|
+
});
|
|
434
407
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
await
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
408
|
+
async function seed(
|
|
409
|
+
accountConfigId: string,
|
|
410
|
+
mailboxId: string,
|
|
411
|
+
rows: Array<Partial<CreateThreadMessageInput>>,
|
|
412
|
+
): Promise<void> {
|
|
413
|
+
for (const overrides of rows) {
|
|
414
|
+
const created = await repo.create(
|
|
415
|
+
makeInput(accountConfigId, mailboxId, overrides),
|
|
416
|
+
);
|
|
417
|
+
cleanup.push(() => repo.delete(accountConfigId, created.threadMessageId));
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const subjectsOf = (r: { items: Array<{ subject?: string }> }) =>
|
|
422
|
+
r.items.map((i) => i.subject).sort();
|
|
423
|
+
|
|
424
|
+
test("subject match is case-insensitive (client lowercases the query)", async () => {
|
|
425
|
+
const acct = uuid();
|
|
426
|
+
const mbx = uuid();
|
|
427
|
+
await seed(acct, mbx, [
|
|
428
|
+
{ subject: "Quarterly Invoice" },
|
|
429
|
+
{ subject: "invoice reminder" },
|
|
430
|
+
{ subject: "Newsletter" },
|
|
431
|
+
]);
|
|
432
|
+
|
|
433
|
+
const res = await repo.searchByMailboxWindow(acct, mbx, {
|
|
434
|
+
query: "invoice",
|
|
454
435
|
});
|
|
436
|
+
assert.deepEqual(subjectsOf(res), [
|
|
437
|
+
"Quarterly Invoice",
|
|
438
|
+
"invoice reminder",
|
|
439
|
+
]);
|
|
440
|
+
});
|
|
455
441
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
const accented = await repo.searchByMailboxWindow(acct, mbx, {
|
|
465
|
-
query: "angelique",
|
|
466
|
-
});
|
|
467
|
-
assert.deepEqual(
|
|
468
|
-
subjectsOf(accented),
|
|
469
|
-
["a"],
|
|
470
|
-
"accent-folded name matches",
|
|
471
|
-
);
|
|
442
|
+
test("sender name match is case-insensitive and accent-insensitive", async () => {
|
|
443
|
+
const acct = uuid();
|
|
444
|
+
const mbx = uuid();
|
|
445
|
+
await seed(acct, mbx, [
|
|
446
|
+
{ fromName: "Angélique Müller", subject: "a" },
|
|
447
|
+
{ fromName: "Someone Else", subject: "b" },
|
|
448
|
+
]);
|
|
472
449
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
});
|
|
476
|
-
assert.deepEqual(subjectsOf(upper), ["a"], "case-folded name matches");
|
|
450
|
+
const accented = await repo.searchByMailboxWindow(acct, mbx, {
|
|
451
|
+
query: "angelique",
|
|
477
452
|
});
|
|
453
|
+
assert.deepEqual(subjectsOf(accented), ["a"], "accent-folded name matches");
|
|
478
454
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
const mbx = uuid();
|
|
482
|
-
await seed(acct, mbx, [
|
|
483
|
-
{ fromName: "Jane Doe", fromEmail: "jane@acme.example", subject: "a" },
|
|
484
|
-
{ fromName: "Bob", fromEmail: "bob@other.example", subject: "b" },
|
|
485
|
-
]);
|
|
486
|
-
|
|
487
|
-
const byDomain = await repo.searchByMailboxWindow(acct, mbx, {
|
|
488
|
-
from: "acme",
|
|
489
|
-
});
|
|
490
|
-
assert.deepEqual(subjectsOf(byDomain), ["a"], "matches the email domain");
|
|
455
|
+
const upper = await repo.searchByMailboxWindow(acct, mbx, {
|
|
456
|
+
query: "muller",
|
|
491
457
|
});
|
|
458
|
+
assert.deepEqual(subjectsOf(upper), ["a"], "case-folded name matches");
|
|
459
|
+
});
|
|
492
460
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
]);
|
|
501
|
-
|
|
502
|
-
// "emma sleep": each token must appear in subject OR from — row 1 (Emma in
|
|
503
|
-
// from, Sleep in subject) and row 3 (both in subject) match; row 2 does not.
|
|
504
|
-
const res = await repo.searchByMailboxWindow(acct, mbx, {
|
|
505
|
-
query: "emma sleep",
|
|
506
|
-
});
|
|
507
|
-
assert.deepEqual(subjectsOf(res), [
|
|
508
|
-
"Emma Sleep mattress order",
|
|
509
|
-
"Sleep schedule",
|
|
510
|
-
]);
|
|
511
|
-
});
|
|
461
|
+
test("from filter matches the email address as well as the display name", async () => {
|
|
462
|
+
const acct = uuid();
|
|
463
|
+
const mbx = uuid();
|
|
464
|
+
await seed(acct, mbx, [
|
|
465
|
+
{ fromName: "Jane Doe", fromEmail: "jane@acme.example", subject: "a" },
|
|
466
|
+
{ fromName: "Bob", fromEmail: "bob@other.example", subject: "b" },
|
|
467
|
+
]);
|
|
512
468
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const mbx = uuid();
|
|
516
|
-
await seed(acct, mbx, [
|
|
517
|
-
{ fromName: "Marketing", subject: "Weekly digest" },
|
|
518
|
-
{ subject: "Marketing update" },
|
|
519
|
-
]);
|
|
520
|
-
|
|
521
|
-
const res = await repo.searchByMailboxWindow(acct, mbx, {
|
|
522
|
-
subject: "marketing",
|
|
523
|
-
});
|
|
524
|
-
assert.deepEqual(subjectsOf(res), ["Marketing update"]);
|
|
469
|
+
const byDomain = await repo.searchByMailboxWindow(acct, mbx, {
|
|
470
|
+
from: "acme",
|
|
525
471
|
});
|
|
472
|
+
assert.deepEqual(subjectsOf(byDomain), ["a"], "matches the email domain");
|
|
473
|
+
});
|
|
526
474
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
475
|
+
test("multi-word query ANDs tokens across the subject and from fields", async () => {
|
|
476
|
+
const acct = uuid();
|
|
477
|
+
const mbx = uuid();
|
|
478
|
+
await seed(acct, mbx, [
|
|
479
|
+
{ fromName: "Emma Stone", subject: "Sleep schedule" },
|
|
480
|
+
{ fromName: "Emma Watson", subject: "Travel plans" },
|
|
481
|
+
{ subject: "Emma Sleep mattress order" },
|
|
482
|
+
]);
|
|
483
|
+
|
|
484
|
+
// "emma sleep": each token must appear in subject OR from — row 1 (Emma in
|
|
485
|
+
// from, Sleep in subject) and row 3 (both in subject) match; row 2 does not.
|
|
486
|
+
const res = await repo.searchByMailboxWindow(acct, mbx, {
|
|
487
|
+
query: "emma sleep",
|
|
540
488
|
});
|
|
489
|
+
assert.deepEqual(subjectsOf(res), [
|
|
490
|
+
"Emma Sleep mattress order",
|
|
491
|
+
"Sleep schedule",
|
|
492
|
+
]);
|
|
493
|
+
});
|
|
541
494
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
495
|
+
test("subject filter does not match text that only appears in the from fields", async () => {
|
|
496
|
+
const acct = uuid();
|
|
497
|
+
const mbx = uuid();
|
|
498
|
+
await seed(acct, mbx, [
|
|
499
|
+
{ fromName: "Marketing", subject: "Weekly digest" },
|
|
500
|
+
{ subject: "Marketing update" },
|
|
501
|
+
]);
|
|
549
502
|
|
|
550
|
-
|
|
551
|
-
|
|
503
|
+
const res = await repo.searchByMailboxWindow(acct, mbx, {
|
|
504
|
+
subject: "marketing",
|
|
552
505
|
});
|
|
506
|
+
assert.deepEqual(subjectsOf(res), ["Marketing update"]);
|
|
507
|
+
});
|
|
553
508
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
rows[39].subject = "needle in the haystack";
|
|
566
|
-
await seed(acct, mbx, rows);
|
|
567
|
-
|
|
568
|
-
const res = await repo.searchByMailboxWindow(
|
|
569
|
-
acct,
|
|
570
|
-
mbx,
|
|
571
|
-
{ query: "needle" },
|
|
572
|
-
{ limit: 5 },
|
|
573
|
-
);
|
|
574
|
-
assert.deepEqual(subjectsOf(res), ["needle in the haystack"]);
|
|
509
|
+
test("results stay scoped to the account and mailbox", async () => {
|
|
510
|
+
const acctA = uuid();
|
|
511
|
+
const acctB = uuid();
|
|
512
|
+
const mbx1 = uuid();
|
|
513
|
+
const mbx2 = uuid();
|
|
514
|
+
await seed(acctA, mbx1, [{ subject: "shared token here" }]);
|
|
515
|
+
await seed(acctB, mbx1, [{ subject: "shared token elsewhere" }]);
|
|
516
|
+
await seed(acctA, mbx2, [{ subject: "shared token other mailbox" }]);
|
|
517
|
+
|
|
518
|
+
const res = await repo.searchByMailboxWindow(acctA, mbx1, {
|
|
519
|
+
query: "shared",
|
|
575
520
|
});
|
|
576
|
-
|
|
577
|
-
);
|
|
521
|
+
assert.deepEqual(subjectsOf(res), ["shared token here"]);
|
|
522
|
+
});
|
|
578
523
|
|
|
579
|
-
|
|
524
|
+
test("LIKE metacharacters in the query match literally", async () => {
|
|
525
|
+
const acct = uuid();
|
|
526
|
+
const mbx = uuid();
|
|
527
|
+
await seed(acct, mbx, [
|
|
528
|
+
{ subject: "50% off today" },
|
|
529
|
+
{ subject: "50 percent off" },
|
|
530
|
+
]);
|
|
580
531
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
() => {
|
|
585
|
-
let repo: DrizzleThreadMessageRepository;
|
|
586
|
-
const cleanup: Array<() => Promise<void>> = [];
|
|
532
|
+
const res = await repo.searchByMailboxWindow(acct, mbx, { query: "50%" });
|
|
533
|
+
assert.deepEqual(subjectsOf(res), ["50% off today"]);
|
|
534
|
+
});
|
|
587
535
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
536
|
+
test("whole-mailbox recall: a match far behind the recent rows is found", async () => {
|
|
537
|
+
const acct = uuid();
|
|
538
|
+
const mbx = uuid();
|
|
539
|
+
const now = Date.now();
|
|
540
|
+
const rows = Array.from({ length: 40 }, (_, i) => ({
|
|
541
|
+
subject: i === 0 ? "needle in the haystack" : `filler ${i}`,
|
|
542
|
+
sentDate: now - i,
|
|
543
|
+
internalDate: now - i,
|
|
544
|
+
}));
|
|
545
|
+
// Put the only match at the OLDEST position.
|
|
546
|
+
rows[0].subject = "filler 0";
|
|
547
|
+
rows[39].subject = "needle in the haystack";
|
|
548
|
+
await seed(acct, mbx, rows);
|
|
549
|
+
|
|
550
|
+
const res = await repo.searchByMailboxWindow(
|
|
551
|
+
acct,
|
|
552
|
+
mbx,
|
|
553
|
+
{ query: "needle" },
|
|
554
|
+
{ limit: 5 },
|
|
555
|
+
);
|
|
556
|
+
assert.deepEqual(subjectsOf(res), ["needle in the haystack"]);
|
|
557
|
+
});
|
|
558
|
+
});
|
|
592
559
|
|
|
593
|
-
|
|
594
|
-
for (const fn of cleanup.reverse()) {
|
|
595
|
-
await fn();
|
|
596
|
-
}
|
|
597
|
-
await repo.close();
|
|
598
|
-
});
|
|
560
|
+
// ─── Smoke tests for the full interface ──────────────────────────────────────
|
|
599
561
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
);
|
|
606
|
-
cleanup.push(() => repo.delete(acct, created.threadMessageId));
|
|
607
|
-
assert.ok(created.threadMessageId);
|
|
608
|
-
assert.equal(created.subject, "hello");
|
|
562
|
+
describe("DrizzleThreadMessageRepository — core CRUD", {
|
|
563
|
+
skip: !RUN_INTEG,
|
|
564
|
+
}, () => {
|
|
565
|
+
let repo: DrizzleThreadMessageRepository;
|
|
566
|
+
const cleanup: Array<() => Promise<void>> = [];
|
|
609
567
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
568
|
+
before(async () => {
|
|
569
|
+
await setupDb();
|
|
570
|
+
repo = new DrizzleThreadMessageRepository(PG_URL);
|
|
571
|
+
});
|
|
613
572
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
cleanup.push(() => repo.delete(acct, created.threadMessageId));
|
|
573
|
+
after(async () => {
|
|
574
|
+
for (const fn of cleanup.reverse()) {
|
|
575
|
+
await fn();
|
|
576
|
+
}
|
|
577
|
+
await repo.close();
|
|
578
|
+
});
|
|
621
579
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
580
|
+
test("create and get round-trip", async () => {
|
|
581
|
+
const acct = uuid();
|
|
582
|
+
const mbx = uuid();
|
|
583
|
+
const created = await repo.create(
|
|
584
|
+
makeInput(acct, mbx, { subject: "hello" }),
|
|
585
|
+
);
|
|
586
|
+
cleanup.push(() => repo.delete(acct, created.threadMessageId));
|
|
587
|
+
assert.ok(created.threadMessageId);
|
|
588
|
+
assert.equal(created.subject, "hello");
|
|
589
|
+
|
|
590
|
+
const fetched = await repo.get(acct, created.threadMessageId);
|
|
591
|
+
assert.equal(fetched.threadMessageId, created.threadMessageId);
|
|
592
|
+
});
|
|
627
593
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
594
|
+
test("update persists", async () => {
|
|
595
|
+
const acct = uuid();
|
|
596
|
+
const mbx = uuid();
|
|
597
|
+
const created = await repo.create(
|
|
598
|
+
makeInput(acct, mbx, { subject: "original" }),
|
|
599
|
+
);
|
|
600
|
+
cleanup.push(() => repo.delete(acct, created.threadMessageId));
|
|
601
|
+
|
|
602
|
+
const updated = await repo.update(acct, created.threadMessageId, {
|
|
603
|
+
subject: "updated",
|
|
637
604
|
});
|
|
605
|
+
assert.equal(updated.subject, "updated");
|
|
606
|
+
});
|
|
638
607
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
);
|
|
650
|
-
cleanup.push(() => repo.delete(acct, a.threadMessageId));
|
|
651
|
-
const b = await repo.create(
|
|
652
|
-
makeInput(acct, mbx, {
|
|
653
|
-
sentDate: now,
|
|
654
|
-
internalDate: now,
|
|
655
|
-
subject: "newer",
|
|
656
|
-
}),
|
|
657
|
-
);
|
|
658
|
-
cleanup.push(() => repo.delete(acct, b.threadMessageId));
|
|
608
|
+
test("delete removes the row", async () => {
|
|
609
|
+
const acct = uuid();
|
|
610
|
+
const mbx = uuid();
|
|
611
|
+
const created = await repo.create(makeInput(acct, mbx));
|
|
612
|
+
await repo.delete(acct, created.threadMessageId);
|
|
613
|
+
await assert.rejects(
|
|
614
|
+
() => repo.get(acct, created.threadMessageId),
|
|
615
|
+
/not found/i,
|
|
616
|
+
);
|
|
617
|
+
});
|
|
659
618
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
619
|
+
test("listByMailbox returns desc-ordered rows for the mailbox", async () => {
|
|
620
|
+
const acct = uuid();
|
|
621
|
+
const mbx = uuid();
|
|
622
|
+
const now = Date.now();
|
|
623
|
+
const a = await repo.create(
|
|
624
|
+
makeInput(acct, mbx, {
|
|
625
|
+
sentDate: now - 1000,
|
|
626
|
+
internalDate: now - 1000,
|
|
627
|
+
subject: "older",
|
|
628
|
+
}),
|
|
629
|
+
);
|
|
630
|
+
cleanup.push(() => repo.delete(acct, a.threadMessageId));
|
|
631
|
+
const b = await repo.create(
|
|
632
|
+
makeInput(acct, mbx, {
|
|
633
|
+
sentDate: now,
|
|
634
|
+
internalDate: now,
|
|
635
|
+
subject: "newer",
|
|
636
|
+
}),
|
|
637
|
+
);
|
|
638
|
+
cleanup.push(() => repo.delete(acct, b.threadMessageId));
|
|
639
|
+
|
|
640
|
+
const result = await repo.listByMailbox(acct, mbx);
|
|
641
|
+
assert.equal(result.items.length, 2);
|
|
642
|
+
assert.equal(result.items[0].subject, "newer");
|
|
643
|
+
assert.equal(result.items[1].subject, "older");
|
|
644
|
+
});
|
|
665
645
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
646
|
+
test("get with array returns all matched rows", async () => {
|
|
647
|
+
const acct = uuid();
|
|
648
|
+
const mbx = uuid();
|
|
649
|
+
const a = await repo.create(makeInput(acct, mbx));
|
|
650
|
+
cleanup.push(() => repo.delete(acct, a.threadMessageId));
|
|
651
|
+
const b = await repo.create(makeInput(acct, mbx));
|
|
652
|
+
cleanup.push(() => repo.delete(acct, b.threadMessageId));
|
|
673
653
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
654
|
+
const rows = await repo.get(acct, [a.threadMessageId, b.threadMessageId]);
|
|
655
|
+
assert.equal(rows.length, 2);
|
|
656
|
+
});
|
|
677
657
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
658
|
+
// ── Tenant scoping on the id-only lookups (issue #1193) ────────────────────
|
|
659
|
+
// findByMessageId / findAllByMessageId / getByMessageId / countByThread carry
|
|
660
|
+
// accountConfigId in the WHERE clause, so a row created under account A must
|
|
661
|
+
// never resolve under account B's scope.
|
|
662
|
+
|
|
663
|
+
test("findByMessageId is scoped to accountConfigId", async () => {
|
|
664
|
+
const acct = uuid();
|
|
665
|
+
const other = uuid();
|
|
666
|
+
const mbx = uuid();
|
|
667
|
+
const messageId = uuid();
|
|
668
|
+
const created = await repo.create(makeInput(acct, mbx, { messageId }));
|
|
669
|
+
cleanup.push(() => repo.delete(acct, created.threadMessageId));
|
|
670
|
+
|
|
671
|
+
const own = await repo.findByMessageId(acct, messageId);
|
|
672
|
+
assert.ok(own);
|
|
673
|
+
assert.equal(own.threadMessageId, created.threadMessageId);
|
|
674
|
+
|
|
675
|
+
const foreign = await repo.findByMessageId(other, messageId);
|
|
676
|
+
assert.equal(foreign, null);
|
|
677
|
+
});
|
|
698
678
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
679
|
+
test("findAllByMessageId is scoped to accountConfigId", async () => {
|
|
680
|
+
const acct = uuid();
|
|
681
|
+
const other = uuid();
|
|
682
|
+
const messageId = uuid();
|
|
683
|
+
// Same messageId across two mailboxes of the same account.
|
|
684
|
+
const a = await repo.create(makeInput(acct, uuid(), { messageId }));
|
|
685
|
+
cleanup.push(() => repo.delete(acct, a.threadMessageId));
|
|
686
|
+
const b = await repo.create(makeInput(acct, uuid(), { messageId }));
|
|
687
|
+
cleanup.push(() => repo.delete(acct, b.threadMessageId));
|
|
688
|
+
|
|
689
|
+
const own = await repo.findAllByMessageId(acct, messageId);
|
|
690
|
+
assert.equal(own.length, 2);
|
|
691
|
+
|
|
692
|
+
const foreign = await repo.findAllByMessageId(other, messageId);
|
|
693
|
+
assert.equal(foreign.length, 0);
|
|
694
|
+
});
|
|
715
695
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
696
|
+
test("getByMessageId throws for another account's messageId", async () => {
|
|
697
|
+
const acct = uuid();
|
|
698
|
+
const other = uuid();
|
|
699
|
+
const mbx = uuid();
|
|
700
|
+
const messageId = uuid();
|
|
701
|
+
const created = await repo.create(makeInput(acct, mbx, { messageId }));
|
|
702
|
+
cleanup.push(() => repo.delete(acct, created.threadMessageId));
|
|
723
703
|
|
|
724
|
-
|
|
725
|
-
|
|
704
|
+
const own = await repo.getByMessageId(acct, messageId);
|
|
705
|
+
assert.equal(own.threadMessageId, created.threadMessageId);
|
|
726
706
|
|
|
727
|
-
|
|
728
|
-
|
|
707
|
+
await assert.rejects(() => repo.getByMessageId(other, messageId));
|
|
708
|
+
});
|
|
729
709
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
);
|
|
710
|
+
test("countByThread is scoped to accountConfigId", async () => {
|
|
711
|
+
const acct = uuid();
|
|
712
|
+
const other = uuid();
|
|
713
|
+
const mbx = uuid();
|
|
714
|
+
const threadId = uuid();
|
|
715
|
+
for (let i = 0; i < 3; i++) {
|
|
716
|
+
const row = await repo.create(makeInput(acct, mbx, { threadId }));
|
|
717
|
+
cleanup.push(() => repo.delete(acct, row.threadMessageId));
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
assert.equal(await repo.countByThread(acct, threadId), 3);
|
|
721
|
+
assert.equal(await repo.countByThread(other, threadId), 0);
|
|
722
|
+
});
|
|
723
|
+
});
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
IUnitOfWork,
|
|
3
|
-
UnitOfWorkRepositories,
|
|
4
|
-
} from "@remit/data-ports";
|
|
1
|
+
import type { IUnitOfWork, UnitOfWorkRepositories } from "@remit/data-ports";
|
|
5
2
|
import type { Db } from "../db.js";
|
|
6
3
|
import type { MessageDataSchema } from "../schema/message-data.js";
|
|
7
4
|
import { runInTransaction } from "../tx.js";
|
|
@@ -16,19 +16,13 @@ const hasCheckScript = existsSync(
|
|
|
16
16
|
// Fails when the committed VPS migrations (deploy/vps/migrations/*) no longer
|
|
17
17
|
// produce the schema drizzle would generate from the entity + auth schemas.
|
|
18
18
|
// See npm-scripts/check-vps-migrations.mjs for the mechanism.
|
|
19
|
-
test(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
() => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
stdio: "inherit",
|
|
30
|
-
},
|
|
31
|
-
);
|
|
32
|
-
}, "committed VPS migrations are stale — run `npm run migrations:generate`");
|
|
33
|
-
},
|
|
34
|
-
);
|
|
19
|
+
test("committed VPS migrations match the drizzle schema", {
|
|
20
|
+
skip: !hasCheckScript,
|
|
21
|
+
}, () => {
|
|
22
|
+
assert.doesNotThrow(() => {
|
|
23
|
+
execFileSync("node", ["npm-scripts/check-vps-migrations.mjs", "--check"], {
|
|
24
|
+
cwd: repoRoot,
|
|
25
|
+
stdio: "inherit",
|
|
26
|
+
});
|
|
27
|
+
}, "committed VPS migrations are stale — run `npm run migrations:generate`");
|
|
28
|
+
});
|