Skip to content

Commit

Permalink
feat: add docKeys and getProps/getDocument to BaseEntity
Browse files Browse the repository at this point in the history
  • Loading branch information
vinz243 committed Apr 20, 2018
1 parent 3261413 commit c4b05b7
Show file tree
Hide file tree
Showing 10 changed files with 251 additions and 26 deletions.
20 changes: 19 additions & 1 deletion src/decorators/SlothEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,27 @@ import StaticData from '../models/StaticData'
import PouchFactory from '../models/PouchFactory'
import EntityConstructor from '../helpers/EntityConstructor'
import getProtoData from '../utils/getProtoData'
import ProtoData from '../models/ProtoData'

const slug = require('limax')

function mapPropsOrDocToDocument({ fields }: ProtoData, data: any) {
return fields.reduce(
(props, { key, docKey }) => {
if (!(key in data) && !(docKey in data)) {
return props
}
if (key in data && docKey in data && key !== docKey) {
throw new Error(`Both '${key}' and '${docKey}' exist on ${data}`)
}
return Object.assign({}, props, {
[docKey]: key in data ? data[key] : data[docKey]
})
},
{} as any
)
}

/**
* This decorator is used to mark classes that will be an entity, a document
* This function, by extending the constructor and defining this.sloth property
Expand Down Expand Up @@ -44,7 +62,7 @@ export default function SlothEntity<S extends { _id: string }>(name: string) {
this.sloth = {
name,
updatedProps: {},
props: idOrProps,
props: mapPropsOrDocToDocument(getProtoData(this), idOrProps),
docId: idOrProps._id,
factory,
slug
Expand Down
17 changes: 10 additions & 7 deletions src/decorators/SlothField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ import getProtoData from '../utils/getProtoData'
* - mutating the value will update it in updatedProps
* - accessing the value will first look into updatedProps, then props and then default values
* @typeparam T the value type
* @param docKeyName specifies the document key name, default to prop key name
*/
export default function SlothField<T>() {
export default function SlothField<T>(docKeyName?: string) {
return function(this: any, target: object, key: string) {
const docKey = docKeyName || key

const desc = Reflect.getOwnPropertyDescriptor(target, key)
let defaultValue: T

Expand All @@ -24,18 +27,18 @@ export default function SlothField<T>() {

const data = getProtoData(target, true)

data.fields.push({ key })
data.fields.push({ key, docKey })

Reflect.deleteProperty(target, key)

Reflect.defineProperty(target, key, {
get: function(): T {
const { updatedProps, props } = getSlothData(this)
if (key in updatedProps) {
return (updatedProps as any)[key]
if (docKey in updatedProps) {
return (updatedProps as any)[docKey]
}
if (key in props) {
return (props as any)[key]
if (docKey in props) {
return (props as any)[docKey]
}
return defaultValue
},
Expand All @@ -48,7 +51,7 @@ export default function SlothField<T>() {
return
}

Object.assign(getSlothData(this).updatedProps, { [key]: value })
Object.assign(getSlothData(this).updatedProps, { [docKey]: value })
}
})
}
Expand Down
2 changes: 1 addition & 1 deletion src/decorators/SlothRel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default function SlothRel(rel: RelationDescriptor) {

const { fields, rels } = getProtoData(target, true)

fields.push({ key })
fields.push({ key, docKey: key })
rels.push({ ...rel, key })

Reflect.deleteProperty(target, key)
Expand Down
2 changes: 1 addition & 1 deletion src/decorators/SlothURI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export default function SlothURI<S>(prefix: string, ...propsKeys: (keyof S)[]) {
propsKeys
})

fields.push({ key })
fields.push({ key, docKey: key })

Reflect.defineProperty(target, key, {
get: function() {
Expand Down
43 changes: 32 additions & 11 deletions src/models/BaseEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,32 @@ export default class BaseEntity<S> {
return Object.keys(updatedProps).length > 0 || docId == null
}

/**
* Returns a list of props following the entity schema
*/
getProps(): S {
const { fields } = getProtoData(this)
return fields.reduce(
(props, { key }) => {
return Object.assign({}, props, { [key]: (this as any)[key] })
},
{} as any
)
}

/**
* Returns a list of props mapped with docKey
*/
getDocument() {
const { fields } = getProtoData(this)
return fields.reduce(
(props, { key, docKey }) => {
return Object.assign({}, props, { [docKey]: (this as any)[key] })
},
{} as any
)
}

/**
* Saves document to database. If the document doesn't exist,
* create it. If it exists, update it. If the _id was changed
Expand All @@ -39,15 +65,10 @@ export default class BaseEntity<S> {
*/
async save(): Promise<S & { _rev?: string }> {
const { fields } = getProtoData(this, false)

const props: S = fields
.map(({ key }) => {
return { [key]: (this as any)[key] }
})
.reduce((acc, val) => ({ ...acc, ...val }), {}) as any
const doc = this.getDocument()

if (!this.isDirty()) {
return props as S
return doc
}

const { factory, name, docId } = getSlothData(this)
Expand All @@ -56,11 +77,11 @@ export default class BaseEntity<S> {
try {
const { _rev } = await db.get(this._id)

const { rev } = await db.put(Object.assign({}, props, { _rev }))
const { rev } = await db.put(Object.assign({}, doc, { _rev }))

getSlothData(this).docId = this._id

return Object.assign({}, props, { _rev: rev })
return Object.assign({}, doc, { _rev: rev })
} catch (err) {
// Then document was not found

Expand All @@ -72,11 +93,11 @@ export default class BaseEntity<S> {

getSlothData(this).docId = this._id
}
const { rev, id } = await db.put(props)
const { rev, id } = await db.put(doc)

getSlothData(this).docId = this._id

return Object.assign({}, props, { _rev: rev })
return Object.assign({}, doc, { _rev: rev })
}

throw err
Expand Down
1 change: 1 addition & 0 deletions src/models/ProtoData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export default interface ProtoData {
*/
fields: {
key: string
docKey: string
}[]

views: {
Expand Down
83 changes: 83 additions & 0 deletions test/integration/docKeys.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import PouchDB from 'pouchdb'
import {
SlothDatabase,
SlothURI,
BaseEntity,
SlothEntity,
SlothField
} from '../../src/slothdb'

PouchDB.plugin(require('pouchdb-adapter-memory'))

describe('docKeys', () => {
let prefix: string

const factory = (pre: string) => (name: string) =>
new PouchDB(pre + name, { adapter: 'memory' })

interface IFoo {
_id: string
name: string
bar: string
}

@SlothEntity('foos')
class FooEntity extends BaseEntity<IFoo> {
@SlothURI('foos', 'name', 'bar')
_id: string
@SlothField() name: string
@SlothField('barz') bar: string
}

const Foo = new SlothDatabase<IFoo, FooEntity>(FooEntity)

beforeEach(() => {
prefix = '__' + Date.now().toString(16) + '_'
})

test('getProps maps props', () => {
const doc = Foo.create(factory(prefix), { name: 'Foo Bar', bar: 'barz' })

expect(doc.getProps()).toEqual({
_id: 'foos/foo-bar/barz',
name: 'Foo Bar',
bar: 'barz'
})
})

test('getProps maps props from doc', () => {
const doc = Foo.create(factory(prefix), {
name: 'Foo Bar',
barz: 'barz'
} as any)

expect(doc.getProps()).toEqual({
_id: 'foos/foo-bar/barz',
name: 'Foo Bar',
bar: 'barz'
})
})

test('getDocument maps document', () => {
const doc = Foo.create(factory(prefix), { name: 'Foo Bar', bar: 'barz' })

expect(doc.getDocument()).toEqual({
_id: 'foos/foo-bar/barz',
name: 'Foo Bar',
barz: 'barz'
})
})

test('getDocument maps document from doc', () => {
const doc = Foo.create(factory(prefix), {
name: 'Foo Bar',
barz: 'barz'
} as any)

expect(doc.getDocument()).toEqual({
_id: 'foos/foo-bar/barz',
name: 'Foo Bar',
barz: 'barz'
})
})
})
56 changes: 54 additions & 2 deletions test/unit/decorators/SlothEntity.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import SlothEntity from '../../../src/decorators/SlothEntity'
import localPouchFactory from '../../utils/localPouchFactory'
import emptyProtoData from '../../utils/emptyProtoData'

test('SlothEntity - attaches a sloth object to class', () => {
// tslint:disable-next-line:no-empty
Expand Down Expand Up @@ -31,7 +32,9 @@ test('SlothEntity - set the props when props are passed', () => {
const constr = () => {}

const wrapper = SlothEntity('foo')(constr as any)
const context: any = {}
const context: any = {
__protoData: emptyProtoData({ fields: [{ key: 'foo', docKey: 'foo' }] })
}

wrapper.call(context, localPouchFactory, { foo: 'bar' })

Expand All @@ -40,12 +43,61 @@ test('SlothEntity - set the props when props are passed', () => {
expect(context.sloth.props.foo).toBe('bar')
expect(context.sloth.docId).toBeUndefined()
})
test('SlothEntity - can use keys', () => {
// tslint:disable-next-line:no-empty
const constr = () => {}

const wrapper = SlothEntity('foo')(constr as any)
const context: any = {
__protoData: emptyProtoData({ fields: [{ key: 'foo', docKey: 'barz' }] })
}

wrapper.call(context, localPouchFactory, { foo: 'bar' })

expect(context.sloth).toBeDefined()
expect(context.sloth.name).toBe('foo')
expect(context.sloth.props.barz).toBe('bar')
expect(context.sloth.docId).toBeUndefined()
})
test('SlothEntity - can use docKeys', () => {
// tslint:disable-next-line:no-empty
const constr = () => {}

const wrapper = SlothEntity('foo')(constr as any)
const context: any = {
__protoData: emptyProtoData({ fields: [{ key: 'foo', docKey: 'barz' }] })
}

wrapper.call(context, localPouchFactory, { barz: 'bar' })

expect(context.sloth).toBeDefined()
expect(context.sloth.name).toBe('foo')
expect(context.sloth.props.barz).toBe('bar')
expect(context.sloth.docId).toBeUndefined()
})
test('SlothEntity - throws an error when both keys and docKeys maps are passed', () => {
// tslint:disable-next-line:no-empty
const constr = () => {}

const wrapper = SlothEntity('foo')(constr as any)
const context: any = {
__protoData: emptyProtoData({ fields: [{ key: 'foo', docKey: 'barz' }] })
}

expect(() =>
wrapper.call(context, localPouchFactory, { barz: 'bar', foo: 'barz' })
).toThrowError(/Both 'foo' and 'barz' exist/)
})
test('SlothEntity - eventually set docId when props are passed with _id', () => {
// tslint:disable-next-line:no-empty
const constr = () => {}

const wrapper = SlothEntity('foo')(constr as any)
const context: any = {}
const context: any = {
__protoData: emptyProtoData({
fields: [{ key: 'foo', docKey: 'foo' }, { key: '_id', docKey: '_id' }]
})
}

wrapper.call(context, localPouchFactory, { _id: 'foobar', foo: 'bar' })

Expand Down
2 changes: 1 addition & 1 deletion test/unit/decorators/SlothURI.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ test('SlothURI - pushes to uris', () => {
}
])

expect(__protoData.fields).toEqual([{ key: '_id' }])
expect(__protoData.fields).toEqual([{ key: '_id', docKey: '_id' }])
})

test('SlothURI - throws if on top of another decorator', () => {
Expand Down
Loading

0 comments on commit c4b05b7

Please sign in to comment.