@fjell/express-router 4.4.56 → 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.
@@ -1,601 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-unused-vars */
2
- /**
3
- * Nested Router Example
4
- *
5
- * This example demonstrates the usage of fjell-express-router for hierarchical data structures
6
- * using CItemRouter (Contained Item Router) alongside PItemRouter (Primary Item Router).
7
- * It shows how to create nested routes that handle parent-child relationships in data models.
8
- *
9
- * Perfect for understanding how to handle complex data hierarchies with fjell-express-router.
10
- *
11
- * Run this example with: npx tsx examples/nested-router-example.ts
12
- */
13
-
14
- import express, { Application } from 'express';
15
- import { ComKey, Item, PriKey, UUID } from '@fjell/core';
16
- import { CItemRouter, createRegistry, PItemRouter } from '../src';
17
- import { NotFoundError } from '@fjell/lib';
18
-
19
- // Define our hierarchical data models
20
- export interface Organization extends Item<'organization'> {
21
- id: string;
22
- name: string;
23
- type: 'startup' | 'enterprise' | 'nonprofit';
24
- founded: Date;
25
- industry: string;
26
- }
27
-
28
- export interface Department extends Item<'department', 'organization'> {
29
- id: string;
30
- name: string;
31
- budget: number;
32
- headCount: number;
33
- organizationId: string;
34
- }
35
-
36
- export interface Employee extends Item<'employee', 'organization', 'department'> {
37
- id: string;
38
- name: string;
39
- email: string;
40
- position: string;
41
- salary: number;
42
- hireDate: Date;
43
- organizationId: string;
44
- departmentId: string;
45
- }
46
-
47
- // Mock storage
48
- const mockOrgStorage = new Map<string, Organization>();
49
- const mockDeptStorage = new Map<string, Department>();
50
- const mockEmpStorage = new Map<string, Employee>();
51
-
52
- // Initialize sample data
53
- const initializeSampleData = () => {
54
- // Organizations
55
- const orgs: Organization[] = [
56
- {
57
- key: { kt: 'organization', pk: 'org-1' as UUID },
58
- id: 'org-1',
59
- name: 'TechCorp Solutions',
60
- type: 'enterprise',
61
- founded: new Date('2010-03-15'),
62
- industry: 'Technology',
63
- events: {
64
- created: { at: new Date('2010-03-15') },
65
- updated: { at: new Date() },
66
- deleted: { at: null }
67
- }
68
- },
69
- {
70
- key: { kt: 'organization', pk: 'org-2' as UUID },
71
- id: 'org-2',
72
- name: 'Green Future Initiative',
73
- type: 'nonprofit',
74
- founded: new Date('2018-07-22'),
75
- industry: 'Environmental',
76
- events: {
77
- created: { at: new Date('2018-07-22') },
78
- updated: { at: new Date() },
79
- deleted: { at: null }
80
- }
81
- }
82
- ];
83
-
84
- // Departments
85
- const depts: Department[] = [
86
- {
87
- key: {
88
- kt: 'department',
89
- pk: 'dept-1' as UUID,
90
- loc: [{ kt: 'organization', lk: 'org-1' }]
91
- },
92
- id: 'dept-1',
93
- name: 'Engineering',
94
- budget: 2000000,
95
- headCount: 25,
96
- organizationId: 'org-1',
97
- events: {
98
- created: { at: new Date('2010-04-01') },
99
- updated: { at: new Date() },
100
- deleted: { at: null }
101
- }
102
- },
103
- {
104
- key: {
105
- kt: 'department',
106
- pk: 'dept-2' as UUID,
107
- loc: [{ kt: 'organization', lk: 'org-1' }]
108
- },
109
- id: 'dept-2',
110
- name: 'Marketing',
111
- budget: 800000,
112
- headCount: 12,
113
- organizationId: 'org-1',
114
- events: {
115
- created: { at: new Date('2010-05-15') },
116
- updated: { at: new Date() },
117
- deleted: { at: null }
118
- }
119
- },
120
- {
121
- key: {
122
- kt: 'department',
123
- pk: 'dept-3' as UUID,
124
- loc: [{ kt: 'organization', lk: 'org-2' }]
125
- },
126
- id: 'dept-3',
127
- name: 'Research',
128
- budget: 500000,
129
- headCount: 8,
130
- organizationId: 'org-2',
131
- events: {
132
- created: { at: new Date('2018-08-01') },
133
- updated: { at: new Date() },
134
- deleted: { at: null }
135
- }
136
- }
137
- ];
138
-
139
- // Employees
140
- const employees: Employee[] = [
141
- {
142
- key: {
143
- kt: 'employee',
144
- pk: 'emp-1' as UUID,
145
- loc: [
146
- { kt: 'organization', lk: 'org-1' },
147
- { kt: 'department', lk: 'dept-1' }
148
- ]
149
- },
150
- id: 'emp-1',
151
- name: 'Alice Johnson',
152
- email: 'alice.johnson@techcorp.com',
153
- position: 'Senior Software Engineer',
154
- salary: 120000,
155
- hireDate: new Date('2015-09-01'),
156
- organizationId: 'org-1',
157
- departmentId: 'dept-1',
158
- events: {
159
- created: { at: new Date('2015-09-01') },
160
- updated: { at: new Date() },
161
- deleted: { at: null }
162
- }
163
- },
164
- {
165
- key: {
166
- kt: 'employee',
167
- pk: 'emp-2' as UUID,
168
- loc: [
169
- { kt: 'organization', lk: 'org-1' },
170
- { kt: 'department', lk: 'dept-2' }
171
- ]
172
- },
173
- id: 'emp-2',
174
- name: 'Bob Smith',
175
- email: 'bob.smith@techcorp.com',
176
- position: 'Marketing Manager',
177
- salary: 85000,
178
- hireDate: new Date('2017-03-15'),
179
- organizationId: 'org-1',
180
- departmentId: 'dept-2',
181
- events: {
182
- created: { at: new Date('2017-03-15') },
183
- updated: { at: new Date() },
184
- deleted: { at: null }
185
- }
186
- }
187
- ];
188
-
189
- orgs.forEach(org => mockOrgStorage.set(org.id, org));
190
- depts.forEach(dept => mockDeptStorage.set(dept.id, dept));
191
- employees.forEach(emp => mockEmpStorage.set(emp.id, emp));
192
-
193
- console.log('šŸ“¦ Initialized hierarchical sample data:');
194
- console.log(` Organizations: ${orgs.length} records`);
195
- console.log(` Departments: ${depts.length} records`);
196
- console.log(` Employees: ${employees.length} records`);
197
- };
198
-
199
- // Create mock operations
200
- const createOrgOperations = () => ({
201
- async all() {
202
- return Array.from(mockOrgStorage.values());
203
- },
204
- async get(key: PriKey<'organization'>) {
205
- const org = mockOrgStorage.get(String(key.pk));
206
- if (!org) throw new NotFoundError('get', { kta: ['organization', '', '', '', '', ''], scopes: [] }, key);
207
- return org;
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
-
215
- const id = `org-${Date.now()}`;
216
- const newOrg: Organization = {
217
- ...item,
218
- id,
219
- key: { kt: 'organization', pk: id as UUID },
220
- events: {
221
- created: { at: new Date() },
222
- updated: { at: new Date() },
223
- deleted: { at: null }
224
- }
225
- };
226
- mockOrgStorage.set(id, newOrg);
227
- return newOrg;
228
- },
229
- async update(key: PriKey<'organization'>, updates: Partial<Organization>) {
230
- const existing = mockOrgStorage.get(String(key.pk));
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
- };
240
- mockOrgStorage.set(String(key.pk), updated);
241
- return updated;
242
- },
243
- async remove(key: PriKey<'organization'>) {
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;
248
- },
249
- async find(finder: string, params: any) {
250
- const orgs = Array.from(mockOrgStorage.values());
251
- switch (finder) {
252
- case 'byType':
253
- return orgs.filter(org => org.type === params.type);
254
- case 'byIndustry':
255
- return orgs.filter(org => org.industry === params.industry);
256
- default:
257
- return orgs;
258
- }
259
- }
260
- });
261
-
262
- const createDeptOperations = () => ({
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;
275
- },
276
- async get(key: ComKey<'department', 'organization'>) {
277
- const dept = mockDeptStorage.get(String(key.pk));
278
- if (!dept) throw new NotFoundError('get', { kta: ['department', '', '', '', '', ''], scopes: [] }, key);
279
- return dept;
280
- },
281
- async create(item: Department, context?: any) {
282
- const id = `dept-${Date.now()}`;
283
- const orgId = context?.locations?.[0]?.lk || item.organizationId || String((item.key as any)?.loc?.[0]?.lk || '');
284
- const newDept: Department = {
285
- ...item,
286
- id,
287
- organizationId: orgId,
288
- key: {
289
- kt: 'department',
290
- pk: id as UUID,
291
- loc: context?.locations || (item.key as any)?.loc || []
292
- },
293
- events: {
294
- created: { at: new Date() },
295
- updated: { at: new Date() },
296
- deleted: { at: null }
297
- }
298
- };
299
- mockDeptStorage.set(id, newDept);
300
- return newDept;
301
- },
302
- async update(key: ComKey<'department', 'organization'>, updates: Partial<Department>) {
303
- const existing = mockDeptStorage.get(String(key.pk));
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
- };
313
- mockDeptStorage.set(String(key.pk), updated);
314
- return updated;
315
- },
316
- async remove(key: ComKey<'department', 'organization'>) {
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;
321
- },
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
-
331
- switch (finder) {
332
- case 'byBudget':
333
- return filtered.filter(dept => dept.budget >= params.minBudget);
334
- case 'byHeadCount':
335
- return filtered.filter(dept => dept.headCount >= params.minHeadCount);
336
- default:
337
- return filtered;
338
- }
339
- }
340
- });
341
-
342
- const createEmpOperations = () => ({
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;
365
- },
366
- async get(key: ComKey<'employee', 'organization', 'department'>) {
367
- const emp = mockEmpStorage.get(String(key.pk));
368
- if (!emp) throw new NotFoundError('get', { kta: ['employee', '', '', '', '', ''], scopes: [] }, key);
369
- return emp;
370
- },
371
- async create(item: Employee, context?: any) {
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
-
379
- const newEmp: Employee = {
380
- ...item,
381
- id,
382
- organizationId: orgId,
383
- departmentId: deptId,
384
- key: {
385
- kt: 'employee',
386
- pk: id as UUID,
387
- loc: context?.locations || (item.key as any)?.loc || []
388
- },
389
- events: {
390
- created: { at: new Date() },
391
- updated: { at: new Date() },
392
- deleted: { at: null }
393
- }
394
- };
395
- mockEmpStorage.set(id, newEmp);
396
- return newEmp;
397
- },
398
- async update(key: ComKey<'employee', 'organization', 'department'>, updates: Partial<Employee>) {
399
- const existing = mockEmpStorage.get(String(key.pk));
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
- };
409
- mockEmpStorage.set(String(key.pk), updated);
410
- return updated;
411
- },
412
- async remove(key: ComKey<'employee', 'organization', 'department'>) {
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;
417
- },
418
- async find(finder: string, params: any, locations?: any) {
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
-
428
- switch (finder) {
429
- case 'byDepartment':
430
- return filtered.filter(emp => emp.departmentId === params.departmentId);
431
- case 'byPosition':
432
- return filtered.filter(emp => emp.position.includes(params.position));
433
- default:
434
- return filtered;
435
- }
436
- }
437
- });
438
-
439
- /**
440
- * Main function demonstrating nested fjell-express-router usage
441
- */
442
- export const runNestedRouterExample = async (): Promise<{ app: Application }> => {
443
- console.log('šŸš€ Starting Nested Express Router Example...\n');
444
-
445
- initializeSampleData();
446
-
447
- const registry = createRegistry();
448
-
449
- // Create mock instances
450
- const mockOrgInstance = {
451
- operations: createOrgOperations(),
452
- options: {}
453
- } as any;
454
-
455
- const mockDeptInstance = {
456
- operations: createDeptOperations(),
457
- options: {}
458
- } as any;
459
-
460
- const mockEmpInstance = {
461
- operations: createEmpOperations(),
462
- options: {}
463
- } as any;
464
-
465
- const app: Application = express();
466
- app.use(express.json());
467
-
468
- // Request logging middleware
469
- app.use((req, res, next) => {
470
- console.log(`🌐 ${req.method} ${req.path}`, req.query);
471
- next();
472
- });
473
-
474
- // Create routers
475
- console.log('šŸ›¤ļø Creating nested Express routers...');
476
- const orgRouter = new PItemRouter(mockOrgInstance, 'organization');
477
- const deptRouter = new CItemRouter(mockDeptInstance, 'department', orgRouter);
478
- const empRouter = new CItemRouter(mockEmpInstance, 'employee', deptRouter);
479
-
480
- // Mount the routers to create nested endpoints:
481
- // Primary routes for organizations:
482
- // GET /api/organizations - Get all organizations
483
- // GET /api/organizations/:organizationPk - Get specific organization
484
- // POST /api/organizations - Create new organization
485
- // PUT /api/organizations/:organizationPk - Update organization
486
- // DELETE /api/organizations/:organizationPk - Delete organization
487
- app.use('/api/organizations', orgRouter.getRouter());
488
-
489
- // Nested routes for departments within 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
495
- app.use('/api/organizations/:organizationPk/departments', deptRouter.getRouter());
496
-
497
- // Deeply nested routes for employees within departments within 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
503
- app.use('/api/organizations/:organizationPk/departments/:departmentPk/employees', empRouter.getRouter());
504
-
505
- // Additional hierarchy summary routes
506
- app.get('/api/hierarchy', async (req, res) => {
507
- try {
508
- const orgs = await mockOrgInstance.operations.all();
509
- const depts = await mockDeptInstance.operations.all();
510
- const employees = await mockEmpInstance.operations.all();
511
-
512
- const hierarchy = orgs.map((org: Organization) => {
513
- const orgDepts = depts.filter((dept: Department) => dept.organizationId === org.id);
514
- return {
515
- organization: org,
516
- departments: orgDepts.map((dept: Department) => {
517
- const deptEmployees = employees.filter((emp: Employee) => emp.departmentId === dept.id);
518
- return {
519
- department: dept,
520
- employees: deptEmployees
521
- };
522
- })
523
- };
524
- });
525
-
526
- res.json(hierarchy);
527
- } catch (error) {
528
- res.status(500).json({ error: 'Failed to load hierarchy' });
529
- }
530
- });
531
-
532
- app.get('/api/stats', async (req, res) => {
533
- try {
534
- const orgs = await mockOrgInstance.operations.all();
535
- const depts = await mockDeptInstance.operations.all();
536
- const employees = await mockEmpInstance.operations.all();
537
-
538
- const stats = {
539
- totals: {
540
- organizations: orgs.length,
541
- departments: depts.length,
542
- employees: employees.length
543
- },
544
- organizationTypes: orgs.reduce((acc: any, org: Organization) => {
545
- acc[org.type] = (acc[org.type] || 0) + 1;
546
- return acc;
547
- }, {}),
548
- averageDepartmentBudget: depts.reduce((sum: number, dept: Department) => sum + dept.budget, 0) / depts.length,
549
- totalPayroll: employees.reduce((sum: number, emp: Employee) => sum + emp.salary, 0)
550
- };
551
-
552
- res.json(stats);
553
- } catch (error) {
554
- res.status(500).json({ error: 'Failed to calculate stats' });
555
- }
556
- });
557
-
558
- console.log('\nāœ… Nested Express Router Example setup complete!');
559
- console.log('\nšŸ“š Available nested endpoints:');
560
- console.log(' šŸ“Š GET /api/hierarchy - Full organizational hierarchy');
561
- console.log(' šŸ“ˆ GET /api/stats - Statistics summary');
562
- console.log(' šŸ¢ Organizations (Primary):');
563
- console.log(' GET /api/organizations - List all organizations');
564
- console.log(' GET /api/organizations/:organizationPk - Get specific organization');
565
- console.log(' POST /api/organizations - Create new organization');
566
- console.log(' šŸ¬ Departments (Contained in 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');
570
- console.log(' šŸ‘„ Employees (Contained in Departments):');
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
- });
583
-
584
- return { app };
585
- };
586
-
587
- // If this file is run directly, start the server
588
- if (import.meta.url === `file://${process.argv[1]}`) {
589
- runNestedRouterExample().then(({ app }) => {
590
- const PORT = process.env.PORT || 3002;
591
- app.listen(PORT, () => {
592
- console.log(`\n🌟 Server running on http://localhost:${PORT}`);
593
- console.log('\nšŸ’” Try these example requests:');
594
- console.log(` curl http://localhost:${PORT}/api/hierarchy`);
595
- console.log(` curl http://localhost:${PORT}/api/stats`);
596
- console.log(` curl http://localhost:${PORT}/api/organizations`);
597
- console.log(` curl http://localhost:${PORT}/api/organizations/org-1/departments`);
598
- console.log(` curl http://localhost:${PORT}/api/organizations/org-1/departments/dept-1/employees`);
599
- });
600
- }).catch(console.error);
601
- }