Skip to content

Commit

Permalink
feat(MongoResource): Added automatic index creation and checking
Browse files Browse the repository at this point in the history
  • Loading branch information
0xfede committed Sep 12, 2017
1 parent 7c6708b commit b653712
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 19 deletions.
19 changes: 19 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"@types/decamelize": "^1.2.0",
"@types/express": "^4.0.37",
"@types/lodash": "^4.14.74",
"@types/mongodb": "^2.2.11",
"@types/node": "^8.0.26",
"@types/request": "^2.0.3",
"@types/semver": "^5.4.0",
Expand Down
10 changes: 3 additions & 7 deletions src/mongo/operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export abstract class MongoOperation extends Operation {
return super.resource as MongoResource;
}
get collection(): Promise<mongo.Collection> {
return this.getCollection();
return this.resource.getCollection(this.getCollectionOptions());
}
get requestSchema(): any {
return this.resource.requestSchema;
Expand All @@ -42,12 +42,8 @@ export abstract class MongoOperation extends Operation {
return this.resource.responseSchema;
}

async getCollection(): Promise<mongo.Collection> {
let db:mongo.Db = await this.resource.db;
return db.collection(this.resource.collection, this.getCollectionOptions())
}
protected getCollectionOptions(): mongo.DbCollectionOptions {
return {};
protected getCollectionOptions(): mongo.DbCollectionOptions | undefined {
return undefined;
}
protected getItemQuery(_id) {
let idIsObjectId = this.resource.idIsObjectId || this.resource.id === '_id';
Expand Down
54 changes: 53 additions & 1 deletion src/mongo/resource.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import * as _ from 'lodash';
import * as decamelize from 'decamelize';
import { MongoClient, Db } from 'mongodb';
import { getLogger } from 'debuggo';
import { MongoClient, Db, Collection, DbCollectionOptions } from 'mongodb';
import { Routes, Resource, ResourceDefinition } from '../resource';
import { QueryMongoOperation, ReadMongoOperation, CreateMongoOperation, UpdateMongoOperation, RemoveMongoOperation } from './operation';

const logger = getLogger('arrest');
const __db = Symbol();
const __indexesChecked = Symbol();

export interface MongoResourceDefinition extends ResourceDefinition {
collection?: string;
Expand All @@ -30,6 +34,7 @@ export class MongoResource extends Resource {
if (typeof this.idIsObjectId !== 'boolean') {
this.idIsObjectId = (this.id === '_id');
}
this[__indexesChecked] = false;
}

get db(): Promise<Db> {
Expand All @@ -45,6 +50,53 @@ export class MongoResource extends Resource {
return this.schema;
}

async checkCollectionIndexes(coll:Collection): Promise<any> {
const indexes = this.getIndexes();
if (indexes && indexes.length) {
let currIndexes;
try {
currIndexes = await coll.indexes();
} catch(e) {
currIndexes = [];
}
for (let i of indexes) {
const props = [ 'unique', 'sparse', 'min', 'max', 'expireAfterSeconds', 'key' ];

let c_i = currIndexes.find(t => {
return (typeof i.name === 'undefined' || i.name === t.name) && _.isEqual(_.pick(i, props), _.pick(t, props));
});
if (!c_i) {
logger.info(this.name, 'creating missing index', i);
await coll.createIndex(i.key, _.omit(i, 'key'));
}
}
}
this[__indexesChecked] = true;
}
async getCollection(opts?: DbCollectionOptions): Promise<Collection> {
let db: Db = await this.db;
let coll: Collection = await new Promise<Collection>((resolve, reject) => {
// TODO change this as soon as mongodb typings are fixed. Current version does not let you get a promise if you pass options
db.collection(this.collection, opts || this.getCollectionOptions(), (err: any, coll?: Collection) => {
if (err) {
reject(err);
} else {
resolve(coll);
}
});
});
if (!this[__indexesChecked]) {
await this.checkCollectionIndexes(coll);
}
return coll;
}
protected getCollectionOptions(): DbCollectionOptions {
return { };
}
protected getIndexes(): any[] | undefined {
return undefined;
}

static defaultRoutes():Routes {
return {
'/': {
Expand Down
79 changes: 68 additions & 11 deletions test/mongo.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ chai.use(spies);

describe('mongo', function() {

const md = new mongo.Mongodoki();
const md = new mongo.Mongodoki({ containerName: 'arrest-test', reuse: true });

before(async function() {
this.timeout(0);
Expand All @@ -30,10 +30,22 @@ describe('mongo', function() {

describe('constructor', function() {

const collectionName = 'arrest_test';
let db;

afterEach(function() {
db.then(db => db.close());
afterEach(async function() {
let _db;
if (db) {
try {
_db = await db;
await _db.dropCollection(collectionName);
} catch (err) {

} finally {
await _db.close();
db = undefined;
}
}
});

it('should connect to a mongodb via a connection uri', function() {
Expand All @@ -47,10 +59,7 @@ describe('mongo', function() {

it('should fail to connect to a wrong connection uri', function() {
let r = new MongoResource('mongodb://localhost:57017/local', { name: 'Test' });
db = r.db;
return r.db.then(() => {
should.fail();
}, err => true);
return r.db.should.be.rejected;
});

it('should use an existing valid db connection', function() {
Expand All @@ -63,10 +72,7 @@ describe('mongo', function() {
it('should fail with an existing failed db connection', function() {
let c = mongo.MongoClient.connect('mongodb://localhost:57017/local');
let r = new MongoResource(c, { name: 'Test' });
db = r.db;
return r.db.then(() => {
should.fail();
}, err => true);
return r.db.should.be.rejected;
});

it('should use the specified collection and id parameters', function() {
Expand All @@ -78,6 +84,57 @@ describe('mongo', function() {
return r.db;
});

it('should create the required indexes', async function() {
let r = new MongoResource('mongodb://localhost:27017/local', { name: 'Test', collection: collectionName, id: 'b', idIsObjectId: false });
r.getIndexes = () => [
{
key: { a: 1 },
unique: true,
name: 'test1'
}
];

db = r.db;
let coll = await r.getCollection();
let indexes = await coll.indexes();
return indexes.length.should.equal(2);
});

it('should detect existing indexes matching the required ones', async function() {
let r = new MongoResource('mongodb://localhost:27017/local', { name: 'Test', collection: collectionName, id: 'b', idIsObjectId: false });
r.getIndexes = () => [
{
key: { a: 1 },
unique: true,
name: 'test2'
}
];

db = r.db;
let coll = (await db).collection(r.collection);
await coll.createIndex({ a: 1 }, { unique: true, name: 'test2' });

coll = await r.getCollection();
let indexes = await coll.indexes();
return indexes.length.should.equal(2);
});

it('should fail if an existing index has different options', async function() {
let r = new MongoResource('mongodb://localhost:27017/local', { name: 'Test', collection: collectionName, id: 'b', idIsObjectId: false });
r.getIndexes = () => [
{
key: { a: 1 },
unique: true,
name: 'test3'
}
];

db = r.db;
let coll = (await db).collection(r.collection);
await coll.createIndex({ a: 1 }, { name: 'test3' });
await r.getCollection().should.be.rejected;
});

});

});
Expand Down

0 comments on commit b653712

Please sign in to comment.