Skip to content

Commit 26d789b

Browse files
committed
feat: hierarchical dependency injection
1 parent 47f3518 commit 26d789b

18 files changed

Lines changed: 441 additions & 142 deletions

.eslintrc.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,11 @@ module.exports = {
4141
"@typescript-eslint/no-floating-promises": 2,
4242
"@typescript-eslint/no-misused-new": 2,
4343
"@typescript-eslint/no-namespace": 1,
44-
"@typescript-eslint/no-unused-vars": 1,
44+
"@typescript-eslint/no-unused-vars": [1, {
45+
"argsIgnorePattern": "^_",
46+
"varsIgnorePattern": "^_",
47+
"caughtErrorsIgnorePattern": "^_"
48+
}],
4549
"@typescript-eslint/promise-function-async": 1,
4650
"@typescript-eslint/no-var-requires": 2,
4751
},

examples/complete-example/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,39 @@ curl "http://localhost:3000/api/express/search?q=test&page=2&limit=20"
7272
curl http://localhost:3000/api/health
7373
```
7474

75+
### Dependency Injection
76+
77+
API-Machine uses `hidi`. You can register dependencies at the server, router or endpoint level.
78+
79+
```typescript
80+
class MyService {
81+
...
82+
}
83+
84+
class MyRouter extends BaseApiRouter {
85+
override inject() {
86+
super.inject();
87+
88+
// Register a dependency at the router level
89+
this.container.register(MyService, new MyService());
90+
}
91+
}
92+
93+
class MyEndpoint extends BaseApiEndpoint {
94+
protected myService: MyService;
95+
96+
override inject() {
97+
super.inject();
98+
99+
this.myService = this.container.require(MyService);
100+
}
101+
102+
async handle(request: ApiRequest, response: ApiResponse) {
103+
this.myService.performAction();
104+
}
105+
}
106+
```
107+
75108
### Bearer Token Authentication (Inline, Stateless)
76109

77110
Bearer authentication validates a token on every request without maintaining session state.

examples/complete-example/server.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { RestServer } from '../../src/index';
22
import { MainRouter } from './router';
33
import { Log } from 'ts-tiny-log';
44

5+
import { UsersRepository } from './users/users-repository';
6+
57
/**
68
* Complete Example - Server
79
*
@@ -12,6 +14,12 @@ import { Log } from 'ts-tiny-log';
1214
export class MyServer extends RestServer {
1315
override router = MainRouter;
1416

17+
override async inject(): Promise<void> {
18+
await super.inject();
19+
20+
this.container.register(UsersRepository, new UsersRepository());
21+
}
22+
1523
protected override async setupExpress(): Promise<void> {
1624
await super.setupExpress();
1725
// Add middleware to parse sessionId from cookies

examples/complete-example/users/create-user-endpoint.ts

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ApiRequest, ApiResponse, PostEndpoint } from '../../../src/index';
2-
import { usersRepo, User } from './users-repository';
2+
import { UsersRepository } from './users-repository';
33
import { ObjectValSan, EmailValidator, IntegerValidator } from 'valsan';
44
import { NameValSan } from './name-valsan';
55

@@ -11,6 +11,8 @@ import { NameValSan } from './name-valsan';
1111
export class CreateUserEndpoint extends PostEndpoint {
1212
override path = '/';
1313

14+
protected usersRepo: UsersRepository;
15+
1416
override bodyExample = {
1517
name: 'John Doe',
1618
email: 'john@example.com',
@@ -38,22 +40,15 @@ export class CreateUserEndpoint extends PostEndpoint {
3840
},
3941
});
4042

43+
override inject(): void {
44+
this.usersRepo = this.container.require(UsersRepository);
45+
}
46+
4147
async handle(request: ApiRequest, response: ApiResponse) {
4248
const { name, email } = request.body;
4349

44-
// Generate new ID
45-
const newId = Math.max(...Object.keys(usersRepo).map(Number)) + 1;
46-
47-
// Create new user
48-
const newUser: User = {
49-
id: newId,
50-
name,
51-
email,
52-
created: new Date(),
53-
};
54-
55-
// Add to repository
56-
usersRepo[newId] = newUser;
50+
// Create and add to repository
51+
const newUser = this.usersRepo.add({ name, email });
5752

5853
// Return new user (PostEndpoint automatically sets 201 status)
5954
return newUser;

examples/complete-example/users/delete-user-endpoint.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
DeleteEndpoint,
55
NotFoundError,
66
} from '../../../src/index';
7-
import { usersRepo } from './users-repository';
7+
import { UsersRepository } from './users-repository';
88

99
/**
1010
* Complete Example - Delete User Endpoint (DELETE)
@@ -14,18 +14,23 @@ import { usersRepo } from './users-repository';
1414
export class DeleteUserEndpoint extends DeleteEndpoint {
1515
override path = '/:id';
1616

17+
protected usersRepo: UsersRepository;
18+
19+
override inject(): void {
20+
this.usersRepo = this.container.require(UsersRepository);
21+
}
22+
1723
async handle(request: ApiRequest, response: ApiResponse) {
1824
const userId = parseInt(request.params['id'], 10);
19-
const user = usersRepo[userId];
25+
const user = this.usersRepo.getById(userId);
2026

2127
if (!user) {
2228
throw new NotFoundError('User not found', {
2329
details: { userId },
2430
});
2531
}
2632

27-
// Delete user from repository
28-
delete usersRepo[userId];
33+
this.usersRepo.delete(userId);
2934

3035
// DeleteEndpoint automatically sets 204 status
3136
return {};

examples/complete-example/users/get-user-endpoint.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ApiRequest, ApiResponse, GetEndpoint } from '../../../src/index';
22
import { NotFoundError } from '../../../src/error';
3-
import { usersRepo } from './users-repository';
3+
import { UsersRepository } from './users-repository';
44
import { IntegerValidator, EmailValidator, ObjectValSan } from 'valsan';
55
import { NameValSan } from './name-valsan';
66

@@ -13,6 +13,8 @@ import { NameValSan } from './name-valsan';
1313
export class GetUserEndpoint extends GetEndpoint {
1414
override path = '/:id';
1515

16+
protected usersRepo: UsersRepository;
17+
1618
override responseExample = {
1719
id: 1,
1820
name: 'Alice',
@@ -28,9 +30,13 @@ export class GetUserEndpoint extends GetEndpoint {
2830
},
2931
});
3032

33+
override inject(): void {
34+
this.usersRepo = this.container.require(UsersRepository);
35+
}
36+
3137
async handle(request: ApiRequest, response: ApiResponse) {
3238
const userId = parseInt(request.params['id'], 10);
33-
const user = usersRepo[userId];
39+
const user = this.usersRepo.getById(userId);
3440

3541
if (!user) {
3642
// Throw HTTPError - server will automatically format response

examples/complete-example/users/list-users-endpoint.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
TrimSanitizer,
66
} from 'valsan';
77
import { ApiRequest, ApiResponse, GetEndpoint } from '../../../src/index';
8-
import { usersRepo } from './users-repository';
8+
import { UsersRepository } from './users-repository';
99

1010
/**
1111
* Complete Example - List Users Endpoint
@@ -16,6 +16,8 @@ import { usersRepo } from './users-repository';
1616
export class ListUsersEndpoint extends GetEndpoint {
1717
override path = '/';
1818

19+
protected usersRepo: UsersRepository;
20+
1921
override params = new ObjectValSan({
2022
schema: {
2123
name: new ComposedValSan(
@@ -29,7 +31,11 @@ export class ListUsersEndpoint extends GetEndpoint {
2931
},
3032
});
3133

34+
override inject(): void {
35+
this.usersRepo = this.container.require(UsersRepository);
36+
}
37+
3238
async handle(request: ApiRequest, response: ApiResponse) {
33-
return Object.values(usersRepo);
39+
return this.usersRepo.getAll();
3440
}
3541
}
Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ApiRequest, PutEndpoint, NotFoundError } from '../../../src/index';
2-
import { usersRepo } from './users-repository';
2+
import { UsersRepository } from './users-repository';
33

44
/**
55
* Complete Example - Update User Endpoint (PUT)
@@ -9,26 +9,25 @@ import { usersRepo } from './users-repository';
99
export class UpdateUserEndpoint extends PutEndpoint {
1010
override path = '/:id';
1111

12+
protected usersRepo: UsersRepository;
13+
14+
override inject(): void {
15+
this.usersRepo = this.container.require(UsersRepository);
16+
}
17+
1218
async handle(request: ApiRequest) {
1319
const userId = parseInt(request.params['id'], 10);
14-
const user = usersRepo[userId];
15-
16-
if (!user) {
17-
throw new NotFoundError('User not found', {
18-
details: { userId },
19-
});
20-
}
2120

2221
// Update user properties
2322
const { name, email } = request.body;
24-
if (name) {
25-
user.name = name;
26-
}
23+
const updatedUser = this.usersRepo.update(userId, { name, email });
2724

28-
if (email) {
29-
user.email = email;
25+
if (!updatedUser) {
26+
throw new NotFoundError('User not found', {
27+
details: { userId },
28+
});
3029
}
3130

32-
return user;
31+
return updatedUser;
3332
}
3433
}

examples/complete-example/users/users-repository.ts

Lines changed: 60 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,63 @@ export interface User {
55
created: Date;
66
}
77

8-
export const usersRepo: { [id: number]: User } = {
9-
1: {
10-
id: 1,
11-
name: 'Alice',
12-
email: 'alice@example.com',
13-
created: new Date('2023-01-01'),
14-
},
15-
2: {
16-
id: 2,
17-
name: 'Bob',
18-
email: 'bob@example.com',
19-
created: new Date('2023-01-02'),
20-
},
21-
};
8+
export class UsersRepository {
9+
protected readonly users: { [id: number]: User } = {
10+
1: {
11+
id: 1,
12+
name: 'Alice',
13+
email: 'alice@example.com',
14+
created: new Date('2023-01-01'),
15+
},
16+
2: {
17+
id: 2,
18+
name: 'Bob',
19+
email: 'bob@example.com',
20+
created: new Date('2023-01-02'),
21+
},
22+
};
23+
24+
public getAll(): User[] {
25+
return Object.values(this.users);
26+
}
27+
28+
public getById(id: number): User | undefined {
29+
return this.users[id];
30+
}
31+
32+
public add(user: Omit<User, 'id' | 'created'>): User {
33+
const id = Math.max(...Object.keys(this.users).map(Number)) + 1;
34+
35+
const newUser: User = {
36+
...user,
37+
id,
38+
created: new Date(),
39+
};
40+
41+
this.users[id] = newUser;
42+
43+
return newUser;
44+
}
45+
46+
public update(
47+
id: number,
48+
user: Partial<Omit<User, 'id' | 'created'>>
49+
): User | undefined {
50+
if (!this.users[id]) {
51+
return undefined;
52+
}
53+
this.users[id] = { ...this.users[id], ...user };
54+
55+
return this.users[id];
56+
}
57+
58+
public delete(id: number): boolean {
59+
if (!this.users[id]) {
60+
return false;
61+
}
62+
63+
delete this.users[id];
64+
65+
return true;
66+
}
67+
}

examples/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import './complete-example/index';

0 commit comments

Comments
 (0)