@declaro/core 2.0.0-y.0 → 2.1.1

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
@@ -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)
@@ -0,0 +1,16 @@
1
+ import { type JSONSchema7 } from 'json-schema'
2
+
3
+ export type JSONSchemaDefinition = JSONSchema | boolean
4
+
5
+ export interface JSONMeta {
6
+ hidden?: boolean
7
+ private?: boolean
8
+ }
9
+
10
+ export interface JSONSchema extends JSONSchema7, JSONMeta {
11
+ properties?:
12
+ | {
13
+ [key: string]: JSONSchemaDefinition
14
+ }
15
+ | undefined
16
+ }
@@ -1,30 +1,30 @@
1
- import { capitalCase, camelCase, paramCase, pascalCase } from 'change-case'
1
+ import { capitalCase, camelCase, kebabCase, pascalCase, sentenceCase } from 'change-case'
2
2
  import pluralize from 'pluralize'
3
3
 
4
- export type EntityLabels = {
5
- singularLabel?: string
6
- pluralLabel?: string
7
- singularParameter?: string
8
- pluralParameter?: string
9
- singularSlug?: string
10
- pluralSlug?: string
11
- singularEntityName?: string
12
- pluralEntityName?: string
13
- singularTableName?: string
14
- pluralTableName?: string
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
15
  }
16
16
 
