@travetto/schema
Advanced tools
Comparing version 0.1.8 to 0.2.0
@@ -7,11 +7,14 @@ { | ||
"dependencies": { | ||
"@travetto/config": "^0.1.4", | ||
"@travetto/registry": "^0.1.5", | ||
"@types/faker": "^4.1.2", | ||
"faker": "^4.1.0" | ||
"@travetto/config": "^0.2.0", | ||
"@travetto/registry": "^0.2.0" | ||
}, | ||
"description": "Data type registry for runtime validation, reflection and binding. ", | ||
"devDependencies": { | ||
"@travetto/test": "^0.1.6" | ||
"@travetto/test": "^0.2.0" | ||
}, | ||
"optionalExtensionDependencies": { | ||
"@travetto/express": "^0.1.11", | ||
"@types/faker": "^4.1.2", | ||
"faker": "^4.1.0" | ||
}, | ||
"homepage": "https://travetto.io", | ||
@@ -36,3 +39,3 @@ "keywords": [ | ||
}, | ||
"version": "0.1.8" | ||
"version": "0.2.0" | ||
} |
@@ -7,4 +7,12 @@ travetto: Schema | ||
## Registration | ||
The registry's schema information is defined by `typescript` AST and only applies to classes registered as `@Schema`s. The module utilizes AST transformations to collect schema information, and facilitate the registration process without user intervention. | ||
The registry's schema information is defined by `typescript` AST and only applies to classes registered with the `@Schema` decoration. | ||
### Classes | ||
The module utilizes AST transformations to collect schema information, and facilitate the registration process without user intervention. The class can also be described using providing a: | ||
* `title` - definition of the schema | ||
* `description` - detailed description of the schema | ||
* `examples` - A set of examples as JSON or YAML | ||
The `title` will be picked up from the [`JSDoc`](http://usejsdoc.org/about-getting-started.html) comments, and additionally all fields can be set using the [`@Describe`](./src/decorator/common) decorator. | ||
```typescript | ||
@@ -38,3 +46,4 @@ @Schema() | ||
This provides a powerful base for data binding and validation at runtime. Additionally there may be types that cannot be detected, or some information that the programmer would like to override. Below are the supported field decorators: | ||
### Fields | ||
This schema provides a powerful base for data binding and validation at runtime. Additionally there may be types that cannot be detected, or some information that the programmer would like to override. Below are the supported field decorators: | ||
@@ -59,2 +68,8 @@ * `@Field` defines a field that will be serialized, generally used in conjunction with ```@Schema(false)``` which disables the auto registration. | ||
Just like the class, all fields can be defined with | ||
* `description` - detailed description of the schema | ||
* `examples` - A set of examples as JSON or YAML | ||
And similarly, the `description` will be picked up from the [`JSDoc`](http://usejsdoc.org/about-getting-started.html) comments, and additionally all fields can be set using the [`@Describe`](./src/decorator/common) decorator. | ||
## Binding/Validation | ||
@@ -158,2 +173,40 @@ At runtime, once a schema is registered, a programmer can utilize this structure to perform specific operations. Specifically binding and validation. | ||
message: address.street2 is a required field | ||
``` | ||
``` | ||
## Extensions | ||
Integration with other modules can be supported by extensions. The dependencies are `optionalExtensionDependencies` and must be installed directly if you want to use them: | ||
### Express | ||
The module provides high level access for [`Express`](https://github.com/travetto/express) support, via decorators, for validating and typing request bodies. | ||
## Decorators | ||
`@SchemaBody` provides the ability to convert the inbound request body into a schema bound object, and provide validation before the controller even receives the request. | ||
```typescript | ||
class User { | ||
name: string; | ||
age: number; | ||
} | ||
... | ||
@Post('/saveUser') | ||
@SchemaBody(User) | ||
async save(req: TypedBody<User>) { | ||
const user = await this.service.update(req.body); | ||
return { success : true }; | ||
} | ||
... | ||
``` | ||
`@SchemaQuery` provides the ability to convert the inbound request query into a schema bound object, and provide validation before the controller even receives the request. | ||
```typescript | ||
class SearchParams { | ||
page: number = 0; | ||
pageSize: number = 100; | ||
} | ||
... | ||
@Get('/search') | ||
@SchemaQuery(SearchParams) | ||
async search(req: TypedQuery<SearchParams>) { | ||
return await this.service.search(req.query); | ||
} | ||
... | ||
``` | ||
@@ -1,2 +0,3 @@ | ||
import { CommonRegExp, SchemaRegistry, ClassList, ValidatorFn } from '../service'; | ||
import { CommonRegExp, SchemaRegistry } from '../service'; | ||
import { ClassList, FieldConfig } from '../types'; | ||
@@ -16,3 +17,3 @@ function prop(obj: { [key: string]: any }) { | ||
} | ||
export function Field(type: ClassList, config?: { [key: string]: any }) { | ||
export function Field(type: ClassList, config?: Partial<FieldConfig>) { | ||
return (f: any, p: string) => { | ||
@@ -26,3 +27,3 @@ SchemaRegistry.registerPendingFieldConfig(f.constructor, p, type); | ||
export const Alias = (...aliases: string[]) => prop({ aliases }); | ||
export const Required = (message?: string) => prop({ required: { message } }); | ||
export const Required = (message?: string) => prop({ required: { active: true, message } }); | ||
export const Enum = (vals: string[] | any, message?: string) => { | ||
@@ -29,0 +30,0 @@ const values = enumKeys(vals); |
export * from './field'; | ||
export * from './schema'; | ||
export * from './schema'; | ||
export * from './common'; |
import { Class } from '@travetto/registry'; | ||
import { SchemaRegistry, ValidatorFn } from '../service'; | ||
import { BindUtil } from '../util'; | ||
import { SchemaRegistry } from '../service'; | ||
import { ValidatorFn } from '../types'; | ||
@@ -5,0 +5,0 @@ export interface ClassWithSchema<T> extends Class<T> { |
@@ -6,1 +6,2 @@ /// <reference path="typings.d.ts" /> | ||
export * from './util'; | ||
export * from './types'; |
import { ChangeEvent, Class } from '@travetto/registry'; | ||
import { FieldConfig, DEFAULT_VIEW, ClassConfig } from './types'; | ||
import { FieldConfig, DEFAULT_VIEW, ClassConfig } from '../types'; | ||
import { EventEmitter } from 'events'; | ||
@@ -4,0 +4,0 @@ |
@@ -1,4 +0,3 @@ | ||
export * from './types'; | ||
export * from './validator'; | ||
export * from './registry'; | ||
export * from './changes'; |
import { MetadataRegistry, RootRegistry, Class, ChangeEvent } from '@travetto/registry'; | ||
import { Env } from '@travetto/base'; | ||
import { ClassList, FieldConfig, ClassConfig, ViewConfig, DEFAULT_VIEW } from './types'; | ||
import { ClassList, FieldConfig, ClassConfig, DEFAULT_VIEW } from '../types'; | ||
import { | ||
@@ -128,2 +128,3 @@ SchemaChangeListener, | ||
} | ||
dest.title = src.title || dest.title; | ||
dest.validators = [...src.validators, ...dest.validators]; | ||
@@ -130,0 +131,0 @@ return dest; |
import { Class } from '@travetto/registry'; | ||
import { BaseError } from '@travetto/base'; | ||
import { FieldConfig, SchemaConfig } from '../types'; | ||
import { FieldConfig, SchemaConfig } from '../../types'; | ||
import { SchemaRegistry } from '../registry'; | ||
@@ -29,4 +29,4 @@ import { Messages } from './messages'; | ||
if (!hasValue) { | ||
if (fieldSchema.required) { | ||
errors.push(...this.prepareErrors(path, [{ kind: 'required' }])); | ||
if (fieldSchema.required && fieldSchema.required.active) { | ||
errors.push(...this.prepareErrors(path, [{ kind: 'required', ...fieldSchema.required }])); | ||
} | ||
@@ -33,0 +33,0 @@ continue; |
import { Class } from '@travetto/registry'; | ||
import { SchemaRegistry, FieldConfig, DEFAULT_VIEW } from '../service'; | ||
import { SchemaRegistry } from '../service'; | ||
import { FieldConfig, DEFAULT_VIEW } from '../types'; | ||
@@ -4,0 +5,0 @@ export class BindUtil { |
@@ -1,3 +0,2 @@ | ||
import * as ts from 'typescript'; | ||
import { TransformUtil, State } from '@travetto/compiler'; | ||
import { TransformUtil, TransformerState } from '@travetto/compiler'; | ||
import { ConfigLoader } from '@travetto/config'; | ||
@@ -10,64 +9,12 @@ | ||
interface AutoState extends State { | ||
interface AutoState extends TransformerState { | ||
inAuto: boolean; | ||
addField: ts.Expression | undefined; | ||
addSchema: ts.Expression | undefined; | ||
} | ||
function resolveType(type: ts.Node, state: State): ts.Expression { | ||
let expr: ts.Expression | undefined; | ||
const kind = type && type!.kind; | ||
function computeProperty(state: AutoState, node: ts.PropertyDeclaration) { | ||
switch (kind) { | ||
case ts.SyntaxKind.TypeReference: | ||
expr = TransformUtil.importIfExternal(type as ts.TypeReferenceNode, state); | ||
break; | ||
case ts.SyntaxKind.LiteralType: expr = resolveType((type as any as ts.LiteralTypeNode).literal, state); break; | ||
case ts.SyntaxKind.StringLiteral: | ||
case ts.SyntaxKind.StringKeyword: expr = ts.createIdentifier('String'); break; | ||
case ts.SyntaxKind.NumericLiteral: | ||
case ts.SyntaxKind.NumberKeyword: expr = ts.createIdentifier('Number'); break; | ||
case ts.SyntaxKind.TrueKeyword: | ||
case ts.SyntaxKind.FalseKeyword: | ||
case ts.SyntaxKind.BooleanKeyword: expr = ts.createIdentifier('Boolean'); break; | ||
case ts.SyntaxKind.ArrayType: | ||
expr = ts.createArrayLiteral([resolveType((type as ts.ArrayTypeNode).elementType, state)]); | ||
break; | ||
case ts.SyntaxKind.TypeLiteral: | ||
const properties: ts.PropertyAssignment[] = []; | ||
for (const member of (type as ts.TypeLiteralNode).members) { | ||
let subMember: ts.TypeNode = (member as any).type; | ||
if ((subMember as any).literal) { | ||
subMember = (subMember as any).literal; | ||
} | ||
properties.push(ts.createPropertyAssignment(member.name as ts.Identifier, resolveType(subMember, state))); | ||
} | ||
expr = ts.createObjectLiteral(properties); | ||
break; | ||
case ts.SyntaxKind.UnionType: { | ||
const types = (type as ts.UnionTypeNode).types; | ||
expr = types.slice(1).reduce((fType, stype) => { | ||
const fTypeStr = (fType as any).text; | ||
if (fTypeStr !== 'Object') { | ||
const resolved = resolveType(stype, state); | ||
if ((resolved as any).text !== fTypeStr) { | ||
fType = ts.createIdentifier('Object'); | ||
} | ||
} | ||
return fType; | ||
}, resolveType(types[0], state)); | ||
break; | ||
} | ||
case ts.SyntaxKind.ObjectKeyword: | ||
default: | ||
break; | ||
} | ||
return expr || ts.createIdentifier('Object'); | ||
} | ||
function computeProperty(node: ts.PropertyDeclaration, state: AutoState) { | ||
const typeExpr = resolveType(node.type!, state); | ||
const typeExpr = TransformUtil.resolveType(state, node.type!); | ||
const properties = []; | ||
if (!node.questionToken) { | ||
properties.push(ts.createPropertyAssignment('required', TransformUtil.fromLiteral({}))); | ||
properties.push(ts.createPropertyAssignment('required', TransformUtil.fromLiteral({ active: true }))); | ||
} | ||
@@ -93,17 +40,14 @@ | ||
if (!state.addField) { | ||
const ident = TransformUtil.generateUniqueId(`import_Field`, state); | ||
state.addField = ts.createPropertyAccess(ident, 'Field'); | ||
state.newImports.push({ | ||
path: require.resolve('../src/decorator/field'), | ||
ident | ||
}); | ||
const dec = TransformUtil.createDecorator(state, require.resolve('../src/decorator/field'), 'Field', ...params); | ||
const newDecs = [dec, ...(node.decorators || [])]; | ||
const comments = TransformUtil.describeByComments(state, node); | ||
if (comments.description) { | ||
newDecs.push(TransformUtil.createDecorator(state, require.resolve('../src/decorator/common'), 'Describe', TransformUtil.fromLiteral({ | ||
description: comments.description | ||
}))); | ||
} | ||
const dec = ts.createDecorator(ts.createCall(state.addField as any, undefined, ts.createNodeArray(params))); | ||
const decls = ts.createNodeArray([ | ||
dec, ...(node.decorators || []) | ||
]); | ||
const res = ts.updateProperty(node, | ||
decls, | ||
ts.createNodeArray(newDecs), | ||
node.modifiers, | ||
@@ -122,7 +66,7 @@ node.name, | ||
if (ts.isClassDeclaration(node)) { | ||
const anySchema = TransformUtil.findAnyDecorator(node, SCHEMAS, state); | ||
const anySchema = TransformUtil.findAnyDecorator(state, node, SCHEMAS); | ||
const schema = TransformUtil.findAnyDecorator(node, { | ||
const schema = TransformUtil.findAnyDecorator(state, node, { | ||
Schema: new Set(['@travetto/schema']) | ||
}, state); | ||
}); | ||
@@ -150,17 +94,14 @@ let auto = !!anySchema; | ||
const ret = node as any as ts.ClassDeclaration; | ||
let decls = node.decorators; | ||
const decls = [...(node.decorators || [])]; | ||
const comments = TransformUtil.describeByComments(state, node); | ||
if (!schema) { | ||
if (!state.addSchema) { | ||
const ident = TransformUtil.generateUniqueId(`import_Schema`, state); | ||
state.newImports.push({ | ||
path: require.resolve('../src/decorator/schema'), | ||
ident | ||
}); | ||
state.addSchema = ts.createPropertyAccess(ident, 'Schema'); | ||
} | ||
decls.unshift(TransformUtil.createDecorator(state, require.resolve('../src/decorator/schema'), 'Schema')); | ||
} | ||
decls = ts.createNodeArray([ | ||
ts.createDecorator(ts.createCall(state.addSchema, undefined, ts.createNodeArray([]))), | ||
...(decls || []) | ||
]); | ||
if (comments.description) { | ||
decls.push(TransformUtil.createDecorator(state, require.resolve('../src/decorator/common'), 'Describe', TransformUtil.fromLiteral({ | ||
title: comments.description | ||
}))); | ||
} | ||
@@ -170,3 +111,3 @@ | ||
ret, | ||
decls, | ||
ts.createNodeArray(decls), | ||
ret.modifiers, | ||
@@ -194,5 +135,5 @@ ret.name, | ||
if (state.inAuto) { | ||
const ignore = TransformUtil.findAnyDecorator(node, { Ignore: new Set(['@travetto/schema']) }, state); | ||
const ignore = TransformUtil.findAnyDecorator(state, node, { Ignore: new Set(['@travetto/schema']) }); | ||
if (!ignore) { | ||
return computeProperty(node, state) as any as T; | ||
return computeProperty(state, node) as any as T; | ||
} | ||
@@ -208,6 +149,5 @@ } | ||
transformer: TransformUtil.importingVisitor<AutoState>(() => ({ | ||
inAuto: false, | ||
addField: undefined | ||
inAuto: false | ||
}), visitNode), | ||
phase: 'before' | ||
}; |
@@ -1,2 +0,2 @@ | ||
import { Schema, Field, View, Required } from '../index'; | ||
import { Schema, Field, Required } from '../index'; | ||
@@ -7,3 +7,2 @@ @Schema() | ||
@Field(String) | ||
@View('test') | ||
@Required() | ||
@@ -10,0 +9,0 @@ street1: string; |
@@ -19,3 +19,2 @@ import { | ||
@Field(String) | ||
@View('test') | ||
area: string; | ||
@@ -105,6 +104,6 @@ | ||
assert(viewPerson.address.street1 === '1234 Fun'); | ||
assert(viewPerson.address.street2 === undefined); | ||
assert(viewPerson.address.street2 === 'Unit 20'); | ||
assert(viewPerson.counts.length === 2); | ||
assert(viewPerson.counts[0] instanceof Count); | ||
assert(viewPerson.counts[0].value === undefined); | ||
assert(viewPerson.counts[0].value === 20); | ||
} | ||
@@ -111,0 +110,0 @@ |
import { Suite, Test, BeforeAll } from '@travetto/test'; | ||
import { Schema, SchemaRegistry } from '../src'; | ||
import { GenerateUtil } from '../support/util.generate'; | ||
import { GenerateUtil } from '../extension/faker'; | ||
@@ -6,0 +6,0 @@ import * as assert from 'assert'; |
@@ -10,3 +10,3 @@ import { Suite, Test } from '@travetto/test'; | ||
telephone() { | ||
assert(CommonRegExp.telephone.test('555-555-5555')); | ||
assert(CommonRegExp.telephone.test('555-555-5545')); | ||
assert(CommonRegExp.telephone.test('5555555555')); | ||
@@ -13,0 +13,0 @@ |
{ | ||
"compilerOptions": { | ||
"module": "commonjs", | ||
"target": "es2017", | ||
"target": "es2018", | ||
"strict": true, | ||
@@ -6,0 +6,0 @@ "noStrictGenericChecks": true, |
57410
2
32
1572
209
+ Added@travetto/base@0.2.11(transitive)
+ Added@travetto/compiler@0.2.10(transitive)
+ Added@travetto/config@0.2.10(transitive)
+ Added@travetto/registry@0.2.10(transitive)
+ Addedcommander@2.20.3(transitive)
+ Addedtypescript@3.9.10(transitive)
- Removed@types/faker@^4.1.2
- Removedfaker@^4.1.0
- Removed@travetto/base@0.1.4(transitive)
- Removed@travetto/compiler@0.1.5(transitive)
- Removed@travetto/config@0.1.4(transitive)
- Removed@travetto/registry@0.1.5(transitive)
- Removed@types/faker@4.1.12(transitive)
- Removedfaker@4.1.0(transitive)
- Removedtypescript@2.9.2(transitive)
Updated@travetto/config@^0.2.0
Updated@travetto/registry@^0.2.0