@declaro/core 2.0.0-beta.9 → 2.0.0-y.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/dist/app/app-context.d.ts +8 -0
- package/dist/app/app-lifecycle.d.ts +4 -0
- package/dist/app/app.d.ts +22 -0
- package/dist/app/index.d.ts +3 -20
- package/dist/auth/permission-validator.d.ts +34 -0
- package/dist/auth/permission-validator.test.d.ts +1 -0
- package/dist/context/context.d.ts +88 -13
- package/dist/context/legacy-context.test.d.ts +1 -0
- package/dist/errors/errors.d.ts +36 -0
- package/dist/events/event-manager.d.ts +11 -6
- package/dist/http/headers.d.ts +4 -0
- package/dist/http/headers.spec.d.ts +1 -0
- package/dist/http/request-context.d.ts +12 -0
- package/dist/http/request-context.spec.d.ts +1 -0
- package/dist/http/request.d.ts +8 -0
- package/dist/http/request.spec.d.ts +1 -0
- package/dist/http/url.d.ts +8 -0
- package/dist/http/url.spec.d.ts +1 -0
- package/dist/index.d.ts +9 -3
- package/dist/pkg.cjs +30 -2
- package/dist/pkg.mjs +56461 -207
- package/dist/schema/application.d.ts +83 -0
- package/dist/schema/application.test.d.ts +1 -0
- package/dist/schema/define-model.d.ts +7 -4
- package/dist/schema/index.d.ts +7 -0
- package/dist/schema/labels.d.ts +13 -0
- package/dist/schema/labels.test.d.ts +1 -0
- package/dist/schema/module.d.ts +7 -0
- package/dist/schema/module.test.d.ts +1 -0
- package/dist/schema/properties.d.ts +19 -0
- package/dist/schema/response.d.ts +31 -0
- package/dist/schema/response.test.d.ts +1 -0
- package/dist/schema/transform-model.d.ts +1 -1
- package/dist/schema/types.d.ts +81 -15
- package/dist/schema/types.test.d.ts +1 -0
- package/dist/typescript/constant-manipulation/snake-case.d.ts +22 -0
- package/dist/typescript/index.d.ts +1 -0
- package/dist/typescript/objects.d.ts +6 -0
- package/package.json +8 -3
- package/src/app/app-context.ts +14 -0
- package/src/app/app-lifecycle.ts +14 -0
- package/src/app/app.ts +45 -0
- package/src/app/index.ts +3 -34
- package/src/auth/permission-validator.test.ts +209 -0
- package/src/auth/permission-validator.ts +135 -0
- package/src/context/context.test.ts +585 -94
- package/src/context/context.ts +348 -32
- package/src/context/legacy-context.test.ts +141 -0
- package/src/errors/errors.ts +73 -0
- package/src/events/event-manager.spec.ts +54 -8
- package/src/events/event-manager.ts +40 -24
- package/src/http/headers.spec.ts +48 -0
- package/src/http/headers.ts +16 -0
- package/src/http/request-context.spec.ts +39 -0
- package/src/http/request-context.ts +43 -0
- package/src/http/request.spec.ts +52 -0
- package/src/http/request.ts +22 -0
- package/src/http/url.spec.ts +87 -0
- package/src/http/url.ts +48 -0
- package/src/index.ts +9 -3
- package/src/schema/application.test.ts +286 -0
- package/src/schema/application.ts +150 -0
- package/src/schema/define-model.test.ts +48 -2
- package/src/schema/define-model.ts +40 -9
- package/src/schema/index.ts +7 -0
- package/src/schema/labels.test.ts +60 -0
- package/src/schema/labels.ts +30 -0
- package/src/schema/module.test.ts +39 -0
- package/src/schema/module.ts +6 -0
- package/src/schema/properties.ts +40 -0
- package/src/schema/response.test.ts +101 -0
- package/src/schema/response.ts +93 -0
- package/src/schema/transform-model.ts +1 -1
- package/src/schema/types.test.ts +28 -0
- package/src/schema/types.ts +135 -15
- package/src/typescript/constant-manipulation/snake-case.md +496 -0
- package/src/typescript/constant-manipulation/snake-case.ts +76 -0
- package/src/typescript/index.ts +1 -0
- package/src/typescript/objects.ts +8 -5
- package/tsconfig.json +4 -1
- package/dist/context/index.d.ts +0 -3
- package/dist/interfaces/IDatastoreProvider.d.ts +0 -16
- package/dist/interfaces/IStore.d.ts +0 -4
- package/dist/interfaces/index.d.ts +0 -2
- package/dist/server/index.d.ts +0 -2
- package/src/context/index.ts +0 -3
- package/src/interfaces/IDatastoreProvider.ts +0 -23
- package/src/interfaces/IStore.ts +0 -4
- package/src/interfaces/index.ts +0 -2
- package/src/server/index.ts +0 -3
package/src/context/context.ts
CHANGED
|
@@ -1,60 +1,367 @@
|
|
|
1
|
+
import { EventManager, type IEvent } from '../events/event-manager'
|
|
2
|
+
import type { Class, PromiseOrValue, UnwrapPromise } from '../typescript'
|
|
1
3
|
import { validate, validateAny, type Validator } from '../validation'
|
|
2
|
-
import { EventManager } from '../events/event-manager'
|
|
3
4
|
import { ContextConsumer } from './context-consumer'
|
|
4
|
-
import {
|
|
5
|
+
import { cloneDeep } from 'lodash'
|
|
6
|
+
|
|
7
|
+
export type AppScope = {}
|
|
8
|
+
export type RequestScope = {}
|
|
9
|
+
export type AppContext = Context<AppScope>
|
|
10
|
+
export type RequestContext = Context<RequestScope>
|
|
5
11
|
|
|
6
12
|
export type ContextMiddleware = (context: Context) => any | Promise<any>
|
|
7
|
-
export type ContextState = Record<
|
|
8
|
-
PropertyKey,
|
|
9
|
-
ContextAttribute<StateValue<any>>
|
|
10
|
-
>
|
|
13
|
+
export type ContextState<TContext extends Context> = Record<PropertyKey, ContextAttribute<TContext, StateValue<any>>>
|
|
11
14
|
|
|
12
15
|
export type ContextResolver<T> = (context: Context) => StateValue<T>
|
|
13
16
|
|
|
14
17
|
export type StateValue<T> = T
|
|
15
18
|
|
|
16
|
-
export
|
|
19
|
+
export enum DependencyType {
|
|
20
|
+
VALUE = 'VALUE',
|
|
21
|
+
FACTORY = 'FACTORY',
|
|
22
|
+
CLASS = 'CLASS',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type FactoryFn<T, A extends any[]> = (...args: A) => T
|
|
26
|
+
export type ValueLoader<C extends Context, T> = (context: C) => T
|
|
27
|
+
export type FilterKeysByType<TScope, TValue> = {
|
|
28
|
+
[Key in keyof TScope]: TScope[Key] extends TValue ? Key : never
|
|
29
|
+
}[keyof TScope]
|
|
30
|
+
export type FilterKeysByAsyncType<TScope, TValue> = {
|
|
31
|
+
[Key in keyof TScope]: TScope[Key] extends PromiseOrValue<TValue> ? Key : never
|
|
32
|
+
}[keyof TScope]
|
|
33
|
+
export type FilterArgsByType<TScope, TArgs extends any[]> = {
|
|
34
|
+
[Key in keyof TArgs]: FilterKeysByType<TScope, TArgs[Key]>
|
|
35
|
+
}
|
|
36
|
+
export type FilterAsyncArgsByType<TScope, TArgs extends any[]> = {
|
|
37
|
+
[Key in keyof TArgs]: FilterKeysByAsyncType<TScope, TArgs[Key]>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type ContextAttribute<TContext extends Context<any>, TValue> = {
|
|
17
41
|
key: PropertyKey
|
|
18
|
-
value?:
|
|
42
|
+
value?: ValueLoader<TContext, TValue>
|
|
43
|
+
type: DependencyType
|
|
44
|
+
resolveOptions: ResolveOptions
|
|
45
|
+
cachedValue?: TValue
|
|
46
|
+
inject: PropertyKey[]
|
|
19
47
|
}
|
|
20
48
|
|
|
21
|
-
export type
|
|
49
|
+
export type ScopeKey<S extends object> = keyof S
|
|
22
50
|
|
|
23
|
-
export
|
|
24
|
-
|
|
51
|
+
export type ContextListener = (context: Context) => any
|
|
52
|
+
|
|
53
|
+
export type ResolveOptions = {
|
|
54
|
+
strict?: boolean
|
|
55
|
+
eager?: boolean
|
|
56
|
+
singleton?: boolean
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function defaultResolveOptions(): ResolveOptions {
|
|
60
|
+
return {
|
|
61
|
+
strict: false,
|
|
62
|
+
eager: false,
|
|
63
|
+
singleton: false,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type ContextOptions = {
|
|
68
|
+
defaultResolveOptions?: ResolveOptions
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class Context<Scope extends object = any> {
|
|
72
|
+
private readonly state: ContextState<this> = {}
|
|
25
73
|
private readonly emitter = new EventManager()
|
|
26
74
|
|
|
75
|
+
public readonly scope: Scope = {} as Scope
|
|
76
|
+
|
|
77
|
+
protected readonly defaultResolveOptions: ResolveOptions
|
|
78
|
+
|
|
79
|
+
constructor(options?: ContextOptions) {
|
|
80
|
+
this.defaultResolveOptions = {
|
|
81
|
+
...defaultResolveOptions(),
|
|
82
|
+
...options?.defaultResolveOptions,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
27
86
|
/**
|
|
28
87
|
* Set a value in context, to be injected later.
|
|
29
88
|
*
|
|
30
89
|
* @param key
|
|
31
90
|
* @param payload
|
|
91
|
+
* @deprecated Use `provideValue` instead, or you can register the same dependency as a factory with `provideFactory` or class with `provideClass`.
|
|
92
|
+
*/
|
|
93
|
+
provide<K extends ScopeKey<Scope>>(key: K, payload: Scope[K]) {
|
|
94
|
+
const attribute: ContextAttribute<this, Scope[K]> = {
|
|
95
|
+
value: () => payload,
|
|
96
|
+
key,
|
|
97
|
+
type: DependencyType.VALUE,
|
|
98
|
+
resolveOptions: {
|
|
99
|
+
eager: false,
|
|
100
|
+
singleton: true,
|
|
101
|
+
strict: false,
|
|
102
|
+
},
|
|
103
|
+
inject: [],
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this.state[key] = attribute
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Manually register a dependency. This should normally be used by utils or integrations that need to register dependencies in creative ways. For normal use cases, using `provideValue`, `provideFactory`, or `provideClass` is sufficient.
|
|
111
|
+
*
|
|
112
|
+
* @param key The key to register the dependency under
|
|
113
|
+
* @param dep The dependency record
|
|
32
114
|
*/
|
|
33
|
-
|
|
34
|
-
|
|
115
|
+
register<K extends ScopeKey<Scope>>(key: K, dep: ContextAttribute<this, Scope[K]>) {
|
|
116
|
+
const existingDep = this.state[key]
|
|
117
|
+
this.state[key] = dep
|
|
118
|
+
|
|
119
|
+
Object.defineProperty(this.scope, key, {
|
|
120
|
+
get: () => this.resolve(key),
|
|
121
|
+
enumerable: true,
|
|
122
|
+
configurable: true,
|
|
123
|
+
})
|
|
35
124
|
|
|
36
|
-
|
|
37
|
-
|
|
125
|
+
if (dep?.resolveOptions?.eager) {
|
|
126
|
+
this.on('declaro:init', async () => {
|
|
127
|
+
await this.resolve(key)
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// kill any cached values that were made by a previous instance of this attribute
|
|
132
|
+
if (existingDep) {
|
|
133
|
+
const dependents = this.getAllDependents(key)
|
|
134
|
+
|
|
135
|
+
dependents.forEach((dependent) => {
|
|
136
|
+
dependent.cachedValue = undefined
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Register a value in context scope.
|
|
143
|
+
*
|
|
144
|
+
* @param key The key to register the dependency under
|
|
145
|
+
* @param value The value to register
|
|
146
|
+
*/
|
|
147
|
+
registerValue<K extends ScopeKey<Scope>>(key: K, value: Scope[K], defaultResolveOptions?: ResolveOptions) {
|
|
148
|
+
const attribute: ContextAttribute<this, Scope[K]> = {
|
|
149
|
+
value: () => value,
|
|
38
150
|
key,
|
|
151
|
+
type: DependencyType.VALUE,
|
|
152
|
+
resolveOptions: defaultResolveOptions,
|
|
153
|
+
inject: [],
|
|
39
154
|
}
|
|
40
155
|
|
|
41
|
-
this.
|
|
156
|
+
this.register(key, attribute)
|
|
157
|
+
|
|
158
|
+
return this
|
|
42
159
|
}
|
|
43
160
|
|
|
44
161
|
/**
|
|
45
|
-
*
|
|
162
|
+
* Register a dependency as a factory in context scope.
|
|
46
163
|
*
|
|
47
|
-
* @param key
|
|
48
|
-
* @
|
|
164
|
+
* @param key The key to register the dependency under
|
|
165
|
+
* @param factory A factory function that will be called to generate the value when it is requested.
|
|
166
|
+
* @param inject An array of keys to use when injecting factory args.
|
|
167
|
+
* @returns A chainable instance of context
|
|
49
168
|
*/
|
|
50
|
-
|
|
51
|
-
|
|
169
|
+
registerFactory<K extends ScopeKey<Scope>, A extends any[]>(
|
|
170
|
+
key: K,
|
|
171
|
+
factory: FactoryFn<Scope[K], A>,
|
|
172
|
+
inject?: FilterArgsByType<Scope, A>,
|
|
173
|
+
defaultResolveOptions?: ResolveOptions,
|
|
174
|
+
) {
|
|
175
|
+
const attribute: ContextAttribute<this, Scope[K]> = {
|
|
176
|
+
value: (context) => {
|
|
177
|
+
const args = (inject?.map((key) => context.resolve(key)) ?? []) as A
|
|
178
|
+
|
|
179
|
+
return factory(...args)
|
|
180
|
+
},
|
|
181
|
+
key,
|
|
182
|
+
type: DependencyType.FACTORY,
|
|
183
|
+
resolveOptions: defaultResolveOptions,
|
|
184
|
+
inject: inject ?? [],
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.register(key, attribute)
|
|
188
|
+
|
|
189
|
+
return this
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
registerAsyncFactory<K extends FilterKeysByType<Scope, Promise<any>>, A extends any[]>(
|
|
193
|
+
key: K,
|
|
194
|
+
factory: FactoryFn<Scope[K], A>,
|
|
195
|
+
inject?: FilterAsyncArgsByType<Scope, A>,
|
|
196
|
+
defaultResolveOptions?: ResolveOptions,
|
|
197
|
+
) {
|
|
198
|
+
const attribute: ContextAttribute<this, Scope[K]> = {
|
|
199
|
+
value: (async (context) => {
|
|
200
|
+
const args = (await Promise.all((inject?.map((key) => context.resolve(key)) as A) ?? [])) as A
|
|
201
|
+
|
|
202
|
+
return await factory(...args)
|
|
203
|
+
}) as ValueLoader<this, Scope[K]>,
|
|
204
|
+
key,
|
|
205
|
+
type: DependencyType.FACTORY,
|
|
206
|
+
resolveOptions: defaultResolveOptions,
|
|
207
|
+
inject: inject ?? [],
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
this.register(key, attribute)
|
|
211
|
+
|
|
212
|
+
return this
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
registerClass<K extends FilterKeysByType<Scope, InstanceType<T>>, T extends Class<Scope[K]>>(
|
|
216
|
+
key: K,
|
|
217
|
+
Class: T,
|
|
218
|
+
inject?: FilterArgsByType<Scope, ConstructorParameters<T>>,
|
|
219
|
+
defaultResolveOptions?: ResolveOptions,
|
|
220
|
+
) {
|
|
221
|
+
const attribute: ContextAttribute<this, Scope[K]> = {
|
|
222
|
+
value: (context) => {
|
|
223
|
+
const args = inject?.map((key) => context.resolve(key)) ?? []
|
|
224
|
+
|
|
225
|
+
return new (Class as any)(...(args as any))
|
|
226
|
+
},
|
|
227
|
+
key,
|
|
228
|
+
type: DependencyType.CLASS,
|
|
229
|
+
resolveOptions: defaultResolveOptions,
|
|
230
|
+
inject: inject ?? [],
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
this.register(key, attribute)
|
|
234
|
+
|
|
235
|
+
return this
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
registerAsyncClass<K extends FilterKeysByType<Scope, InstanceType<any>>, T extends Class<UnwrapPromise<Scope[K]>>>(
|
|
239
|
+
key: K,
|
|
240
|
+
Class: T,
|
|
241
|
+
inject?: FilterAsyncArgsByType<Scope, ConstructorParameters<T>>,
|
|
242
|
+
defaultResolveOptions?: ResolveOptions,
|
|
243
|
+
) {
|
|
244
|
+
const attribute: ContextAttribute<this, Scope[K]> = {
|
|
245
|
+
value: (async (context) => {
|
|
246
|
+
const args = (await Promise.all(
|
|
247
|
+
(inject?.map((key) => context.resolve(key)) ?? []) as ConstructorParameters<T>,
|
|
248
|
+
)) as ConstructorParameters<T>
|
|
249
|
+
|
|
250
|
+
return new (Class as any)(...(args as any))
|
|
251
|
+
}) as ValueLoader<this, Scope[K]>,
|
|
252
|
+
key,
|
|
253
|
+
type: DependencyType.CLASS,
|
|
254
|
+
resolveOptions: defaultResolveOptions,
|
|
255
|
+
inject: inject ?? [],
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
this.register(key, attribute)
|
|
259
|
+
|
|
260
|
+
return this
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
getAllDependencies<K extends ScopeKey<Scope>>(key: K): ContextAttribute<this, any>[] {
|
|
264
|
+
const attribute = this.state[key]
|
|
52
265
|
|
|
53
266
|
if (!attribute) {
|
|
54
|
-
return
|
|
267
|
+
return []
|
|
268
|
+
}
|
|
269
|
+
const dependencies =
|
|
270
|
+
attribute.inject?.map((key) => this.state[key] ?? ({ key } as ContextAttribute<typeof this, any>)) ?? []
|
|
271
|
+
|
|
272
|
+
attribute.inject?.forEach((key) => {
|
|
273
|
+
const nestedDependencies = this.getAllDependencies(key as any)
|
|
274
|
+
dependencies.push(...nestedDependencies)
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
return dependencies
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
getAllDependents<K extends ScopeKey<Scope>>(key: K): ContextAttribute<this, any>[] {
|
|
281
|
+
const dependents = Object.entries(this.state)
|
|
282
|
+
.filter(([_, attribute]) => attribute.inject?.includes(key))
|
|
283
|
+
.map(([key, attribute]) => attribute)
|
|
284
|
+
|
|
285
|
+
dependents.forEach((dependent) => {
|
|
286
|
+
const nestedDependents = this.getAllDependents(dependent.key as any)
|
|
287
|
+
dependents.push(...nestedDependents)
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
return dependents
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
introspect<K extends ScopeKey<Scope>>(key: K) {
|
|
294
|
+
const attribute = this.state[key]
|
|
295
|
+
|
|
296
|
+
return attribute
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
protected _cacheIsValid<K extends ScopeKey<Scope>>(key: K): boolean {
|
|
300
|
+
const attribute = this.state[key]
|
|
301
|
+
|
|
302
|
+
const needsCache = attribute.resolveOptions?.singleton || attribute.resolveOptions?.eager
|
|
303
|
+
|
|
304
|
+
if (!needsCache) {
|
|
305
|
+
return true
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const hasCachedValue = attribute.cachedValue !== undefined && attribute.cachedValue !== null
|
|
309
|
+
|
|
310
|
+
if (!hasCachedValue) {
|
|
311
|
+
return false
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return hasCachedValue && attribute.inject?.every((key) => this._cacheIsValid(key as any))
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
protected _resolveValue<K extends ScopeKey<Scope>>(key: K, resolveOptions?: ResolveOptions): Scope[K] {
|
|
318
|
+
const attribute = this.state[key]
|
|
319
|
+
|
|
320
|
+
const attributeResolveOptions = {
|
|
321
|
+
...this.defaultResolveOptions,
|
|
322
|
+
...attribute?.resolveOptions,
|
|
323
|
+
...resolveOptions,
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (!attribute && attributeResolveOptions.strict) {
|
|
327
|
+
throw new Error(`Dependency ${key?.toString()} not found.`)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
let value: Scope[K]
|
|
331
|
+
|
|
332
|
+
const serveFromCache = attributeResolveOptions.singleton || attributeResolveOptions.eager
|
|
333
|
+
const dependenciesValid = attribute?.inject?.every((key) => this._cacheIsValid(key as any))
|
|
334
|
+
|
|
335
|
+
if (serveFromCache && attribute?.cachedValue && dependenciesValid) {
|
|
336
|
+
value = attribute.cachedValue
|
|
337
|
+
} else {
|
|
338
|
+
value = attribute?.value(this)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (serveFromCache) {
|
|
342
|
+
attribute.cachedValue = value
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (attributeResolveOptions.strict && (value === undefined || value === null)) {
|
|
346
|
+
throw new Error(`Strict dependency ${key?.toString()} has a ${typeof value} value.`)
|
|
55
347
|
}
|
|
56
348
|
|
|
57
|
-
return
|
|
349
|
+
return value
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Extract a value from context.
|
|
354
|
+
*
|
|
355
|
+
* @param key
|
|
356
|
+
* @returns
|
|
357
|
+
* @deprecated Use `resolve` instead
|
|
358
|
+
*/
|
|
359
|
+
inject<T = any>(key: ScopeKey<Scope>): T | undefined {
|
|
360
|
+
return this._resolveValue(key) as T
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
resolve<K extends ScopeKey<Scope>>(key: K, resolveOptions?: ResolveOptions): Scope[K] {
|
|
364
|
+
return this._resolveValue(key, resolveOptions)
|
|
58
365
|
}
|
|
59
366
|
|
|
60
367
|
/**
|
|
@@ -63,10 +370,10 @@ export class Context {
|
|
|
63
370
|
* @param key
|
|
64
371
|
* @param instance
|
|
65
372
|
*/
|
|
66
|
-
singleton<T = any>(key:
|
|
373
|
+
singleton<T = any>(key: ScopeKey<Scope>, instance: T) {
|
|
67
374
|
const existing = this.inject<T>(key)
|
|
68
375
|
if (!existing) {
|
|
69
|
-
this.provide(key, instance)
|
|
376
|
+
this.provide(key, instance as any)
|
|
70
377
|
return instance
|
|
71
378
|
} else {
|
|
72
379
|
return existing
|
|
@@ -92,10 +399,15 @@ export class Context {
|
|
|
92
399
|
* @param contexts
|
|
93
400
|
* @returns
|
|
94
401
|
*/
|
|
95
|
-
extend(...contexts: Context[]) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
402
|
+
extend(...contexts: Context[]): this {
|
|
403
|
+
contexts.forEach((context) => {
|
|
404
|
+
Reflect.ownKeys(context.state).forEach((key) => {
|
|
405
|
+
const dep = cloneDeep(context.state[key])
|
|
406
|
+
this.register(key as any, dep)
|
|
407
|
+
})
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
return this
|
|
99
411
|
}
|
|
100
412
|
|
|
101
413
|
/**
|
|
@@ -138,7 +450,9 @@ export class Context {
|
|
|
138
450
|
* @returns
|
|
139
451
|
*/
|
|
140
452
|
on(event: string, listener: ContextListener) {
|
|
141
|
-
return this.emitter.on(event,
|
|
453
|
+
return this.emitter.on(event, (_) => {
|
|
454
|
+
return listener(this)
|
|
455
|
+
})
|
|
142
456
|
}
|
|
143
457
|
|
|
144
458
|
/**
|
|
@@ -148,7 +462,9 @@ export class Context {
|
|
|
148
462
|
* @param args
|
|
149
463
|
* @returns
|
|
150
464
|
*/
|
|
151
|
-
async emit(event: string
|
|
152
|
-
return await this.emitter.emitAsync(
|
|
465
|
+
async emit(event: string) {
|
|
466
|
+
return await this.emitter.emitAsync({
|
|
467
|
+
type: event,
|
|
468
|
+
})
|
|
153
469
|
}
|
|
154
470
|
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { Context } from './context'
|
|
2
|
+
import { sleep } from '../timing'
|
|
3
|
+
import { ContextConsumer } from './context-consumer'
|
|
4
|
+
import { describe, it, vi } from 'vitest'
|
|
5
|
+
|
|
6
|
+
describe('Context', () => {
|
|
7
|
+
it('Should allow simple value dependency injection', async ({ expect }) => {
|
|
8
|
+
const context = new Context()
|
|
9
|
+
|
|
10
|
+
context.provide('test', 'Hello World')
|
|
11
|
+
|
|
12
|
+
const message = context.inject('test')
|
|
13
|
+
|
|
14
|
+
expect(message).toBe('Hello World')
|
|
15
|
+
|
|
16
|
+
const TEST_KEY = Symbol()
|
|
17
|
+
context.provide(TEST_KEY, 42)
|
|
18
|
+
|
|
19
|
+
const number = context.inject(TEST_KEY)
|
|
20
|
+
expect(number).toBe(42)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('Should allow subscription to arbitrary events', async ({ expect }) => {
|
|
24
|
+
const customEventCall = vi.fn()
|
|
25
|
+
|
|
26
|
+
const context = new Context()
|
|
27
|
+
context.provide('test', 'Hello World')
|
|
28
|
+
expect(customEventCall.mock.calls.length).toBe(0)
|
|
29
|
+
|
|
30
|
+
context.on('customEvent', async (ctx) => {
|
|
31
|
+
customEventCall()
|
|
32
|
+
const message = await ctx.inject('test')
|
|
33
|
+
expect(message).toBe('Hello World')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
await context.emit('customEvent')
|
|
37
|
+
|
|
38
|
+
expect(customEventCall.mock.calls.length).toBe(1)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('Should extend other contexts', async ({ expect }) => {
|
|
42
|
+
const context1 = new Context()
|
|
43
|
+
const context2 = new Context()
|
|
44
|
+
const context3 = new Context()
|
|
45
|
+
|
|
46
|
+
const SYMBOL_KEY = Symbol()
|
|
47
|
+
|
|
48
|
+
context1.provide('a', 1)
|
|
49
|
+
context1.provide('c', 1)
|
|
50
|
+
context2.provide('b', 2)
|
|
51
|
+
context2.provide('c', 2)
|
|
52
|
+
context2.provide(SYMBOL_KEY, 42)
|
|
53
|
+
context3.provide('c', 3)
|
|
54
|
+
|
|
55
|
+
context1.extend(context2, context3)
|
|
56
|
+
|
|
57
|
+
const a = context1.inject('a')
|
|
58
|
+
const b = context1.inject('b')
|
|
59
|
+
const c = context1.inject('c')
|
|
60
|
+
const symbolValue = context1.inject(SYMBOL_KEY)
|
|
61
|
+
|
|
62
|
+
expect(a).toBe(1)
|
|
63
|
+
expect(b).toBe(2)
|
|
64
|
+
expect(c).toBe(3)
|
|
65
|
+
expect(symbolValue).toBe(42)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('Should nest contexts', async ({ expect }) => {
|
|
69
|
+
const context1 = new Context()
|
|
70
|
+
const context2 = new Context()
|
|
71
|
+
|
|
72
|
+
context1.provide('a', 1)
|
|
73
|
+
context2.provide('a', 2)
|
|
74
|
+
|
|
75
|
+
context1.provide('nested', context2)
|
|
76
|
+
|
|
77
|
+
const a1 = context1.inject('a')
|
|
78
|
+
const a2 = context1.inject<Context>('nested')?.inject('a')
|
|
79
|
+
|
|
80
|
+
expect(a1).toBe(1)
|
|
81
|
+
expect(a2).toBe(2)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('Should allow async middleware', async ({ expect }) => {
|
|
85
|
+
const context = new Context()
|
|
86
|
+
|
|
87
|
+
await context.use(async (context) => {
|
|
88
|
+
context.provide('TEST1', 'Hello World')
|
|
89
|
+
await sleep(1)
|
|
90
|
+
context.provide('TEST2', 'Hello Test')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const test1 = context.inject('TEST1')
|
|
94
|
+
const test2 = context.inject('TEST2')
|
|
95
|
+
expect(test1).toBe('Hello World')
|
|
96
|
+
expect(test2).toBe('Hello Test')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('Should allow singletons', ({ expect }) => {
|
|
100
|
+
const context = new Context()
|
|
101
|
+
|
|
102
|
+
const KEY = Symbol()
|
|
103
|
+
let instance = context.singleton(KEY, 1)
|
|
104
|
+
instance = context.singleton(KEY, 2)
|
|
105
|
+
instance = context.singleton(KEY, 3)
|
|
106
|
+
|
|
107
|
+
expect(instance).toBe(1)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('Should allow hydration', ({ expect }) => {
|
|
111
|
+
const context = new Context()
|
|
112
|
+
|
|
113
|
+
class Test extends ContextConsumer {
|
|
114
|
+
constructor(public context: Context) {
|
|
115
|
+
super(context)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
test() {
|
|
119
|
+
return 'Yes'
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
expect(context.hydrate(Test).test()).toBe('Yes')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('Should allow hydration with args', ({ expect }) => {
|
|
127
|
+
const context = new Context()
|
|
128
|
+
|
|
129
|
+
class Test extends ContextConsumer {
|
|
130
|
+
constructor(public context: Context, foo: string, bar: number) {
|
|
131
|
+
super(context)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
test() {
|
|
135
|
+
return 'Yes'
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
expect(context.hydrate(Test, 'hello', 42).test()).toBe('Yes')
|
|
140
|
+
})
|
|
141
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export interface IError {
|
|
2
|
+
code: number
|
|
3
|
+
message: string
|
|
4
|
+
meta: any
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export abstract class BaseError extends Error implements IError {
|
|
8
|
+
public readonly meta: any
|
|
9
|
+
public readonly code: number = 500
|
|
10
|
+
|
|
11
|
+
constructor(message: string, meta?: any) {
|
|
12
|
+
super(message)
|
|
13
|
+
this.name = 'BaseError'
|
|
14
|
+
this.meta = meta
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public toJSON() {
|
|
18
|
+
return {
|
|
19
|
+
code: this.code,
|
|
20
|
+
message: this.message,
|
|
21
|
+
meta: this.meta,
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public toString() {
|
|
26
|
+
return JSON.stringify(this.toJSON())
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class SystemError extends BaseError {
|
|
31
|
+
public readonly code: number = 500
|
|
32
|
+
|
|
33
|
+
constructor(message: string, meta?: any) {
|
|
34
|
+
super(message, meta)
|
|
35
|
+
this.name = 'SystemError'
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class ValidationError extends BaseError {
|
|
40
|
+
public readonly code: number = 400
|
|
41
|
+
|
|
42
|
+
constructor(message: string, meta?: any) {
|
|
43
|
+
super(message, meta)
|
|
44
|
+
this.name = 'ValidationError'
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class NotFoundError extends BaseError {
|
|
49
|
+
public readonly code: number = 404
|
|
50
|
+
|
|
51
|
+
constructor(message: string, meta?: any) {
|
|
52
|
+
super(message, meta)
|
|
53
|
+
this.name = 'NotFoundError'
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class UnauthorizedError extends BaseError {
|
|
58
|
+
public readonly code: number = 401
|
|
59
|
+
|
|
60
|
+
constructor(message: string, meta?: any) {
|
|
61
|
+
super(message, meta)
|
|
62
|
+
this.name = 'UnauthorizedError'
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class ForbiddenError extends BaseError {
|
|
67
|
+
public readonly code: number = 403
|
|
68
|
+
|
|
69
|
+
constructor(message: string, meta?: any) {
|
|
70
|
+
super(message, meta)
|
|
71
|
+
this.name = 'ForbiddenError'
|
|
72
|
+
}
|
|
73
|
+
}
|