From 97154856f99efd86ff295bd5ba5766fc660fb5d2 Mon Sep 17 00:00:00 2001 From: vinz243 Date: Fri, 20 Apr 2018 18:51:19 +0200 Subject: [PATCH 01/18] fix(sloth-index): use docKeys instead of keys --- src/decorators/SlothIndex.ts | 12 +++++++- src/decorators/SlothView.ts | 10 ++++-- test/integration/Track.ts | 2 +- test/integration/views.test.ts | 42 ++++++++++++++++++++++++++ test/unit/decorators/SlothView.test.ts | 14 ++++++++- 5 files changed, 75 insertions(+), 5 deletions(-) diff --git a/src/decorators/SlothIndex.ts b/src/decorators/SlothIndex.ts index f7ce8b7..4b4a8b6 100644 --- a/src/decorators/SlothIndex.ts +++ b/src/decorators/SlothIndex.ts @@ -21,8 +21,18 @@ export default function SlothIndex( docId?: string ) { return (target: object, key: string) => { + const field = getProtoData(target).fields.find(field => field.key === key) + + if (!field) { + throw new Error('Please use SlothIndex on top of a SlothField') + } + SlothView( - new Function('doc', 'emit', `emit(doc['${key}'].toString());`) as any, + new Function( + 'doc', + 'emit', + `emit(doc['${field.docKey}'].toString());` + ) as any, viewId, docId )(target, key) diff --git a/src/decorators/SlothView.ts b/src/decorators/SlothView.ts index 5fe891b..5472bc7 100644 --- a/src/decorators/SlothView.ts +++ b/src/decorators/SlothView.ts @@ -33,11 +33,17 @@ export default function SlothView( (${fn.toString()})(__doc, emit); }` - const { views } = getProtoData(target, true) + const { views, fields } = getProtoData(target, true) + + const field = fields.find(field => field.key === key) + + if (!field) { + throw new Error('Required SlothView on top of a SlothField') + } views.push({ id: docId, - name: viewId || `by_${key}`, + name: viewId || `by_${field.docKey}`, function: fn, code: fun }) diff --git a/test/integration/Track.ts b/test/integration/Track.ts index 5a58a63..fd057da 100644 --- a/test/integration/Track.ts +++ b/test/integration/Track.ts @@ -33,8 +33,8 @@ export class TrackEntity extends BaseEntity { @SlothURI('library', 'album', 'number', 'name') _id: string = '' - @SlothField() @SlothIndex() + @SlothField() name: string = 'Track Name' @SlothField() number: string = '00' diff --git a/test/integration/views.test.ts b/test/integration/views.test.ts index 50f37a1..68d961b 100644 --- a/test/integration/views.test.ts +++ b/test/integration/views.test.ts @@ -2,6 +2,14 @@ import Artist from './Artist' import Track, { TrackViews } from './Track' import PouchDB from 'pouchdb' import delay from '../utils/delay' +import { + SlothEntity, + BaseEntity, + SlothURI, + SlothField, + SlothDatabase, + SlothIndex +} from '../../src/slothdb' PouchDB.plugin(require('pouchdb-adapter-memory')) @@ -115,3 +123,37 @@ describe('views', () => { }) }) }) + +describe('views and docKeys', () => { + const prefix = Date.now().toString(26) + '_' + + const factory = (name: string) => + new PouchDB(prefix + name, { adapter: 'memory' }) + + @SlothEntity('foos') + class FooEnt extends BaseEntity { + @SlothURI('foos', 'name') + _id = '' + + @SlothIndex() + @SlothField('not_name') + name = '' + } + + const Foo = new SlothDatabase(FooEnt) + + test('uses docKeys', async () => { + await Foo.put(factory, { name: 'foo' }) + await Foo.put(factory, { name: 'foobar' }) + await Foo.put(factory, { name: 'bar' }) + + const keys = await Foo.queryKeys( + factory, + 'views/by_not_name', + 'foo', + 'foo\uffff' + ) + + expect(keys).toEqual(['foo', 'foobar']) + }) +}) diff --git a/test/unit/decorators/SlothView.test.ts b/test/unit/decorators/SlothView.test.ts index d865a4e..a237d8d 100644 --- a/test/unit/decorators/SlothView.test.ts +++ b/test/unit/decorators/SlothView.test.ts @@ -7,13 +7,25 @@ test('SlothView - fails without a decorator', () => { /Required SlothView/ ) }) - test('SlothView - generates a working function for es5 view', () => { const proto = emptyProtoData({}) const obj = { __protoData: proto } Reflect.defineProperty(obj, 'foo', { get: () => 42 }) + expect(() => + SlothView(function(doc: { bar: string }, emit) { + emit(doc.bar) + })(obj, 'foo') + ).toThrowError(/Required SlothView/) +}) + +test('SlothView - generates a working function for es5 view', () => { + const proto = emptyProtoData({ fields: [{ key: 'foo', docKey: 'foo' }] }) + const obj = { __protoData: proto } + + Reflect.defineProperty(obj, 'foo', { get: () => 42 }) + SlothView(function(doc: { bar: string }, emit) { emit(doc.bar) })(obj, 'foo') From a7204cf547931611309de9444a348da3eed9aaf6 Mon Sep 17 00:00:00 2001 From: vinz243 Date: Sat, 21 Apr 2018 17:42:26 +0200 Subject: [PATCH 02/18] feat: split idOrProps parsing between BaseEntity and SlothEntity allows the evalutation of default values during constructor per instance --- src/decorators/SlothEntity.ts | 41 +++----- src/decorators/SlothField.ts | 27 ++++-- src/helpers/EntityConstructor.ts | 7 +- src/models/BaseEntity.ts | 54 ++++++++++- src/models/SlothData.ts | 4 +- src/models/SlothDatabase.ts | 13 ++- src/models/StaticData.ts | 12 --- src/utils/getSlothData.ts | 3 +- src/utils/relationMappers.ts | 5 +- test/integration/object.test.ts | 1 + test/unit/decorators/SlothEntity.test.ts | 113 +---------------------- test/unit/decorators/SlothField.test.ts | 25 +++-- test/unit/models/BaseEntity.test.ts | 96 +++++++++++++++++++ test/unit/models/SlothDatabase.test.ts | 6 +- 14 files changed, 223 insertions(+), 184 deletions(-) delete mode 100644 src/models/StaticData.ts diff --git a/src/decorators/SlothEntity.ts b/src/decorators/SlothEntity.ts index eecbc02..a5076cc 100644 --- a/src/decorators/SlothEntity.ts +++ b/src/decorators/SlothEntity.ts @@ -1,14 +1,14 @@ import BaseEntity from '../models/BaseEntity' import SlothData from '../models/SlothData' -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) { + if (typeof data === 'string') { + return {} + } return fields.reduce( (props, { key, docKey }) => { if (!(key in data) && !(docKey in data)) { @@ -36,40 +36,21 @@ export default function SlothEntity(name: string) { return >(constructor: { new (factory: PouchFactory, idOrProps: Partial | string): T }) => { - const constr = (constructor as any) as { desc: StaticData } - - constr.desc = { name } - const data = getProtoData(constructor.prototype, true) data.name = name - class WrappedEntity extends (constructor as EntityConstructor) { - sloth: SlothData - + return class WrappedEntity extends constructor as EntityConstructor< + any, + any + > { constructor(factory: PouchFactory, idOrProps: Partial | string) { super(factory, idOrProps) - if (typeof idOrProps === 'string') { - this.sloth = { - name, - updatedProps: {}, - props: {}, - docId: idOrProps, - factory, - slug - } - } else { - this.sloth = { - name, - updatedProps: {}, - props: mapPropsOrDocToDocument(getProtoData(this), idOrProps), - docId: idOrProps._id, - factory, - slug - } - } + this.sloth.props = mapPropsOrDocToDocument( + getProtoData(this), + idOrProps + ) } } - return WrappedEntity as any } } diff --git a/src/decorators/SlothField.ts b/src/decorators/SlothField.ts index ea9e44f..13acd14 100644 --- a/src/decorators/SlothField.ts +++ b/src/decorators/SlothField.ts @@ -17,7 +17,6 @@ export default function SlothField(docKeyName?: string) { const docKey = docKeyName || key const desc = Reflect.getOwnPropertyDescriptor(target, key) - let defaultValue: T if (desc) { if (desc.get || desc.set) { @@ -32,26 +31,34 @@ export default function SlothField(docKeyName?: string) { Reflect.deleteProperty(target, key) Reflect.defineProperty(target, key, { - get: function(): T { - const { updatedProps, props } = getSlothData(this) + enumerable: true, + get: function(): T | undefined { + const { updatedProps, props = {}, defaultProps } = getSlothData(this) if (docKey in updatedProps) { return (updatedProps as any)[docKey] } if (docKey in props) { return (props as any)[docKey] } - return defaultValue + return (defaultProps as any)[docKey] }, set: function(value: T) { - // Typescript calls this function before class decorator - // Thus, when assigning default values in constructor we can get it and write it down - // However this should only happen once to avoid missing bugs - if (!('sloth' in this) && (!defaultValue || defaultValue === value)) { - defaultValue = value + const { props, defaultProps, updatedProps } = getSlothData(this) + + if (!props) { + defaultProps[docKey] = value + + return + } + + if (docKey in defaultProps && value == null) { + delete props[docKey] + delete updatedProps[docKey] + return } - Object.assign(getSlothData(this).updatedProps, { [docKey]: value }) + Object.assign(updatedProps, { [docKey]: value }) } }) } diff --git a/src/helpers/EntityConstructor.ts b/src/helpers/EntityConstructor.ts index a62e7fd..ffeab15 100644 --- a/src/helpers/EntityConstructor.ts +++ b/src/helpers/EntityConstructor.ts @@ -1,11 +1,12 @@ import BaseEntity from '../models/BaseEntity' import PouchFactory from '../models/PouchFactory' -import StaticData from '../models/StaticData' /** * @private */ -export default interface EntityConstructor> { - desc?: StaticData +export default interface EntityConstructor< + S extends { _id: string }, + T extends BaseEntity +> { new (factory: PouchFactory, idOrProps: Partial | string): T } diff --git a/src/models/BaseEntity.ts b/src/models/BaseEntity.ts index d8b5c38..13c1f32 100644 --- a/src/models/BaseEntity.ts +++ b/src/models/BaseEntity.ts @@ -4,17 +4,49 @@ import getProtoData from '../utils/getProtoData' import Dict from '../helpers/Dict' import { RelationDescriptor } from './relationDescriptors' import { join } from 'path' +import SlothData from './SlothData' +import ProtoData from './ProtoData' +import Debug from 'debug' + +const debug = Debug('slothdb') + +const slug = require('limax') /** * Base abstract entity, for all entitoies * The generic parameter S is the schema of the document * @typeparam S the document schema */ -export default class BaseEntity { +export default class BaseEntity { _id: string = '' - // tslint:disable-next-line:no-empty - constructor(factory: PouchFactory, idOrProps: Partial | string) {} + private sloth: SlothData + // tslint:disable-next-line:no-empty + constructor(factory: PouchFactory, idOrProps: Partial | string) { + const { name } = getProtoData(this) + if (!name) { + throw new Error('Please use SlothEntity') + } + if (typeof idOrProps === 'string') { + this.sloth = { + name, + updatedProps: {}, + defaultProps: {}, + docId: idOrProps, + factory, + slug + } + } else { + this.sloth = { + name, + defaultProps: {}, + updatedProps: {}, + docId: idOrProps._id, + factory, + slug + } + } + } /** * Returns whether this document hhas unsaved updated properties */ @@ -64,10 +96,13 @@ export default class BaseEntity { * then no _rev property is returned */ async save(): Promise { + debug('save document "%s"', this._id) + const { fields } = getProtoData(this, false) const doc = this.getDocument() if (!this.isDirty()) { + debug('document "%s" is not dirty, skippping saving', this._id) return doc } @@ -77,6 +112,12 @@ export default class BaseEntity { try { const { _rev } = await db.get(this._id) + debug( + 'document "%s" already exists with revision "%s", updating...', + this._id, + _rev + ) + const { rev } = await db.put(Object.assign({}, doc, { _rev })) getSlothData(this).docId = this._id @@ -87,13 +128,20 @@ export default class BaseEntity { if (err.name === 'not_found') { if (docId) { + debug( + 'document "%s" was renamed to "%s", removing old document', + docId, + this._id + ) // We need to delete old doc const originalDoc = await db.get(docId) await db.remove(originalDoc) getSlothData(this).docId = this._id } + const { rev, id } = await db.put(doc) + debug('document "%s" rev "%s" was created', this._id, rev) getSlothData(this).docId = this._id diff --git a/src/models/SlothData.ts b/src/models/SlothData.ts index 0ce53bc..899e5d7 100644 --- a/src/models/SlothData.ts +++ b/src/models/SlothData.ts @@ -15,7 +15,7 @@ export default interface SlothData { /** * Loaded properties from database or constructor */ - props: Partial + props?: Partial /** * Properties updated at runtime */ @@ -29,5 +29,7 @@ export default interface SlothData { */ slug: (str: string) => string + defaultProps: Partial + factory: PouchFactory } diff --git a/src/models/SlothDatabase.ts b/src/models/SlothDatabase.ts index de6fabf..bc6d976 100644 --- a/src/models/SlothDatabase.ts +++ b/src/models/SlothDatabase.ts @@ -17,7 +17,7 @@ const debug = Debug('slothdb') * @typeparam V the (optional) view type that defines a list of possible view IDs */ export default class SlothDatabase< - S, + S extends { _id: string }, E extends BaseEntity, V extends string = never > { @@ -54,11 +54,14 @@ export default class SlothDatabase< constructor(model: EntityConstructor, root: string = '') { this._model = model this._root = root - if (model.desc && model.desc.name) { - this._name = model.desc.name - } else { - throw new Error('Please use SlothEntity') + + const { name } = getProtoData(model.prototype) + + if (!name) { + throw new Error('SlothEntity decorator is required') } + + this._name = name } /** diff --git a/src/models/StaticData.ts b/src/models/StaticData.ts deleted file mode 100644 index 6dd6092..0000000 --- a/src/models/StaticData.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Represents that is not inherent to each entity, - * but needed by each one - * Available statically - * @api private - */ -export default interface StaticData { - /** - * Database name - */ - name?: string -} diff --git a/src/utils/getSlothData.ts b/src/utils/getSlothData.ts index 9f067e1..3aeb495 100644 --- a/src/utils/getSlothData.ts +++ b/src/utils/getSlothData.ts @@ -9,11 +9,10 @@ import { inspect } from 'util' * @private */ export default function getSlothData(obj: object) { - // console.trace(obj); const wrapped = obj as { sloth: SlothData } if (!wrapped.sloth) { - throw new Error('Please use SlothEntity') + throw new Error(`Class ${wrapped} does not extend SlothEntity`) } return wrapped.sloth diff --git a/src/utils/relationMappers.ts b/src/utils/relationMappers.ts index b117107..fdc19ec 100644 --- a/src/utils/relationMappers.ts +++ b/src/utils/relationMappers.ts @@ -2,7 +2,10 @@ import getProtoData from './getProtoData' import PouchFactory from '../models/PouchFactory' import BaseEntity from '../models/BaseEntity' -export function belongsToMapper(target: any, keyName: string) { +export function belongsToMapper( + target: any, + keyName: string +) { return (factory: PouchFactory): Promise> => { const { rels } = getProtoData(target) diff --git a/test/integration/object.test.ts b/test/integration/object.test.ts index 2ab98ff..5f5a58d 100644 --- a/test/integration/object.test.ts +++ b/test/integration/object.test.ts @@ -49,6 +49,7 @@ describe('nested objects', () => { barz: 'foobarbarz' } }) + expect(await factory('foos').get('foos/foobar')).toMatchObject({ name: 'Foobar', foo: { diff --git a/test/unit/decorators/SlothEntity.test.ts b/test/unit/decorators/SlothEntity.test.ts index 3719d64..3870dec 100644 --- a/test/unit/decorators/SlothEntity.test.ts +++ b/test/unit/decorators/SlothEntity.test.ts @@ -6,119 +6,14 @@ test('SlothEntity - attaches a sloth object to class', () => { // tslint:disable-next-line:no-empty const constr = () => {} - const wrapper = SlothEntity('foo')(constr as any) - const context: any = {} - - wrapper.call(context, localPouchFactory, 'foos/foo') - - expect(context.sloth).toBeDefined() -}) - -test('SlothEntity - set the name using the passed argument', () => { - // tslint:disable-next-line:no-empty - const constr = () => {} - - const wrapper = SlothEntity('foo')(constr as any) - const context: any = {} - - wrapper.call(context, localPouchFactory, 'foos/foo') - - expect(context.sloth).toBeDefined() - expect(context.sloth.name).toBe('foo') -}) - -test('SlothEntity - set the props when props 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: 'foo' }] }) + __protoData: { fields: [] }, + sloth: {} } - wrapper.call(context, localPouchFactory, { foo: 'bar' }) - - expect(context.sloth).toBeDefined() - expect(context.sloth.name).toBe('foo') - 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 = { - __protoData: emptyProtoData({ - fields: [{ key: 'foo', docKey: 'foo' }, { key: '_id', docKey: '_id' }] - }) - } - - wrapper.call(context, localPouchFactory, { _id: 'foobar', foo: 'bar' }) - - expect(context.sloth).toBeDefined() - expect(context.sloth.name).toBe('foo') - expect(context.sloth.props.foo).toBe('bar') - expect(context.sloth.updatedProps).toEqual({}) - expect(context.sloth.docId).toBe('foobar') -}) -test('SlothEntity - set the docId only when string is passed', () => { - // tslint:disable-next-line:no-empty - const constr = () => {} - - const wrapper = SlothEntity('foo')(constr as any) - const context: any = {} - - wrapper.call(context, localPouchFactory, 'foobar') + wrapper.call(context, localPouchFactory, 'foos/foo') expect(context.sloth).toBeDefined() - expect(context.sloth.name).toBe('foo') - expect(context.sloth.props).toEqual({}) - expect(context.sloth.updatedProps).toEqual({}) - expect(context.sloth.docId).toBe('foobar') + expect(context.sloth.props).toBeDefined() }) diff --git a/test/unit/decorators/SlothField.test.ts b/test/unit/decorators/SlothField.test.ts index c038fea..4ea99c7 100644 --- a/test/unit/decorators/SlothField.test.ts +++ b/test/unit/decorators/SlothField.test.ts @@ -1,4 +1,5 @@ import SlothField from '../../../src/decorators/SlothField' +import emptyProtoData from '../../utils/emptyProtoData' test('SlothField - fails on a defined property using get', () => { const object = {} @@ -65,24 +66,36 @@ test('SlothField - uses props if not updated', () => { }) test('SlothField - uses default value', () => { - const object: { foobar: string; sloth?: any } = { - foobar: '' + const object: any = { + foobar: '', + sloth: { + defaultProps: {}, + updatedProps: {} + }, + __protoData: emptyProtoData({ + fields: [{ key: 'foobar', docKey: 'foobar' }] + }), + props: null } SlothField()(object, 'foobar') object.foobar = 'default' - expect(() => (object.foobar = 'notsofast')).toThrowError(/SlothEntity/) + expect(object.foobar).toBe('default') + + object.sloth.props = {} + object.foobar = 'foobar' - object.sloth = { updatedProps: {}, props: {} } + expect(object.foobar).toBe('foobar') + object.foobar = null expect(object.foobar).toBe('default') }) -test('SlothField - updated updatedProps', () => { +test('SlothField - update updatedProps', () => { const object = { foobar: '', - sloth: { updatedProps: {}, props: {} } + sloth: { updatedProps: {}, props: {}, defaultProps: {} } } SlothField()(object, 'foobar') diff --git a/test/unit/models/BaseEntity.test.ts b/test/unit/models/BaseEntity.test.ts index 4977562..193f5ab 100644 --- a/test/unit/models/BaseEntity.test.ts +++ b/test/unit/models/BaseEntity.test.ts @@ -1,5 +1,7 @@ import BaseEntity from '../../../src/models/BaseEntity' import { RelationDescriptor } from '../../../src/models/relationDescriptors' +import SlothEntity from '../../../src/decorators/SlothEntity' +import emptyProtoData from '../../utils/emptyProtoData' test('BaseEntity#isDirty returns false without any updated props', () => { expect( @@ -350,3 +352,97 @@ test('BaseEntity#getDocument returns props', () => { bar: 'bar' }) }) + +describe('BaseEntity#constructor', () => { + // tslint:disable-next-line:no-empty + const constr = () => {} + const localPouchFactory = () => null + + test('set the props when props are passed', () => { + const context: any = { + __protoData: emptyProtoData({ + name: 'foo', + fields: [{ key: 'foo', docKey: 'foo' }] + }), + name: 'foo', + props: {} + } + + BaseEntity.call(context, localPouchFactory, { foo: 'bar' }) + + expect(context.sloth).toBeDefined() + expect(context.sloth.name).toBe('foo') + expect(context.sloth.docId).toBeUndefined() + }) + test('can use keys', () => { + // tslint:disable-next-line:no-empty + const constr = () => {} + + const context: any = { + name: 'foo', + __protoData: emptyProtoData({ + name: 'foo', + fields: [{ key: 'foo', docKey: 'barz' }] + }) + } + + BaseEntity.call(context, localPouchFactory, { foo: 'bar' }) + + expect(context.sloth).toBeDefined() + expect(context.sloth.name).toBe('foo') + expect(context.sloth.docId).toBeUndefined() + }) + test('can use docKeys', () => { + // tslint:disable-next-line:no-empty + const constr = () => {} + + const context: any = { + name: 'foo', + __protoData: emptyProtoData({ + name: 'foo', + fields: [{ key: 'foo', docKey: 'barz' }] + }) + } + + BaseEntity.call(context, localPouchFactory, { barz: 'bar' }) + + expect(context.sloth).toBeDefined() + expect(context.sloth.name).toBe('foo') + expect(context.sloth.docId).toBeUndefined() + }) + test('eventually set docId when props are passed with _id', () => { + // tslint:disable-next-line:no-empty + const constr = () => {} + + const context: any = { + name: 'foo', + __protoData: emptyProtoData({ + name: 'foo', + fields: [{ key: 'foo', docKey: 'foo' }, { key: '_id', docKey: '_id' }] + }) + } + + BaseEntity.call(context, localPouchFactory, { _id: 'foobar', foo: 'bar' }) + + expect(context.sloth).toBeDefined() + expect(context.sloth.name).toBe('foo') + expect(context.sloth.updatedProps).toEqual({}) + expect(context.sloth.docId).toBe('foobar') + }) + test('set the docId only when string is passed', () => { + // tslint:disable-next-line:no-empty + const constr = () => {} + + const context: any = { + name: 'foo', + __protoData: emptyProtoData({ name: 'foo' }) + } + + BaseEntity.call(context, localPouchFactory, 'foobar') + + expect(context.sloth).toBeDefined() + expect(context.sloth.name).toBe('foo') + expect(context.sloth.updatedProps).toEqual({}) + expect(context.sloth.docId).toBe('foobar') + }) +}) diff --git a/test/unit/models/SlothDatabase.test.ts b/test/unit/models/SlothDatabase.test.ts index 0069119..c8afbec 100644 --- a/test/unit/models/SlothDatabase.test.ts +++ b/test/unit/models/SlothDatabase.test.ts @@ -3,7 +3,9 @@ import localPouchFactory from '../../utils/localPouchFactory' import emptyProtoData from '../../utils/emptyProtoData' test('SlothDatabase#constructor - sets the db name from desc', () => { - const db1 = new SlothDatabase({ desc: { name: 'foos' } } as any) + const db1 = new SlothDatabase({ + prototype: { __protoData: { name: 'foos' } } + } as any) expect((db1 as any)._name).toBe('foos') }) @@ -11,7 +13,7 @@ test('SlothDatabase#constructor - sets the db name from desc', () => { test('SlothDatabase#constructor - throws without a desc', () => { expect(() => { const db1 = new SlothDatabase({} as any) - }).toThrowError(/SlothEntity/) + }).toThrowError(/Cannot read property '__protoData' of undefined/) }) test('SlothDatabase#create - create a model instance with props', async () => { From ba1447b75dc7201fb6fdda2785c506988b127f33 Mon Sep 17 00:00:00 2001 From: vinz243 Date: Sat, 21 Apr 2018 17:53:08 +0200 Subject: [PATCH 03/18] test(changes): increase delay to avoid CI build failing --- test/integration/changes.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/integration/changes.test.ts b/test/integration/changes.test.ts index d2010bb..736647d 100644 --- a/test/integration/changes.test.ts +++ b/test/integration/changes.test.ts @@ -17,7 +17,11 @@ describe('changes#subscribe', () => { await Artist.create(factory, { name: 'foo' }).save() - await delay(10) + let i = 0 + + do { + await delay(250) + } while (i++ < 120 && subscriber.mock.calls.length === 0) const { calls } = subscriber.mock From 9e09a3d730c47255259f88c153c83359c95f61c4 Mon Sep 17 00:00:00 2001 From: vinz243 Date: Sat, 21 Apr 2018 18:01:49 +0200 Subject: [PATCH 04/18] fix(sloth-entity): cast constructor as a BaseEntity --- src/decorators/SlothEntity.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/decorators/SlothEntity.ts b/src/decorators/SlothEntity.ts index a5076cc..eca8dc3 100644 --- a/src/decorators/SlothEntity.ts +++ b/src/decorators/SlothEntity.ts @@ -39,11 +39,8 @@ export default function SlothEntity(name: string) { const data = getProtoData(constructor.prototype, true) data.name = name - - return class WrappedEntity extends constructor as EntityConstructor< - any, - any - > { + const BaseEntity = constructor as EntityConstructor + return class WrappedEntity extends BaseEntity { constructor(factory: PouchFactory, idOrProps: Partial | string) { super(factory, idOrProps) this.sloth.props = mapPropsOrDocToDocument( From 4d29c90c14997d364b60bf4d520ea037a07cf750 Mon Sep 17 00:00:00 2001 From: vinz243 Date: Sat, 21 Apr 2018 18:39:32 +0200 Subject: [PATCH 05/18] fix(sloth-rel): only push to fields belongsTo relations --- src/decorators/SlothRel.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/decorators/SlothRel.ts b/src/decorators/SlothRel.ts index 9711753..31f8b38 100644 --- a/src/decorators/SlothRel.ts +++ b/src/decorators/SlothRel.ts @@ -27,7 +27,10 @@ export default function SlothRel(rel: RelationDescriptor) { const { fields, rels } = getProtoData(target, true) - fields.push({ key, docKey: key }) + if ('belongsTo' in rel) { + fields.push({ key, docKey: key }) + } + rels.push({ ...rel, key }) Reflect.deleteProperty(target, key) From f41833dc7427ed215134ebaa442fae9fc7371b84 Mon Sep 17 00:00:00 2001 From: vinz243 Date: Tue, 24 Apr 2018 21:19:34 +0200 Subject: [PATCH 06/18] fix(sloth-entity): returns type constructor --- src/decorators/SlothEntity.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/decorators/SlothEntity.ts b/src/decorators/SlothEntity.ts index eca8dc3..0e8fb0a 100644 --- a/src/decorators/SlothEntity.ts +++ b/src/decorators/SlothEntity.ts @@ -35,11 +35,13 @@ function mapPropsOrDocToDocument({ fields }: ProtoData, data: any) { export default function SlothEntity(name: string) { return >(constructor: { new (factory: PouchFactory, idOrProps: Partial | string): T - }) => { + }): EntityConstructor => { const data = getProtoData(constructor.prototype, true) data.name = name + const BaseEntity = constructor as EntityConstructor + return class WrappedEntity extends BaseEntity { constructor(factory: PouchFactory, idOrProps: Partial | string) { super(factory, idOrProps) @@ -48,6 +50,6 @@ export default function SlothEntity(name: string) { idOrProps ) } - } + } as any } } From 68f900096c3a76f02d234c0c77d9ce0a5e0cfc74 Mon Sep 17 00:00:00 2001 From: vinz243 Date: Tue, 24 Apr 2018 21:20:02 +0200 Subject: [PATCH 07/18] fix(ssloth-database): avoid concurrent initSetup --- src/models/SlothDatabase.ts | 122 ++++++++++++++++++++---------------- 1 file changed, 69 insertions(+), 53 deletions(-) diff --git a/src/models/SlothDatabase.ts b/src/models/SlothDatabase.ts index bc6d976..de4c71b 100644 --- a/src/models/SlothDatabase.ts +++ b/src/models/SlothDatabase.ts @@ -44,6 +44,8 @@ export default class SlothDatabase< changes: PouchDB.Core.Changes }[] = [] + private _setupPromise?: Promise + /** * Create a new database instance * @param factory the pouch factory to use @@ -57,47 +59,81 @@ export default class SlothDatabase< const { name } = getProtoData(model.prototype) + /* istanbul ignore if */ if (!name) { throw new Error('SlothEntity decorator is required') } this._name = name } - /** - * Queries and maps docs to Entity objects + * Run a query * * @param factory the pouch factory * @param view the view identifier * @param startKey the optional startkey * @param endKey the optional endkey + * @param includeDocs include_docs */ - queryDocs( + query( factory: PouchFactory, view: V, startKey = '', - endKey = join(startKey, '\uffff') - ): Promise { + endKey = join(startKey, '\uffff'), + includeDocs = false + ): Promise> { return factory(this._name) - .query(view, { + .query(view, { startkey: startKey, endkey: endKey, - include_docs: true - }) - .then(({ rows }) => { - return rows.map(({ doc }) => new this._model(factory, doc as any)) + include_docs: includeDocs }) .catch(err => { if (err.name === 'not_found') { debug(`Design document '%s' is missing, generating views...`, view) - return this.initSetup(factory).then(() => { + + /* istanbul ignore if */ + if (this._setupPromise) { + this._setupPromise.then(() => { + return this.query(factory, view, startKey, endKey, includeDocs) + }) + } + + this._setupPromise = this.initSetup(factory) + + return this._setupPromise.then(() => { debug('Created design documents') - return this.queryDocs(factory, view, startKey, endKey) + this._setupPromise = undefined + return this.query(factory, view, startKey, endKey, includeDocs) }) } throw err }) } + /** + * Queries and maps docs to Entity objects + * + * @param factory the pouch factory + * @param view the view identifier + * @param startKey the optional startkey + * @param endKey the optional endkey + */ + queryDocs( + factory: PouchFactory, + view: V, + startKey = '', + endKey = join(startKey, '\uffff') + ): Promise { + return this.query( + factory, + view, + startKey, + endKey, + true + ).then(({ rows }) => { + return rows.map(({ doc }) => new this._model(factory, doc as any)) + }) + } /** * Queries keys. Returns an array of emitted keys @@ -113,25 +149,15 @@ export default class SlothDatabase< startKey = '', endKey = join(startKey, '\uffff') ): Promise { - return factory(this._name) - .query(view, { - startkey: startKey, - endkey: endKey, - include_docs: false - }) - .then(({ rows }) => { - return rows.map(({ key }) => key) - }) - .catch(err => { - if (err.name === 'not_found') { - debug(`Design document '%s' is missing, generating views...`, view) - return this.initSetup(factory).then(() => { - debug('Created design documents') - return this.queryKeys(factory, view, startKey, endKey) - }) - } - throw err - }) + return this.query( + factory, + view, + startKey, + endKey, + false + ).then(({ rows }) => { + return rows.map(({ key }) => key) + }) } /** @@ -148,28 +174,18 @@ export default class SlothDatabase< startKey = '', endKey = join(startKey, '\uffff') ): Promise> { - return factory(this._name) - .query(view, { - startkey: startKey, - endkey: endKey, - include_docs: false - }) - .then(({ rows }) => { - return rows.reduce( - (acc, { key, id }) => ({ ...acc, [key]: id }), - {} as Dict - ) - }) - .catch(err => { - if (err.name === 'not_found') { - debug(`Design document '%s' is missing, generating views...`, view) - return this.initSetup(factory).then(() => { - debug('Created design documents') - return this.queryKeysIDs(factory, view, startKey, endKey) - }) - } - throw err - }) + return this.query( + factory, + view, + startKey, + endKey, + false + ).then(({ rows }) => { + return rows.reduce( + (acc, { key, id }) => ({ ...acc, [key]: id }), + {} as Dict + ) + }) } /** From 8f0d7b718b23d27549df2ea5d3f18407250d5ce8 Mon Sep 17 00:00:00 2001 From: vinz243 Date: Thu, 26 Apr 2018 23:37:21 +0200 Subject: [PATCH 08/18] build(tsc): change target to ES6 --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 215a7f1..1cacee9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "moduleResolution": "node", - "target": "es5", + "target": "es6", "module":"es2015", "lib": ["es2015", "es2016", "es2017", "dom"], "strict": true, From 2e26cefc6e285727205c689dbd5f3c485f326691 Mon Sep 17 00:00:00 2001 From: vinz243 Date: Fri, 27 Apr 2018 00:17:49 +0200 Subject: [PATCH 09/18] feat: use es6 BREAKING CHANGE: Use ES6 --- test/unit/decorators/SlothEntity.test.ts | 19 - test/unit/models/BaseEntity.test.ts | 448 ----------------------- test/unit/models/SlothDatabase.test.ts | 337 ----------------- test/utils/assignProto.ts | 15 + 4 files changed, 15 insertions(+), 804 deletions(-) delete mode 100644 test/unit/decorators/SlothEntity.test.ts delete mode 100644 test/unit/models/BaseEntity.test.ts delete mode 100644 test/unit/models/SlothDatabase.test.ts create mode 100644 test/utils/assignProto.ts diff --git a/test/unit/decorators/SlothEntity.test.ts b/test/unit/decorators/SlothEntity.test.ts deleted file mode 100644 index 3870dec..0000000 --- a/test/unit/decorators/SlothEntity.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -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 - const constr = () => {} - - const wrapper = SlothEntity('foo')(constr as any) - const context: any = { - __protoData: { fields: [] }, - sloth: {} - } - - wrapper.call(context, localPouchFactory, 'foos/foo') - - expect(context.sloth).toBeDefined() - expect(context.sloth.props).toBeDefined() -}) diff --git a/test/unit/models/BaseEntity.test.ts b/test/unit/models/BaseEntity.test.ts deleted file mode 100644 index 193f5ab..0000000 --- a/test/unit/models/BaseEntity.test.ts +++ /dev/null @@ -1,448 +0,0 @@ -import BaseEntity from '../../../src/models/BaseEntity' -import { RelationDescriptor } from '../../../src/models/relationDescriptors' -import SlothEntity from '../../../src/decorators/SlothEntity' -import emptyProtoData from '../../utils/emptyProtoData' - -test('BaseEntity#isDirty returns false without any updated props', () => { - expect( - BaseEntity.prototype.isDirty.call({ - sloth: { updatedProps: {}, docId: '' } - }) - ).toBe(false) -}) - -test('BaseEntity#isDirty returns true with updated props', () => { - expect( - BaseEntity.prototype.isDirty.call({ - sloth: { updatedProps: { foo: 'bar' }, docId: '' } - }) - ).toBe(true) -}) - -test('BaseEntity#isDirty returns true with no docId', () => { - expect( - BaseEntity.prototype.isDirty.call({ - sloth: { updatedProps: { foo: 'bar' } } - }) - ).toBe(true) -}) - -test('BaseEntity#save immediately returns props if not dirty', async () => { - const isDirty = jest.fn().mockReturnValue(false) - expect( - await BaseEntity.prototype.save.call({ - isDirty, - getDocument: () => 'foo', - __protoData: { fields: [{ key: 'foo' }] }, - foo: 'bar' - }) - ).toEqual('foo') - - expect(isDirty).toHaveBeenCalled() -}) - -test('BaseEntity#save create doc if does not exist', async () => { - const isDirty = jest.fn().mockReturnValue(true) - const getDocument = jest - .fn() - .mockReturnValue({ _id: 'foos/bar', name: 'bar' }) - - const get = jest.fn().mockRejectedValue({ name: 'not_found' }) - const put = jest.fn().mockResolvedValue({ rev: 'revision' }) - - const factory = jest.fn().mockReturnValue({ get, put }) - - const { _rev } = await BaseEntity.prototype.save.call({ - isDirty, - getDocument, - __protoData: { - fields: [{ key: '_id' }, { key: 'name' }] - }, - sloth: { - factory, - name: 'foos' - }, - _id: 'foos/bar', - name: 'bar' - }) - - expect(isDirty).toHaveBeenCalled() - expect(factory).toHaveBeenCalledWith('foos') - expect(get).toHaveBeenCalledWith('foos/bar') - expect(put).toHaveBeenCalledWith({ _id: 'foos/bar', name: 'bar' }) - expect(_rev).toBe('revision') -}) - -test('BaseEntity#save remove previous doc', async () => { - const isDirty = jest.fn().mockReturnValue(true) - const { getDocument } = BaseEntity.prototype - - const get = jest - .fn() - .mockRejectedValueOnce({ name: 'not_found' }) - .mockResolvedValueOnce('foobar') - - const put = jest.fn().mockResolvedValue({ rev: 'myrev' }) - const remove = jest.fn().mockResolvedValue(null) - - const factory = jest.fn().mockReturnValue({ get, put, remove }) - - const { _rev } = await BaseEntity.prototype.save.call({ - isDirty, - getDocument, - __protoData: { - fields: [{ key: '_id', docKey: '_id' }, { key: 'name', docKey: 'name' }] - }, - sloth: { - factory, - name: 'foos', - docId: 'original_doc' - }, - _id: 'foos/bar', - name: 'bar' - }) - - expect(isDirty).toHaveBeenCalled() - - expect(factory).toHaveBeenCalledWith('foos') - - expect(get).toHaveBeenCalledWith('foos/bar') - expect(get).toHaveBeenCalledWith('original_doc') - - expect(put).toHaveBeenCalledWith({ _id: 'foos/bar', name: 'bar' }) - - expect(remove).toHaveBeenCalledWith('foobar') - expect(_rev).toBe('myrev') -}) - -test('BaseEntity#save throws error if not not_found', async () => { - const isDirty = jest.fn().mockReturnValue(true) - const { getDocument } = BaseEntity.prototype - - const get = jest.fn().mockRejectedValue(new Error('foo_error')) - const put = jest.fn().mockResolvedValue(null) - - const factory = jest.fn().mockReturnValue({ get, put }) - - await expect( - BaseEntity.prototype.save.apply({ - getDocument, - isDirty, - __protoData: { - fields: [{ key: '_id' }, { key: 'name' }] - }, - sloth: { - factory, - name: 'foos' - }, - _id: 'foos/bar', - name: 'bar' - }) - ).rejects.toMatchObject({ message: 'foo_error' }) - - expect(isDirty).toHaveBeenCalled() - - expect(factory).toHaveBeenCalledWith('foos') - - expect(get).toHaveBeenCalledWith('foos/bar') - - expect(put).toHaveBeenCalledTimes(0) -}) - -test('BaseEntity#remove returns false if document has no docId', async () => { - const flag = await BaseEntity.prototype.remove.call({ sloth: {} }) - expect(flag).toBe(false) -}) - -test('BaseEntity#remove calls db.remove with _rev', async () => { - const get = jest.fn().mockResolvedValue({ _rev: 'revision' }) - const remove = jest.fn().mockResolvedValue(null) - const removeRelations = jest.fn() - - const factory = jest.fn().mockReturnValue({ get, remove }) - - const flag = await BaseEntity.prototype.remove.call({ - sloth: { factory, docId: 'foobar', name: 'foos' }, - removeRelations - }) - - expect(flag).toBe(true) - expect(removeRelations).toHaveBeenCalled() - expect(factory).toHaveBeenCalledWith('foos') - expect(get).toHaveBeenCalledWith('foobar') - expect(remove).toHaveBeenCalledWith('foobar', 'revision') -}) - -test('BaseEntity#removeRelations doesnt remove parent if cascade is set to false', async () => { - const allDocs = jest.fn().mockResolvedValue({ rows: { length: 0 } }) - const factory = jest.fn().mockReturnValue({ allDocs }) - const remove = jest.fn() - const belongsTo = jest.fn().mockResolvedValue({ remove }) - - const name = 'foobars' - - const rels = [ - { - belongsTo, - cascade: false, - key: 'foo' - } - ] - - await BaseEntity.prototype.removeRelations.apply({ - ...BaseEntity.prototype, - __protoData: { rels }, - foo: 'bar', - sloth: { - factory, - name - } - }) - - expect(allDocs).not.toHaveBeenCalled() - expect(factory).not.toHaveBeenCalledWith(name) - expect(remove).not.toHaveBeenCalled() - expect(belongsTo).not.toHaveBeenCalled() -}) - -test('BaseEntity#removeRelations doesnt remove parent if has child', async () => { - const allDocs = jest.fn().mockResolvedValue({ rows: ['foo'] }) - const factory = jest.fn().mockReturnValue({ allDocs }) - const remove = jest.fn() - const belongsTo = jest.fn().mockResolvedValue({ remove }) - - const name = 'foobars' - - const rels = [ - { - belongsTo, - cascade: true, - key: 'foo' - } - ] - - await BaseEntity.prototype.removeRelations.apply({ - ...BaseEntity.prototype, - __protoData: { rels }, - foo: 'bar', - sloth: { - factory, - name - } - }) - - expect(allDocs).toHaveBeenCalledWith({ - include_docs: false, - startkey: 'bar/', - endkey: 'bar/\uffff' - }) - expect(factory).toHaveBeenCalledWith(name) - expect(remove).not.toHaveBeenCalled() - expect(belongsTo).not.toHaveBeenCalled() -}) - -test('BaseEntity#removeRelations remove parent if no child', async () => { - const allDocs = jest.fn().mockResolvedValue({ rows: [] }) - const factory = jest.fn().mockReturnValue({ allDocs }) - const remove = jest.fn() - const findById = jest.fn().mockResolvedValue({ remove }) - const belongsTo = jest.fn().mockReturnValue({ findById }) - - const name = 'foobars' - - const rels = [ - { - belongsTo, - cascade: true, - key: 'foo' - } - ] - - await BaseEntity.prototype.removeRelations.apply({ - ...BaseEntity.prototype, - __protoData: { rels }, - foo: 'bar', - sloth: { - factory, - name - } - }) - - expect(allDocs).toHaveBeenCalledWith({ - include_docs: false, - startkey: 'bar/', - endkey: 'bar/\uffff' - }) - expect(factory).toHaveBeenCalledWith(name) - expect(remove).toHaveBeenCalled() - expect(belongsTo).toHaveBeenCalled() -}) - -test('BaseEntity#removeRelations remove parent if no child', async () => { - const allDocs = jest.fn().mockResolvedValue({ rows: [] }) - const factory = jest.fn().mockReturnValue({ allDocs }) - const remove = jest.fn() - const findById = jest.fn().mockResolvedValue({ remove }) - const belongsTo = jest.fn().mockReturnValue({ findById }) - - const name = 'foobars' - - const rels = [ - { - belongsTo, - cascade: true, - key: 'foo' - } - ] - - await BaseEntity.prototype.removeRelations.apply({ - ...BaseEntity.prototype, - __protoData: { rels }, - foo: 'bar', - sloth: { - factory, - name - } - }) - - expect(allDocs).toHaveBeenCalledWith({ - include_docs: false, - startkey: 'bar/', - endkey: 'bar/\uffff' - }) - expect(factory).toHaveBeenCalledWith(name) - expect(remove).toHaveBeenCalled() - expect(belongsTo).toHaveBeenCalled() -}) - -test('BaseEntity#getProps returns props', () => { - const doc = BaseEntity.prototype.getProps.call({ - __protoData: { - fields: [{ key: 'name' }, { key: '_id' }, { key: 'foo' }] - }, - name: 'John', - _id: 'john', - foo: 'bar' - }) - - expect(doc).toEqual({ - name: 'John', - _id: 'john', - foo: 'bar' - }) -}) - -test('BaseEntity#getDocument returns props', () => { - const doc = BaseEntity.prototype.getDocument.call({ - __protoData: { - fields: [ - { key: 'name', docKey: 'not_name' }, - { key: '_id', docKey: '_id' }, - { key: 'foo', docKey: 'bar' } - ] - }, - name: 'John', - _id: 'john', - foo: 'bar' - }) - - expect(doc).toEqual({ - not_name: 'John', - _id: 'john', - bar: 'bar' - }) -}) - -describe('BaseEntity#constructor', () => { - // tslint:disable-next-line:no-empty - const constr = () => {} - const localPouchFactory = () => null - - test('set the props when props are passed', () => { - const context: any = { - __protoData: emptyProtoData({ - name: 'foo', - fields: [{ key: 'foo', docKey: 'foo' }] - }), - name: 'foo', - props: {} - } - - BaseEntity.call(context, localPouchFactory, { foo: 'bar' }) - - expect(context.sloth).toBeDefined() - expect(context.sloth.name).toBe('foo') - expect(context.sloth.docId).toBeUndefined() - }) - test('can use keys', () => { - // tslint:disable-next-line:no-empty - const constr = () => {} - - const context: any = { - name: 'foo', - __protoData: emptyProtoData({ - name: 'foo', - fields: [{ key: 'foo', docKey: 'barz' }] - }) - } - - BaseEntity.call(context, localPouchFactory, { foo: 'bar' }) - - expect(context.sloth).toBeDefined() - expect(context.sloth.name).toBe('foo') - expect(context.sloth.docId).toBeUndefined() - }) - test('can use docKeys', () => { - // tslint:disable-next-line:no-empty - const constr = () => {} - - const context: any = { - name: 'foo', - __protoData: emptyProtoData({ - name: 'foo', - fields: [{ key: 'foo', docKey: 'barz' }] - }) - } - - BaseEntity.call(context, localPouchFactory, { barz: 'bar' }) - - expect(context.sloth).toBeDefined() - expect(context.sloth.name).toBe('foo') - expect(context.sloth.docId).toBeUndefined() - }) - test('eventually set docId when props are passed with _id', () => { - // tslint:disable-next-line:no-empty - const constr = () => {} - - const context: any = { - name: 'foo', - __protoData: emptyProtoData({ - name: 'foo', - fields: [{ key: 'foo', docKey: 'foo' }, { key: '_id', docKey: '_id' }] - }) - } - - BaseEntity.call(context, localPouchFactory, { _id: 'foobar', foo: 'bar' }) - - expect(context.sloth).toBeDefined() - expect(context.sloth.name).toBe('foo') - expect(context.sloth.updatedProps).toEqual({}) - expect(context.sloth.docId).toBe('foobar') - }) - test('set the docId only when string is passed', () => { - // tslint:disable-next-line:no-empty - const constr = () => {} - - const context: any = { - name: 'foo', - __protoData: emptyProtoData({ name: 'foo' }) - } - - BaseEntity.call(context, localPouchFactory, 'foobar') - - expect(context.sloth).toBeDefined() - expect(context.sloth.name).toBe('foo') - expect(context.sloth.updatedProps).toEqual({}) - expect(context.sloth.docId).toBe('foobar') - }) -}) diff --git a/test/unit/models/SlothDatabase.test.ts b/test/unit/models/SlothDatabase.test.ts deleted file mode 100644 index c8afbec..0000000 --- a/test/unit/models/SlothDatabase.test.ts +++ /dev/null @@ -1,337 +0,0 @@ -import SlothDatabase from '../../../src/models/SlothDatabase' -import localPouchFactory from '../../utils/localPouchFactory' -import emptyProtoData from '../../utils/emptyProtoData' - -test('SlothDatabase#constructor - sets the db name from desc', () => { - const db1 = new SlothDatabase({ - prototype: { __protoData: { name: 'foos' } } - } as any) - - expect((db1 as any)._name).toBe('foos') -}) - -test('SlothDatabase#constructor - throws without a desc', () => { - expect(() => { - const db1 = new SlothDatabase({} as any) - }).toThrowError(/Cannot read property '__protoData' of undefined/) -}) - -test('SlothDatabase#create - create a model instance with props', async () => { - const _model = jest.fn() - const props = { foo: 'bar' } - const doc = SlothDatabase.prototype.create.call( - { _model }, - localPouchFactory, - props - ) - - expect(_model).toHaveBeenCalledTimes(1) - expect(_model).toHaveBeenCalledWith(localPouchFactory, props) -}) - -test('SlothDatabase#create - create a model instance with props', async () => { - const create = jest.fn().mockImplementation((omit: never, el: object) => el) - - const _model = jest.fn() - const props = { _id: 'foos/bar', foo: 'bar' } - const dbMock = { - get: jest.fn().mockResolvedValue(props) - } - const factory = jest.fn().mockReturnValue(dbMock) - - const doc = await SlothDatabase.prototype.findById.call( - { _model, _name: 'foos' }, - factory, - 'foos/bar' - ) - - expect(factory).toHaveBeenCalledTimes(1) - expect(factory).toHaveBeenCalledWith('foos') - - expect(dbMock.get).toHaveBeenCalledTimes(1) - expect(dbMock.get).toHaveBeenCalledWith('foos/bar') - - expect(_model).toHaveBeenCalledWith(factory, props) -}) - -test('SlothDatabase#findAllDocs - calls allDocs and creates models', async () => { - const create = jest.fn().mockImplementation((omit: never, el: object) => el) - - const props = { _id: 'foos/bar', foo: 'bar' } - const docs = [{ foo: 'bar' }, { bar: 'foo' }] - - const allDocs = jest - .fn() - .mockResolvedValue({ rows: docs.map(doc => ({ doc })) }) - const factory = jest.fn().mockReturnValue({ allDocs }) - - expect( - await SlothDatabase.prototype.findAllDocs.call( - { create, _name: 'foos' }, - factory - ) - ).toEqual(docs) - expect( - await SlothDatabase.prototype.findAllDocs.call( - { create, _name: 'foos' }, - factory, - 'foos/bar' - ) - ).toEqual(docs) - expect( - await SlothDatabase.prototype.findAllDocs.call( - { create, _name: 'foos' }, - factory, - 'foo', - 'bar' - ) - ).toEqual(docs) - - expect(create).toHaveBeenCalledWith(factory, docs[0]) - expect(create).toHaveBeenCalledWith(factory, docs[1]) - expect(create).toHaveBeenCalledTimes(6) - - expect(factory).toHaveBeenCalledTimes(3) - expect(factory).toHaveBeenCalledWith('foos') - - expect(allDocs).toHaveBeenCalledTimes(3) - expect(allDocs.mock.calls).toMatchObject([ - [ - { - include_docs: true, - startkey: '', - endkey: '\uffff' - } - ], - [ - { - include_docs: true, - startkey: 'foos/bar', - endkey: 'foos/bar/\uffff' - } - ], - [ - { - include_docs: true, - startkey: 'foo', - endkey: 'bar' - } - ] - ]) -}) - -test('SlothDatabase#findAllIDs - calls allDocs and return ids', async () => { - const create = jest.fn().mockImplementation((omit: never, el: object) => el) - - const rows = ['foo', 'bar'] - - const allDocs = jest - .fn() - .mockResolvedValue({ rows: rows.map(id => ({ id })) }) - - const factory = jest.fn().mockReturnValue({ allDocs }) - - expect( - await SlothDatabase.prototype.findAllIDs.call({ _name: 'foos' }, factory) - ).toEqual(rows) - expect( - await SlothDatabase.prototype.findAllIDs.call( - { _name: 'foos' }, - factory, - 'foos/bar' - ) - ).toEqual(rows) - expect( - await SlothDatabase.prototype.findAllIDs.call( - { _name: 'foos' }, - factory, - 'foo', - 'bar' - ) - ).toEqual(rows) - - expect(factory).toHaveBeenCalledTimes(3) - expect(factory).toHaveBeenCalledWith('foos') - - expect(allDocs).toHaveBeenCalledTimes(3) - expect(allDocs.mock.calls).toMatchObject([ - [ - { - include_docs: false, - startkey: '', - endkey: '\uffff' - } - ], - [ - { - include_docs: false, - startkey: 'foos/bar', - endkey: 'foos/bar/\uffff' - } - ], - [ - { - include_docs: false, - startkey: 'foo', - endkey: 'bar' - } - ] - ]) -}) - -describe('SlothDatabase#changes', () => { - const proto = Object.assign({}, SlothDatabase.prototype, { _subscribers: [] }) - - const cancel = jest.fn() - const on = jest.fn().mockReturnValue({ cancel }) - const changes = jest.fn().mockReturnValue({ on }) - - const factory1 = () => ({ changes }) - const factory2 = () => ({ changes }) - - const sub1 = () => ({}) - const sub2 = () => ({}) - - test(`Pushes sub and start listening`, () => { - SlothDatabase.prototype.subscribe.call(proto, factory1, sub1) - - expect(on).toHaveBeenCalledTimes(1) - expect(changes).toHaveBeenCalledTimes(1) - expect(cancel).not.toHaveBeenCalled() - }) - - test(`Pushes sub and doesn't listen if already`, () => { - SlothDatabase.prototype.subscribe.call(proto, factory1, sub2) - - expect(on).toHaveBeenCalledTimes(1) - expect(changes).toHaveBeenCalledTimes(1) - expect(cancel).not.toHaveBeenCalled() - }) - - test(`Doesn't call changes.cancel with remaining subs`, () => { - SlothDatabase.prototype.cancel.call(proto, factory1, sub2) - - expect(on).toHaveBeenCalledTimes(1) - expect(changes).toHaveBeenCalledTimes(1) - expect(cancel).not.toHaveBeenCalled() - }) - - test(`Call changes.cancel without remaining subs`, () => { - SlothDatabase.prototype.cancel.call(proto, factory1, sub2) - - expect(on).toHaveBeenCalledTimes(1) - expect(changes).toHaveBeenCalledTimes(1) - expect(cancel).toHaveBeenCalled() - }) -}) - -describe('SlothDatabase#initSetup', () => { - const proto = Object.assign({}, SlothDatabase.prototype, { - _model: { - prototype: { - __protoData: emptyProtoData({ - views: [ - { - id: 'views', - name: 'by_bar', - code: 'function (doc) { emit(doc.bar); }', - function: () => ({}) - }, - { - id: 'views', - name: 'by_barz', - code: 'function (doc) { emit(doc.barz); }', - function: () => ({}) - } - ] - }) - } - } - }) - - const get = jest.fn() - const put = jest.fn() - - const factory = () => ({ get, put }) - - const sub1 = () => ({}) - const sub2 = () => ({}) - - test(`Creates views if no document found`, async () => { - get.mockRejectedValue(new Error('')) - put.mockResolvedValue(null) - - await SlothDatabase.prototype.initSetup.call(proto, factory) - - expect(get).toHaveBeenCalledTimes(2) - expect(get).toHaveBeenCalledWith('_design/views') - - expect(put).toHaveBeenCalledTimes(2) - expect(put.mock.calls).toEqual([ - [ - { - _id: '_design/views', - views: { - by_bar: { map: 'function (doc) { emit(doc.bar); }' } - } - } - ], - [ - { - _id: '_design/views', - views: { - by_barz: { map: 'function (doc) { emit(doc.barz); }' } - } - } - ] - ]) - }) - - test('Update document if already exists', async () => { - put.mockClear() - get.mockClear() - - get.mockResolvedValue({ - _rev: 'foobar', - views: { - by_foobar: { - map: 'foobar!' - } - } - }) - put.mockResolvedValue(null) - - await SlothDatabase.prototype.initSetup.call(proto, factory) - - expect(get).toHaveBeenCalledTimes(2) - expect(get).toHaveBeenCalledWith('_design/views') - - expect(put).toHaveBeenCalledTimes(2) - expect(put.mock.calls).toEqual([ - [ - { - _rev: 'foobar', - _id: '_design/views', - views: { - by_bar: { map: 'function (doc) { emit(doc.bar); }' }, - by_foobar: { - map: 'foobar!' - } - } - } - ], - [ - { - _rev: 'foobar', - _id: '_design/views', - views: { - by_barz: { map: 'function (doc) { emit(doc.barz); }' }, - by_foobar: { - map: 'foobar!' - } - } - } - ] - ]) - }) -}) diff --git a/test/utils/assignProto.ts b/test/utils/assignProto.ts new file mode 100644 index 0000000..3e7c036 --- /dev/null +++ b/test/utils/assignProto.ts @@ -0,0 +1,15 @@ +export default function(target: any, ...objects: any[]) { + const res = Object.assign({}, target, ...objects) + + for (const obj in objects) { + for (const name in Object.getOwnPropertyNames(obj)) { + const desc = Object.getOwnPropertyDescriptor(obj, name) + if (!desc) { + return + } + res[name] = desc.value + } + } + + return res +} From ca487315e175999a87fe16bf6baddba68b8fd6ff Mon Sep 17 00:00:00 2001 From: vinz243 Date: Fri, 27 Apr 2018 00:18:23 +0200 Subject: [PATCH 10/18] 1.0.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index ae8a041..4e10f98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "slothdb", - "version": "0.0.0", + "version": "1.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 9d904d4..f9cd02d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "slothdb", - "version": "0.0.0", + "version": "1.0.0", "description": "", "keywords": [], "main": "dist/slothdb.umd.js", From 12c5905a80d2821f93df52f38ccf28fbed9adb8a Mon Sep 17 00:00:00 2001 From: vinz243 Date: Fri, 27 Apr 2018 00:18:35 +0200 Subject: [PATCH 11/18] 2.0.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4e10f98..eacbfc2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "slothdb", - "version": "1.0.0", + "version": "2.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index f9cd02d..37083de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "slothdb", - "version": "1.0.0", + "version": "2.0.0", "description": "", "keywords": [], "main": "dist/slothdb.umd.js", From 323b9135af493307cf17b050780141102aacd57c Mon Sep 17 00:00:00 2001 From: vinz243 Date: Sat, 28 Apr 2018 14:28:34 +0200 Subject: [PATCH 12/18] fix(sloth-database): query functions support any type --- src/models/SlothDatabase.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/models/SlothDatabase.ts b/src/models/SlothDatabase.ts index de4c71b..6fbcb64 100644 --- a/src/models/SlothDatabase.ts +++ b/src/models/SlothDatabase.ts @@ -78,8 +78,10 @@ export default class SlothDatabase< query( factory: PouchFactory, view: V, - startKey = '', - endKey = join(startKey, '\uffff'), + startKey?: any, + endKey: any = typeof startKey === 'string' + ? join(startKey, '\uffff') + : undefined, includeDocs = false ): Promise> { return factory(this._name) @@ -121,8 +123,8 @@ export default class SlothDatabase< queryDocs( factory: PouchFactory, view: V, - startKey = '', - endKey = join(startKey, '\uffff') + startKey?: any, + endKey?: any ): Promise { return this.query( factory, @@ -146,8 +148,8 @@ export default class SlothDatabase< queryKeys( factory: PouchFactory, view: V, - startKey = '', - endKey = join(startKey, '\uffff') + startKey?: any, + endKey?: any ): Promise { return this.query( factory, @@ -171,8 +173,8 @@ export default class SlothDatabase< queryKeysIDs( factory: PouchFactory, view: V, - startKey = '', - endKey = join(startKey, '\uffff') + startKey?: any, + endKey?: any ): Promise> { return this.query( factory, From 1df12735da2b75abaa4ae0578ec3c5f7899d2c61 Mon Sep 17 00:00:00 2001 From: vinz243 Date: Sat, 28 Apr 2018 15:10:51 +0200 Subject: [PATCH 13/18] feat(sloth-database): add joinURIParams function --- src/models/SlothDatabase.ts | 21 +++++++++++++++++++++ test/integration/Album.test.ts | 14 ++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/models/SlothDatabase.ts b/src/models/SlothDatabase.ts index 6fbcb64..4133376 100644 --- a/src/models/SlothDatabase.ts +++ b/src/models/SlothDatabase.ts @@ -66,6 +66,27 @@ export default class SlothDatabase< this._name = name } + + /** + * Join URI params provided as specified by SlothURI + * Useful to recreate document id from URL params + * Please note that + * @param props the props + * @param field + */ + joinURIParams(props: Partial, field = '_id') { + const idURI = getProtoData(this._model.prototype).uris.find( + ({ name }) => name === field + ) + if (!idURI) { + throw new Error(`Field ${field} not found in URIs`) + } + return join( + idURI.prefix, + ...idURI.propsKeys.map(key => (props as any)[key]) + ) + } + /** * Run a query * diff --git a/test/integration/Album.test.ts b/test/integration/Album.test.ts index cd97514..22e46f3 100644 --- a/test/integration/Album.test.ts +++ b/test/integration/Album.test.ts @@ -77,7 +77,9 @@ test('remove parent if last child is removed', async () => { }) await expect( db.get('library/flatbush-zombies/betteroffdead') - ).rejects.toMatchObject({ message: 'missing' }) + ).rejects.toMatchObject({ + message: 'missing' + }) }) test('doesnt remove parent if still has children', async () => { @@ -137,7 +139,9 @@ test('doesnt remove parent if still has children', async () => { await expect( db.get('library/flatbush-zombies/betteroffdead') - ).rejects.toMatchObject({ message: 'missing' }) + ).rejects.toMatchObject({ + message: 'missing' + }) }) test('rels.artist - maps with artist', async () => { @@ -160,3 +164,9 @@ test('rels.artist - maps with artist', async () => { expect(flatbush._id).toBe('library/flatbush-zombies') expect(flatbush.name).toBe('Flatbush Zombies') }) + +test('joinURIParams', () => { + expect( + Album.joinURIParams({ name: 'betteroffdead', artist: 'flatbush-zombies' }) + ).toBe('library/flatbush-zombies/betteroffdead') +}) From a689501b5a8626cf5263026fab2333aef8cec4c8 Mon Sep 17 00:00:00 2001 From: vinz243 Date: Sat, 28 Apr 2018 15:11:32 +0200 Subject: [PATCH 14/18] fix(base-entity): don't separate number for slug --- src/models/BaseEntity.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/models/BaseEntity.ts b/src/models/BaseEntity.ts index 13c1f32..d0b0721 100644 --- a/src/models/BaseEntity.ts +++ b/src/models/BaseEntity.ts @@ -10,7 +10,8 @@ import Debug from 'debug' const debug = Debug('slothdb') -const slug = require('limax') +const limax = require('limax') +const slug = (text: string) => limax(text, { separateNumbers: false }) /** * Base abstract entity, for all entitoies From 2558e3f3bc3d36695b128722f098aa86ec6da7ce Mon Sep 17 00:00:00 2001 From: vinz243 Date: Sat, 28 Apr 2018 15:11:51 +0200 Subject: [PATCH 15/18] 2.1.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index eacbfc2..333e9e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "slothdb", - "version": "2.0.0", + "version": "2.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 37083de..b334ae9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "slothdb", - "version": "2.0.0", + "version": "2.1.0", "description": "", "keywords": [], "main": "dist/slothdb.umd.js", From 622dfb510be84c26b61d8061d0849460064dbd32 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Wed, 2 May 2018 16:54:35 +0000 Subject: [PATCH 16/18] chore(package): update @types/node to version 10.0.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b334ae9..06021ae 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "devDependencies": { "@types/jest": "^22.0.0", "@types/joi": "^13.0.5", - "@types/node": "^9.3.0", + "@types/node": "^10.0.3", "@types/pouchdb": "^6.3.2", "@types/slug": "^0.9.0", "colors": "^1.1.2", From b48fede44c9cb12899d14cbb48612f9bf7c5703a Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Sat, 2 Jun 2018 12:55:43 +0000 Subject: [PATCH 17/18] chore(package): update @types/jest to version 23.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 06021ae..b38aad7 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "collectCoverage": true }, "devDependencies": { - "@types/jest": "^22.0.0", + "@types/jest": "^23.0.0", "@types/joi": "^13.0.5", "@types/node": "^10.0.3", "@types/pouchdb": "^6.3.2", From f6bba727efe30fb664bb637051988709e6d4fd8e Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Fri, 17 Aug 2018 18:03:47 +0000 Subject: [PATCH 18/18] chore(package): update typedoc to version 0.12.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b38aad7..6bb56d4 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "tslint": "^5.8.0", "tslint-config-prettier": "^1.1.0", "tslint-config-standard": "^7.0.0", - "typedoc": "^0.11.0", + "typedoc": "^0.12.0", "typescript": "^2.6.2", "validate-commit-msg": "^2.12.2" },