8000 feat: add SlothView and SlothIndex · compactd/slothdb@3a5e0f8 · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Commit 3a5e0f8

Browse files
committed
feat: add SlothView and SlothIndex
1 parent 67fe0f2 commit 3a5e0f8

File tree

11 files changed

+392
-5
lines changed

11 files changed

+392
-5
lines changed

src/decorators/SlothIndex.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import BaseEntity from '../models/BaseEntity'
2+
import getSlothData from '../utils/getSlothData'
3+
import { join } from 'path'
4+
import getProtoData from '../utils/getProtoData'
5+
import SlothView from './SlothView'
6+
7+
/**
8+
* Creates an index for a field. It's a view function that simply emits
9+
* the document key
10+
*
11+
* @see [[SlothDatabase.queryDocs]]
12+
* @export
13+
* @template S
14+
* @param {(doc: S, emit: Function) => void} fn the view function, as arrow or es5 function
15+
* @param {string} [docId='views'] the _design document identifier
16+
* @param {string} [viewId] the view identifier, default by_<property name>
17+
* @returns the decorator to apply on the field
18+
*/
19+
export default function SlothIndex<S, V extends string = string>(
20+
viewId?: V,
21+
docId?: string
22+
) {
23+
return (target: object, key: string) => {
24+
SlothView(
25+
new Function('doc', 'emit', `emit(doc['${key}'].toString());`) as any,
26+
viewId,
27+
docId
28+
)(target, key)
29+
}
30+
}

src/decorators/SlothView.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import BaseEntity from '../models/BaseEntity'
2+
import getSlothData from '../utils/getSlothData'
3+
import { join } from 'path'
4+
import getProtoData from '../utils/getProtoData'
5+
6+
/**
7+
* Creates a view for a field. This function does not modify the
8+
* behavior of the current field, hence requires another decorator
9+
* such as SlothURI or SlothField. The view will be created by the SlothDatabase
10+
*
11+
* @export
12+
* @template S
13+
* @param {(doc: S, emit: Function) => void} fn the view function, as arrow or es5 function
14+
* @param {string} [docId='views'] the _design document identifier
15+
* @param {string} [viewId] the view identifier, default by_<property name>
16+
* @returns the decorator to apply on the field
17+
*/
18+
export default function SlothView<S, V extends string = string>(
19+
fn: (doc: S, emit: Function) => void,
20+
viewId?: V,
21+
docId = 'views'
22+
) {
23+
return (target: object, key: string) => {
24+
const desc = Reflect.getOwnPropertyDescriptor(target, key)
25+
26+
if (desc) {
27+
if (!desc.get && !desc.set) {
28+
throw new Error('Required SlothView on top of another decorator')
29+
}
30+
}
31+
32+
const fun = `function (__doc) {
33+
(${fn.toString()})(__doc, emit);
34+
}`
35+
36+
const { views } = getProtoData(target, true)
37+
38+
views.push({
39+
id: docId,
40+
name: viewId || `by_${key}`,
41+
function: fn,
42+
code: fun
43+
})
44+
}
45+
}

src/models/ProtoData.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,12 @@ export default interface ProtoData {
2727
key: string
2828
}[]
2929

30+
views: {
31+
id: string
32+
name: string
33+
function: Function
34+
code: string
35+
}[]
36+
3037
rels: (RelationDescriptor & { key: string })[]
3138
}

