@fjell/express-router 4.4.8 ā 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.
- package/dist/CItemRouter.cjs.map +1 -1
- package/dist/CItemRouter.d.ts +2 -2
- package/dist/CItemRouter.js.map +1 -1
- package/dist/Instance.cjs +27 -0
- package/dist/Instance.cjs.map +1 -0
- package/dist/Instance.d.ts +22 -0
- package/dist/Instance.js +22 -0
- package/dist/Instance.js.map +1 -0
- package/dist/InstanceFactory.cjs +23 -0
- package/dist/InstanceFactory.cjs.map +1 -0
- package/dist/InstanceFactory.d.ts +10 -0
- package/dist/InstanceFactory.js +19 -0
- package/dist/InstanceFactory.js.map +1 -0
- package/dist/ItemRouter.cjs +5 -5
- package/dist/ItemRouter.cjs.map +1 -1
- package/dist/ItemRouter.js +5 -5
- package/dist/ItemRouter.js.map +1 -1
- package/dist/PItemRouter.cjs.map +1 -1
- package/dist/PItemRouter.d.ts +2 -2
- package/dist/PItemRouter.js.map +1 -1
- package/dist/Registry.cjs +36 -0
- package/dist/Registry.cjs.map +1 -0
- package/dist/Registry.d.ts +15 -0
- package/dist/Registry.js +31 -0
- package/dist/Registry.js.map +1 -0
- package/dist/index.cjs +68 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/examples/README.md +339 -0
- package/examples/basic-router-example.ts +386 -0
- package/examples/full-application-example.ts +610 -0
- package/examples/nested-router-example.ts +501 -0
- package/package.json +6 -5
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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"
|