Skip to content

Commit

Permalink
Merge branch 'LotfiMEZIANI-feature/virtual-decorator'
Browse files Browse the repository at this point in the history
  • Loading branch information
kamilmysliwiec committed Oct 21, 2024
2 parents a6285c2 + bd17fe1 commit 77add3d
Show file tree
Hide file tree
Showing 9 changed files with 287 additions and 2 deletions.
1 change: 1 addition & 0 deletions lib/decorators/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './prop.decorator';
export * from './schema.decorator';
export * from './virtual.decorator';
42 changes: 42 additions & 0 deletions lib/decorators/virtual.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { VirtualTypeOptions } from 'mongoose';
import { TypeMetadataStorage } from '../storages/type-metadata.storage';

/**
* Interface defining the options that can be passed to the `@Virtual()` decorator.
*/
export interface VirtualOptions {
/**
* The options to pass to the virtual type.
*/
options?: VirtualTypeOptions;
/**
* The sub path to use for the virtual.
* Defaults to the property key.
*/
subPath?: string;
/**
* The getter function to use for the virtual.
*/
get?: (...args: any[]) => any;
/**
* The setter function to use for the virtual.
*/
set?: (...args: any[]) => any;
}

/**
* The Virtual decorator marks a 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,
});
};
}
1 change: 1 addition & 0 deletions lib/factories/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './definitions.factory';
export * from './schema.factory';
export * from './virtuals.factory';
7 changes: 6 additions & 1 deletion lib/factories/schema.factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
static createForClass<TClass = any>(
Expand All @@ -13,9 +14,13 @@ export class SchemaFactory {
TypeMetadataStorage.getSchemaMetadataByTarget(target);
const schemaOpts = schemaMetadata?.options;

return new mongoose.Schema<TClass>(
const schema = new mongoose.Schema<TClass>(
schemaDefinition as SchemaDefinition<SchemaDefinitionType<TClass>>,
schemaOpts as mongoose.SchemaOptions<any>,
);

VirtualsFactory.inspect(target, schema);

return schema;
}
}
24 changes: 24 additions & 0 deletions lib/factories/virtuals.factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Type } from '@nestjs/common';
import * as mongoose from 'mongoose';
import { TypeMetadataStorage } from '../storages/type-metadata.storage';

export class VirtualsFactory {
static inspect<TClass = any>(
target: Type<TClass>,
schema: mongoose.Schema<TClass>,
): 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);
}
});
}
}
9 changes: 9 additions & 0 deletions lib/metadata/virtual-metadata.interface.ts
Original file line number Diff line number Diff line change
@@ -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;
}
10 changes: 10 additions & 0 deletions lib/storages/type-metadata.storage.ts
Original file line number Diff line number Diff line change
@@ -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<SchemaMetadata>();
private properties = new Array<PropertyMetadata>();
private virtuals = new Array<VirtualMetadataInterface>();

addPropertyMetadata(metadata: PropertyMetadata) {
this.properties.unshift(metadata);
Expand All @@ -16,6 +18,10 @@ export class TypeMetadataStorageHost {
this.schemas.push(metadata);
}

addVirtualMetadata(metadata: VirtualMetadataInterface) {
this.virtuals.push(metadata);
}

getSchemaMetadataByTarget(target: Type<unknown>): SchemaMetadata | undefined {
return this.schemas.find((item) => item.target === target);
}
Expand All @@ -33,6 +39,10 @@ export class TypeMetadataStorageHost {
) {
return this.properties.filter(belongsToClass);
}

getVirtualsMetadataByTarget<TClass>(targetFilter: Type<TClass>) {
return this.virtuals.filter(({ target }) => target === targetFilter);
}
}

const globalRef = global as any;
Expand Down
55 changes: 54 additions & 1 deletion tests/e2e/schema.factory.spec.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -9,6 +10,9 @@ class ChildClass {
name: string;
}

const getterFunctionMock = jest.fn();
const setterFunctionMock = jest.fn();

@Schema({
validateBeforeSave: false,
_id: true,
Expand All @@ -24,6 +28,21 @@ class ExampleClass {

@Prop()
array: Array<any>;

@Virtual({
options: {
localField: 'array',
ref: 'ChildClass',
foreignField: 'id',
},
})
virtualPropsWithOptions: Array<ChildClass>;

@Virtual({
get: getterFunctionMock,
set: setterFunctionMock,
})
virtualPropsWithGetterSetterFunctions: Array<ChildClass>;
}

describe('SchemaFactory', () => {
Expand All @@ -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: {},
}),
);
});
});
140 changes: 140 additions & 0 deletions tests/e2e/virtual.factory.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { VirtualsFactory } from '../../lib';
import { VirtualMetadataInterface } from '../../lib/metadata/virtual-metadata.interface';
import { TypeMetadataStorage } from '../../lib/storages/type-metadata.storage';

describe('VirtualsFactory', () => {
const setVirtualSetterFunctionMock = jest.fn();
const setVirtualGetterFunctionMock = jest.fn();
const schemaMock = {
virtual: jest.fn(() => ({
get: setVirtualGetterFunctionMock,
set: setVirtualSetterFunctionMock,
})),
} as any;

const targetConstructorMock = jest.fn();

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', () => {
TypeMetadataStorage['virtuals'] = [];

VirtualsFactory.inspect(targetConstructorMock, schemaMock);

expect(schemaMock.virtual).toHaveBeenCalledTimes(0);
});

it('should not define virtuals if there is no stored virtual definition linked to schema model', () => {
TypeMetadataStorage['virtuals'] = [virtualMetadataNotLikedToModelMock];

VirtualsFactory.inspect(targetConstructorMock, schemaMock);

expect(schemaMock.virtual).toHaveBeenCalledTimes(0);
});

it('should defines virtual for each stored virtualMetadata linked to schema model', () => {
TypeMetadataStorage['virtuals'] = [
virtualMetadataWithOnlyRequiredAttributesMock,
virtualMetadataNotLikedToModelMock,
virtualMetadataWithOptionsMock,
];

VirtualsFactory.inspect(targetConstructorMock, schemaMock);

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', () => {
TypeMetadataStorage['virtuals'] = [
virtualMetadataWithOptionsMock,
] as VirtualMetadataInterface[];

VirtualsFactory.inspect(targetConstructorMock, schemaMock);

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', () => {
TypeMetadataStorage['virtuals'] = [
virtualMetadataWithOptionsMock,
virtualMetadataWithGetterMock,
virtualMetadataWithSetterMock,
virtualMetadataWithGetterSetterMock,
] as VirtualMetadataInterface[];

VirtualsFactory.inspect(targetConstructorMock, schemaMock);

expect(setVirtualGetterFunctionMock.mock.calls).toEqual([
[virtualMetadataWithGetterMock.getter],
[virtualMetadataWithGetterSetterMock.getter],
]);
expect(setVirtualSetterFunctionMock.mock.calls).toEqual([
[virtualMetadataWithSetterMock.setter],
[virtualMetadataWithGetterSetterMock.setter],
]);
});
});
});

0 comments on commit 77add3d

Please sign in to comment.