@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
|
@@ -94,14 +94,16 @@ describe('Pipelines', () => {
|
|
|
94
94
|
number,
|
|
95
95
|
}),
|
|
96
96
|
)
|
|
97
|
-
const message = vi.fn((meta
|
|
98
|
-
meta
|
|
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
|
|
102
|
+
: meta?.negative
|
|
101
103
|
? `${meta.number} is negative`
|
|
102
|
-
: meta
|
|
103
|
-
? `${meta
|
|
104
|
-
: `${meta
|
|
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
|
|
170
|
+
: meta?.negative
|
|
169
171
|
? `${meta.number} is negative`
|
|
170
|
-
: meta
|
|
172
|
+
: meta?.zero
|
|
171
173
|
? `${meta.number} is zero`
|
|
172
|
-
: `${meta
|
|
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
|
+
}
|
package/src/schema/labels.ts
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
import { capitalCase, camelCase,
|
|
1
|
+
import { capitalCase, camelCase, kebabCase, pascalCase, sentenceCase } from 'change-case'
|
|
2
2
|
import pluralize from 'pluralize'
|
|
3
3
|
|
|
4
|
-
export type
|
|
5
|
-
singularLabel
|
|
6
|
-
pluralLabel
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
17
|
+
export function getLabels(modelName: string, labels: Partial<ModelLabels> = {}): ModelLabels {
|
|
18
18
|
return {
|
|
19
|
-
singularLabel: labels.singularLabel ??
|
|
20
|
-
pluralLabel: labels.pluralLabel ??
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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>
|