Skip to content

Commit 7023032

Browse files
committed
feat: more types + strip fix + extend fix
1 parent 7e3a3a9 commit 7023032

9 files changed

+234
-39
lines changed

README.md

+19-3
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,23 @@ user2.isValid() // false
3737

3838
### Static methods
3939

40-
- `schema(ModeEnum): Joi.Schema`
40+
- `schema(ModeEnum | EntityOptions): Joi.Schema`
4141
- Build and return the schema for a mode
4242

4343
### Instance methods
4444

45-
- `schema(ModeEnum): Joi.Schema`
45+
- `schema(ModeEnum | EntityOptions): Joi.Schema`
4646
- Build and return the schema for a mode
4747
- `isValid(ModeEnum): boolean`
4848
- Based on the schema, valid the instance
4949

50+
### EntityOptions
51+
52+
`strict?: boolean` - Default: `true` - Used in the constructor of the entity to throw an error if validation failed
53+
`mode?: ModeEnum` - Default: `ModeEnum.READ` - Schema mode
54+
`array?: boolean` - Default: `false` - Build the schema as a list of the Entity
55+
`unknown?: boolean` - Default: `true` - Allow unknown keys in the payload
56+
5057
### Decorators
5158

5259
#### Type
@@ -157,7 +164,14 @@ Works with types: `String | Array | Buffer | TypeEnum`
157164
@Description(string)
158165
```
159166

160-
Add description metadata to the property
167+
#### Regex
168+
169+
```javascript
170+
@Regex(pattern)
171+
```
172+
173+
Add regex validation for a string
174+
Works with type: `String`
161175

162176
### TypeEnum
163177

@@ -167,6 +181,8 @@ Add description metadata to the property
167181
- `Entity.Type.Base64`: String in base64 format
168182
- `Entity.Type.IsoDate`: String in iso date format
169183
- `Entity.Type.URI`: String URI format
184+
- `Entity.Type.Email`: String Email format
185+
- `Entity.Type.Timestamp`: Date timestamp format
170186

171187
### ModeEnum
172188

src/lib/decorators.ts

+10
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,16 @@ export function ObjectPattern(pattern: RegExp, schema: PropertyType): DecoratorF
156156
return insertRule({ key: decorators.KEY_OBJECT_PATTERN, value: { pattern, schema }});
157157
}
158158

159+
/**
160+
* Decorator @Regex()
161+
*
162+
* @param {RegExp} pattern
163+
* @returns DecoratorFunc
164+
*/
165+
export function Regex(pattern: RegExp): DecoratorFunc {
166+
return insertRule({ key: decorators.KEY_REGEX, value: pattern });
167+
}
168+
159169
/**
160170
* Insert a new rule of a property
161171
* in the metadata of the Entity

src/lib/entity.ts

+20-10
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ import { JoiTransformer } from '../transformers/joi.transformer';
66
export interface EntityOptions {
77
strict?: boolean;
88
mode?: ModeEnum;
9+
array?: boolean;
10+
unknown?: boolean;
911
}
1012

1113
export interface EntityTransformer<T> {
12-
build: (source: PropertyMetadata[], mode: ModeEnum, more?: T, parent?: BaseEntity) => T;
14+
build: (source: PropertyMetadata[], opts: EntityOptions, more?: T, parent?: BaseEntity) => T;
1315
isValid: (value: BaseEntity, schema: T) => boolean;
1416
validate: (value: BaseEntity, schema: T) => { value: any, error: Error };
1517
}
@@ -36,13 +38,18 @@ export class BaseEntity {
3638
* @param {ModeEnum=ModeEnum.READ} mode
3739
* @returns T
3840
*/
39-
static schema<T>(mode: ModeEnum = ModeEnum.READ): T {
41+
static schema<T>(opts?: ModeEnum | EntityOptions): T {
42+
if (Object.values(ModeEnum).includes(opts)) {
43+
opts = <EntityOptions>{ mode: opts };
44+
} else if (!opts) {
45+
opts = <EntityOptions>{ mode: ModeEnum.READ };
46+
}
4047
if (!(this.transformers && this.transformers.length > 0)) {
4148
return;
4249
}
4350
return this
4451
.transformers[0]
45-
.build(Reflect.getMetadata(KEY_PROPS, this), mode, this.more(), this.parent);
52+
.build(Reflect.getMetadata(KEY_PROPS, this), <EntityOptions>opts, this.more(), this.parent);
4653
}
4754

4855
/**
@@ -65,17 +72,17 @@ export class BaseEntity {
6572
constructor(payload = {}, options?: EntityOptions) {
6673
options = options || { strict: true, mode: ModeEnum.READ };
6774
payload = payload || {};
75+
options.unknown = false;
6876
const result = this
6977
.constructor
7078
['transformers'][0]
71-
.validate(payload, this.constructor['schema'](options.mode));
79+
.validate(payload, this.constructor['schema'](options));
7280
if (options.strict !== false && !!result.error) {
7381
throw result.error;
7482
}
7583
[]
76-
.concat(Reflect.getOwnMetadata(KEY_PROPS, this.constructor))
77-
.filter(_ => !!_)
78-
.forEach((_: PropertyMetadata) => Reflect.set(this, _.property, result.value[_.property] || undefined))
84+
.concat(Object.keys(result.value))
85+
.forEach((_: string) => Reflect.set(this, _, result.value[_] || undefined))
7986
}
8087

8188
/**
@@ -96,8 +103,8 @@ export class BaseEntity {
96103
*
97104
* @param {ModeEnum} mode
98105
*/
99-
schema<T>(mode?: ModeEnum): T {
100-
return this.constructor['schema'](mode);
106+
schema<T>(opts?: ModeEnum | EntityOptions): T {
107+
return this.constructor['schema'](opts);
101108
}
102109
}
103110

@@ -120,9 +127,12 @@ export const Entity = EntityTo(JoiTransformer);
120127

121128
export function EntityExtends(parent: any) {
122129
const _p = Reflect.construct(parent, [{}, { strict: false }]);
130+
if (!(_p instanceof BaseEntity)) {
131+
throw new Error('You need to extends another Entity');
132+
}
123133
return class extends BaseEntity {
124134
static transformers = [new JoiTransformer()];
125-
static parent = (_p instanceof BaseEntity) ? parent : undefined;
135+
static parent = parent;
126136
static more() {};
127137
}
128138
}

src/lib/enums.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,7 @@ export enum TypeEnum {
1212
IsoDate = <any>Symbol('type_isodate'),
1313
Integer = <any>Symbol('type_integer'),
1414
IP = <any>Symbol('type_ip'),
15-
URI = <any>Symbol('type_uri')
15+
URI = <any>Symbol('type_uri'),
16+
Email = <any>Symbol('type_email'),
17+
Timestamp = <any>Symbol('type_timestamp')
1618
}

src/lib/symbols.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,8 @@ export const decorators = {
3737
KEY_LENGTH : Symbol('key_length'),
3838

3939
// Key of @ObjectPattern()
40-
KEY_OBJECT_PATTERN : Symbol('key_object_pattern')
40+
KEY_OBJECT_PATTERN : Symbol('key_object_pattern'),
41+
42+
// Key of @Regex()
43+
KEY_REGEX : Symbol('key_regex')
4144
}

src/transformers/joi.transformer.ts

+48-19
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as Joi from 'joi';
22
import { PropertyMetadata, PropertyRule } from '../lib/decorators';
33
import { ModeEnum, TypeEnum } from '../lib/enums';
4-
import { BaseEntity, EntityRef, EntityTransformer } from '../lib/entity';
4+
import { BaseEntity, EntityRef, EntityTransformer, EntityOptions } from '../lib/entity';
55
import { decorators } from '../lib/symbols';
66

77
interface PropertySchema {
@@ -12,14 +12,21 @@ interface PropertySchema {
1212

1313
export { ObjectSchema as SchemaType } from 'joi';
1414

15-
export class JoiTransformer implements EntityTransformer<Joi.ObjectSchema> {
15+
export class JoiTransformer implements EntityTransformer<Joi.ObjectSchema | Joi.ArraySchema> {
1616

1717
private objectIdRegex = /^[0-9a-fA-F]{24}$/;
1818

19-
build(source: PropertyMetadata[], mode: ModeEnum, more: Joi.ObjectSchema = Joi.object(), parent?: BaseEntity): Joi.ObjectSchema {
20-
return !source ? undefined : more.concat(
21-
this.reduceSchema(source.map(_ => this.propertyHandler(_, mode)), !!parent ? parent.schema(mode) : undefined)
19+
build(source: PropertyMetadata[], opts: EntityOptions, more: Joi.ObjectSchema = Joi.object(),
20+
parent?: BaseEntity): Joi.ObjectSchema | Joi.ArraySchema {
21+
opts.unknown = opts.unknown === undefined || opts.unknown === null ? true : opts.unknown;
22+
const result = !source ? undefined : more.concat(
23+
this.reduceSchema(source.map(_ => this.propertyHandler(_, opts.mode, opts.unknown)),
24+
opts.unknown, !!parent ? parent.schema(opts) : undefined)
2225
);
26+
if (!!opts.array) {
27+
return Joi.array().items(result);
28+
}
29+
return result;
2330
}
2431

2532
isValid(data: BaseEntity, schema: Joi.ObjectSchema): boolean {
@@ -43,9 +50,9 @@ export class JoiTransformer implements EntityTransformer<Joi.ObjectSchema> {
4350
* @param {PropertySchema[]} source
4451
* @returns Joi.ObjectSchema
4552
*/
46-
private reduceSchema(source: PropertySchema[], parent?: Joi.ObjectSchema): Joi.ObjectSchema {
53+
private reduceSchema(source: PropertySchema[], unknown: boolean, parent?: Joi.ObjectSchema): Joi.ObjectSchema {
4754
const _s = !!parent ? parent : Joi.object();
48-
return _s.keys(
55+
const res = _s.keys(
4956
source
5057
.map(_ => ({
5158
property: _.property,
@@ -55,7 +62,8 @@ export class JoiTransformer implements EntityTransformer<Joi.ObjectSchema> {
5562
acc[cur.property] = cur.schema;
5663
return acc;
5764
}, {})
58-
).unknown();
65+
);
66+
return !!unknown ? res.unknown() : res;
5967
}
6068

6169
/**
@@ -67,19 +75,19 @@ export class JoiTransformer implements EntityTransformer<Joi.ObjectSchema> {
6775
* @param {ModeEnum} mode
6876
* @returns PropertySchema
6977
*/
70-
private propertyHandler(source: PropertyMetadata, mode: ModeEnum): PropertySchema {
78+
private propertyHandler(source: PropertyMetadata, mode: ModeEnum, unknown: boolean): PropertySchema {
7179
const base = source
7280
.rules
7381
.filter(_ => _.key === decorators.KEY_TYPE || _.key === decorators.KEY_ARRAY || _.key === decorators.KEY_OBJECT_PATTERN)
74-
.map(_ => this.ruleHandler(_, mode))
82+
.map(_ => this.ruleHandler(_, mode, unknown))
7583
.shift() || Joi.any();
7684
return {
7785
property: source.property,
7886
base,
7987
schemas: source
8088
.rules
8189
.filter(_ => (_.key !== decorators.KEY_TYPE && _.key !== decorators.KEY_ARRAY && _.key !== decorators.KEY_OBJECT_PATTERN))
82-
.map(_ => this.ruleHandler(_, mode, base))
90+
.map(_ => this.ruleHandler(_, mode, unknown, base))
8391
.filter(_ => !!_)
8492
}
8593
}
@@ -94,12 +102,12 @@ export class JoiTransformer implements EntityTransformer<Joi.ObjectSchema> {
94102
* @param {Joi.Schema} base
95103
* @returns Joi.Schema
96104
*/
97-
private ruleHandler(rule: PropertyRule, mode: ModeEnum, base?: Joi.Schema): Joi.Schema {
105+
private ruleHandler(rule: PropertyRule, mode: ModeEnum, unknown: boolean, base?: Joi.Schema): Joi.Schema {
98106
switch (rule.key) {
99107
case decorators.KEY_TYPE:
100-
return this.typeMapper(rule, mode);
108+
return this.typeMapper(rule, mode, unknown);
101109
case decorators.KEY_ARRAY:
102-
return Joi.array().items(this.typeMapper(rule, mode));
110+
return Joi.array().items(this.typeMapper(rule, mode, unknown));
103111
case decorators.KEY_REQUIRED:
104112
return this.requireMapper(rule, mode);
105113
case decorators.KEY_STRIP:
@@ -119,7 +127,9 @@ export class JoiTransformer implements EntityTransformer<Joi.ObjectSchema> {
119127
case decorators.KEY_LENGTH:
120128
return this.lengthMapper(rule, base);
121129
case decorators.KEY_OBJECT_PATTERN:
122-
return this.objectPatternMapper(rule);
130+
return this.objectPatternMapper(rule, unknown);
131+
case decorators.KEY_REGEX:
132+
return this.regexMapper(rule);
123133
/* istanbul ignore next */
124134
default:
125135
return Joi.any();
@@ -132,7 +142,7 @@ export class JoiTransformer implements EntityTransformer<Joi.ObjectSchema> {
132142
* @param {PropertyRule} rule
133143
* @returns Joi.Schema
134144
*/
135-
private typeMapper(rule: PropertyRule, mode: ModeEnum): Joi.Schema {
145+
private typeMapper(rule: PropertyRule, mode: ModeEnum, unknown: boolean): Joi.Schema {
136146
switch (rule.value) {
137147
case String:
138148
return Joi.string();
@@ -141,7 +151,7 @@ export class JoiTransformer implements EntityTransformer<Joi.ObjectSchema> {
141151
case Boolean:
142152
return Joi.boolean();
143153
case Object:
144-
return Joi.object().unknown();
154+
return unknown ? Joi.object().unknown() : Joi.object();
145155
case Buffer:
146156
return Joi.binary();
147157
case Date:
@@ -160,6 +170,10 @@ export class JoiTransformer implements EntityTransformer<Joi.ObjectSchema> {
160170
return Joi.string().ip();
161171
case TypeEnum.URI:
162172
return Joi.string().uri();
173+
case TypeEnum.Email:
174+
return Joi.string().email();
175+
case TypeEnum.Timestamp:
176+
return Joi.date().timestamp();
163177
/* istanbul ignore next */
164178
default:
165179
if (typeof rule.value === 'function' && new rule.value(null, { strict: false }) instanceof BaseEntity) {
@@ -317,13 +331,28 @@ export class JoiTransformer implements EntityTransformer<Joi.ObjectSchema> {
317331
* @param {PropertyRule} rule
318332
* @returns Joi.Schema
319333
*/
320-
private objectPatternMapper(rule: PropertyRule): Joi.Schema {
334+
private objectPatternMapper(rule: PropertyRule, unknown: boolean): Joi.Schema {
321335
if (!rule.value.schema) {
322336
throw new Error('Wrong schema provided');
323337
} else if (rule.value.schema instanceof BaseEntity) {
324338
return Joi.object().pattern(rule.value.pattern, rule.value.schema.schema());
325339
} else {
326-
return Joi.object().pattern(rule.value.pattern, this.typeMapper({ key: null, value: rule.value.schema }, ModeEnum.READ));
340+
return Joi.object().pattern(rule.value.pattern,
341+
this.typeMapper({ key: null, value: rule.value.schema }, ModeEnum.READ, unknown));
342+
}
343+
}
344+
345+
/**
346+
* Regex mapping
347+
*
348+
* @param {PropertyRule} rule
349+
* @returns Joi.Schema
350+
*/
351+
private regexMapper(rule: PropertyRule): Joi.Schema {
352+
if (!(rule.value instanceof RegExp)) {
353+
throw new Error('Wrong regex provided');
354+
} else {
355+
return Joi.string().regex(rule.value);
327356
}
328357
}
329358
}

test/unit/entity.test.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,13 @@ export class SuiteEntity {
6565
name: string;
6666
}
6767

68-
const instance = new MyTest();
68+
const instance = new MyTest(null, { strict: false });
6969

7070
unit
7171
.object(instance)
72-
.isInstanceOf(BaseEntity)
73-
.hasProperty('id', undefined)
74-
.hasProperty('name', undefined)
72+
.isInstanceOf(BaseEntity);
73+
unit.value(instance.id).is(undefined);
74+
unit.value(instance.name).is(undefined);
7575

7676
}
7777

0 commit comments

Comments
 (0)