From 6dc07b90a559062edc5a4dda6cb665ed2cf73799 Mon Sep 17 00:00:00 2001 From: Lotfi MEZIANI Date: Wed, 18 May 2022 15:18:09 +0200 Subject: [PATCH 1/6] feat: add virtual metadata to type metadata storage --- lib/metadata/virtual-metadata.interface.ts | 9 +++++++++ lib/storages/type-metadata.storage.ts | 10 ++++++++++ 2 files changed, 19 insertions(+) create mode 100644 lib/metadata/virtual-metadata.interface.ts diff --git a/lib/metadata/virtual-metadata.interface.ts b/lib/metadata/virtual-metadata.interface.ts new file mode 100644 index 00000000..473ad828 --- /dev/null +++ b/lib/metadata/virtual-metadata.interface.ts @@ -0,0 +1,9 @@ +import { VirtualTypeOptions } from 'mongoose'; + +export interface VirtualMetadataInterface { + target: Function; + name: string; + options?: VirtualTypeOptions; + getter?: (...args: any[]) => any; + setter?: (...args: any[]) => any; +} diff --git a/lib/storages/type-metadata.storage.ts b/lib/storages/type-metadata.storage.ts index 25073689..20578234 100644 --- a/lib/storages/type-metadata.storage.ts +++ b/lib/storages/type-metadata.storage.ts @@ -1,11 +1,13 @@ import { Type } from '@nestjs/common'; import { PropertyMetadata } from '../metadata/property-metadata.interface'; import { SchemaMetadata } from '../metadata/schema-metadata.interface'; +import { VirtualMetadataInterface } from '../metadata/virtual-metadata.interface'; import { isTargetEqual } from '../utils/is-target-equal-util'; export class TypeMetadataStorageHost { private schemas = new Array(); private properties = new Array(); + private virtuals = new Array(); addPropertyMetadata(metadata: PropertyMetadata) { this.properties.unshift(metadata); @@ -16,6 +18,10 @@ export class TypeMetadataStorageHost { this.schemas.push(metadata); } + addVirtualMetadata(metadata: VirtualMetadataInterface) { + this.virtuals.push(metadata); + } + getSchemaMetadataByTarget(target: Type): SchemaMetadata | undefined { return this.schemas.find((item) => item.target === target); } @@ -33,6 +39,10 @@ export class TypeMetadataStorageHost { ) { return this.properties.filter(belongsToClass); } + + getVirtualsMetadataByTarget(targetFilter: Type) { + return this.virtuals.filter(({ target }) => target === targetFilter); + } } const globalRef = global as any; From 1bbf2e6bdb14836afa067d3d038689295919f8bf Mon Sep 17 00:00:00 2001 From: Lotfi MEZIANI Date: Wed, 18 May 2022 15:21:43 +0200 Subject: [PATCH 2/6] feat: implement virtuals factory --- lib/factories/index.ts | 1 + lib/factories/virtuals.factory.ts | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 lib/factories/virtuals.factory.ts diff --git a/lib/factories/index.ts b/lib/factories/index.ts index c979f514..ef2661dd 100644 --- a/lib/factories/index.ts +++ b/lib/factories/index.ts @@ -1,2 +1,3 @@ export * from './definitions.factory'; export * from './schema.factory'; +export * from './virtuals.factory'; diff --git a/lib/factories/virtuals.factory.ts b/lib/factories/virtuals.factory.ts new file mode 100644 index 00000000..d0bf3cdc --- /dev/null +++ b/lib/factories/virtuals.factory.ts @@ -0,0 +1,24 @@ +import * as mongoose from 'mongoose'; +import { Type } from '@nestjs/common'; +import { TypeMetadataStorage } from '../storages/type-metadata.storage'; + +export class VirtualsFactory { + static createForClass( + target: Type, + schema: mongoose.Schema, + ): void { + const virtuals = TypeMetadataStorage.getVirtualsMetadataByTarget(target); + + virtuals.forEach(({ options, name, getter, setter }) => { + const virtual = schema.virtual(name, options); + + if (getter) { + virtual.get(getter); + } + + if (setter) { + virtual.set(setter); + } + }); + } +} From de9b3fb13be698f3b84e05d24fa993d4e643c983 Mon Sep 17 00:00:00 2001 From: Lotfi MEZIANI Date: Wed, 18 May 2022 15:23:14 +0200 Subject: [PATCH 3/6] feat: use virtuals factory in schema factory --- lib/factories/schema.factory.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/factories/schema.factory.ts b/lib/factories/schema.factory.ts index f175aa49..83a65bcf 100644 --- a/lib/factories/schema.factory.ts +++ b/lib/factories/schema.factory.ts @@ -3,6 +3,7 @@ import * as mongoose from 'mongoose'; import { SchemaDefinition, SchemaDefinitionType } from 'mongoose'; import { TypeMetadataStorage } from '../storages/type-metadata.storage'; import { DefinitionsFactory } from './definitions.factory'; +import { VirtualsFactory } from './virtuals.factory'; export class SchemaFactory { // TODO: remove unused, deprecated type argument @@ -13,9 +14,13 @@ export class SchemaFactory { const schemaDefinition = DefinitionsFactory.createForClass(target); const schemaMetadata = TypeMetadataStorage.getSchemaMetadataByTarget(target); - return new mongoose.Schema( + const schema = new mongoose.Schema( schemaDefinition as SchemaDefinition>, schemaMetadata && schemaMetadata.options, ); + + VirtualsFactory.createForClass(target, schema); + + return schema; } } From e70232a64f659846d4354a8815fd4e083a8ae37d Mon Sep 17 00:00:00 2001 From: Lotfi MEZIANI Date: Wed, 18 May 2022 15:23:47 +0200 Subject: [PATCH 4/6] feat: implement virtual decorator --- lib/decorators/index.ts | 1 + lib/decorators/virtual.decorator.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 lib/decorators/virtual.decorator.ts diff --git a/lib/decorators/index.ts b/lib/decorators/index.ts index 6124afb7..ec4a7c3c 100644 --- a/lib/decorators/index.ts +++ b/lib/decorators/index.ts @@ -1,2 +1,3 @@ export * from './prop.decorator'; export * from './schema.decorator'; +export * from './virtual.decorator'; diff --git a/lib/decorators/virtual.decorator.ts b/lib/decorators/virtual.decorator.ts new file mode 100644 index 00000000..14d41864 --- /dev/null +++ b/lib/decorators/virtual.decorator.ts @@ -0,0 +1,29 @@ +import { VirtualTypeOptions } from 'mongoose'; +import { TypeMetadataStorage } from '../storages/type-metadata.storage'; + +/** + * Interface defining property options that can be passed to `@Virtual()` decorator. + */ +export interface VirtualOptions { + options?: VirtualTypeOptions; + subPath?: string; + get?: (...args: any[]) => any; + set?: (...args: any[]) => any; +} + +/** + * @Virtual decorator is used to mark a specific class property as a Mongoose virtual. + */ +export function Virtual(options?: VirtualOptions): PropertyDecorator { + return (target: object, propertyKey: string | symbol) => { + TypeMetadataStorage.addVirtualMetadata({ + target: target.constructor, + options: options?.options, + name: + propertyKey.toString() + + (options?.subPath ? `.${options.subPath}` : ''), + setter: options?.set, + getter: options?.get, + }); + }; +} From f92c7de7fc5062c12621b39968d8a7ff6f7c2006 Mon Sep 17 00:00:00 2001 From: Lotfi MEZIANI Date: Sun, 22 May 2022 18:23:04 +0200 Subject: [PATCH 5/6] test: add test for virtuals factory --- tests/e2e/virtual.factory.spec.ts | 161 ++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 tests/e2e/virtual.factory.spec.ts diff --git a/tests/e2e/virtual.factory.spec.ts b/tests/e2e/virtual.factory.spec.ts new file mode 100644 index 00000000..b87aa2b0 --- /dev/null +++ b/tests/e2e/virtual.factory.spec.ts @@ -0,0 +1,161 @@ +import * as mongoose from 'mongoose'; +import { TypeMetadataStorage } from '../../lib/storages/type-metadata.storage'; +import { VirtualMetadataInterface } from '../../lib/metadata/virtual-metadata.interface'; +import { VirtualsFactory } from '../../lib'; + +describe('VirtualsFactory', () => { + // Mock schema & model + + const setVirtualSetterFunctionMock = jest.fn(); + const setVirtualGetterFunctionMock = jest.fn(); + const schemaMock = { + virtual: jest.fn(() => ({ + get: setVirtualGetterFunctionMock, + set: setVirtualSetterFunctionMock, + })), + } as any; + + const targetConstructorMock = jest.fn(); + + // Mock virtuals + + const virtualOptionsMock = { + ref: 'collectionNameMock', + localField: 'localFieldMockValue', + foreignField: 'foreignFieldMockValue', + }; + + const virtualMetadataWithOnlyRequiredAttributesMock = { + target: targetConstructorMock, + name: 'attribute1Mock', + }; + + const virtualMetadataNotLikedToModelMock = { + target: jest.fn(), + name: 'attribute1Mock', + }; + + const virtualMetadataWithOptionsMock = { + target: targetConstructorMock, + name: 'virtualMetadataWithOptionsMock', + options: virtualOptionsMock, + }; + + const virtualMetadataWithGetterMock = { + target: targetConstructorMock, + name: 'virtualMetadataWithGetterMock', + options: virtualOptionsMock, + getter: jest.fn(), + }; + + const virtualMetadataWithSetterMock = { + target: targetConstructorMock, + name: 'virtualMetadataWithSetterMock', + options: virtualOptionsMock, + setter: jest.fn(), + }; + + const virtualMetadataWithGetterSetterMock = { + target: targetConstructorMock, + name: 'virtualMetadataWithGetterSetterMock', + options: virtualOptionsMock, + getter: jest.fn(), + setter: jest.fn(), + }; + + beforeEach(() => { + (schemaMock.virtual as any) = jest.fn(() => ({ + get: setVirtualGetterFunctionMock, + set: setVirtualSetterFunctionMock, + })); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('Schema virtual definition', () => { + it('should not define virtuals if there is no stored virtual definition', () => { + // arrange + TypeMetadataStorage['virtuals'] = []; + + // Act + VirtualsFactory.createForClass(targetConstructorMock, schemaMock); + + // Assert + expect(schemaMock.virtual).toHaveBeenCalledTimes(0); + }); + + it('should not define virtuals if there is no stored virtual definition linked to schema model', () => { + // arrange + TypeMetadataStorage['virtuals'] = [virtualMetadataNotLikedToModelMock]; + + // Act + VirtualsFactory.createForClass(targetConstructorMock, schemaMock); + + // Assert + expect(schemaMock.virtual).toHaveBeenCalledTimes(0); + }); + + it('should defines virtual for each stored virtualMetadata linked to schema model', () => { + // arrange + TypeMetadataStorage['virtuals'] = [ + virtualMetadataWithOnlyRequiredAttributesMock, + virtualMetadataNotLikedToModelMock, + virtualMetadataWithOptionsMock, + ]; + + // Act + VirtualsFactory.createForClass(targetConstructorMock, schemaMock); + + // Assert + expect(schemaMock.virtual['mock'].calls).toEqual([ + [virtualMetadataWithOnlyRequiredAttributesMock.name, undefined], + [ + virtualMetadataWithOptionsMock.name, + virtualMetadataWithOptionsMock.options, + ], + ]); + }); + }); + + describe('Schema virtual getter/setter definition', () => { + it('should not call the getter/setter definition method if no getter/setter defined in the stored virtual metadata linked to the schema model', () => { + // arrange + TypeMetadataStorage['virtuals'] = [ + virtualMetadataWithOptionsMock, + ] as VirtualMetadataInterface[]; + + // Act + VirtualsFactory.createForClass(targetConstructorMock, schemaMock); + + // Assert + expect(setVirtualGetterFunctionMock).toHaveBeenCalledTimes(0); + expect(setVirtualSetterFunctionMock).toHaveBeenCalledTimes(0); + }); + + it('should call the getter/setter definition method for each stored virtuals metadata with defined getter/setter linked to the schema model', () => { + // arrange + TypeMetadataStorage['virtuals'] = [ + virtualMetadataWithOptionsMock, + virtualMetadataWithGetterMock, + virtualMetadataWithSetterMock, + virtualMetadataWithGetterSetterMock, + ] as VirtualMetadataInterface[]; + + // Act + VirtualsFactory.createForClass(targetConstructorMock, schemaMock); + + // Assert + // expect(setVirtualGetterFunctionMock).toHaveBeenNthCalledWith(1,) + expect(setVirtualGetterFunctionMock.mock.calls).toEqual([ + [virtualMetadataWithGetterMock.getter], + [virtualMetadataWithGetterSetterMock.getter], + ]); + expect(setVirtualSetterFunctionMock.mock.calls).toEqual([ + [virtualMetadataWithSetterMock.setter], + [virtualMetadataWithGetterSetterMock.setter], + ]); + }); + }); +}); From 7f4df5b1d2bd898f4e5c34d7f8a6cf0a46fabf07 Mon Sep 17 00:00:00 2001 From: Lotfi MEZIANI Date: Sun, 22 May 2022 18:23:48 +0200 Subject: [PATCH 6/6] test: add test for schema factory --- tests/e2e/schema.factory.spec.ts | 55 +++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/tests/e2e/schema.factory.spec.ts b/tests/e2e/schema.factory.spec.ts index db50b49e..fd7dd2a2 100644 --- a/tests/e2e/schema.factory.spec.ts +++ b/tests/e2e/schema.factory.spec.ts @@ -1,4 +1,5 @@ -import { Prop, Schema, SchemaFactory } from '../../lib'; +import { VirtualTypeOptions } from 'mongoose'; +import { Prop, Schema, SchemaFactory, Virtual } from '../../lib'; @Schema({ validateBeforeSave: false, _id: true, autoIndex: true }) class ChildClass { @@ -9,6 +10,9 @@ class ChildClass { name: string; } +const getterFunctionMock = jest.fn(); +const setterFunctionMock = jest.fn(); + @Schema({ validateBeforeSave: false, _id: true, @@ -24,6 +28,21 @@ class ExampleClass { @Prop() array: Array; + + @Virtual({ + options: { + localField: 'array', + ref: 'ChildClass', + foreignField: 'id', + }, + }) + virtualPropsWithOptions: Array; + + @Virtual({ + get: getterFunctionMock, + set: setterFunctionMock, + }) + virtualPropsWithGetterSetterFunctions: Array; } describe('SchemaFactory', () => { @@ -50,4 +69,38 @@ describe('SchemaFactory', () => { }), ); }); + + it('should define for schema a virtuals with options', () => { + const { + virtuals: { virtualPropsWithOptions }, + } = SchemaFactory.createForClass(ExampleClass) as any; + + expect(virtualPropsWithOptions).toEqual( + expect.objectContaining({ + path: 'virtualPropsWithOptions', + setters: [expect.any(Function)], + getters: [], + options: expect.objectContaining({ + localField: 'array', + ref: 'ChildClass', + foreignField: 'id', + }), + }), + ); + }); + + it('should define for schema a virtual with getter/setter functions', () => { + const { + virtuals: { virtualPropsWithGetterSetterFunctions }, + } = SchemaFactory.createForClass(ExampleClass) as any; + + expect(virtualPropsWithGetterSetterFunctions).toEqual( + expect.objectContaining({ + path: 'virtualPropsWithGetterSetterFunctions', + setters: [setterFunctionMock], + getters: [getterFunctionMock], + options: {}, + }), + ); + }); });