Braid

Defaults and required joins

What a join falls back to when nothing matches, and how required turns a miss into a thrown error.

default

default is the value used when a main row’s key finds no match. It defaults to null for single joins and a fresh [] for many joins. You don’t need to pass it just to get ordinary null-safety.

new Braid()
	.main({ source: products, key: (p) => p.sku })
	.join({
		name: "bigcommerce",
		source: bcProducts,
		on: (bc) => bc.sku,
		type: "single",
		default: { sku: "", bcId: -1, price: 0 },
	})
	.run();

Passing an explicit default widens the join’s type to include it: row.bigcommerce becomes BcProduct | { sku: string; bcId: number; price: number } (which TypeScript collapses to BcProduct here since the shapes match, but a differently-shaped default, like a string sentinel, shows up as its own member of the union). See Type safety for how that’s tracked.

Sharing vs. fresh values

For many joins specifically, the default you get without passing one matters: when you don’t supply a default, each unmatched row gets its own empty array, so mutating one row’s result can’t affect another:

const rows = new Braid()
	.main({ source: products, key: (p) => p.sku })
	.join({
		name: "variations",
		source: [],
		on: (v) => v.productId,
		key: (p) => p.id,
		type: "many",
	})
	.run();

rows[0].variations.push({ productId: 1, option: "added", stock: 1 });
rows[1].variations.length; // 0 — unaffected

When you do supply a default, that exact value is reused for every miss: every unmatched row gets a reference to the same array or object. Share it only if you mean to; if callers might mutate the result, pass a fresh value per call or freeze it.

required

required: true turns a miss into a thrown BraidError instead of falling back to the default. Use it where a missing match means the data is wrong, not where a missing match is an expected, handleable case:

new Braid()
	.main({ name: "order", source: orders, key: (order) => order.reference })
	.join({
		name: "customer",
		source: customers,
		on: (customer) => customer.id,
		key: (order) => order.customerId,
		type: "single",
		// Every order must belong to a customer; a miss here is a data bug worth
		// failing on rather than a null to handle downstream.
		required: true,
	});

The thrown error names the join and the offending key, which is usually enough to find the row on its own:

Join "customer" is required, but no match was found for order row with key 7.

(If .main() didn’t set a name, the message says "main row" instead of naming the collection.)

required also changes the join’s type: it drops the default from the union, so a required single join is typed as D rather than D | null, and a required many join is typed as D[] with no alternate default, because a required join either matched or the call already threw. See Type safety.

required and async sources

required throws through the async path the same way: a rejected .run() promise, not a differently-shaped error:

try {
	await new Braid()
		.main({ name: "order", source: orders, key: (order) => order.reference })
		.join({
			name: "shipments",
			source: fetchShipments,
			on: (shipment) => shipment.orderReference,
			type: "many",
			required: true,
		});
} catch (error) {
	if (!(error instanceof BraidError)) throw error;
	console.log(`Caught as expected: ${error.message}`);
}