@declaro/core 2.0.0-beta.9 → 2.0.0-beta.90

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 (232) hide show
  1. package/{LICENSE → LICENSE.md} +1 -1
  2. package/README.md +203 -0
  3. package/dist/browser/index.js +34 -0
  4. package/dist/browser/index.js.map +123 -0
  5. package/dist/browser/scope/index.js +4 -0
  6. package/dist/browser/scope/index.js.map +9 -0
  7. package/dist/node/index.cjs +17329 -0
  8. package/dist/node/index.cjs.map +123 -0
  9. package/dist/node/index.js +17303 -0
  10. package/dist/node/index.js.map +123 -0
  11. package/dist/node/scope/index.cjs +48 -0
  12. package/dist/node/scope/index.cjs.map +9 -0
  13. package/dist/node/scope/index.js +29 -0
  14. package/dist/node/scope/index.js.map +9 -0
  15. package/dist/ts/app/app-context.d.ts +9 -0
  16. package/dist/ts/app/app-context.d.ts.map +1 -0
  17. package/dist/ts/app/app-lifecycle.d.ts +6 -0
  18. package/dist/ts/app/app-lifecycle.d.ts.map +1 -0
  19. package/dist/ts/app/app.d.ts +24 -0
  20. package/dist/ts/app/app.d.ts.map +1 -0
  21. package/dist/ts/app/index.d.ts +4 -0
  22. package/dist/ts/app/index.d.ts.map +1 -0
  23. package/dist/ts/application/create-request-context.d.ts +4 -0
  24. package/dist/ts/application/create-request-context.d.ts.map +1 -0
  25. package/dist/ts/application/create-request-context.test.d.ts +2 -0
  26. package/dist/ts/application/create-request-context.test.d.ts.map +1 -0
  27. package/dist/ts/application/use-declaro.d.ts +3 -0
  28. package/dist/ts/application/use-declaro.d.ts.map +1 -0
  29. package/dist/ts/auth/permission-validator.d.ts +35 -0
  30. package/dist/ts/auth/permission-validator.d.ts.map +1 -0
  31. package/dist/ts/auth/permission-validator.test.d.ts +2 -0
  32. package/dist/ts/auth/permission-validator.test.d.ts.map +1 -0
  33. package/dist/{context → ts/context}/context-consumer.d.ts +4 -0
  34. package/dist/ts/context/context-consumer.d.ts.map +1 -0
  35. package/dist/ts/context/context.d.ts +193 -0
  36. package/dist/ts/context/context.d.ts.map +1 -0
  37. package/dist/ts/context/context.test.d.ts +2 -0
  38. package/dist/ts/context/context.test.d.ts.map +1 -0
  39. package/dist/ts/context/legacy-context.test.d.ts +2 -0
  40. package/dist/ts/context/legacy-context.test.d.ts.map +1 -0
  41. package/dist/{context → ts/context}/validators.d.ts +2 -1
  42. package/dist/ts/context/validators.d.ts.map +1 -0
  43. package/dist/ts/dataflow/index.d.ts +2 -0
  44. package/dist/ts/dataflow/index.d.ts.map +1 -0
  45. package/dist/ts/dataflow/objects.d.ts +7 -0
  46. package/dist/ts/dataflow/objects.d.ts.map +1 -0
  47. package/dist/ts/dataflow/objects.test.d.ts +2 -0
  48. package/dist/ts/dataflow/objects.test.d.ts.map +1 -0
  49. package/dist/ts/errors/errors.d.ts +49 -0
  50. package/dist/ts/errors/errors.d.ts.map +1 -0
  51. package/dist/ts/events/event-manager.d.ts +19 -0
  52. package/dist/ts/events/event-manager.d.ts.map +1 -0
  53. package/dist/ts/events/event-manager.spec.d.ts +2 -0
  54. package/dist/ts/events/event-manager.spec.d.ts.map +1 -0
  55. package/dist/ts/events/index.d.ts +2 -0
  56. package/dist/ts/events/index.d.ts.map +1 -0
  57. package/dist/ts/http/headers.d.ts +21 -0
  58. package/dist/ts/http/headers.d.ts.map +1 -0
  59. package/dist/ts/http/headers.spec.d.ts +2 -0
  60. package/dist/ts/http/headers.spec.d.ts.map +1 -0
  61. package/dist/ts/http/request-context.d.ts +17 -0
  62. package/dist/ts/http/request-context.d.ts.map +1 -0
  63. package/dist/ts/http/request-context.spec.d.ts +2 -0
  64. package/dist/ts/http/request-context.spec.d.ts.map +1 -0
  65. package/dist/ts/http/request.d.ts +31 -0
  66. package/dist/ts/http/request.d.ts.map +1 -0
  67. package/dist/ts/http/request.spec.d.ts +2 -0
  68. package/dist/ts/http/request.spec.d.ts.map +1 -0
  69. package/dist/ts/http/url.d.ts +9 -0
  70. package/dist/ts/http/url.d.ts.map +1 -0
  71. package/dist/ts/http/url.spec.d.ts +2 -0
  72. package/dist/ts/http/url.spec.d.ts.map +1 -0
  73. package/dist/ts/index.d.ts +45 -0
  74. package/dist/ts/index.d.ts.map +1 -0
  75. package/dist/{pipelines → ts/pipelines}/index.d.ts +1 -0
  76. package/dist/ts/pipelines/index.d.ts.map +1 -0
  77. package/dist/{pipelines → ts/pipelines}/pipeline-action.d.ts +1 -0
  78. package/dist/ts/pipelines/pipeline-action.d.ts.map +1 -0
  79. package/dist/ts/pipelines/pipeline-action.test.d.ts +2 -0
  80. package/dist/ts/pipelines/pipeline-action.test.d.ts.map +1 -0
  81. package/dist/{pipelines → ts/pipelines}/pipeline.d.ts +3 -2
  82. package/dist/ts/pipelines/pipeline.d.ts.map +1 -0
  83. package/dist/ts/pipelines/pipeline.test.d.ts +2 -0
  84. package/dist/ts/pipelines/pipeline.test.d.ts.map +1 -0
  85. package/dist/ts/schema/entity-schema.test.d.ts +1 -0
  86. package/dist/ts/schema/entity-schema.test.d.ts.map +1 -0
  87. package/dist/ts/schema/json-schema.d.ts +4 -0
  88. package/dist/ts/schema/json-schema.d.ts.map +1 -0
  89. package/dist/ts/schema/labels.d.ts +14 -0
  90. package/dist/ts/schema/labels.d.ts.map +1 -0
  91. package/dist/ts/schema/model-schema.d.ts +75 -0
  92. package/dist/ts/schema/model-schema.d.ts.map +1 -0
  93. package/dist/ts/schema/model-schema.test.d.ts +2 -0
  94. package/dist/ts/schema/model-schema.test.d.ts.map +1 -0
  95. package/dist/ts/schema/model.d.ts +30 -0
  96. package/dist/ts/schema/model.d.ts.map +1 -0
  97. package/dist/ts/schema/schema-mixin.d.ts +24 -0
  98. package/dist/ts/schema/schema-mixin.d.ts.map +1 -0
  99. package/dist/ts/schema/test/mock-model.d.ts +8 -0
  100. package/dist/ts/schema/test/mock-model.d.ts.map +1 -0
  101. package/dist/ts/scope/index.d.ts +34 -0
  102. package/dist/ts/scope/index.d.ts.map +1 -0
  103. package/dist/ts/shared/utils/action-descriptor.d.ts +28 -0
  104. package/dist/ts/shared/utils/action-descriptor.d.ts.map +1 -0
  105. package/dist/ts/shared/utils/action-descriptor.test.d.ts +2 -0
  106. package/dist/ts/shared/utils/action-descriptor.test.d.ts.map +1 -0
  107. package/dist/{timing.d.ts → ts/timing.d.ts} +1 -0
  108. package/dist/ts/timing.d.ts.map +1 -0
  109. package/dist/{typescript → ts/typescript}/arrays.d.ts +1 -0
  110. package/dist/ts/typescript/arrays.d.ts.map +1 -0
  111. package/dist/{typescript → ts/typescript}/baseModel.d.ts +1 -0
  112. package/dist/ts/typescript/baseModel.d.ts.map +1 -0
  113. package/dist/{typescript → ts/typescript}/classes.d.ts +1 -0
  114. package/dist/ts/typescript/classes.d.ts.map +1 -0
  115. package/dist/ts/typescript/constant-manipulation/snake-case.d.ts +23 -0
  116. package/dist/ts/typescript/constant-manipulation/snake-case.d.ts.map +1 -0
  117. package/dist/{typescript → ts/typescript}/errors.d.ts +1 -0
  118. package/dist/ts/typescript/errors.d.ts.map +1 -0
  119. package/dist/ts/typescript/fetch.d.ts +3 -0
  120. package/dist/ts/typescript/fetch.d.ts.map +1 -0
  121. package/dist/{typescript → ts/typescript}/generics.d.ts +1 -0
  122. package/dist/ts/typescript/generics.d.ts.map +1 -0
  123. package/dist/{typescript → ts/typescript}/index.d.ts +2 -0
  124. package/dist/ts/typescript/index.d.ts.map +1 -0
  125. package/dist/ts/typescript/objects.d.ts +26 -0
  126. package/dist/ts/typescript/objects.d.ts.map +1 -0
  127. package/dist/{typescript → ts/typescript}/promises.d.ts +1 -0
  128. package/dist/ts/typescript/promises.d.ts.map +1 -0
  129. package/dist/{validation → ts/validation}/index.d.ts +1 -0
  130. package/dist/ts/validation/index.d.ts.map +1 -0
  131. package/dist/{validation → ts/validation}/validation.d.ts +1 -0
  132. package/dist/ts/validation/validation.d.ts.map +1 -0
  133. package/dist/{validation → ts/validation}/validator.d.ts +1 -0
  134. package/dist/ts/validation/validator.d.ts.map +1 -0
  135. package/dist/ts/validation/validator.test.d.ts +2 -0
  136. package/dist/ts/validation/validator.test.d.ts.map +1 -0
  137. package/package.json +44 -14
  138. package/src/app/app-context.ts +13 -0
  139. package/src/app/app-lifecycle.ts +15 -0
  140. package/src/app/app.ts +47 -0
  141. package/src/app/index.ts +3 -34
  142. package/src/application/create-request-context.test.ts +351 -0
  143. package/src/application/create-request-context.ts +19 -0
  144. package/src/application/use-declaro.ts +19 -0
  145. package/src/auth/permission-validator.test.ts +445 -0
  146. package/src/auth/permission-validator.ts +135 -0
  147. package/src/context/context-consumer.ts +4 -4
  148. package/src/context/context.test.ts +851 -93
  149. package/src/context/context.ts +414 -33
  150. package/src/context/legacy-context.test.ts +141 -0
  151. package/src/dataflow/objects.test.ts +7 -7
  152. package/src/dataflow/objects.ts +10 -9
  153. package/src/errors/errors.ts +89 -0
  154. package/src/events/event-manager.spec.ts +183 -8
  155. package/src/events/event-manager.ts +58 -31
  156. package/src/http/headers.spec.ts +48 -0
  157. package/src/http/headers.ts +31 -0
  158. package/src/http/request-context.spec.ts +39 -0
  159. package/src/http/request-context.ts +54 -0
  160. package/src/http/request.spec.ts +52 -0
  161. package/src/http/request.ts +43 -0
  162. package/src/http/url.spec.ts +87 -0
  163. package/src/http/url.ts +48 -0
  164. package/src/index.ts +41 -6
  165. package/src/pipelines/pipeline.test.ts +11 -9
  166. package/src/schema/entity-schema.test.ts +0 -0
  167. package/src/schema/json-schema.ts +3 -0
  168. package/src/schema/labels.ts +30 -0
  169. package/src/schema/model-schema.test.ts +128 -0
  170. package/src/schema/model-schema.ts +197 -0
  171. package/src/schema/model.ts +112 -0
  172. package/src/schema/schema-mixin.ts +51 -0
  173. package/src/schema/test/mock-model.ts +15 -0
  174. package/src/scope/index.ts +33 -0
  175. package/src/shared/utils/action-descriptor.test.ts +182 -0
  176. package/src/shared/utils/action-descriptor.ts +102 -0
  177. package/src/typescript/constant-manipulation/snake-case.md +496 -0
  178. package/src/typescript/constant-manipulation/snake-case.ts +76 -0
  179. package/src/typescript/index.ts +1 -0
  180. package/src/typescript/objects.ts +39 -5
  181. package/src/validation/validator.test.ts +12 -20
  182. package/dist/app/index.d.ts +0 -20
  183. package/dist/context/context.d.ts +0 -86
  184. package/dist/context/context.test.d.ts +0 -1
  185. package/dist/context/index.d.ts +0 -3
  186. package/dist/dataflow/index.d.ts +0 -1
  187. package/dist/dataflow/objects.d.ts +0 -5
  188. package/dist/dataflow/objects.test.d.ts +0 -1
  189. package/dist/events/event-manager.d.ts +0 -11
  190. package/dist/events/event-manager.spec.d.ts +0 -1
  191. package/dist/events/index.d.ts +0 -1
  192. package/dist/helpers/index.d.ts +0 -1
  193. package/dist/helpers/ucfirst.d.ts +0 -1
  194. package/dist/index.d.ts +0 -13
  195. package/dist/interfaces/IDatastoreProvider.d.ts +0 -16
  196. package/dist/interfaces/IStore.d.ts +0 -4
  197. package/dist/interfaces/index.d.ts +0 -2
  198. package/dist/pipelines/pipeline-action.test.d.ts +0 -1
  199. package/dist/pipelines/pipeline.test.d.ts +0 -1
  200. package/dist/pkg.cjs +0 -2
  201. package/dist/pkg.mjs +0 -358
  202. package/dist/schema/define-model.d.ts +0 -7
  203. package/dist/schema/define-model.test.d.ts +0 -1
  204. package/dist/schema/formats.d.ts +0 -10
  205. package/dist/schema/index.d.ts +0 -3
  206. package/dist/schema/supported-types.d.ts +0 -12
  207. package/dist/schema/supported-types.test.d.ts +0 -1
  208. package/dist/schema/transform-model.d.ts +0 -4
  209. package/dist/schema/transform-model.test.d.ts +0 -1
  210. package/dist/schema/types.d.ts +0 -29
  211. package/dist/server/index.d.ts +0 -2
  212. package/dist/typescript/fetch.d.ts +0 -2
  213. package/dist/typescript/objects.d.ts +0 -6
  214. package/dist/validation/validator.test.d.ts +0 -1
  215. package/src/context/index.ts +0 -3
  216. package/src/helpers/index.ts +0 -1
  217. package/src/helpers/ucfirst.ts +0 -3
  218. package/src/interfaces/IDatastoreProvider.ts +0 -23
  219. package/src/interfaces/IStore.ts +0 -4
  220. package/src/interfaces/index.ts +0 -2
  221. package/src/schema/define-model.test.ts +0 -35
  222. package/src/schema/define-model.ts +0 -19
  223. package/src/schema/formats.ts +0 -23
  224. package/src/schema/index.ts +0 -3
  225. package/src/schema/supported-types.test.ts +0 -20
  226. package/src/schema/supported-types.ts +0 -15
  227. package/src/schema/transform-model.test.ts +0 -31
  228. package/src/schema/transform-model.ts +0 -24
  229. package/src/schema/types.ts +0 -43
  230. package/src/server/index.ts +0 -3
  231. package/tsconfig.json +0 -8
  232. package/vite.config.ts +0 -24
