Braid
Composite and derived keys
Why keys must be primitives, and how to build a composite key when one field isn't enough to match on.
Why not object keys
BraidKey is restricted to primitives:
export type BraidKey = string | number | bigint | boolean | symbol | null | undefined;
A Map can technically key on anything, including an object, so Braid could
have allowed key: (row) => row.someObject. It doesn’t, on purpose: object
keys match by reference identity, and the moment the main side and the
detail side come from two different fetches, two structurally identical
objects are never === to each other. Every row would silently take its
default, which is exactly the class of bug this library exists to prevent.
Derive a primitive key instead.
Building a composite key
When no single field uniquely identifies a row (a per-store SKU, a composite of warehouse and bin, a date plus a category), derive one with a template literal in the key extractor:
new Braid()
.main({
source: storeProducts,
key: (row) => `${row.storeId}:${row.sku}`,
})
.join({
name: "inventory",
source: inventoryRows,
on: (inventory) => `${inventory.storeId}:${inventory.sku}`,
type: "single",
});
Both extractors are plain functions, so there’s no special composite-key API
to learn: this is the same key / on mechanism used everywhere else,
just producing a string that happens to encode more than one field.
A few things worth being careful about with derived keys:
- Use a separator that can’t appear inside either field, or a genuinely
different id (
"1"+"23"and"12"+"3"both produce"1:23"and"12:3"respectively if the fields could contain digits adjacent to the separator; pick a separator, or a fixed-width encoding, that rules this out for your data). - Normalize before combining if either field’s representation might differ between the main and detail side. See Key equality and nullish keys for the quoted-vs-unquoted id problem this causes.
- A composite key is still just one
BraidKey. Braid doesn’t have a multi-field join mode; the template literal is the join key as far as the index and the lookup are concerned.
Derived, non-composite keys
The same mechanism works for any derived key, not only composite ones: normalizing case, trimming whitespace, or converting a type are all just transformations inside the extractor function:
.join({
name: "dbProduct",
source: dbProducts,
on: (p) => String(p.id),
key: (row) => String(row.productId),
type: "single",
})