src/models/SlothDatabase.ts

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@ import { join } from 'path'
1010
*
1111
* @typeparam S the database schema
1212
* @typeparam E the Entity
13-
* @typeparam T the entity constructor
13+
* @typeparam V the (optional) view type that defines a list of possible view IDs
1414
*/
15-
export default class SlothDatabase<S, E extends BaseEntity<S>> {
15+
export default class SlothDatabase<
16+
S,
17+
E extends BaseEntity<S>,
18+
V extends string = never
19+
> {
1620
_root: string
1721
/**
1822
*
@@ -53,6 +57,31 @@ export default class SlothDatabase<S, E extends BaseEntity<S>> {
5357
}
5458
}
5559

60+
/**
61+
* Queries and maps docs to Entity objects
62+
*
63+
* @param factory the pouch factory
64+
* @param view the view identifier
65+
* @param startKey the optional startkey
66+
* @param endKey the optional endkey
67+
*/
68+
queryDocs(
69+
factory: PouchFactory<S>,
70+
view: V,
71+
startKey = '',
72+
endKey = join(startKey, '\uffff')
73+
) {
74+
return factory(this._name)
75+
.query(view, {
76+
startkey: startKey,
77+
endkey: endKey,
78+
include_docs: true
79+
})
80+
.then(({ rows }) => {
81+
return rows.map(({ doc }) => new this._model(factory, doc as any))
82+
})
83+
}
84+
5685
/**
5786
* Returns a database that will only find entities with _id
5887
* starting with the root path
@@ -141,6 +170,17 @@ export default class SlothDatabase<S, E extends BaseEntity<S>> {
141170
return new this._model(factory, props)
142171
}
143172

173+
/**
174+
* Create a new model instance and save it to database
175+
* @param factory The database factory to attach to the model
176+
* @param props the entity properties
177+
* @returns an entity instance
178+
*/
179+
put(factory: PouchFactory<S>, props: Partial<S>) {
180+
const doc = new this._model(factory, props)
181+
return doc.save().then(() => doc)
182+
}
183+
144184
/**
145185
* Subscribes a function to PouchDB changes, so that
146186
* the function will be called when changes are made
@@ -205,11 +245,62 @@ export default class SlothDatabase<S, E extends BaseEntity<S>> {
205245
}
206246
}
207247

248+
/**
249+
* Creates view documents (if required)
250+
* @param factory
251+
*/
252+
async initSetup(factory: PouchFactory<S>) {
253+
await this.setupViews(factory)
254+
}
255+
208256
protected getSubscriberFor(factory: PouchFactory<S>) {
209257
return this._subscribers.find(el => el.factory === factory)
210258
}
211259

212260
protected dispatch(action: ChangeAction<S>) {
213261
this._subscribers.forEach(({ sub }) => sub(action))
214262
}
263+
264+
private setupViews(factory: PouchFactory<S>): Promise<void> {
265+
const { views } = getProtoData(this._model.prototype)
266+
const db = factory(this._name)
267+
268+
const promises = views.map(({ name, id, code }) => async () => {
269+
const views = {}
270+
let _rev
271+
272+
try {
273+
const doc = (await db.get(`_design/${id}`)) as any
274+
275+
if (doc.views[name] && doc.views[name].map === code) {
276+
// view already exists and is up-to-date
277+
return
278+
}
279+
280+
Object.assign(views, doc.views)
281+
282+
_rev = doc._rev
283+
} catch (err) {
284+
// Do nothing
285+
}
286+
287+
await db.put(Object.assign(
288+
{},
289+
{
290+
_id: `_design/${id}`,
291+
views: {
292+
...views,
293+
[name]: {
294+
map: code
295+
}
296+
}
297+
},
298+
_rev ? { _rev } : {}
299+
) as any)
300+
})
301+
302+
return promises.reduce((acc, fn) => {
303+
return acc.then(() => fn())
304+
}, Promise.resolve())
305+
}
215306
}

src/slothdb.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import PouchFactory from './models/PouchFactory'
1212
import { belongsToMapper } from './utils/relationMappers'
1313
import SlothDatabase from './models/SlothDatabase'
14+
import SlothView from './decorators/SlothView'
1415

1516
export {
1617
SlothEntity,
@@ -25,5 +26,6 @@ export {
2526
BelongsToDescriptor,
2627
HasManyDescriptor,
2728
SlothDatabase,
29+
SlothView,
2830
belongsToMapper
2931
}

src/utils/getProtoData.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ export default function getProtoData(
1919
wrapped.__protoData = {
2020
uris: [],
2121
fields: [],
22-
rels: []
22+
rels: [],
23+
views: []
2324
}
2425
} else {
2526
throw new Error(`Object ${wrapped} has no __protoData`)

test/integration/Track.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
SlothURI,
66
SlothField,
77
SlothRel,
8+
SlothView,
89
belongsToMapper
910
} from '../../src/slothdb'
1011
import Artist from './Artist'
@@ -17,6 +18,12 @@ export interface TrackSchema {
1718
artist: string
1819
album: string
1920
}
21+
22+
export enum TrackViews {
23+
ByArtist = 'by_artist',
24+
ByAlbum = 'views/by_album'
25+
}
26+
2027
const artist = belongsToMapper(() => Artist, 'album')
2128
const album = belongsToMapper(() => Album, 'artist')
2229

@@ -32,6 +39,7 @@ export class TrackEntity extends BaseEntity<TrackSchema> {
3239
@SlothRel({ belongsTo: () => Artist })
3340
artist: string = ''
3441

42+
@SlothView((doc: TrackSchema, emit) => emit(doc.album))
3543
@SlothRel({ belongsTo: () => Album })
3644
album: string = ''
3745

@@ -41,4 +49,6 @@ export class TrackEntity extends BaseEntity<TrackSchema> {
4149
}
4250
}
4351

44-
export default new SlothDatabase<TrackSchema, TrackEntity>(TrackEntity)
52+
export default new SlothDatabase<TrackSchema, TrackEntity, TrackViews>(
53+
TrackEntity
54+
)

test/integration/views.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import Artist from './Artist'
2+
import Track, { TrackViews } from './Track'
3+
import PouchDB from 'pouchdb'
4+
import delay from '../utils/delay'
5+
6+
PouchDB.plugin(require('pouchdb-adapter-memory'))
7+
8+
describe('views', () => {
9+
const prefix = Date.now().toString(26) + '_'
10+
11+
const factory = (name: string) =>
12+
new PouchDB(prefix + name, { adapter: 'memory' })
13+
14+
beforeAll(async () => {
15+
await Track.put(factory, {
16+
name: 'Palm Trees',
17+
artist: 'library/flatbush-zombies',
18+
album: 'library/flatbush-zombies/betteroffdead',
19+
number: '12'
20+
})
21+
await Track.put(factory, {
22+
name: 'Not Palm Trees',
23+
artist: 'library/not-flatbush-zombies',
24+
album: 'library/flatbush-zombies/betteroffdead-2',
25+
number: '12'
26+
})
27+
await Track.put(factory, {
28+
name: 'Mocking Bird',
29+
artist: 'library/eminem',
30+
album: 'library/eminem/some-album-i-forgot',
31+
number: '12'
32+
})
33+
})
34+
35+
test('create views', async () => {
36+
await Track.initSetup(factory)
37+
expect(await factory('tracks').get('_design/views')).toMatchObject({
38+
views: { by_album: {} }
39+
})
40+
})
41+
42+
test('query by view', async () => {
43+
const docs = await Track.queryDocs(
44+
factory,
45+
TrackViews.ByAlbum,
46+
'library/flatbush-zombies'
47+
)
48+
49+
expect(docs.length).toBe(2)
50+
})
51+
})
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { SlothView } from '../../../src/slothdb'
2+
import emptyProtoData from '../../utils/emptyProtoData'
3+
4+
test('SlothView - fails without a decorator', () => {
5+
const obj = { foo: 'bar' }
6+
expect(() => SlothView(() => ({}))(obj, 'foo')).toThrowError(
7+
/Required SlothView/
8+
)
9+
})
10+
11+
test('SlothView - generates a working function for es5 view', () => {
12+
const proto = emptyProtoData({})
13+
const obj = { __protoData: proto }
14+
15+
Reflect.defineProperty(obj, 'foo', { get: () => 42 })
16+
17+
SlothView(function(doc: { bar: string }, emit) {
18+
emit(doc.bar)
19+
})(obj, 'foo')
20+
21+
expect(proto.views).toHaveLength(1)
22+
23+
const { views } = proto
24+
const [{ id, name, code }] = views
25+
26+
expect(name).toBe('by_foo')
27+
28+
let fun: Function
29+
30+
const emit = jest.fn()
31+
32+
// tslint:disable-next-line:no-eval
33+
eval('fun = ' + code)
34+
fun({ bar: 'barz' })
35+
36+
expect(emit).toHaveBeenCalledWith('barz')
37+
})

0 commit comments

Comments
 (0)
0