Skip to content

Commit f2951c4

Browse files
feat: relations embed api implementation (#7)
1 parent da2051a commit f2951c4

30 files changed

Lines changed: 6426 additions & 77 deletions

β€Žpackages/mizzle-orm/docs/README.mdβ€Ž

Lines changed: 433 additions & 0 deletions
Large diffs are not rendered by default.

β€Žpackages/mizzle-orm/docs/embeds-guide.mdβ€Ž

Lines changed: 745 additions & 0 deletions
Large diffs are not rendered by default.

β€Žpackages/mizzle-orm/docs/embeds.mdβ€Ž

Lines changed: 915 additions & 0 deletions
Large diffs are not rendered by default.

β€Žpackages/mizzle-orm/docs/lookup-to-embed-migration.mdβ€Ž

Lines changed: 523 additions & 0 deletions
Large diffs are not rendered by default.

β€Žpackages/mizzle-orm/examples/blog-with-embeds.tsβ€Ž

Lines changed: 415 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 398 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,398 @@
1+
/**
2+
* E-Commerce Order System with Historical Embeds
3+
*
4+
* This example demonstrates using embeds for order history where
5+
* you want to preserve the EXACT state of products at purchase time.
6+
*
7+
* Key Features:
8+
* - Product snapshots in order items (NO auto-update)
9+
* - Customer info in orders (auto-updates for contact changes)
10+
* - Shipping address snapshots
11+
*
12+
* Run with: tsx examples/ecommerce-orders.ts
13+
*/
14+
15+
import {
16+
createMongoOrm,
17+
mongoCollection,
18+
embed,
19+
objectId,
20+
string,
21+
number,
22+
date,
23+
array,
24+
object,
25+
publicId,
26+
} from '../src/index';
27+
28+
// =============================================================================
29+
// Collections
30+
// =============================================================================
31+
32+
/**
33+
* Products Collection
34+
* Current product catalog (prices and details can change)
35+
*/
36+
const products = mongoCollection('products', {
37+
_id: objectId().internalId(),
38+
sku: string(),
39+
name: string(),
40+
description: string(),
41+
price: number(), // Current price (may change over time)
42+
category: string(),
43+
inStock: number().int(),
44+
imageUrl: string().url().optional(),
45+
updatedAt: date().onUpdateNow(),
46+
});
47+
48+
/**
49+
* Customers Collection
50+
* Customer profiles
51+
*/
52+
const customers = mongoCollection('customers', {
53+
id: publicId('cus'),
54+
email: string().email(),
55+
name: string(),
56+
phone: string().optional(),
57+
createdAt: date().defaultNow(),
58+
});
59+
60+
/**
61+
* Orders Collection
62+
* Order headers with customer snapshot
63+
*/
64+
const orders = mongoCollection(
65+
'orders',
66+
{
67+
_id: objectId().internalId(),
68+
orderNumber: string(),
69+
customerId: string(), // Customer publicId
70+
71+
// Shipping info (snapshot at order time)
72+
shippingAddress: object({
73+
street: string(),
74+
city: string(),
75+
state: string(),
76+
zipCode: string(),
77+
country: string(),
78+
}),
79+
80+
// Order details
81+
status: string(), // 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled'
82+
subtotal: number(),
83+
tax: number(),
84+
shipping: number(),
85+
total: number(),
86+
87+
placedAt: date().defaultNow(),
88+
updatedAt: date().onUpdateNow(),
89+
},
90+
{
91+
relations: {
92+
// EMBED: Customer contact info (auto-updates for email/phone changes)
93+
customer: embed(customers, {
94+
forward: {
95+
from: 'customerId',
96+
fields: ['name', 'email', 'phone'],
97+
embedIdField: 'id',
98+
},
99+
reverse: {
100+
enabled: true,
101+
strategy: 'async',
102+
// Update customer contact, but NOT if they change their name
103+
// (orders should preserve customer name at time of purchase)
104+
watchFields: ['email', 'phone'],
105+
},
106+
}),
107+
},
108+
}
109+
);
110+
111+
/**
112+
* Order Items Collection
113+
* Individual items in an order with product SNAPSHOT
114+
*/
115+
const orderItems = mongoCollection(
116+
'order_items',
117+
{
118+
_id: objectId().internalId(),
119+
orderId: objectId(),
120+
productId: objectId(),
121+
122+
// Purchase details
123+
quantity: number().int(),
124+
unitPrice: number(), // Price at time of purchase
125+
total: number(),
126+
127+
createdAt: date().defaultNow(),
128+
},
129+
{
130+
relations: {
131+
// EMBED: Product snapshot (NO auto-update - preserve historical data)
132+
product: embed(products, {
133+
forward: {
134+
from: 'productId',
135+
fields: ['sku', 'name', 'description', 'category', 'imageUrl'],
136+
},
137+
// NO reverse config - we want the snapshot at purchase time!
138+
}),
139+
140+
// EMBED: Order info (snapshot)
141+
order: embed(orders, {
142+
forward: {
143+
from: 'orderId',
144+
fields: ['orderNumber', 'status'],
145+
},
146+
// NO auto-update - preserve order state at item creation
147+
}),
148+
},
149+
}
150+
);
151+
152+
// =============================================================================
153+
// ORM Setup
154+
// =============================================================================
155+
156+
const orm = await createMongoOrm({
157+
uri: process.env.MONGO_URI || 'mongodb://localhost:27017',
158+
dbName: 'ecommerce_example',
159+
collections: {
160+
products,
161+
customers,
162+
orders,
163+
orderItems,
164+
},
165+
});
166+
167+
const ctx = orm.createContext({});
168+
const db = orm.withContext(ctx);
169+
170+
// =============================================================================
171+
// Example Usage
172+
// =============================================================================
173+
174+
async function main() {
175+
console.log('πŸ›’ E-Commerce Order System Example\n');
176+
177+
// ---------------------------------------------------------------------------
178+
// 1. Create products
179+
// ---------------------------------------------------------------------------
180+
console.log('πŸ“¦ Creating products...');
181+
182+
const laptop = await db.products.create({
183+
sku: 'LAPTOP-001',
184+
name: 'UltraBook Pro 15"',
185+
description: 'Powerful laptop for professionals',
186+
price: 1299.99,
187+
category: 'Electronics',
188+
inStock: 50,
189+
imageUrl: 'https://example.com/laptop.jpg',
190+
});
191+
192+
const mouse = await db.products.create({
193+
sku: 'MOUSE-001',
194+
name: 'Wireless Mouse',
195+
description: 'Ergonomic wireless mouse',
196+
price: 29.99,
197+
category: 'Accessories',
198+
inStock: 200,
199+
});
200+
201+
console.log(`βœ… Created ${laptop.name} - $${laptop.price}`);
202+
console.log(`βœ… Created ${mouse.name} - $${mouse.price}\n`);
203+
204+
// ---------------------------------------------------------------------------
205+
// 2. Create customer
206+
// ---------------------------------------------------------------------------
207+
console.log('πŸ‘€ Creating customer...');
208+
209+
const customer = await db.customers.create({
210+
email: 'john@example.com',
211+
name: 'John Doe',
212+
phone: '+1-555-0123',
213+
});
214+
215+
console.log(`βœ… Created customer: ${customer.name} (${customer.id})\n`);
216+
217+
// ---------------------------------------------------------------------------
218+
// 3. Place order
219+
// ---------------------------------------------------------------------------
220+
console.log('πŸ›οΈ Placing order...');
221+
222+
const subtotal = laptop.price + mouse.price;
223+
const tax = subtotal * 0.08; // 8% tax
224+
const shipping = 15.00;
225+
const total = subtotal + tax + shipping;
226+
227+
const order = await db.orders.create({
228+
orderNumber: `ORD-${Date.now()}`,
229+
customerId: customer.id,
230+
shippingAddress: {
231+
street: '123 Main St',
232+
city: 'San Francisco',
233+
state: 'CA',
234+
zipCode: '94102',
235+
country: 'USA',
236+
},
237+
status: 'pending',
238+
subtotal,
239+
tax,
240+
shipping,
241+
total,
242+
});
243+
244+
console.log(`βœ… Created order ${order.orderNumber}`);
245+
console.log(` Customer: ${order.customer?.name} (${order.customer?.email})`);
246+
console.log(` Total: $${order.total.toFixed(2)}\n`);
247+
248+
// ---------------------------------------------------------------------------
249+
// 4. Add order items with product snapshots
250+
// ---------------------------------------------------------------------------
251+
console.log('πŸ“ Adding order items...');
252+
253+
const item1 = await db.orderItems.create({
254+
orderId: order._id,
255+
productId: laptop._id,
256+
quantity: 1,
257+
unitPrice: laptop.price,
258+
total: laptop.price,
259+
});
260+
261+
const item2 = await db.orderItems.create({
262+
orderId: order._id,
263+
productId: mouse._id,
264+
quantity: 2,
265+
unitPrice: mouse.price,
266+
total: mouse.price * 2,
267+
});
268+
269+
console.log(`βœ… Added item: ${item1.product?.name} x${item1.quantity} @ $${item1.unitPrice}`);
270+
console.log(` Product snapshot:`, item1.product);
271+
console.log(`βœ… Added item: ${item2.product?.name} x${item2.quantity} @ $${item2.unitPrice}`);
272+
console.log(` Product snapshot:`, item2.product);
273+
console.log();
274+
275+
// ---------------------------------------------------------------------------
276+
// 5. Update product price (should NOT affect order)
277+
// ---------------------------------------------------------------------------
278+
console.log('πŸ’° Updating product price...');
279+
280+
const oldPrice = laptop.price;
281+
const newPrice = 1399.99;
282+
283+
await db.products.updateById(laptop._id, {
284+
price: newPrice,
285+
});
286+
287+
console.log(`βœ… Updated laptop price: $${oldPrice} β†’ $${newPrice}`);
288+
289+
// Check order item - should still have OLD price (historical snapshot)
290+
const itemAfterPriceChange = await db.orderItems.findById(item1._id);
291+
console.log(` Order item still shows: $${itemAfterPriceChange?.unitPrice}`);
292+
console.log(` Product name: "${itemAfterPriceChange?.product?.name}"`);
293+
console.log(` βœ… Historical snapshot preserved!\n`);
294+
295+
// ---------------------------------------------------------------------------
296+
// 6. Update customer email (should update order)
297+
// ---------------------------------------------------------------------------
298+
console.log('πŸ“§ Updating customer email...');
299+
300+
await db.customers.updateOne(
301+
{ id: customer.id },
302+
{ email: 'john.doe@newmail.com' }
303+
);
304+
305+
console.log(`βœ… Updated customer email`);
306+
307+
// Check order - should have NEW email (auto-updated)
308+
const orderAfterEmailChange = await db.orders.findById(order._id);
309+
console.log(` Order now shows: ${orderAfterEmailChange?.customer?.email}`);
310+
console.log(` βœ… Contact info auto-updated!\n`);
311+
312+
// ---------------------------------------------------------------------------
313+
// 7. Update customer name (should NOT update order)
314+
// ---------------------------------------------------------------------------
315+
console.log('πŸ‘€ Updating customer name...');
316+
317+
await db.customers.updateOne(
318+
{ id: customer.id },
319+
{ name: 'Jonathan Doe' }
320+
);
321+
322+
console.log(`βœ… Updated customer name to "Jonathan Doe"`);
323+
324+
// Check order - should still have OLD name (not in watchFields)
325+
const orderAfterNameChange = await db.orders.findById(order._id);
326+
console.log(` Order still shows: ${orderAfterNameChange?.customer?.name}`);
327+
console.log(` βœ… Historical name preserved (not in watchFields)!\n`);
328+
329+
// ---------------------------------------------------------------------------
330+
// 8. Display complete order
331+
// ---------------------------------------------------------------------------
332+
console.log('πŸ“‹ Complete Order Details:\n');
333+
334+
const fullOrder = await db.orders.findById(order._id);
335+
const items = await db.orderItems.findMany({ orderId: order._id });
336+
337+
console.log(` Order: ${fullOrder?.orderNumber}`);
338+
console.log(` Status: ${fullOrder?.status}`);
339+
console.log(` Customer: ${fullOrder?.customer?.name}`);
340+
console.log(` Email: ${fullOrder?.customer?.email}`);
341+
console.log(` Phone: ${fullOrder?.customer?.phone}`);
342+
console.log();
343+
console.log(` Shipping Address:`);
344+
console.log(` ${fullOrder?.shippingAddress.street}`);
345+
console.log(` ${fullOrder?.shippingAddress.city}, ${fullOrder?.shippingAddress.state} ${fullOrder?.shippingAddress.zipCode}`);
346+
console.log();
347+
console.log(` Items:`);
348+
349+
for (const item of items) {
350+
console.log(` β€’ ${item.product?.name} x${item.quantity}`);
351+
console.log(` SKU: ${item.product?.sku}`);
352+
console.log(` Price: $${item.unitPrice.toFixed(2)} each`);
353+
console.log(` Total: $${item.total.toFixed(2)}`);
354+
}
355+
356+
console.log();
357+
console.log(` Subtotal: $${fullOrder?.subtotal.toFixed(2)}`);
358+
console.log(` Tax: $${fullOrder?.tax.toFixed(2)}`);
359+
console.log(` Shipping: $${fullOrder?.shipping.toFixed(2)}`);
360+
console.log(` ─────────────────────`);
361+
console.log(` Total: $${fullOrder?.total.toFixed(2)}\n`);
362+
363+
// ---------------------------------------------------------------------------
364+
// 9. Show current vs historical prices
365+
// ---------------------------------------------------------------------------
366+
console.log('πŸ’‘ Price Comparison:\n');
367+
368+
const currentLaptop = await db.products.findById(laptop._id);
369+
370+
console.log(` ${laptop.name}:`);
371+
console.log(` Current price: $${currentLaptop?.price.toFixed(2)}`);
372+
console.log(` Price at purchase: $${item1.unitPrice.toFixed(2)}`);
373+
console.log(` Difference: $${(currentLaptop!.price - item1.unitPrice).toFixed(2)}`);
374+
console.log();
375+
376+
// ---------------------------------------------------------------------------
377+
// Summary
378+
// ---------------------------------------------------------------------------
379+
console.log('✨ E-Commerce Embed Summary:');
380+
console.log(' βœ… Product snapshots: Preserve exact purchase-time details');
381+
console.log(' βœ… Price history: Orders unaffected by price changes');
382+
console.log(' βœ… Customer contact: Auto-updates for email/phone');
383+
console.log(' βœ… Customer name: Preserved at order time (historical)');
384+
console.log(' βœ… Perfect audit trail: See exactly what was ordered');
385+
console.log();
386+
console.log('πŸŽ‰ Example complete!');
387+
}
388+
389+
// Run example
390+
main()
391+
.then(async () => {
392+
await orm.close();
393+
process.exit(0);
394+
})
395+
.catch((error) => {
396+
console.error('❌ Error:', error);
397+
process.exit(1);
398+
});

0 commit comments

Comments
Β (0)