ts-ioc-container 71.0.0 → 72.1.0
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/README.md +203 -65
- package/cjm/container/Container.js +5 -5
- package/cjm/hooks/HookContext.js +3 -2
- package/cjm/hooks/injectProp.js +2 -4
- package/cjm/index.js +7 -4
- package/cjm/injector/IInjector.js +2 -2
- package/cjm/injector/MetadataInjector.js +14 -9
- package/cjm/injector/ProxyInjector.js +1 -1
- package/cjm/injector/SimpleInjector.js +2 -2
- package/cjm/metadata/class.js +3 -1
- package/cjm/metadata/method.js +3 -1
- package/cjm/metadata/parameter.js +7 -1
- package/cjm/provider/Provider.js +9 -8
- package/cjm/registration/IRegistration.js +2 -2
- package/cjm/select.js +2 -2
- package/cjm/token/ClassToken.js +3 -3
- package/cjm/token/FunctionToken.js +5 -4
- package/cjm/token/GroupAliasToken.js +3 -3
- package/cjm/token/InjectionToken.js +1 -1
- package/cjm/token/SingleAliasToken.js +3 -3
- package/cjm/token/SingleToken.js +3 -3
- package/cjm/token/toToken.js +1 -8
- package/esm/container/Container.js +5 -5
- package/esm/hooks/HookContext.js +3 -2
- package/esm/hooks/injectProp.js +1 -4
- package/esm/index.js +5 -5
- package/esm/injector/IInjector.js +2 -2
- package/esm/injector/MetadataInjector.js +13 -9
- package/esm/injector/ProxyInjector.js +1 -1
- package/esm/injector/SimpleInjector.js +2 -2
- package/esm/metadata/class.js +1 -0
- package/esm/metadata/method.js +1 -0
- package/esm/metadata/parameter.js +5 -0
- package/esm/provider/Provider.js +9 -8
- package/esm/registration/IRegistration.js +2 -2
- package/esm/select.js +2 -2
- package/esm/token/ClassToken.js +3 -3
- package/esm/token/FunctionToken.js +5 -4
- package/esm/token/GroupAliasToken.js +3 -3
- package/esm/token/InjectionToken.js +1 -1
- package/esm/token/SingleAliasToken.js +3 -3
- package/esm/token/SingleToken.js +3 -3
- package/esm/token/toToken.js +0 -6
- package/package.json +1 -1
- package/typings/container/IContainer.d.ts +2 -2
- package/typings/hooks/HookContext.d.ts +3 -3
- package/typings/hooks/hook.d.ts +1 -2
- package/typings/hooks/injectProp.d.ts +2 -14
- package/typings/index.d.ts +7 -7
- package/typings/injector/IInjector.d.ts +7 -4
- package/typings/injector/MetadataInjector.d.ts +6 -16
- package/typings/injector/ProxyInjector.d.ts +1 -2
- package/typings/injector/SimpleInjector.d.ts +1 -2
- package/typings/metadata/class.d.ts +1 -0
- package/typings/metadata/method.d.ts +1 -0
- package/typings/metadata/parameter.d.ts +1 -0
- package/typings/provider/IProvider.d.ts +5 -4
- package/typings/provider/Provider.d.ts +2 -2
- package/typings/select.d.ts +3 -3
- package/typings/token/ClassToken.d.ts +2 -2
- package/typings/token/FunctionToken.d.ts +2 -2
- package/typings/token/GroupAliasToken.d.ts +2 -2
- package/typings/token/InjectionToken.d.ts +2 -2
- package/typings/token/SingleAliasToken.d.ts +3 -3
- package/typings/token/SingleToken.d.ts +2 -2
- package/typings/token/toToken.d.ts +0 -2
package/README.md
CHANGED
|
@@ -52,6 +52,7 @@ provider pipelines, aliases, and custom injector strategies.
|
|
|
52
52
|
- [Registration](#registration) `@register`
|
|
53
53
|
- [Token](#token) `bindTo`
|
|
54
54
|
- [Scope](#scope) `scope`
|
|
55
|
+
- [Composing decorators](#composing-decorators) `createComposeClassDecorator`
|
|
55
56
|
- [Module](#module)
|
|
56
57
|
- [Hook](#hook) `@hook`
|
|
57
58
|
- [Hook domains](#hook-domains) `ScopeHook` `InjectorHook` `ProviderHook`
|
|
@@ -114,7 +115,7 @@ bundlers tree-shake unused exports.
|
|
|
114
115
|
## Quickstart
|
|
115
116
|
|
|
116
117
|
```typescript
|
|
117
|
-
import { bindTo, Container, inject, register, Registration as R, singleton, SingleToken } from 'ts-ioc-container';
|
|
118
|
+
import { bindTo, Container, inject, register, Registration as R, singleton, SingleToken, by } from 'ts-ioc-container';
|
|
118
119
|
|
|
119
120
|
interface ILogger {
|
|
120
121
|
log(message: string): void;
|
|
@@ -130,7 +131,7 @@ class Logger implements ILogger {
|
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
class App {
|
|
133
|
-
constructor(@inject(ILoggerToken) private logger: ILogger) {}
|
|
134
|
+
constructor(@inject(by(ILoggerToken)) private logger: ILogger) {}
|
|
134
135
|
start() {
|
|
135
136
|
this.logger.log('hello');
|
|
136
137
|
}
|
|
@@ -153,16 +154,18 @@ describe('Quickstart', function () {
|
|
|
153
154
|
|
|
154
155
|
- Register class with key (preferred): `@register(bindTo('Key')) class Service {}` then `container.addRegistration(R.fromClass(Service))`
|
|
155
156
|
- Register value: `R.fromValue(config).bindTo('Config')`
|
|
156
|
-
- Register factory: `R.fromFn((
|
|
157
|
+
- Register factory: `R.fromFn(({ scope }) => createX(scope)).bindTo('X')`
|
|
157
158
|
- Singleton: `@register(singleton())`
|
|
158
159
|
- Eager service: `@register(autoResolve())` + `container.useModule(new AutoResolveModule())`
|
|
159
160
|
- Scoped registration: `@register(scope((s) => s.hasTag('request')))`
|
|
160
161
|
- Resolve by alias: `container.resolveByAlias('Alias')`
|
|
161
162
|
- Current scope token: `select.scope.current`
|
|
162
163
|
- Lazy token: `select.token('Service').lazy()`
|
|
163
|
-
- Inject decorator: `@inject('Key')`
|
|
164
|
-
-
|
|
165
|
-
-
|
|
164
|
+
- Inject decorator: `@inject(by(Token))` / `@inject(by('Key'))` / `@inject(by(Logger))` — the argument is always one `InjectFn`, `by(...)` builds the usual one
|
|
165
|
+
- Custom inject function: `@inject(({ scope, args }) => ...)`
|
|
166
|
+
- Map an injected value: `@inject(pipe(by('Key'), sanitize(), validate()))`
|
|
167
|
+
- Property inject: `@hook('onInit', injectProp(by('Key')))`
|
|
168
|
+
- Name a decorator stack: `const repository = (token) => createComposeClassDecorator(register(token, singleton()), addClassMeta('injection-token', () => token))`
|
|
166
169
|
|
|
167
170
|
> [!TIP]
|
|
168
171
|
> For classes, prefer the `@register(bindTo('Key'))` decorator over the fluent
|
|
@@ -184,7 +187,7 @@ describe('Quickstart', function () {
|
|
|
184
187
|
|
|
185
188
|
```typescript
|
|
186
189
|
import 'reflect-metadata';
|
|
187
|
-
import { bindTo, Container, type IContainer, inject, register, Registration as R, select } from 'ts-ioc-container';
|
|
190
|
+
import { bindTo, Container, type IContainer, inject, register, Registration as R, select, by } from 'ts-ioc-container';
|
|
188
191
|
|
|
189
192
|
/**
|
|
190
193
|
* User Management Domain - Basic Dependency Injection
|
|
@@ -219,7 +222,7 @@ describe('Basic usage', function () {
|
|
|
219
222
|
it('should inject dependencies', function () {
|
|
220
223
|
// AuthService depends on IUserRepository
|
|
221
224
|
class AuthService {
|
|
222
|
-
constructor(@inject('IUserRepository') private userRepo: IUserRepository) {}
|
|
225
|
+
constructor(@inject(by('IUserRepository')) private userRepo: IUserRepository) {}
|
|
223
226
|
|
|
224
227
|
authenticate(email: string): boolean {
|
|
225
228
|
const user = this.userRepo.findByEmail(email);
|
|
@@ -243,7 +246,7 @@ describe('Basic usage', function () {
|
|
|
243
246
|
const appContainer = new Container({ tags: ['application'] });
|
|
244
247
|
|
|
245
248
|
class RequestHandler {
|
|
246
|
-
constructor(@inject(select.scope.current) public requestScope: IContainer) {}
|
|
249
|
+
constructor(@inject(by(select.scope.current)) public requestScope: IContainer) {}
|
|
247
250
|
|
|
248
251
|
handleRequest(): string {
|
|
249
252
|
// Access request-scoped dependencies
|
|
@@ -286,6 +289,7 @@ import {
|
|
|
286
289
|
scope,
|
|
287
290
|
select,
|
|
288
291
|
singleton,
|
|
292
|
+
by,
|
|
289
293
|
} from 'ts-ioc-container';
|
|
290
294
|
|
|
291
295
|
/**
|
|
@@ -345,7 +349,10 @@ describe('Scopes', function () {
|
|
|
345
349
|
|
|
346
350
|
// RequestHandler can create a transaction scope for database operations
|
|
347
351
|
class RequestHandler {
|
|
348
|
-
constructor(
|
|
352
|
+
constructor(
|
|
353
|
+
@inject(by(select.scope.create({ tags: ['transaction'] })))
|
|
354
|
+
public transactionScope: IContainer,
|
|
355
|
+
) {}
|
|
349
356
|
|
|
350
357
|
executeInTransaction(): boolean {
|
|
351
358
|
// Transaction scope inherits from request scope
|
|
@@ -371,7 +378,7 @@ Sometimes you want to get all instances from container and its scopes. For examp
|
|
|
371
378
|
- you can get instances from container and scope which were created by injector
|
|
372
379
|
|
|
373
380
|
```typescript
|
|
374
|
-
import { bindTo, Container, inject, register, Registration as R, select } from 'ts-ioc-container';
|
|
381
|
+
import { bindTo, Container, inject, register, Registration as R, select, by } from 'ts-ioc-container';
|
|
375
382
|
|
|
376
383
|
/**
|
|
377
384
|
* User Management Domain - Instance Collection
|
|
@@ -391,7 +398,7 @@ describe('Instances', function () {
|
|
|
391
398
|
it('should collect instances across scope hierarchy', () => {
|
|
392
399
|
// App that needs access to all logger instances (e.g., for flushing)
|
|
393
400
|
class App {
|
|
394
|
-
constructor(@inject(select.instances()) public loggers: Logger[]) {}
|
|
401
|
+
constructor(@inject(by(select.instances())) public loggers: Logger[]) {}
|
|
395
402
|
}
|
|
396
403
|
|
|
397
404
|
const appContainer = new Container({ tags: ['application'] }).addRegistration(R.fromClass(Logger));
|
|
@@ -413,7 +420,7 @@ describe('Instances', function () {
|
|
|
413
420
|
it('should return only current scope instances when cascade is disabled', () => {
|
|
414
421
|
// Only get instances from current scope, not parent scopes
|
|
415
422
|
class App {
|
|
416
|
-
constructor(@inject(select.instances().cascade(false)) public loggers: Logger[]) {}
|
|
423
|
+
constructor(@inject(by(select.instances().cascade(false))) public loggers: Logger[]) {}
|
|
417
424
|
}
|
|
418
425
|
|
|
419
426
|
const appContainer = new Container({ tags: ['application'] }).addRegistration(R.fromClass(Logger));
|
|
@@ -432,7 +439,7 @@ describe('Instances', function () {
|
|
|
432
439
|
const isLogger = (instance: unknown) => instance instanceof Logger;
|
|
433
440
|
|
|
434
441
|
class App {
|
|
435
|
-
constructor(@inject(select.instances(isLogger)) public loggers: Logger[]) {}
|
|
442
|
+
constructor(@inject(by(select.instances(isLogger))) public loggers: Logger[]) {}
|
|
436
443
|
}
|
|
437
444
|
|
|
438
445
|
const container = new Container({ tags: ['application'] }).addRegistration(R.fromClass(Logger));
|
|
@@ -569,7 +576,7 @@ Sometimes you want to create dependency only when somebody want to invoke it's m
|
|
|
569
576
|
|
|
570
577
|
```typescript
|
|
571
578
|
import 'reflect-metadata';
|
|
572
|
-
import { Container, inject, register, Registration as R, select as s, singleton } from 'ts-ioc-container';
|
|
579
|
+
import { Container, inject, register, Registration as R, select as s, singleton, by } from 'ts-ioc-container';
|
|
573
580
|
|
|
574
581
|
/**
|
|
575
582
|
* User Management Domain - Lazy Loading
|
|
@@ -600,7 +607,7 @@ describe('lazy provider', () => {
|
|
|
600
607
|
|
|
601
608
|
// EmailNotifier is expensive - establishes SMTP connection on construction
|
|
602
609
|
class EmailNotifier {
|
|
603
|
-
constructor(@inject('SmtpConnectionStatus') private smtp: SmtpConnectionStatus) {
|
|
610
|
+
constructor(@inject(by('SmtpConnectionStatus')) private smtp: SmtpConnectionStatus) {
|
|
604
611
|
// Simulate expensive SMTP connection
|
|
605
612
|
this.smtp.connect();
|
|
606
613
|
}
|
|
@@ -613,7 +620,10 @@ describe('lazy provider', () => {
|
|
|
613
620
|
// AuthService might need to send password reset emails
|
|
614
621
|
// But most login requests don't need email (only password reset does)
|
|
615
622
|
class AuthService {
|
|
616
|
-
constructor(
|
|
623
|
+
constructor(
|
|
624
|
+
@inject(by(s.token('EmailNotifier').lazy()))
|
|
625
|
+
public emailNotifier: EmailNotifier,
|
|
626
|
+
) {}
|
|
617
627
|
|
|
618
628
|
login(email: string, password: string): boolean {
|
|
619
629
|
// Most requests just validate credentials - no email needed
|
|
@@ -720,6 +730,7 @@ import {
|
|
|
720
730
|
register,
|
|
721
731
|
Registration as R,
|
|
722
732
|
singleton,
|
|
733
|
+
by,
|
|
723
734
|
} from 'ts-ioc-container';
|
|
724
735
|
|
|
725
736
|
/**
|
|
@@ -762,7 +773,7 @@ describe('lazy registerPipe', () => {
|
|
|
762
773
|
// Analytics service - expensive, but only used occasionally
|
|
763
774
|
@register(bindTo('AnalyticsService'), lazy(), singleton())
|
|
764
775
|
class AnalyticsService {
|
|
765
|
-
constructor(@inject('DatabasePool') private db: DatabasePool) {
|
|
776
|
+
constructor(@inject(by('DatabasePool')) private db: DatabasePool) {
|
|
766
777
|
initLog.push('AnalyticsService initialized');
|
|
767
778
|
}
|
|
768
779
|
|
|
@@ -777,7 +788,7 @@ describe('lazy registerPipe', () => {
|
|
|
777
788
|
|
|
778
789
|
// Application service - always used
|
|
779
790
|
class AppService {
|
|
780
|
-
constructor(@inject('AnalyticsService') public analytics: AnalyticsService) {
|
|
791
|
+
constructor(@inject(by('AnalyticsService')) public analytics: AnalyticsService) {
|
|
781
792
|
initLog.push('AppService initialized');
|
|
782
793
|
}
|
|
783
794
|
|
|
@@ -874,8 +885,8 @@ describe('lazy registerPipe', () => {
|
|
|
874
885
|
// Notification service - uses email and SMS, but maybe not both
|
|
875
886
|
class NotificationService {
|
|
876
887
|
constructor(
|
|
877
|
-
@inject('EmailService') public email: EmailService,
|
|
878
|
-
@inject('SmsService') public sms: SmsService,
|
|
888
|
+
@inject(by('EmailService')) public email: EmailService,
|
|
889
|
+
@inject(by('SmsService')) public sms: SmsService,
|
|
879
890
|
) {
|
|
880
891
|
initLog.push('NotificationService initialized');
|
|
881
892
|
}
|
|
@@ -967,7 +978,7 @@ describe('lazy registerPipe', () => {
|
|
|
967
978
|
}
|
|
968
979
|
|
|
969
980
|
class ApiService {
|
|
970
|
-
constructor(@inject('CacheService') private cache: CacheService) {
|
|
981
|
+
constructor(@inject(by('CacheService')) private cache: CacheService) {
|
|
971
982
|
initLog.push('ApiService initialized');
|
|
972
983
|
}
|
|
973
984
|
|
|
@@ -1085,8 +1096,8 @@ describe('lazy registerPipe', () => {
|
|
|
1085
1096
|
|
|
1086
1097
|
class Application {
|
|
1087
1098
|
constructor(
|
|
1088
|
-
@inject('FeatureFlagService') private flags: FeatureFlagService,
|
|
1089
|
-
@inject('PremiumFeature') private premium: PremiumFeature,
|
|
1099
|
+
@inject(by('FeatureFlagService')) private flags: FeatureFlagService,
|
|
1100
|
+
@inject(by('PremiumFeature')) private premium: PremiumFeature,
|
|
1090
1101
|
) {
|
|
1091
1102
|
initLog.push('Application initialized');
|
|
1092
1103
|
}
|
|
@@ -1144,8 +1155,23 @@ describe('lazy registerPipe', () => {
|
|
|
1144
1155
|
This type of injector uses `@inject` decorator to mark where dependencies should be injected. It's bases on `reflect-metadata` package. That's why I call it `MetadataInjector`.
|
|
1145
1156
|
Also you can [inject property.](#inject-property)
|
|
1146
1157
|
|
|
1158
|
+
`@inject` takes exactly one argument, an `InjectFn`: `(options: ProviderOptions) => T`.
|
|
1159
|
+
It is called with the resolution context of the class being constructed — the
|
|
1160
|
+
`scope` it is built in and the runtime `args` it is built with — and whatever it
|
|
1161
|
+
returns is injected. There is no other form, only functions which build one:
|
|
1162
|
+
|
|
1163
|
+
- `by(target)` — resolves a token, a `DependencyKey` or a class from the scope,
|
|
1164
|
+
forwarding the runtime args (see
|
|
1165
|
+
[runtime args flow through tokens](#runtime-args-flow-through-tokens)):
|
|
1166
|
+
`@inject(by(LoggerToken))`, `@inject(by('Logger'))`, `@inject(by(Logger))`.
|
|
1167
|
+
- `arg(index)` / `args` / `argsFn(predicate)` — pick from the runtime args.
|
|
1168
|
+
- `pipe(fn, ...mappers)` — map what another `InjectFn` returns.
|
|
1169
|
+
- Your own: `@inject(({ scope }) => scope)` injects the current scope,
|
|
1170
|
+
`@inject(({ scope }) => Token.resolve(scope))` resolves a token *without*
|
|
1171
|
+
the args.
|
|
1172
|
+
|
|
1147
1173
|
```typescript
|
|
1148
|
-
import { bindTo, Container, inject, register, Registration as R } from 'ts-ioc-container';
|
|
1174
|
+
import { bindTo, Container, inject, register, Registration as R, by } from 'ts-ioc-container';
|
|
1149
1175
|
|
|
1150
1176
|
/**
|
|
1151
1177
|
* User Management Domain - Metadata Injection
|
|
@@ -1154,7 +1180,7 @@ import { bindTo, Container, inject, register, Registration as R } from 'ts-ioc-c
|
|
|
1154
1180
|
* to automatically inject dependencies into constructor parameters.
|
|
1155
1181
|
*
|
|
1156
1182
|
* How it works:
|
|
1157
|
-
* 1. @inject('key') decorator marks a parameter for injection
|
|
1183
|
+
* 1. @inject(by('key')) decorator marks a parameter for injection
|
|
1158
1184
|
* 2. Container reads metadata at resolution time
|
|
1159
1185
|
* 3. Dependencies are resolved and passed to constructor
|
|
1160
1186
|
*
|
|
@@ -1169,10 +1195,10 @@ class Logger {
|
|
|
1169
1195
|
|
|
1170
1196
|
class App {
|
|
1171
1197
|
// @inject tells the container which dependency to resolve for this parameter
|
|
1172
|
-
constructor(@inject('ILogger') private logger: Logger) {}
|
|
1198
|
+
constructor(@inject(by('ILogger')) private logger: Logger) {}
|
|
1173
1199
|
|
|
1174
1200
|
// Alternative: inject via function for dynamic resolution
|
|
1175
|
-
// constructor(@inject((
|
|
1201
|
+
// constructor(@inject(by('ILogger')) private logger: ILogger) {}
|
|
1176
1202
|
|
|
1177
1203
|
getLoggerName(): string {
|
|
1178
1204
|
return this.logger.name;
|
|
@@ -1194,31 +1220,32 @@ describe('Metadata Injector', function () {
|
|
|
1194
1220
|
|
|
1195
1221
|
### Mapping injected values
|
|
1196
1222
|
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1223
|
+
An `InjectFn` is a function, so mapping the value it resolves is function
|
|
1224
|
+
composition — `pipe(fn, ...mappers)` (exported) runs each **mapper** on the
|
|
1225
|
+
previous result, left to right, and the value the last one returns is what
|
|
1226
|
+
reaches the constructor parameter:
|
|
1200
1227
|
|
|
1201
1228
|
```typescript
|
|
1202
|
-
@inject('Config', takeApiUrl(), stripTrailingSlash(), requireHttps())
|
|
1229
|
+
@inject(pipe(by<Config>('Config'), takeApiUrl(), stripTrailingSlash(), requireHttps()))
|
|
1203
1230
|
```
|
|
1204
1231
|
|
|
1205
1232
|
A mapper is a plain `(value) => value` function, so mappers compose into named,
|
|
1206
1233
|
reusable steps (selecting a member, sanitizing, validating) and each step's
|
|
1207
|
-
parameter type is inferred from the previous one.
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
`injectProp` takes the same rest parameters: `injectProp('Config', takeApiUrl())`.
|
|
1234
|
+
parameter type is inferred from the previous one. `pipe(fn, ...mappers)` is
|
|
1235
|
+
itself an `InjectFn`, so it goes wherever one does — `@inject(...)`, a
|
|
1236
|
+
`FunctionToken`, or `injectProp(pipe(by('Config'), takeApiUrl()))`.
|
|
1211
1237
|
|
|
1212
1238
|
```typescript
|
|
1213
1239
|
import 'reflect-metadata';
|
|
1214
|
-
import { Container, inject, Registration as R } from 'ts-ioc-container';
|
|
1240
|
+
import { Container, inject, Registration as R, pipe, by } from 'ts-ioc-container';
|
|
1215
1241
|
|
|
1216
1242
|
/**
|
|
1217
1243
|
* Mapping injected values
|
|
1218
1244
|
*
|
|
1219
|
-
*
|
|
1220
|
-
* mapper
|
|
1221
|
-
*
|
|
1245
|
+
* `@inject` takes one `InjectFn`, so mapping what it resolves is composition:
|
|
1246
|
+
* `pipe(fn, ...mappers)` applies each mapper to the previous result, left to
|
|
1247
|
+
* right, and is itself an `InjectFn`. Mappers are plain functions, so they
|
|
1248
|
+
* compose into reusable, named steps.
|
|
1222
1249
|
*/
|
|
1223
1250
|
|
|
1224
1251
|
interface Config {
|
|
@@ -1239,7 +1266,10 @@ const requireHttps = () => (url: string) => {
|
|
|
1239
1266
|
describe('inject mappers', () => {
|
|
1240
1267
|
it('should pipe the resolved dependency through every mapper', () => {
|
|
1241
1268
|
class ApiClient {
|
|
1242
|
-
constructor(
|
|
1269
|
+
constructor(
|
|
1270
|
+
@inject(pipe(by<Config>('Config'), takeApiUrl(), stripTrailingSlash(), requireHttps()))
|
|
1271
|
+
readonly apiUrl: string,
|
|
1272
|
+
) {}
|
|
1243
1273
|
}
|
|
1244
1274
|
|
|
1245
1275
|
const container = new Container().addRegistration(
|
|
@@ -1251,7 +1281,10 @@ describe('inject mappers', () => {
|
|
|
1251
1281
|
|
|
1252
1282
|
it('should throw from a mapper when the resolved value is not acceptable', () => {
|
|
1253
1283
|
class ApiClient {
|
|
1254
|
-
constructor(
|
|
1284
|
+
constructor(
|
|
1285
|
+
@inject(pipe(by<Config>('Config'), takeApiUrl(), requireHttps()))
|
|
1286
|
+
readonly apiUrl: string,
|
|
1287
|
+
) {}
|
|
1255
1288
|
}
|
|
1256
1289
|
|
|
1257
1290
|
const container = new Container().addRegistration(
|
|
@@ -1391,7 +1424,7 @@ Provider is dependency factory which creates dependency.
|
|
|
1391
1424
|
|
|
1392
1425
|
- `Provider.fromClass(Logger)`
|
|
1393
1426
|
- `Provider.fromValue(logger)`
|
|
1394
|
-
- `new Provider((
|
|
1427
|
+
- `new Provider(({ scope, ...options }) => scope.resolve(Logger, options))`
|
|
1395
1428
|
|
|
1396
1429
|
```typescript
|
|
1397
1430
|
import { arg, bindTo, Container, inject, lazy, Provider, register, Registration as R } from 'ts-ioc-container';
|
|
@@ -1466,7 +1499,7 @@ describe('Provider', () => {
|
|
|
1466
1499
|
|
|
1467
1500
|
const container = new Container().register(
|
|
1468
1501
|
'FileService',
|
|
1469
|
-
Provider.fromClass(FileService).addArgsFn((
|
|
1502
|
+
Provider.fromClass(FileService).addArgsFn(({ args = [] }) => [...args, '/var/data']),
|
|
1470
1503
|
);
|
|
1471
1504
|
|
|
1472
1505
|
const service = container.resolve<FileService>('FileService');
|
|
@@ -1481,7 +1514,7 @@ describe('Provider', () => {
|
|
|
1481
1514
|
const container = new Container().register('DbPath', Provider.fromValue('localhost:5432')).register(
|
|
1482
1515
|
'Database',
|
|
1483
1516
|
// Dynamically resolve connection string at creation time
|
|
1484
|
-
Provider.fromClass(Database).addArgsFn((scope) => [`postgres://${scope.resolve('DbPath')}`]),
|
|
1517
|
+
Provider.fromClass(Database).addArgsFn(({ scope }) => [`postgres://${scope.resolve('DbPath')}`]),
|
|
1485
1518
|
);
|
|
1486
1519
|
|
|
1487
1520
|
const db = container.resolve<Database>('Database');
|
|
@@ -1771,7 +1804,7 @@ describe('Auto resolve', function () {
|
|
|
1771
1804
|
Sometimes you want to bind some arguments to provider.
|
|
1772
1805
|
|
|
1773
1806
|
- `provider(appendArgs('someArgument'))`
|
|
1774
|
-
- `provider(appendArgsFn((
|
|
1807
|
+
- `provider(appendArgsFn(({ scope }) => [scope.resolve(Logger), 'someValue']))`
|
|
1775
1808
|
- `Provider.fromClass(Logger).pipe(appendArgs('someArgument'))`
|
|
1776
1809
|
|
|
1777
1810
|
### Dependencies as arguments
|
|
@@ -1783,7 +1816,7 @@ Args are passed to the constructor **as-is** — the library never resolves an `
|
|
|
1783
1816
|
- `ServiceToken.args('literal')` — literal value passed directly
|
|
1784
1817
|
- `ServiceToken.args(ValueToken)` — the token object itself is passed as arg
|
|
1785
1818
|
|
|
1786
|
-
`argToToken(value)` is the helper for a call site that wants "resolve tokens, pass literals through": it returns an `InjectionToken` as-is and wraps anything else in a `ConstantToken`, so `@inject((scope,
|
|
1819
|
+
`argToToken(value)` is the helper for a call site that wants "resolve tokens, pass literals through": it returns an `InjectionToken` as-is and wraps anything else in a `ConstantToken`, so `@inject(({ scope, args = [] }) => argToToken(args[0]).resolve(scope))` accepts either.
|
|
1787
1820
|
|
|
1788
1821
|
### Positional arg injection with `arg(index)`, `args`, and `argsFn`
|
|
1789
1822
|
|
|
@@ -1793,7 +1826,7 @@ Constructor parameters that should pick up positional args from `ProviderOptions
|
|
|
1793
1826
|
- `@inject(args)` — resolves the whole runtime `args` array
|
|
1794
1827
|
- Works together with `token.args(...)` / `token.argsFn(...)` to pass typed dependencies through the args context
|
|
1795
1828
|
|
|
1796
|
-
`argsFn(predicate)` is the general form: it iterates the runtime `args` array and returns the **first argument matching** `predicate(value, index)` — think `args.find(predicate)`. `arg(index)` is just a shortcut for matching by position: `arg(0)` is `argsFn((value, index) => index === 0)`. `args` is `(
|
|
1829
|
+
`argsFn(predicate)` is the general form: it iterates the runtime `args` array and returns the **first argument matching** `predicate(value, index)` — think `args.find(predicate)`. `arg(index)` is just a shortcut for matching by position: `arg(0)` is `argsFn((value, index) => index === 0)`. `args` is `({ args }) => args`, i.e. it returns the runtime args array as-is. Every `InjectFn` receives one `ProviderOptions` object — `{ scope, args, lazy }` — where `args` is the runtime args array.
|
|
1797
1830
|
|
|
1798
1831
|
`findOrFail(predicate)` is the strict counterpart for `singleton()` cache keys — `singleton(findOrFail(isUserId))` keys a per-argument singleton, for example. It receives the full runtime `args` array, returns the first argument matching `predicate(value)`, and throws `ArgumentNotFoundError` when none does, instead of silently handing out `undefined`.
|
|
1799
1832
|
|
|
@@ -1801,7 +1834,7 @@ Constructor parameters that should pick up positional args from `ProviderOptions
|
|
|
1801
1834
|
|
|
1802
1835
|
Every container-backed token (`SingleToken`, `ClassToken`, `SingleAliasToken`, `GroupAliasToken`, `FunctionToken`) forwards the `args` of its own `resolve` call to the provider, exactly like `container.resolve(key, { args })` does. `token.args(...)` and `token.argsFn(...)` **append after** the runtime args, so `token.args('x').resolve(container, { args: ['r'] })` hands the provider `['r', 'x']`.
|
|
1803
1836
|
|
|
1804
|
-
|
|
1837
|
+
Every `@inject` function is called with the args of the class being constructed, so an `InjectFn` which hands them on — `by(Token)` does, being `({ scope, ...options }) => Token.resolve(scope, options)` — **cascades** them into the injected dependency: they reach the dependency's provider, its `@inject(arg(index))` parameters, its `scopeAccess` rule and its `singleton()` cache key. One that does not — `({ scope }) => Token.resolve(scope)` — resolves the dependency with no args at all; which one a parameter wants is written at the parameter. The cascade is what lets a per-user `UserService` share a per-user `UserRepository` without passing the id along by hand:
|
|
1805
1838
|
|
|
1806
1839
|
```typescript
|
|
1807
1840
|
import {
|
|
@@ -1814,6 +1847,7 @@ import {
|
|
|
1814
1847
|
Registration as R,
|
|
1815
1848
|
singleton,
|
|
1816
1849
|
SingleToken,
|
|
1850
|
+
by,
|
|
1817
1851
|
} from 'ts-ioc-container';
|
|
1818
1852
|
|
|
1819
1853
|
interface IUserRepository {
|
|
@@ -1830,7 +1864,7 @@ class UserRepository implements IUserRepository {
|
|
|
1830
1864
|
}
|
|
1831
1865
|
|
|
1832
1866
|
class UserService {
|
|
1833
|
-
constructor(@inject(IUserRepositoryKey) public repository: IUserRepository) {}
|
|
1867
|
+
constructor(@inject(by(IUserRepositoryKey)) public repository: IUserRepository) {}
|
|
1834
1868
|
}
|
|
1835
1869
|
|
|
1836
1870
|
describe('Token Runtime Arguments', function () {
|
|
@@ -1881,6 +1915,7 @@ import {
|
|
|
1881
1915
|
Registration as R,
|
|
1882
1916
|
SingleToken,
|
|
1883
1917
|
singleton,
|
|
1918
|
+
by,
|
|
1884
1919
|
} from 'ts-ioc-container';
|
|
1885
1920
|
|
|
1886
1921
|
/**
|
|
@@ -1938,7 +1973,7 @@ describe('IProvider', function () {
|
|
|
1938
1973
|
}
|
|
1939
1974
|
|
|
1940
1975
|
// Extract 'env' from Config service dynamically
|
|
1941
|
-
@register(appendArgsFn((scope) => [scope.resolve<Config>('Config').env]))
|
|
1976
|
+
@register(appendArgsFn(({ scope }) => [scope.resolve<Config>('Config').env]))
|
|
1942
1977
|
class Service {
|
|
1943
1978
|
constructor(@inject(arg(0)) public env: string) {}
|
|
1944
1979
|
}
|
|
@@ -1974,7 +2009,7 @@ describe('IProvider', function () {
|
|
|
1974
2009
|
tenant = 'tenant-a';
|
|
1975
2010
|
}
|
|
1976
2011
|
|
|
1977
|
-
@register(appendArgs('fixed'), appendArgsFn((scope) => [scope.resolve<Config>('Config').tenant]))
|
|
2012
|
+
@register(appendArgs('fixed'), appendArgsFn(({ scope }) => [scope.resolve<Config>('Config').tenant]))
|
|
1978
2013
|
class Service {
|
|
1979
2014
|
constructor(
|
|
1980
2015
|
@inject(arg(0)) public runtime: string,
|
|
@@ -2033,11 +2068,11 @@ describe('IProvider', function () {
|
|
|
2033
2068
|
class App {
|
|
2034
2069
|
constructor(
|
|
2035
2070
|
// Inject EntityManager configured for Users
|
|
2036
|
-
@inject(withRepository(UserRepositoryToken))
|
|
2071
|
+
@inject(by(withRepository(UserRepositoryToken)))
|
|
2037
2072
|
public userManager: EntityManager,
|
|
2038
2073
|
|
|
2039
2074
|
// Inject EntityManager configured for Todos
|
|
2040
|
-
@inject(withRepository(TodoRepositoryToken))
|
|
2075
|
+
@inject(by(withRepository(TodoRepositoryToken)))
|
|
2041
2076
|
public todoManager: EntityManager,
|
|
2042
2077
|
) {}
|
|
2043
2078
|
}
|
|
@@ -2200,6 +2235,7 @@ import {
|
|
|
2200
2235
|
Registration as R,
|
|
2201
2236
|
scope,
|
|
2202
2237
|
select as s,
|
|
2238
|
+
by,
|
|
2203
2239
|
} from 'ts-ioc-container';
|
|
2204
2240
|
|
|
2205
2241
|
/**
|
|
@@ -2254,7 +2290,10 @@ describe('alias', () => {
|
|
|
2254
2290
|
it('should notify through all channels', () => {
|
|
2255
2291
|
// NotificationManager broadcasts to ALL registered channels
|
|
2256
2292
|
class NotificationManager {
|
|
2257
|
-
constructor(
|
|
2293
|
+
constructor(
|
|
2294
|
+
@inject(by(s.alias(INotificationChannel)))
|
|
2295
|
+
private channels: INotificationChannel[],
|
|
2296
|
+
) {}
|
|
2258
2297
|
|
|
2259
2298
|
notifyUser(userId: string, message: string): void {
|
|
2260
2299
|
for (const channel of this.channels) {
|
|
@@ -2346,6 +2385,7 @@ import {
|
|
|
2346
2385
|
Registration as R,
|
|
2347
2386
|
select as s,
|
|
2348
2387
|
singleton,
|
|
2388
|
+
by,
|
|
2349
2389
|
} from 'ts-ioc-container';
|
|
2350
2390
|
|
|
2351
2391
|
/**
|
|
@@ -2391,7 +2431,7 @@ describe('Decorator Pattern', () => {
|
|
|
2391
2431
|
class LoggingRepository implements IRepository {
|
|
2392
2432
|
constructor(
|
|
2393
2433
|
@inject(arg(0)) private repository: IRepository,
|
|
2394
|
-
@inject(s.token('Logger').lazy()) private logger: Logger,
|
|
2434
|
+
@inject(by(s.token('Logger').lazy())) private logger: Logger,
|
|
2395
2435
|
) {}
|
|
2396
2436
|
|
|
2397
2437
|
async save(item: Todo): Promise<void> {
|
|
@@ -2415,7 +2455,7 @@ describe('Decorator Pattern', () => {
|
|
|
2415
2455
|
}
|
|
2416
2456
|
|
|
2417
2457
|
class App {
|
|
2418
|
-
constructor(@inject('IRepository') public repository: IRepository) {}
|
|
2458
|
+
constructor(@inject(by('IRepository')) public repository: IRepository) {}
|
|
2419
2459
|
|
|
2420
2460
|
async run() {
|
|
2421
2461
|
await this.repository.save({ id: '1', text: 'Buy groceries' });
|
|
@@ -2533,7 +2573,7 @@ Registration is provider factory which registers provider in container.
|
|
|
2533
2573
|
- `Registration.fromClass(Logger).bindTo('logger')`
|
|
2534
2574
|
- `Registration.fromClass(Logger)`
|
|
2535
2575
|
- `Registration.fromValue(Logger)`
|
|
2536
|
-
- `Registration.fromFn((
|
|
2576
|
+
- `Registration.fromFn(({ scope, ...options }) => scope.resolve(Logger, options))`
|
|
2537
2577
|
|
|
2538
2578
|
### Token
|
|
2539
2579
|
|
|
@@ -2673,6 +2713,7 @@ import {
|
|
|
2673
2713
|
scope,
|
|
2674
2714
|
select,
|
|
2675
2715
|
singleton,
|
|
2716
|
+
by,
|
|
2676
2717
|
} from 'ts-ioc-container';
|
|
2677
2718
|
|
|
2678
2719
|
/**
|
|
@@ -2732,7 +2773,10 @@ describe('Scopes', function () {
|
|
|
2732
2773
|
|
|
2733
2774
|
// RequestHandler can create a transaction scope for database operations
|
|
2734
2775
|
class RequestHandler {
|
|
2735
|
-
constructor(
|
|
2776
|
+
constructor(
|
|
2777
|
+
@inject(by(select.scope.create({ tags: ['transaction'] })))
|
|
2778
|
+
public transactionScope: IContainer,
|
|
2779
|
+
) {}
|
|
2736
2780
|
|
|
2737
2781
|
executeInTransaction(): boolean {
|
|
2738
2782
|
// Transaction scope inherits from request scope
|
|
@@ -2751,6 +2795,98 @@ describe('Scopes', function () {
|
|
|
2751
2795
|
|
|
2752
2796
|
```
|
|
2753
2797
|
|
|
2798
|
+
### Composing decorators
|
|
2799
|
+
|
|
2800
|
+
A decorator stack repeated on every class of a layer is worth a name.
|
|
2801
|
+
`createComposeClassDecorator(...decorators)` gives it one: it returns a single
|
|
2802
|
+
`ClassDecorator` which applies the stack **bottom-up, exactly as stacking would**,
|
|
2803
|
+
so `@createComposeClassDecorator(a, b)` behaves like `@a @b`. A decorator which
|
|
2804
|
+
returns a replacement class hands it to the next one, the way the runtime threads
|
|
2805
|
+
a stack.
|
|
2806
|
+
|
|
2807
|
+
`createComposeMethodDecorator` and `createComposeParameterDecorator` do the same
|
|
2808
|
+
for methods and constructor parameters. The method form threads the property
|
|
2809
|
+
descriptor, so wrapping decorators (`@once`, `@throttle`, ...) compose too.
|
|
2810
|
+
|
|
2811
|
+
```typescript
|
|
2812
|
+
import {
|
|
2813
|
+
addClassMeta,
|
|
2814
|
+
Container,
|
|
2815
|
+
createComposeClassDecorator,
|
|
2816
|
+
createComposeParameterDecorator,
|
|
2817
|
+
getClassMeta,
|
|
2818
|
+
inject,
|
|
2819
|
+
by,
|
|
2820
|
+
addParamLabel,
|
|
2821
|
+
getParamLabels,
|
|
2822
|
+
register,
|
|
2823
|
+
Registration as R,
|
|
2824
|
+
scope,
|
|
2825
|
+
singleton,
|
|
2826
|
+
SingleToken,
|
|
2827
|
+
} from 'ts-ioc-container';
|
|
2828
|
+
|
|
2829
|
+
/**
|
|
2830
|
+
* A decorator stack repeated on every class of a layer is worth a name.
|
|
2831
|
+
* `createComposeClassDecorator` (and its `createComposeMethodDecorator` /
|
|
2832
|
+
* `createComposeParameterDecorator` siblings) turns one into a single decorator,
|
|
2833
|
+
* applied bottom-up exactly as stacking would.
|
|
2834
|
+
*/
|
|
2835
|
+
describe('composing decorators', () => {
|
|
2836
|
+
const INJECTION_TOKEN = 'injection-token';
|
|
2837
|
+
|
|
2838
|
+
// Every repository binds to its own token, is an application-scoped singleton,
|
|
2839
|
+
// and remembers the token it was registered under.
|
|
2840
|
+
const repository = <T>(token: SingleToken<T>) =>
|
|
2841
|
+
createComposeClassDecorator(
|
|
2842
|
+
register(
|
|
2843
|
+
token,
|
|
2844
|
+
scope((s) => s.hasTag('application')),
|
|
2845
|
+
singleton(),
|
|
2846
|
+
),
|
|
2847
|
+
addClassMeta(INJECTION_TOKEN, () => token),
|
|
2848
|
+
);
|
|
2849
|
+
|
|
2850
|
+
it('should apply the whole stack the composed decorator stands for', () => {
|
|
2851
|
+
const UserRepositoryToken = new SingleToken<UserRepository>('IUserRepository');
|
|
2852
|
+
|
|
2853
|
+
@repository(UserRepositoryToken)
|
|
2854
|
+
class UserRepository {
|
|
2855
|
+
findById(id: string) {
|
|
2856
|
+
return { id };
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
const app = new Container({ tags: ['application'] }).addRegistration(R.fromClass(UserRepository));
|
|
2861
|
+
|
|
2862
|
+
expect(UserRepositoryToken.resolve(app)).toBeInstanceOf(UserRepository);
|
|
2863
|
+
// singleton() applied, so the same instance comes back
|
|
2864
|
+
expect(UserRepositoryToken.resolve(app)).toBe(UserRepositoryToken.resolve(app));
|
|
2865
|
+
// and the class still carries the metadata written beside the registration
|
|
2866
|
+
expect(getClassMeta(UserRepository, INJECTION_TOKEN)).toBe(UserRepositoryToken);
|
|
2867
|
+
});
|
|
2868
|
+
|
|
2869
|
+
it('should compose parameter decorators the same way', () => {
|
|
2870
|
+
const ConfigToken = new SingleToken<{ apiUrl: string }>('IConfig');
|
|
2871
|
+
|
|
2872
|
+
// @inject plus a label describing where the value came from
|
|
2873
|
+
const fromConfig = createComposeParameterDecorator(inject(by(ConfigToken)), addParamLabel('source', 'config'));
|
|
2874
|
+
|
|
2875
|
+
class ApiClient {
|
|
2876
|
+
constructor(@fromConfig public config: { apiUrl: string }) {}
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
const app = new Container({ tags: ['application'] }).addRegistration(
|
|
2880
|
+
R.fromValue({ apiUrl: 'https://api.example.com' }).bindTo(ConfigToken),
|
|
2881
|
+
);
|
|
2882
|
+
|
|
2883
|
+
expect(app.resolve(ApiClient).config.apiUrl).toBe('https://api.example.com');
|
|
2884
|
+
expect(getParamLabels(ApiClient, 0).get('source')).toBe('config');
|
|
2885
|
+
});
|
|
2886
|
+
});
|
|
2887
|
+
|
|
2888
|
+
```
|
|
2889
|
+
|
|
2754
2890
|
## Module
|
|
2755
2891
|
|
|
2756
2892
|
Sometimes you want to encapsulate registration logic in separate module. This is what `IContainerModule` is for.
|
|
@@ -3103,6 +3239,7 @@ import {
|
|
|
3103
3239
|
toTask,
|
|
3104
3240
|
type ExecutionContext,
|
|
3105
3241
|
type HookAction,
|
|
3242
|
+
by,
|
|
3106
3243
|
} from 'ts-ioc-container';
|
|
3107
3244
|
|
|
3108
3245
|
const execute: HookFn = (ctx) => {
|
|
@@ -3151,7 +3288,7 @@ describe('onConstruct', function () {
|
|
|
3151
3288
|
connectionString = '';
|
|
3152
3289
|
|
|
3153
3290
|
@onConstruct(execute)
|
|
3154
|
-
connect(@inject('ConnectionString') connectionString: string) {
|
|
3291
|
+
connect(@inject(by('ConnectionString')) connectionString: string) {
|
|
3155
3292
|
this.connectionString = connectionString;
|
|
3156
3293
|
this.isConnected = true;
|
|
3157
3294
|
}
|
|
@@ -3229,7 +3366,7 @@ describe('onConstruct', function () {
|
|
|
3229
3366
|
}
|
|
3230
3367
|
|
|
3231
3368
|
@onConstruct(executeAsync)
|
|
3232
|
-
async connect(@inject('ConnectionString') connectionString: string) {
|
|
3369
|
+
async connect(@inject(by('ConnectionString')) connectionString: string) {
|
|
3233
3370
|
await Promise.resolve();
|
|
3234
3371
|
this.connectionString = connectionString;
|
|
3235
3372
|
this.isConnected = true;
|
|
@@ -3300,6 +3437,7 @@ import {
|
|
|
3300
3437
|
type ExecutionContext,
|
|
3301
3438
|
type HookAction,
|
|
3302
3439
|
type IContainerModule,
|
|
3440
|
+
by,
|
|
3303
3441
|
} from 'ts-ioc-container';
|
|
3304
3442
|
|
|
3305
3443
|
const execute: HookFn = (ctx) => {
|
|
@@ -3349,7 +3487,7 @@ class LogsRepo {
|
|
|
3349
3487
|
class Logger {
|
|
3350
3488
|
private messages: string[] = [];
|
|
3351
3489
|
|
|
3352
|
-
constructor(@inject('logsRepo') private logsRepo: LogsRepo) {}
|
|
3490
|
+
constructor(@inject(by('logsRepo')) private logsRepo: LogsRepo) {}
|
|
3353
3491
|
|
|
3354
3492
|
log(message: string): void {
|
|
3355
3493
|
this.messages.push(message);
|
|
@@ -3384,7 +3522,7 @@ describe('onScopeDisposed', function () {
|
|
|
3384
3522
|
|
|
3385
3523
|
```typescript
|
|
3386
3524
|
import 'reflect-metadata';
|
|
3387
|
-
import { Container, hook, HookCollector, injectProp, Registration, sequential, toTask } from 'ts-ioc-container';
|
|
3525
|
+
import { Container, hook, HookCollector, injectProp, Registration, sequential, toTask, by } from 'ts-ioc-container';
|
|
3388
3526
|
|
|
3389
3527
|
/**
|
|
3390
3528
|
* UI Components - Property Injection
|
|
@@ -3404,7 +3542,7 @@ describe('inject property', () => {
|
|
|
3404
3542
|
|
|
3405
3543
|
class UserViewModel {
|
|
3406
3544
|
// Inject 'GreetingService' into 'greeting' property during 'onInit'
|
|
3407
|
-
@hook('onInit', injectProp('GreetingService'))
|
|
3545
|
+
@hook('onInit', injectProp(by('GreetingService')))
|
|
3408
3546
|
greetingService!: string;
|
|
3409
3547
|
|
|
3410
3548
|
display(): string {
|
|
@@ -3435,7 +3573,7 @@ describe('inject property', () => {
|
|
|
3435
3573
|
class UserViewModel {
|
|
3436
3574
|
@hook(
|
|
3437
3575
|
'onInit',
|
|
3438
|
-
sequential(injectProp('GreetingService'), (context) => {
|
|
3576
|
+
sequential(injectProp(by('GreetingService')), (context) => {
|
|
3439
3577
|
injectedValue = context.getProperty();
|
|
3440
3578
|
}),
|
|
3441
3579
|
)
|