@fjell/express-router 4.4.55 ā 4.4.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/dist/util/general.js +1 -1
- package/dist/util/general.js.map +2 -2
- package/package.json +11 -11
- package/MIGRATION_v3.md +0 -255
- package/build.js +0 -4
- package/docs/docs.config.ts +0 -44
- package/docs/index.html +0 -18
- package/docs/package.json +0 -34
- package/docs/public/README.md +0 -332
- package/docs/public/examples-README.md +0 -339
- package/docs/public/fjell-icon.svg +0 -1
- package/docs/public/pano.png +0 -0
- package/docs/tsconfig.node.json +0 -6
- package/examples/README.md +0 -386
- package/examples/basic-router-example.ts +0 -391
- package/examples/full-application-example.ts +0 -768
- package/examples/nested-router-example.ts +0 -601
- package/examples/router-handlers-example.ts +0 -394
- package/examples/router-options-example.ts +0 -214
- package/vitest.config.ts +0 -39
|
@@ -1,768 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
2
|
-
/**
|
|
3
|
-
* Full Application Example
|
|
4
|
-
*
|
|
5
|
-
* This example demonstrates a complete Express.js application built with fjell-express-router,
|
|
6
|
-
* showcasing a realistic e-commerce-like system with multiple interconnected entities,
|
|
7
|
-
* business logic, middleware, error handling, and advanced routing patterns.
|
|
8
|
-
*
|
|
9
|
-
* Perfect for understanding how to build production-ready applications with fjell-express-router.
|
|
10
|
-
*
|
|
11
|
-
* Run this example with: npx tsx examples/full-application-example.ts
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import express, { Application, NextFunction, Request, Response } from 'express';
|
|
15
|
-
import { ComKey, Item, LocKey, PriKey, UUID } from '@fjell/core';
|
|
16
|
-
import { CItemRouter, createRegistry, PItemRouter } from '../src';
|
|
17
|
-
|
|
18
|
-
// ===== Data Models =====
|
|
19
|
-
|
|
20
|
-
interface Customer extends Item<'customer'> {
|
|
21
|
-
id: string;
|
|
22
|
-
name: string;
|
|
23
|
-
email: string;
|
|
24
|
-
phone: string;
|
|
25
|
-
address: {
|
|
26
|
-
street: string;
|
|
27
|
-
city: string;
|
|
28
|
-
state: string;
|
|
29
|
-
zipCode: string;
|
|
30
|
-
};
|
|
31
|
-
registrationDate: Date;
|
|
32
|
-
tier: 'bronze' | 'silver' | 'gold' | 'platinum';
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface Product extends Item<'product'> {
|
|
36
|
-
id: string;
|
|
37
|
-
name: string;
|
|
38
|
-
description: string;
|
|
39
|
-
price: number;
|
|
40
|
-
category: string;
|
|
41
|
-
inStock: number;
|
|
42
|
-
sku: string;
|
|
43
|
-
featured: boolean;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
interface Order extends Item<'order', 'customer'> {
|
|
47
|
-
id: string;
|
|
48
|
-
customerId: string;
|
|
49
|
-
orderDate: Date;
|
|
50
|
-
status: 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled';
|
|
51
|
-
total: number;
|
|
52
|
-
shippingAddress: {
|
|
53
|
-
street: string;
|
|
54
|
-
city: string;
|
|
55
|
-
state: string;
|
|
56
|
-
zipCode: string;
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
interface OrderItem extends Item<'orderItem', 'customer', 'order'> {
|
|
61
|
-
id: string;
|
|
62
|
-
orderId: string;
|
|
63
|
-
productId: string;
|
|
64
|
-
quantity: number;
|
|
65
|
-
unitPrice: number;
|
|
66
|
-
totalPrice: number;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
interface Review extends Item<'review', 'customer', 'product'> {
|
|
70
|
-
id: string;
|
|
71
|
-
customerId: string;
|
|
72
|
-
productId: string;
|
|
73
|
-
rating: number;
|
|
74
|
-
title: string;
|
|
75
|
-
content: string;
|
|
76
|
-
reviewDate: Date;
|
|
77
|
-
verified: boolean;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// ===== Mock Storage =====
|
|
81
|
-
const mockCustomerStorage = new Map<string, Customer>();
|
|
82
|
-
const mockProductStorage = new Map<string, Product>();
|
|
83
|
-
const mockOrderStorage = new Map<string, Order>();
|
|
84
|
-
const mockOrderItemStorage = new Map<string, OrderItem>();
|
|
85
|
-
const mockReviewStorage = new Map<string, Review>();
|
|
86
|
-
|
|
87
|
-
// ===== Sample Data Initialization =====
|
|
88
|
-
const initializeSampleData = () => {
|
|
89
|
-
// Clear existing data
|
|
90
|
-
mockCustomerStorage.clear();
|
|
91
|
-
mockProductStorage.clear();
|
|
92
|
-
mockOrderStorage.clear();
|
|
93
|
-
mockOrderItemStorage.clear();
|
|
94
|
-
mockReviewStorage.clear();
|
|
95
|
-
|
|
96
|
-
// Sample customers
|
|
97
|
-
const customers: Customer[] = [
|
|
98
|
-
{
|
|
99
|
-
key: { kt: 'customer', pk: 'cust-1' as UUID },
|
|
100
|
-
id: 'cust-1',
|
|
101
|
-
name: 'John Smith',
|
|
102
|
-
email: 'john.smith@email.com',
|
|
103
|
-
phone: '+1-555-0123',
|
|
104
|
-
address: {
|
|
105
|
-
street: '123 Main St',
|
|
106
|
-
city: 'San Francisco',
|
|
107
|
-
state: 'CA',
|
|
108
|
-
zipCode: '94105'
|
|
109
|
-
},
|
|
110
|
-
registrationDate: new Date('2023-01-15'),
|
|
111
|
-
tier: 'gold',
|
|
112
|
-
events: {
|
|
113
|
-
created: { at: new Date('2023-01-15') },
|
|
114
|
-
updated: { at: new Date() },
|
|
115
|
-
deleted: { at: null }
|
|
116
|
-
}
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
key: { kt: 'customer', pk: 'cust-2' as UUID },
|
|
120
|
-
id: 'cust-2',
|
|
121
|
-
name: 'Sarah Johnson',
|
|
122
|
-
email: 'sarah.johnson@email.com',
|
|
123
|
-
phone: '+1-555-0456',
|
|
124
|
-
address: {
|
|
125
|
-
street: '456 Oak Ave',
|
|
126
|
-
city: 'New York',
|
|
127
|
-
state: 'NY',
|
|
128
|
-
zipCode: '10001'
|
|
129
|
-
},
|
|
130
|
-
registrationDate: new Date('2023-03-22'),
|
|
131
|
-
tier: 'silver',
|
|
132
|
-
events: {
|
|
133
|
-
created: { at: new Date('2023-03-22') },
|
|
134
|
-
updated: { at: new Date() },
|
|
135
|
-
deleted: { at: null }
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
];
|
|
139
|
-
|
|
140
|
-
// Sample products
|
|
141
|
-
const products: Product[] = [
|
|
142
|
-
{
|
|
143
|
-
key: { kt: 'product', pk: 'prod-1' as UUID },
|
|
144
|
-
id: 'prod-1',
|
|
145
|
-
name: 'Wireless Headphones',
|
|
146
|
-
description: 'Premium noise-cancelling wireless headphones with 30-hour battery life',
|
|
147
|
-
price: 299.99,
|
|
148
|
-
category: 'Electronics',
|
|
149
|
-
inStock: 45,
|
|
150
|
-
sku: 'WH-XM4-001',
|
|
151
|
-
featured: true,
|
|
152
|
-
events: {
|
|
153
|
-
created: { at: new Date('2023-01-01') },
|
|
154
|
-
updated: { at: new Date() },
|
|
155
|
-
deleted: { at: null }
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
{
|
|
159
|
-
key: { kt: 'product', pk: 'prod-2' as UUID },
|
|
160
|
-
id: 'prod-2',
|
|
161
|
-
name: 'Ergonomic Office Chair',
|
|
162
|
-
description: 'Comfortable ergonomic office chair with lumbar support and adjustable height',
|
|
163
|
-
price: 459.99,
|
|
164
|
-
category: 'Furniture',
|
|
165
|
-
inStock: 23,
|
|
166
|
-
sku: 'CHR-ERG-002',
|
|
167
|
-
featured: false,
|
|
168
|
-
events: {
|
|
169
|
-
created: { at: new Date('2023-01-10') },
|
|
170
|
-
updated: { at: new Date() },
|
|
171
|
-
deleted: { at: null }
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
];
|
|
175
|
-
|
|
176
|
-
// Sample orders and order items
|
|
177
|
-
const orders: Order[] = [
|
|
178
|
-
{
|
|
179
|
-
key: {
|
|
180
|
-
kt: 'order',
|
|
181
|
-
pk: 'order-1' as UUID,
|
|
182
|
-
loc: [{ kt: 'customer', lk: 'cust-1' }]
|
|
183
|
-
},
|
|
184
|
-
id: 'order-1',
|
|
185
|
-
customerId: 'cust-1',
|
|
186
|
-
orderDate: new Date('2024-01-15'),
|
|
187
|
-
status: 'delivered',
|
|
188
|
-
total: 299.99,
|
|
189
|
-
shippingAddress: {
|
|
190
|
-
street: '123 Main St',
|
|
191
|
-
city: 'San Francisco',
|
|
192
|
-
state: 'CA',
|
|
193
|
-
zipCode: '94105'
|
|
194
|
-
},
|
|
195
|
-
events: {
|
|
196
|
-
created: { at: new Date('2024-01-15') },
|
|
197
|
-
updated: { at: new Date() },
|
|
198
|
-
deleted: { at: null }
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
];
|
|
202
|
-
|
|
203
|
-
const orderItems: OrderItem[] = [
|
|
204
|
-
{
|
|
205
|
-
key: {
|
|
206
|
-
kt: 'orderItem',
|
|
207
|
-
pk: 'oi-1' as UUID,
|
|
208
|
-
loc: [
|
|
209
|
-
{ kt: 'customer', lk: 'cust-1' },
|
|
210
|
-
{ kt: 'order', lk: 'order-1' }
|
|
211
|
-
]
|
|
212
|
-
},
|
|
213
|
-
id: 'oi-1',
|
|
214
|
-
orderId: 'order-1',
|
|
215
|
-
productId: 'prod-1',
|
|
216
|
-
quantity: 1,
|
|
217
|
-
unitPrice: 299.99,
|
|
218
|
-
totalPrice: 299.99,
|
|
219
|
-
events: {
|
|
220
|
-
created: { at: new Date('2024-01-15') },
|
|
221
|
-
updated: { at: new Date() },
|
|
222
|
-
deleted: { at: null }
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
];
|
|
226
|
-
|
|
227
|
-
const reviews: Review[] = [
|
|
228
|
-
{
|
|
229
|
-
key: {
|
|
230
|
-
kt: 'review',
|
|
231
|
-
pk: 'rev-1' as UUID,
|
|
232
|
-
loc: [
|
|
233
|
-
{ kt: 'customer', lk: 'cust-1' },
|
|
234
|
-
{ kt: 'product', lk: 'prod-1' }
|
|
235
|
-
]
|
|
236
|
-
},
|
|
237
|
-
id: 'rev-1',
|
|
238
|
-
customerId: 'cust-1',
|
|
239
|
-
productId: 'prod-1',
|
|
240
|
-
rating: 5,
|
|
241
|
-
title: 'Excellent sound quality!',
|
|
242
|
-
content: 'These headphones have amazing sound quality and the noise cancellation is perfect for my daily commute.',
|
|
243
|
-
reviewDate: new Date('2024-01-20'),
|
|
244
|
-
verified: true,
|
|
245
|
-
events: {
|
|
246
|
-
created: { at: new Date('2024-01-20') },
|
|
247
|
-
updated: { at: new Date() },
|
|
248
|
-
deleted: { at: null }
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
];
|
|
252
|
-
|
|
253
|
-
// Store in maps
|
|
254
|
-
customers.forEach(c => mockCustomerStorage.set(c.id, c));
|
|
255
|
-
products.forEach(p => mockProductStorage.set(p.id, p));
|
|
256
|
-
orders.forEach(o => mockOrderStorage.set(o.id, o));
|
|
257
|
-
orderItems.forEach(oi => mockOrderItemStorage.set(oi.id, oi));
|
|
258
|
-
reviews.forEach(r => mockReviewStorage.set(r.id, r));
|
|
259
|
-
|
|
260
|
-
console.log('š¦ Initialized full application sample data:');
|
|
261
|
-
console.log(` Customers: ${customers.length}`);
|
|
262
|
-
console.log(` Products: ${products.length}`);
|
|
263
|
-
console.log(` Orders: ${orders.length}`);
|
|
264
|
-
console.log(` Order Items: ${orderItems.length}`);
|
|
265
|
-
console.log(` Reviews: ${reviews.length}`);
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
// ===== Mock Operations =====
|
|
269
|
-
const createCustomerOperations = () => ({
|
|
270
|
-
async all() { return Array.from(mockCustomerStorage.values()); },
|
|
271
|
-
async get(key: PriKey<'customer'>) {
|
|
272
|
-
const customer = mockCustomerStorage.get(String(key.pk));
|
|
273
|
-
if (!customer) throw new Error(`Customer not found: ${key.pk}`);
|
|
274
|
-
return customer;
|
|
275
|
-
},
|
|
276
|
-
async create(item: Customer) {
|
|
277
|
-
const id = `cust-${Date.now()}`;
|
|
278
|
-
const newCustomer: Customer = {
|
|
279
|
-
...item,
|
|
280
|
-
id,
|
|
281
|
-
key: { kt: 'customer', pk: id as UUID },
|
|
282
|
-
events: { created: { at: new Date() }, updated: { at: new Date() }, deleted: { at: null } }
|
|
283
|
-
};
|
|
284
|
-
mockCustomerStorage.set(id, newCustomer);
|
|
285
|
-
return newCustomer;
|
|
286
|
-
},
|
|
287
|
-
async update(key: PriKey<'customer'>, updates: Partial<Customer>) {
|
|
288
|
-
const existing = mockCustomerStorage.get(String(key.pk));
|
|
289
|
-
if (!existing) throw new Error(`Customer not found: ${key.pk}`);
|
|
290
|
-
const updated = {
|
|
291
|
-
...existing,
|
|
292
|
-
...updates,
|
|
293
|
-
key: existing.key, // Preserve the key
|
|
294
|
-
events: {
|
|
295
|
-
...existing.events,
|
|
296
|
-
updated: { at: new Date() }
|
|
297
|
-
}
|
|
298
|
-
};
|
|
299
|
-
mockCustomerStorage.set(String(key.pk), updated);
|
|
300
|
-
return updated;
|
|
301
|
-
},
|
|
302
|
-
async remove(key: PriKey<'customer'>) {
|
|
303
|
-
const customer = mockCustomerStorage.get(String(key.pk));
|
|
304
|
-
if (!customer) throw new Error(`Customer not found: ${key.pk}`);
|
|
305
|
-
mockCustomerStorage.delete(String(key.pk));
|
|
306
|
-
return customer;
|
|
307
|
-
},
|
|
308
|
-
async find(finder: string, params: any) {
|
|
309
|
-
const customers = Array.from(mockCustomerStorage.values());
|
|
310
|
-
switch (finder) {
|
|
311
|
-
case 'byTier': return customers.filter(c => c.tier === params.tier);
|
|
312
|
-
case 'byState': return customers.filter(c => c.address.state === params.state);
|
|
313
|
-
default: return customers;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
const createProductOperations = () => ({
|
|
319
|
-
async all() { return Array.from(mockProductStorage.values()); },
|
|
320
|
-
async get(key: PriKey<'product'>) {
|
|
321
|
-
const product = mockProductStorage.get(String(key.pk));
|
|
322
|
-
if (!product) throw new Error(`Product not found: ${key.pk}`);
|
|
323
|
-
return product;
|
|
324
|
-
},
|
|
325
|
-
async create(item: Product) {
|
|
326
|
-
const id = `prod-${Date.now()}`;
|
|
327
|
-
const newProduct: Product = {
|
|
328
|
-
...item,
|
|
329
|
-
id,
|
|
330
|
-
key: { kt: 'product', pk: id as UUID },
|
|
331
|
-
events: { created: { at: new Date() }, updated: { at: new Date() }, deleted: { at: null } }
|
|
332
|
-
};
|
|
333
|
-
mockProductStorage.set(id, newProduct);
|
|
334
|
-
return newProduct;
|
|
335
|
-
},
|
|
336
|
-
async update(key: PriKey<'product'>, updates: Partial<Product>) {
|
|
337
|
-
const existing = mockProductStorage.get(String(key.pk));
|
|
338
|
-
if (!existing) throw new Error(`Product not found: ${key.pk}`);
|
|
339
|
-
const updated = {
|
|
340
|
-
...existing,
|
|
341
|
-
...updates,
|
|
342
|
-
key: existing.key, // Preserve the key
|
|
343
|
-
events: {
|
|
344
|
-
...existing.events,
|
|
345
|
-
updated: { at: new Date() }
|
|
346
|
-
}
|
|
347
|
-
};
|
|
348
|
-
mockProductStorage.set(String(key.pk), updated);
|
|
349
|
-
return updated;
|
|
350
|
-
},
|
|
351
|
-
async remove(key: PriKey<'product'>) {
|
|
352
|
-
const product = mockProductStorage.get(String(key.pk));
|
|
353
|
-
if (!product) throw new Error(`Product not found: ${key.pk}`);
|
|
354
|
-
mockProductStorage.delete(String(key.pk));
|
|
355
|
-
return product;
|
|
356
|
-
},
|
|
357
|
-
async find(finder: string, params: any) {
|
|
358
|
-
const products = Array.from(mockProductStorage.values());
|
|
359
|
-
switch (finder) {
|
|
360
|
-
case 'byCategory': return products.filter(p => p.category === params.category);
|
|
361
|
-
case 'featured': return products.filter(p => p.featured);
|
|
362
|
-
case 'inStock': return products.filter(p => p.inStock > 0);
|
|
363
|
-
default: return products;
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
// Additional operations for contained items (simplified for brevity)
|
|
369
|
-
const createOrderOperations = () => ({
|
|
370
|
-
async all() { return Array.from(mockOrderStorage.values()); },
|
|
371
|
-
async get(key: ComKey<'order', 'customer'>) {
|
|
372
|
-
const order = mockOrderStorage.get(String(key.pk));
|
|
373
|
-
if (!order) throw new Error(`Order not found: ${key.pk}`);
|
|
374
|
-
return order;
|
|
375
|
-
},
|
|
376
|
-
async create(item: Order, options?: { locations?: any[] }) {
|
|
377
|
-
console.log('Order create called with:', { item, options });
|
|
378
|
-
const id = `order-${Date.now()}`;
|
|
379
|
-
// Extract customerId from locations (passed from URL params) or item data
|
|
380
|
-
const customerId = options?.locations?.[0]?.lk || item.customerId;
|
|
381
|
-
console.log('Extracted customerId:', customerId);
|
|
382
|
-
if (!customerId) {
|
|
383
|
-
throw new Error('CustomerId is required for order creation');
|
|
384
|
-
}
|
|
385
|
-
const newOrder: Order = {
|
|
386
|
-
...item,
|
|
387
|
-
id,
|
|
388
|
-
customerId,
|
|
389
|
-
orderDate: item.orderDate ? new Date(item.orderDate) : new Date(),
|
|
390
|
-
key: {
|
|
391
|
-
kt: 'order',
|
|
392
|
-
pk: id as UUID,
|
|
393
|
-
loc: [{ kt: 'customer', lk: customerId }]
|
|
394
|
-
},
|
|
395
|
-
events: { created: { at: new Date() }, updated: { at: new Date() }, deleted: { at: null } }
|
|
396
|
-
};
|
|
397
|
-
console.log('Created order:', newOrder);
|
|
398
|
-
mockOrderStorage.set(id, newOrder);
|
|
399
|
-
return newOrder;
|
|
400
|
-
},
|
|
401
|
-
async update(key: ComKey<'order', 'customer'>, updates: Partial<Order>) {
|
|
402
|
-
const existing = mockOrderStorage.get(String(key.pk));
|
|
403
|
-
if (!existing) throw new Error(`Order not found: ${key.pk}`);
|
|
404
|
-
const updated = {
|
|
405
|
-
...existing,
|
|
406
|
-
...updates,
|
|
407
|
-
// Ensure orderDate is a Date object if it's being updated
|
|
408
|
-
...(updates.orderDate && { orderDate: new Date(updates.orderDate) }),
|
|
409
|
-
key: existing.key, // Preserve the key
|
|
410
|
-
events: {
|
|
411
|
-
...existing.events,
|
|
412
|
-
updated: { at: new Date() }
|
|
413
|
-
}
|
|
414
|
-
};
|
|
415
|
-
mockOrderStorage.set(String(key.pk), updated);
|
|
416
|
-
return updated;
|
|
417
|
-
},
|
|
418
|
-
async remove(key: ComKey<'order', 'customer'>) {
|
|
419
|
-
const order = mockOrderStorage.get(String(key.pk));
|
|
420
|
-
if (!order) throw new Error(`Order not found: ${key.pk}`);
|
|
421
|
-
mockOrderStorage.delete(String(key.pk));
|
|
422
|
-
return order;
|
|
423
|
-
},
|
|
424
|
-
async find(finder: string, params: any) {
|
|
425
|
-
const orders = Array.from(mockOrderStorage.values());
|
|
426
|
-
switch (finder) {
|
|
427
|
-
case 'byStatus': return orders.filter(o => o.status === params.status);
|
|
428
|
-
case 'byCustomer': return orders.filter(o => o.customerId === params.customerId);
|
|
429
|
-
default: return orders;
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
// ===== Middleware =====
|
|
435
|
-
const errorHandler = (err: Error, req: Request, res: Response, next: NextFunction) => {
|
|
436
|
-
console.error('ā Application Error:', err.message);
|
|
437
|
-
console.error('ā Stack trace:', err.stack);
|
|
438
|
-
res.status(500).json({
|
|
439
|
-
error: 'Internal Server Error',
|
|
440
|
-
message: process.env.NODE_ENV === 'development' ? err.message : 'Something went wrong'
|
|
441
|
-
});
|
|
442
|
-
};
|
|
443
|
-
|
|
444
|
-
const requestLogger = (req: Request, res: Response, next: NextFunction) => {
|
|
445
|
-
const start = Date.now();
|
|
446
|
-
console.log(`š ${req.method} ${req.path} - Started`);
|
|
447
|
-
|
|
448
|
-
res.on('finish', () => {
|
|
449
|
-
const duration = Date.now() - start;
|
|
450
|
-
console.log(`ā
${req.method} ${req.path} - ${res.statusCode} (${duration}ms)`);
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
next();
|
|
454
|
-
};
|
|
455
|
-
|
|
456
|
-
const validateCustomerTier = (req: Request, res: Response, next: NextFunction) => {
|
|
457
|
-
// Example business logic middleware
|
|
458
|
-
const tier = req.headers['x-customer-tier'] as string;
|
|
459
|
-
if (tier && !['bronze', 'silver', 'gold', 'platinum'].includes(tier)) {
|
|
460
|
-
return res.status(400).json({ error: 'Invalid customer tier' });
|
|
461
|
-
}
|
|
462
|
-
next();
|
|
463
|
-
};
|
|
464
|
-
|
|
465
|
-
/**
|
|
466
|
-
* Main function demonstrating a full fjell-express-router application
|
|
467
|
-
*/
|
|
468
|
-
export const runFullApplicationExample = async (): Promise<{ app: Application }> => {
|
|
469
|
-
console.log('š Starting Full Application Example...\n');
|
|
470
|
-
|
|
471
|
-
initializeSampleData();
|
|
472
|
-
const registry = createRegistry();
|
|
473
|
-
|
|
474
|
-
// Create mock instances
|
|
475
|
-
const customerInstance = { operations: createCustomerOperations(), options: {} } as any;
|
|
476
|
-
const productInstance = { operations: createProductOperations(), options: {} } as any;
|
|
477
|
-
const orderInstance = { operations: createOrderOperations(), options: {} } as any;
|
|
478
|
-
|
|
479
|
-
const app: Application = express();
|
|
480
|
-
|
|
481
|
-
// ===== Global Middleware =====
|
|
482
|
-
app.use(express.json({ limit: '10mb' }));
|
|
483
|
-
app.use(express.urlencoded({ extended: true }));
|
|
484
|
-
app.use(requestLogger);
|
|
485
|
-
app.use(validateCustomerTier);
|
|
486
|
-
|
|
487
|
-
// JSON parsing error handler
|
|
488
|
-
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
|
|
489
|
-
if (err instanceof SyntaxError && 'body' in err) {
|
|
490
|
-
return res.status(400).json({ error: 'Invalid JSON' });
|
|
491
|
-
}
|
|
492
|
-
next(err);
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
// Validation middleware for customer creation only (not nested routes)
|
|
496
|
-
app.use('/api/customers', (req, res, next) => {
|
|
497
|
-
// Only validate for direct customer creation, not nested routes like orders
|
|
498
|
-
if (req.method === 'POST' && req.path === '/') {
|
|
499
|
-
const { name, email, phone, address, tier } = req.body;
|
|
500
|
-
if (!name || !email) {
|
|
501
|
-
return res.status(500).json({ error: 'Missing required fields: name and email' });
|
|
502
|
-
}
|
|
503
|
-
if (!phone || !address || !tier) {
|
|
504
|
-
return res.status(500).json({ error: 'Missing required fields: phone, address, and tier' });
|
|
505
|
-
}
|
|
506
|
-
if (!address.street || !address.city || !address.state || !address.zipCode) {
|
|
507
|
-
return res.status(500).json({ error: 'Missing required address fields: street, city, state, and zipCode' });
|
|
508
|
-
}
|
|
509
|
-
if (!['bronze', 'silver', 'gold', 'platinum'].includes(tier)) {
|
|
510
|
-
return res.status(500).json({ error: 'Invalid tier. Must be bronze, silver, gold, or platinum' });
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
next();
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
app.use('/api/products', (req, res, next) => {
|
|
517
|
-
if (req.method === 'POST') {
|
|
518
|
-
const { name, price, category } = req.body;
|
|
519
|
-
if (!name || typeof price !== 'number' || !category) {
|
|
520
|
-
return res.status(500).json({ error: 'Missing required fields: name, price, and category' });
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
next();
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
// ===== CORS and Security Headers =====
|
|
527
|
-
app.use((req, res, next) => {
|
|
528
|
-
res.header('Access-Control-Allow-Origin', '*');
|
|
529
|
-
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
530
|
-
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization, x-customer-tier');
|
|
531
|
-
res.header('X-Powered-By', 'Fjell Express Router');
|
|
532
|
-
if (req.method === 'OPTIONS') {
|
|
533
|
-
return res.sendStatus(200);
|
|
534
|
-
}
|
|
535
|
-
next();
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
// ===== Create Routers =====
|
|
539
|
-
console.log('š¤ļø Creating application routers...');
|
|
540
|
-
const customerRouter = new PItemRouter(customerInstance, 'customer');
|
|
541
|
-
const productRouter = new PItemRouter(productInstance, 'product');
|
|
542
|
-
const orderRouter = new CItemRouter(orderInstance, 'order', customerRouter);
|
|
543
|
-
|
|
544
|
-
// ===== API Routes =====
|
|
545
|
-
|
|
546
|
-
// Health and system routes
|
|
547
|
-
app.get('/health', (req, res) => {
|
|
548
|
-
res.json({
|
|
549
|
-
status: 'healthy',
|
|
550
|
-
timestamp: new Date().toISOString(),
|
|
551
|
-
version: '1.0.0',
|
|
552
|
-
uptime: process.uptime(),
|
|
553
|
-
data: {
|
|
554
|
-
customers: mockCustomerStorage.size,
|
|
555
|
-
products: mockProductStorage.size,
|
|
556
|
-
orders: mockOrderStorage.size
|
|
557
|
-
}
|
|
558
|
-
});
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
// Core entity routes
|
|
562
|
-
app.use('/api/products', productRouter.getRouter());
|
|
563
|
-
|
|
564
|
-
// Mount order router BEFORE customer router to avoid conflicts
|
|
565
|
-
// Middleware to extract customerPk parameter for order router
|
|
566
|
-
app.use('/api/customers/:customerPk/orders', (req, res, next) => {
|
|
567
|
-
res.locals.customerPk = req.params.customerPk;
|
|
568
|
-
next();
|
|
569
|
-
});
|
|
570
|
-
app.use('/api/customers/:customerPk/orders', orderRouter.getRouter());
|
|
571
|
-
|
|
572
|
-
// Security validation middleware for customer routes - must be BEFORE router
|
|
573
|
-
app.use('/api/customers/:customerPk', (req, res, next) => {
|
|
574
|
-
const { customerPk } = req.params;
|
|
575
|
-
|
|
576
|
-
// Check for path traversal attempts
|
|
577
|
-
if (customerPk.includes('../') || customerPk.includes('..\\') || customerPk.includes('%2e%2e')) {
|
|
578
|
-
return res.status(500).json({ error: 'Path traversal attempt detected' });
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
// Check for null byte injection
|
|
582
|
-
if (customerPk.includes('\x00') || customerPk.includes('%00') || customerPk.includes('\u0000')) {
|
|
583
|
-
return res.status(500).json({ error: 'Null byte injection attempt detected' });
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
next();
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
// Customer router after security validation
|
|
590
|
-
app.use('/api/customers', customerRouter.getRouter());
|
|
591
|
-
|
|
592
|
-
// Custom find routes for better test compatibility
|
|
593
|
-
app.get('/api/customers/find/:finder', async (req, res, next) => {
|
|
594
|
-
try {
|
|
595
|
-
const { finder } = req.params;
|
|
596
|
-
const params = req.query;
|
|
597
|
-
const customers = await customerInstance.operations.find(finder, params);
|
|
598
|
-
res.json(customers);
|
|
599
|
-
} catch (error) {
|
|
600
|
-
next(error);
|
|
601
|
-
}
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
app.get('/api/products/find/:finder', async (req, res, next) => {
|
|
605
|
-
try {
|
|
606
|
-
const { finder } = req.params;
|
|
607
|
-
const params = req.query;
|
|
608
|
-
const products = await productInstance.operations.find(finder, params);
|
|
609
|
-
res.json(products);
|
|
610
|
-
} catch (error) {
|
|
611
|
-
next(error);
|
|
612
|
-
}
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
app.get('/api/customers/:customerPk/orders/find/:finder', async (req, res, next) => {
|
|
616
|
-
try {
|
|
617
|
-
const { finder } = req.params;
|
|
618
|
-
const params = req.query;
|
|
619
|
-
const orders = await orderInstance.operations.find(finder, params);
|
|
620
|
-
res.json(orders);
|
|
621
|
-
} catch (error) {
|
|
622
|
-
next(error);
|
|
623
|
-
}
|
|
624
|
-
});
|
|
625
|
-
|
|
626
|
-
// Business logic routes
|
|
627
|
-
app.get('/api/dashboard', async (req, res, next) => {
|
|
628
|
-
try {
|
|
629
|
-
const customers = await customerInstance.operations.all();
|
|
630
|
-
const products = await productInstance.operations.all();
|
|
631
|
-
const orders = await orderInstance.operations.all();
|
|
632
|
-
|
|
633
|
-
const dashboard = {
|
|
634
|
-
summary: {
|
|
635
|
-
totalCustomers: customers.length,
|
|
636
|
-
totalProducts: products.length,
|
|
637
|
-
totalOrders: orders.length,
|
|
638
|
-
revenue: orders.reduce((sum: number, order: Order) => sum + order.total, 0)
|
|
639
|
-
},
|
|
640
|
-
customerTiers: customers.reduce((acc: any, customer: Customer) => {
|
|
641
|
-
acc[customer.tier] = (acc[customer.tier] || 0) + 1;
|
|
642
|
-
return acc;
|
|
643
|
-
}, {}),
|
|
644
|
-
orderStatuses: orders.reduce((acc: any, order: Order) => {
|
|
645
|
-
acc[order.status] = (acc[order.status] || 0) + 1;
|
|
646
|
-
return acc;
|
|
647
|
-
}, {}),
|
|
648
|
-
featuredProducts: products.filter((product: Product) => product.featured),
|
|
649
|
-
recentOrders: orders
|
|
650
|
-
.sort((a: Order, b: Order) => new Date(b.orderDate).getTime() - new Date(a.orderDate).getTime())
|
|
651
|
-
.slice(0, 10)
|
|
652
|
-
};
|
|
653
|
-
|
|
654
|
-
res.json(dashboard);
|
|
655
|
-
} catch (error) {
|
|
656
|
-
next(error);
|
|
657
|
-
}
|
|
658
|
-
});
|
|
659
|
-
|
|
660
|
-
// Product catalog with search and filtering
|
|
661
|
-
app.get('/api/catalog', async (req, res, next) => {
|
|
662
|
-
try {
|
|
663
|
-
const { category, featured, minPrice, maxPrice, search } = req.query;
|
|
664
|
-
let products = await productInstance.operations.all();
|
|
665
|
-
|
|
666
|
-
if (category) {
|
|
667
|
-
products = products.filter((p: Product) => p.category === category);
|
|
668
|
-
}
|
|
669
|
-
if (featured === 'true') {
|
|
670
|
-
products = products.filter((p: Product) => p.featured);
|
|
671
|
-
}
|
|
672
|
-
if (minPrice) {
|
|
673
|
-
products = products.filter((p: Product) => p.price >= Number(minPrice));
|
|
674
|
-
}
|
|
675
|
-
if (maxPrice) {
|
|
676
|
-
products = products.filter((p: Product) => p.price <= Number(maxPrice));
|
|
677
|
-
}
|
|
678
|
-
if (search) {
|
|
679
|
-
const searchTerm = String(search).toLowerCase();
|
|
680
|
-
products = products.filter((p: Product) =>
|
|
681
|
-
p.name.toLowerCase().includes(searchTerm) ||
|
|
682
|
-
p.description.toLowerCase().includes(searchTerm)
|
|
683
|
-
);
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
res.json({
|
|
687
|
-
products,
|
|
688
|
-
totalCount: products.length,
|
|
689
|
-
filters: { category, featured, minPrice, maxPrice, search }
|
|
690
|
-
});
|
|
691
|
-
} catch (error) {
|
|
692
|
-
next(error);
|
|
693
|
-
}
|
|
694
|
-
});
|
|
695
|
-
|
|
696
|
-
// Customer analytics
|
|
697
|
-
app.get('/api/customers/:customerPk/analytics', async (req, res, next) => {
|
|
698
|
-
try {
|
|
699
|
-
const { customerPk } = req.params;
|
|
700
|
-
const customer = await customerInstance.operations.get({ kt: 'customer', pk: customerPk });
|
|
701
|
-
const customerOrders = await orderInstance.operations.find('byCustomer', { customerId: customerPk });
|
|
702
|
-
|
|
703
|
-
const analytics = {
|
|
704
|
-
customer: {
|
|
705
|
-
id: customer.id,
|
|
706
|
-
name: customer.name,
|
|
707
|
-
tier: customer.tier,
|
|
708
|
-
registrationDate: customer.registrationDate
|
|
709
|
-
},
|
|
710
|
-
orderStats: {
|
|
711
|
-
totalOrders: customerOrders.length,
|
|
712
|
-
totalSpent: customerOrders.reduce((sum: number, order: Order) => sum + order.total, 0),
|
|
713
|
-
averageOrderValue: customerOrders.length > 0
|
|
714
|
-
? customerOrders.reduce((sum: number, order: Order) => sum + order.total, 0) / customerOrders.length
|
|
715
|
-
: 0,
|
|
716
|
-
ordersByStatus: customerOrders.reduce((acc: any, order: Order) => {
|
|
717
|
-
acc[order.status] = (acc[order.status] || 0) + 1;
|
|
718
|
-
return acc;
|
|
719
|
-
}, {})
|
|
720
|
-
},
|
|
721
|
-
recentOrders: customerOrders
|
|
722
|
-
.sort((a: Order, b: Order) => new Date(b.orderDate).getTime() - new Date(a.orderDate).getTime())
|
|
723
|
-
.slice(0, 5)
|
|
724
|
-
};
|
|
725
|
-
|
|
726
|
-
res.json(analytics);
|
|
727
|
-
} catch (error) {
|
|
728
|
-
next(error);
|
|
729
|
-
}
|
|
730
|
-
});
|
|
731
|
-
|
|
732
|
-
// Error handling middleware (must be last)
|
|
733
|
-
app.use(errorHandler);
|
|
734
|
-
|
|
735
|
-
// 404 handler
|
|
736
|
-
app.use((req, res) => {
|
|
737
|
-
res.status(404).json({ error: 'Not Found', path: req.originalUrl });
|
|
738
|
-
});
|
|
739
|
-
|
|
740
|
-
console.log('\nā
Full Application Example setup complete!');
|
|
741
|
-
console.log('\nš Available endpoints:');
|
|
742
|
-
console.log(' š„ GET /health - System health check');
|
|
743
|
-
console.log(' š GET /api/dashboard - Business dashboard');
|
|
744
|
-
console.log(' šļø GET /api/catalog - Product catalog with filtering');
|
|
745
|
-
console.log(' š„ REST /api/customers - Customer management');
|
|
746
|
-
console.log(' š REST /api/customers/:customerPk/orders - Order management');
|
|
747
|
-
console.log(' š¦ REST /api/products - Product management');
|
|
748
|
-
console.log(' š GET /api/customers/:customerPk/analytics - Customer analytics');
|
|
749
|
-
|
|
750
|
-
return { app };
|
|
751
|
-
};
|
|
752
|
-
|
|
753
|
-
// If this file is run directly, start the server
|
|
754
|
-
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
755
|
-
runFullApplicationExample().then(({ app }) => {
|
|
756
|
-
const PORT = process.env.PORT || 3003;
|
|
757
|
-
app.listen(PORT, () => {
|
|
758
|
-
console.log(`\nš Full Application Server running on http://localhost:${PORT}`);
|
|
759
|
-
console.log('\nš” Try these example requests:');
|
|
760
|
-
console.log(` curl http://localhost:${PORT}/health`);
|
|
761
|
-
console.log(` curl http://localhost:${PORT}/api/dashboard`);
|
|
762
|
-
console.log(` curl http://localhost:${PORT}/api/catalog`);
|
|
763
|
-
console.log(` curl http://localhost:${PORT}/api/customers`);
|
|
764
|
-
console.log(` curl http://localhost:${PORT}/api/products`);
|
|
765
|
-
console.log(` curl "http://localhost:${PORT}/api/catalog?category=Electronics&featured=true"`);
|
|
766
|
-
});
|
|
767
|
-
}).catch(console.error);
|
|
768
|
-
}
|