Skip to content

Commit

Permalink
feat(mongodb): Use typegoose for MongoDB support
Browse files Browse the repository at this point in the history
  • Loading branch information
marian2js authored and doug-martin committed Oct 16, 2020
1 parent 22decdf commit 702dc83
Show file tree
Hide file tree
Showing 27 changed files with 313 additions and 265 deletions.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { connections } from 'mongoose';
import { MongoMemoryServer } from 'mongodb-memory-server';
import { seed } from './seeds';

const mongoServer = new MongoMemoryServer();

export function getConnectionUri(): Promise<string> {
return mongoServer.getUri();
}

export const dropDatabase = async (): Promise<void> => {
await connections[connections.length - 1].dropDatabase();
};

export const prepareDb = async (): Promise<void> => {
await seed(connections[connections.length - 1]);
};

export const closeDbConnection = async (): Promise<void> => {
await connections[connections.length - 1].close();
};
35 changes: 35 additions & 0 deletions packages/query-typegoose/__tests__/__fixtures__/seeds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Connection } from 'mongoose';
import { plainToClass } from 'class-transformer';
import { TestEntity } from './test.entity';
import { TestReference } from './test-reference.entity';

export const TEST_ENTITIES: TestEntity[] = new Array(15)
.fill(0)
.map((e, i) => i + 1)
.map((i) => {
return plainToClass(TestEntity, {
boolType: i % 2 === 0,
dateType: new Date(`2020-02-${i}`),
numberType: i,
stringType: `foo${i}`,
});
});

export const TEST_REFERENCES: TestReference[] = [1, 2, 3, 4, 5].map((i) => {
return plainToClass(TestReference, {
name: `name${i}`,
});
});