@@ -0,0 +1,52 @@
1
+ import { Context } from '../context/context'
2
+ import { createRequest } from 'node-mocks-http'
3
+ import { provideRequest, useRequest, useRequestMethod } from './request'
4
+ import { describe, expect, it } from 'vitest'
5
+
6
+ describe('Http Request', () => {
7
+ it('Should get undefined by default', () => {
8
+ const context = new Context()
9
+
10
+ const request = useRequest(context)
11
+
12
+ expect(request).toBeUndefined()
13
+ })
14
+ it('Should provide a request', () => {
15
+ const context = new Context()
16
+
17
+ const request = createRequest({
18
+ method: 'GET',
19
+ url: '/test',
20
+ headers: {
21
+ 'Content-Type': 'application/json',
22
+ 'X-Test': 'test',
23
+ Authorization: 'Bearer 123',
24
+ },
25
+ })
26
+
27
+ provideRequest(context, request)
28
+
29
+ const request2 = useRequest(context)
30
+
31
+ expect(request2).toEqual(request)
32
+ })
33
+
34
+ it('Should get the method', () => {
35
+ const context = new Context()
36
+
37
+ const request = createRequest({
38
+ method: 'GET',
39
+ url: '/test',
40
+ headers: {
41
+ 'Content-Type': 'application/json',
42
+ 'X-Test': 'test',
43
+ },
44
+ })
45
+
46
+ provideRequest(context, request)
47
+
48
+ const method = useRequestMethod(context)
49
+
50
+ expect(method).toEqual('GET')
51
+ })
52
+ })
@@ -0,0 +1,43 @@
1
+ import type { IncomingMessage } from 'http'
2
+ import { Context, type DeclaroRequestScope } from '../context/context'
3
+
4
+ export interface Request extends IncomingMessage {}
5
+
6
+ /**
7
+ * Provide a request to a context that supports request scope.
8
+ * The context must have all properties required by DeclaroRequestScope.
9
+ *
10
+ * @param context A context that includes DeclaroRequestScope properties
11
+ * @param request The request to provide
12
+ */
13
+ export function provideRequest<S extends DeclaroRequestScope>(context: Context<S>, request: Request) {
14
+ context.registerValue('request', request)
15
+ }
16
+
17
+ /**
18
+ * Get the request from the context.
19
+ *
20
+ * @deprecated You can now inject the request directly from Context<RequestScope>.
21
+ * This function will be removed in a future version.
22
+ * @param context The context to use for the request.
23
+ * @returns
24
+ */
25
+ export function useRequest(context: Context) {
26
+ const request = context.resolve('request')
27
+
28
+ return request
29
+ }
30
+
31
+ /**
32
+ * Get the HTTP method of the request.
33
+ *
34
+ * @deprecated You can now inject the request directly from Context<RequestScope>.
35
+ * This function will be removed in a future version.
36
+ * @param context The context to use for the request.
37
+ * @returns The HTTP method of the request, or undefined if not available.
38
+ */
39
+ export function useRequestMethod(context: Context) {
40
+ const request = useRequest(context)
41
+
42
+ return request?.method
43
+ }
@@ -0,0 +1,87 @@
1
+ // Tests for url.ts:
2
+ // Create tests for the following functions:
3
+ // - useURL: make sure it returns undefined by default, and the URL object if provided
4
+ // - provideURL: make sure you can provide a URL object or a string
5
+ // - useURLString: make sure it returns the stringified URL
6
+ // - useURLDomain: make sure it returns the domain part of the URL provided
7
+ // - useURLPath
8
+
9
+ import { Context } from '../context/context'
10
+ import { useRequestDomain, useRequestPath, useRequestQuery, useRequestURL, useRequestURLString } from './url'
11
+ import { provideRequest } from './request'
12
+ import { createRequest } from 'node-mocks-http'
13
+ import { describe, expect, it } from 'vitest'
14
+
15
+ // - useURLQueryString
16
+ describe('Http URL', () => {
17
+ const testUrlString = 'https://example.com/test?a=1&b=2'
18
+ const testUrlObject = new URL(testUrlString)
19
+ const request = createRequest({
20
+ method: 'GET',
21
+ url: testUrlString,
22
+ headers: {
23
+ 'Content-Type': 'application/json',
24
+ 'X-Test': 'test',
25
+ Authorization: 'Bearer 123',
26
+ },
27
+ })
28
+
29
+ it('Should get undefined by default', () => {
30
+ const context = new Context()
31
+
32
+ const url = useRequestURL(context)
33
+
34
+ expect(url).toBeUndefined()
35
+ })
36
+
37
+ it('Should get the URL object if provided', () => {
38
+ const context = new Context()
39
+
40
+ provideRequest(context, request)
41
+
42
+ const url2 = useRequestURL(context)
43
+
44
+ expect(url2).toEqual(testUrlObject)
45
+ })
46
+
47
+ it('Should get the stringified URL', () => {
48
+ const context = new Context()
49
+
50
+ provideRequest(context, request)
51
+
52
+ const url2 = useRequestURLString(context)
53
+
54
+ expect(url2).toEqual(testUrlString)
55
+ })
56
+
57
+ it('Should get the domain part of the URL', () => {
58
+ const context = new Context()
59
+
60
+ provideRequest(context, request)
61
+
62
+ const domain = useRequestDomain(context)
63
+
64
+ expect(domain).toEqual('example.com')
65
+ })
66
+
67
+ it('Should get the path part of the URL', () => {
68
+ const context = new Context()
69
+
70
+ provideRequest(context, request)
71
+
72
+ const path = useRequestPath(context)
73
+
74
+ expect(path).toEqual('/test')
75
+ })
76
+
77
+ it('Should get the query string part of the URL', () => {
78
+ const context = new Context()
79
+
80
+ provideRequest(context, request)
81
+
82
+ const query = useRequestQuery(context)
83
+
84
+ expect(query['a']).toEqual('1')
85
+ expect(query['b']).toEqual('2')
86
+ })
87
+ })
@@ -0,0 +1,48 @@
1
+ import { Context } from '../context/context'
2
+ import { URL } from 'whatwg-url'
3
+ import { useRequest } from './request'
4
+
5
+ export function useRequestURL(context: Context): URL | undefined {
6
+ const request = useRequest(context)
7
+ const url = request?.url && new URL(request?.url)
8
+
9
+ return url as URL
10
+ }
11
+
12
+ export function useRequestURLString(context: Context) {
13
+ const url = useRequestURL(context)
14
+
15
+ return url?.toString()
16
+ }
17
+
18
+ export function useRequestDomain(context: Context) {
19
+ const url = useRequestURL(context)
20
+
21
+ return url?.hostname
22
+ }
23
+
24
+ export function useRequestPath(context: Context) {
25
+ const url = useRequestURL(context)
26
+
27
+ return url?.pathname
28
+ }
29
+
30
+ export function useRequestQuery(context: Context): Record<string, string> {
31
+ const url = useRequestURL(context)
32
+
33
+ const qs = url?.searchParams
34
+
35
+ // convert qs to an object of key value pairs
36
+ const qsMap = [...(qs?.entries() as any)].reduce((qsMap, [key, value]) => {
37
+ qsMap[key] = value
38
+ return qsMap
39
+ }, {} as Record<string, string>)
40
+
41
+ return qsMap
42
+ }
43
+
44
+ export function useFormattedQueryParam(context: Context, key: string) {
45
+ const qs = useRequestQuery(context)
46
+
47
+ const value = qs[key]
48
+ }
package/src/index.ts CHANGED
@@ -1,13 +1,48 @@
1
+ export type {
2
+ /**
3
+ * @deprecated Import AppScope from '#scope' instead.
4
+ *
5
+ * Migration: Change `import { AppScope } from '@declaro/core'` to `import { AppScope } from '#scope'`
6
+ *
7
+ * The #scope import allows for better type augmentation and module resolution.
8
+ */
9
+ AppScope,
10
+ /**
11
+ * @deprecated Import RequestScope from '#scope' instead.
12
+ *
13
+ * Migration: Change `import { RequestScope } from '@declaro/core'` to `import { RequestScope } from '#scope'`
14
+ *
15
+ * The #scope import allows for better type augmentation and module resolution.
16
+ */
17
+ RequestScope,
18
+ } from '#scope'
19
+
20
+ export * from '@standard-schema/spec'
21
+
1
22
  export * from './typescript'
