@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
@@ -0,0 +1,143 @@
1
+ import type { StandardSchemaV1 } from '@standard-schema/spec'
2
+ import type { JSONSchema } from './json-schema'
3
+ import { SystemError, ValidationError } from '../errors/errors'
4
+ import { getLabels, type ModelLabels } from './labels'
5
+
6
+ export interface ModelValidationOptions {
7
+ strict?: boolean
8
+ }
9
+
10
+ export interface ModelSchemaOptions {
11
+ includePrivateFields?: boolean
12
+ }
13
+
14
+ export function getDefaultModelSchemaOptions(): ModelSchemaOptions {
15
+ return {
16
+ includePrivateFields: false,
17
+ }
18
+ }
19
+
20
+ export abstract class Model<TName extends Readonly<string>, TSchema extends StandardSchemaV1>
21
+ implements StandardSchemaV1<StandardSchemaV1.InferInput<TSchema>, StandardSchemaV1.InferOutput<TSchema>>
22
+ {
23
+ public readonly name: TName
24
+ /**
25
+ * @warning You may not need to use this property directly.
26
+ * Use the `validate` method instead to ensure proper validation and error handling.
27
+ * Use the `toJSONSchema` method to get the JSON Schema representation for introspection or documentation.
28
+ */
29
+ public readonly schema: TSchema
30
+
31
+ constructor(name: TName, schema: TSchema) {
32
+ if (!schema || !schema['~standard'] || schema['~standard'].version !== 1) {
33
+ throw new SystemError(`Invalid schema provided for model "${name}". Must implement StandardSchemaV1.`)
34
+ }
35
+
36
+ this.name = name
37
+ this.schema = schema
38
+ }
39
+
40
+ stripExcludedFields(value: StandardSchemaV1.InferInput<TSchema>): StandardSchemaV1.InferInput<TSchema> {
41
+ const meta = this.toJSONSchema({
42
+ includePrivateFields: true,
43
+ })
44
+
45
+ const excludedKeys = Object.keys(meta.properties ?? {}).filter((key) => {
46
+ const property = meta.properties?.[key]
47
+ return !!property && typeof property === 'object' && property.private === true
48
+ })
49
+
50
+ if (value && excludedKeys.length > 0) {
51
+ excludedKeys.forEach((key) => {
52
+ delete value[key]
53
+ })
54
+ }
55
+
56
+ return value
57
+ }
58
+
59
+ async validate(
60
+ value: StandardSchemaV1.InferInput<TSchema>,
61
+ options?: ModelValidationOptions,
62
+ ): Promise<StandardSchemaV1.Result<StandardSchemaV1.InferOutput<TSchema>>> {
63
+ const meta = this.toJSONSchema()
64
+
65
+ value = this.stripExcludedFields(value)
66
+
67
+ const result = await this.schema['~standard'].validate(value)
68
+
69
+ if (result.issues) {
70
+ const issues = result.issues.map((issue) => {
71
+ let schema: JSONSchema | undefined = meta
72
+ let field: string | undefined = undefined
73
+ issue.path?.forEach((segment) => {
74
+ field = segment as string
75
+ const nested = schema?.properties?.[segment as string] as JSONSchema | undefined
76
+ if (nested) {
77
+ schema = nested
78
+ } else {
79
+ schema = undefined
80
+ }
81
+ })
82
+
83
+ let title = schema?.title
84
+ if (!title && field) {
85
+ const labels = getLabels(field)
86
+
87
+ title = labels?.singularLabel
88
+ }
89
+
90
+ const message = title ? `Validation failed for field "${title}": ${issue.message}` : issue.message
91
+
92
+ return {
93
+ ...issue,
94
+ message,
95
+ }
96
+ })
97
+
98
+ const message = issues.map((issue) => issue.message)?.[0]
99
+
100
+ if (options?.strict === false) {
101
+ return {
102
+ issues,
103
+ }
104
+ }
105
+
106
+ throw new ValidationError(message, { result })
107
+ }
108
+
109
+ return result
110
+ }
111
+
112
+ get labels(): ModelLabels {
113
+ return getLabels(this.name)
114
+ }
115
+
116
+ abstract toJSONSchema(options?: ModelSchemaOptions): JSONSchema
117
+
118
+ // Implementing StandardSchemaV1 interface
119
+ get version(): number {
120
+ return 1
121
+ }
122
+
123
+ // Correcting the '~standard' property implementation to match StandardSchemaV1
124
+ get '~standard'(): StandardSchemaV1['~standard'] {
125
+ return {
126
+ version: 1,
127
+ vendor: 'Declaro',
128
+ validate: this.validate.bind(this),
129
+ }
130
+ }
131
+ }
132
+
133
+ export type UnwrapModelSchema<T extends Model<any, any>> = T extends Model<any, infer S> ? S : never
134
+ export type InferModelOutput<T extends Model<any, any>> = StandardSchemaV1.InferOutput<UnwrapModelSchema<T>>
135
+ export type InferModelInput<T extends Model<any, any>> = StandardSchemaV1.InferInput<UnwrapModelSchema<T>>
136
+
137
+ export type IAnyModel = Model<Readonly<string>, StandardSchemaV1>
138
+
139
+ export interface IModelHelper<TNameRecommendation extends Readonly<string>> {
140
+ name: TNameRecommendation
141
+ }
142
+
143
+ export type ModelFactory<TName extends Readonly<string>> = (helper: IModelHelper<TName>) => IAnyModel
@@ -0,0 +1,51 @@
1
+ import { type IAnyModel, type ModelFactory } from './model'
2
+
3
+ export interface IAnyMixin {
4
+ [key: string]: IAnyModel
5
+ }
6
+
7
+ export type IMixin<TInput extends IMixinInput> = {
8
+ [K in keyof TInput]: TInput[K] extends ModelFactory<any> ? ReturnType<TInput[K]> : never
9
+ }
10
+
11
+ export interface IMixinInput {
12
+ [key: string]: ModelFactory<any>
13
+ }
14
+
15
+ export interface IMixinHelper<TName extends Readonly<string>> {
16
+ name: TName
17
+ }
18
+
19
+ export type IMixinHelpers<TMixin extends IMixinInput> = {
20
+ [K in keyof TMixin]: TMixin[K] extends ModelFactory<infer TName> ? IMixinHelper<TName> : never
21
+ }
22
+
23
+ export function buildMixin<TMixin extends IMixinHelpers<any>>(
24
+ helpers: TMixin,
25
+ input: InferMixinInput<TMixin>,
26
+ ): IMixin<InferMixinInput<TMixin>> {
27
+ const mixin: IAnyMixin = {}
28
+ for (const key in helpers) {
29
+ const helper = helpers[key]
30
+ if (typeof helper?.name === 'string') {
31
+ mixin[key] = input[key](helper)
32
+ } else {
33
+ throw new Error(`Invalid helper name for key "${key}": ${helper.name}`)
34
+ }
35
+ }
36
+ return mixin as IMixin<InferMixinInput<TMixin>>
37
+ }
38
+
39
+ export type MixinFn<THelpers extends IMixinHelpers<any>> = <TName extends Readonly<string>>(
40
+ helper: IMixinHelper<TName>,
41
+ ) => THelpers
42
+
43
+ export function defineMixin<TInput extends IMixinHelpers<any>, Fn extends MixinFn<TInput>>(fn: Fn) {
44
+ return fn
45
+ }
46
+
47
+ export type InferMixinInput<T extends IMixinHelpers<any>> = {
48
+ [K in keyof T]: T[K] extends IMixinHelper<infer TName> ? ModelFactory<TName> : never
49
+ }
50
+
51
+ export type InferMixinSchema<TMixin extends IMixinHelpers<any>> = IMixin<InferMixinInput<TMixin>>
@@ -0,0 +1,19 @@
1
+ import { z } from 'zod/v4'
2
+ import type { $ZodType } from 'zod/v4/core'
3
+ import type { JSONSchema } from '../json-schema'
4
+ import { Model, type ModelSchemaOptions } from '../model'
5
+ import { stripPrivateFieldsFromSchema } from '../../shared/utils/schema-utils'
6
+
7
+ export class MockModel<TName extends Readonly<string>, TSchema extends $ZodType<any>> extends Model<TName, TSchema> {
8
+ constructor(name: TName, schema: TSchema) {
9
+ super(name, schema)
10
+ }
11
+
12
+ toJSONSchema(options?: ModelSchemaOptions): JSONSchema {
13
+ const jsonSchema = z.toJSONSchema(this.schema)
14
+ if (options?.includePrivateFields !== true) {
15
+ stripPrivateFieldsFromSchema(jsonSchema as JSONSchema)
16
+ }
17
+ return jsonSchema as JSONSchema
18
+ }
19
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Base scope interfaces for @declaro/core
3
+ *
4
+ * Users can augment these interfaces by creating their own declaration file:
5
+ *
6
+ * ```typescript
7
+ * // types/scope.d.ts
8
+ * declare module '#scope' {
9
+ * interface AppScope {
10
+ * // Add your app-level scope properties
11
+ * config: MyAppConfig
12
+ * }
13
+ *
14
+ * interface RequestScope {
15
+ * // Add your request-level scope properties
16
+ * user: User
17
+ * }
18
+ * }
19
+ * ```
20
+ */
21
+
22
+ /**
23
+ * Base application scope interface.
24
+ * This represents data that is available throughout the entire application lifecycle.
25
+ */
26
+ export interface AppScope {}
27
+
28
+ /**
29
+ * Base request scope interface.
30
+ * This represents data that is available for the duration of a single request.
31
+ * Extends AppScope to inherit application-level data.
32
+ */
33
+ export interface RequestScope extends AppScope {}
@@ -0,0 +1,182 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+ import { ActionDescriptor } from './action-descriptor'
3
+
4
+ describe('ActionDescriptor', () => {
5
+ describe('constructor', () => {
6
+ it('should initialize with input values', () => {
7
+ const input = { namespace: 'auth', resource: 'user', action: 'create', scope: 'admin' }
8
+ const descriptor = new ActionDescriptor(input)
9
+
10
+ expect(descriptor.namespace).toBe('auth')
11
+ expect(descriptor.resource).toBe('user')
12
+ expect(descriptor.action).toBe('create')
13
+ expect(descriptor.scope).toBe('admin')
14
+ })
15
+
16
+ it('should initialize with default values if input is missing', () => {
17
+ const input = {}
18
+ const descriptor = new ActionDescriptor(input)
19
+
20
+ expect(descriptor.namespace).toBe('global')
21
+ expect(descriptor.resource).toBe('*')
22
+ expect(descriptor.action).toBe('*')
23
+ expect(descriptor.scope).toBeUndefined()
24
+ })
25
+ })
26
+
27
+ describe('update', () => {
28
+ it('should update descriptor values', () => {
29
+ const input = { namespace: 'auth', resource: 'user', action: 'create', scope: 'admin' }
30
+ const descriptor = new ActionDescriptor(input)
31
+
32
+ descriptor.update({ resource: 'group', action: 'delete' })
33
+
34
+ expect(descriptor.namespace).toBe('auth')
35
+ expect(descriptor.resource).toBe('group')
36
+ expect(descriptor.action).toBe('delete')
37
+ expect(descriptor.scope).toBe('admin')
38
+ })
39
+
40
+ it('should retain existing values if update input is missing', () => {
41
+ const input = { namespace: 'auth', resource: 'user', action: 'create', scope: 'admin' }
42
+ const descriptor = new ActionDescriptor(input)
43
+
44
+ descriptor.update({})
45
+
46
+ expect(descriptor.namespace).toBe('auth')
47
+ expect(descriptor.resource).toBe('user')
48
+ expect(descriptor.action).toBe('create')
49
+ expect(descriptor.scope).toBe('admin')
50
+ })
51
+ })
52
+
53
+ describe('toJSON', () => {
54
+ it('should return a JSON representation of the descriptor', () => {
55
+ const input = { namespace: 'auth', resource: 'user', action: 'create', scope: 'admin' }
56
+ const descriptor = new ActionDescriptor(input)
57
+
58
+ const json = descriptor.toJSON()
59
+ expect(json).toEqual(input)
60
+ })
61
+ })
62
+
63
+ describe('toString', () => {
64
+ it('should return a string representation of the descriptor', () => {
65
+ const input = { namespace: 'auth', resource: 'user', action: 'create', scope: 'admin' }
66
+ const descriptor = new ActionDescriptor(input)
67
+
68
+ const str = descriptor.toString()
69
+ expect(str).toBe('auth::user.create:admin')
70
+ })
71
+
72
+ it('should handle missing scope gracefully', () => {
73
+ const input = { namespace: 'auth', resource: 'user', action: 'create' }
74
+ const descriptor = new ActionDescriptor(input)
75
+
76
+ const str = descriptor.toString()
77
+ expect(str).toBe('auth::user.create')
78
+ })
79
+ })
80
+
81
+ describe('parse', () => {
82
+ it('should parse a string into an ActionDescriptor', () => {
83
+ const descriptor = new ActionDescriptor({ namespace: 'global' })
84
+
85
+ const input = 'auth::user.create:admin'
86
+ descriptor.parse(input)
87
+
88
+ expect(descriptor.namespace).toBe('auth')
89
+ expect(descriptor.resource).toBe('user')
90
+ expect(descriptor.action).toBe('create')
91
+ expect(descriptor.scope).toBe('admin')
92
+ })
93
+
94
+ it('should handle missing scope gracefully', () => {
95
+ const descriptor = new ActionDescriptor({ namespace: 'global' })
96
+
97
+ const input = 'auth::user.create'
98
+ descriptor.parse(input)
99
+
100
+ expect(descriptor.namespace).toBe('auth')
101
+ expect(descriptor.resource).toBe('user')
102
+ expect(descriptor.action).toBe('create')
103
+ expect(descriptor.scope).toBeUndefined()
104
+ })
105
+
106
+ it('should use default values for missing parts', () => {
107
+ const descriptor = new ActionDescriptor({ namespace: 'global' })
108
+
109
+ const input = 'auth'
110
+ descriptor.parse(input)
111
+
112
+ expect(descriptor.namespace).toBe('auth')
113
+ expect(descriptor.resource).toBe('*')
114
+ expect(descriptor.action).toBe('*')
115
+ expect(descriptor.scope).toBeUndefined()
116
+ })
117
+ })
118
+
119
+ describe('fromJSON', () => {
120
+ it('should create an ActionDescriptor from a JSON object', () => {
121
+ const input = { namespace: 'auth', resource: 'user', action: 'create', scope: 'admin' }
122
+ const descriptor = ActionDescriptor.fromJSON(input)
123
+
124
+ expect(descriptor.namespace).toBe('auth')
125
+ expect(descriptor.resource).toBe('user')
126
+ expect(descriptor.action).toBe('create')
127
+ expect(descriptor.scope).toBe('admin')
128
+ })
129
+
130
+ it('should handle missing scope gracefully', () => {
131
+ const input = { namespace: 'auth', resource: 'user', action: 'create' }
132
+ const descriptor = ActionDescriptor.fromJSON(input)
133
+
134
+ expect(descriptor.namespace).toBe('auth')
135
+ expect(descriptor.resource).toBe('user')
136
+ expect(descriptor.action).toBe('create')
137
+ expect(descriptor.scope).toBeUndefined()
138
+ })
139
+
140
+ it('should use default values for missing parts', () => {
141
+ const input = { namespace: 'auth', resource: '*', action: '*', scope: undefined }
142
+ const descriptor = ActionDescriptor.fromJSON(input)
143
+
144
+ expect(descriptor.namespace).toBe('auth')
145
+ expect(descriptor.resource).toBe('*')
146
+ expect(descriptor.action).toBe('*')
147
+ expect(descriptor.scope).toBeUndefined()
148
+ })
149
+ })
150
+
151
+ describe('fromString', () => {
152
+ it('should create an ActionDescriptor from a string', () => {
153
+ const input = 'auth::user.create:admin'
154
+ const descriptor = ActionDescriptor.fromString(input)
155
+
156
+ expect(descriptor.namespace).toBe('auth')
157
+ expect(descriptor.resource).toBe('user')
158
+ expect(descriptor.action).toBe('create')
159
+ expect(descriptor.scope).toBe('admin')
160
+ })
161
+
162
+ it('should handle missing scope gracefully', () => {
163
+ const input = 'auth::user.create'
164
+ const descriptor = ActionDescriptor.fromString(input)
165
+
166
+ expect(descriptor.namespace).toBe('auth')
167
+ expect(descriptor.resource).toBe('user')
168
+ expect(descriptor.action).toBe('create')
169
+ expect(descriptor.scope).toBeUndefined()
170
+ })
171
+
172
+ it('should use default values for missing parts', () => {
173
+ const input = 'auth'
174
+ const descriptor = ActionDescriptor.fromString(input)
175
+
176
+ expect(descriptor.namespace).toBe('auth')
177
+ expect(descriptor.resource).toBe('*')
178
+ expect(descriptor.action).toBe('*')
179
+ expect(descriptor.scope).toBeUndefined()
180
+ })
181
+ })
182
+ })
@@ -0,0 +1,102 @@
1
+ import { kebabCase } from 'change-case'
2
+
3
+ export interface IActionDescriptorInput {
4
+ namespace?: string
5
+ resource?: string
6
+ action?: string
7
+ scope?: string
8
+ }
9
+
10
+ export interface IActionDescriptor {
11
+ namespace: string
12
+ resource: string
13
+ action: string
14
+ scope?: string
15
+ }
16
+
17
+ export class ActionDescriptor implements IActionDescriptor {
18
+ protected readonly descriptor: IActionDescriptorInput
19
+
20
+ constructor(input: IActionDescriptorInput) {
21
+ this.descriptor = {}
22
+ this.update(input)
23
+ }
24
+
25
+ update(input: IActionDescriptorInput) {
26
+ this.descriptor.namespace = this.parameterize(input.namespace ?? this.descriptor.namespace)
27
+ this.descriptor.resource = this.parameterize(input.resource ?? this.descriptor.resource)
28
+ this.descriptor.action = input.action ?? this.descriptor.action
29
+ this.descriptor.scope = this.parameterize(input.scope ?? this.descriptor.scope)
30
+
31
+ return this
32
+ }
33
+
34
+ get namespace(): string {
35
+ return this.descriptor.namespace ?? 'global'
36
+ }
37
+
38
+ get resource(): string {
39
+ return this.descriptor.resource ?? '*'
40
+ }
41
+
42
+ get action(): string {
43
+ return this.descriptor.action ?? '*'
44
+ }
45
+
46
+ get scope(): string | undefined {
47
+ return this.descriptor.scope
48
+ }
49
+
50
+ toString(): string {
51
+ return `${this.namespace}::${this.resource}.${this.action}${this.scope ? `:${this.scope}` : ''}`
52
+ }
53
+
54
+ toJSON(): IActionDescriptor {
55
+ return {
56
+ namespace: this.namespace,
57
+ resource: this.resource,
58
+ action: this.action,
59
+ scope: this.scope,
60
+ }
61
+ }
62
+
63
+ parse(input: string): ActionDescriptor {
64
+ let remainder = input.trim()
65
+
66
+ const [namespace, ...rest] = remainder.split('::').filter((s) => s.trim() !== '')
67
+ remainder = rest.join('::').trim()
68
+
69
+ const [resource, ...rest2] = remainder.split('.').filter((s) => s.trim() !== '')
70
+ remainder = rest2.join('.').trim()
71
+
72
+ const [action, ...rest3] = remainder.split(':').filter((s) => s.trim() !== '')
73
+ remainder = rest3.join(':').trim()
74
+
75
+ const [scope] = remainder.split(':').filter((s) => s.trim() !== '')
76
+
77
+ this.update({
78
+ namespace: namespace,
79
+ resource: resource,
80
+ action: action,
81
+ scope: scope,
82
+ })
83
+
84
+ return this
85
+ }
86
+
87
+ static fromString(input: string): ActionDescriptor {
88
+ const descriptor = new ActionDescriptor({ namespace: 'global', resource: '*', action: '*' })
89
+ return descriptor.parse(input)
90
+ }
91
+
92
+ static fromJSON(json: IActionDescriptorInput): ActionDescriptor {
93
+ return new ActionDescriptor(json)
94
+ }
95
+
96
+ protected parameterize(string?: string) {
97
+ if (!string || string === '*') {
98
+ return string
99
+ }
100
+ return kebabCase(string)
101
+ }
102
+ }
@@ -0,0 +1,33 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type { JSONSchema } from '../../schema/json-schema'
3
+ import { stripPrivateFieldsFromSchema } from './schema-utils'
4
+
5
+ describe('Schema Utils', () => {
6
+ it('should strip private fields from schema', () => {
7
+ const schema: JSONSchema = {
8
+ type: 'object',
9
+ properties: {
10
+ id: { type: 'string' },
11
+ name: { type: 'string' },
12
+ secret: { type: 'string', private: true },
13
+ internalNote: { type: 'string', hidden: true },
14
+ },
15
+ }
16
+
17
+ const stripped = stripPrivateFieldsFromSchema(schema)
18
+
19
+ // The return value should have the private field removed
20
+ expect(stripped.properties).toBeDefined()
21
+ expect(stripped.properties?.['id']).toBeDefined()
22
+ expect(stripped.properties?.['name']).toBeDefined()
23
+ expect(stripped.properties?.['secret']).toBeUndefined()
24
+ expect(stripped.properties?.['internalNote']).toBeDefined()
25
+
26
+ // The original schema should also have the private field removed
27
+ expect(schema.properties).toBeDefined()
28
+ expect(schema.properties?.['id']).toBeDefined()
29
+ expect(schema.properties?.['name']).toBeDefined()
30
+ expect(schema.properties?.['secret']).toBeUndefined()
31
+ expect(schema.properties?.['internalNote']).toBeDefined()
32
+ })
33
+ })
@@ -0,0 +1,17 @@
1
+ import type { JSONSchema } from '../../schema/json-schema'
2
+
3
+ export function stripPrivateFieldsFromSchema(schema: JSONSchema): JSONSchema {
4
+ if (typeof schema?.properties === 'object') {
5
+ for (const key of Object.keys(schema.properties)) {
6
+ const property = schema.properties[key]
7
+ if (typeof property === 'object') {
8
+ if (property.private === true) {
9
+ delete schema.properties[key]
10
+ } else {
11
+ stripPrivateFieldsFromSchema(property)
12
+ }
13
+ }
14
+ }
15
+ }
16
+ return schema
17
+ }