Skip to content

Instantly share code, notes, and snippets.

@misantronic
Last active May 18, 2018 09:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save misantronic/83c41bc89aa4ce44c3ed8213dc75a32d to your computer and use it in GitHub Desktop.
Save misantronic/83c41bc89aa4ce44c3ed8213dc75a32d to your computer and use it in GitHub Desktop.
import { db, binding, vm, misc } from 'baqend';
import {
observable,
decorate,
isObservableArray,
reaction,
IReactionDisposer
} from 'mobx';
function isObject(obj): boolean {
return (
obj &&
!Array.isArray(obj) &&
!isObservableArray(obj) &&
!(obj instanceof Date) &&
typeof obj === 'object'
);
}
function isArray<T = any>(obj): obj is Array<T> {
return Array.isArray(obj) || isObservableArray(obj);
}
const viewModelClasses: { [key: string]: any } = {};
const Entities: binding.EntityFactory<any>[] = [];
const listener = new Map<string, Set<ViewModelBase>>();
function isViewmodel(obj: any): obj is vm.ViewModel {
return Boolean(obj && obj.$ref);
}
async function ensureMetadata(item: any): Promise<void> {
if (item instanceof db.File) {
if (!item.isMetadataLoaded) {
await item.loadMetadata();
}
} else if (isObject(item)) {
for (const [key, value] of Object.entries(item)) {
if (key === 'acl') {
continue;
}
await ensureMetadata(value);
}
} else if (isArray(item)) {
for (const arrayItem of item) {
await ensureMetadata(arrayItem);
}
}
}
function getEntityKeys(item: binding.Entity) {
const Entity = item
.toString()
.replace(/^\/db\//, '')
.match(/^(\w+)\/?.*/)![1];
return { keys: Object.keys(db[Entity].prototype), Entity };
}
function toDate(date: Date | undefined): Date {
return (date ? new Date(date) : undefined) as any;
}
function isEntity(entity: any): entity is binding.Entity {
if (Entities.length === 0) {
Entities.push(
...Object.keys(db.metamodel.entities)
.map(key => key.replace(/^\/db\//, ''))
.map(Entity => db[Entity])
.filter(entity => Boolean(entity))
);
}
return Entities.some(Entitiy => entity instanceof Entitiy);
}
function getFileUrl(file: binding.File): string {
return file.url
.replace(':8080', '.app.baqend.com')
.replace(/\?BAT=(?:\w+)/, '');
}
function convertFilesToJSON(ref: binding.Entity, base: { [key: string]: any }) {
Object.keys(base).map(key => {
const itemRef = ref[key];
if (itemRef instanceof db.File) {
let headers = {};
try {
headers = itemRef.headers;
} catch (_e) {}
base[key] = {
id: itemRef.id,
url: getFileUrl(itemRef),
headers
};
} else if (isArray(itemRef)) {
itemRef.forEach((subItemRef, i) => {
if (subItemRef instanceof db.File) {
let headers = {};
try {
headers = subItemRef.headers;
} catch (_e) {}
base[key][i] = {
id: subItemRef.id,
url: getFileUrl(subItemRef),
headers
};
} else if (isEntity(subItemRef)) {
convertFilesToJSON(subItemRef, base[key][i]);
}
});
} else if (isEntity(itemRef)) {
convertFilesToJSON(itemRef, base[key]);
}
});
}
function getViewmodelConfig(item: binding.Entity) {
const { keys, Entity } = getEntityKeys(item);
if (viewModelClasses[Entity]) {
return { keys, Entity, ViewModel: viewModelClasses[Entity] };
}
function ViewModel(
this: ViewModelBase,
ref: binding.Entity,
type: string,
keys: string[]
) {
this.$ref = ref;
this.$type = type;
this.key = ref.key;
this.id = ref.id;
this.updatedAt = toDate(ref.updatedAt);
this.createdAt = toDate(ref.createdAt);
this.acl = ref.acl.toJSON() as misc.AclJSON;
this.version = ref.version;
if (!this.disposeReactions) {
this.disposeReactions = [];
}
this.disposeReactions.forEach(disposer => disposer());
this.disposeReactions = [];
keys.forEach(key => {
let value;
try {
value = ref[key];
} catch (_e) {}
if (isArray(value)) {
value = value.map(toJSON);
}
if (value instanceof Set) {
const newValue = [];
for (const v of value) {
newValue.push(toJSON(v));
}
value = newValue;
}
this[key] = toJSON(value);
this.disposeReactions.push(
reaction(() => this[key], () => (this.dirty = true))
);
});
this.addListener();
}
ViewModel.prototype = Object.create(ViewModelBase.prototype);
ViewModel.prototype.constructor = ViewModel;
decorate(
ViewModel,
keys.reduce(
(memo, key) => ({
...memo,
[key]: observable
}),
{} as any
)
);
viewModelClasses[Entity] = ViewModel;
return { keys, Entity, ViewModel };
}
export function clearViewModelListeners(): void {
listener.clear();
}
class ViewModelBase implements vm.ViewModel {
public $ref!: binding.Entity;
public $type!: string;
// @ts-ignore
@observable public acl!: misc.AclJSON;
@observable public key!: string;
@observable public id!: string;
@observable public updatedAt!: Date;
@observable public createdAt!: Date;
@observable public version!: number;
@observable public dirty = false;
public disposeReactions!: IReactionDisposer[];
private sync(entity: binding.Entity): binding.Entity {
const { keys, Entity } = getViewmodelConfig(entity);
if (listener.has(this.id)) {
// update all viewModels with the same id
const viewmodels = listener.get(this.id);
if (viewmodels) {
for (const viewModel of viewmodels) {
viewModel.constructor.call(viewModel, entity, Entity, keys);
}
}
}
this.dirty = false;
return entity;
}
public async delete(options?: misc.DeleteOptions): Promise<any> {
return await this.fromJSON().delete(options);
}
public async save(options?: misc.SaveOptions): Promise<any> {
const $ref = this.fromJSON(options);
if (this.dirty) {
await ensureMetadata(this);
}
const result = await $ref.save(options);
return this.sync(result);
}
public async update(options?: misc.SaveOptions): Promise<any> {
const $ref = this.fromJSON(options);
if (this.dirty) {
await ensureMetadata(this);
}
const result = await $ref.update(options);
return this.sync(result);
}
public async insert(options?: misc.InsertOptions): Promise<any> {
const result = await this.fromJSON().insert(options);
return this.sync(result);
}
public async load(_options?: misc.LoadOptions): Promise<any> {
throw new Error('Not implemented yet');
}
public async ready(): Promise<any> {
throw new Error('Not implemented yet');
}
public async attach(): Promise<any> {
throw new Error('Not implemented yet');
}
public async optimisticSave(): Promise<any> {
throw new Error('Not implemented yet');
}
public async validate(): Promise<any> {
throw new Error('Not implemented yet');
}
public getReferencing(): any {
throw new Error('Not implemented yet');
}
public toJSON(options?: misc.ToJSONOptions): any {
const json = this.$ref.toJSON(options);
if (options && options.files) {
convertFilesToJSON(this.$ref, json);
}
return json;
}
public fromJSON(options?: { force?: boolean }): binding.Entity {
const { keys } = getEntityKeys(this.$ref);
// sync changes back
keys.forEach(key => {
try {
const refValue = this.$ref[key];
const viewModelValue = this[key];
if (isViewmodel(viewModelValue)) {
this.$refAssign(key, viewModelValue.$ref);
} else if (
refValue instanceof Set &&
isObservableArray(viewModelValue)
) {
this.$refAssign(
key,
new Set(
viewModelValue.map(
(item: vm.ViewModel) => item.$ref
)
)
);
} else if (isArray(viewModelValue)) {
this.$refAssign(
key,
viewModelValue.map(item => {
if (isViewmodel(item)) {
return item.$ref;
}
return item;
})
);
} else {
this.$refAssign(key, viewModelValue);
}
} catch (_e) {}
});
// handle acl
this.$ref.acl.fromJSON(this.acl);
// TODO: not an optimal solution
if (options && options.force) {
// force update some prop
this.$ref[keys[0]] = this.$ref[keys[0]];
this.dirty = true;
}
return this.$ref;
}
public addListener(): void {
if (!listener.has(this.id)) {
listener.set(this.id, new Set<ViewModelBase>());
}
listener.get(this.id)!.add(this);
}
private $refAssign(key: string, newVal: any): void {
const refValue = this.$ref[key];
// compare Array-changes
if (
isArray(newVal) &&
newVal.length === refValue.length &&
newVal.every((item, i) => item === refValue[i])
) {
return;
}
// compare Set-changes
if (
newVal instanceof Set &&
refValue instanceof Set &&
newVal.size === refValue.size &&
Array.from(newVal).every(item => refValue.has(item))
) {
return;
}
// compare atomic-changes
if (refValue !== newVal) {
this.$ref[key] = newVal;
}
}
}
export function toJSON<T = any>(item: binding.Entity | vm.ViewModel): T {
if (isViewmodel(item)) {
item = item.$ref;
}
if (!isEntity(item)) {
return item as any;
}
const { keys, ViewModel, Entity } = getViewmodelConfig(item);
const viewmodel: ViewModelBase = new ViewModel(item, Entity, keys);
// console.log('viewmodel', viewmodel);
return viewmodel as any;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment