@remit/drizzle-service 0.0.10 → 0.0.11

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.10",
3
+ "version": "0.0.11",
4
4
  "description": "Drizzle ORM service parameterized by dialect (Postgres / SQLite)",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -209,6 +209,57 @@ describe("AddressRepo", () => {
209
209
  await repo.deleteManyAddresses(accountConfigId, created);
210
210
  });
211
211
 
212
+ test("listByAccountConfig resolves a search by exact email even when the sender has a display name", async () => {
213
+ const accountConfigId = randomId();
214
+ const created = await repo.createAddress({
215
+ ...makeAddressInput(accountConfigId, "support@npmjs.com"),
216
+ displayName: "npm support",
217
+ // How message-sync writes it: display name first, then the email.
218
+ normalizedCompound: "npm support support@npmjs.com",
219
+ });
220
+
221
+ const byEmail = await repo.listByAccountConfig({
222
+ accountConfigId,
223
+ search: "support@npmjs.com",
224
+ });
225
+ assert.deepEqual(
226
+ byEmail.items.map((a) => a.addressId),
227
+ [created.addressId],
228
+ "an exact-address lookup must resolve the row",
229
+ );
230
+
231
+ const byDisplayName = await repo.listByAccountConfig({
232
+ accountConfigId,
233
+ search: "npm",
234
+ });
235
+ assert.deepEqual(
236
+ byDisplayName.items.map((a) => a.addressId),
237
+ [created.addressId],
238
+ "display-name search keeps working",
239
+ );
240
+
241
+ await repo.deleteManyAddresses(accountConfigId, [created.addressId]);
242
+ });
243
+
244
+ test("listByAccountConfig treats LIKE metacharacters in a search term literally", async () => {
245
+ const accountConfigId = randomId();
246
+ const plain = await repo.createAddress(
247
+ makeAddressInput(accountConfigId, "ab@x.com"),
248
+ );
249
+
250
+ const result = await repo.listByAccountConfig({
251
+ accountConfigId,
252
+ search: "a_@x.com",
253
+ });
254
+ assert.equal(
255
+ result.items.length,
256
+ 0,
257
+ "`_` must not act as a single-character wildcard",
258
+ );
259
+
260
+ await repo.deleteManyAddresses(accountConfigId, [plain.addressId]);
261
+ });
262
+
212
263
  test("cross-tenant: getAddress refuses a foreign accountConfig", async () => {
213
264
  const addr = await repo.createAddress(makeAddressInput(randomId()));
214
265
  const other = randomId();
@@ -20,6 +20,31 @@ import { shouldPromoteWellknown } from "./i4-address-wellknown.js";
20
20
 
21
21
  type DB = Db<Record<string, unknown>>;
22
22
 
23
+ /**
24
+ * Escape the LIKE metacharacters so a term containing `%` or `_` (both legal in
25
+ * an email local part) matches literally instead of as a wildcard.
26
+ */
27
+ const escapeLikeTerm = (term: string): string =>
28
+ term.replace(/[\\%_]/g, (char) => `\\${char}`);
29
+
30
+ /**
31
+ * Match an address search term as a prefix of either the display-name compound
32
+ * or the normalized email.
33
+ *
34
+ * `normalizedCompound` is stored as `"<display name> <email>"`, so a prefix
35
+ * match on it only ever answers display-name queries — an exact-address lookup
36
+ * such as `support@npmjs.com` can never match a row whose sender has a display
37
+ * name. Matching `normalizedEmail` in the same predicate is what makes address
38
+ * resolution by email work at all (issue #51).
39
+ */
40
+ const addressSearchPredicate = (term: string) => {
41
+ const pattern = `${escapeLikeTerm(term)}%`;
42
+ return or(
43
+ sql`${addressTable.normalizedCompound} LIKE ${pattern} ESCAPE '\\'`,
44
+ sql`${addressTable.normalizedEmail} LIKE ${pattern} ESCAPE '\\'`,
45
+ );
46
+ };
47
+
23
48
  export function rowToAddress(
24
49
  row: typeof addressTable.$inferSelect,
25
50
  ): AddressItem {
@@ -454,11 +479,11 @@ export class AddressRepo implements IAddressRepository {
454
479
 
455
480
  async listByAccountConfig(input: {
456
481
  accountConfigId: string;
457
- normalizedCompound?: string;
482
+ search?: string;
458
483
  cursor?: string;
459
484
  limit?: number;
460
485
  }): Promise<ResultList<AddressItem>> {
461
- const { accountConfigId, normalizedCompound, cursor, limit = 100 } = input;
486
+ const { accountConfigId, search, cursor, limit = 100 } = input;
462
487
  const decoded = cursor ? decodeToken(cursor) : undefined;
463
488
  const after = decoded
464
489
  ? {
@@ -473,9 +498,7 @@ export class AddressRepo implements IAddressRepository {
473
498
  .where(
474
499
  and(
475
500
  eq(addressTable.accountConfigId, accountConfigId),
476
- normalizedCompound
477
- ? sql`${addressTable.normalizedCompound} LIKE ${`${normalizedCompound}%`}`
478
- : undefined,
501
+ search ? addressSearchPredicate(search) : undefined,
479
502
  after
480
503
  ? or(
481
504
  gt(addressTable.normalizedCompound, after.normalizedCompound),