@remit/drizzle-service 0.0.56 → 0.0.57

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.56",
3
+ "version": "0.0.57",
4
4
  "description": "Drizzle ORM service over SQLite",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -1,4 +1,4 @@
1
- import { or, type SQL, sql } from "drizzle-orm";
1
+ import { eq, or, type SQL, sql } from "drizzle-orm";
2
2
  import { addressTable } from "../schema/i4-address.js";
3
3
 
4
4
  // The address search seam. A term is matched as a substring of the display name,
@@ -49,10 +49,12 @@ export const addressSearchMatch = (term: string): SQL => {
49
49
  * spells out ranks above every prefix of it: a display name is free text the
50
50
  * sender picks, and a domain somebody else registered is a prefix away from the
51
51
  * address the reader typed, so neither may take the suggestion slot from the
52
- * address that matches whole. Below that, a match at the start of a column
53
- * outranks one in the middle, and within each the whole address, the local part
54
- * and the display name outrank the domain they share. A mid-string match still
55
- * comes back this only decides the order.
52
+ * address that matches whole. A term that spells out a domain whole ranks next,
53
+ * so `ischen.nl` reaches the domain it names before `ischen.nl.co`, which only
54
+ * starts with it (#829). Below that, a match at the start of a column outranks
55
+ * one in the middle, and within each the whole address, the local part and the
56
+ * display name outrank the domain they share. A mid-string match still comes
57
+ * back — this only decides the order.
56
58
  */
57
59
  export const addressMatchRank = (term: string | undefined): SQL<number> => {
58
60
  // Not the bare literal `0`: SQLite reads an integer literal in ORDER BY as a
@@ -60,7 +62,10 @@ export const addressMatchRank = (term: string | undefined): SQL<number> => {
60
62
  if (!term) return sql<number>`cast(0 as integer)`;
61
63
  const { leading, anywhere } = patterns(term);
62
64
  const tiers = [
63
- sql`${addressTable.normalizedEmail} = ${term.toLowerCase()}`,
65
+ eq(addressTable.normalizedEmail, term.toLowerCase()),
66
+ // `domain` is stored as the envelope spelled it, so `Ischen.NL` only meets
67
+ // a folded term through `lower()`; `like` folds ASCII on its own.
68
+ sql`lower(${addressTable.domain}) = ${term.toLowerCase()}`,
64
69
  ...SEARCH_COLUMNS.map((column) => like(column, leading)),
65
70
  ...SEARCH_COLUMNS.map((column) => like(column, anywhere)),
66
71
  ];
@@ -474,6 +474,85 @@ describe("AddressRepo", () => {
474
474
  ]);
475
475
  });
476
476
 
477
+ test("listByAccountConfig ranks the domain a bare term names above one that only starts with it", async () => {
478
+ const accountConfigId = randomId();
479
+ const lookalike = await repo.createAddress({
480
+ ...makeAddressInput(accountConfigId, "matthijs@ischen.nl.co"),
481
+ displayName: "Ischen Support",
482
+ normalizedCompound: "ischen support matthijs@ischen.nl.co",
483
+ inboundCount: 900,
484
+ });
485
+ // The envelope's own spelling: `domain` is stored unfolded.
486
+ const own = await repo.createAddress({
487
+ ...makeAddressInput(accountConfigId, "matthijs@ischen.nl"),
488
+ domain: "Ischen.NL",
489
+ displayName: "Matthijs van Henten",
490
+ normalizedCompound: "matthijs van henten matthijs@ischen.nl",
491
+ inboundCount: 1,
492
+ });
493
+
494
+ const found = await repo.listByAccountConfig({
495
+ accountConfigId,
496
+ search: "ischen.nl",
497
+ });
498
+ assert.deepEqual(
499
+ found.items.map((a) => a.normalizedEmail),
500
+ [own.normalizedEmail, lookalike.normalizedEmail],
501
+ "the domain the term names leads one that registered a suffix of it",
502
+ );
503
+
504
+ const onlySuggestion = await repo.listByAccountConfig({
505
+ accountConfigId,
506
+ search: "ischen.nl",
507
+ limit: 1,
508
+ });
509
+ assert.deepEqual(
510
+ onlySuggestion.items.map((a) => a.normalizedEmail),
511
+ [own.normalizedEmail],
512
+ "volume on the lookalike must not take the single suggestion slot",
513
+ );
514
+
515
+ await repo.deleteManyAddresses(accountConfigId, [
516
+ lookalike.addressId,
517
+ own.addressId,
518
+ ]);
519
+ });
520
+
521
+ test("listByAccountConfig keeps an address the term spells out whole above a row that only owns the domain", async () => {
522
+ const accountConfigId = randomId();
523
+ // A harvested header that carried the bare domain where an address belongs.
524
+ const bare = await repo.createAddress({
525
+ addressId: randomId(),
526
+ accountConfigId,
527
+ localPart: "ischen.nl",
528
+ domain: "",
529
+ normalizedEmail: "ischen.nl",
530
+ normalizedCompound: "ischen.nl",
531
+ displayName: "Ischen",
532
+ });
533
+ const domainOwner = await repo.createAddress({
534
+ ...makeAddressInput(accountConfigId, "matthijs@ischen.nl"),
535
+ displayName: "Matthijs van Henten",
536
+ normalizedCompound: "matthijs van henten matthijs@ischen.nl",
537
+ inboundCount: 900,
538
+ });
539
+
540
+ const found = await repo.listByAccountConfig({
541
+ accountConfigId,
542
+ search: "ischen.nl",
543
+ });
544
+ assert.deepEqual(
545
+ found.items.map((a) => a.normalizedEmail),
546
+ [bare.normalizedEmail, domainOwner.normalizedEmail],
547
+ "the address that matches whole stays above the domain tier below it",
548
+ );
549
+
550
+ await repo.deleteManyAddresses(accountConfigId, [
551
+ bare.addressId,
552
+ domainOwner.addressId,
553
+ ]);
554
+ });
555
+
477
556
  test("listByAccountConfig keeps a leading display-name match above a domain that only contains the term", async () => {
478
557
  const accountConfigId = randomId();
479
558
  const shop = await repo.createAddress({