mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
472a9a186e
Summary: for users who don't automatically have deep rights to the document, provide them with attachment metadata only for rows they have access to. This is a little tricky to do efficiently. We provide attachment metadata when an individual table is fetched, rather than on initial document load, so we don't block that load on a full document scan. We provide attachment metadata to a client when we see that we are shipping rows mentioning particular attachments, without making any effort to keep track of the metadata they already have. Test Plan: updated tests Reviewers: dsagal, jarek Reviewed By: dsagal, jarek Differential Revision: https://phab.getgrist.com/D3722
174 lines
7.1 KiB
TypeScript
174 lines
7.1 KiB
TypeScript
/**
|
|
* 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<TableFetchResult>;
|
|
|
|
export class DocData extends ActionDispatcher {
|
|
private _tables: Map<string, TableData> = new Map();
|
|
|
|
private _fetchTableFunc: (tableId: string) => Promise<TableDataAction>;
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
/**
|
|
* Like getTable, but the result knows about the types of its records
|
|
*/
|
|
public getMetaTable<TableId extends keyof SchemaTypes>(tableId: TableId): MetaTableData<TableId> {
|
|
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<string, TableData> {
|
|
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<void> {
|
|
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<void> {
|
|
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 {}
|
|
}
|