@fjell/express-router 4.4.22 → 4.4.24
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/README.md +205 -0
- package/dist/CItemRouter.d.ts +3 -3
- package/dist/CItemRouter.d.ts.map +1 -1
- package/dist/CItemRouter.js +24 -9
- package/dist/CItemRouter.js.map +2 -2
- package/dist/Instance.d.ts +1 -1
- package/dist/Instance.d.ts.map +1 -1
- package/dist/Instance.js +1 -1
- package/dist/Instance.js.map +1 -1
- package/dist/InstanceFactory.d.ts +1 -1
- package/dist/InstanceFactory.d.ts.map +1 -1
- package/dist/InstanceFactory.js +2 -2
- package/dist/InstanceFactory.js.map +1 -1
- package/dist/ItemRouter.d.ts +13 -4
- package/dist/ItemRouter.d.ts.map +1 -1
- package/dist/ItemRouter.js +92 -16
- package/dist/ItemRouter.js.map +2 -2
- package/dist/PItemRouter.d.ts +3 -3
- package/dist/PItemRouter.d.ts.map +1 -1
- package/dist/PItemRouter.js +1 -1
- package/dist/PItemRouter.js.map +2 -2
- package/dist/Registry.js +1 -1
- package/dist/Registry.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -6
- package/dist/index.js.map +1 -1
- package/dist/util/general.js.map +1 -1
- package/examples/README.md +47 -0
- package/examples/basic-router-example.ts +7 -6
- package/examples/full-application-example.ts +137 -12
- package/examples/nested-router-example.ts +154 -54
- package/examples/router-handlers-example.ts +370 -0
- package/package.json +2 -1
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import express, { Application, NextFunction, Request, Response } from 'express';
|
|
15
|
-
import { ComKey, Item, PriKey, UUID } from '@fjell/core';
|
|
15
|
+
import { ComKey, Item, LocKey, PriKey, UUID } from '@fjell/core';
|
|
16
16
|
import { CItemRouter, createRegistry, PItemRouter } from '../src';
|
|
17
17
|
|
|
18
18
|
// ===== Data Models =====
|
|
@@ -280,12 +280,23 @@ const createCustomerOperations = () => ({
|
|
|
280
280
|
async update(key: PriKey<'customer'>, updates: Partial<Customer>) {
|
|
281
281
|
const existing = mockCustomerStorage.get(String(key.pk));
|
|
282
282
|
if (!existing) throw new Error(`Customer not found: ${key.pk}`);
|
|
283
|
-
const updated = {
|
|
283
|
+
const updated = {
|
|
284
|
+
...existing,
|
|
285
|
+
...updates,
|
|
286
|
+
key: existing.key, // Preserve the key
|
|
287
|
+
events: {
|
|
288
|
+
...existing.events,
|
|
289
|
+
updated: { at: new Date() }
|
|
290
|
+
}
|
|
291
|
+
};
|
|
284
292
|
mockCustomerStorage.set(String(key.pk), updated);
|
|
285
293
|
return updated;
|
|
286
294
|
},
|
|
287
295
|
async remove(key: PriKey<'customer'>) {
|
|
288
|
-
|
|
296
|
+
const customer = mockCustomerStorage.get(String(key.pk));
|
|
297
|
+
if (!customer) throw new Error(`Customer not found: ${key.pk}`);
|
|
298
|
+
mockCustomerStorage.delete(String(key.pk));
|
|
299
|
+
return customer;
|
|
289
300
|
},
|
|
290
301
|
async find(finder: string, params: any) {
|
|
291
302
|
const customers = Array.from(mockCustomerStorage.values());
|
|
@@ -318,12 +329,23 @@ const createProductOperations = () => ({
|
|
|
318
329
|
async update(key: PriKey<'product'>, updates: Partial<Product>) {
|
|
319
330
|
const existing = mockProductStorage.get(String(key.pk));
|
|
320
331
|
if (!existing) throw new Error(`Product not found: ${key.pk}`);
|
|
321
|
-
const updated = {
|
|
332
|
+
const updated = {
|
|
333
|
+
...existing,
|
|
334
|
+
...updates,
|
|
335
|
+
key: existing.key, // Preserve the key
|
|
336
|
+
events: {
|
|
337
|
+
...existing.events,
|
|
338
|
+
updated: { at: new Date() }
|
|
339
|
+
}
|
|
340
|
+
};
|
|
322
341
|
mockProductStorage.set(String(key.pk), updated);
|
|
323
342
|
return updated;
|
|
324
343
|
},
|
|
325
344
|
async remove(key: PriKey<'product'>) {
|
|
326
|
-
|
|
345
|
+
const product = mockProductStorage.get(String(key.pk));
|
|
346
|
+
if (!product) throw new Error(`Product not found: ${key.pk}`);
|
|
347
|
+
mockProductStorage.delete(String(key.pk));
|
|
348
|
+
return product;
|
|
327
349
|
},
|
|
328
350
|
async find(finder: string, params: any) {
|
|
329
351
|
const products = Array.from(mockProductStorage.values());
|
|
@@ -344,25 +366,54 @@ const createOrderOperations = () => ({
|
|
|
344
366
|
if (!order) throw new Error(`Order not found: ${key.pk}`);
|
|
345
367
|
return order;
|
|
346
368
|
},
|
|
347
|
-
async create(item: Order) {
|
|
369
|
+
async create(item: Order, options?: { locations?: any[] }) {
|
|
370
|
+
console.log('Order create called with:', { item, options });
|
|
348
371
|
const id = `order-${Date.now()}`;
|
|
372
|
+
// Extract customerId from locations (passed from URL params) or item data
|
|
373
|
+
const customerId = options?.locations?.[0]?.lk || item.customerId;
|
|
374
|
+
console.log('Extracted customerId:', customerId);
|
|
375
|
+
if (!customerId) {
|
|
376
|
+
throw new Error('CustomerId is required for order creation');
|
|
377
|
+
}
|
|
349
378
|
const newOrder: Order = {
|
|
350
379
|
...item,
|
|
351
380
|
id,
|
|
352
|
-
|
|
381
|
+
customerId,
|
|
382
|
+
orderDate: item.orderDate ? new Date(item.orderDate) : new Date(),
|
|
383
|
+
key: {
|
|
384
|
+
kt: 'order',
|
|
385
|
+
pk: id as UUID,
|
|
386
|
+
loc: [{ kt: 'customer', lk: customerId }]
|
|
387
|
+
},
|
|
353
388
|
events: { created: { at: new Date() }, updated: { at: new Date() }, deleted: { at: null } }
|
|
354
389
|
};
|
|
390
|
+
console.log('Created order:', newOrder);
|
|
355
391
|
mockOrderStorage.set(id, newOrder);
|
|
356
392
|
return newOrder;
|
|
357
393
|
},
|
|
358
394
|
async update(key: ComKey<'order', 'customer'>, updates: Partial<Order>) {
|
|
359
395
|
const existing = mockOrderStorage.get(String(key.pk));
|
|
360
396
|
if (!existing) throw new Error(`Order not found: ${key.pk}`);
|
|
361
|
-
const updated = {
|
|
397
|
+
const updated = {
|
|
398
|
+
...existing,
|
|
399
|
+
...updates,
|
|
400
|
+
// Ensure orderDate is a Date object if it's being updated
|
|
401
|
+
...(updates.orderDate && { orderDate: new Date(updates.orderDate) }),
|
|
402
|
+
key: existing.key, // Preserve the key
|
|
403
|
+
events: {
|
|
404
|
+
...existing.events,
|
|
405
|
+
updated: { at: new Date() }
|
|
406
|
+
}
|
|
407
|
+
};
|
|
362
408
|
mockOrderStorage.set(String(key.pk), updated);
|
|
363
409
|
return updated;
|
|
364
410
|
},
|
|
365
|
-
async remove(key: ComKey<'order', 'customer'>) {
|
|
411
|
+
async remove(key: ComKey<'order', 'customer'>) {
|
|
412
|
+
const order = mockOrderStorage.get(String(key.pk));
|
|
413
|
+
if (!order) throw new Error(`Order not found: ${key.pk}`);
|
|
414
|
+
mockOrderStorage.delete(String(key.pk));
|
|
415
|
+
return order;
|
|
416
|
+
},
|
|
366
417
|
async find(finder: string, params: any) {
|
|
367
418
|
const orders = Array.from(mockOrderStorage.values());
|
|
368
419
|
switch (finder) {
|
|
@@ -376,6 +427,7 @@ const createOrderOperations = () => ({
|
|
|
376
427
|
// ===== Middleware =====
|
|
377
428
|
const errorHandler = (err: Error, req: Request, res: Response, next: NextFunction) => {
|
|
378
429
|
console.error('❌ Application Error:', err.message);
|
|
430
|
+
console.error('❌ Stack trace:', err.stack);
|
|
379
431
|
res.status(500).json({
|
|
380
432
|
error: 'Internal Server Error',
|
|
381
433
|
message: process.env.NODE_ENV === 'development' ? err.message : 'Something went wrong'
|
|
@@ -425,6 +477,36 @@ export const runFullApplicationExample = async (): Promise<{ app: Application }>
|
|
|
425
477
|
app.use(requestLogger);
|
|
426
478
|
app.use(validateCustomerTier);
|
|
427
479
|
|
|
480
|
+
// JSON parsing error handler
|
|
481
|
+
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
|
|
482
|
+
if (err instanceof SyntaxError && 'body' in err) {
|
|
483
|
+
return res.status(400).json({ error: 'Invalid JSON' });
|
|
484
|
+
}
|
|
485
|
+
next(err);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// Validation middleware for customer creation only (not nested routes)
|
|
489
|
+
app.use('/api/customers', (req, res, next) => {
|
|
490
|
+
// Only validate for direct customer creation, not nested routes like orders
|
|
491
|
+
if (req.method === 'POST' && req.path === '/') {
|
|
492
|
+
const { name, email } = req.body;
|
|
493
|
+
if (!name || !email) {
|
|
494
|
+
return res.status(500).json({ error: 'Missing required fields: name and email' });
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
next();
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
app.use('/api/products', (req, res, next) => {
|
|
501
|
+
if (req.method === 'POST') {
|
|
502
|
+
const { name, price, category } = req.body;
|
|
503
|
+
if (!name || typeof price !== 'number' || !category) {
|
|
504
|
+
return res.status(500).json({ error: 'Missing required fields: name, price, and category' });
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
next();
|
|
508
|
+
});
|
|
509
|
+
|
|
428
510
|
// ===== CORS and Security Headers =====
|
|
429
511
|
app.use((req, res, next) => {
|
|
430
512
|
res.header('Access-Control-Allow-Origin', '*');
|
|
@@ -461,10 +543,53 @@ export const runFullApplicationExample = async (): Promise<{ app: Application }>
|
|
|
461
543
|
});
|
|
462
544
|
|
|
463
545
|
// Core entity routes
|
|
464
|
-
app.use('/api/customers', customerRouter.getRouter());
|
|
465
546
|
app.use('/api/products', productRouter.getRouter());
|
|
547
|
+
|
|
548
|
+
// Mount order router BEFORE customer router to avoid conflicts
|
|
549
|
+
// Middleware to extract customerPk parameter for order router
|
|
550
|
+
app.use('/api/customers/:customerPk/orders', (req, res, next) => {
|
|
551
|
+
res.locals.customerPk = req.params.customerPk;
|
|
552
|
+
next();
|
|
553
|
+
});
|
|
466
554
|
app.use('/api/customers/:customerPk/orders', orderRouter.getRouter());
|
|
467
555
|
|
|
556
|
+
// Customer router last to avoid route conflicts
|
|
557
|
+
app.use('/api/customers', customerRouter.getRouter());
|
|
558
|
+
|
|
559
|
+
// Custom find routes for better test compatibility
|
|
560
|
+
app.get('/api/customers/find/:finder', async (req, res, next) => {
|
|
561
|
+
try {
|
|
562
|
+
const { finder } = req.params;
|
|
563
|
+
const params = req.query;
|
|
564
|
+
const customers = await customerInstance.operations.find(finder, params);
|
|
565
|
+
res.json(customers);
|
|
566
|
+
} catch (error) {
|
|
567
|
+
next(error);
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
app.get('/api/products/find/:finder', async (req, res, next) => {
|
|
572
|
+
try {
|
|
573
|
+
const { finder } = req.params;
|
|
574
|
+
const params = req.query;
|
|
575
|
+
const products = await productInstance.operations.find(finder, params);
|
|
576
|
+
res.json(products);
|
|
577
|
+
} catch (error) {
|
|
578
|
+
next(error);
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
app.get('/api/customers/:customerPk/orders/find/:finder', async (req, res, next) => {
|
|
583
|
+
try {
|
|
584
|
+
const { finder } = req.params;
|
|
585
|
+
const params = req.query;
|
|
586
|
+
const orders = await orderInstance.operations.find(finder, params);
|
|
587
|
+
res.json(orders);
|
|
588
|
+
} catch (error) {
|
|
589
|
+
next(error);
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
|
|
468
593
|
// Business logic routes
|
|
469
594
|
app.get('/api/dashboard', async (req, res, next) => {
|
|
470
595
|
try {
|
|
@@ -489,7 +614,7 @@ export const runFullApplicationExample = async (): Promise<{ app: Application }>
|
|
|
489
614
|
}, {}),
|
|
490
615
|
featuredProducts: products.filter((product: Product) => product.featured),
|
|
491
616
|
recentOrders: orders
|
|
492
|
-
.sort((a: Order, b: Order) => b.orderDate.getTime() - a.orderDate.getTime())
|
|
617
|
+
.sort((a: Order, b: Order) => new Date(b.orderDate).getTime() - new Date(a.orderDate).getTime())
|
|
493
618
|
.slice(0, 10)
|
|
494
619
|
};
|
|
495
620
|
|
|
@@ -561,7 +686,7 @@ export const runFullApplicationExample = async (): Promise<{ app: Application }>
|
|
|
561
686
|
}, {})
|
|
562
687
|
},
|
|
563
688
|
recentOrders: customerOrders
|
|
564
|
-
.sort((a: Order, b: Order) => b.orderDate.getTime() - a.orderDate.getTime())
|
|
689
|
+
.sort((a: Order, b: Order) => new Date(b.orderDate).getTime() - new Date(a.orderDate).getTime())
|
|
565
690
|
.slice(0, 5)
|
|
566
691
|
};
|
|
567
692
|
|
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
import express, { Application } from 'express';
|
|
15
15
|
import { ComKey, Item, PriKey, UUID } from '@fjell/core';
|
|
16
16
|
import { CItemRouter, createRegistry, PItemRouter } from '../src';
|
|
17
|
+
import { NotFoundError } from '@fjell/lib';
|
|
17
18
|
|
|
18
19
|
// Define our hierarchical data models
|
|
19
|
-
interface Organization extends Item<'organization'> {
|
|
20
|
+
export interface Organization extends Item<'organization'> {
|
|
20
21
|
id: string;
|
|
21
22
|
name: string;
|
|
22
23
|
type: 'startup' | 'enterprise' | 'nonprofit';
|
|
@@ -24,7 +25,7 @@ interface Organization extends Item<'organization'> {
|
|
|
24
25
|
industry: string;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
interface Department extends Item<'department', 'organization'> {
|
|
28
|
+
export interface Department extends Item<'department', 'organization'> {
|
|
28
29
|
id: string;
|
|
29
30
|
name: string;
|
|
30
31
|
budget: number;
|
|
@@ -32,7 +33,7 @@ interface Department extends Item<'department', 'organization'> {
|
|
|
32
33
|
organizationId: string;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
interface Employee extends Item<'employee', 'organization', 'department'> {
|
|
36
|
+
export interface Employee extends Item<'employee', 'organization', 'department'> {
|
|
36
37
|
id: string;
|
|
37
38
|
name: string;
|
|
38
39
|
email: string;
|
|
@@ -202,10 +203,15 @@ const createOrgOperations = () => ({
|
|
|
202
203
|
},
|
|
203
204
|
async get(key: PriKey<'organization'>) {
|
|
204
205
|
const org = mockOrgStorage.get(String(key.pk));
|
|
205
|
-
if (!org) throw new
|
|
206
|
+
if (!org) throw new NotFoundError('get', { kta: ['organization', '', '', '', '', ''], scopes: [] }, key);
|
|
206
207
|
return org;
|
|
207
208
|
},
|
|
208
209
|
async create(item: Organization) {
|
|
210
|
+
// Validate required fields
|
|
211
|
+
if (!item.name || !item.type || !item.founded || !item.industry) {
|
|
212
|
+
throw new Error('Missing required fields: name, type, founded, and industry are required');
|
|
213
|
+
}
|
|
214
|
+
|
|
209
215
|
const id = `org-${Date.now()}`;
|
|
210
216
|
const newOrg: Organization = {
|
|
211
217
|
...item,
|
|
@@ -222,13 +228,23 @@ const createOrgOperations = () => ({
|
|
|
222
228
|
},
|
|
223
229
|
async update(key: PriKey<'organization'>, updates: Partial<Organization>) {
|
|
224
230
|
const existing = mockOrgStorage.get(String(key.pk));
|
|
225
|
-
if (!existing) throw new
|
|
226
|
-
const updated = {
|
|
231
|
+
if (!existing) throw new NotFoundError('update', { kta: ['organization', '', '', '', '', ''], scopes: [] }, key);
|
|
232
|
+
const updated = {
|
|
233
|
+
...existing,
|
|
234
|
+
...updates,
|
|
235
|
+
events: {
|
|
236
|
+
...existing.events,
|
|
237
|
+
updated: { at: new Date() }
|
|
238
|
+
}
|
|
239
|
+
};
|
|
227
240
|
mockOrgStorage.set(String(key.pk), updated);
|
|
228
241
|
return updated;
|
|
229
242
|
},
|
|
230
243
|
async remove(key: PriKey<'organization'>) {
|
|
231
|
-
|
|
244
|
+
const existing = mockOrgStorage.get(String(key.pk));
|
|
245
|
+
if (!existing) throw new NotFoundError('remove', { kta: ['organization', '', '', '', '', ''], scopes: [] }, key);
|
|
246
|
+
mockOrgStorage.delete(String(key.pk));
|
|
247
|
+
return existing;
|
|
232
248
|
},
|
|
233
249
|
async find(finder: string, params: any) {
|
|
234
250
|
const orgs = Array.from(mockOrgStorage.values());
|
|
@@ -244,23 +260,35 @@ const createOrgOperations = () => ({
|
|
|
244
260
|
});
|
|
245
261
|
|
|
246
262
|
const createDeptOperations = () => ({
|
|
247
|
-
async all() {
|
|
248
|
-
|
|
263
|
+
async all(query?: any, locations?: any) {
|
|
264
|
+
const departments = Array.from(mockDeptStorage.values());
|
|
265
|
+
if (locations && locations.length >= 1) {
|
|
266
|
+
const orgId = String(locations[0]?.lk || '');
|
|
267
|
+
// Check if the parent organization exists
|
|
268
|
+
const org = mockOrgStorage.get(orgId);
|
|
269
|
+
if (!org) {
|
|
270
|
+
throw new NotFoundError('get', { kta: ['organization', '', '', '', '', ''], scopes: [] }, { kt: 'organization', pk: orgId as UUID });
|
|
271
|
+
}
|
|
272
|
+
return departments.filter(dept => dept.organizationId === orgId);
|
|
273
|
+
}
|
|
274
|
+
return departments;
|
|
249
275
|
},
|
|
250
276
|
async get(key: ComKey<'department', 'organization'>) {
|
|
251
277
|
const dept = mockDeptStorage.get(String(key.pk));
|
|
252
|
-
if (!dept) throw new
|
|
278
|
+
if (!dept) throw new NotFoundError('get', { kta: ['department', '', '', '', '', ''], scopes: [] }, key);
|
|
253
279
|
return dept;
|
|
254
280
|
},
|
|
255
|
-
async create(item: Department) {
|
|
281
|
+
async create(item: Department, context?: any) {
|
|
256
282
|
const id = `dept-${Date.now()}`;
|
|
283
|
+
const orgId = context?.locations?.[0]?.lk || item.organizationId || String((item.key as any)?.loc?.[0]?.lk || '');
|
|
257
284
|
const newDept: Department = {
|
|
258
285
|
...item,
|
|
259
286
|
id,
|
|
287
|
+
organizationId: orgId,
|
|
260
288
|
key: {
|
|
261
289
|
kt: 'department',
|
|
262
290
|
pk: id as UUID,
|
|
263
|
-
loc: (item.key as any)?.loc || []
|
|
291
|
+
loc: context?.locations || (item.key as any)?.loc || []
|
|
264
292
|
},
|
|
265
293
|
events: {
|
|
266
294
|
created: { at: new Date() },
|
|
@@ -273,45 +301,90 @@ const createDeptOperations = () => ({
|
|
|
273
301
|
},
|
|
274
302
|
async update(key: ComKey<'department', 'organization'>, updates: Partial<Department>) {
|
|
275
303
|
const existing = mockDeptStorage.get(String(key.pk));
|
|
276
|
-
if (!existing) throw new
|
|
277
|
-
const updated = {
|
|
304
|
+
if (!existing) throw new NotFoundError('update', { kta: ['department', '', '', '', '', ''], scopes: [] }, key);
|
|
305
|
+
const updated = {
|
|
306
|
+
...existing,
|
|
307
|
+
...updates,
|
|
308
|
+
events: {
|
|
309
|
+
...existing.events,
|
|
310
|
+
updated: { at: new Date() }
|
|
311
|
+
}
|
|
312
|
+
};
|
|
278
313
|
mockDeptStorage.set(String(key.pk), updated);
|
|
279
314
|
return updated;
|
|
280
315
|
},
|
|
281
316
|
async remove(key: ComKey<'department', 'organization'>) {
|
|
282
|
-
|
|
317
|
+
const existing = mockDeptStorage.get(String(key.pk));
|
|
318
|
+
if (!existing) throw new NotFoundError('remove', { kta: ['department', '', '', '', '', ''], scopes: [] }, key);
|
|
319
|
+
mockDeptStorage.delete(String(key.pk));
|
|
320
|
+
return existing;
|
|
283
321
|
},
|
|
284
|
-
async find(finder: string, params: any) {
|
|
285
|
-
const
|
|
322
|
+
async find(finder: string, params: any, locations?: any) {
|
|
323
|
+
const departments = Array.from(mockDeptStorage.values());
|
|
324
|
+
let filtered = departments;
|
|
325
|
+
|
|
326
|
+
if (locations && locations.length >= 1) {
|
|
327
|
+
const orgId = String(locations[0]?.lk || '');
|
|
328
|
+
filtered = filtered.filter(dept => dept.organizationId === orgId);
|
|
329
|
+
}
|
|
330
|
+
|
|
286
331
|
switch (finder) {
|
|
287
|
-
case '
|
|
288
|
-
return
|
|
289
|
-
case '
|
|
290
|
-
return
|
|
332
|
+
case 'byBudget':
|
|
333
|
+
return filtered.filter(dept => dept.budget >= params.minBudget);
|
|
334
|
+
case 'byHeadCount':
|
|
335
|
+
return filtered.filter(dept => dept.headCount >= params.minHeadCount);
|
|
291
336
|
default:
|
|
292
|
-
return
|
|
337
|
+
return filtered;
|
|
293
338
|
}
|
|
294
339
|
}
|
|
295
340
|
});
|
|
296
341
|
|
|
297
342
|
const createEmpOperations = () => ({
|
|
298
|
-
async all() {
|
|
299
|
-
|
|
343
|
+
async all(query?: any, locations?: any) {
|
|
344
|
+
const employees = Array.from(mockEmpStorage.values());
|
|
345
|
+
if (locations && locations.length >= 2) {
|
|
346
|
+
// The locations array is: [{kt: 'department', lk: 'dept-1'}, {kt: 'organization', lk: 'org-1'}]
|
|
347
|
+
const deptId = String(locations[0]?.lk || '');
|
|
348
|
+
const orgId = String(locations[1]?.lk || '');
|
|
349
|
+
|
|
350
|
+
// Check if the parent organization exists
|
|
351
|
+
const org = mockOrgStorage.get(orgId);
|
|
352
|
+
if (!org) {
|
|
353
|
+
throw new NotFoundError('get', { kta: ['organization', '', '', '', '', ''], scopes: [] }, { kt: 'organization', pk: orgId as UUID });
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Check if the parent department exists
|
|
357
|
+
const dept = mockDeptStorage.get(deptId);
|
|
358
|
+
if (!dept) {
|
|
359
|
+
throw new NotFoundError('get', { kta: ['department', '', '', '', '', ''], scopes: [] }, { kt: 'department', pk: deptId as UUID });
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return employees.filter(emp => emp.organizationId === orgId && emp.departmentId === deptId);
|
|
363
|
+
}
|
|
364
|
+
return employees;
|
|
300
365
|
},
|
|
301
366
|
async get(key: ComKey<'employee', 'organization', 'department'>) {
|
|
302
367
|
const emp = mockEmpStorage.get(String(key.pk));
|
|
303
|
-
if (!emp) throw new
|
|
368
|
+
if (!emp) throw new NotFoundError('get', { kta: ['employee', '', '', '', '', ''], scopes: [] }, key);
|
|
304
369
|
return emp;
|
|
305
370
|
},
|
|
306
|
-
async create(item: Employee) {
|
|
371
|
+
async create(item: Employee, context?: any) {
|
|
307
372
|
const id = `emp-${Date.now()}`;
|
|
373
|
+
|
|
374
|
+
// Extract organization and department IDs from the locations context
|
|
375
|
+
// The locations array is: [{kt: 'department', lk: 'dept-1'}, {kt: 'organization', lk: 'org-1'}]
|
|
376
|
+
const deptId = context?.locations?.[0]?.lk || item.departmentId || String((item.key as any)?.loc?.[0]?.lk || '');
|
|
377
|
+
const orgId = context?.locations?.[1]?.lk || item.organizationId || String((item.key as any)?.loc?.[1]?.lk || '');
|
|
378
|
+
|
|
308
379
|
const newEmp: Employee = {
|
|
309
380
|
...item,
|
|
310
381
|
id,
|
|
382
|
+
organizationId: orgId,
|
|
383
|
+
departmentId: deptId,
|
|
311
384
|
key: {
|
|
312
385
|
kt: 'employee',
|
|
313
386
|
pk: id as UUID,
|
|
314
|
-
loc: (item.key as any)?.loc || []
|
|
387
|
+
loc: context?.locations || (item.key as any)?.loc || []
|
|
315
388
|
},
|
|
316
389
|
events: {
|
|
317
390
|
created: { at: new Date() },
|
|
@@ -324,23 +397,41 @@ const createEmpOperations = () => ({
|
|
|
324
397
|
},
|
|
325
398
|
async update(key: ComKey<'employee', 'organization', 'department'>, updates: Partial<Employee>) {
|
|
326
399
|
const existing = mockEmpStorage.get(String(key.pk));
|
|
327
|
-
if (!existing) throw new
|
|
328
|
-
const updated = {
|
|
400
|
+
if (!existing) throw new NotFoundError('update', { kta: ['employee', '', '', '', '', ''], scopes: [] }, key);
|
|
401
|
+
const updated = {
|
|
402
|
+
...existing,
|
|
403
|
+
...updates,
|
|
404
|
+
events: {
|
|
405
|
+
...existing.events,
|
|
406
|
+
updated: { at: new Date() }
|
|
407
|
+
}
|
|
408
|
+
};
|
|
329
409
|
mockEmpStorage.set(String(key.pk), updated);
|
|
330
410
|
return updated;
|
|
331
411
|
},
|
|
332
412
|
async remove(key: ComKey<'employee', 'organization', 'department'>) {
|
|
333
|
-
|
|
413
|
+
const existing = mockEmpStorage.get(String(key.pk));
|
|
414
|
+
if (!existing) throw new NotFoundError('remove', { kta: ['employee', '', '', '', '', ''], scopes: [] }, key);
|
|
415
|
+
mockEmpStorage.delete(String(key.pk));
|
|
416
|
+
return existing;
|
|
334
417
|
},
|
|
335
|
-
async find(finder: string, params: any) {
|
|
418
|
+
async find(finder: string, params: any, locations?: any) {
|
|
336
419
|
const employees = Array.from(mockEmpStorage.values());
|
|
420
|
+
let filtered = employees;
|
|
421
|
+
|
|
422
|
+
if (locations && locations.length >= 2) {
|
|
423
|
+
const deptId = String(locations[0]?.lk || '');
|
|
424
|
+
const orgId = String(locations[1]?.lk || '');
|
|
425
|
+
filtered = filtered.filter(emp => emp.organizationId === orgId && emp.departmentId === deptId);
|
|
426
|
+
}
|
|
427
|
+
|
|
337
428
|
switch (finder) {
|
|
338
429
|
case 'byDepartment':
|
|
339
|
-
return
|
|
430
|
+
return filtered.filter(emp => emp.departmentId === params.departmentId);
|
|
340
431
|
case 'byPosition':
|
|
341
|
-
return
|
|
432
|
+
return filtered.filter(emp => emp.position.includes(params.position));
|
|
342
433
|
default:
|
|
343
|
-
return
|
|
434
|
+
return filtered;
|
|
344
435
|
}
|
|
345
436
|
}
|
|
346
437
|
});
|
|
@@ -389,26 +480,26 @@ export const runNestedRouterExample = async (): Promise<{ app: Application }> =>
|
|
|
389
480
|
// Mount the routers to create nested endpoints:
|
|
390
481
|
// Primary routes for organizations:
|
|
391
482
|
// GET /api/organizations - Get all organizations
|
|
392
|
-
// GET /api/organizations/:
|
|
483
|
+
// GET /api/organizations/:organizationPk - Get specific organization
|
|
393
484
|
// POST /api/organizations - Create new organization
|
|
394
|
-
// PUT /api/organizations/:
|
|
395
|
-
// DELETE /api/organizations/:
|
|
485
|
+
// PUT /api/organizations/:organizationPk - Update organization
|
|
486
|
+
// DELETE /api/organizations/:organizationPk - Delete organization
|
|
396
487
|
app.use('/api/organizations', orgRouter.getRouter());
|
|
397
488
|
|
|
398
489
|
// Nested routes for departments within organizations:
|
|
399
|
-
// GET /api/organizations/:
|
|
400
|
-
// GET /api/organizations/:
|
|
401
|
-
// POST /api/organizations/:
|
|
402
|
-
// PUT /api/organizations/:
|
|
403
|
-
// DELETE /api/organizations/:
|
|
490
|
+
// GET /api/organizations/:organizationPk/departments - Get all departments for organization
|
|
491
|
+
// GET /api/organizations/:organizationPk/departments/:departmentPk - Get specific department
|
|
492
|
+
// POST /api/organizations/:organizationPk/departments - Create new department in organization
|
|
493
|
+
// PUT /api/organizations/:organizationPk/departments/:departmentPk - Update department
|
|
494
|
+
// DELETE /api/organizations/:organizationPk/departments/:departmentPk - Delete department
|
|
404
495
|
app.use('/api/organizations/:organizationPk/departments', deptRouter.getRouter());
|
|
405
496
|
|
|
406
497
|
// Deeply nested routes for employees within departments within organizations:
|
|
407
|
-
// GET /api/organizations/:
|
|
408
|
-
// GET /api/organizations/:
|
|
409
|
-
// POST /api/organizations/:
|
|
410
|
-
// PUT /api/organizations/:
|
|
411
|
-
// DELETE /api/organizations/:
|
|
498
|
+
// GET /api/organizations/:organizationPk/departments/:departmentPk/employees - Get all employees for department
|
|
499
|
+
// GET /api/organizations/:organizationPk/departments/:departmentPk/employees/:employeePk - Get specific employee
|
|
500
|
+
// POST /api/organizations/:organizationPk/departments/:departmentPk/employees - Create new employee in department
|
|
501
|
+
// PUT /api/organizations/:organizationPk/departments/:departmentPk/employees/:employeePk - Update employee
|
|
502
|
+
// DELETE /api/organizations/:organizationPk/departments/:departmentPk/employees/:employeePk - Delete employee
|
|
412
503
|
app.use('/api/organizations/:organizationPk/departments/:departmentPk/employees', empRouter.getRouter());
|
|
413
504
|
|
|
414
505
|
// Additional hierarchy summary routes
|
|
@@ -470,16 +561,25 @@ export const runNestedRouterExample = async (): Promise<{ app: Application }> =>
|
|
|
470
561
|
console.log(' 📈 GET /api/stats - Statistics summary');
|
|
471
562
|
console.log(' 🏢 Organizations (Primary):');
|
|
472
563
|
console.log(' GET /api/organizations - List all organizations');
|
|
473
|
-
console.log(' GET /api/organizations/:
|
|
564
|
+
console.log(' GET /api/organizations/:organizationPk - Get specific organization');
|
|
474
565
|
console.log(' POST /api/organizations - Create new organization');
|
|
475
566
|
console.log(' 🏬 Departments (Contained in Organizations):');
|
|
476
|
-
console.log(' GET /api/organizations/:
|
|
477
|
-
console.log(' GET /api/organizations/:
|
|
478
|
-
console.log(' POST /api/organizations/:
|
|
567
|
+
console.log(' GET /api/organizations/:organizationPk/departments - List departments for organization');
|
|
568
|
+
console.log(' GET /api/organizations/:organizationPk/departments/:departmentPk - Get specific department');
|
|
569
|
+
console.log(' POST /api/organizations/:organizationPk/departments - Create department in organization');
|
|
479
570
|
console.log(' 👥 Employees (Contained in Departments):');
|
|
480
|
-
console.log(' GET /api/organizations/:
|
|
481
|
-
console.log(' GET /api/organizations/:
|
|
482
|
-
console.log(' POST /api/organizations/:
|
|
571
|
+
console.log(' GET /api/organizations/:organizationPk/departments/:departmentPk/employees - List employees for department');
|
|
572
|
+
console.log(' GET /api/organizations/:organizationPk/departments/:departmentPk/employees/:employeePk - Get specific employee');
|
|
573
|
+
console.log(' POST /api/organizations/:organizationPk/departments/:departmentPk/employees - Create employee in department');
|
|
574
|
+
|
|
575
|
+
// Error handling middleware
|
|
576
|
+
app.use((err: any, req: any, res: any, next: any) => {
|
|
577
|
+
console.error('Error:', err.message);
|
|
578
|
+
if (err instanceof NotFoundError || err.message.includes('not found') || err.message.includes('NotFoundError')) {
|
|
579
|
+
return res.status(404).json({ error: err.message });
|
|
580
|
+
}
|
|
581
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
582
|
+
});
|
|
483
583
|
|
|
484
584
|
return { app };
|
|
485
585
|
};
|