type-arango
Version:
ArangoDB Foxx decorators and utilities for TypeScript
224 lines (183 loc) • 6.23 kB
text/typescript
import {getDocumentForContainer, Document} from '.'
import {DocumentData} from '../types'
import {AttributeNotInEntityError, MissingKeyError, AttributeIsNotARelationError, SymbolKeysNotSupportedError} from '../errors'
import {db, isFoxx} from '../utils'
// const nativeKeys = ['constructor','toString']
const unenumerable = ['_saveKeys', '_collection', '_relations']
const globalAttributes = ['_key', '_rev', '_id', '_oldRev']
const accessibleKeys = globalAttributes.concat('_saveKeys')
interface SaveOptions extends ArangoDB.UpdateOptions, ArangoDB.InsertOptions {
update: boolean
}
export class Entity {
public _collection: ArangoDB.Collection
public _saveKeys: string[] = []
public _id?: string
public _key?: string
public _rev?: string
public _from?: string | any
public _to?: string | any
[key: string]: any
static get _doc(){
return getDocumentForContainer(this)
}
static get _db(){
return db
}
get _doc() {
return (this.constructor as typeof Entity)._doc as Document
}
static get _relations(){
return Object.keys(this._doc.relation)
}
constructor(doc?: DocumentData | string, isSync: boolean = false) {
if(typeof doc === 'string')
doc = {_key:doc}
if(doc){
if(!isSync) this._saveKeys = Object.keys(doc).filter(k => (this as any)[k] !== (doc as DocumentData)[k])
this.merge(doc)
}
const { _doc } = this
const constructor = (this.constructor as typeof Entity)
const attribute = _doc.attribute
const keys = Object.keys(attribute).concat(accessibleKeys)
this._collection = isFoxx() ? _doc.col!.db : {} as any
// hide some attrs
unenumerable.forEach(attr => Object.defineProperty(this, attr, {enumerable:false}))
if(!isFoxx())
return this
// setup proxy
return new Proxy(this, {
// get(target: any, key: string){
// if(nativeKeys.includes(key)) return target[key]
//
// // const cleanKey = key.startsWith('_') ? key.substr(1) : key
//
// // // return relation values as entity._attribute
// // if(key.startsWith('_') && _doc.relation[cleanKey]){
// // return target[cleanKey]
// // }
// //
// // // support relation entity load via entity.relation()
// // if(_doc.relation[key]) return _doc.resolveRelation.bind(_doc, target, key)
//
// return target[key]
// },
set(target: any, key: string, val: any){
if(typeof key === 'symbol')
throw new SymbolKeysNotSupportedError()
if(_doc.relation[key]){
target._saveKeys.push(key)
target[key] = val
return true
}
// check key
else if(!keys.includes(String(key))) {
throw new AttributeNotInEntityError(constructor.name, key)
}
// joi validation
else if(attribute[key]){
const {error, value} = attribute[key].schema!.validate(val)
if(error) throw error
val = value
}
if(target[key] === val) return true
// set
target._saveKeys.push(key)
target[key] = val
return true
}
})
}
/**
* Returns related entity
*/
related(attribute: string, attributes?: string[]) {
if(!this._doc.relation[attribute])
throw new AttributeIsNotARelationError(this.name, attribute)
return this._doc.resolveRelation(this, attribute, attributes)
}
/**
* Creates the document by using .save({update:false})
*/
insert(){
return this.save({update:false})
}
/**
* Alias for insert
* @deprecated
*/
create(){
console.warn('Using entity.create is deprecated, use entity.insert instead')
return this.insert()
}
/**
* Merges `obj` into `this`
*/
merge(...obj: DocumentData[]){
return Object.assign(this, ...obj)
}
/**
* Converts entity to object (strips property listeners)
*/
toObject(){
return Object.assign({}, this)
}
/**
* Saves changed attributes to database. Creates a new document when no _key is available.
* Use the option {update:false} to always create a new document even when a _key is provided.
*/
save(options: Partial<SaveOptions> = {}){
const { _saveKeys, _key, _doc, _collection } = this
if(options.update === undefined) options.update = true
if(!_saveKeys.length)
return this
// accumulate changed values from _saveKeys into object
let data = _saveKeys.reduce((o: any, key: string) => {
const v = this[key]
const relation = _doc.relation[key]
if(relation){
// save assigned entity
if(v instanceof Entity && v._saveKeys.length) v.save()
o[key] = v instanceof Entity ? v._key : v
}
else
o[key] = v
return o
}, {})
let res
// insert / update
if(options.update && _key) {
data = _doc.emitBefore('update', data)
res = _doc.emitAfter('insert', _collection.update({_key}, data, options) )
}
else {
data = _doc.emitBefore('insert', data)
res = _doc.emitAfter('insert', _collection.insert(data, Object.assign(options, {returnNew:true})).new )
}
this.merge(res)
// reset
this._saveKeys = []
return this
}
/**
* Replaces the document with the provided doc, ignoring _saveKeys
*/
replace(doc: DocumentData, options?: ArangoDB.ReplaceOptions){
const { _key, _doc, _collection } = this
if(!_key)
throw new MissingKeyError(this.constructor.name)
doc = _doc.emitBefore('replace', doc)
return this.merge( _doc.emitAfter('insert', _collection.replace({_key}, doc, options) ))
}
/**
* Removes the document
*/
remove(options?: ArangoDB.RemoveOptions){
const { _key, _doc, _collection } = this
if(!_key)
throw new MissingKeyError(this.constructor.name)
_doc.emitBefore('remove', _key)
return this.merge(_doc.emitAfter('remove', _collection.remove({_key}, options)))
}
}