@declaro/core 2.0.0-y.0 → 2.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.
Files changed (266) hide show
  1. package/{LICENSE → LICENSE.md} +1 -1
  2. package/README.md +203 -0
  3. package/dist/browser/index.js +28 -0
  4. package/dist/browser/index.js.map +133 -0
  5. package/dist/browser/scope/index.js +3 -0
  6. package/dist/browser/scope/index.js.map +9 -0
  7. package/dist/bun/index.js +19011 -0
  8. package/dist/bun/index.js.map +132 -0
  9. package/dist/bun/scope/index.js +4 -0
  10. package/dist/bun/scope/index.js.map +9 -0
  11. package/dist/node/index.cjs +19039 -0
  12. package/dist/node/index.cjs.map +132 -0
  13. package/dist/node/index.js +19010 -0
  14. package/dist/node/index.js.map +132 -0
  15. package/dist/node/scope/index.cjs +69 -0
  16. package/dist/node/scope/index.cjs.map +9 -0
  17. package/dist/node/scope/index.js +3 -0
  18. package/dist/node/scope/index.js.map +9 -0
  19. package/dist/ts/app/app-context.d.ts +9 -0
  20. package/dist/ts/app/app-context.d.ts.map +1 -0
  21. package/dist/ts/app/app-lifecycle.d.ts +6 -0
  22. package/dist/ts/app/app-lifecycle.d.ts.map +1 -0
  23. package/dist/ts/app/app.d.ts +24 -0
  24. package/dist/ts/app/app.d.ts.map +1 -0
  25. package/dist/{app → ts/app}/index.d.ts +1 -0
  26. package/dist/ts/app/index.d.ts.map +1 -0
  27. package/dist/ts/application/create-request-context.d.ts +4 -0
  28. package/dist/ts/application/create-request-context.d.ts.map +1 -0
  29. package/dist/ts/application/create-request-context.test.d.ts +2 -0
  30. package/dist/ts/application/create-request-context.test.d.ts.map +1 -0
  31. package/dist/ts/application/use-declaro.d.ts +3 -0
  32. package/dist/ts/application/use-declaro.d.ts.map +1 -0
  33. package/dist/{auth → ts/auth}/permission-validator.d.ts +1 -0
  34. package/dist/ts/auth/permission-validator.d.ts.map +1 -0
  35. package/dist/ts/auth/permission-validator.test.d.ts +2 -0
  36. package/dist/ts/auth/permission-validator.test.d.ts.map +1 -0
  37. package/dist/ts/context/async-context.d.ts +54 -0
  38. package/dist/ts/context/async-context.d.ts.map +1 -0
  39. package/dist/ts/context/async-context.test.d.ts +2 -0
  40. package/dist/ts/context/async-context.test.d.ts.map +1 -0
  41. package/dist/{context → ts/context}/context-consumer.d.ts +4 -0
  42. package/dist/ts/context/context-consumer.d.ts.map +1 -0
  43. package/dist/ts/context/context.circular-deps.test.d.ts +2 -0
  44. package/dist/ts/context/context.circular-deps.test.d.ts.map +1 -0
  45. package/dist/ts/context/context.d.ts +452 -0
  46. package/dist/ts/context/context.d.ts.map +1 -0
  47. package/dist/ts/context/context.test.d.ts +2 -0
  48. package/dist/ts/context/context.test.d.ts.map +1 -0
  49. package/dist/ts/context/legacy-context.test.d.ts +2 -0
  50. package/dist/ts/context/legacy-context.test.d.ts.map +1 -0
  51. package/dist/{context → ts/context}/validators.d.ts +2 -1
  52. package/dist/ts/context/validators.d.ts.map +1 -0
  53. package/dist/ts/dataflow/index.d.ts +2 -0
  54. package/dist/ts/dataflow/index.d.ts.map +1 -0
  55. package/dist/ts/dataflow/objects.d.ts +7 -0
  56. package/dist/ts/dataflow/objects.d.ts.map +1 -0
  57. package/dist/ts/dataflow/objects.test.d.ts +2 -0
  58. package/dist/ts/dataflow/objects.test.d.ts.map +1 -0
  59. package/dist/{errors → ts/errors}/errors.d.ts +16 -3
  60. package/dist/ts/errors/errors.d.ts.map +1 -0
  61. package/dist/ts/events/event-manager.d.ts +19 -0
  62. package/dist/ts/events/event-manager.d.ts.map +1 -0
  63. package/dist/ts/events/event-manager.spec.d.ts +2 -0
  64. package/dist/ts/events/event-manager.spec.d.ts.map +1 -0
  65. package/dist/ts/events/index.d.ts +2 -0
  66. package/dist/ts/events/index.d.ts.map +1 -0
  67. package/dist/ts/http/headers.d.ts +21 -0
  68. package/dist/ts/http/headers.d.ts.map +1 -0
  69. package/dist/ts/http/headers.spec.d.ts +2 -0
  70. package/dist/ts/http/headers.spec.d.ts.map +1 -0
  71. package/dist/ts/http/request-context.d.ts +17 -0
  72. package/dist/ts/http/request-context.d.ts.map +1 -0
  73. package/dist/ts/http/request-context.spec.d.ts +2 -0
  74. package/dist/ts/http/request-context.spec.d.ts.map +1 -0
  75. package/dist/ts/http/request.d.ts +31 -0
  76. package/dist/ts/http/request.d.ts.map +1 -0
  77. package/dist/ts/http/request.spec.d.ts +2 -0
  78. package/dist/ts/http/request.spec.d.ts.map +1 -0
  79. package/dist/{http → ts/http}/url.d.ts +5 -4
  80. package/dist/ts/http/url.d.ts.map +1 -0
  81. package/dist/ts/http/url.spec.d.ts +2 -0
  82. package/dist/ts/http/url.spec.d.ts.map +1 -0
  83. package/dist/ts/index.d.ts +47 -0
  84. package/dist/ts/index.d.ts.map +1 -0
  85. package/dist/{pipelines → ts/pipelines}/index.d.ts +1 -0
  86. package/dist/ts/pipelines/index.d.ts.map +1 -0
  87. package/dist/{pipelines → ts/pipelines}/pipeline-action.d.ts +1 -0
  88. package/dist/ts/pipelines/pipeline-action.d.ts.map +1 -0
  89. package/dist/ts/pipelines/pipeline-action.test.d.ts +2 -0
  90. package/dist/ts/pipelines/pipeline-action.test.d.ts.map +1 -0
  91. package/dist/{pipelines → ts/pipelines}/pipeline.d.ts +3 -2
  92. package/dist/ts/pipelines/pipeline.d.ts.map +1 -0
  93. package/dist/ts/pipelines/pipeline.test.d.ts +2 -0
  94. package/dist/ts/pipelines/pipeline.test.d.ts.map +1 -0
  95. package/dist/ts/schema/json-schema.d.ts +12 -0
  96. package/dist/ts/schema/json-schema.d.ts.map +1 -0
  97. package/dist/ts/schema/labels.d.ts +14 -0
  98. package/dist/ts/schema/labels.d.ts.map +1 -0
  99. package/dist/ts/schema/model-schema.d.ts +75 -0
  100. package/dist/ts/schema/model-schema.d.ts.map +1 -0
  101. package/dist/ts/schema/model-schema.test.d.ts +2 -0
  102. package/dist/ts/schema/model-schema.test.d.ts.map +1 -0
  103. package/dist/ts/schema/model.d.ts +35 -0
  104. package/dist/ts/schema/model.d.ts.map +1 -0
  105. package/dist/ts/schema/schema-mixin.d.ts +24 -0
  106. package/dist/ts/schema/schema-mixin.d.ts.map +1 -0
  107. package/dist/ts/schema/test/mock-model.d.ts +8 -0
  108. package/dist/ts/schema/test/mock-model.d.ts.map +1 -0
  109. package/dist/ts/scope/index.d.ts +34 -0
  110. package/dist/ts/scope/index.d.ts.map +1 -0
  111. package/dist/ts/shared/utils/action-descriptor.d.ts +28 -0
  112. package/dist/ts/shared/utils/action-descriptor.d.ts.map +1 -0
  113. package/dist/ts/shared/utils/action-descriptor.test.d.ts +2 -0
  114. package/dist/ts/shared/utils/action-descriptor.test.d.ts.map +1 -0
  115. package/dist/ts/shared/utils/schema-utils.d.ts +3 -0
  116. package/dist/ts/shared/utils/schema-utils.d.ts.map +1 -0
  117. package/dist/ts/shared/utils/schema-utils.test.d.ts +2 -0
  118. package/dist/ts/shared/utils/schema-utils.test.d.ts.map +1 -0
  119. package/dist/ts/shims/async-local-storage.d.ts +36 -0
  120. package/dist/ts/shims/async-local-storage.d.ts.map +1 -0
  121. package/dist/ts/shims/async-local-storage.test.d.ts +2 -0
  122. package/dist/ts/shims/async-local-storage.test.d.ts.map +1 -0
  123. package/dist/{timing.d.ts → ts/timing.d.ts} +1 -0
  124. package/dist/ts/timing.d.ts.map +1 -0
  125. package/dist/{typescript → ts/typescript}/arrays.d.ts +1 -0
  126. package/dist/ts/typescript/arrays.d.ts.map +1 -0
  127. package/dist/{typescript → ts/typescript}/baseModel.d.ts +1 -0
  128. package/dist/ts/typescript/baseModel.d.ts.map +1 -0
  129. package/dist/{typescript → ts/typescript}/classes.d.ts +1 -0
  130. package/dist/ts/typescript/classes.d.ts.map +1 -0
  131. package/dist/{typescript → ts/typescript}/constant-manipulation/snake-case.d.ts +1 -0
  132. package/dist/ts/typescript/constant-manipulation/snake-case.d.ts.map +1 -0
  133. package/dist/{typescript → ts/typescript}/errors.d.ts +1 -0
  134. package/dist/ts/typescript/errors.d.ts.map +1 -0
  135. package/dist/ts/typescript/fetch.d.ts +3 -0
  136. package/dist/ts/typescript/fetch.d.ts.map +1 -0
  137. package/dist/{typescript → ts/typescript}/generics.d.ts +1 -0
  138. package/dist/ts/typescript/generics.d.ts.map +1 -0
  139. package/dist/{typescript → ts/typescript}/index.d.ts +1 -0
  140. package/dist/ts/typescript/index.d.ts.map +1 -0
  141. package/dist/ts/typescript/objects.d.ts +26 -0
  142. package/dist/ts/typescript/objects.d.ts.map +1 -0
  143. package/dist/{typescript → ts/typescript}/promises.d.ts +1 -0
  144. package/dist/ts/typescript/promises.d.ts.map +1 -0
  145. package/dist/{validation → ts/validation}/index.d.ts +1 -0
  146. package/dist/ts/validation/index.d.ts.map +1 -0
  147. package/dist/{validation → ts/validation}/validation.d.ts +1 -0
  148. package/dist/ts/validation/validation.d.ts.map +1 -0
  149. package/dist/{validation → ts/validation}/validator.d.ts +1 -0
  150. package/dist/ts/validation/validator.d.ts.map +1 -0
  151. package/dist/ts/validation/validator.test.d.ts +2 -0
  152. package/dist/ts/validation/validator.test.d.ts.map +1 -0
  153. package/package.json +46 -13
  154. package/src/app/app-context.ts +4 -5
  155. package/src/app/app-lifecycle.ts +4 -3
  156. package/src/app/app.ts +7 -5
  157. package/src/application/create-request-context.test.ts +345 -0
  158. package/src/application/create-request-context.ts +19 -0
  159. package/src/application/use-declaro.ts +27 -0
  160. package/src/auth/permission-validator.test.ts +238 -2
  161. package/src/auth/permission-validator.ts +3 -3
  162. package/src/context/async-context.test.ts +348 -0
  163. package/src/context/async-context.ts +129 -0
  164. package/src/context/context-consumer.ts +4 -4
  165. package/src/context/context.circular-deps.test.ts +1047 -0
  166. package/src/context/context.test.ts +420 -3
  167. package/src/context/context.ts +590 -87
  168. package/src/context/legacy-context.test.ts +9 -9
  169. package/src/dataflow/objects.test.ts +7 -7
  170. package/src/dataflow/objects.ts +10 -9
  171. package/src/errors/errors.ts +19 -3
  172. package/src/events/event-manager.spec.ts +129 -0
  173. package/src/events/event-manager.ts +25 -14
  174. package/src/http/headers.ts +17 -2
  175. package/src/http/request-context.ts +24 -15
  176. package/src/http/request.ts +27 -6
  177. package/src/http/url.ts +3 -3
  178. package/src/index.ts +34 -3
  179. package/src/pipelines/pipeline.test.ts +11 -9
  180. package/src/schema/json-schema.ts +16 -0
  181. package/src/schema/labels.ts +23 -23
  182. package/src/schema/model-schema.test.ts +282 -0
  183. package/src/schema/model-schema.ts +197 -0
  184. package/src/schema/model.ts +143 -0
  185. package/src/schema/schema-mixin.ts +51 -0
  186. package/src/schema/test/mock-model.ts +19 -0
  187. package/src/scope/index.ts +33 -0
  188. package/src/shared/utils/action-descriptor.test.ts +182 -0
  189. package/src/shared/utils/action-descriptor.ts +102 -0
  190. package/src/shared/utils/schema-utils.test.ts +33 -0
  191. package/src/shared/utils/schema-utils.ts +17 -0
  192. package/src/shims/async-local-storage.test.ts +258 -0
  193. package/src/shims/async-local-storage.ts +82 -0
  194. package/src/typescript/objects.ts +32 -1
  195. package/src/validation/validator.test.ts +12 -20
  196. package/dist/app/app-context.d.ts +0 -8
  197. package/dist/app/app-lifecycle.d.ts +0 -4
  198. package/dist/app/app.d.ts +0 -22
  199. package/dist/auth/permission-validator.test.d.ts +0 -1
  200. package/dist/context/context.d.ts +0 -161
  201. package/dist/context/context.test.d.ts +0 -1
  202. package/dist/context/legacy-context.test.d.ts +0 -1
  203. package/dist/dataflow/index.d.ts +0 -1
  204. package/dist/dataflow/objects.d.ts +0 -5
  205. package/dist/dataflow/objects.test.d.ts +0 -1
  206. package/dist/events/event-manager.d.ts +0 -16
  207. package/dist/events/event-manager.spec.d.ts +0 -1
  208. package/dist/events/index.d.ts +0 -1
  209. package/dist/helpers/index.d.ts +0 -1
  210. package/dist/helpers/ucfirst.d.ts +0 -1
  211. package/dist/http/headers.d.ts +0 -4
  212. package/dist/http/headers.spec.d.ts +0 -1
  213. package/dist/http/request-context.d.ts +0 -12
  214. package/dist/http/request-context.spec.d.ts +0 -1
  215. package/dist/http/request.d.ts +0 -8
  216. package/dist/http/request.spec.d.ts +0 -1
  217. package/dist/http/url.spec.d.ts +0 -1
  218. package/dist/index.d.ts +0 -19
  219. package/dist/pipelines/pipeline-action.test.d.ts +0 -1
  220. package/dist/pipelines/pipeline.test.d.ts +0 -1
  221. package/dist/pkg.cjs +0 -30
  222. package/dist/pkg.mjs +0 -56612
  223. package/dist/schema/application.d.ts +0 -83
  224. package/dist/schema/application.test.d.ts +0 -1
  225. package/dist/schema/define-model.d.ts +0 -10
  226. package/dist/schema/define-model.test.d.ts +0 -1
  227. package/dist/schema/formats.d.ts +0 -10
  228. package/dist/schema/index.d.ts +0 -10
  229. package/dist/schema/labels.d.ts +0 -13
  230. package/dist/schema/labels.test.d.ts +0 -1
  231. package/dist/schema/module.d.ts +0 -7
  232. package/dist/schema/module.test.d.ts +0 -1
  233. package/dist/schema/properties.d.ts +0 -19
  234. package/dist/schema/response.d.ts +0 -31
  235. package/dist/schema/response.test.d.ts +0 -1
  236. package/dist/schema/supported-types.d.ts +0 -12
  237. package/dist/schema/supported-types.test.d.ts +0 -1
  238. package/dist/schema/transform-model.d.ts +0 -4
  239. package/dist/schema/transform-model.test.d.ts +0 -1
  240. package/dist/schema/types.d.ts +0 -95
  241. package/dist/schema/types.test.d.ts +0 -1
  242. package/dist/typescript/fetch.d.ts +0 -2
  243. package/dist/typescript/objects.d.ts +0 -12
  244. package/dist/validation/validator.test.d.ts +0 -1
  245. package/src/helpers/index.ts +0 -1
  246. package/src/helpers/ucfirst.ts +0 -3
  247. package/src/schema/application.test.ts +0 -286
  248. package/src/schema/application.ts +0 -150
  249. package/src/schema/define-model.test.ts +0 -81
  250. package/src/schema/define-model.ts +0 -50
  251. package/src/schema/formats.ts +0 -23
  252. package/src/schema/index.ts +0 -10
  253. package/src/schema/labels.test.ts +0 -60
  254. package/src/schema/module.test.ts +0 -39
  255. package/src/schema/module.ts +0 -6
  256. package/src/schema/properties.ts +0 -40
  257. package/src/schema/response.test.ts +0 -101
  258. package/src/schema/response.ts +0 -93
  259. package/src/schema/supported-types.test.ts +0 -20
  260. package/src/schema/supported-types.ts +0 -15
  261. package/src/schema/transform-model.test.ts +0 -31
  262. package/src/schema/transform-model.ts +0 -24
  263. package/src/schema/types.test.ts +0 -28
  264. package/src/schema/types.ts +0 -163
  265. package/tsconfig.json +0 -11
  266. package/vite.config.ts +0 -24