export const seed = async (connection: Connection): Promise<void> => {
await connection.collection('testentities').insertMany(TEST_ENTITIES);
await connection.collection('testreferences').insertMany(TEST_REFERENCES);

await Promise.all(
TEST_REFERENCES.map((testReference, i) => {
return connection
.collection('testentities')
.updateOne({ stringType: TEST_ENTITIES[i + 10].stringType }, { $set: { testReference: testReference.id } });
}),
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { mongoose, prop } from '@typegoose/typegoose';

export class TestReference {
get id() {
const idKey = '_id';
return ((this as unknown) as Record<string, mongoose.Types.ObjectId>)[idKey]?.toString();
}

@prop({ required: true })
name!: string;
}
33 changes: 33 additions & 0 deletions packages/query-typegoose/__tests__/__fixtures__/test.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { mongoose, prop, Ref } from '@typegoose/typegoose';
import { TestReference } from './test-reference.entity';

export class TestEntity {
get id() {
const idKey = '_id';
return ((this as unknown) as Record<string, mongoose.Types.ObjectId>)[idKey]?.toString();
}

@prop({ required: true })
stringType!: string;

@prop({ required: true })
boolType!: boolean;

@prop({ required: true })
numberType!: number;

@prop({ required: true })
dateType!: Date;

@prop({ ref: TestReference })
testReference?: Ref<TestReference>;

getInputData() {
return {
stringType: this.stringType,
boolType: this.boolType,
numberType: this.numberType,
dateType: this.dateType,
};
}
}
12 changes: 12 additions & 0 deletions packages/query-typegoose/__tests__/module.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { NestjsQueryTypegooseModule } from '../src';

describe('NestjsQueryTypegooseModule', () => {
it('should create a module', () => {
class TestEntity {}
const typeOrmModule = NestjsQueryTypegooseModule.forFeature([TestEntity]);
expect(typeOrmModule.imports).toHaveLength(1);
expect(typeOrmModule.module).toBe(NestjsQueryTypegooseModule);
expect(typeOrmModule.providers).toHaveLength(1);
expect(typeOrmModule.exports).toHaveLength(2);
});
});
15 changes: 15 additions & 0 deletions packages/query-typegoose/__tests__/providers.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { getQueryServiceToken } from '@nestjs-query/core';
import { instance } from 'ts-mockito';
import { createTypegooseQueryServiceProviders } from '../src/providers';
import { TypegooseQueryService } from '../src/services';

describe('createTypegooseQueryServiceProviders', () => {
it('should create a provider for the entity', () => {
class TestEntity {}
const providers = createTypegooseQueryServiceProviders([TestEntity]);
expect(providers).toHaveLength(1);
expect(providers[0].provide).toBe(getQueryServiceToken(TestEntity));
expect(providers[0].inject).toEqual([`${TestEntity.name}Model`]);
expect(providers[0].useFactory(instance(() => {}))).toBeInstanceOf(TypegooseQueryService);
});
});
Original file line number Diff line number Diff line change
@@ -1,38 +1,38 @@
import { MongoRepository } from 'typeorm';
import { Test, TestingModule } from '@nestjs/testing';
import { plainToClass } from 'class-transformer';
import { InjectRepository, TypeOrmModule } from '@nestjs/typeorm';
import { ObjectID } from 'mongodb';
import { TypeOrmMongoQueryService } from '../../src/services';
import { InjectModel, TypegooseModule } from 'nestjs-typegoose';
import { ReturnModelType, mongoose } from '@typegoose/typegoose';
import { TypegooseQueryService } from '../../src/services';
import { TestEntity } from '../__fixtures__/test.entity';
import {
closeTestConnection,
CONNECTION_OPTIONS,
refresh,
getTestConnection,
truncate,
} from '../__fixtures__/connection.fixture';
import { TEST_ENTITIES } from '../__fixtures__/seeds';

describe('TypeOrmMongoQueryService', () => {
import { getConnectionUri, prepareDb, closeDbConnection, dropDatabase } from '../__fixtures__/connection.fixture';
import { TEST_ENTITIES, TEST_REFERENCES } from '../__fixtures__/seeds';
import { TestReference } from '../__fixtures__/test-reference.entity';

describe('TypegooseQueryService', () => {
let moduleRef: TestingModule;

class TestEntityService extends TypeOrmMongoQueryService<TestEntity> {
constructor(@InjectRepository(TestEntity) readonly repo: MongoRepository<TestEntity>) {
super(repo);
class TestEntityService extends TypegooseQueryService<TestEntity> {
constructor(@InjectModel(TestEntity) readonly model: ReturnModelType<typeof TestEntity>) {
super(model);
}
}

afterEach(closeTestConnection);

beforeEach(async () => {
beforeAll(async () => {
moduleRef = await Test.createTestingModule({
imports: [TypeOrmModule.forRoot(CONNECTION_OPTIONS), TypeOrmModule.forFeature([TestEntity])],
imports: [
TypegooseModule.forRoot(await getConnectionUri(), { useFindAndModify: false }),
TypegooseModule.forFeature([TestEntity, TestReference]),
],
providers: [TestEntityService],
}).compile();
await refresh();
});

afterAll(async () => closeDbConnection());

beforeEach(() => prepareDb());

afterEach(() => dropDatabase());

describe('#query', () => {
it('call find and return the result', async () => {
const queryService = moduleRef.get(TestEntityService);
Expand All @@ -42,10 +42,10 @@ describe('TypeOrmMongoQueryService', () => {
});

describe('#count', () => {
it('call find and return the result', async () => {
it('should return number of elements matching a query', async () => {
const queryService = moduleRef.get(TestEntityService);
const queryResult = await queryService.count({ stringType: { gt: 'foo' } });
return expect(queryResult).toBe(10);
return expect(queryResult).toBe(15);
});
});

Expand All @@ -59,7 +59,7 @@ describe('TypeOrmMongoQueryService', () => {

it('return undefined if not found', async () => {
const queryService = moduleRef.get(TestEntityService);
const found = await queryService.findById(new ObjectID().toString());
const found = await queryService.findById(new mongoose.Types.ObjectId().toString());
expect(found).toBeUndefined();
});
});
Expand All @@ -73,63 +73,55 @@ describe('TypeOrmMongoQueryService', () => {
});

it('return undefined if not found', () => {
const badId = new ObjectID().toString();
const badId = new mongoose.Types.ObjectId().toString();
const queryService = moduleRef.get(TestEntityService);
return expect(queryService.getById(badId)).rejects.toThrow(`Unable to find TestEntity with id: ${badId}`);
});
});

describe('#createMany', () => {
it('call save on the repo with instances of entities when passed plain objects', async () => {
await truncate(getTestConnection());
const queryService = moduleRef.get(TestEntityService);
const created = await queryService.createMany(TEST_ENTITIES);
const created = await queryService.createMany(TEST_ENTITIES.map((e) => e.getInputData()));
created.forEach((createdEntity, i) => {
expect(createdEntity).toEqual({
...TEST_ENTITIES[i],
id: expect.any(ObjectID),
});
expect(createdEntity).toEqual(expect.objectContaining(TEST_ENTITIES[i].getInputData()));
});
});

it('call save on the repo with instances of entities when passed instances', async () => {
await truncate(getTestConnection());
const instances = TEST_ENTITIES.map((e) => plainToClass(TestEntity, e));
const instances = TEST_ENTITIES.map((e) => plainToClass(TestEntity, e.getInputData()));
const queryService = moduleRef.get(TestEntityService);
const created = await queryService.createMany(instances);
expect(created).toEqual(instances);
created.forEach((createdEntity, i) => {
expect(createdEntity).toEqual(expect.objectContaining(instances[i].getInputData()));
});
});

it('should reject if the entities already exist', async () => {
const queryService = moduleRef.get(TestEntityService);
return expect(queryService.createMany(TEST_ENTITIES)).rejects.toThrow('Entity already exists');
return expect(queryService.createMany(TEST_ENTITIES)).rejects.toThrow(/duplicate key error dup key/);
});
});

describe('#createOne', () => {
it('call save on the repo with an instance of the entity when passed a plain object', async () => {
await truncate(getTestConnection());
const entity = TEST_ENTITIES[0];
const entity = TEST_ENTITIES[0].getInputData();
const queryService = moduleRef.get(TestEntityService);
const created = await queryService.createOne(entity);
expect(created).toEqual({
...entity,
id: expect.any(ObjectID),
});
expect(created).toEqual(expect.objectContaining(entity));
});

it('call save on the repo with an instance of the entity when passed an instance', async () => {
await truncate(getTestConnection());
const entity = plainToClass(TestEntity, TEST_ENTITIES[0]);
const entity = plainToClass(TestEntity, TEST_ENTITIES[0].getInputData());
const queryService = moduleRef.get(TestEntityService);
const created = await queryService.createOne(entity);
expect(created).toEqual(entity);
expect(created).toEqual(expect.objectContaining(entity));
});

it('should reject if the entity contains an id', async () => {
const entity = TEST_ENTITIES[0];
const queryService = moduleRef.get(TestEntityService);
return expect(queryService.createOne(entity)).rejects.toThrow('Entity already exists');
return expect(queryService.createOne({ ...entity })).rejects.toThrow(/duplicate key error dup key/);
});
});

Expand All @@ -141,23 +133,21 @@ describe('TypeOrmMongoQueryService', () => {
});
expect(deletedCount).toEqual(expect.any(Number));
const allCount = await queryService.count({});
expect(allCount).toBe(5);
expect(allCount).toBe(TEST_ENTITIES.length - 5);
});
});

describe('#deleteOne', () => {
it('remove the entity', async () => {
const queryService = moduleRef.get(TestEntityService);
const deleted = await queryService.deleteOne(TEST_ENTITIES[0].id.toString());
expect(deleted).toEqual({ ...TEST_ENTITIES[0], id: undefined });
expect(deleted).toEqual(TEST_ENTITIES[0]);
});

it('call fail if the entity is not found', async () => {
const badId = new ObjectID().toString();
const badId = new mongoose.Types.ObjectId().toString();
const queryService = moduleRef.get(TestEntityService);
return expect(queryService.deleteOne(badId)).rejects.toThrow(
`Could not find any entity of type "TestEntity" matching: "${badId}"`,
);
return expect(queryService.deleteOne(badId)).rejects.toThrow(`Unable to find TestEntity with id: ${badId}`);
});
});

Expand All @@ -174,7 +164,7 @@ describe('TypeOrmMongoQueryService', () => {

it('should reject if the update contains the ID', () => {
const queryService = moduleRef.get(TestEntityService);
return expect(queryService.updateMany({ id: new ObjectID() }, {})).rejects.toThrow(
return expect(queryService.updateMany({ id: new mongoose.Types.ObjectId().toString() }, {})).rejects.toThrow(
'Id cannot be specified when updating',
);
});
Expand All @@ -184,22 +174,53 @@ describe('TypeOrmMongoQueryService', () => {
it('update the entity', async () => {
const queryService = moduleRef.get(TestEntityService);
const updated = await queryService.updateOne(TEST_ENTITIES[0].id.toString(), { stringType: 'updated' });
expect(updated).toEqual({ ...TEST_ENTITIES[0], stringType: 'updated' });
expect(updated).toEqual({
...TEST_ENTITIES[0],
stringType: 'updated',
});
});

it('should reject if the update contains the ID', async () => {
const queryService = moduleRef.get(TestEntityService);
return expect(queryService.updateOne(TEST_ENTITIES[0].id.toString(), { id: new ObjectID() })).rejects.toThrow(
'Id cannot be specified when updating',
);
return expect(
queryService.updateOne(TEST_ENTITIES[0].id.toString(), { id: new mongoose.Types.ObjectId().toString() }),
).rejects.toThrow('Id cannot be specified when updating');
});

it('call fail if the entity is not found', async () => {
const badId = new ObjectID().toString();
const badId = new mongoose.Types.ObjectId().toString();
const queryService = moduleRef.get(TestEntityService);
return expect(queryService.updateOne(badId, { stringType: 'updated' })).rejects.toThrow(
`Could not find any entity of type "TestEntity" matching: "${badId}"`,
`Unable to find TestEntity with id: ${badId}`,
);
});
});

describe('#findRelation', () => {
it('call select and return the result', async () => {
const entity = TEST_ENTITIES[10];
entity.testReference = new mongoose.Types.ObjectId(TEST_REFERENCES[0].id);
const queryService = moduleRef.get(TestEntityService);
const queryResult = await queryService.findRelation(TestReference, 'testReference', [entity]);

expect(queryResult.values().next().value).toEqual(TEST_REFERENCES[0]);
});

it('should return undefined select if no results are found.', async () => {
const entity = TEST_ENTITIES[0];
const queryService = moduleRef.get(TestEntityService);
const queryResult = await queryService.findRelation(TestReference, 'testReference', [entity]);

expect(queryResult.values().next().value).toBeUndefined();
});

it('should return undefined select if relation entity does not exist.', async () => {
const entity = TEST_ENTITIES[10];
entity.testReference = new mongoose.Types.ObjectId(TEST_REFERENCES[0].id);
const queryService = moduleRef.get(TestEntityService);
const queryResult = await queryService.findRelation(TestReference, 'badRelation', [entity]);

expect(queryResult.values().next().value).toBeUndefined();
});
});
});
Loading

0 comments on commit 702dc83

Please sign in to comment.