/** * DocData maintains all underlying data for a Grist document, knows how to load it, * subscribes to actions which change it, and forwards those actions to individual tables. * It also provides the interface to apply actions to data. */ import {DocumentSettings} from 'app/common/DocumentSettings'; import {safeJsonParse} from 'app/common/gutil'; import {schema, SchemaTypes} from 'app/common/schema'; import fromPairs = require('lodash/fromPairs'); import groupBy = require('lodash/groupBy'); import {ActionDispatcher} from './ActionDispatcher'; import {TableFetchResult} from './ActiveDocAPI'; import { BulkColValues, ColInfo, ColInfoWithId, ColValues, DocAction, RowRecord, TableDataAction } from './DocActions'; import {ColTypeMap, MetaRowRecord, MetaTableData, TableData} from './TableData'; type FetchTableFunc = (tableId: string) => Promise; export class DocData extends ActionDispatcher { private _tables: Map = new Map(); private _fetchTableFunc: (tableId: string) => Promise; /** * If metaTableData is not supplied, then any tables needed should be loaded manually, * using syncTable(). All column types will be set to Any, which will affect default * values. */ constructor(fetchTableFunc: FetchTableFunc, metaTableData: {[tableId: string]: TableDataAction} | null) { super(); // Wrap fetchTableFunc slightly to handle any extra attachment data that // may come along for the ride. this._fetchTableFunc = async (tableId: string) => { const {tableData, attachments} = await fetchTableFunc(tableId); if (attachments) { // Back-end doesn't keep track of which attachments we already have, // so there may be duplicates of rows we already have - but happily // BulkAddRecord overwrites duplicates now. this.receiveAction(attachments); } return tableData; }; if (metaTableData === null) { return; } // Create all meta tables, and populate data we already have. for (const tableId in schema) { if (schema.hasOwnProperty(tableId)) { const colTypes: ColTypeMap = (schema as any)[tableId]; this._tables.set(tableId, this.createTableData(tableId, metaTableData[tableId], colTypes)); } } // Build a map from tableRef to [columnRecords] const colsByTable = groupBy(this._tables.get('_grist_Tables_column')!.getRecords(), 'parentId'); for (const t of this._tables.get('_grist_Tables')!.getRecords()) { const tableId = t.tableId as string; const colRecords: RowRecord[] = colsByTable[t.id] || []; const colTypes = fromPairs(colRecords.map(c => [c.colId, c.type])); this._tables.set(tableId, this.createTableData(tableId, null, colTypes)); } } /** * Creates a new TableData object. A derived class may override to return an object derived from TableData. */ public createTableData(tableId: string, tableData: TableDataAction|null, colTypes: ColTypeMap): TableData { return new (tableId in schema ? MetaTableData : TableData)(tableId, tableData, colTypes); } /** * Returns the TableData object for the requested table. */ public getTable(tableId: string): TableData|undefined { return this._tables.get(tableId); } public async requireTable(tableId: string): Promise { await this.fetchTable(tableId); const td = this._tables.get(tableId); if (!td) { throw new Error(`could not fetch table: ${tableId}`); } return td; } /** * Like getTable, but the result knows about the types of its records */ public getMetaTable(tableId: TableId): MetaTableData { return this.getTable(tableId) as any; } /** * Returns an unsorted list of all tableIds in this doc, including both metadata and user tables. */ public getTables(): ReadonlyMap { return this._tables; } /** * Fetches the data for tableId if needed, and returns a promise that is fulfilled when the data * is loaded. */ public fetchTable(tableId: string, force?: boolean): Promise { const table = this._tables.get(tableId); if (!table) { throw new Error(`DocData.fetchTable: unknown table ${tableId}`); } return (!table.isLoaded || force) ? table.fetchData(this._fetchTableFunc) : Promise.resolve(); } /** * Fetches the data for tableId unconditionally, and without knowledge of its metadata. * Columns will be assumed to have type 'Any'. */ public async syncTable(tableId: string): Promise { const tableData = await this._fetchTableFunc(tableId); const colTypes = fromPairs(Object.keys(tableData[3]).map(c => [c, 'Any'])); colTypes.id = 'Any'; this._tables.set(tableId, this.createTableData(tableId, tableData, colTypes)); } /** * Handles an action received from the server, by forwarding it to the appropriate TableData * object. */ public receiveAction(action: DocAction): void { // Look up TableData before processing the action in case we rename or remove it. const tableId: string = action[1]; const table = this._tables.get(tableId); this.dispatchAction(action); // Forward all actions to per-table TableData objects. if (table) { table.receiveAction(action); } } public docInfo(): MetaRowRecord<'_grist_DocInfo'> { const docInfoTable = this.getMetaTable('_grist_DocInfo'); return docInfoTable.getRecord(1)!; } public docSettings(): DocumentSettings { return safeJsonParse(this.docInfo().documentSettings, {}); } // ---- The following methods implement ActionDispatcher interface ---- protected onAddTable(action: DocAction, tableId: string, columns: ColInfoWithId[]): void { const colTypes = fromPairs(columns.map(c => [c.id, c.type])); this._tables.set(tableId, this.createTableData(tableId, null, colTypes)); } protected onRemoveTable(action: DocAction, tableId: string): void { this._tables.delete(tableId); } protected onRenameTable(action: DocAction, oldTableId: string, newTableId: string): void { const table = this._tables.get(oldTableId); if (table) { this._tables.set(newTableId, table); this._tables.delete(oldTableId); } } // tslint:disable:no-empty protected onAddRecord(action: DocAction, tableId: string, rowId: number, colValues: ColValues): void {} protected onUpdateRecord(action: DocAction, tableId: string, rowId: number, colValues: ColValues): void {} protected onRemoveRecord(action: DocAction, tableId: string, rowId: number): void {} protected onBulkAddRecord(action: DocAction, tableId: string, rowIds: number[], colValues: BulkColValues): void {} protected onBulkUpdateRecord(action: DocAction, tableId: string, rowIds: number[], colValues: BulkColValues): void {} protected onBulkRemoveRecord(action: DocAction, tableId: string, rowIds: number[]) {} protected onReplaceTableData(action: DocAction, tableId: string, rowIds: number[], colValues: BulkColValues): void {} protected onAddColumn(action: DocAction, tableId: string, colId: string, colInfo: ColInfo): void {} protected onRemoveColumn(action: DocAction, tableId: string, colId: string): void {} protected onRenameColumn(action: DocAction, tableId: string, oldColId: string, newColId: string): void {} protected onModifyColumn(action: DocAction, tableId: string, colId: string, colInfo: ColInfo): void {} }