17
- export function getEntityLabels(entity: string, labels: Partial<EntityLabels> = {}): EntityLabels {
17
+ export function getLabels(modelName: string, labels: Partial<ModelLabels> = {}): ModelLabels {
18
18
  return {
19
- singularLabel: labels.singularLabel ?? capitalCase(pluralize(entity, 1)),
20
- pluralLabel: labels.pluralLabel ?? capitalCase(pluralize(entity, 10)),
21
- singularParameter: labels.singularParameter ?? camelCase(pluralize(entity, 1)),
22
- pluralParameter: labels.pluralParameter ?? camelCase(pluralize(entity, 10)),
23
- singularSlug: labels.singularSlug ?? paramCase(pluralize(entity, 1)),
24
- pluralSlug: labels.pluralSlug ?? paramCase(pluralize(entity, 10)),
25
- singularEntityName: labels.singularEntityName ?? pascalCase(pluralize(entity, 1)),
26
- pluralEntityName: labels.pluralEntityName ?? pascalCase(pluralize(entity, 10)),
27
- singularTableName: labels.singularTableName ?? paramCase(pluralize(entity, 1)),
28
- pluralTableName: labels.pluralTableName ?? paramCase(pluralize(entity, 10)),
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
29
  }
30
30
  }
@@ -0,0 +1,282 @@
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 { InferModelInput, InferModelOutput } from './model'
6
+ import type { ShallowMerge, UniqueKeys } from '../typescript'
7
+ import type { StandardSchemaV1 } from '@standard-schema/spec'
8
+
9
+ describe('ModelSchema', () => {
10
+ it('should create a ModelSchema instance', () => {
11
+ const schema = ModelSchema.create('TestModel')
12
+ expect(schema).toBeInstanceOf(ModelSchema)
13
+ expect(schema.name).toBe('TestModel')
14
+ })
15
+
16
+ it('should support read definitions with MixinFactory', () => {
17
+ const schema = ModelSchema.create('Book').read({
18
+ detail: (h) => new MockModel(h.name, z.object({ id: z.string(), name: z.string() })),
19
+ lookup: (h) => new MockModel(h.name, z.object({ id: z.string(), name: z.string() })),
20
+ })
21
+
22
+ expect(schema.definition.detail).toBeInstanceOf(MockModel)
23
+ expect(schema.definition.lookup).toBeInstanceOf(MockModel)
24
+
25
+ const X = {} as any as InferModelOutput<(typeof schema)['definition']['lookup']>
26
+
27
+ expect(schema.definition.detail.name).toBe('BookDetail')
28
+ expect(schema.definition.lookup.name).toBe('BookLookup')
29
+ })
30
+
31
+ it('should support search definitions with MixinFactory', () => {
32
+ const schema = ModelSchema.create('Book').search({
33
+ filters: (h) =>
34
+ new MockModel(h.name, z.object({ title: z.string().optional(), author: z.string().optional() })),
35
+ summary: (h) => new MockModel(h.name, z.object({ id: z.string(), title: z.string() })),
36
+ sort: (h) =>
37
+ new MockModel(h.name, z.object({ title: z.enum(['asc', 'desc']), author: z.enum(['asc', 'desc']) })),
38
+ })
39
+
40
+ expect(schema.definition.filters).toBeInstanceOf(MockModel)
41
+ expect(schema.definition.summary).toBeInstanceOf(MockModel)
42
+ expect(schema.definition.filters.name).toBe('BookFilters')
43
+ expect(schema.definition.summary.name).toBe('BookSummary')
44
+ })
45
+
46
+ it('should support write definitions with MixinFactory', () => {
47
+ const schema = ModelSchema.create('Book').write({
48
+ input: (h) => new MockModel(h.name, z.object({ title: z.string(), author: z.string() })),
49
+ })
50
+
51
+ expect(schema.definition.input).toBeInstanceOf(MockModel)
52
+ expect(schema.definition.input.name).toBe('BookInput')
53
+ })
54
+
55
+ it('should define a primary key', () => {
56
+ const schema = ModelSchema.create('Book')
57
+ .read({
58
+ detail: (h) => new MockModel(h.name, z.object({ id: z.string(), name: z.string() })),
59
+ lookup: (h) => new MockModel(h.name, z.object({ id: z.string(), name: z.string() })),
60
+ })
61
+ .write({
62
+ input: (h) => new MockModel(h.name, z.object({ id: z.string(), title: z.string() })),
63
+ })
64
+ .entity({
65
+ primaryKey: 'id',
66
+ })
67
+
68
+ expect(schema.getEntityMetadata().primaryKey).toBe('id')
69
+ })
70
+
71
+ it('should allow redefinition of types', () => {
72
+ const schema = ModelSchema.create('Book')
73
+ .read({
74
+ detail: (h) => new MockModel(h.name, z.object({ id: z.string(), name: z.string() })),
75
+ lookup: (h) => new MockModel(h.name, z.object({ id: z.string(), name: z.string() })),
76
+ })
77
+ .write({
78
+ input: (h) => new MockModel(h.name, z.object({ id: z.string(), title: z.string() })),
79
+ })
80
+ .entity({
81
+ primaryKey: 'id',
82
+ })
83
+
84
+ const redefinedSchema = schema.read({
85
+ detail: (h) =>
86
+ new MockModel(
87
+ h.name,
88
+ z.object({ id: z.number(), name: z.string().optional(), age: z.number().optional() }),
89
+ ),
90
+ lookup: (h) => new MockModel(h.name, z.object({ id: z.number() })),
91
+ })
92
+
93
+ const patchedSchema = redefinedSchema.custom({
94
+ detail: () =>
95
+ new MockModel(
96
+ 'BookDetail',
97
+ z.object({ id: z.string(), name: z.string(), foo: z.string(), bar: z.string() }),
98
+ ),
99
+ })
100
+
101
+ const detailSchema = redefinedSchema.definition.detail.toJSONSchema()
102
+ const patchedDetailSchema = patchedSchema.definition.detail.toJSONSchema()
103
+
104
+ expect(redefinedSchema.definition.detail).toBeInstanceOf(MockModel)
105
+ expect(Object.keys(detailSchema.properties ?? {})).toEqual(['id', 'name', 'age'])
106
+
107
+ expect(Object.keys(patchedDetailSchema.properties ?? {})).toEqual(['id', 'name', 'foo', 'bar'])
108
+ })
109
+
110
+ it('should be able to redefine schema without changes to the primary key', () => {
111
+ const schema = ModelSchema.create('Book')
112
+ .read({
113
+ detail: (h) => new MockModel(h.name, z.object({ id: z.string(), name: z.string() })),
114
+ lookup: (h) => new MockModel(h.name, z.object({ id: z.string(), name: z.string() })),
115
+ })
116
+ .write({
117
+ input: (h) => new MockModel(h.name, z.object({ id: z.string(), title: z.string() })),
118
+ })
119
+ .entity({
120
+ primaryKey: 'id',
121
+ })
122
+ .read({
123
+ detail: (h) => new MockModel(h.name, z.object({ uuid: z.string(), name: z.string() })),
124
+ lookup: (h) => new MockModel(h.name, z.object({ uuid: z.string(), name: z.string() })),
125
+ })
126
+
127
+ expect(schema.getEntityMetadata().primaryKey).toBe('id')
128
+ })
129
+
130
+ it('should be able to manually strip private and hidden keys from input', () => {
131
+ const testModel = new MockModel(
132
+ 'TestModel',
133
+ z.object({
134
+ id: z.string(),
135
+ name: z.string(),
136
+ secret: z.string().optional().meta({ private: true }),
137
+ internalNote: z.string().optional().meta({ hidden: true }),
138
+ }),
139
+ )
140
+
141
+ const payload: InferModelInput<typeof testModel> = {
142
+ id: '1',
143
+ name: 'Test',
144
+ secret: 'top-secret',
145
+ internalNote: 'for-internal-use-only',
146
+ }
147
+
148
+ const stripped = testModel.stripExcludedFields(payload)
149
+
150
+ expect(stripped).toEqual({
151
+ id: '1',
152
+ name: 'Test',
153
+ internalNote: 'for-internal-use-only',
154
+ })
155
+ })
156
+
157
+ it('should strip private fields from validation input', async () => {
158
+ const testModel = new MockModel(
159
+ 'TestModel',
160
+ z.object({
161
+ id: z.string(),
162
+ name: z.string(),
163
+ secret: z.string().optional().meta({ private: true }),
164
+ internalNote: z.string().optional().meta({ hidden: true }),
165
+ }),
166
+ )
167
+
168
+ const payload: InferModelInput<typeof testModel> = {
169
+ id: '1',
170
+ name: 'Test',
171
+ secret: 'top-secret',
172
+ internalNote: 'for-internal-use-only',
173
+ }
174
+
175
+ const validation = await testModel.validate(payload, { strict: false })
176
+
177
+ expect(validation.issues).toBeUndefined()
178
+
179
+ const output = (validation as StandardSchemaV1.SuccessResult<InferModelOutput<typeof testModel>>).value
180
+
181
+ expect(output.secret).toBeUndefined()
182
+ })
183
+
184
+ it('should not strip hidden fields from validation input', async () => {
185
+ const testModel = new MockModel(
186
+ 'TestModel',
187
+ z.object({
188
+ id: z.string(),
189
+ name: z.string(),
190
+ secret: z.string().optional().meta({ private: true }),
191
+ internalNote: z.string().optional().meta({ hidden: true }),
192
+ }),
193
+ )
194
+
195
+ const payload: InferModelInput<typeof testModel> = {
196
+ id: '1',
197
+ name: 'Test',
198
+ secret: 'top-secret',
199
+ internalNote: 'for-internal-use-only',
200
+ }
201
+
202
+ const validation = await testModel.validate(payload, { strict: false })
203
+
204
+ expect(validation.issues).toBeUndefined()
205
+
206
+ const output = (validation as StandardSchemaV1.SuccessResult<InferModelOutput<typeof testModel>>).value
207
+
208
+ expect(output.internalNote).toBe('for-internal-use-only')
209
+ })
210
+
211
+ it('should strip private fields from model schema by default', () => {
212
+ const testModel = new MockModel(
213
+ 'TestModel',
214
+ z.object({
215
+ id: z.string(),
216
+ name: z.string(),
217
+ secret: z.string().optional().meta({ private: true }),
218
+ internalNote: z.string().optional().meta({ hidden: true }),
219
+ }),
220
+ )
221
+
222
+ const jsonSchema = testModel.toJSONSchema()
223
+
224
+ expect(Object.keys(jsonSchema.properties!)).toEqual(['id', 'name', 'internalNote'])
225
+ })
226
+
227
+ it('should not strip hidden fields from model schema', () => {
228
+ const testModel = new MockModel(
229
+ 'TestModel',
230
+ z.object({
231
+ id: z.string(),
232
+ name: z.string(),
233
+ secret: z.string().optional().meta({ private: true }),
234
+ internalNote: z.string().optional().meta({ hidden: true }),
235
+ }),
236
+ )
237
+
238
+ const jsonSchema = testModel.toJSONSchema({ includePrivateFields: false })
239
+
240
+ expect(Object.keys(jsonSchema.properties!)).toEqual(['id', 'name', 'internalNote'])
241
+ })
242
+
243
+ it('should not strip private fields from model schema when specified', () => {
244
+ const testModel = new MockModel(
245
+ 'TestModel',
246
+ z.object({
247
+ id: z.string(),
248
+ name: z.string(),
249
+ secret: z.string().optional().meta({ private: true }),
250
+ internalNote: z.string().optional().meta({ hidden: true }),
251
+ }),
252
+ )
253
+
254
+ const jsonSchema = testModel.toJSONSchema({ includePrivateFields: true })
255
+
256
+ expect(Object.keys(jsonSchema.properties!)).toEqual(['id', 'name', 'secret', 'internalNote'])
257
+ })
258
+
259
+ it('should include hidden fields in model schema when specified', () => {
260
+ const testModel = new MockModel(
261
+ 'TestModel',
262
+ z.object({
263
+ id: z.string(),
264
+ name: z.string(),
265
+ secret: z.string().optional().meta({ private: true }),
266
+ internalNote: z.string().optional().meta({ hidden: true }),
267
+ }),
268
+ )
269
+
270
+ const jsonSchema = testModel.toJSONSchema()
271
+
272
+ expect(Object.keys(jsonSchema.properties!)).toEqual(['id', 'name', 'internalNote'])
273
+
274
+ const internalNoteSchema = jsonSchema.properties?.['internalNote']
275
+
276
+ if (typeof internalNoteSchema === 'object') {
277
+ expect(internalNoteSchema.hidden).toBe(true)
278
+ } else {
279
+ throw new Error('internalNote schema is not an object')
280
+ }
281
+ })
282
+ })
@@ -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>