@platform/cell.typesystem
Version:
The 'strongly typed sheets' system of the CellOS.
247 lines (246 loc) • 9.69 kB
JavaScript
import { filter, map } from 'rxjs/operators';
import { TypeDefault, TypeTarget } from '../../TypeSystem.core';
import { Schema, Uri, R } from './common';
import { TypedSheetRef } from './TypedSheetRef';
import { TypedSheetRefs } from './TypedSheetRefs';
export class TypedSheetRow {
constructor(args) {
this._columns = [];
this._prop = {};
this._refs = {};
this._data = {};
this._isLoaded = false;
this._status = 'INIT';
this._loading = {};
this.typename = args.typename;
this.uri = Uri.row(args.uri);
this.index = Number.parseInt(this.uri.key, 10) - 1;
this._columns = args.columns;
this._ctx = args.ctx;
this._sheet = args.sheet;
const cellChange$ = this._ctx.event$.pipe(filter(e => e.type === 'SHEET/change'), map(e => e.payload), filter(e => e.kind === 'CELL'), map(({ to, ns, key }) => ({ to, uri: Uri.parse(Uri.create.cell(ns, key)) })), filter(({ uri }) => uri.ok && uri.type === 'CELL' && uri.parts.ns === this.uri.ns), map(e => (Object.assign(Object.assign({}, e), { uri: e.uri.parts }))));
cellChange$
.pipe(map(e => {
const columnKey = Schema.coord.cell.toColumnKey(e.uri.key);
const columnDef = this._columns.find(def => def.column === columnKey);
return Object.assign(Object.assign({}, e), { columnDef });
}), filter(e => Boolean(e.columnDef)), map(e => (Object.assign(Object.assign({}, e), { target: TypeTarget.parse(e.columnDef.target) }))), filter(e => e.target.isValid && e.target.isInline))
.subscribe(e => {
this.setData(e.columnDef, e.to);
});
}
get status() {
return this._status;
}
get isLoaded() {
return this._isLoaded;
}
get types() {
if (!this._types) {
let map;
let list;
const columns = this._columns;
const row = this.uri;
const types = (this._types = {
get list() {
if (!list) {
list = columns.map(type => {
let uri;
return Object.assign(Object.assign({}, type), { get uri() {
return (uri || (uri = Uri.cell(Uri.create.cell(row.ns, `${type.column}${row.key}`))));
} });
});
}
return list;
},
get map() {
if (!map) {
map = types.list.reduce((acc, type) => {
acc[type.prop] = type;
return acc;
}, {});
}
return map;
},
});
}
return this._types;
}
get props() {
if (!this._props) {
const props = {};
this._columns.forEach(typeDef => {
const name = typeDef.prop;
Object.defineProperty(props, name, {
get: () => this.prop(name).get(),
set: value => this.prop(name).set(value),
});
});
this._props = props;
}
return this._props;
}
toString() {
return this.uri.toString();
}
async load(options = {}) {
if (this.isLoaded && !options.force) {
return this;
}
const { props } = options;
const cacheKey = props ? `load:${props.join(',')}` : 'load';
if (!options.force && this._loading[cacheKey]) {
return this._loading[cacheKey];
}
const promise = new Promise(async (resolve, reject) => {
this._status = 'LOADING';
const ns = this.uri.ns;
const index = this.index;
const sheet = this._sheet;
this.fire({ type: 'SHEET/row/loading', payload: { sheet, index } });
const query = `${this.uri.key}:${this.uri.key}`;
await Promise.all(this._columns
.filter(columnDef => (!props ? true : props.includes(columnDef.prop)))
.map(async (columnDef) => {
const res = await this._ctx.fetch.getCells({ ns, query });
const key = `${columnDef.column}${this.index + 1}`;
this.setData(columnDef, (res.cells || {})[key] || {});
}));
this._status = 'LOADED';
this._isLoaded = true;
this.fire({ type: 'SHEET/row/loaded', payload: { sheet, index } });
delete this._loading[cacheKey];
resolve(this);
});
this._loading[cacheKey] = promise;
return promise;
}
toObject() {
return this._columns.reduce((acc, typeDef) => {
const prop = typeDef.prop;
acc[prop] = this.prop(prop).get();
return acc;
}, {});
}
type(prop) {
const typeDef = this.findColumnByProp(prop);
if (!typeDef) {
const err = `The property '${prop}' is not defined by a column on [${this.uri}]`;
throw new Error(err);
}
return typeDef;
}
prop(name) {
const propname = name;
if (this._prop[propname]) {
return this._prop[propname];
}
const self = this;
const columnDef = this.findColumnByProp(name);
const target = TypeTarget.parse(columnDef.target);
const typename = columnDef.type.typename;
if (!target.isValid) {
const err = `Property '${name}' (column ${columnDef.column}) has an invalid target '${columnDef.target}'.`;
throw new Error(err);
}
const api = {
get() {
const done = (result) => {
if (result === undefined && TypeDefault.isTypeDefaultValue(columnDef.default)) {
result = columnDef.default.value;
}
if (result === undefined && columnDef.type.isArray) {
result = [];
}
return result;
};
if (!target.isValid) {
const err = `Cannot read property '${columnDef.prop}' (column ${columnDef.column}) because the target '${columnDef.target}' is invalid.`;
throw new Error(err);
}
const cell = self._data[columnDef.column] || {};
if (target.isInline) {
return done(TypeTarget.inline(columnDef).read(cell));
}
if (target.isRef) {
if (self._refs[propname]) {
return done(self._refs[propname]);
}
const links = cell.links;
const typeDef = columnDef;
const res = self.getOrCreateRef({ typename, typeDef, links });
self._refs[propname] = res.ref;
return done(res.ref);
}
throw new Error(`Failed to read property '${name}' (column ${columnDef.column}).`);
},
set(value) {
if (target.isInline) {
const isChanged = !R.equals(api.get(), value);
if (isChanged) {
const cell = self._data[columnDef.column] || {};
const data = TypeTarget.inline(columnDef).write({ cell, data: value });
self.fireChange(columnDef, data);
}
}
if (target.isRef) {
const err = `Cannot write to property '${name}' (column ${columnDef.column}) because it is a REF target.`;
throw new Error(err);
}
return self;
},
clear() {
if (target.isInline) {
api.set(undefined);
}
return self;
},
};
this._prop[name] = api;
return api;
}
fire(e) {
this._ctx.event$.next(e);
}
findColumnByProp(prop) {
const res = this._columns.find(def => def.prop === prop);
if (!res) {
const err = `Column-definition for the property '${prop}' not found.`;
throw new Error(err);
}
return res;
}
fireChange(columnDef, to) {
const key = `${columnDef.column}${this.index + 1}`;
const ns = Uri.create.ns(this.uri.ns);
this._ctx.event$.next({
type: 'SHEET/change',
payload: { kind: 'CELL', ns, key, to },
});
}
setData(columnDef, data) {
this._data[columnDef.column] = data;
}
getOrCreateRef(args) {
const { typename, typeDef, links = {} } = args;
const ctx = this._ctx;
const isArray = typeDef.type.isArray;
const { link } = TypedSheetRefs.refLink({ typeDef, links });
const exists = Boolean(link);
const uri = Uri.create.cell(this.uri.ns, `${typeDef.column}${this.index + 1}`);
const parent = {
cell: Uri.cell(uri),
sheet: this._sheet,
};
const ref = isArray
? TypedSheetRefs.create({ typename, typeDef, ctx, parent })
: TypedSheetRef.create({ typename, typeDef, ctx });
return { ref, exists };
}
}
TypedSheetRow.create = (args) => {
return new TypedSheetRow(args);
};
TypedSheetRow.load = (args) => {
return TypedSheetRow.create(args).load();
};