From c3e7359f304cbd7749a4a3b475229a74a734caa3 Mon Sep 17 00:00:00 2001 From: "Marc J. Schmidt" Date: Fri, 20 Dec 2019 22:53:37 +0100 Subject: [PATCH] introduce new mongo db methods: fieldsOne() and fields(). increase code coverage. Removed @Field, @UUIDField, @MongoField, in docblocks and readme. --- README.md | 170 ++--- packages/core/package.json | 2 +- packages/core/src/decorators.ts | 608 +++++++++++------- packages/core/src/validation.ts | 71 +- packages/core/tests/big-entity.ts | 2 +- packages/core/tests/decorator.spec.ts | 29 +- .../tests/document-scenario/DocumentClass.ts | 4 +- packages/core/tests/entities.ts | 6 +- packages/core/tests/method-decorators.spec.ts | 46 +- packages/core/tests/validation.spec.ts | 14 +- packages/core/tests/validation2.spec.ts | 12 +- packages/mongo/package.json | 2 +- packages/mongo/src/database.ts | 50 +- packages/mongo/src/mapping.ts | 23 +- packages/mongo/tests/mongo.spec.ts | 31 +- packages/mongo/tests/to-plain.spec.ts | 4 +- 16 files changed, 615 insertions(+), 459 deletions(-) diff --git a/README.md b/README.md index 32a0dd97a..91c9a6f0e 100644 --- a/README.md +++ b/README.md @@ -62,17 +62,14 @@ Install `buffer` as well if you want to have Binary support. ```typescript import { - Field, - UUIDField, - EnumField, + f, plainToClass, uuid, } from '@marcj/marshal'; import {Buffer} from 'buffer'; class SubModel { - @Field() - label: string; + @f label: string; } export enum Plan { @@ -82,32 +79,32 @@ export enum Plan { } class SimpleModel { - @UUIDField().asId() + @f.primary().uuid() id: string = uuid(); - @Field(String).asArray() + @f.array(String) tags: string[] = []; - @Field(Buffer).optional() //binary + @f.type(Buffer).optional() //binary picture?: Buffer; - @Field() + @f type: number = 0; - @EnumField(Plan) + @f.enum(Plan) plan: Plan = Plan.DEFAULT; - @Field() + @f created: Date = new Date; - @Field(SubModel).asArray() + @f.array(SubModel) children: SubModel[] = []; - @Field(SubModel).asMap() + @f.map(SubModel) childrenMap: {[key: string]: SubModel} = {}; constructor( - @Field().index().asName('name') //asName is required for minimized code + @f.index().asName('name') //asName is required for minimized code public name: string ) {} } @@ -182,13 +179,14 @@ You can validate incoming object literals or an class instance. First make sure you have some validators attached to your fields you want to validate. ```typescript -import {Field, validate, ValidationError, validatedPlainToClass, plainToClass} from '@marcj/marshal'; +import 'jest'; +import {f, validate, ValidationError, validatedPlainToClass, plainToClass} from '@marcj/marshal'; class Page { - @Field() + @f name: string; - @Field() + @f age: number; } @@ -208,10 +206,10 @@ const page = validatedPlainToClass(Page, {name: 'peter'}); You can also custom validators ```typescript -import {Field, AddValidator, PropertyValidator, PropertyValidatorError, ClassType} from '@marcj/marshal'; +import {f, PropertyValidatorError, PropertyValidator} from '@marcj/marshal'; class MyCustomValidator implements PropertyValidator { - validate(value: any, target: ClassType, propertyName: string): PropertyValidatorError { + validate(value: any): PropertyValidatorError { if (value.length > 10) { return new PropertyValidatorError('too_long', 'Too long :()'); } @@ -219,8 +217,7 @@ class MyCustomValidator implements PropertyValidator { } class Entity { - @Field() - @AddValidator(MyCustomValidator) + @f.validator(MyCustomValidator) name: string; } ``` @@ -228,11 +225,10 @@ class Entity { or inline validators ```typescript -import {Field, AddValidator, InlineValidator, ClassType, PropertyValidatorError} from '@marcj/marshal'; +import {f, PropertyValidatorError} from '@marcj/marshal'; class Entity { - @Field() - @InlineValidator((value: any) => { + @f.validator((value: any) => { if (value.length > 10) { return new PropertyValidatorError('too_long', 'Too long :()'); } @@ -256,97 +252,54 @@ you might have the need to serialize only one field. ## Types / Decorators -Class fields are annotated using mainly [@Field decorators](https://marshal.marcj.dev/modules/_marcj_marshal.html#field). +Class fields are annotated using mainly [@f](https://marshal.marcj.dev/modules/_marcj_marshal.html#f). You can define primitives, class mappings, relations between parents, and indices for the database (currently MongoDB). -Most of the time @Field is able to detect the primitive type by reading the emitted meta data from TypeScript when you declared -the type correctly in Typescript. +Most of the time @f is able to detect the primitive type by reading the emitted meta data from TypeScript when you declared +the type correctly in Typescript. However, `@f` provides additional chainable methods to let you further define the type. +This duplication in defining the type is necessary since Typescript's reflection ability is only very rudimentary. Example valid decoration: ```typescript +import {f} from '@marcj/marshal'; + class Page { - @Field() //will be detected as String + @f.optional() //will be detected as String name?: string; - @Field(String).asArray() //will be detected as String array + @f.array(String) //will be detected as String array name: string[] = []; - @Field(String).asMap() //will be detected as String map + @f.map(String) //will be detected as String map name: {[name: string]: string} = {}; } ```` Example *not* valid decorators: ```typescript +import {f} from '@marcj/marshal'; + class Page { - @Field() //can't be detected, you get an error with further instructions + @f //can't be detected, you get an error with further instructions name; } ```` -See [documentation of @marcj/marshal](https://marshal.marcj.dev/modules/_marcj_marshal.html) for all available decorators. (Search for "Category: Decorator") -Available type decorators: +More examples: ```typescript -@Field() -@EnumField(MyEnumClass) //implicitly calls @Field() -@UUIDField() //implicitly calls @Field() -@MongoIdField() //for mongodb only, implicitly calls @Field() -``` - -which have all a common chainable interface: - -```typescript - -interface FieldDecoratorResult { - (target: Object, property?: string, parameterIndexOrDescriptor?: any): void; - - /** - * Sets the name of this property. Important for cases where the actual name is lost during compilation. - * @param name - */ - asName(name: string): FieldDecoratorResult; - - /** - * @see Optional - */ - optional(): FieldDecoratorResult; - - /** - * @see IDField - */ - asId(): FieldDecoratorResult; - - /** - * @see Index - */ - index(options?: IndexOptions): FieldDecoratorResult; - - /** - * @see FieldArray - */ - asArray(): FieldDecoratorResult; - - /** - * @see FieldMap - */ - asMap(): FieldDecoratorResult; -} -``` +import {f, uuid} from '@marcj/marshal'; -Example: - -```typescript class MyModel { - @UUIDField().asId() + @f.primary().uuid() id: string = uuid(); - @Field().optional().index() + @f.optional().index() name?: string; - @Field(String).optional().asArray() + @f.array(String).optional() tags?: string[]; } ``` @@ -384,39 +337,43 @@ which makes it possible to sync the schema defined only with Marshal decorators direction. Per default it excludes to export to `*toPlain` and `*toMongo`. You can also use `'mongo'` or `'plain'` to have more control. -Note: Fields that are not decorated with `@Field`, `@MongoIdField`, `@EnumField` or `@UUIDField` are not mapped and will be excluded per default +Note: Fields that are not decorated with `@f` are not mapped and will be excluded per default. ```typescript +import {f} from '@marcj/marshal'; + class MyEntity { - @MongoIdField().asId() + @f.primary().mongoId() id: string; - @Field().exclude() + @f.exclude() internalState: string; - @Field().exclude('mongo') + @f.exclude('mongo') publicState: string; } ``` ### ParentReference -`@ParentReference` together with `@Field` is used for all `*ToClass` functions -and allows you to have the parent from instance of class given in `@Field` assigned +`@ParentReference` together with `@f` is used for all `*ToClass` functions +and allows you to have the parent from instance of class given in `@f` assigned as reference. Properties that used `@ParentReference` are automatically excluded in `*ToPlain` and `*ToMongo` functions. ```typescript +import {f, ParentReference, plainToClass} from '@marcj/marshal'; + class Page { - @Field() + @f name: string; - @Field([Page]) + @f.array(Page) children: Page[] = []; - @Field(Page) + @f.type(Page) @ParentReference() - parent?: PageClass; + parent?: Page; } const root = plainToClass(Page, { @@ -441,28 +398,29 @@ of objects has been created, which means when all parents and siblings are fully initialised. ```typescript +import {f, OnLoad} from '@marcj/marshal'; class Page { - @Field() + @f name: string; @OnLoad() onLoad() { - console.log('initialised') + console.log('initialised'); } } ```` ### Value decorator -`@Decorated` lets you transform the actual class into something +`decorated()` lets you transform the actual class into something different. This is useful if you have in the actual class instance (plainToClass or mongoToClass) a wrapper for a certain property, for example `string[]` => `ChildrenCollection`. ```typescript +import {f} from '@marcj/marshal'; class ChildrenCollection { - @Decorated() - @Field(String).asArray() + @f.array(String).decorated() items: string[]; constructor(items: string[]) { @@ -475,11 +433,11 @@ class ChildrenCollection { } class MyEntity { - @MongoIdField().index() + @f.primary().mongoId() id: string; //in *toMongo and *toPlain is children the value of ChildrenCollection::items - @Field(ChildrenCollection) + @f.type(ChildrenCollection) children: ChildrenCollection = new ChildrenCollection([]); } ``` @@ -489,6 +447,8 @@ constructor of ChildrenCollection receives the actual value as first argument. ```typescript +import {classToPlain} from '@marcj/marshal'; + const entity = new MyEntity(); entity.children.add('Foo'); entity.children.add('Bar'); @@ -506,6 +466,8 @@ plainToClass) your decorator will be used again and receives as first argument the actual property value: ```typescript +import {classToPlain} from '@marcj/marshal'; + const entity = plainToClass(MyEntity, { id: 'abcde', children: ['Foo', 'Bar'] @@ -527,6 +489,8 @@ to retrieve type information, so you can use this also in combination with JSON- #### partialPlainToClass ```typescript +import {partialPlainToClass} from '@marcj/marshal'; + const converted = partialPlainToClass(SimpleModel, { id: 'abcde', ['childrenMap.item.label']: 3 @@ -544,9 +508,11 @@ expect(i2['children'][0].label).toBe('3'); #### partialClassToPlain / partialClassToMongo toPlain and toMongo differ in the way, that latter will transform -@ObjectID and @UUID in different way, suitable for Mongo's binary storage. +`mongoId`) and `uuid()` in different way, suitable for Mongo's binary storage. ```typescript +import {partialClassToPlain} from '@marcj/marshal'; + const plain = partialClassToPlain(SimpleModel, { 'children.0': i.children[0], 'stringChildrenCollection': new StringCollectionWrapper(['Foo', 'Bar']), diff --git a/packages/core/package.json b/packages/core/package.json index 1299c2d66..d344f400c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -18,7 +18,7 @@ "buffer": "^5.2.1" }, "dependencies": { - "@marcj/estdlib": "^0.1.11", + "@marcj/estdlib": "^0.1.15", "clone": "^2.1.2", "get-parameter-names": "^0.3.0", "uuid": "^3.2.1" diff --git a/packages/core/src/decorators.ts b/packages/core/src/decorators.ts index a8eb1cf78..ceea23305 100644 --- a/packages/core/src/decorators.ts +++ b/packages/core/src/decorators.ts @@ -8,7 +8,7 @@ import { StringValidator, UUIDValidator } from "./validation"; -import {ClassType, eachKey, eachPair, getClassName, isArray, isNumber, isObject, isPlainObject} from '@marcj/estdlib'; +import {ClassType, eachKey, eachPair, getClassName, isClass, isNumber, isObject, isPlainObject} from '@marcj/estdlib'; import {Buffer} from "buffer"; import * as getParameterNames from "get-parameter-names"; @@ -21,6 +21,10 @@ export interface PropertyValidator { validate(value: any, target: ClassType, propertyName: string, propertySchema: PropertySchema): PropertyValidatorError | void; } +export function isPropertyValidator(object: any): object is ClassType { + return isClass(object); +} + type IndexOptions = Partial<{ unique: boolean, spatial: boolean, @@ -73,6 +77,49 @@ export class PropertySchema { this.name = name; } + static getTypeFromJSType(type: any) { + if (type === String) { + return 'string'; + } + if (type === Number) { + return 'number'; + } + if (type === Date) { + return 'date'; + } + if (type === Buffer) { + return 'binary'; + } + if (type === Boolean) { + return 'boolean'; + } + + return 'any'; + } + + setFromJSType(type: any) { + this.type = PropertySchema.getTypeFromJSType(type); + + const isCustomObject = type !== String + && type !== String + && type !== Number + && type !== Date + && type !== Buffer + && type !== Boolean + && type !== Any + && type !== Object; + + if (isCustomObject) { + this.type = 'class'; + this.classType = type as ClassType; + + if (type instanceof ForwardedRef) { + this.classTypeForwardRef = type; + delete this.classType; + } + } + } + clone(): PropertySchema { const s = new PropertySchema(this.name); for (const i of eachKey(this)) { @@ -171,7 +218,7 @@ export class ClassSchema { * Each method can have its own PropertySchema definition for each argument, where map key = method name. */ methodProperties = new Map(); - normalizedMethodProperties: {[name: string]: true} = {}; + normalizedMethodProperties: { [name: string]: true } = {}; classProperties: { [name: string]: PropertySchema } = {}; @@ -239,12 +286,16 @@ export class ClassSchema { public getMethodProperties(name: string): PropertySchema[] { const properties = this.getOrCreateMethodProperties(name); if (!this.normalizedMethodProperties[name]) { - for (const [i, p] of eachPair(properties)) { - if (!p) { + const returnTypes = Reflect.getMetadata('design:paramtypes', this.proto, name); + if (!returnTypes) { + throw new Error(`Method ${name} has no decorated used, so reflection does not work.`); + } + + for (const [i, t] of eachPair(returnTypes)) { + if (!properties[i]) { properties[i] = new PropertySchema(String(i)); - const returnTypes = Reflect.getMetadata('design:paramtypes', this.proto, name); - if (returnTypes) { - properties[i].type = returnTypes + if (properties[i].type === 'any' && returnTypes[i] !== Object) { + properties[i].setFromJSType(t) } } } @@ -399,7 +450,7 @@ export function getClassTypeFromInstance(target: T): ClassType { } /** - * Returns true if given class has an @Entity() or @Field()s defined, and thus became + * Returns true if given class has an @Entity() or @f defined, and thus became * a Marshal entity. */ export function isRegisteredEntity(classType: ClassType): boolean { @@ -446,65 +497,176 @@ export interface FieldDecoratorResult { /** * Sets the name of this property. Important for cases where the actual name is lost during compilation. - * @param name */ asName(name: string): FieldDecoratorResult; /** - * @see Optional + * Marks this field as optional. The validation requires field values per default, this makes it optional. */ optional(): FieldDecoratorResult; /** * Used to define a field as excluded when serialized from class to different targets (currently to Mongo or JSON). - * - * @see Exclude + * PlainToClass or mongoToClass is not effected by this. */ exclude(t?: 'all' | 'mongo' | 'plain'): FieldDecoratorResult; /** - * @see IDField + * Marks this field as an ID aka primary. + * This is important if you interact with the database abstraction. + * + * Only one field in a class can be the ID. */ - asId(): FieldDecoratorResult; + primary(): FieldDecoratorResult; - id(): FieldDecoratorResult; + /** + * @see primary + * @deprecated + */ + asId(): FieldDecoratorResult; /** - * @see Index + * Used to define an index on a field. */ index(options?: IndexOptions, name?: string): FieldDecoratorResult; /** - * Mongo's ObjectID. - * @see MongoIdField + * Used to define a field as MongoDB ObjectId. This decorator is necessary if you want to use Mongo's _id. + * + * ```typescript + * class Page { + * @f.mongoId() + * referenceToSomething?: string; + * + * constructor( + * @f.id().mongoId() + * public readonly _id: string + * ) { + * + * } + * } + * ``` */ mongoId(): FieldDecoratorResult; /** - * @see UUIDField + * Used to define a field as UUID (v4). */ uuid(): FieldDecoratorResult; /** - * @see Decorated + * Used to define a field as decorated. + * This is necessary if you want to wrap a field value in the class instance using + * a own class, like for example for Array manipulations, but keep the JSON and Database value + * as primitive as possible. + * + * Only one field per class can be the decorated one. + * + * @category Decorator + * + * Example + * ```typescript + * export class PageCollection { + * @f.forward(() => PageClass).decorated() + * private readonly pages: PageClass[] = []; + * + * constructor(pages: PageClass[] = []) { + * this.pages = pages; + * } + * + * public count(): number { + * return this.pages.length; + * } + * + * public add(name: string): number { + * return this.pages.push(new PageClass(name)); + * } + * } + * + * export class PageClass { + * @f.uuid() + * id: string = uuid(); + * + * @f + * name: string; + * + * @f.forward(() => PageCollection) + * children: PageCollection = new PageCollection; + * + * constructor(name: string) { + * this.name = name; + * } + * } + * ``` + * + * If you use classToPlain(PageClass, ...) or classToMongo(PageClass, ...) the field value of `children` will be the type of + * `PageCollection.pages` (always the field where @Decorated() is applied to), here a array of PagesClass `PageClass[]`. */ decorated(): FieldDecoratorResult; /** - * @see FieldArray + * Marks a field as array. You should prefer `@f.array(T)` syntax. + * + * ```typescript + * class User { + * @f.type(String).asArray() + * tags: strings[] = []; + * } + * ``` */ asArray(): FieldDecoratorResult; /** - * @see FieldMap + * Marks a field as map. You should prefer `@f.map(T)` syntax. + * + * ```typescript + * class User { + * @f.type(String).asMap() + * tags: {[name: string]: string} = []; + * } + * ``` */ asMap(): FieldDecoratorResult; /** * Uses an additional decorator. - * @see FieldMap */ use(decorator: (target: Object, propertyOrMethodName?: string, parameterIndexOrDescriptor?: any) => void): FieldDecoratorResult; + + /** + * Adds a custom validator class or validator callback. + * + * @example + * ```typescript + * import {PropertyValidator, PropertyValidatorError} from '@marcj/marshal'; + * + * class MyCustomValidator implements PropertyValidator { + * async validate(value: any, target: ClassType, propertyName: string): PropertyValidatorError | void { + * if (value.length > 10) { + * return new PropertyValidatorError('too_long', 'Too long :()'); + * } + * }; + * } + * + * class Entity { + * @f.validator(MyCustomValidator) + * name: string; + * + * @f.validator(MyCustomValidator) + * name: string; + * + * @f.validator((value: any, target: ClassType, propertyName: string) => { + * if (value.length > 255) { + * return new PropertyValidatorError('too_long', 'Too long :()'); + * } + * }) + * title: string; + * } + * + * ``` + */ + validator(validator: ClassType | ((value: any, target: ClassType, propertyName: string) => PropertyValidatorError | void)): + FieldDecoratorResult; } function createFieldDecoratorResult( @@ -549,7 +711,7 @@ function createFieldDecoratorResult( //we got a new decorator with a different name on a constructor param //since we cant not resolve logically which name to use, we forbid that case. throw new Error(`Defining multiple Marshal decorators with different names at arguments of ${getClassName(target)}::${methodName} #${parameterIndexOrDescriptor} is forbidden.` + - ` @Field.asName('name') is required. Got ${methodsParamNames[parameterIndexOrDescriptor] || methodsParamNamesAutoResolved[parameterIndexOrDescriptor]} !== ${givenPropertyName}`) + ` @f.asName('name') is required. Got ${methodsParamNames[parameterIndexOrDescriptor] || methodsParamNamesAutoResolved[parameterIndexOrDescriptor]} !== ${givenPropertyName}`) } if (givenPropertyName) { @@ -560,17 +722,6 @@ function createFieldDecoratorResult( const constructorParamNames = getParameterNames((target as ClassType).prototype.constructor); // const constructorParamNames = getCachedParameterNames((target as ClassType).prototype.constructor); givenPropertyName = constructorParamNames[parameterIndexOrDescriptor]; - if (!givenPropertyName) { - console.debug('constructorParamNames', parameterIndexOrDescriptor, constructorParamNames); - throw new Error('Unable not extract constructor argument names'); - } - - if (methodsParamNames[parameterIndexOrDescriptor] && methodsParamNames[parameterIndexOrDescriptor] !== givenPropertyName) { - //we got a new decorator with a different name on a constructor param - //since we cant not resolve logically which name to use, we forbid that case. - throw new Error(`Defining multiple Marshal decorators with different names at arguments of ${getClassName(target)}::${methodName} is forbidden.` + - ` @Field.asName('name') is required.`) - } if (givenPropertyName) { methodsParamNamesAutoResolved[parameterIndexOrDescriptor] = givenPropertyName; @@ -604,9 +755,6 @@ function createFieldDecoratorResult( if (isNumber(parameterIndexOrDescriptor)) { //decorator is used on a method argument. Might be on constructor or any other method. if (methodName === 'constructor') { - if (!givenPropertyName) { - throw new Error(`Could not resolve property name for class property on ${getClassName(target)} ${propertyOrMethodName}`); - } if (!schema.classProperties[givenPropertyName]) { schema.classProperties[givenPropertyName] = new PropertySchema(givenPropertyName); schema.propertyNames.push(givenPropertyName); @@ -661,7 +809,7 @@ function createFieldDecoratorResult( return createFieldDecoratorResult(cb, givenPropertyName, [...modifier, Exclude(target)], modifiedOptions); }; - fn.id = fn.asId = () => { + fn.primary = fn.asId = () => { resetIfNecessary(); return createFieldDecoratorResult(cb, givenPropertyName, [...modifier, IDField()], modifiedOptions); }; @@ -703,6 +851,24 @@ function createFieldDecoratorResult( return createFieldDecoratorResult(cb, givenPropertyName, modifier, {...modifiedOptions, map: true}); }; + fn.validator = (validator: ClassType | ((value: any, target: ClassType, propertyName: string) => PropertyValidatorError | void)) => { + resetIfNecessary(); + + const validatorClass: ClassType = isPropertyValidator(validator) ? validator : class implements PropertyValidator { + validate(value: any, target: ClassType, propertyName: string): PropertyValidatorError | void { + try { + return validator(value, target, propertyName); + } catch (error) { + return new PropertyValidatorError('error', error.message ? error.message : error); + } + } + }; + + return createFieldDecoratorResult(cb, givenPropertyName, [...modifier, FieldDecoratorWrapper((target, property) => { + property.validators.push(validatorClass); + })], modifiedOptions); + }; + return fn; } @@ -718,50 +884,7 @@ export function FieldDecoratorWrapper( } /** - * Used to define a field as decorated. - * This is necessary if you want to wrap a field value in the class instance using - * a own class, like for example for Array manipulations, but keep the JSON and Database value - * as primitive as possible. - * - * Only one field per class can be the decorated one. - * - * Example - * ```typescript - * export class PageCollection { - * @f.forward(() => PageClass).decorated() - * private readonly pages: PageClass[] = []; - * - * constructor(pages: PageClass[] = []) { - * this.pages = pages; - * } - * - * public count(): number { - * return this.pages.length; - * } - * - * public add(name: string): number { - * return this.pages.push(new PageClass(name)); - * } - * } - * - * export class PageClass { - * @f.uuid() - * id: string = uuid(); - * - * @f - * name: string; - * - * @f.forward(() => PageCollection) - * children: PageCollection = new PageCollection; - * - * constructor(name: string) { - * this.name = name; - * } - * } - * ``` - * - * If you use classToPlain(PageClass, ...) or classToMongo(PageClass, ...) the field value of `children` will be the type of - * `PageCollection.pages` (always the field where @Decorated() is applied to), here a array of PagesClass `PageClass[]`. + * @internal */ function Decorated() { return FieldDecoratorWrapper((target, property) => { @@ -771,11 +894,7 @@ function Decorated() { } /** - * - * Used to define a field as a reference to an ID. - * This is important if you interact with the database abstraction. - * - * Only one field can be the ID. + * @internal */ function IDField() { return FieldDecoratorWrapper((target, property) => { @@ -785,7 +904,7 @@ function IDField() { } /** - * Used to mark a field as optional. The validation requires field values per default, this makes it optional. + * @internal */ function Optional() { return FieldDecoratorWrapper((target, property) => { @@ -808,8 +927,7 @@ function Optional() { * } * * class Job { - * @Field() - * config: JobConfig; + * @f config: JobConfig; * } * ``` * @@ -874,8 +992,7 @@ export function OnLoad(options: { fullLoad?: boolean } = {}) { } /** - * Used to define a field as excluded when serialized from class to different targets (currently to Mongo or JSON). - * PlainToClass or mongoToClass is not effected by this. + * @internal */ function Exclude(t: 'all' | 'mongo' | 'plain' = 'all') { return FieldDecoratorWrapper((target, property) => { @@ -902,23 +1019,14 @@ class ForwardedRef { * * ```typescript * class User { - * @Field(forwardRef(() => Config) + * @f.forward(() => Config) * config: Config; - - * - * @Field([forwardRef(() => Config]) - * configArray: Config[] = []; * - * @FieldArray(forwardRef(() => Config) + * @f.forwardArray(() => Config) * configArray: Config[] = []; * - * - * @FieldMap(forwardRef(() => Config) - * configMap: {[k: string]: Config} = {}; - * - * @Field({forwardRef(() => Config}) + * @f.forwardMap(() => Config) * configMap: {[k: string]: Config} = {}; - * } * * ``` */ @@ -926,44 +1034,19 @@ export function forwardRef(forward: ForwardRefFn): ForwardedRef { return new ForwardedRef(forward); } +/** + * @internal + */ interface FieldOptions { - /** - * Whether the type is a map. You should prefer the short {} annotation - * - * Example short {} annotation - * ```typescript - * class User { - * @Field({MyClass}) - * config2: {[name: string]: MyClass} = {}; - * } - * ``` - * - * Example verbose annotation is necessary for factory method, if you face circular dependencies. - * ```typescript - * class User { - * @Field(() => MyClass, {map: true}) - * config2: {[name: string]: MyClass} = {}; - * } - * ``` - */ map?: boolean; - /** - * @internal - */ array?: boolean; - - /** - * @internal - */ - type?: Types; } - /** * Decorator to define a field for an entity. */ -export function Field(oriType?: FieldTypes | FieldTypes[] | { [n: string]: FieldTypes }) { +export function Field(oriType?: FieldTypes) { return FieldDecoratorWrapper((target, property, returnType, options) => { if (property.typeSet) return; property.typeSet = true; @@ -991,96 +1074,35 @@ export function Field(oriType?: FieldTypes | FieldTypes[] | { [n: string]: Field return getTypeName(t); } - if (!options.type) { - // console.log(`${id} ${returnType} ${typeof type} ${type}`); - - if (type && isArray(type)) { - type = type[0]; - options.array = true; - } - - if (type && isPlainObject(type)) { - type = type[Object.keys(type)[0]]; - options.map = true; - } - - if (type && options.array && returnType !== Array) { - throw new Error(`${id} type mismatch. Given ${getTypeDeclaration(type, options)}, but declared is ${getTypeName(returnType)}. ` + - `Please use the correct type in @Field().` - ); - } - - if (type && !options.array && returnType === Array) { - throw new Error(`${id} type mismatch. Given ${getTypeDeclaration(type, options)}, but declared is ${getTypeName(returnType)}. ` + - `Please use @f.array(MyType) or @f.forwardArray(() => MyType), e.g. @f.array(String) for '${propertyName}: String[]'.`); - } - - if (type && options.map && returnType !== Object) { - throw new Error(`${id} type mismatch. Given ${getTypeDeclaration(type, options)}, but declared is ${getTypeName(returnType)}. ` + - `Please use the correct type in @f.type(TYPE).`); - } - - if (!type && returnType === Array) { - throw new Error(`${id} type mismatch. Given nothing, but declared is Array. You have to specify what type is in that array. ` + - `When you don't declare a type in TypeScript or types are excluded, you need to pass a type manually via @f.type(String).\n` + - `If you don't have a type, use @f.any(). If you reference a class with circular dependency, use @f.forward(forwardRef(() => MyType)).` - ); - } - - if (!type && returnType === Object) { - //typescript puts `Object` for undefined types. - throw new Error(`${id} type mismatch. Given ${getTypeDeclaration(type, options)}, but declared is Object or undefined. ` + - `When you don't declare a type in TypeScript or types are excluded, you need to pass a type manually via @f.type(String).\n` + - `If you don't have a type, use @f.any(). If you reference a class with circular dependency, use @f.forward(() => MyType).` - ); - } - - const isCustomObject = type !== String - && type !== String - && type !== Number - && type !== Date - && type !== Buffer - && type !== Boolean - && type !== Any - && type !== Object - && !(type instanceof ForwardedRef); - - if (type && !options.map && isCustomObject && returnType === Object) { - throw new Error(`${id} type mismatch. Given ${getTypeDeclaration(type, options)}, but declared is Object or undefined. ` + - `The actual type is an Object, but you specified a Class in @f.type(T).\n` + - `Please declare a type or use @f.map(${getClassName(type)} for '${propertyName}: {[k: string]: ${getClassName(type)}}'.`); - } - - options.type = 'any'; - - if (!type) { - type = returnType; - } + if (type && options.array && returnType !== Array) { + throw new Error(`${id} type mismatch. Given ${getTypeDeclaration(type, options)}, but declared is ${getTypeName(returnType)}. ` + + `Please use the correct type in @f.type(T).` + ); + } - if (type === String) { - options.type = 'string'; - } + if (type && !options.array && returnType === Array) { + throw new Error(`${id} type mismatch. Given ${getTypeDeclaration(type, options)}, but declared is ${getTypeName(returnType)}. ` + + `Please use @f.array(MyType) or @f.forwardArray(() => MyType), e.g. @f.array(String) for '${propertyName}: String[]'.`); + } - if (type === Number) { - options.type = 'number'; - } - if (type === Date) { - options.type = 'date'; - } - if (type === Buffer) { - options.type = 'binary'; - } - if (type === Boolean) { - options.type = 'boolean'; - } + if (type && options.map && returnType !== Object) { + throw new Error(`${id} type mismatch. Given ${getTypeDeclaration(type, options)}, but declared is ${getTypeName(returnType)}. ` + + `Please use the correct type in @f.type(TYPE).`); } - if (options.array) { - property.isArray = true; + if (!type && returnType === Array) { + throw new Error(`${id} type mismatch. Given nothing, but declared is Array. You have to specify what type is in that array. ` + + `When you don't declare a type in TypeScript or types are excluded, you need to pass a type manually via @f.type(String).\n` + + `If you don't have a type, use @f.any(). If you reference a class with circular dependency, use @f.forward(forwardRef(() => MyType)).` + ); } - if (options.map) { - property.isMap = true; + if (!type && returnType === Object) { + //typescript puts `Object` for undefined types. + throw new Error(`${id} type mismatch. Given ${getTypeDeclaration(type, options)}, but declared is Object or undefined. ` + + `When you don't declare a type in TypeScript or types are excluded, you need to pass a type manually via @f.type(String).\n` + + `If you don't have a type, use @f.any(). If you reference a class with circular dependency, use @f.forward(() => MyType).` + ); } const isCustomObject = type !== String @@ -1090,26 +1112,34 @@ export function Field(oriType?: FieldTypes | FieldTypes[] | { [n: string]: Field && type !== Buffer && type !== Boolean && type !== Any - && type !== Object; + && type !== Object + && !(type instanceof ForwardedRef); - if (isCustomObject) { - property.type = 'class'; - property.classType = type as ClassType; + if (type && !options.map && isCustomObject && returnType === Object) { + throw new Error(`${id} type mismatch. Given ${getTypeDeclaration(type, options)}, but declared is Object or undefined. ` + + `The actual type is an Object, but you specified a Class in @f.type(T).\n` + + `Please declare a type or use @f.map(${getClassName(type)} for '${propertyName}: {[k: string]: ${getClassName(type)}}'.`); + } - if (type instanceof ForwardedRef) { - property.classTypeForwardRef = type; - delete property.classType; - } - return; + if (!type) { + type = returnType; + } + + if (options.array) { + property.isArray = true; + } + + if (options.map) { + property.isMap = true; } if (property.type === 'any') { - property.type = options.type!; + property.setFromJSType(type); } }, true); } -declare type TYPES = FieldTypes | FieldTypes[] | { [n: string]: FieldTypes }; +declare type TYPES = FieldTypes; const fRaw = Field(); @@ -1146,16 +1176,111 @@ fRaw['forwardMap'] = function (this: FieldDecoratorResult, f: () => TYPES): Fiel }; /** - * Same as @Field() but a short version @f where you can use it like `@f public name: string;`. + * THis is the main decorator to define a properties on class or arguments on methods. + * + * @see FieldDecoratorResult + * @category Decorator */ export const f: FieldDecoratorResult & { + /** + * Defines a type for a certain field. This is only necessary for custom classes + * if the Typescript compiler does not include the reflection type in the build output. + * + * ```typescript + * class User { + * @f.type(MyClass) + * tags: MyClass = new MyClass; + * } + * ``` + */ type: (type: TYPES) => FieldDecoratorResult, + + /** + * Marks a field as array. + * + * ```typescript + * class User { + * @f.array(String) + * tags: string[] = []; + * } + * ``` + */ array: (type: TYPES) => FieldDecoratorResult, + + /** + * Marks a field as enum. + * + * ```typescript + * enum MyEnum { + * low; + * medium; + * hight; + * } + * + * class User { + * @f.enum(MyEnum) + * level: MyEnum = MyEnum.low; + * } + * ``` + * + * If allowLabelsAsValue is set, you can use the enum labels as well for setting the property value using plainToClass(). + */ enum: (type: any, allowLabelsAsValue?: boolean) => FieldDecoratorResult, + + /** + * Marks a field as type any. It does not transform the value and directly uses JSON.parse/stringify. + */ any: () => FieldDecoratorResult, + + /** + * Marks a field as map. + * + * ```typescript + * class User { + * @f.map(String) + * tags: {[k: string]: string}; + * + * @f.forwardMap(() => MyClass) + * tags: {[k: string]: MyClass}; + * } + * ``` + */ map: (type: TYPES) => FieldDecoratorResult, + + /** + * Forward references a type, required for circular reference. + * + * ```typescript + * class User { + * @f.forward(() => User)).optional() + * parent: User; + * } + * ``` + */ forward: (f: () => TYPES) => FieldDecoratorResult, + + /** + * Forward references a type in an array, required for circular reference. + * + * ```typescript + * class User { + * @f.forwardArray(() => User)).optional() + * parents: User[] = []; + * } + * ``` + */ forwardArray: (f: () => TYPES) => FieldDecoratorResult, + + /** + * Forward references a type in a map, required for circular reference. + * + * ```typescript + * class User { + * @f.forwardRef(() => User)).optional() + * parents: {[name: string]: User} = {}}; + * } + * ``` + */ forwardMap: (f: () => TYPES) => FieldDecoratorResult, } = fRaw as any; @@ -1168,39 +1293,22 @@ function Type(type: Types) { }); } - /** - * Used to define a field as ObjectId. This decorator is necessary if you want to use Mongo's _id. - * - * - * ```typescript - * class Page { - * @MongoIdField() - * referenceToSomething?: string; - * - * constructor( - * @IdType() - * @MongoIdField() - * public readonly _id: string - * ) { - * - * } - * } - * ``` + * @internal */ function MongoIdField() { return Type('objectId'); } /** - * Used to define a field as UUID (v4). + * @internal */ function UUIDField() { return Type('uuid'); } /** - * Used to define an index on a field. + * @internal */ function Index(options?: IndexOptions, name?: string) { return FieldDecoratorWrapper((target, property) => { diff --git a/packages/core/src/validation.ts b/packages/core/src/validation.ts index 9d9aeff87..ec976dd44 100644 --- a/packages/core/src/validation.ts +++ b/packages/core/src/validation.ts @@ -1,18 +1,13 @@ import {ClassType, eachKey, getClassName, isArray, isObject, isPlainObject, typeOf} from "@marcj/estdlib"; import {applyDefaultValues, getRegisteredProperties} from "./mapper"; import { - getClassTypeFromInstance, getClassSchema, + getClassTypeFromInstance, getOrCreateEntitySchema, PropertySchema, PropertyValidator, - FieldDecoratorWrapper } from "./decorators"; -function addValidator(target: Object, property: PropertySchema, validator: ClassType) { - property.validators.push(validator); -} - export class PropertyValidatorError { constructor( public readonly code: string, @@ -21,68 +16,6 @@ export class PropertyValidatorError { } } -/** - * Decorator to add a custom validator class. - * - * @example - * ```typescript - * import {PropertyValidator} from '@marcj/marshal'; - * - * class MyCustomValidator implements PropertyValidator { - * async validate(value: any, target: ClassType, propertyName: string): PropertyValidatorError | void { - * if (value.length > 10) { - * return new PropertyValidatorError('too_long', 'Too long :()'); - * } - * }; - * } - * - * class Entity { - * @Field() - * @AddValidator(MyCustomValidator) - * name: string; - * } - * - * ``` - * - * @category Decorator - */ -export function AddValidator(validator: ClassType) { - return FieldDecoratorWrapper((target, property) => { - addValidator(target, property, validator); - }); -} - -/** - * Decorator to add a custom inline validator. - * - * @example - * ```typescript - * class Entity { - * @Field() - * @InlineValidator(async (value: any) => { - * if (value.length > 10) { - * return new PropertyValidatorError('too_long', 'Too long :()'); - * } - * })) - * name: string; - * } - * ``` - * @category Decorator - */ -export function InlineValidator(cb: (value: any, target: ClassType, propertyName: string) => PropertyValidatorError | void) { - return FieldDecoratorWrapper((target, property) => { - addValidator(target, property, class implements PropertyValidator { - validate(value: any, target: ClassType, propertyName: string): PropertyValidatorError | void { - try { - return cb(value, target, propertyName); - } catch (error) { - return new PropertyValidatorError('error', error.message ? error.message : error); - } - } - }); - }); -} - /** * @hidden */ @@ -100,6 +33,7 @@ export class BooleanValidator implements PropertyValidator { const objectIdValidation = new RegExp(/^[a-fA-F0-9]{24}$/); + /** * @hidden */ @@ -116,6 +50,7 @@ export class ObjectIdValidator implements PropertyValidator { } const uuidValidation = new RegExp(/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i); + /** * @hidden */ diff --git a/packages/core/tests/big-entity.ts b/packages/core/tests/big-entity.ts index 2f888b478..f65923f02 100644 --- a/packages/core/tests/big-entity.ts +++ b/packages/core/tests/big-entity.ts @@ -354,7 +354,7 @@ export class JobTask { @Entity('job', 'jobs') export class Job { - @f.id().uuid() + @f.primary().uuid() id: string; @f.uuid() diff --git a/packages/core/tests/decorator.spec.ts b/packages/core/tests/decorator.spec.ts index 0984d220e..bce5f70a0 100644 --- a/packages/core/tests/decorator.spec.ts +++ b/packages/core/tests/decorator.spec.ts @@ -110,7 +110,7 @@ test('test entity database', async () => { @Entity('DifferentDataBase', 'differentCollection') @DatabaseName('testing1') class DifferentDataBase { - @f.id().mongoId() + @f.primary().mongoId() _id?: string; @f @@ -241,9 +241,34 @@ test('test decorator ParentReference without class', () => { } getParentReferenceClass(Model, 'sub'); + expect(getClassSchema(Model).getProperty('sub').isResolvedClassTypeIsDecorated()).toBe(false); }).toThrowError('Model::sub has @ParentReference but no @Class defined.'); }); +test('test No decorated property found', () => { + expect(() => { + class Model { + } + + getClassSchema(Model).getDecoratedPropertySchema(); + getClassSchema(Model).getDecoratedPropertySchema(); + }).toThrowError('No decorated property found'); +}); + +test('test custom decorator', () => { + let called = false; + function Decorator(target: Object, propertyOrMethodName?: string, parameterIndexOrDescriptor?: any) { + called = true; + } + + class Model { + @f.use(Decorator) b!: string; + } + + expect(getClassSchema(Model).getProperty('b').type).toBe('string'); + expect(called).toBe(true); +}); + test('test same name', () => { expect(() => { @Entity('same-name') @@ -299,7 +324,7 @@ test('test properties', () => { @Entity('Model') class Model { - @f.id().mongoId() + @f.primary().mongoId() _id?: string; @f diff --git a/packages/core/tests/document-scenario/DocumentClass.ts b/packages/core/tests/document-scenario/DocumentClass.ts index 796d55c25..95e2c9191 100644 --- a/packages/core/tests/document-scenario/DocumentClass.ts +++ b/packages/core/tests/document-scenario/DocumentClass.ts @@ -3,7 +3,7 @@ import {ParentReference, f} from "../../src/decorators"; import {PageClass} from "./PageClass"; export class DocumentClass { - @f.id().mongoId() + @f.primary().mongoId() _id?: string; @f @@ -17,7 +17,7 @@ export class DocumentClass { } export class ImpossibleToMetDocumentClass { - @f.id().mongoId() + @f.primary().mongoId() _id?: string; diff --git a/packages/core/tests/entities.ts b/packages/core/tests/entities.ts index c739e3541..e1a032274 100644 --- a/packages/core/tests/entities.ts +++ b/packages/core/tests/entities.ts @@ -76,7 +76,7 @@ export class StringCollectionWrapper { @Entity('SimpleModel') @MultiIndex(['name', 'type'], {unique: true}) export class SimpleModel { - @f.id().uuid() + @f.primary().uuid() id: string = uuid(); @f.index() @@ -136,7 +136,7 @@ export class SimpleModel { @Entity('SuperSimple') export class SuperSimple { - @f.id().mongoId() + @f.primary().mongoId() _id?: string; @f @@ -145,7 +145,7 @@ export class SuperSimple { @Entity('BaseClass') export class BaseClass { - @f.id().mongoId() + @f.primary().mongoId() _id?: string; } diff --git a/packages/core/tests/method-decorators.spec.ts b/packages/core/tests/method-decorators.spec.ts index f7ed693b5..915655625 100644 --- a/packages/core/tests/method-decorators.spec.ts +++ b/packages/core/tests/method-decorators.spec.ts @@ -93,7 +93,8 @@ test('method args', () => { expect(props[0].name).toBe('0'); expect(props[0].type).toBe('string'); - expect(props[1]).toBeUndefined(); + expect(props[1].name).toBe('1'); + expect(props[1].type).toBe('boolean'); expect(props[2].name).toBe('2'); expect(props[2].type).toBe('boolean'); @@ -142,10 +143,29 @@ test('short @f multi', () => { }); +test('no decorators', () => { + expect(() => { + class Controller { + public foo(bar: string, nothing: boolean) { + } + } + const s = getClassSchema(Controller); + s.getMethodProperties('foo'); + + }).toThrow('has no decorated used, so reflection does not work'); +}); + test('short @f multi gap', () => { class Controller { public foo(@f bar: string, nothing: boolean, @f foo: number) { } + + @f + public undefined(bar: string, nothing: boolean) { + } + + public onlyFirst(@f.array(String) bar: string[], nothing: boolean) { + } } const s = getClassSchema(Controller); @@ -157,12 +177,34 @@ test('short @f multi gap', () => { expect(props[0].type).toBe('string'); expect(props[0].isArray).toBe(false); - expect(props[1]).toBeUndefined(); + expect(props[1].name).toBe('1'); + expect(props[1].type).toBe('boolean'); expect(props[2].name).toBe('2'); expect(props[2].type).toBe('number'); expect(props[2].isArray).toBe(false); } + { + const props = s.getMethodProperties('undefined'); + + expect(props).toBeArrayOfSize(2); + expect(props[0].name).toBe('0'); + expect(props[0].type).toBe('string'); + + expect(props[1].name).toBe('1'); + expect(props[1].type).toBe('boolean'); + } + { + const props = s.getMethodProperties('onlyFirst'); + + expect(props).toBeArrayOfSize(2); + expect(props[0].name).toBe('0'); + expect(props[0].type).toBe('string'); + expect(props[0].isArray).toBe(true); + + expect(props[1].name).toBe('1'); + expect(props[1].type).toBe('boolean'); + } }); diff --git a/packages/core/tests/validation.spec.ts b/packages/core/tests/validation.spec.ts index 5e2614a5d..93e86c18e 100644 --- a/packages/core/tests/validation.spec.ts +++ b/packages/core/tests/validation.spec.ts @@ -1,8 +1,6 @@ import 'reflect-metadata'; import 'jest-extended' import { - AddValidator, - InlineValidator, plainToClass, PropertyValidator, PropertyValidatorError, @@ -136,8 +134,7 @@ test('test AddValidator', async () => { } class Model { - @f - @AddValidator(MyValidator) + @f.validator(MyValidator) id: string = '2'; } @@ -153,8 +150,7 @@ test('test inline validator throw Error', async () => { } class Model { - @f - @InlineValidator((value: string) => { + @f.validator((value: string) => { if (value.length > 5) { throw new MyError(); } @@ -168,8 +164,7 @@ test('test inline validator throw Error', async () => { test('test inline validator throw string', async () => { class Model { - @f - @InlineValidator((value: string) => { + @f.validator((value: string) => { if (value.length > 5) { throw 'Too long'; } @@ -183,8 +178,7 @@ test('test inline validator throw string', async () => { test('test inline validator', async () => { class Model { - @f - @InlineValidator((value: string) => { + @f.validator((value: string) => { if (value.length > 5) { return new PropertyValidatorError('too_long', 'Too long'); } diff --git a/packages/core/tests/validation2.spec.ts b/packages/core/tests/validation2.spec.ts index cdf76da7c..199c53937 100644 --- a/packages/core/tests/validation2.spec.ts +++ b/packages/core/tests/validation2.spec.ts @@ -9,8 +9,8 @@ test('test minimized code', async () => { sshPort: number = 22; constructor( - @f.id().uuid().asName('nodeId') - @f.id() + @f.primary().uuid().asName('nodeId') + @f.primary() public e: string ) { } @@ -23,8 +23,8 @@ test('test minimized code', async () => { sshPort: number = 22; constructor( - @f.id().uuid().asName('nodeId') - @f.id().uuid().asName('asd') + @f.primary().uuid().asName('nodeId') + @f.primary().uuid().asName('asd') public e: string ) { } @@ -36,8 +36,8 @@ test('test minimized code', async () => { sshPort: number = 22; constructor( - @f.id().uuid().asName('nodeId') - @f.id().uuid().asName('nodeId') + @f.primary().uuid().asName('nodeId') + @f.primary().uuid().asName('nodeId') public e: string ) { } diff --git a/packages/mongo/package.json b/packages/mongo/package.json index d5df317b2..5c4b37b6a 100644 --- a/packages/mongo/package.json +++ b/packages/mongo/package.json @@ -18,7 +18,7 @@ "typeorm": "^0.2.14" }, "dependencies": { - "@marcj/estdlib": "^0.1.11", + "@marcj/estdlib": "^0.1.15", "@marcj/marshal": "^0.10.0", "mongo-uuid": "^1.0.0" }, diff --git a/packages/mongo/src/database.ts b/packages/mongo/src/database.ts index d0fe42199..75db4b990 100644 --- a/packages/mongo/src/database.ts +++ b/packages/mongo/src/database.ts @@ -4,12 +4,13 @@ import { classToMongo, convertClassQueryToMongo, mongoToClass, - partialClassToMongo, + partialClassToMongo, partialMongoToClass, partialMongoToPlain, propertyClassToMongo } from "./mapping"; import {Collection, Connection, Cursor} from 'typeorm'; import {ClassType, getClassName} from '@marcj/estdlib'; +import {FindOneOptions} from "mongodb"; export class NoIDDefinedError extends Error { } @@ -107,6 +108,51 @@ export class Database { return cursor; } + /** + * Returns specified fields from the first document. + */ + public async fieldsOne( + classType: ClassType, + filter: { [field: string]: any }, + fields: (keyof T)[] | string[], + ): Promise | {[P in keyof T]?: any} | undefined> { + const collection = await this.getCollection(classType); + + const projection: any = {}; + for (const f of fields) { + projection[f] = 1; + } + + const item = await collection.findOne(convertClassQueryToMongo(classType, filter), {projection} as FindOneOptions); + if (!item) { + return undefined; + } + + return partialMongoToClass(classType, item) as Partial | {[P in keyof T]?: any}; + } + + /** + * Returns specified fields from a list of document. + */ + public async fields( + classType: ClassType, + filter: { [field: string]: any }, + fields: (keyof T)[] | string[], + ): Promise<(Partial | {[P in keyof T]?: any})[]> { + const collection = await this.getCollection(classType); + + const projection: any = {}; + for (const f of fields) { + projection[f] = 1; + } + + return await collection + .find(convertClassQueryToMongo(classType, filter)) + .project(projection) + .map((v) => partialClassToMongo(classType, v)) + .toArray(); + } + /** * Removes ONE item from the database that has the given id. You need to use @ID() decorator * for at least and max one property at your entity to use this method. @@ -264,7 +310,7 @@ export class Database { } /** - * Patches all items in the collection and returns the count of modified items.. + * Patches all items in the collection and returns the count of modified items. * It's possible to provide nested key-value pairs, where the path should be based on dot symbol separation. * * Example diff --git a/packages/mongo/src/mapping.ts b/packages/mongo/src/mapping.ts index b9b0e4de9..8d5481cd4 100644 --- a/packages/mongo/src/mapping.ts +++ b/packages/mongo/src/mapping.ts @@ -59,6 +59,19 @@ export function partialPlainToMongo( return result; } +export function partialMongoToClass( + classType: ClassType, + target: { [path: string]: any }, + parents: any[] = [], +): { [path: string]: any } { + const result = {}; + for (const i of eachKey(target)) { + result[i] = propertyMongoToClass(classType, i, target[i], parents); + } + + return result; +} + export function partialMongoToPlain( classType: ClassType, target: { [path: string]: any }, @@ -264,7 +277,7 @@ export function propertyPlainToMongo( if (isOptional(typeValue, property)) { continue; } - throw new Error(`Missing value in ${getClassPropertyName(resolvedClassType, propertyName)} for `+ + throw new Error(`Missing value in ${getClassPropertyName(resolvedClassType, propertyName)} for ` + `${getClassPropertyName(typeValue, property)}. Can not convert to mongo.`); } @@ -299,9 +312,9 @@ export function propertyMongoToClass( classType: ClassType, propertyName: string, propertyValue: any, - parents: any[], - incomingLevel: number, - state: ToClassState + parents: any[] = [], + incomingLevel: number = 1, + state: ToClassState = {onFullLoadCallbacks: []} ) { if (isUndefined(propertyValue)) { return undefined; @@ -384,7 +397,7 @@ export function propertyMongoToClass( if (map) { const result: any = {}; if (isObject(propertyValue)) { - for (const i of eachKey(propertyValue)) { + for (const i of eachKey(propertyValue)) { result[i] = convert((propertyValue as any)[i]); } } diff --git a/packages/mongo/tests/mongo.spec.ts b/packages/mongo/tests/mongo.spec.ts index aac841215..922618d1a 100644 --- a/packages/mongo/tests/mongo.spec.ts +++ b/packages/mongo/tests/mongo.spec.ts @@ -138,6 +138,33 @@ test('test save model', async () => { expect(await database.has(SimpleModel, {name: 'New Name 2'})).toBeTrue(); }); +test('test patchAll', async () => { + const database = await createDatabase('testing'); + + await database.add(SimpleModel, new SimpleModel('myName1')); + await database.add(SimpleModel, new SimpleModel('myName2')); + await database.add(SimpleModel, new SimpleModel('peter')); + + expect(await database.count(SimpleModel, {name: {$regex: /^myName?/}})).toBe(2); + expect(await database.count(SimpleModel, {name: {$regex: /^peter.*/}})).toBe(1); + + await database.patchAll(SimpleModel, {name: {$regex: /^myName?/}}, { + name: 'peterNew' + }); + + expect(await database.count(SimpleModel, {name: {$regex: /^myName?/}})).toBe(0); + expect(await database.count(SimpleModel, {name: {$regex: /^peter.*/}})).toBe(3); + + const fields = await database.fieldsOne(SimpleModel, {name: 'peterNew'}, ['name']); + expect(fields!.name).toBe('peterNew'); + + const fieldRows = await database.fields(SimpleModel, {}, ['name']); + expect(fieldRows).toBeArrayOfSize(3); + expect(fieldRows[0].name).toBe('peterNew'); + expect(fieldRows[1].name).toBe('peterNew'); + expect(fieldRows[2].name).toBe('peter'); +}); + test('test delete', async () => { const database = await createDatabase('testing'); @@ -222,7 +249,7 @@ test('test databaseName', async () => { @Entity('DifferentDataBase', 'differentCollection') @DatabaseName('testing2') class DifferentDataBase { - @f.id().mongoId() + @f.primary().mongoId() _id?: string; @f @@ -284,7 +311,7 @@ test('second object id', async () => { @Entity('SecondObjectId') class SecondObjectId { - @f.id().mongoId() + @f.primary().mongoId() _id?: string; @f diff --git a/packages/mongo/tests/to-plain.spec.ts b/packages/mongo/tests/to-plain.spec.ts index cf7d28c93..d996e1ff9 100644 --- a/packages/mongo/tests/to-plain.spec.ts +++ b/packages/mongo/tests/to-plain.spec.ts @@ -6,7 +6,7 @@ import {mongoToPlain, partialMongoToPlain, uuid4Binary} from "../src/mapping"; test('mongo to plain', () => { class Model { - @f.id().mongoId() + @f.primary().mongoId() _id?: string; @f @@ -24,7 +24,7 @@ test('mongo to plain', () => { test('mongo to plain partial', () => { class Model { - @f.id().mongoId() + @f.primary().mongoId() _id?: string; @f.uuid()