-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* start global query * fix: switch to one module syntax so api can be required in node as is * fix: add app specific api * global query cont * no entity api * store contructor cleanup * entity api uses adapter again * create page setup * fix: export pouch * ditch store for now * fix: searching * fix: lol standard
- Loading branch information
Showing
52 changed files
with
1,490 additions
and
581 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
EntityApi | ||
- implements common methods | ||
- has a single data access adapter | ||
- has a schema that rejects on create/update | ||
- extended for entity-specific domain logic (custom functions) | ||
- can be used on default without modification | ||
|
||
AdapterApi | ||
- this is an interface extended that has the same methods as EntityApi | ||
- it defaults to the entity's schema but can have different schema (location) | ||
|
||
Why only one adapter in EntityApi? What about "smart" adapters that need two+ different data sources? | ||
|
||
(strange that js-data had a "defaultAdapter" but no other way to change adapters externally...) | ||
|
||
I think for these (pretty common) use cases, instead of trying to have the default | ||
EntityApi + pouch Adapter Api, you'd create a custom adapter that can do stuff. | ||
|
||
e.g. orders needs a single 'fetch' endpoint, so | ||
|
||
```js | ||
OrdersDataAdapter extends PouchDBAdapter { | ||
constructor () { | ||
super() | ||
this.fetchAdapter = new FetchAdapter() | ||
} | ||
list () { | ||
return this.fetchAdapter() | ||
} | ||
} | ||
``` | ||
|
||
(e.g. fetch + pouch, or localPouch + remotePouch) | ||
|
||
to point out: | ||
- adapter expected to return `id` | ||
- adapter can use json schema if it wants | ||
- pouch adapter definitely uses json schema | ||
|
||
|
||
# todos: | ||
- entity api has schema but not sure how to use it yet | ||
- common interface for Entity & Adapter |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
const {SchemaInterface} = require('./schema-interface') | ||
|
||
class EntityApi extends SchemaInterface { | ||
constructor ({ | ||
schema, adapter, user, relations = {}, description = '' | ||
}) { | ||
if (typeof user !== 'object' || !user.name) { | ||
throw new Error('EntityApi usage: user object with name is requred') | ||
} | ||
if (!adapter) { | ||
throw new Error('EntityApi usage: adapter is required') | ||
} | ||
|
||
super(schema) | ||
|
||
this.adapter = adapter | ||
this.name = schema.name | ||
this.relations = relations | ||
this.description = description | ||
// done in schema interface | ||
// this.schema = schema | ||
this.user = user | ||
} | ||
|
||
async list (...params) { | ||
return this.adapter.list(...params) | ||
} | ||
|
||
async get (...params) { | ||
return this.adapter.get(...params) | ||
} | ||
|
||
async create (...params) { | ||
return this.adapter.create(...params) | ||
} | ||
|
||
async update (...params) { | ||
return this.adapter.update(...params) | ||
} | ||
|
||
async createMany (...params) { | ||
return this.adapter.createMany(...params) | ||
} | ||
|
||
async updateMany (...params) { | ||
return this.adapter.updateMany(...params) | ||
} | ||
|
||
async remove (...params) { | ||
return this.adapter.remove(...params) | ||
} | ||
|
||
async find (...params) { | ||
return this.adapter.find(...params) | ||
} | ||
|
||
async findOne (...params) { | ||
return this.adapter.findOne(...params) | ||
} | ||
} | ||
|
||
module.exports = {EntityApi} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,12 @@ | ||
const {RemoteCouchApi} = require('./remote-couch-api') | ||
const {PouchAdapter, IndexedDBPouchAdapter} = require('./pouch-adapter') | ||
const {EntityApi} = require('./entity-api') | ||
const {PouchDB} = require('./pouchdb') | ||
|
||
module.exports = {RemoteCouchApi} | ||
module.exports = { | ||
EntityApi, | ||
PouchDB, | ||
RemoteCouchApi, | ||
PouchAdapter, | ||
IndexedDBPouchAdapter | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
const keyBy = require('lodash/keyBy') | ||
const cloneDeep = require('lodash/cloneDeep') | ||
const get = require('lodash/get') | ||
const {PouchDB} = require('./pouchdb') | ||
const {SchemaInterface} = require('./schema-interface') | ||
const {addSchemaDefaults} = require('./pouch-schema-utils') | ||
|
||
class PouchAdapter extends SchemaInterface { | ||
constructor ({ | ||
schema, pouchDB, pouchDBName, pouchOptions = {}, relations = {} | ||
}) { | ||
if (typeof schema !== 'object' || !schema.properties || !schema.name) { | ||
throw new Error('SchemaInterface usage: schema object with {properties: {}, name} is required') | ||
} | ||
|
||
if (!pouchDB && !pouchDBName) { | ||
throw new Error('PouchAdapter usage: pouchDB instance or pouchDB name are required') | ||
} | ||
|
||
const withDefaults = addSchemaDefaults(schema) | ||
super(withDefaults) | ||
|
||
this.type = schema.name | ||
this.pouchDB = pouchDB || new PouchDB(pouchDBName, pouchOptions) | ||
} | ||
|
||
toEntity (doc) { | ||
const entity = cloneDeep(doc) | ||
if (doc._id) { | ||
entity.id = doc._id | ||
delete entity._id | ||
} | ||
delete entity._rev | ||
return entity | ||
} | ||
|
||
beforeCreate (entity) { | ||
const doc = Object.assign( | ||
this.getSchemaDefaults(), | ||
cloneDeep(entity) | ||
) | ||
if (doc.hasOwnProperty('_id') && !doc._id) { | ||
delete doc._id | ||
} | ||
|
||
if (entity.id) { | ||
doc._id = entity.id | ||
delete doc.id | ||
} | ||
|
||
this.throwIfInvalid(doc) | ||
return doc | ||
} | ||
|
||
beforeUpdate (entity, _rev) { | ||
if (!_rev) { | ||
throw new Error('usage: beforeUpdate requires _rev') | ||
} | ||
const doc = cloneDeep(entity) | ||
doc._rev = _rev | ||
doc._id = entity.id | ||
delete doc.id | ||
|
||
this.throwIfInvalid(doc) | ||
return doc | ||
} | ||
|
||
async list () { | ||
const {docs} = await this.pouchDB.find({ | ||
selector: {type: {'$eq': this.type}}, | ||
limit: Number.MAX_SAFE_INTEGER | ||
}) | ||
|
||
return docs.map(this.toEntity) | ||
} | ||
|
||
async get (id) { | ||
const doc = await this.pouchDB.get(id) | ||
return this.toEntity(doc) | ||
} | ||
|
||
async create (inputRow) { | ||
const doc = this.beforeCreate(inputRow) | ||
|
||
// confusing warning: pouch/couch return `id` on creates (and sometimes other functions) | ||
// but `_id` other times. | ||
// toEntity is going to expect an `_id` because normally docs have it. | ||
const response = await this.pouchDB.post(doc) | ||
const _id = doc._id || response.id | ||
return this.toEntity(Object.assign({_id}, doc)) | ||
} | ||
|
||
async update (id, updatedRow) { | ||
if (typeof id === 'object') { | ||
updatedRow = id | ||
id = updatedRow.id | ||
} else if (updatedRow.id && updatedRow.id !== id) { | ||
throw new Error(`update() received id that does not match id on update`) | ||
} else { | ||
updatedRow.id = id | ||
} | ||
|
||
const {_rev} = await this.pouchDB.get(id) | ||
const doc = this.beforeUpdate(updatedRow, _rev) | ||
|
||
await this.pouchDB.put(doc) | ||
return updatedRow | ||
} | ||
|
||
// this can also returned a mixed bag of [{createdEntity, {error!}}] | ||
// because there are no "all or nothing" bulk insertions in couch, | ||
// some succeed, some might not. | ||
async createMany (inputRows) { | ||
const docs = inputRows.map(row => this.beforeCreate(row)) | ||
|
||
const response = await this.pouchDB.bulkDocs(docs) | ||
// response is not the full doc, | ||
// but we need to get the {id} out of it in case pouch/couch | ||
// was expected to auto-create it. | ||
return inputRows.map((row, index) => { | ||
if (response[index].error || !response[index].ok) { | ||
return response[index] | ||
} | ||
|
||
const _id = response[index].id | ||
|
||
return this.toEntity(Object.assign({_id}, docs[index])) | ||
}) | ||
} | ||
|
||
// TODO: this could be done in batches of like 500ish | ||
// for 10k+ updates to couch that will network fail | ||
async updateMany (rows) { | ||
const revsResponse = await this.pouchDB.find({ | ||
selector: {_id: {'$in': rows.map(row => row.id)}}, | ||
limit: Number.MAX_SAFE_INTEGER, | ||
fields: ['_id', '_rev'] | ||
}) | ||
const revsById = keyBy(revsResponse.docs, '_id') | ||
const docs = rows | ||
.map(row => this.beforeUpdate(row, get(revsById[row.id], '_rev'))) | ||
|
||
// return the original row sent if there was no error | ||
const response = await this.pouchDB.bulkDocs(docs) | ||
return rows.map((row, index) => { | ||
if (response[index].error || !response[index].ok) { | ||
return response[index] | ||
} | ||
return this.toEntity(rows[index]) | ||
}) | ||
} | ||
|
||
async remove (id) { | ||
if (typeof id !== 'string') throw new Error('remove expects id') | ||
const doc = await this.pouchDB.get(id) | ||
// doing this with pouch.remove(id) threw a 404 | ||
return this.pouchDB.put({...doc, _deleted: true}) | ||
} | ||
|
||
async find (selector, limit = Number.MAX_SAFE_INTEGER) { | ||
const {docs} = await this.pouchDB.find({ | ||
selector, | ||
limit | ||
}) | ||
|
||
return docs.map(doc => this.toEntity(doc)) | ||
} | ||
|
||
// returns undefined if doesn't exist | ||
async findOne (selector) { | ||
const [doc] = await this.find(selector) | ||
return doc | ||
? this.toEntity(doc) | ||
: undefined | ||
} | ||
|
||
destroyDatabase () { | ||
return this.pouchDB.destroy() | ||
} | ||
} | ||
|
||
class IndexedDBPouchAdapter extends PouchAdapter { | ||
constructor (options) { | ||
const pouchDBOptions = {pouchDBOptions: {adapter: 'idb'}} | ||
super(Object.assign({}, options, pouchDBOptions)) | ||
} | ||
} | ||
|
||
module.exports = {PouchAdapter, IndexedDBPouchAdapter} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
const baseProperties = { | ||
type: {type: 'string'}, | ||
// this is here but not required for the | ||
// option of having pouch/couch create the _id. | ||
_id: {type: 'string'}, | ||
createdAt: { | ||
type: 'string', | ||
format: 'date-time', | ||
default: () => new Date().toJSON() | ||
}, | ||
createdBy: { | ||
type: 'string' | ||
}, | ||
updatedAt: { | ||
type: 'string', | ||
format: 'date-time', | ||
default: () => new Date().toJSON() | ||
}, | ||
updatedBy: { | ||
type: 'string' | ||
} | ||
} | ||
|
||
const baseRequired = ['type', 'createdAt', 'createdBy'] | ||
|
||
function addSchemaDefaults (schema) { | ||
const properties = Object.assign({}, baseProperties, schema.properties) | ||
properties.type.default = schema.name | ||
|
||
const required = baseRequired.concat(schema.required || []) | ||
return Object.assign({}, schema, {properties, required}) | ||
} | ||
|
||
module.exports = { | ||
addSchemaDefaults | ||
} |
Oops, something went wrong.