@@ -0,0 +1,345 @@
1
+ import { createRequest } from 'node-mocks-http'
2
+ import { beforeEach, describe, expect, it } from 'vitest'
3
+ import { Context, type DeclaroRequestScope, type DeclaroScope } from '../context/context'
4
+ import type { Request } from '../http/request'
5
+ import { createRequestContext } from './create-request-context'
6
+ import { useDeclaro } from './use-declaro'
7
+
8
+ // Extended scopes for testing
9
+ interface TestAppScope extends DeclaroScope {
10
+ testAppValue?: string
11
+ sharedKey?: string
12
+ appValue?: string
13
+ appFactory?: string
14
+ appClass?: TestClass
15
+ eagerDep?: string
16
+ }
17
+
18
+ interface TestRequestScope extends DeclaroRequestScope {
19
+ middlewareTest?: string
20
+ asyncTest?: string
21
+ middleware1?: string
22
+ middleware2?: string
23
+ sharedKey?: string
24
+ appValue?: string
25
+ appFactory?: string
26
+ appClass?: TestClass
27
+ }
28
+
29
+ class TestClass {
30
+ getValue() {
31
+ return 'class-result'
32
+ }
33
+ }
34
+
35
+ describe('createRequestContext', () => {
36
+ let appContext: Context<TestAppScope>
37
+ let mockRequest: Request
38
+
39
+ beforeEach(async () => {
40
+ // Create app context with Declaro middleware
41
+ appContext = new Context<TestAppScope>()
42
+ await appContext.use(useDeclaro())
43
+
44
+ // Create mock request
45
+ mockRequest = createRequest({
46
+ method: 'POST',
47
+ url: 'https://example.com/api/test?param1=value1&param2=value2',
48
+ headers: {
49
+ 'Content-Type': 'application/json',
50
+ Authorization: 'Bearer test-token-123',
51
+ 'X-Custom-Header': 'custom-value',
52
+ 'User-Agent': 'test-agent/1.0',
53
+ },
54
+ body: {
55
+ test: 'data',
56
+ },
57
+ }) as Request
58
+ })
59
+
60
+ describe('basic functionality', () => {
61
+ it('should create a request context from app context', async () => {
62
+ const requestContext = await createRequestContext(appContext, mockRequest)
63
+
64
+ expect(requestContext).toBeInstanceOf(Context)
65
+ expect(requestContext).not.toBe(appContext)
66
+ })
67
+
68
+ it('should extend the app context', async () => {
69
+ // Add a value to app context
70
+ appContext.registerValue('testAppValue', 'app-test-value')
71
+
72
+ const requestContext = (await createRequestContext(appContext, mockRequest)) as Context<TestRequestScope>
73
+
74
+ // Should be able to access app context values
75
+ expect((requestContext as any).resolve('testAppValue')).toBe('app-test-value')
76
+ })
77
+
78
+ it('should run request middleware', async () => {
79
+ let middlewareRan = false
80
+
81
+ appContext.scope.requestMiddleware.push((context) => {
82
+ middlewareRan = true
83
+ ;(context as any).registerValue('middlewareTest', 'middleware-value')
84
+ })
85
+
86
+ const requestContext = await createRequestContext(appContext, mockRequest)
87
+
88
+ expect(middlewareRan).toBe(true)
89
+ expect((requestContext as any).resolve('middlewareTest')).toBe('middleware-value')
90
+ })
91
+
92
+ it('should initialize eager dependencies', async () => {
93
+ let eagerFactoryRan = false
94
+
95
+ appContext.registerFactory(
96
+ 'eagerDep',
97
+ () => {
98
+ eagerFactoryRan = true
99
+ return 'eager-value'
100
+ },
101
+ [],
102
+ { eager: true },
103
+ )
104
+
105
+ await createRequestContext(appContext, mockRequest)
106
+
107
+ expect(eagerFactoryRan).toBe(true)
108
+ })
109
+ })
110
+
111
+ describe('request injection', () => {
112
+ it('should inject the request object', async () => {
113
+ const requestContext = await createRequestContext(appContext, mockRequest)
114
+
115
+ const injectedRequest = requestContext.resolve('request')
116
+
117
+ expect(injectedRequest).toBe(mockRequest)
118
+ expect(injectedRequest.method).toBe('POST')
119
+ expect(injectedRequest.url).toBe('https://example.com/api/test?param1=value1&param2=value2')
120
+ })
121
+
122
+ it('should provide request via scope property', async () => {
123
+ const requestContext = await createRequestContext(appContext, mockRequest)
124
+
125
+ expect(requestContext.scope.request).toBe(mockRequest)
126
+ })
127
+ })
128
+
129
+ describe('headers injection', () => {
130
+ it('should inject headers object', async () => {
131
+ const requestContext = await createRequestContext(appContext, mockRequest)
132
+
133
+ const headers = requestContext.resolve('headers')
134
+
135
+ expect(headers).toBeDefined()
136
+ expect(headers['content-type']).toBe('application/json')
137
+ expect(headers['authorization']).toBe('Bearer test-token-123')
138
+ expect(headers['x-custom-header']).toBe('custom-value')
139
+ expect(headers['user-agent']).toBe('test-agent/1.0')
140
+ })
141
+
142
+ it('should provide headers via scope property', async () => {
143
+ const requestContext = await createRequestContext(appContext, mockRequest)
144
+
145
+ expect(requestContext.scope.headers).toBeDefined()
146
+ expect(requestContext.scope.headers).toBe(mockRequest.headers)
147
+ })
148
+
149
+ it('should handle missing headers gracefully', async () => {
150
+ const requestWithoutHeaders = createRequest({
151
+ method: 'GET',
152
+ url: '/test',
153
+ // No headers
154
+ }) as Request
155
+
156
+ const requestContext = await createRequestContext(appContext, requestWithoutHeaders)
157
+
158
+ const headers = requestContext.resolve('headers')
159
+ expect(headers).toBeDefined()
160
+ expect(Object.keys(headers).length).toBeGreaterThanOrEqual(0)
161
+ })
162
+ })
163
+
164
+ describe('header function injection', () => {
165
+ it('should inject header function', async () => {
166
+ const requestContext = await createRequestContext(appContext, mockRequest)
167
+
168
+ const headerFunction = requestContext.resolve('header')
169
+
170
+ expect(typeof headerFunction).toBe('function')
171
+ })
172
+
173
+ it('should provide header function via scope property', async () => {
174
+ const requestContext = await createRequestContext(appContext, mockRequest)
175
+
176
+ expect(typeof requestContext.scope.header).toBe('function')
177
+ })
178
+
179
+ it('should retrieve specific headers', async () => {
180
+ const requestContext = await createRequestContext(appContext, mockRequest)
181
+
182
+ const header = requestContext.scope.header
183
+
184
+ expect(header('content-type')).toBe('application/json')
185
+ expect(header('authorization')).toBe('Bearer test-token-123')
186
+ expect(header('x-custom-header')).toBe('custom-value')
187
+ expect(header('user-agent')).toBe('test-agent/1.0')
188
+ })
189
+
190
+ it('should return undefined for non-existent headers', async () => {
191
+ const requestContext = await createRequestContext(appContext, mockRequest)
192
+
193
+ const header = requestContext.scope.header
194
+
195
+ expect(header('non-existent-header' as any)).toBeUndefined()
196
+ })
197
+
198
+ it('should handle case-insensitive header names', async () => {
199
+ const requestContext = await createRequestContext(appContext, mockRequest)
200
+
201
+ const header = requestContext.scope.header
202
+
203
+ // HTTP headers are case-insensitive, Node.js typically lowercases them
204
+ expect(header('Content-Type' as any)).toBe('application/json')
205
+ })
206
+ })
207
+
208
+ describe('middleware arrays injection', () => {
209
+ it('should inject requestMiddleware array', async () => {
210
+ const requestContext = await createRequestContext(appContext, mockRequest)
211
+
212
+ const requestMiddleware = requestContext.resolve('requestMiddleware')
213
+
214
+ expect(Array.isArray(requestMiddleware)).toBe(true)
215
+ })
216
+
217
+ it('should inject nodeMiddleware array', async () => {
218
+ const requestContext = await createRequestContext(appContext, mockRequest)
219
+
220
+ const nodeMiddleware = requestContext.resolve('nodeMiddleware')
221
+
222
+ expect(Array.isArray(nodeMiddleware)).toBe(true)
223
+ })
224
+
225
+ it('should provide middleware arrays via scope properties', async () => {
226
+ const requestContext = await createRequestContext(appContext, mockRequest)
227
+
228
+ expect(Array.isArray(requestContext.scope.requestMiddleware)).toBe(true)
229
+ expect(Array.isArray(requestContext.scope.nodeMiddleware)).toBe(true)
230
+ })
231
+ })
232
+
233
+ describe('type safety and scope validation', () => {
234
+ it('should satisfy RequestScope interface', async () => {
235
+ const requestContext = await createRequestContext(appContext, mockRequest)
236
+
237
+ // Test that all RequestScope properties are available
238
+ expect(requestContext.scope.request).toBeDefined()
239
+ expect(requestContext.scope.headers).toBeDefined()
240
+ expect(typeof requestContext.scope.header).toBe('function')
241
+ expect(Array.isArray(requestContext.scope.requestMiddleware)).toBe(true)
242
+ expect(Array.isArray(requestContext.scope.nodeMiddleware)).toBe(true)
243
+ })
244
+
245
+ it('should allow injection of all RequestScope dependencies', async () => {
246
+ const requestContext = await createRequestContext(appContext, mockRequest)
247
+
248
+ // Test direct resolution of all expected dependencies
249
+ expect(() => requestContext.resolve('request')).not.toThrow()
250
+ expect(() => requestContext.resolve('headers')).not.toThrow()
251
+ expect(() => requestContext.resolve('header')).not.toThrow()
252
+ expect(() => requestContext.resolve('requestMiddleware')).not.toThrow()
253
+ expect(() => requestContext.resolve('nodeMiddleware')).not.toThrow()
254
+ })
255
+ })
256
+
257
+ describe('dependency inheritance', () => {
258
+ it('should inherit all app dependencies', async () => {
259
+ // Register various types of dependencies in app context
260
+ appContext.registerValue('appValue', 'test-value')
261
+ appContext.registerFactory('appFactory', () => 'factory-result', [])
262
+ ;(appContext as any).registerClass('appClass', TestClass, [])
263
+
264
+ const requestContext = (await createRequestContext(appContext, mockRequest)) as Context<TestRequestScope>
265
+
266
+ // All should be accessible in request context
267
+ expect((requestContext as any).resolve('appValue')).toBe('test-value')
268
+ expect((requestContext as any).resolve('appFactory')).toBe('factory-result')
269
+ expect(((requestContext as any).resolve('appClass') as TestClass).getValue()).toBe('class-result')
270
+ })
271
+
272
+ it('should allow request context to override app dependencies', async () => {
273
+ appContext.registerValue('sharedKey', 'app-value')
274
+
275
+ // Add middleware that overrides the value
276
+ appContext.scope.requestMiddleware.push((context) => {
277
+ ;(context as any).registerValue('sharedKey', 'request-value')
278
+ })
279
+
280
+ const requestContext = await createRequestContext(appContext, mockRequest)
281
+
282
+ expect((requestContext as any).resolve('sharedKey')).toBe('request-value')
283
+ })
284
+ })
285
+
286
+ describe('error handling', () => {
287
+ it('should handle middleware errors gracefully', async () => {
288
+ // Add middleware that throws
289
+ appContext.scope.requestMiddleware.push(() => {
290
+ throw new Error('Middleware error')
291
+ })
292
+
293
+ await expect(createRequestContext(appContext, mockRequest)).rejects.toThrow('Middleware error')
294
+ })
295
+
296
+ it('should handle null/undefined request', async () => {
297
+ // The current implementation doesn't validate the request parameter
298
+ // It will set request to null and continue processing
299
+ const requestContext = await createRequestContext(appContext, null as any)
300
+
301
+ expect(requestContext.resolve('request')).toBeNull()
302
+ expect(requestContext.resolve('headers')).toEqual({})
303
+ })
304
+ })
305
+
306
+ describe('async middleware support', () => {
307
+ it('should handle async middleware', async () => {
308
+ let asyncMiddlewareRan = false
309
+
310
+ appContext.scope.requestMiddleware.push(async (context) => {
311
+ await new Promise((resolve) => setTimeout(resolve, 10))
312
+ asyncMiddlewareRan = true
313
+ ;(context as any).registerValue('asyncTest', 'async-result')
314
+ })
315
+
316
+ const requestContext = await createRequestContext(appContext, mockRequest)
317
+
318
+ expect(asyncMiddlewareRan).toBe(true)
319
+ expect((requestContext as any).resolve('asyncTest')).toBe('async-result')
320
+ })
321
+
322
+ it('should run multiple async middleware in sequence', async () => {
323
+ const executionOrder: number[] = []
324
+
325
+ appContext.scope.requestMiddleware.push(
326
+ async (context) => {
327
+ await new Promise((resolve) => setTimeout(resolve, 20))
328
+ executionOrder.push(1)
329
+ ;(context as any).registerValue('middleware1', 'first')
330
+ },
331
+ async (context) => {
332
+ await new Promise((resolve) => setTimeout(resolve, 10))
333
+ executionOrder.push(2)
334
+ ;(context as any).registerValue('middleware2', 'second')
335
+ },
336
+ )
337
+
338
+ const requestContext = await createRequestContext(appContext, mockRequest)
339
+
340
+ expect(executionOrder).toEqual([1, 2])
341
+ expect((requestContext as any).resolve('middleware1')).toBe('first')
342
+ expect((requestContext as any).resolve('middleware2')).toBe('second')
343
+ })
344
+ })
345
+ })
@@ -0,0 +1,19 @@
1
+ import { Context, type DeclaroRequestScope, type DeclaroScope } from '../context/context'
2
+ import { provideRequest, type Request } from '../http/request'
3
+
4
+ export async function createRequestContext<S extends DeclaroScope>(
5
+ appContext: Context<S>,
6
+ request: Request,
7
+ ): Promise<Context<DeclaroRequestScope>> {
8
+ const context = new Context<DeclaroRequestScope>()
9
+ context.extend(appContext)
10
+
11
+ provideRequest(context, request)
12
+
13
+ const requestMiddleware = appContext.scope.requestMiddleware
14
+ await context.use(...requestMiddleware)
15
+
16
+ await context.initializeEagerDependencies()
17
+
18
+ return context
19
+ }
@@ -0,0 +1,27 @@
1
+ import type { IncomingHttpHeaders } from 'http'
2
+ import type { Context, DeclaroScope } from '../context/context'
3
+ import { provideRequestMiddleware } from '../http/request-context'
4
+
5
+ export function useDeclaro() {
6
+ return async <S extends DeclaroScope>(context: Context<S>) => {
7
+ context.registerValue('requestMiddleware', [])
8
+ context.registerValue('nodeMiddleware', [])
9
+
10
+ provideRequestMiddleware(context, async (context) => {
11
+ // TODO: Support modern web Request type, and Headers instance for full fetch compatibility
12
+
13
+ context.registerFactory(
14
+ 'headers',
15
+ (request: Request) => {
16
+ return request?.headers ?? {}
17
+ },
18
+ ['request'],
19
+ )
20
+
21
+ context.registerValue('header', (header: keyof IncomingHttpHeaders) => {
22
+ const headers = context.resolve('headers')
23
+ return headers[header]
24
+ })
25
+ })
26
+ }
27
+ }
@@ -43,7 +43,7 @@ describe('Permission Builder', () => {
43
43
 
44
44
  const validResult = validator.safeValidate(['look', 'we', 'have', 'some', 'permissions', 'and', 'some', 'more'])
45
45
 
46
- expect(validResult).to.deep.equal({
46
+ expect(validResult).toEqual({
47
47
  valid: true,
48
48
  errorMessage: '',
49
49
  errors: [],
@@ -51,7 +51,7 @@ describe('Permission Builder', () => {
51
51
 
52
52
  expect(() => validator.safeValidate(['we', 'are', 'not', 'authorized'])).not.toThrowError()
53
53
  const result = validator.safeValidate(['we', 'are', 'not', 'authorized'])
54
- expect(result).to.deep.equal({
54
+ expect(result).toEqual({
55
55
  valid: false,
56
56
  errorMessage: 'Tisk tisk',
57
57
  errors: [new PermissionError('Tisk tisk', validator.rules[0], ['we', 'are', 'not', 'authorized'])],
@@ -206,4 +206,240 @@ describe('Permission Builder', () => {
206
206
  expect(validator3.safeValidate(['namespace::resource.action1:write']).valid).toBe(true)
207
207
  expect(validator3.safeValidate(['namespace::resource.action2:delete']).valid).toBe(true)
208
208
  })
209
+
210
+ describe('Nested Permission Validators', () => {
211
+ it('should support nesting validators with someOf for OR logic', () => {
212
+ // Create nested validator: permission1 OR (permission2 AND permission3)
213
+ const nestedValidator = PermissionValidator.create().allOf(['permission2', 'permission3'])
214
+ const validator = PermissionValidator.create().someOf(
215
+ ['permission1', nestedValidator],
216
+ 'Need permission1 OR (permission2 AND permission3)',
217
+ )
218
+
219
+ // Test with permission1 only - should pass
220
+ expect(validator.safeValidate(['permission1']).valid).toBe(true)
221
+
222
+ // Test with permission2 and permission3 - should pass
223
+ expect(validator.safeValidate(['permission2', 'permission3']).valid).toBe(true)
224
+
225
+ // Test with all permissions - should pass
226
+ expect(validator.safeValidate(['permission1', 'permission2', 'permission3']).valid).toBe(true)
227
+
228
+ // Test with permission2 only - should fail
229
+ expect(validator.safeValidate(['permission2']).valid).toBe(false)
230
+
231
+ // Test with permission3 only - should fail
232
+ expect(validator.safeValidate(['permission3']).valid).toBe(false)
233
+
234
+ // Test with no permissions - should fail
235
+ expect(validator.safeValidate([]).valid).toBe(false)
236
+ })
237
+
238
+ it('should support nesting validators with allOf for AND logic', () => {
239
+ // Create nested validator: permission1 AND (permission2 OR permission3)
240
+ const nestedValidator = PermissionValidator.create().someOf(['permission2', 'permission3'])
241
+ const validator = PermissionValidator.create().allOf(
242
+ ['permission1', nestedValidator],
243
+ 'Need permission1 AND (permission2 OR permission3)',
244
+ )
245
+
246
+ // Test with permission1 and permission2 - should pass
247
+ expect(validator.safeValidate(['permission1', 'permission2']).valid).toBe(true)
248
+
249
+ // Test with permission1 and permission3 - should pass
250
+ expect(validator.safeValidate(['permission1', 'permission3']).valid).toBe(true)
251
+
252
+ // Test with all permissions - should pass
253
+ expect(validator.safeValidate(['permission1', 'permission2', 'permission3']).valid).toBe(true)
254
+
255
+ // Test with permission1 only - should fail
256
+ expect(validator.safeValidate(['permission1']).valid).toBe(false)
257
+
258
+ // Test with permission2 only - should fail
259
+ expect(validator.safeValidate(['permission2']).valid).toBe(false)
260
+
261
+ // Test with permission2 and permission3 only - should fail
262
+ expect(validator.safeValidate(['permission2', 'permission3']).valid).toBe(false)
263
+ })
264
+
265
+ it('should support multiple levels of nesting', () => {
266
+ // Create complex nested validator: permission1 OR (permission2 AND (permission3 OR permission4))
267
+ const deepNestedValidator = PermissionValidator.create().someOf(['permission3', 'permission4'])
268
+ const nestedValidator = PermissionValidator.create().allOf(['permission2', deepNestedValidator])
269
+ const validator = PermissionValidator.create().someOf(
270
+ ['permission1', nestedValidator],
271
+ 'Complex permission logic failed',
272
+ )
273
+
274
+ // Test with permission1 only - should pass
275
+ expect(validator.safeValidate(['permission1']).valid).toBe(true)
276
+
277
+ // Test with permission2 and permission3 - should pass
278
+ expect(validator.safeValidate(['permission2', 'permission3']).valid).toBe(true)
279
+
280
+ // Test with permission2 and permission4 - should pass
281
+ expect(validator.safeValidate(['permission2', 'permission4']).valid).toBe(true)
282
+
283
+ // Test with permission2, permission3, and permission4 - should pass
284
+ expect(validator.safeValidate(['permission2', 'permission3', 'permission4']).valid).toBe(true)
285
+
286
+ // Test with permission2 only - should fail
287
+ expect(validator.safeValidate(['permission2']).valid).toBe(false)
288
+
289
+ // Test with permission3 and permission4 only - should fail
290
+ expect(validator.safeValidate(['permission3', 'permission4']).valid).toBe(false)
291
+ })
292
+
293
+ it('should support nesting with noneOf for exclusion logic', () => {
294
+ // Create validator: permission1 AND NOT(permission2 OR permission3)
295
+ const excludeValidator = PermissionValidator.create().someOf(['permission2', 'permission3'])
296
+ const validator = PermissionValidator.create()
297
+ .allOf(['permission1'], 'Must have permission1')
298
+ .noneOf([excludeValidator], 'Cannot have permission2 or permission3')
299
+
300
+ // Test with permission1 only - should pass
301
+ expect(validator.safeValidate(['permission1']).valid).toBe(true)
302
+
303
+ // Test with permission1 and permission4 - should pass
304
+ expect(validator.safeValidate(['permission1', 'permission4']).valid).toBe(true)
305
+
306
+ // Test with permission1 and permission2 - should fail
307
+ expect(validator.safeValidate(['permission1', 'permission2']).valid).toBe(false)
308
+
309
+ // Test with permission1 and permission3 - should fail
310
+ expect(validator.safeValidate(['permission1', 'permission3']).valid).toBe(false)
311
+
312
+ // Test with permission1, permission2, and permission3 - should fail
313
+ expect(validator.safeValidate(['permission1', 'permission2', 'permission3']).valid).toBe(false)
314
+ })
315
+
316
+ it('should provide meaningful error messages for nested validators', () => {
317
+ const nestedValidator = PermissionValidator.create().allOf(
318
+ ['permission2', 'permission3'],
319
+ 'Missing required permissions 2 and 3',
320
+ )
321
+ const validator = PermissionValidator.create().someOf(
322
+ ['permission1', nestedValidator],
323
+ 'Need permission1 OR both permission2 and permission3',
324
+ )
325
+
326
+ const result = validator.safeValidate(['permission2'])
327
+
328
+ expect(result.valid).toBe(false)
329
+ expect(result.errorMessage).toBe('Need permission1 OR both permission2 and permission3')
330
+ expect(result.errors).toHaveLength(1)
331
+ })
332
+
333
+ it('should work with real-world permission patterns using nesting', () => {
334
+ // Create validator for: write:all OR (create:all AND update:all)
335
+ const createAndUpdate = PermissionValidator.create().allOf(['create:all', 'update:all'])
336
+ const validator = PermissionValidator.create().someOf(
337
+ ['write:all', createAndUpdate],
338
+ 'Need write:all OR (create:all AND update:all)',
339
+ )
340
+
341
+ // Test with write:all - should pass
342
+ expect(validator.safeValidate(['write:all']).valid).toBe(true)
343
+
344
+ // Test with create:all and update:all - should pass
345
+ expect(validator.safeValidate(['create:all', 'update:all']).valid).toBe(true)
346
+
347
+ // Test with all permissions - should pass
348
+ expect(validator.safeValidate(['write:all', 'create:all', 'update:all']).valid).toBe(true)
349
+
350
+ // Test with create:all only - should fail
351
+ expect(validator.safeValidate(['create:all']).valid).toBe(false)
352
+
353
+ // Test with update:all only - should fail
354
+ expect(validator.safeValidate(['update:all']).valid).toBe(false)
355
+
356
+ // Test with read:all - should fail
357
+ expect(validator.safeValidate(['read:all']).valid).toBe(false)
358
+ })
359
+
360
+ it('should support the exact syntax mentioned: someOf with nested allOf for OR-AND logic', () => {
361
+ // Test the exact pattern: baseValidator.someOf(['permission1', baseValidator.someOf(['permission2', 'permission3'])])
362
+ // This creates: permission1 OR (permission2 AND permission3)
363
+ const nestedValidator = PermissionValidator.create().allOf(['permission2', 'permission3'])
364
+ const validator = PermissionValidator.create().someOf(['permission1', nestedValidator])
365
+
366
+ // Test permission1 only - should pass
367
+ expect(validator.safeValidate(['permission1']).valid).toBe(true)
368
+
369
+ // Test permission2 and permission3 - should pass
370
+ expect(validator.safeValidate(['permission2', 'permission3']).valid).toBe(true)
371
+
372
+ // Test all permissions - should pass
373
+ expect(validator.safeValidate(['permission1', 'permission2', 'permission3']).valid).toBe(true)
374
+
375
+ // Test permission2 only - should fail
376
+ expect(validator.safeValidate(['permission2']).valid).toBe(false)
377
+
378
+ // Test permission3 only - should fail
379
+ expect(validator.safeValidate(['permission3']).valid).toBe(false)
380
+
381
+ // Test no permissions - should fail
382
+ expect(validator.safeValidate([]).valid).toBe(false)
383
+ })
384
+
385
+ it('should work with wildcards in nested validators', () => {
386
+ // Create validator: admin:* OR (books:read AND books:write)
387
+ const booksReadWrite = PermissionValidator.create().allOf(['books:read', 'books:write'])
388
+ const validator = PermissionValidator.create().someOf(
389
+ ['admin:*', booksReadWrite],
390
+ 'Need admin access OR books read/write',
391
+ )
392
+
393
+ // Test with admin wildcard - should pass
394
+ expect(validator.safeValidate(['admin:read']).valid).toBe(true)
395
+ expect(validator.safeValidate(['admin:write']).valid).toBe(true)
396
+ expect(validator.safeValidate(['admin:delete']).valid).toBe(true)
397
+
398
+ // Test with books read and write - should pass
399
+ expect(validator.safeValidate(['books:read', 'books:write']).valid).toBe(true)
400
+
401
+ // Test with books read only - should fail
402
+ expect(validator.safeValidate(['books:read']).valid).toBe(false)
403
+
404
+ // Test with books write only - should fail
405
+ expect(validator.safeValidate(['books:write']).valid).toBe(false)
406
+
407
+ // Test with other permissions - should fail
408
+ expect(validator.safeValidate(['users:read']).valid).toBe(false)
409
+ })
410
+
411
+ it('should demonstrate practical usage for upsert/bulkUpsert permissions', () => {
412
+ // Create validator for upsert/bulkUpsert: books:write:all OR (books:create:all AND books:update:all)
413
+ const createAndUpdate = PermissionValidator.create().allOf(['books:create:all', 'books:update:all'])
414
+ const upsertValidator = PermissionValidator.create().someOf(
415
+ ['books:write:all', createAndUpdate],
416
+ 'Need books:write:all OR (books:create:all AND books:update:all)',
417
+ )
418
+
419
+ // Test with write permission - should pass
420
+ expect(upsertValidator.safeValidate(['books:write:all']).valid).toBe(true)
421
+
422
+ // Test with create and update permissions - should pass
423
+ expect(upsertValidator.safeValidate(['books:create:all', 'books:update:all']).valid).toBe(true)
424
+
425
+ // Test with all permissions - should pass
426
+ expect(
427
+ upsertValidator.safeValidate(['books:write:all', 'books:create:all', 'books:update:all']).valid,
428
+ ).toBe(true)
429
+
430
+ // Test with create only - should fail
431
+ expect(upsertValidator.safeValidate(['books:create:all']).valid).toBe(false)
432
+
433
+ // Test with update only - should fail
434
+ expect(upsertValidator.safeValidate(['books:update:all']).valid).toBe(false)
435
+
436
+ // Test with read only - should fail
437
+ expect(upsertValidator.safeValidate(['books:read:all']).valid).toBe(false)
438
+
439
+ // Test error message
440
+ const result = upsertValidator.safeValidate(['books:create:all'])
441
+ expect(result.valid).toBe(false)
442
+ expect(result.errorMessage).toBe('Need books:write:all OR (books:create:all AND books:update:all)')
443
+ })
444
+ })
209
445
  })
@@ -44,7 +44,7 @@ export class PermissionValidator {
44
44
  return this
45
45
  }
46
46
 
47
- allOf(permissions: RulePermission[], errorMessage?: string) {
47
+ allOf(permissions: RulePermission[], errorMessage: string = 'You do not have the required permissions') {
48
48
  this.addRule({
49
49
  type: PermissionRuleType.ALL_OF,
50
50
  permissions,
@@ -53,7 +53,7 @@ export class PermissionValidator {
53
53
  return this
54
54
  }
55
55
 
56
- noneOf(permissions: RulePermission[], errorMessage?: string) {
56
+ noneOf(permissions: RulePermission[], errorMessage: string = 'You have forbidden permissions') {
57
57
  this.addRule({
58
58
  type: PermissionRuleType.NONE_OF,
59
59
  permissions,
@@ -62,7 +62,7 @@ export class PermissionValidator {
62
62
  return this
63
63
  }
64
64
 
65
- someOf(permissions: RulePermission[], errorMessage?: string) {
65
+ someOf(permissions: RulePermission[], errorMessage: string = 'You do not have any of the required permissions') {
66
66
  this.addRule({
67
67
  type: PermissionRuleType.SOME_OF,
68
68
  permissions,