@fjell/express-router 4.4.7 → 4.4.10

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.
@@ -0,0 +1,501 @@
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
+
18
+ // Define our hierarchical data models
19
+ interface Organization extends Item<'organization'> {
20
+ id: string;
21
+ name: string;
22
+ type: 'startup' | 'enterprise' | 'nonprofit';
23
+ founded: Date;
24
+ industry: string;
25
+ }
26
+
27
+ interface Department extends Item<'department', 'organization'> {
28
+ id: string;
29
+ name: string;
30
+ budget: number;
31
+ headCount: number;
32
+ organizationId: string;
33
+ }
34
+
35
+ interface Employee extends Item<'employee', 'organization', 'department'> {
36
+ id: string;
37
+ name: string;
38
+ email: string;
39
+ position: string;
40
+ salary: number;
41
+ hireDate: Date;
42
+ organizationId: string;
43
+ departmentId: string;
44
+ }
45
+
46
+ // Mock storage
47
+ const mockOrgStorage = new Map<string, Organization>();
48
+ const mockDeptStorage = new Map<string, Department>();
49
+ const mockEmpStorage = new Map<string, Employee>();
50
+
51
+ // Initialize sample data
52
+ const initializeSampleData = () => {
53
+ // Organizations
54
+ const orgs: Organization[] = [
55
+ {
56
+ key: { kt: 'organization', pk: 'org-1' as UUID },
57
+ id: 'org-1',
58
+ name: 'TechCorp Solutions',
59
+ type: 'enterprise',
60
+ founded: new Date('2010-03-15'),
61
+ industry: 'Technology',
62
+ events: {
63
+ created: { at: new Date('2010-03-15') },
64
+ updated: { at: new Date() },
65
+ deleted: { at: null }
66
+ }
67
+ },
68
+ {
69
+ key: { kt: 'organization', pk: 'org-2' as UUID },
70
+ id: 'org-2',
71
+ name: 'Green Future Initiative',
72
+ type: 'nonprofit',
73
+ founded: new Date('2018-07-22'),
74
+ industry: 'Environmental',
75
+ events: {
76
+ created: { at: new Date('2018-07-22') },
77
+ updated: { at: new Date() },
78
+ deleted: { at: null }
79
+ }
80
+ }
81
+ ];
82
+
83
+ // Departments
84
+ const depts: Department[] = [
85
+ {
86
+ key: {
87
+ kt: 'department',
88
+ pk: 'dept-1' as UUID,
89
+ loc: [{ kt: 'organization', lk: 'org-1' }]
90
+ },
91
+ id: 'dept-1',
92
+ name: 'Engineering',
93
+ budget: 2000000,
94
+ headCount: 25,
95
+ organizationId: 'org-1',
96
+ events: {
97
+ created: { at: new Date('2010-04-01') },
98
+ updated: { at: new Date() },
99
+ deleted: { at: null }
100
+ }
101
+ },
102
+ {
103
+ key: {
104
+ kt: 'department',
105
+ pk: 'dept-2' as UUID,
106
+ loc: [{ kt: 'organization', lk: 'org-1' }]
107
+ },
108
+ id: 'dept-2',
109
+ name: 'Marketing',
110
+ budget: 800000,
111
+ headCount: 12,
112
+ organizationId: 'org-1',
113
+ events: {
114
+ created: { at: new Date('2010-05-15') },
115
+ updated: { at: new Date() },
116
+ deleted: { at: null }
117
+ }
118
+ },
119
+ {
120
+ key: {
121
+ kt: 'department',
122
+ pk: 'dept-3' as UUID,
123
+ loc: [{ kt: 'organization', lk: 'org-2' }]
124
+ },
125
+ id: 'dept-3',
126
+ name: 'Research',
127
+ budget: 500000,
128
+ headCount: 8,
129
+ organizationId: 'org-2',
130
+ events: {
131
+ created: { at: new Date('2018-08-01') },
132
+ updated: { at: new Date() },
133
+ deleted: { at: null }
134
+ }
135
+ }
136
+ ];
137
+
138
+ // Employees
139
+ const employees: Employee[] = [
140
+ {
141
+ key: {
142
+ kt: 'employee',
143
+ pk: 'emp-1' as UUID,
144
+ loc: [
145
+ { kt: 'organization', lk: 'org-1' },
146
+ { kt: 'department', lk: 'dept-1' }
147
+ ]
148
+ },
149
+ id: 'emp-1',
150
+ name: 'Alice Johnson',
151
+ email: 'alice.johnson@techcorp.com',
152
+ position: 'Senior Software Engineer',
153
+ salary: 120000,
154
+ hireDate: new Date('2015-09-01'),
155
+ organizationId: 'org-1',
156
+ departmentId: 'dept-1',
157
+ events: {
158
+ created: { at: new Date('2015-09-01') },
159
+ updated: { at: new Date() },
160
+ deleted: { at: null }
161
+ }
162
+ },
163
+ {
164
+ key: {
165
+ kt: 'employee',
166
+ pk: 'emp-2' as UUID,
167
+ loc: [
168
+ { kt: 'organization', lk: 'org-1' },
169
+ { kt: 'department', lk: 'dept-2' }
170
+ ]
171
+ },
172
+ id: 'emp-2',
173
+ name: 'Bob Smith',
174
+ email: 'bob.smith@techcorp.com',
175
+ position: 'Marketing Manager',
176
+ salary: 85000,
177
+ hireDate: new Date('2017-03-15'),
178
+ organizationId: 'org-1',
179
+ departmentId: 'dept-2',
180
+ events: {
181
+ created: { at: new Date('2017-03-15') },
182
+ updated: { at: new Date() },
183
+ deleted: { at: null }
184
+ }
185
+ }
186
+ ];
187
+
188
+ orgs.forEach(org => mockOrgStorage.set(org.id, org));
189
+ depts.forEach(dept => mockDeptStorage.set(dept.id, dept));
190
+ employees.forEach(emp => mockEmpStorage.set(emp.id, emp));
191
+
192
+ console.log('šŸ“¦ Initialized hierarchical sample data:');
193
+ console.log(` Organizations: ${orgs.length} records`);
194
+ console.log(` Departments: ${depts.length} records`);
195
+ console.log(` Employees: ${employees.length} records`);
196
+ };
197
+
198
+ // Create mock operations
199
+ const createOrgOperations = () => ({
200
+ async all() {
201
+ return Array.from(mockOrgStorage.values());
202
+ },
203
+ async get(key: PriKey<'organization'>) {
204
+ const org = mockOrgStorage.get(String(key.pk));
205
+ if (!org) throw new Error(`Organization not found: ${key.pk}`);
206
+ return org;
207
+ },
208
+ async create(item: Organization) {
209
+ const id = `org-${Date.now()}`;
210
+ const newOrg: Organization = {
211
+ ...item,
212
+ id,
213
+ key: { kt: 'organization', pk: id as UUID },
214
+ events: {
215
+ created: { at: new Date() },
216
+ updated: { at: new Date() },
217
+ deleted: { at: null }
218
+ }
219
+ };
220
+ mockOrgStorage.set(id, newOrg);
221
+ return newOrg;
222
+ },
223
+ async update(key: PriKey<'organization'>, updates: Partial<Organization>) {
224
+ const existing = mockOrgStorage.get(String(key.pk));
225
+ if (!existing) throw new Error(`Organization not found: ${key.pk}`);
226
+ const updated = { ...existing, ...updates };
227
+ mockOrgStorage.set(String(key.pk), updated);
228
+ return updated;
229
+ },
230
+ async remove(key: PriKey<'organization'>) {
231
+ return mockOrgStorage.delete(String(key.pk));
232
+ },
233
+ async find(finder: string, params: any) {
234
+ const orgs = Array.from(mockOrgStorage.values());
235
+ switch (finder) {
236
+ case 'byType':
237
+ return orgs.filter(org => org.type === params.type);
238
+ case 'byIndustry':
239
+ return orgs.filter(org => org.industry === params.industry);
240
+ default:
241
+ return orgs;
242
+ }
243
+ }
244
+ });
245
+
246
+ const createDeptOperations = () => ({
247
+ async all() {
248
+ return Array.from(mockDeptStorage.values());
249
+ },
250
+ async get(key: ComKey<'department', 'organization'>) {
251
+ const dept = mockDeptStorage.get(String(key.pk));
252
+ if (!dept) throw new Error(`Department not found: ${key.pk}`);
253
+ return dept;
254
+ },
255
+ async create(item: Department) {
256
+ const id = `dept-${Date.now()}`;
257
+ const newDept: Department = {
258
+ ...item,
259
+ id,
260
+ key: {
261
+ kt: 'department',
262
+ pk: id as UUID,
263
+ loc: (item.key as any)?.loc || []
264
+ },
265
+ events: {
266
+ created: { at: new Date() },
267
+ updated: { at: new Date() },
268
+ deleted: { at: null }
269
+ }
270
+ };
271
+ mockDeptStorage.set(id, newDept);
272
+ return newDept;
273
+ },
274
+ async update(key: ComKey<'department', 'organization'>, updates: Partial<Department>) {
275
+ const existing = mockDeptStorage.get(String(key.pk));
276
+ if (!existing) throw new Error(`Department not found: ${key.pk}`);
277
+ const updated = { ...existing, ...updates };
278
+ mockDeptStorage.set(String(key.pk), updated);
279
+ return updated;
280
+ },
281
+ async remove(key: ComKey<'department', 'organization'>) {
282
+ return mockDeptStorage.delete(String(key.pk));
283
+ },
284
+ async find(finder: string, params: any) {
285
+ const depts = Array.from(mockDeptStorage.values());
286
+ switch (finder) {
287
+ case 'byOrganization':
288
+ return depts.filter(dept => dept.organizationId === params.organizationId);
289
+ case 'byBudgetRange':
290
+ return depts.filter(dept => dept.budget >= params.min && dept.budget <= params.max);
291
+ default:
292
+ return depts;
293
+ }
294
+ }
295
+ });
296
+
297
+ const createEmpOperations = () => ({
298
+ async all() {
299
+ return Array.from(mockEmpStorage.values());
300
+ },
301
+ async get(key: ComKey<'employee', 'organization', 'department'>) {
302
+ const emp = mockEmpStorage.get(String(key.pk));
303
+ if (!emp) throw new Error(`Employee not found: ${key.pk}`);
304
+ return emp;
305
+ },
306
+ async create(item: Employee) {
307
+ const id = `emp-${Date.now()}`;
308
+ const newEmp: Employee = {
309
+ ...item,
310
+ id,
311
+ key: {
312
+ kt: 'employee',
313
+ pk: id as UUID,
314
+ loc: (item.key as any)?.loc || []
315
+ },
316
+ events: {
317
+ created: { at: new Date() },
318
+ updated: { at: new Date() },
319
+ deleted: { at: null }
320
+ }
321
+ };
322
+ mockEmpStorage.set(id, newEmp);
323
+ return newEmp;
324
+ },
325
+ async update(key: ComKey<'employee', 'organization', 'department'>, updates: Partial<Employee>) {
326
+ const existing = mockEmpStorage.get(String(key.pk));
327
+ if (!existing) throw new Error(`Employee not found: ${key.pk}`);
328
+ const updated = { ...existing, ...updates };
329
+ mockEmpStorage.set(String(key.pk), updated);
330
+ return updated;
331
+ },
332
+ async remove(key: ComKey<'employee', 'organization', 'department'>) {
333
+ return mockEmpStorage.delete(String(key.pk));
334
+ },
335
+ async find(finder: string, params: any) {
336
+ const employees = Array.from(mockEmpStorage.values());
337
+ switch (finder) {
338
+ case 'byDepartment':
339
+ return employees.filter(emp => emp.departmentId === params.departmentId);
340
+ case 'byPosition':
341
+ return employees.filter(emp => emp.position.includes(params.position));
342
+ default:
343
+ return employees;
344
+ }
345
+ }
346
+ });
347
+
348
+ /**
349
+ * Main function demonstrating nested fjell-express-router usage
350
+ */
351
+ export const runNestedRouterExample = async (): Promise<{ app: Application }> => {
352
+ console.log('šŸš€ Starting Nested Express Router Example...\n');
353
+
354
+ initializeSampleData();
355
+
356
+ const registry = createRegistry();
357
+
358
+ // Create mock instances
359
+ const mockOrgInstance = {
360
+ operations: createOrgOperations(),
361
+ options: {}
362
+ } as any;
363
+
364
+ const mockDeptInstance = {
365
+ operations: createDeptOperations(),
366
+ options: {}
367
+ } as any;
368
+
369
+ const mockEmpInstance = {
370
+ operations: createEmpOperations(),
371
+ options: {}
372
+ } as any;
373
+
374
+ const app: Application = express();
375
+ app.use(express.json());
376
+
377
+ // Request logging middleware
378
+ app.use((req, res, next) => {
379
+ console.log(`🌐 ${req.method} ${req.path}`, req.query);
380
+ next();
381
+ });
382
+
383
+ // Create routers
384
+ console.log('šŸ›¤ļø Creating nested Express routers...');
385
+ const orgRouter = new PItemRouter(mockOrgInstance, 'organization');
386
+ const deptRouter = new CItemRouter(mockDeptInstance, 'department', orgRouter);
387
+ const empRouter = new CItemRouter(mockEmpInstance, 'employee', deptRouter);
388
+
389
+ // Mount the routers to create nested endpoints:
390
+ // Primary routes for organizations:
391
+ // GET /api/organizations - Get all organizations
392
+ // GET /api/organizations/:orgPk - Get specific organization
393
+ // POST /api/organizations - Create new organization
394
+ // PUT /api/organizations/:orgPk - Update organization
395
+ // DELETE /api/organizations/:orgPk - Delete organization
396
+ app.use('/api/organizations', orgRouter.getRouter());
397
+
398
+ // Nested routes for departments within organizations:
399
+ // GET /api/organizations/:orgPk/departments - Get all departments for organization
400
+ // GET /api/organizations/:orgPk/departments/:deptPk - Get specific department
401
+ // POST /api/organizations/:orgPk/departments - Create new department in organization
402
+ // PUT /api/organizations/:orgPk/departments/:deptPk - Update department
403
+ // DELETE /api/organizations/:orgPk/departments/:deptPk - Delete department
404
+ app.use('/api/organizations/:organizationPk/departments', deptRouter.getRouter());
405
+
406
+ // Deeply nested routes for employees within departments within organizations:
407
+ // GET /api/organizations/:orgPk/departments/:deptPk/employees - Get all employees for department
408
+ // GET /api/organizations/:orgPk/departments/:deptPk/employees/:empPk - Get specific employee
409
+ // POST /api/organizations/:orgPk/departments/:deptPk/employees - Create new employee in department
410
+ // PUT /api/organizations/:orgPk/departments/:deptPk/employees/:empPk - Update employee
411
+ // DELETE /api/organizations/:orgPk/departments/:deptPk/employees/:empPk - Delete employee
412
+ app.use('/api/organizations/:organizationPk/departments/:departmentPk/employees', empRouter.getRouter());
413
+
414
+ // Additional hierarchy summary routes
415
+ app.get('/api/hierarchy', async (req, res) => {
416
+ try {
417
+ const orgs = await mockOrgInstance.operations.all();
418
+ const depts = await mockDeptInstance.operations.all();
419
+ const employees = await mockEmpInstance.operations.all();
420
+
421
+ const hierarchy = orgs.map((org: Organization) => {
422
+ const orgDepts = depts.filter((dept: Department) => dept.organizationId === org.id);
423
+ return {
424
+ organization: org,
425
+ departments: orgDepts.map((dept: Department) => {
426
+ const deptEmployees = employees.filter((emp: Employee) => emp.departmentId === dept.id);
427
+ return {
428
+ department: dept,
429
+ employees: deptEmployees
430
+ };
431
+ })
432
+ };
433
+ });
434
+
435
+ res.json(hierarchy);
436
+ } catch (error) {
437
+ res.status(500).json({ error: 'Failed to load hierarchy' });
438
+ }
439
+ });
440
+
441
+ app.get('/api/stats', async (req, res) => {
442
+ try {
443
+ const orgs = await mockOrgInstance.operations.all();
444
+ const depts = await mockDeptInstance.operations.all();
445
+ const employees = await mockEmpInstance.operations.all();
446
+
447
+ const stats = {
448
+ totals: {
449
+ organizations: orgs.length,
450
+ departments: depts.length,
451
+ employees: employees.length
452
+ },
453
+ organizationTypes: orgs.reduce((acc: any, org: Organization) => {
454
+ acc[org.type] = (acc[org.type] || 0) + 1;
455
+ return acc;
456
+ }, {}),
457
+ averageDepartmentBudget: depts.reduce((sum: number, dept: Department) => sum + dept.budget, 0) / depts.length,
458
+ totalPayroll: employees.reduce((sum: number, emp: Employee) => sum + emp.salary, 0)
459
+ };
460
+
461
+ res.json(stats);
462
+ } catch (error) {
463
+ res.status(500).json({ error: 'Failed to calculate stats' });
464
+ }
465
+ });
466
+
467
+ console.log('\nāœ… Nested Express Router Example setup complete!');
468
+ console.log('\nšŸ“š Available nested endpoints:');
469
+ console.log(' šŸ“Š GET /api/hierarchy - Full organizational hierarchy');
470
+ console.log(' šŸ“ˆ GET /api/stats - Statistics summary');
471
+ console.log(' šŸ¢ Organizations (Primary):');
472
+ console.log(' GET /api/organizations - List all organizations');
473
+ console.log(' GET /api/organizations/:orgPk - Get specific organization');
474
+ console.log(' POST /api/organizations - Create new organization');
475
+ console.log(' šŸ¬ Departments (Contained in Organizations):');
476
+ console.log(' GET /api/organizations/:orgPk/departments - List departments for organization');
477
+ console.log(' GET /api/organizations/:orgPk/departments/:deptPk - Get specific department');
478
+ console.log(' POST /api/organizations/:orgPk/departments - Create department in organization');
479
+ console.log(' šŸ‘„ Employees (Contained in Departments):');
480
+ console.log(' GET /api/organizations/:orgPk/departments/:deptPk/employees - List employees for department');
481
+ console.log(' GET /api/organizations/:orgPk/departments/:deptPk/employees/:empPk - Get specific employee');
482
+ console.log(' POST /api/organizations/:orgPk/departments/:deptPk/employees - Create employee in department');
483
+
484
+ return { app };
485
+ };
486
+
487
+ // If this file is run directly, start the server
488
+ if (require.main === module) {
489
+ runNestedRouterExample().then(({ app }) => {
490
+ const PORT = process.env.PORT || 3002;
491
+ app.listen(PORT, () => {
492
+ console.log(`\n🌟 Server running on http://localhost:${PORT}`);
493
+ console.log('\nšŸ’” Try these example requests:');
494
+ console.log(` curl http://localhost:${PORT}/api/hierarchy`);
495
+ console.log(` curl http://localhost:${PORT}/api/stats`);
496
+ console.log(` curl http://localhost:${PORT}/api/organizations`);
497
+ console.log(` curl http://localhost:${PORT}/api/organizations/org-1/departments`);
498
+ console.log(` curl http://localhost:${PORT}/api/organizations/org-1/departments/dept-1/employees`);
499
+ });
500
+ }).catch(console.error);
501
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjell/express-router",
3
- "version": "4.4.7",
3
+ "version": "4.4.10",
4
4
  "license": "Apache-2.0",
5
5
  "keywords": [
6
6
  "express",
@@ -23,18 +23,19 @@
23
23
  "type": "module",
24
24
  "dependencies": {
25
25
  "@fjell/core": "^4.4.7",
26
- "@fjell/lib": "^4.4.10",
26
+ "@fjell/lib": "^4.4.11",
27
27
  "@fjell/logging": "^4.4.7",
28
+ "@fjell/registry": "^4.4.7",
28
29
  "deepmerge": "^4.3.1",
29
30
  "express": "^5.1.0"
30
31
  },
31
32
  "devDependencies": {
32
33
  "@eslint/eslintrc": "^3.3.1",
33
34
  "@eslint/js": "^9.31.0",
34
- "@swc/core": "^1.12.14",
35
+ "@swc/core": "^1.13.1",
35
36
  "@tsconfig/recommended": "^1.0.10",
36
37
  "@types/express": "^5.0.3",
37
- "@types/node": "^24.0.14",
38
+ "@types/node": "^24.0.15",
38
39
  "@typescript-eslint/eslint-plugin": "^8.37.0",
39
40
  "@typescript-eslint/parser": "^8.37.0",
40
41
  "@vitest/coverage-v8": "^3.2.4",
@@ -42,7 +43,7 @@
42
43
  "nodemon": "^3.1.10",
43
44
  "rimraf": "^6.0.1",
44
45
  "typescript": "^5.8.3",
45
- "vite": "^7.0.4",
46
+ "vite": "^7.0.5",
46
47
  "vite-plugin-dts": "^4.5.4",
47
48
  "vite-plugin-node": "^7.0.0",
48
49
  "vitest": "^3.2.4"