Skip to content

Commit

Permalink
Merge pull request #2 from DopplerLabs/feature/tests
Browse files Browse the repository at this point in the history
Feature/tests
  • Loading branch information
jacob-meacham committed Mar 29, 2017
2 parents fcd6c82 + d929131 commit 9e44795
Show file tree
Hide file tree
Showing 8 changed files with 2,007 additions and 51 deletions.
1 change: 1 addition & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"presets": ["node6", "stage-1"],
"plugins": [
"espower",
"add-module-exports"
],
"env": {
Expand Down
3 changes: 2 additions & 1 deletion .eslintrc.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
---
extends: [standard]
extends: [standard, plugin:ava/recommended]
plugins: [ava]
rules:
space-before-function-paren: ["error", "never"]
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ language: node_js
node_js:
- '6'
script: npm run build
after_success: npm run ci:coverage
39 changes: 38 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,39 @@
# parse-server-migrating-adapter
Parse Server file adapter for migrating between different adapters
[![Coverage Status](https://coveralls.io/repos/github/DopplerLabs/parse-server-migrating-adapter/badge.svg?branch=develop)](https://coveralls.io/github/DopplerLabs/parse-server-migrating-adapter?branch=develop)

[![Build Status](https://travis-ci.org/DopplerLabs/parse-server-migrating-adapter.svg?branch=develop)](https://travis-ci.org/DopplerLabs/parse-server-migrating-adapter)

Parse Server file adapter for migrating between different adapters.

# Quick Start
`$ npm install parse-server-migrating-adapter --save`

In your parse server index:
```
var ParseServer = require('parse-server').ParseServer;
var MigratingAdapter = require('parse-server-migrating-adapter')
var GridStoreAdapter = require('parse-server/lib/Adapters/Files/GridStoreAdapter').GridStoreAdapter
var S3Adapter = require('parse-server-s3-adapter')
var s3Adapter = new S3Adapter({
bucket: process.env.S3_BUCKET_NAME,
accessKey: process.env.AWS_ACCESS_KEY,
secretKey: process.env.AWS_SECRET,
region: 'us-east-1'
})
var fileAdapter = new MigratingAdapter(s3Adapter, [new GridStoreAdapter(process.env.DATABASE_URI)])
var api = new new ParseServer({
filesAdapter: fileAdapter
})
```

# Implementation
The adapter takes a main adapter, which is what is used to create any new files. It also takes a list of old adapters. When requesting a file, the main adapter is searched first, and then the old adapters are searched. If the file is found in an old adapter, it is stored on the main adapter.

# Contributing
This projects follows [standardjs](https://standardjs.com/). We also try to maintain 100% real test coverage. When submitting a PR, make sure that there is an accompanying test, and that `npm run build` is clean.

# TODO
* Support for file streaming
19 changes: 16 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "parse-server-migrating-adapter",
"version": "0.1.0",
"version": "1.0.0",
"engines": {
"node": ">=6.0"
},
Expand All @@ -23,14 +23,19 @@
],
"scripts": {
"lint": "eslint .",
"test": "nyc ava",
"test:watch": "ava --watch",
"build:node": "cross-env BABEL_ENV=production babel src --out-dir lib",
"build": "npm run lint && npm run build:node"
"build": "npm run lint && npm run test && npm run build:node",
"ci:coverage": "nyc report --reporter=text-lcov | coveralls"
},
"dependencies": {
"bluebird": "3.5.0",
"bluebird-settle": "1.0.2"
"bluebird-settle": "1.0.2",
"sinon": "^2.1.0"
},
"devDependencies": {
"ava": "0.17.0",
"babel-cli": "6.18.0",
"babel-core": "6.21.0",
"babel-eslint": "7.1.1",
Expand All @@ -42,8 +47,16 @@
"cross-env": "3.1.4",
"eslint": "3.13.1",
"eslint-config-standard": "6.2.1",
"eslint-plugin-ava": "4.0.1",
"eslint-plugin-promise": "3.4.0",
"eslint-plugin-standard": "2.0.1",
"nyc": "10.0.0",
"rimraf": "2.5.4"
},
"ava": {
"require": [
"babel-register"
],
"babel": "inherit"
}
}
9 changes: 5 additions & 4 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,26 +42,27 @@ export default class MigratingAdapter {
return Promise.settle(promises)
.then((results) => {
for (const result of results) {
if (result.isResolved()) {
if (result.isFulfilled()) {
return Promise.resolve(result.value())
}
}

// None were resolved
return Promise.reject(results.result[0].reason())
return Promise.reject(results[0].reason())
})
}

getFileData(filename) {
return this.mainAdapter.getFileData(filename)
.catch(err => {
const promises = this.oldAdapters.map((adapter) => {
return Promise.resolve(adapter.getFileData(filename))
return adapter.getFileData(filename)
})

return Promise.any(promises)
.then(result => {
return this.createFile(filename, result)
this.createFile(filename, result)
return result
}).catch(() => {
return err
})
Expand Down
160 changes: 160 additions & 0 deletions test/index.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import sinon from 'sinon'
import test from 'ava'
import MigratingAdapter from '../src'

class StubAdapter {
createFile(filename, data) { }
deleteFile(filename) { }
getFileData(filename) { }
getFileLocation(config, filename) { }
}

test.beforeEach((t) => {
t.context.sandbox = sinon.sandbox.create()
t.context.mainAdapter = new StubAdapter()
t.context.oldAdapters = [new StubAdapter(), new StubAdapter()]
t.context.migratingAdapter = new MigratingAdapter(
t.context.mainAdapter, t.context.oldAdapters)
})

test.afterEach.always((t) => {
t.context.sandbox.restore()
})

test('constructor#requiresMainAdapter', t => {
try {
new MigratingAdapter(null, t.context.oldAdapters) // eslint-disable-line
} catch (err) {
t.is(err.message, 'Main adapter required')
}
})

test('constructor#requiresOldAdapter', t => {
try {
new MigratingAdapter(t.context.mainAdapter, null) // eslint-disable-line
} catch (err) {
t.is(err.message, 'At least one old adapter is required')
}

try {
new MigratingAdapter(t.context.mainAdapter, []) // eslint-disable-line
} catch (err) {
t.is(err.message, 'At least one old adapter is required')
}
})

test('createFile#createsInMainAdapter', t => {
const migratingAdapter = t.context.migratingAdapter
const sandbox = t.context.sandbox

const createFileSpy = sandbox.spy(t.context.mainAdapter, 'createFile')

migratingAdapter.createFile('foo', 'bar')
t.true(createFileSpy.calledOnce)
t.true(createFileSpy.calledWith('foo', 'bar'))
})

test('getFileData#returnsFromMainAdapter', t => {
const migratingAdapter = t.context.migratingAdapter
const sandbox = t.context.sandbox

const mainGetFileStub = sandbox.stub(t.context.mainAdapter, 'getFileData').resolves('myData')
const oldGetFileSpy = sandbox.spy(t.context.oldAdapters[0], 'getFileData')

migratingAdapter.getFileData('foo')
t.true(mainGetFileStub.calledOnce)
t.true(mainGetFileStub.calledWith('foo'))
t.true(oldGetFileSpy.notCalled)
})

test('getFileData#getsFromOldAdapters', async t => {
const migratingAdapter = t.context.migratingAdapter
const sandbox = t.context.sandbox

sandbox.stub(t.context.mainAdapter, 'getFileData').rejects()
const oldStubs = [
sandbox.stub(t.context.oldAdapters[0], 'getFileData').rejects('wtf'),
sandbox.stub(t.context.oldAdapters[1], 'getFileData').resolves('myData')
]

const fileData = await migratingAdapter.getFileData('foo')
t.is(fileData, 'myData')

for (const oldStub of oldStubs) {
t.true(oldStub.calledOnce)
t.true(oldStub.calledWith('foo'))
}
})

test('getFileData#storesToMainAdapter', async t => {
const migratingAdapter = t.context.migratingAdapter
const sandbox = t.context.sandbox

const createFileSpy = sandbox.spy(migratingAdapter, 'createFile')
sandbox.stub(t.context.mainAdapter, 'getFileData').rejects()
sandbox.stub(t.context.oldAdapters[0], 'getFileData').resolves('myData')
sandbox.stub(t.context.oldAdapters[1], 'getFileData').rejects()

await migratingAdapter.getFileData('foo')

t.true(createFileSpy.calledOnce)
t.true(createFileSpy.calledWith('foo', 'myData'))
})

test('getFileData#throwsIfNotFound', async t => {
const migratingAdapter = t.context.migratingAdapter
const sandbox = t.context.sandbox

const rejectingError = { err: 'Not Found' }
sandbox.stub(t.context.mainAdapter, 'getFileData').rejects(rejectingError)
sandbox.stub(t.context.oldAdapters[0], 'getFileData').rejects()
sandbox.stub(t.context.oldAdapters[1], 'getFileData').rejects()

try {
await migratingAdapter.getFileData('foo')
} catch (err) {
t.is(err, rejectingError)
}
})

test('getFileLocation#returnsMainAdapterLocation', t => {
const migratingAdapter = t.context.migratingAdapter
const sandbox = t.context.sandbox

sandbox.stub(t.context.mainAdapter, 'getFileLocation').returns('mainFileLocation')

t.is(migratingAdapter.getFileLocation({}, 'foo'), 'mainFileLocation')
})

test('deleteFile#deletesFromAllAdapters', async t => {
const migratingAdapter = t.context.migratingAdapter
const sandbox = t.context.sandbox

const deleteStubs = [
sandbox.stub(t.context.mainAdapter, 'deleteFile').resolves(),
sandbox.stub(t.context.oldAdapters[0], 'deleteFile').rejects(),
sandbox.stub(t.context.oldAdapters[1], 'deleteFile').resolves()
]

await migratingAdapter.deleteFile('foo')
for (const stub of deleteStubs) {
t.true(stub.calledOnce)
t.true(stub.calledWith('foo'))
}
})

test('deleteFile#throwsIfNotDeletedFromAnyAdapter', async t => {
const migratingAdapter = t.context.migratingAdapter
const sandbox = t.context.sandbox

const rejectingError = { err: 'Not Found' }
sandbox.stub(t.context.mainAdapter, 'deleteFile').rejects(rejectingError)
sandbox.stub(t.context.oldAdapters[0], 'deleteFile').rejects()
sandbox.stub(t.context.oldAdapters[1], 'deleteFile').rejects()

try {
await migratingAdapter.deleteFile('foo')
} catch (err) {
t.is(err, rejectingError)
}
})

0 comments on commit 9e44795

Please sign in to comment.