2
23
  export * from './app'
3
- export * from './server'
4
- export * from './context'
24
+ export * from './application/create-request-context'
25
+ export * from './application/use-declaro'
26
+ export * from './context/context'
27
+ export * from './context/context-consumer'
28
+ export * from './context/validators'
5
29
  export * from './dataflow'
6
30
  export * from './events'
7
31
  export * from './validation'
8
32
  export * from './timing'
9
- export * from './schema'
10
- export * from './helpers'
11
- export * from './interfaces'
12
- export * from './schema/formats'
33
+ export * from './scope'
34
+ export * from './errors/errors'
13
35
  export * from './pipelines'
36
+ export * from './http/headers'
37
+ export * from './http/request-context'
38
+ export * from './http/request'
39
+ export * from './http/url'
40
+ export * from './auth/permission-validator'
41
+ export * from './schema/model'
42
+ export * from './schema/json-schema'
43
+ export * from './schema/labels'
44
+ export * from './schema/model-schema'
45
+ export * from './schema/schema-mixin'
46
+ export * from './schema/test/mock-model'
47
+
48
+ export * from './shared/utils/action-descriptor'
@@ -94,14 +94,16 @@ describe('Pipelines', () => {
94
94
  number,
95
95
  }),
96
96
  )
97
- const message = vi.fn((meta?: Meta) =>
98
- meta?.positive
97
+ const message = vi.fn((meta: Meta | string): string =>
98
+ typeof meta === 'string'
99
+ ? meta
100
+ : meta?.positive
99
101
  ? `${meta.number} is positive`
100
- : meta.negative
102
+ : meta?.negative
101
103
  ? `${meta.number} is negative`
102
- : meta.zero
103
- ? `${meta.number} is zero`
104
- : `${meta.number} is not a number`,
104
+ : meta?.zero
105
+ ? `${meta?.number} is zero`
106
+ : `${meta?.number} is not a number`,
105
107
  )
106
108
 
107
109
  const originalPipeline = new Pipeline(initialInput<number>()).pipe(meta)
@@ -165,11 +167,11 @@ describe('Pipelines', () => {
165
167
 
166
168
  return meta?.positive
167
169
  ? `${meta.number} is positive`
168
- : meta.negative
170
+ : meta?.negative
169
171
  ? `${meta.number} is negative`
170
- : meta.zero
172
+ : meta?.zero
171
173
  ? `${meta.number} is zero`
172
- : `${meta.number} is not a number`
174
+ : `${meta?.number} is not a number`
173
175
  })
174
176
 
175
177
  const originalPipeline = new Pipeline(initialInput<number>()).pipe(meta)
File without changes
@@ -0,0 +1,3 @@
1
+ import { type JSONSchema7 } from 'json-schema'
2
+
3
+ export interface JSONSchema extends JSONSchema7 {}
@@ -0,0 +1,30 @@
1
+ import { capitalCase, camelCase, kebabCase, pascalCase, sentenceCase } from 'change-case'
2
+ import pluralize from 'pluralize'
3
+
4
+ export type ModelLabels = {
5
+ singularLabel: string
6
+ pluralLabel: string
7
+ singularSentence: string
8
+ pluralSentence: string
9
+ singularParameter: string
10
+ pluralParameter: string
11
+ singularSlug: string
12
+ pluralSlug: string
13
+ singularEntityName: string
14
+ pluralEntityName: string
15
+ }
16
+
17
+ export function getLabels(modelName: string, labels: Partial<ModelLabels> = {}): ModelLabels {
18
+ return {
19
+ singularLabel: capitalCase(pluralize(labels.singularLabel ?? modelName, 1)),
20
+ pluralLabel: capitalCase(pluralize(labels.pluralLabel ?? modelName, 10)),
21
+ singularSentence: sentenceCase(pluralize(labels.singularLabel ?? modelName, 1)),
22
+ pluralSentence: sentenceCase(pluralize(labels.pluralLabel ?? modelName, 10)),
23
+ singularParameter: camelCase(pluralize(labels.singularParameter ?? modelName, 1)),
24
+ pluralParameter: camelCase(pluralize(labels.pluralParameter ?? modelName, 10)),
25
+ singularSlug: kebabCase(pluralize(labels.singularSlug ?? modelName, 1)),
26
+ pluralSlug: kebabCase(pluralize(labels.pluralSlug ?? modelName, 10)),
27
+ singularEntityName: pascalCase(pluralize(labels.singularEntityName ?? modelName, 1)),
28
+ pluralEntityName: pascalCase(pluralize(labels.pluralEntityName ?? modelName, 10)),
29
+ }
30
+ }
@@ -0,0 +1,128 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { z } from 'zod/v4'
3
+ import { ModelSchema, type MergeMixins } from './model-schema'
4
+ import { MockModel } from './test/mock-model'
5
+ import type { InferModelOutput } from './model'
6
+ import type { ShallowMerge, UniqueKeys } from '../typescript'
7
+
8
+ describe('ModelSchema', () => {
9
+ it('should create a ModelSchema instance', () => {
10
+ const schema = ModelSchema.create('TestModel')
11
+ expect(schema).toBeInstanceOf(ModelSchema)
12
+ expect(schema.name).toBe('TestModel')
13
+ })
14
+
15
+ it('should support read definitions with MixinFactory', () => {
16
+ const schema = ModelSchema.create('Book').read({
17
+ detail: (h) => new MockModel(h.name, z.object({ id: z.string(), name: z.string() })),
18
+ lookup: (h) => new MockModel(h.name, z.object({ id: z.string(), name: z.string() })),
19
+ })
20
+
21
+ expect(schema.definition.detail).toBeInstanceOf(MockModel)
22
+ expect(schema.definition.lookup).toBeInstanceOf(MockModel)
23
+
24
+ const X = {} as any as InferModelOutput<(typeof schema)['definition']['lookup']>
25
+
26
+ expect(schema.definition.detail.name).toBe('BookDetail')
27
+ expect(schema.definition.lookup.name).toBe('BookLookup')
28
+ })
29
+
30
+ it('should support search definitions with MixinFactory', () => {
31
+ const schema = ModelSchema.create('Book').search({
32
+ filters: (h) =>
33
+ new MockModel(h.name, z.object({ title: z.string().optional(), author: z.string().optional() })),
34
+ summary: (h) => new MockModel(h.name, z.object({ id: z.string(), title: z.string() })),
35
+ sort: (h) =>
36
+ new MockModel(h.name, z.object({ title: z.enum(['asc', 'desc']), author: z.enum(['asc', 'desc']) })),
37
+ })
38
+
39
+ expect(schema.definition.filters).toBeInstanceOf(MockModel)
40
+ expect(schema.definition.summary).toBeInstanceOf(MockModel)
41
+ expect(schema.definition.filters.name).toBe('BookFilters')
42
+ expect(schema.definition.summary.name).toBe('BookSummary')
43
+ })
44
+
45
+ it('should support write definitions with MixinFactory', () => {
46
+ const schema = ModelSchema.create('Book').write({
47
+ input: (h) => new MockModel(h.name, z.object({ title: z.string(), author: z.string() })),
48
+ })
49
+
50
+ expect(schema.definition.input).toBeInstanceOf(MockModel)
51
+ expect(schema.definition.input.name).toBe('BookInput')
52
+ })
53
+
54
+ it('should define a primary key', () => {
55
+ const schema = ModelSchema.create('Book')
56
+ .read({
57
+ detail: (h) => new MockModel(h.name, z.object({ id: z.string(), name: z.string() })),
58
+ lookup: (h) => new MockModel(h.name, z.object({ id: z.string(), name: z.string() })),
59
+ })
60
+ .write({
61
+ input: (h) => new MockModel(h.name, z.object({ id: z.string(), title: z.string() })),
62
+ })
63
+ .entity({
64
+ primaryKey: 'id',
65
+ })
66
+
67
+ expect(schema.getEntityMetadata().primaryKey).toBe('id')
68
+ })
69
+
70
+ it('should allow redefinition of types', () => {
71
+ const schema = ModelSchema.create('Book')
72
+ .read({
73
+ detail: (h) => new MockModel(h.name, z.object({ id: z.string(), name: z.string() })),
74
+ lookup: (h) => new MockModel(h.name, z.object({ id: z.string(), name: z.string() })),
75
+ })
76
+ .write({
77
+ input: (h) => new MockModel(h.name, z.object({ id: z.string(), title: z.string() })),
78
+ })
79
+ .entity({
80
+ primaryKey: 'id',
81
+ })
82
+
83
+ const redefinedSchema = schema.read({
84
+ detail: (h) =>
85
+ new MockModel(
86
+ h.name,
87
+ z.object({ id: z.number(), name: z.string().optional(), age: z.number().optional() }),
88
+ ),
89
+ lookup: (h) => new MockModel(h.name, z.object({ id: z.number() })),
90
+ })
91
+
92
+ const patchedSchema = redefinedSchema.custom({
93
+ detail: () =>
94
+ new MockModel(
95
+ 'BookDetail',
96
+ z.object({ id: z.string(), name: z.string(), foo: z.string(), bar: z.string() }),
97
+ ),
98
+ })
99
+
100
+ const detailSchema = redefinedSchema.definition.detail.toJSONSchema()
101
+ const patchedDetailSchema = patchedSchema.definition.detail.toJSONSchema()
102
+
103
+ expect(redefinedSchema.definition.detail).toBeInstanceOf(MockModel)
104
+ expect(Object.keys(detailSchema.properties ?? {})).toEqual(['id', 'name', 'age'])
105
+
106
+ expect(Object.keys(patchedDetailSchema.properties ?? {})).toEqual(['id', 'name', 'foo', 'bar'])
107
+ })
108
+
109
+ it('should be able to redefine schema without changes to the primary key', () => {
110
+ const schema = ModelSchema.create('Book')
111
+ .read({
112
+ detail: (h) => new MockModel(h.name, z.object({ id: z.string(), name: z.string() })),
113
+ lookup: (h) => new MockModel(h.name, z.object({ id: z.string(), name: z.string() })),
114
+ })
115
+ .write({
116
+ input: (h) => new MockModel(h.name, z.object({ id: z.string(), title: z.string() })),
117
+ })
118
+ .entity({
119
+ primaryKey: 'id',
120
+ })
121
+ .read({
122
+ detail: (h) => new MockModel(h.name, z.object({ uuid: z.string(), name: z.string() })),
123
+ lookup: (h) => new MockModel(h.name, z.object({ uuid: z.string(), name: z.string() })),
124
+ })
125
+
126
+ expect(schema.getEntityMetadata().primaryKey).toBe('id')
127
+ })
128
+ })
@@ -0,0 +1,197 @@
1
+ import { merge } from '../dataflow'
2
+ import type { Merge, ShallowMerge } from '../typescript'
3
+ import { getLabels, type ModelLabels } from './labels'
4
+ import { type InferModelInput } from './model'
5
+ import {
6
+ buildMixin,
7
+ defineMixin,
8
+ type IAnyMixin,
9
+ type IMixin,
10
+ type IMixinHelper,
11
+ type IMixinHelpers,
12
+ type IMixinInput,
13
+ type InferMixinInput,
14
+ } from './schema-mixin'
15
+ import { MockModel } from './test/mock-model'
16
+ import { z } from 'zod/v4'
17
+
18
+ export type Subset<T> = {
19
+ [K in keyof T]?: T[K]
20
+ }
21
+ export type Simplify<T> = { [K in keyof T]: T[K] } & {}
22
+
23
+ export type MergeMixins<TA extends IAnyMixin | undefined, TB extends IAnyMixin> = Simplify<
24
+ TA extends IAnyMixin ? ShallowMerge<TA, TB> : TB
25
+ >
26
+
27
+ export function getReadHelpers<TName extends Readonly<string>>(h: IMixinHelper<TName>) {
28
+ return {
29
+ detail: { name: h.name },
30
+ lookup: { name: `${h.name}Lookup` as const },
31
+ }
32
+ }
33
+
34
+ const readMixin = defineMixin((h) => ({
35
+ detail: {
36
+ name: `${h.name}Detail` as const,
37
+ },
38
+ lookup: {
39
+ name: `${h.name}Lookup` as const,
40
+ },
41
+ }))
42
+ export type ReadMixin<TName extends Readonly<string>> = ReturnType<typeof readMixin<TName>>
43
+
44
+ const defaultMixin = defineMixin((h) => ({}))
45
+ export type DefaultMixin<TName extends Readonly<string>> = ReturnType<typeof defaultMixin<TName>>
46
+
47
+ const searchMixin = defineMixin((h) => ({
48
+ summary: { name: `${h.name}Summary` as const },
49
+ filters: { name: `${h.name}Filters` as const },
50
+ sort: { name: `${h.name}Sort` as const },
51
+ }))
52
+ export type SearchMixin<TName extends Readonly<string>> = ReturnType<typeof searchMixin<TName>>
53
+
54
+ const writeMixin = defineMixin((h) => ({
55
+ input: { name: `${h.name}Input` as const },
56
+ }))
57
+ export type WriteMixin<TName extends Readonly<string>> = ReturnType<typeof writeMixin<TName>>
58
+
59
+ export type IModelNames<T extends object> = {
60
+ [K in keyof T]: Readonly<string>
61
+ }
62
+
63
+ export type InferPKeyBaseType<T extends IAnyMixin | undefined> = T extends IAnyMixin
64
+ ? T['lookup'] extends object
65
+ ? keyof InferModelInput<T['lookup']> | undefined
66
+ : undefined
67
+ : undefined
68
+
69
+ export interface IModelEntityMetadata {
70
+ primaryKey: string
71
+ }
72
+
73
+ export class ModelSchema<
74
+ TName extends Readonly<string> = Readonly<string>,
75
+ T extends IAnyMixin | undefined = undefined,
76
+ TEntityMeta extends IModelEntityMetadata | undefined = undefined,
77
+ > {
78
+ static create<TName extends Readonly<string>>(name: TName): ModelSchema<TName> {
79
+ return new ModelSchema(name)
80
+ }
81
+
82
+ public readonly definition: Simplify<T>
83
+ public readonly name: TName
84
+ protected readonly entityMetadata: TEntityMeta
85
+
86
+ constructor(name: TName, definition: T = {} as T, entityMetadata?: TEntityMeta) {
87
+ this.definition = definition
88
+ this.name = name
89
+ this.entityMetadata = entityMetadata!
90
+ }
91
+
92
+ get labels(): ModelLabels {
93
+ return getLabels(this.name)
94
+ }
95
+
96
+ get helper(): IMixinHelper<TName> {
97
+ return {
98
+ name: this.name,
99
+ }
100
+ }
101
+
102
+ custom<TInput extends IMixinInput>(input: TInput): ModelSchema<TName, MergeMixins<T, IMixin<TInput>>, TEntityMeta> {
103
+ const helpers: IMixinHelpers<TInput> = Object.keys(input).reduce((acc: any, key) => {
104
+ const helper = this.helper
105
+ acc[key] = helper
106
+ return acc
107
+ }, {} as IMixinHelpers<TInput>)
108
+ const definition = buildMixin(helpers, input as any)
109
+
110
+ return new ModelSchema(
111
+ this.name,
112
+ {
113
+ ...this.definition,
114
+ ...definition,
115
+ },
116
+ this.entityMetadata,
117
+ ) as any
118
+ }
119
+
120
+ read<TInput extends InferMixinInput<ReadMixin<TName>>>(
121
+ input: TInput,
122
+ ): ModelSchema<TName, MergeMixins<T, IMixin<TInput>>, TEntityMeta> {
123
+ const helpers = readMixin(this.helper)
124
+ const definition = buildMixin(helpers, input)
125
+
126
+ return new ModelSchema(
127
+ this.name,
128
+ {
129
+ ...this.definition,
130
+ ...definition,
131
+ },
132
+ this.entityMetadata,
133
+ ) as any
134
+ }
135
+
136
+ search<TInput extends InferMixinInput<SearchMixin<TName>>>(
137
+ input: TInput,
138
+ ): ModelSchema<TName, MergeMixins<T, IMixin<TInput>>> {
139
+ const helpers = searchMixin(this.helper)
140
+ const definition = buildMixin(helpers, input)
141
+
142
+ return new ModelSchema(
143
+ this.name,
144
+ {
145
+ ...this.definition,
146
+ ...definition,
147
+ },
148
+ this.entityMetadata,
149
+ ) as any
150
+ }
151
+
152
+ write<TInput extends InferMixinInput<WriteMixin<TName>>>(
153
+ input: TInput,
154
+ ): ModelSchema<TName, MergeMixins<T, IMixin<TInput>>> {
155
+ const helpers = writeMixin(this.helper)
156
+ const definition = buildMixin(helpers, input)
157
+
158
+ return new ModelSchema(
159
+ this.name,
160
+ {
161
+ ...this.definition,
162
+ ...definition,
163
+ },
164
+ this.entityMetadata,
165
+ ) as any
166
+ }
167
+
168
+ entity<
169
+ TEntityMeta extends {
170
+ primaryKey: InferPKeyBaseType<T>
171
+ },
172
+ >(meta: TEntityMeta): ModelSchema<TName, T, TEntityMeta extends IModelEntityMetadata ? TEntityMeta : undefined> {
173
+ const lookupMeta = this.definition?.['lookup']?.toJSONSchema()
174
+ const lookupKeys = Object.keys(lookupMeta?.properties ?? {})
175
+
176
+ const metaIsValid = meta && typeof meta.primaryKey === 'string' && lookupKeys.includes(meta.primaryKey)
177
+ return new ModelSchema<TName, T, TEntityMeta extends IModelEntityMetadata ? TEntityMeta : undefined>(
178
+ this.name,
179
+ this.definition,
180
+ metaIsValid ? (meta as any) : undefined,
181
+ )
182
+ }
183
+
184
+ getEntityMetadata(): TEntityMeta {
185
+ return this.entityMetadata
186
+ }
187
+ }
188
+
189
+ export type AnyModelSchema = ModelSchema<any, any, any>
190
+
191
+ const test = ModelSchema.create('Test').read({
192
+ detail: (h) => new MockModel(h.name, z.object({ id: z.string(), name: z.string() })),
193
+ lookup: (h) => new MockModel(h.name, z.object({ id: z.string(), name: z.string() })),
194
+ })
195
+
196
+ const d = test.definition
197
+ const l = {} as any as keyof InferModelInput<typeof test.definition.lookup>