UNPKG

@platform/cell.typesystem

Version:

The 'strongly typed sheets' system of the CellOS.

247 lines (246 loc) 9.69 kB
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(); };