|
|
|
class StructuredNode {
|
|
|
|
constructor(json = {}) {
|
|
|
|
if (this.constructor === StructuredNode) {
|
|
|
|
throw new TypeError('Cannot instantiate object of abstract type StructuredNode!')
|
|
|
|
}
|
|
|
|
|
|
|
|
this.json = json
|
|
|
|
this.changes = {}
|
|
|
|
this.definition = StructuredNode.classes[this.constructor.name]
|
|
|
|
this.hooks = []
|
|
|
|
this.deleted = false
|
|
|
|
this.__name__ = this.constructor.name.toLowerCase()
|
|
|
|
// holds a list of all relations that are already loaded
|
|
|
|
|
|
|
|
this._update_json()
|
|
|
|
|
|
|
|
if (StructuredNode.instances[this.__name__][this.json.uid]) {
|
|
|
|
console.warn('two instances of ' + this.__name__ + '?')
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!json.uid) {
|
|
|
|
this.changes = this.json
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_update_json(json) {
|
|
|
|
let old_json = this.json;
|
|
|
|
this.__loaded__ = [];
|
|
|
|
if (json) {
|
|
|
|
this.json = json
|
|
|
|
} else {
|
|
|
|
old_json = {}
|
|
|
|
}
|
|
|
|
|
|
|
|
Object.values(this.definition.rels).forEach(rel => {
|
|
|
|
// if we did not recieve an update for this relation, don't touch it!
|
|
|
|
// we might have gotten an update without relational information
|
|
|
|
if (this.json[rel.field] === undefined) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
this.__loaded__.push(rel.field)
|
|
|
|
const type = StructuredNode.classes[rel.target].klass;
|
|
|
|
|
|
|
|
if (rel.cardinality.endsWith('OrMore')) {
|
|
|
|
this.json[rel.field] = this.json[rel.field].map(x => type.from_json(x))
|
|
|
|
} else {
|
|
|
|
if (this.json[rel.field] === null) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
this.json[rel.field] = type.from_json(this.json[rel.field])
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
Object.values(this.definition.props).forEach(prop => {
|
|
|
|
// no update for this prop? don't put anything in.
|
|
|
|
if (this.json[prop.field] === undefined || this.json[prop.field] === null)
|
|
|
|
return
|
|
|
|
|
|
|
|
this.json[prop.field] = PropertyConversion.deserialize(prop.type, this.json[prop.field])
|
|
|
|
})
|
|
|
|
|
|
|
|
// keep fields from old_json which are not defined in the new json
|
|
|
|
Object.keys(old_json).forEach(key => {
|
|
|
|
if (this.json[key] === undefined) {
|
|
|
|
this.json = old_json[key];
|
|
|
|
// keep loaded state
|
|
|
|
if (this.definition.rels[key]) {
|
|
|
|
this.__loaded__.push(key)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
check(name, new_value) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
_link_relation(name, elm) {
|
|
|
|
const relation = this.definition.rels[name]
|
|
|
|
|
|
|
|
if (!relation) {
|
|
|
|
throw new ApiError(`Cannot find realation ${name} of object ${this.constructor.name}!`, 404, {obj: this, elm: elm})
|
|
|
|
}
|
|
|
|
const type = StructuredNode.classes[relation.target].klass
|
|
|
|
|
|
|
|
if (typeof elm == "string") {
|
|
|
|
elm = type.by_id(elm)
|
|
|
|
}
|
|
|
|
if (elm instanceof type) {
|
|
|
|
if (!elm.exists() || !this.exists()) {
|
|
|
|
throw new ApiError("Cannot connect object before it exists on the backend!")
|
|
|
|
}
|
|
|
|
|
|
|
|
// make results available immediately
|
|
|
|
if (relation.cardinality.endsWith('OrMore')) {
|
|
|
|
if (this.json[name] && !this.json[name].filter(e => e.json.uid === elm.json.uid)) {
|
|
|
|
this.json[name].push(elm)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
this.json[name] = elm
|
|
|
|
}
|
|
|
|
|
|
|
|
return Api.post(`/${this.__name__}/${this.json.uid}/${name}/${elm.json.uid}`).then(json => {
|
|
|
|
this._update_json(json)
|
|
|
|
this.update('link')
|
|
|
|
return this
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_unlink_relation(name, type, elm) {
|
|
|
|
return Api.delete(`/${this.__name__}/${this.json.uid}/${name}/${elm.json.uid}`).then(json => {
|
|
|
|
this._update_json(json)
|
|
|
|
this.update('link')
|
|
|
|
return this
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
_get_relation(name) {
|
|
|
|
if (this.__loaded__.indexOf(name) > -1) {
|
|
|
|
return Promise.resolve(this.json[name])
|
|
|
|
} else {
|
|
|
|
return this.refresh().then(elm => elm.json[name])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_set(name, new_val) {
|
|
|
|
const def = this.definition.props[name]
|
|
|
|
|
|
|
|
if (!def)
|
|
|
|
throw new Error("No attribute " + name + " found!")
|
|
|
|
|
|
|
|
if (this.check(def, new_val)) {
|
|
|
|
this.json[name] = new_val
|
|
|
|
this.changes[name] = PropertyConversion.serialize(def.type, new_val)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
save() {
|
|
|
|
if (this.deleted) {
|
|
|
|
throw new ApiError("Cannot save deleted object!", {obj: this})
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.json.uid) {
|
|
|
|
return Api.put(`/${this.__name__}/${this.json.uid}`, this.changes).then(json => {
|
|
|
|
this._update_json(json)
|
|
|
|
this.changes = {}
|
|
|
|
this.update('save')
|
|
|
|
return this
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
return Api.post('/' + this.__name__, this.changes).then(json => {
|
|
|
|
this._update_json(json)
|
|
|
|
this.changes = {}
|
|
|
|
this.update('created')
|
|
|
|
return this
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
delete() {
|
|
|
|
if (!this.exists()) {
|
|
|
|
throw new ApiError("Cannot delete object that does not exist yet!", {obj: this})
|
|
|
|
}
|
|
|
|
this.deleted = true
|
|
|
|
Api.delete(`/${this.__name__}/${this.json.uid}`).then(r => {
|
|
|
|
this.update('deleted')
|
|
|
|
return this
|
|
|
|
})
|
|
|
|
|
|
|
|
delete StructuredNode.instances[this.__name__][this.json.uid]
|
|
|
|
}
|
|
|
|
|
|
|
|
refresh() {
|
|
|
|
return Api.get(`/${this.__name__}/${this.json.uid}`).then(json => {
|
|
|
|
this._update_json(json)
|
|
|
|
this.update('refresh')
|
|
|
|
return this
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
exists() {
|
|
|
|
return !this.deleted && !!this.json.uid
|
|
|
|
}
|
|
|
|
|
|
|
|
onupdate(callback) {
|
|
|
|
this.hooks.push(callback)
|
|
|
|
}
|
|
|
|
|
|
|
|
update(event) {
|
|
|
|
this.hooks.forEach(cb => {
|
|
|
|
try {
|
|
|
|
cb(this, event)
|
|
|
|
} catch (e) {
|
|
|
|
console.error(`Error while running update on ${this.constructor.name} update function ${cb}: ${e}`)
|
|
|
|
console.error(cb, event, e)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
static registerClassDefinition(klass, props, rels) {
|
|
|
|
console.log(klass, props, rels)
|
|
|
|
StructuredNode.classes[klass.name] = {klass, props, rels}
|
|
|
|
StructuredNode.instances[klass.name.toLowerCase()] = {}
|
|
|
|
}
|
|
|
|
|
|
|
|
static by_id(uid) {
|
|
|
|
return Api.get('/' + this.name.toLowerCase() + '/' + uid)
|
|
|
|
.then(x => this.from_json(x))
|
|
|
|
}
|
|
|
|
|
|
|
|
static filter(attributes = {}, settings = {}) {
|
|
|
|
Object.keys(settings).forEach(key => {
|
|
|
|
attributes['$' + key] = settings[key]
|
|
|
|
})
|
|
|
|
|
|
|
|
return Api.get(`/${this.name.toLowerCase()}?` + Api.queryParams(attributes))
|
|
|
|
.then(json => json.map(x => this.from_json(x)))
|
|
|
|
}
|
|
|
|
|
|
|
|
static find(attributes) {
|
|
|
|
return this.filter(attributes, {limit: 1})
|
|
|
|
.then(r => r.length === 0 ? null : r[0])
|
|
|
|
}
|
|
|
|
|
|
|
|
static from_json(json) {
|
|
|
|
if (json.uid && StructuredNode.instances[this.name.toLowerCase()][json.uid]) {
|
|
|
|
return StructuredNode.instances[this.name.toLowerCase()][json.uid]._update_json(json)
|
|
|
|
}
|
|
|
|
const obj = new this(json)
|
|
|
|
StructuredNode.instances[this.name.toLowerCase()][json.uid] = obj
|
|
|
|
return obj
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
StructuredNode.classes = {}
|
|
|
|
StructuredNode.instances = {}
|
|
|
|
|
|
|
|
class Api {
|
|
|
|
static set_auth(token) {
|
|
|
|
Api.token = token;
|
|
|
|
}
|
|
|
|
|
|
|
|
static get(url) {
|
|
|
|
return fetch('/api' + url, {headers: Api.headers()}).then(r => {
|
|
|
|
if (r.ok) {
|
|
|
|
return r.json()
|
|
|
|
} else {
|
|
|
|
// evaluate response and raise error
|
|
|
|
return ApiError.fromResponse(r)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
static post(url, body="") {
|
|
|
|
return fetch('/api' + url, {headers: Api.headers(), body: JSON.stringify(body), method: 'POST'}).then(r => {
|
|
|
|
if (r.ok) {
|
|
|
|
return r.json()
|
|
|
|
} else {
|
|
|
|
// evaluate response and raise error
|
|
|
|
return ApiError.fromResponse(r)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
static put(url, body) {
|
|
|
|
return fetch('/api' + url, {headers: Api.headers(), body: JSON.stringify(body), method: 'PUT'}).then(r => {
|
|
|
|
if (r.ok) {
|
|
|
|
return r.json()
|
|
|
|
} else {
|
|
|
|
// evaluate response and raise error
|
|
|
|
return ApiError.fromResponse(r)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
static delete(url) {
|
|
|
|
return fetch('/api' + url, {headers: Api.headers(), method: 'DELETE'}).then(r => {
|
|
|
|
if (r.ok) {
|
|
|
|
return r
|
|
|
|
} else {
|
|
|
|
// evaluate response and raise error
|
|
|
|
return ApiError.fromResponse(r)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
static headers() {
|
|
|
|
return {
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
// 'Authorization': 'Bearer ' + Api.token
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static queryParams(...dicts) {
|
|
|
|
return dicts.map(dict =>
|
|
|
|
Object.keys(dict).map(key =>
|
|
|
|
encodeURIComponent(key)+'='+encodeURIComponent(dict[key])).join('&')
|
|
|
|
).join('&')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class ApiError {
|
|
|
|
constructor(msg, code, data) {
|
|
|
|
this.msg = msg
|
|
|
|
this.code = code
|
|
|
|
this.data = data
|
|
|
|
}
|
|
|
|
|
|
|
|
static fromResponse(r) {
|
|
|
|
if (r.headers.get('Content-Type') === 'application/json') {
|
|
|
|
return r.json().then(json => {
|
|
|
|
throw new ApiError(json.msg, r.status, json.data)
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
return r.text().then(text => {
|
|
|
|
throw new ApiError(text, r.status)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class PropertyConversion {
|
|
|
|
static serialize(type, value) {
|
|
|
|
if (PropertyConversion.to[type]) {
|
|
|
|
return PropertyConversion.to[type](value)
|
|
|
|
}
|
|
|
|
return value
|
|
|
|
}
|
|
|
|
|
|
|
|
static deserialize(type, value) {
|
|
|
|
if (PropertyConversion.to[type]) {
|
|
|
|
return PropertyConversion.from[type](value)
|
|
|
|
}
|
|
|
|
return value
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
PropertyConversion.from = {
|
|
|
|
DateTimeProperty: (val) => new Date(val), // assume local time
|
|
|
|
DateProperty: (val) => new Date(val) // ensure no minutes/hours are added/subtracted from date
|
|
|
|
}
|
|
|
|
PropertyConversion.to = {
|
|
|
|
DateTimeProperty: (val) => val.toISOString().slice(0,-1),
|
|
|
|
DateProperty: (val) => `${val.getFullYear()}-${val.getMonth()+1}-${val.getDate()}`
|
|
|
|
}
|