Skip to content

Commit 3f5ae2a

Browse files
authored
Merge pull request #14 from relekang/move-flat-file-db-to-adapter
Add db adapters
2 parents 5ea0c93 + f29ae12 commit 3f5ae2a

File tree

9 files changed

+412
-50
lines changed

9 files changed

+412
-50
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ See [`server-setup.md`](./server-setup.md) for instructions on acquiring a serve
2424

2525
> **Note**: You can pass any option to the `micro-analytics` command that you can pass to [`micro`](https://github.com/zeit/micro). As an example, to change the host you'd do `micro-analytics -H 127.0.0.1`
2626
27+
### Database adapters
28+
29+
micro-analytics supports custom database adapters. They can be configured with the environment
30+
variable `DB_ADAPTER`. Setting it to `redis` will make it require `micro-analytics-adapter-redis`.
31+
Leaving it unset will make micro-analytics use the builtin flat-file-db adapter.
32+
2733
## Usage
2834

2935
### Tracking views

src/db.js

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,28 @@
1-
const flatfile = require('flat-file-db')
21
const promise = require('promise')
32

4-
const db = flatfile.sync(process.env.DB_NAME || 'views.db')
3+
let adapter
54

6-
const promisifiedDb = {
7-
// Promisify async operations
8-
put: promise.denodeify(db.put.bind(db)),
9-
del: promise.denodeify(db.del.bind(db)),
10-
clear: promise.denodeify(db.clear.bind(db)),
5+
const repeatCharacter = (char, n) => `${Array(n + 1).join(char)}`
116

12-
get: db.get.bind(db),
13-
has: db.has.bind(db),
14-
keys: db.keys.bind(db),
15-
close: db.close.bind(db),
7+
if (process.env.DB_ADAPTER) {
8+
const adapterName = `micro-analytics-adapter-${process.env.DB_ADAPTER}`
9+
try {
10+
adapter = require(adapterName)
11+
} catch (err) {
12+
if (err.code === 'MODULE_NOT_FOUND') {
13+
// Console.error a warning message, but normally exit the process to avoid printing ugly npm ERR lines and stack trace.
14+
console.error(`\n${repeatCharacter(' ', 22)}⚠️ ERROR ⚠️\n${repeatCharacter('-', 55)}\nYou specified "${process.env.DB_ADAPTER}" as the DB_ADAPTER, but no package\ncalled "${adapterName}" was found.\n\nPlease make sure you spelled the name correctly and\nhave "npm install"ed the necessary adapter package!\n${repeatCharacter('-', 55)}\n`)
15+
process.exit(0)
16+
}
17+
}
18+
} else {
19+
adapter = require('./flat-file-adapter')
1620
}
1721

18-
module.exports = promisifiedDb
22+
module.exports = {
23+
get: adapter.get,
24+
getAll: adapter.getAll,
25+
put: adapter.put,
26+
has: adapter.has,
27+
keys: adapter.keys,
28+
}

src/flat-file-adapter.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
const flatfile = require('flat-file-db')
2+
const promise = require('promise')
3+
4+
const db = flatfile.sync(process.env.DB_NAME || 'views.db')
5+
6+
module.exports = {
7+
put: promise.denodeify(db.put.bind(db)),
8+
9+
has: (key) => Promise.resolve(db.has(key)),
10+
get: (key, options) => {
11+
const value = db.get(key) || { views: [] }
12+
13+
return {
14+
views: value.views.filter(view => {
15+
if (options && options.before && view.time > options.before) return false
16+
if (options && options.after && view.time < options.after) return false
17+
return true
18+
})
19+
}
20+
},
21+
keys: () => Promise.resolve(db.keys()),
22+
23+
getAll: async function getAll(options) {
24+
const data = {}
25+
const keys = await module.exports.keys()
26+
27+
keys
28+
.filter(key => key.startsWith(options.pathname))
29+
.forEach((key) => {
30+
data[key] = module.exports.get(key, { before: options.before, after: options.after })
31+
})
32+
33+
return data
34+
}
35+
}

src/index.js

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,21 @@ module.exports = async function (req, res) {
99
res.setHeader('Access-Control-Allow-Origin', '*')
1010
// Send all views down if "?all" is true
1111
if (String(query.all) === 'true') {
12-
const data = {
13-
data: {},
14-
time: Date.now()
12+
try {
13+
const data = {
14+
data: await db.getAll({
15+
pathname: pathname,
16+
before: parseInt(query.before, 10),
17+
after: parseInt(query.after, 10),
18+
}),
19+
time: Date.now()
20+
}
21+
send(res, 200, data)
22+
return
23+
} catch (err) {
24+
console.log(err)
25+
throw createError(500, 'Internal server error.')
1526
}
16-
for (let key of db.keys().filter(key => String(query.filter) === 'false' ? true : key.startsWith(pathname))) {
17-
data.data[key] = db.get(key)
18-
}
19-
send(res, 200, data)
20-
return
2127
}
2228
// Check that a page is provided
2329
if (pathname.length <= 1) {
@@ -28,7 +34,7 @@ module.exports = async function (req, res) {
2834
}
2935
const shouldIncrement = String(query.inc) !== 'false'
3036
try {
31-
const currentViews = db.has(pathname) ? db.get(pathname).views.length : 0
37+
const currentViews = await db.has(pathname) ? (await db.get(pathname)).views.length : 0
3238
// Add a view and send the total views back to the client
3339
if (shouldIncrement) {
3440
await pushView(pathname, { time: Date.now() })

src/utils.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ const pushView = async (key, view) => {
1010
if (locks[key]) return setImmediate(async () => { await push() })
1111
locks[key] = true
1212

13-
const views = db.has(key)
14-
? db.get(key).views
13+
const views = await db.has(key)
14+
? (await db.get(key)).views
1515
: []
1616

1717
try {

tests/atomicity.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const request = require('request-promise')
22
const { listen, mockDb } = require('./utils')
33

4-
jest.mock('../src/db', () => mockDb)
4+
jest.mock('flat-file-db', () => mockDb)
55
const service = require('../src')
66
let url
77

tests/items.test.js

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const request = require('request-promise')
22
const { listen, mockDb } = require('./utils')
33

4-
jest.mock('../src/db', () => mockDb)
4+
jest.mock('flat-file-db', () => mockDb)
55
const service = require('../src')
66
let url
77

@@ -91,12 +91,26 @@ describe('all', () => {
9191
expect(body.data['/rover2'].views.length).toBe(1)
9292
})
9393

94-
it('should not filter if filter is set to false', async () => {
95-
await request(`${url}/rover`)
96-
await request(`${url}/rover2`)
97-
await request(`${url}/route`)
98-
const body = JSON.parse(await request(`${url}/rover?all=true&filter=false`))
99-
expect(Object.keys(body.data).length).toBe(3)
94+
it('should filter based on before after', async () => {
95+
const after = new Date('2017-01-01T09:11:00.000Z').getTime()
96+
const before = new Date('2017-01-01T09:41:00.000Z').getTime()
97+
98+
mockDb._put('/rover', { views: [
99+
{ time: new Date('2017-01-01T09:00:00.000Z').getTime() },
100+
{ time: new Date('2017-01-01T09:10:00.000Z').getTime() },
101+
{ time: new Date('2017-01-01T09:20:00.000Z').getTime() },
102+
{ time: new Date('2017-01-01T09:30:00.000Z').getTime() },
103+
{ time: new Date('2017-01-01T09:40:00.000Z').getTime() },
104+
{ time: new Date('2017-01-01T09:50:00.000Z').getTime() },
105+
]})
106+
107+
const mapToIsoString = view => new Date(view.time).toISOString()
108+
const body = JSON.parse(await request(`${url}/rover?all=true&before=${before}&after=${after}`))
109+
expect(body.data['/rover'].views.map(mapToIsoString)).toEqual([
110+
'2017-01-01T09:20:00.000Z',
111+
'2017-01-01T09:30:00.000Z',
112+
'2017-01-01T09:40:00.000Z'
113+
])
100114
})
101115
})
102116
})

tests/utils.js

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,22 @@ const DB = () => {
66
let DELAY = 1
77

88
return {
9-
get: (key) => data[key],
10-
put: (key, val) => new Promise((res, rej) => {
11-
setTimeout(() => {
12-
data[key] = val
13-
res()
14-
}, DELAY)
9+
sync: () => ({
10+
get: (key) => data[key],
11+
put: (key, val, cb) => {
12+
setTimeout(() => {
13+
data[key] = val
14+
cb()
15+
}, DELAY)
16+
},
17+
has: (key) => ({}.hasOwnProperty.call(data, key)),
18+
keys: () => Object.keys(data),
1519
}),
16-
has: (key) => !!data[key],
17-
keys: () => Object.keys(data),
20+
1821
// Custom methods used in tests
1922
_reset: () => { data = {} },
20-
_setDelay: (ms) => { DELAY = ms || 1 }
23+
_setDelay: (ms) => { DELAY = ms || 1 },
24+
_put: (key, value) => { data[key] = value }
2125
}
2226
}
2327

0 commit comments

Comments
 (0)