@declaro/core 2.0.0-beta.8 → 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.
Files changed (90) hide show
  1. package/dist/app/app-context.d.ts +8 -0
  2. package/dist/app/app-lifecycle.d.ts +4 -0
  3. package/dist/app/app.d.ts +22 -0
  4. package/dist/app/index.d.ts +3 -20
  5. package/dist/auth/permission-validator.d.ts +34 -0
  6. package/dist/auth/permission-validator.test.d.ts +1 -0
  7. package/dist/context/context.d.ts +88 -13
  8. package/dist/context/legacy-context.test.d.ts +1 -0
  9. package/dist/errors/errors.d.ts +36 -0
  10. package/dist/events/event-manager.d.ts +11 -6
  11. package/dist/http/headers.d.ts +4 -0
  12. package/dist/http/headers.spec.d.ts +1 -0
  13. package/dist/http/request-context.d.ts +12 -0
  14. package/dist/http/request-context.spec.d.ts +1 -0
  15. package/dist/http/request.d.ts +8 -0
  16. package/dist/http/request.spec.d.ts +1 -0
  17. package/dist/http/url.d.ts +8 -0
  18. package/dist/http/url.spec.d.ts +1 -0
  19. package/dist/index.d.ts +9 -3
  20. package/dist/pkg.cjs +30 -2
  21. package/dist/pkg.mjs +56461 -207
  22. package/dist/schema/application.d.ts +83 -0
  23. package/dist/schema/application.test.d.ts +1 -0
  24. package/dist/schema/define-model.d.ts +7 -4
  25. package/dist/schema/index.d.ts +7 -0
  26. package/dist/schema/labels.d.ts +13 -0
  27. package/dist/schema/labels.test.d.ts +1 -0
  28. package/dist/schema/module.d.ts +7 -0
  29. package/dist/schema/module.test.d.ts +1 -0
  30. package/dist/schema/properties.d.ts +19 -0
  31. package/dist/schema/response.d.ts +31 -0
  32. package/dist/schema/response.test.d.ts +1 -0
  33. package/dist/schema/transform-model.d.ts +1 -1
  34. package/dist/schema/types.d.ts +81 -15
  35. package/dist/schema/types.test.d.ts +1 -0
  36. package/dist/typescript/constant-manipulation/snake-case.d.ts +22 -0
  37. package/dist/typescript/index.d.ts +1 -0
  38. package/dist/typescript/objects.d.ts +6 -0
  39. package/package.json +8 -3
  40. package/src/app/app-context.ts +14 -0
  41. package/src/app/app-lifecycle.ts +14 -0
  42. package/src/app/app.ts +45 -0
  43. package/src/app/index.ts +3 -34
  44. package/src/auth/permission-validator.test.ts +209 -0
  45. package/src/auth/permission-validator.ts +135 -0
  46. package/src/context/context.test.ts +585 -94
  47. package/src/context/context.ts +348 -32
  48. package/src/context/legacy-context.test.ts +141 -0
  49. package/src/errors/errors.ts +73 -0
  50. package/src/events/event-manager.spec.ts +54 -8
  51. package/src/events/event-manager.ts +40 -24
  52. package/src/http/headers.spec.ts +48 -0
  53. package/src/http/headers.ts +16 -0
  54. package/src/http/request-context.spec.ts +39 -0
  55. package/src/http/request-context.ts +43 -0
  56. package/src/http/request.spec.ts +52 -0
  57. package/src/http/request.ts +22 -0
  58. package/src/http/url.spec.ts +87 -0
  59. package/src/http/url.ts +48 -0
  60. package/src/index.ts +9 -3
  61. package/src/schema/application.test.ts +286 -0
  62. package/src/schema/application.ts +150 -0
  63. package/src/schema/define-model.test.ts +48 -2
  64. package/src/schema/define-model.ts +40 -9
  65. package/src/schema/index.ts +7 -0
  66. package/src/schema/labels.test.ts +60 -0
  67. package/src/schema/labels.ts +30 -0
  68. package/src/schema/module.test.ts +39 -0
  69. package/src/schema/module.ts +6 -0
  70. package/src/schema/properties.ts +40 -0
  71. package/src/schema/response.test.ts +101 -0
  72. package/src/schema/response.ts +93 -0
  73. package/src/schema/transform-model.ts +1 -1
  74. package/src/schema/types.test.ts +28 -0
  75. package/src/schema/types.ts +135 -15
  76. package/src/typescript/constant-manipulation/snake-case.md +496 -0
  77. package/src/typescript/constant-manipulation/snake-case.ts +76 -0
  78. package/src/typescript/index.ts +1 -0
  79. package/src/typescript/objects.ts +8 -5
  80. package/tsconfig.json +4 -1
  81. package/dist/context/index.d.ts +0 -3
  82. package/dist/interfaces/IDatastoreProvider.d.ts +0 -16
  83. package/dist/interfaces/IStore.d.ts +0 -4
  84. package/dist/interfaces/index.d.ts +0 -2
  85. package/dist/server/index.d.ts +0 -2
  86. package/src/context/index.ts +0 -3
  87. package/src/interfaces/IDatastoreProvider.ts +0 -23
  88. package/src/interfaces/IStore.ts +0 -4
  89. package/src/interfaces/index.ts +0 -2
  90. package/src/server/index.ts +0 -3
@@ -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 { merge } from '../dataflow'
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 type ContextAttribute<T> = {
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?: T
42
+ value?: ValueLoader<TContext, TValue>
43
+ type: DependencyType
44
+ resolveOptions: ResolveOptions
45
+ cachedValue?: TValue
46
+ inject: PropertyKey[]
19
47
  }
20
48
 
21
- export type ContextListener = (context: Context, ...args: any[]) => any
49
+ export type ScopeKey<S extends object> = keyof S
22
50
 
23
- export class Context {
24
- private readonly state: ContextState = {}
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
- provide<T>(key: PropertyKey, payload: StateValue<T>) {
34
- let value: T | undefined = payload
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
- const attribute: ContextAttribute<T> = {
37
- value,
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.state[key as any] = attribute
156
+ this.register(key, attribute)
157
+
158
+ return this
42
159
  }
43
160
 
44
161
  /**
45
- * Extract a value from context.
162
+ * Register a dependency as a factory in context scope.
46
163
  *
47
- * @param key
48
- * @returns
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
- inject<T = any>(key: PropertyKey): T | undefined {
51
- const attribute = this.state[key as any]
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 undefined
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 attribute.value
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: PropertyKey, instance: T) {
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
- return contexts.reduce((workingState, context) => {
97
- return merge(workingState, context.state)
98
- }, this.state)
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, listener)
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, ...args: any[]) {
152
- return await this.emitter.emitAsync(event, this, ...args)
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
+ }