@fjell/express-router 4.4.23 → 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.
@@ -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 Error(`Organization not found: ${key.pk}`);
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 Error(`Organization not found: ${key.pk}`);
226
- const updated = { ...existing, ...updates };
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
- return mockOrgStorage.delete(String(key.pk));
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
- return Array.from(mockDeptStorage.values());
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 Error(`Department not found: ${key.pk}`);
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 Error(`Department not found: ${key.pk}`);
277
- const updated = { ...existing, ...updates };
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
- return mockDeptStorage.delete(String(key.pk));
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 depts = Array.from(mockDeptStorage.values());
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 '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);
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 depts;
337
+ return filtered;
293
338
  }
294
339
  }
295
340
  });
296
341
 
297
342
  const createEmpOperations = () => ({
298
- async all() {
299
- return Array.from(mockEmpStorage.values());
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 Error(`Employee not found: ${key.pk}`);
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 Error(`Employee not found: ${key.pk}`);
328
- const updated = { ...existing, ...updates };
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
- return mockEmpStorage.delete(String(key.pk));
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 employees.filter(emp => emp.departmentId === params.departmentId);
430
+ return filtered.filter(emp => emp.departmentId === params.departmentId);
340
431
  case 'byPosition':
341
- return employees.filter(emp => emp.position.includes(params.position));
432
+ return filtered.filter(emp => emp.position.includes(params.position));
342
433
  default:
343
- return employees;
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/:orgPk - Get specific organization
483
+ // GET /api/organizations/:organizationPk - Get specific organization
393
484
  // POST /api/organizations - Create new organization
394
- // PUT /api/organizations/:orgPk - Update organization
395
- // DELETE /api/organizations/:orgPk - Delete organization
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/: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
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/: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
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/:orgPk - Get specific organization');
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/: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');
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/: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');
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
  };