@declaro/core 2.0.0-y.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{LICENSE → LICENSE.md} +1 -1
- package/README.md +203 -0
- package/dist/browser/index.js +28 -0
- package/dist/browser/index.js.map +133 -0
- package/dist/browser/scope/index.js +3 -0
- package/dist/browser/scope/index.js.map +9 -0
- package/dist/bun/index.js +19011 -0
- package/dist/bun/index.js.map +132 -0
- package/dist/bun/scope/index.js +4 -0
- package/dist/bun/scope/index.js.map +9 -0
- package/dist/node/index.cjs +19039 -0
- package/dist/node/index.cjs.map +132 -0
- package/dist/node/index.js +19010 -0
- package/dist/node/index.js.map +132 -0
- package/dist/node/scope/index.cjs +69 -0
- package/dist/node/scope/index.cjs.map +9 -0
- package/dist/node/scope/index.js +3 -0
- package/dist/node/scope/index.js.map +9 -0
- package/dist/ts/app/app-context.d.ts +9 -0
- package/dist/ts/app/app-context.d.ts.map +1 -0
- package/dist/ts/app/app-lifecycle.d.ts +6 -0
- package/dist/ts/app/app-lifecycle.d.ts.map +1 -0
- package/dist/ts/app/app.d.ts +24 -0
- package/dist/ts/app/app.d.ts.map +1 -0
- package/dist/{app → ts/app}/index.d.ts +1 -0
- package/dist/ts/app/index.d.ts.map +1 -0
- package/dist/ts/application/create-request-context.d.ts +4 -0
- package/dist/ts/application/create-request-context.d.ts.map +1 -0
- package/dist/ts/application/create-request-context.test.d.ts +2 -0
- package/dist/ts/application/create-request-context.test.d.ts.map +1 -0
- package/dist/ts/application/use-declaro.d.ts +3 -0
- package/dist/ts/application/use-declaro.d.ts.map +1 -0
- package/dist/{auth → ts/auth}/permission-validator.d.ts +1 -0
- package/dist/ts/auth/permission-validator.d.ts.map +1 -0
- package/dist/ts/auth/permission-validator.test.d.ts +2 -0
- package/dist/ts/auth/permission-validator.test.d.ts.map +1 -0
- package/dist/ts/context/async-context.d.ts +54 -0
- package/dist/ts/context/async-context.d.ts.map +1 -0
- package/dist/ts/context/async-context.test.d.ts +2 -0
- package/dist/ts/context/async-context.test.d.ts.map +1 -0
- package/dist/{context → ts/context}/context-consumer.d.ts +4 -0
- package/dist/ts/context/context-consumer.d.ts.map +1 -0
- package/dist/ts/context/context.circular-deps.test.d.ts +2 -0
- package/dist/ts/context/context.circular-deps.test.d.ts.map +1 -0
- package/dist/ts/context/context.d.ts +452 -0
- package/dist/ts/context/context.d.ts.map +1 -0
- package/dist/ts/context/context.test.d.ts +2 -0
- package/dist/ts/context/context.test.d.ts.map +1 -0
- package/dist/ts/context/legacy-context.test.d.ts +2 -0
- package/dist/ts/context/legacy-context.test.d.ts.map +1 -0
- package/dist/{context → ts/context}/validators.d.ts +2 -1
- package/dist/ts/context/validators.d.ts.map +1 -0
- package/dist/ts/dataflow/index.d.ts +2 -0
- package/dist/ts/dataflow/index.d.ts.map +1 -0
- package/dist/ts/dataflow/objects.d.ts +7 -0
- package/dist/ts/dataflow/objects.d.ts.map +1 -0
- package/dist/ts/dataflow/objects.test.d.ts +2 -0
- package/dist/ts/dataflow/objects.test.d.ts.map +1 -0
- package/dist/{errors → ts/errors}/errors.d.ts +16 -3
- package/dist/ts/errors/errors.d.ts.map +1 -0
- package/dist/ts/events/event-manager.d.ts +19 -0
- package/dist/ts/events/event-manager.d.ts.map +1 -0
- package/dist/ts/events/event-manager.spec.d.ts +2 -0
- package/dist/ts/events/event-manager.spec.d.ts.map +1 -0
- package/dist/ts/events/index.d.ts +2 -0
- package/dist/ts/events/index.d.ts.map +1 -0
- package/dist/ts/http/headers.d.ts +21 -0
- package/dist/ts/http/headers.d.ts.map +1 -0
- package/dist/ts/http/headers.spec.d.ts +2 -0
- package/dist/ts/http/headers.spec.d.ts.map +1 -0
- package/dist/ts/http/request-context.d.ts +17 -0
- package/dist/ts/http/request-context.d.ts.map +1 -0
- package/dist/ts/http/request-context.spec.d.ts +2 -0
- package/dist/ts/http/request-context.spec.d.ts.map +1 -0
- package/dist/ts/http/request.d.ts +31 -0
- package/dist/ts/http/request.d.ts.map +1 -0
- package/dist/ts/http/request.spec.d.ts +2 -0
- package/dist/ts/http/request.spec.d.ts.map +1 -0
- package/dist/{http → ts/http}/url.d.ts +5 -4
- package/dist/ts/http/url.d.ts.map +1 -0
- package/dist/ts/http/url.spec.d.ts +2 -0
- package/dist/ts/http/url.spec.d.ts.map +1 -0
- package/dist/ts/index.d.ts +47 -0
- package/dist/ts/index.d.ts.map +1 -0
- package/dist/{pipelines → ts/pipelines}/index.d.ts +1 -0
- package/dist/ts/pipelines/index.d.ts.map +1 -0
- package/dist/{pipelines → ts/pipelines}/pipeline-action.d.ts +1 -0
- package/dist/ts/pipelines/pipeline-action.d.ts.map +1 -0
- package/dist/ts/pipelines/pipeline-action.test.d.ts +2 -0
- package/dist/ts/pipelines/pipeline-action.test.d.ts.map +1 -0
- package/dist/{pipelines → ts/pipelines}/pipeline.d.ts +3 -2
- package/dist/ts/pipelines/pipeline.d.ts.map +1 -0
- package/dist/ts/pipelines/pipeline.test.d.ts +2 -0
- package/dist/ts/pipelines/pipeline.test.d.ts.map +1 -0
- package/dist/ts/schema/json-schema.d.ts +12 -0
- package/dist/ts/schema/json-schema.d.ts.map +1 -0
- package/dist/ts/schema/labels.d.ts +14 -0
- package/dist/ts/schema/labels.d.ts.map +1 -0
- package/dist/ts/schema/model-schema.d.ts +75 -0
- package/dist/ts/schema/model-schema.d.ts.map +1 -0
- package/dist/ts/schema/model-schema.test.d.ts +2 -0
- package/dist/ts/schema/model-schema.test.d.ts.map +1 -0
- package/dist/ts/schema/model.d.ts +35 -0
- package/dist/ts/schema/model.d.ts.map +1 -0
- package/dist/ts/schema/schema-mixin.d.ts +24 -0
- package/dist/ts/schema/schema-mixin.d.ts.map +1 -0
- package/dist/ts/schema/test/mock-model.d.ts +8 -0
- package/dist/ts/schema/test/mock-model.d.ts.map +1 -0
- package/dist/ts/scope/index.d.ts +34 -0
- package/dist/ts/scope/index.d.ts.map +1 -0
- package/dist/ts/shared/utils/action-descriptor.d.ts +28 -0
- package/dist/ts/shared/utils/action-descriptor.d.ts.map +1 -0
- package/dist/ts/shared/utils/action-descriptor.test.d.ts +2 -0
- package/dist/ts/shared/utils/action-descriptor.test.d.ts.map +1 -0
- package/dist/ts/shared/utils/schema-utils.d.ts +3 -0
- package/dist/ts/shared/utils/schema-utils.d.ts.map +1 -0
- package/dist/ts/shared/utils/schema-utils.test.d.ts +2 -0
- package/dist/ts/shared/utils/schema-utils.test.d.ts.map +1 -0
- package/dist/ts/shims/async-local-storage.d.ts +36 -0
- package/dist/ts/shims/async-local-storage.d.ts.map +1 -0
- package/dist/ts/shims/async-local-storage.test.d.ts +2 -0
- package/dist/ts/shims/async-local-storage.test.d.ts.map +1 -0
- package/dist/{timing.d.ts → ts/timing.d.ts} +1 -0
- package/dist/ts/timing.d.ts.map +1 -0
- package/dist/{typescript → ts/typescript}/arrays.d.ts +1 -0
- package/dist/ts/typescript/arrays.d.ts.map +1 -0
- package/dist/{typescript → ts/typescript}/baseModel.d.ts +1 -0
- package/dist/ts/typescript/baseModel.d.ts.map +1 -0
- package/dist/{typescript → ts/typescript}/classes.d.ts +1 -0
- package/dist/ts/typescript/classes.d.ts.map +1 -0
- package/dist/{typescript → ts/typescript}/constant-manipulation/snake-case.d.ts +1 -0
- package/dist/ts/typescript/constant-manipulation/snake-case.d.ts.map +1 -0
- package/dist/{typescript → ts/typescript}/errors.d.ts +1 -0
- package/dist/ts/typescript/errors.d.ts.map +1 -0
- package/dist/ts/typescript/fetch.d.ts +3 -0
- package/dist/ts/typescript/fetch.d.ts.map +1 -0
- package/dist/{typescript → ts/typescript}/generics.d.ts +1 -0
- package/dist/ts/typescript/generics.d.ts.map +1 -0
- package/dist/{typescript → ts/typescript}/index.d.ts +1 -0
- package/dist/ts/typescript/index.d.ts.map +1 -0
- package/dist/ts/typescript/objects.d.ts +26 -0
- package/dist/ts/typescript/objects.d.ts.map +1 -0
- package/dist/{typescript → ts/typescript}/promises.d.ts +1 -0
- package/dist/ts/typescript/promises.d.ts.map +1 -0
- package/dist/{validation → ts/validation}/index.d.ts +1 -0
- package/dist/ts/validation/index.d.ts.map +1 -0
- package/dist/{validation → ts/validation}/validation.d.ts +1 -0
- package/dist/ts/validation/validation.d.ts.map +1 -0
- package/dist/{validation → ts/validation}/validator.d.ts +1 -0
- package/dist/ts/validation/validator.d.ts.map +1 -0
- package/dist/ts/validation/validator.test.d.ts +2 -0
- package/dist/ts/validation/validator.test.d.ts.map +1 -0
- package/package.json +46 -13
- package/src/app/app-context.ts +4 -5
- package/src/app/app-lifecycle.ts +4 -3
- package/src/app/app.ts +7 -5
- package/src/application/create-request-context.test.ts +345 -0
- package/src/application/create-request-context.ts +19 -0
- package/src/application/use-declaro.ts +27 -0
- package/src/auth/permission-validator.test.ts +238 -2
- package/src/auth/permission-validator.ts +3 -3
- package/src/context/async-context.test.ts +348 -0
- package/src/context/async-context.ts +129 -0
- package/src/context/context-consumer.ts +4 -4
- package/src/context/context.circular-deps.test.ts +1047 -0
- package/src/context/context.test.ts +420 -3
- package/src/context/context.ts +590 -87
- package/src/context/legacy-context.test.ts +9 -9
- package/src/dataflow/objects.test.ts +7 -7
- package/src/dataflow/objects.ts +10 -9
- package/src/errors/errors.ts +19 -3
- package/src/events/event-manager.spec.ts +129 -0
- package/src/events/event-manager.ts +25 -14
- package/src/http/headers.ts +17 -2
- package/src/http/request-context.ts +24 -15
- package/src/http/request.ts +27 -6
- package/src/http/url.ts +3 -3
- package/src/index.ts +34 -3
- package/src/pipelines/pipeline.test.ts +11 -9
- package/src/schema/json-schema.ts +16 -0
- package/src/schema/labels.ts +23 -23
- package/src/schema/model-schema.test.ts +282 -0
- package/src/schema/model-schema.ts +197 -0
- package/src/schema/model.ts +143 -0
- package/src/schema/schema-mixin.ts +51 -0
- package/src/schema/test/mock-model.ts +19 -0
- package/src/scope/index.ts +33 -0
- package/src/shared/utils/action-descriptor.test.ts +182 -0
- package/src/shared/utils/action-descriptor.ts +102 -0
- package/src/shared/utils/schema-utils.test.ts +33 -0
- package/src/shared/utils/schema-utils.ts +17 -0
- package/src/shims/async-local-storage.test.ts +258 -0
- package/src/shims/async-local-storage.ts +82 -0
- package/src/typescript/objects.ts +32 -1
- package/src/validation/validator.test.ts +12 -20
- package/dist/app/app-context.d.ts +0 -8
- package/dist/app/app-lifecycle.d.ts +0 -4
- package/dist/app/app.d.ts +0 -22
- package/dist/auth/permission-validator.test.d.ts +0 -1
- package/dist/context/context.d.ts +0 -161
- package/dist/context/context.test.d.ts +0 -1
- package/dist/context/legacy-context.test.d.ts +0 -1
- package/dist/dataflow/index.d.ts +0 -1
- package/dist/dataflow/objects.d.ts +0 -5
- package/dist/dataflow/objects.test.d.ts +0 -1
- package/dist/events/event-manager.d.ts +0 -16
- package/dist/events/event-manager.spec.d.ts +0 -1
- package/dist/events/index.d.ts +0 -1
- package/dist/helpers/index.d.ts +0 -1
- package/dist/helpers/ucfirst.d.ts +0 -1
- package/dist/http/headers.d.ts +0 -4
- package/dist/http/headers.spec.d.ts +0 -1
- package/dist/http/request-context.d.ts +0 -12
- package/dist/http/request-context.spec.d.ts +0 -1
- package/dist/http/request.d.ts +0 -8
- package/dist/http/request.spec.d.ts +0 -1
- package/dist/http/url.spec.d.ts +0 -1
- package/dist/index.d.ts +0 -19
- package/dist/pipelines/pipeline-action.test.d.ts +0 -1
- package/dist/pipelines/pipeline.test.d.ts +0 -1
- package/dist/pkg.cjs +0 -30
- package/dist/pkg.mjs +0 -56612
- package/dist/schema/application.d.ts +0 -83
- package/dist/schema/application.test.d.ts +0 -1
- package/dist/schema/define-model.d.ts +0 -10
- package/dist/schema/define-model.test.d.ts +0 -1
- package/dist/schema/formats.d.ts +0 -10
- package/dist/schema/index.d.ts +0 -10
- package/dist/schema/labels.d.ts +0 -13
- package/dist/schema/labels.test.d.ts +0 -1
- package/dist/schema/module.d.ts +0 -7
- package/dist/schema/module.test.d.ts +0 -1
- package/dist/schema/properties.d.ts +0 -19
- package/dist/schema/response.d.ts +0 -31
- package/dist/schema/response.test.d.ts +0 -1
- package/dist/schema/supported-types.d.ts +0 -12
- package/dist/schema/supported-types.test.d.ts +0 -1
- package/dist/schema/transform-model.d.ts +0 -4
- package/dist/schema/transform-model.test.d.ts +0 -1
- package/dist/schema/types.d.ts +0 -95
- package/dist/schema/types.test.d.ts +0 -1
- package/dist/typescript/fetch.d.ts +0 -2
- package/dist/typescript/objects.d.ts +0 -12
- package/dist/validation/validator.test.d.ts +0 -1
- package/src/helpers/index.ts +0 -1
- package/src/helpers/ucfirst.ts +0 -3
- package/src/schema/application.test.ts +0 -286
- package/src/schema/application.ts +0 -150
- package/src/schema/define-model.test.ts +0 -81
- package/src/schema/define-model.ts +0 -50
- package/src/schema/formats.ts +0 -23
- package/src/schema/index.ts +0 -10
- package/src/schema/labels.test.ts +0 -60
- package/src/schema/module.test.ts +0 -39
- package/src/schema/module.ts +0 -6
- package/src/schema/properties.ts +0 -40
- package/src/schema/response.test.ts +0 -101
- package/src/schema/response.ts +0 -93
- package/src/schema/supported-types.test.ts +0 -20
- package/src/schema/supported-types.ts +0 -15
- package/src/schema/transform-model.test.ts +0 -31
- package/src/schema/transform-model.ts +0 -24
- package/src/schema/types.test.ts +0 -28
- package/src/schema/types.ts +0 -163
- package/tsconfig.json +0 -11
- 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
|
+
}
|