mirror of
				https://github.com/gristlabs/grist-core.git
				synced 2025-06-13 20:53:59 +00:00 
			
		
		
		
	(core) add an option to action summarization to preserve columns entirely
Summary: Action summaries by default will drop rows in bulk changes, keeping only a few of them as examples. This diff allows overriding that, or selectively preserving some columns in their entirety. This is intended for use with webhooks. Test Plan: added test Reviewers: alexmojaki Reviewed By: alexmojaki Differential Revision: https://phab.getgrist.com/D3098
This commit is contained in:
		
							parent
							
								
									35e18cc0ad
								
							
						
					
					
						commit
						6c53f3e820
					
				@ -12,131 +12,152 @@ import toPairs = require('lodash/toPairs');
 | 
			
		||||
import values = require('lodash/values');
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The maximum number of rows in a single bulk change that will be recorded
 | 
			
		||||
 * The default maximum number of rows in a single bulk change that will be recorded
 | 
			
		||||
 * individually.  Bulk changes that touch more than this number of rows
 | 
			
		||||
 * will be summarized only by the number of rows touched.
 | 
			
		||||
 */
 | 
			
		||||
const MAXIMUM_INLINE_ROWS = 10;
 | 
			
		||||
 | 
			
		||||
/** helper function to access summary changes for a specific table by name */
 | 
			
		||||
function _forTable(summary: ActionSummary, tableId: string): TableDelta {
 | 
			
		||||
  return summary.tableDeltas[tableId] || (summary.tableDeltas[tableId] = createEmptyTableDelta());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** helper function to access summary changes for a specific cell by rowId and colId */
 | 
			
		||||
function _forCell(td: TableDelta, rowId: number, colId: string): CellDelta {
 | 
			
		||||
  const cd = td.columnDeltas[colId] || (td.columnDeltas[colId] = {});
 | 
			
		||||
  return cd[rowId] || (cd[rowId] = [null, null]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * helper function to store detailed cell changes for a single row.
 | 
			
		||||
 * Direction parameter is 0 if values are prior values of cells, 1 if values are new values.
 | 
			
		||||
 * Options when producing an action sumary.
 | 
			
		||||
 */
 | 
			
		||||
function _addRow(td: TableDelta, rowId: number, colValues: Action.ColValues,
 | 
			
		||||
                 direction: 0|1) {
 | 
			
		||||
  for (const [colId, colChanges] of toPairs(colValues)) {
 | 
			
		||||
    const cell = _forCell(td, rowId, colId);
 | 
			
		||||
    cell[direction] = [colChanges];
 | 
			
		||||
  }
 | 
			
		||||
export interface ActionSummaryOptions {
 | 
			
		||||
  maximumInlineRows?: number;       // Overrides the maximum number of rows in a
 | 
			
		||||
                                    // single bulk change that will be recorded individually.
 | 
			
		||||
  alwaysPreserveColIds?: string[];  // If set, all cells in these columns are preserved
 | 
			
		||||
                                    // regardless of maximumInlineRows setting.
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** helper function to store detailed cell changes for a set of rows */
 | 
			
		||||
function _addRows(tableId: string, td: TableDelta, rowIds: number[],
 | 
			
		||||
                  colValues: Action.BulkColValues, direction: 0|1) {
 | 
			
		||||
  let rows: Array<[number, number]>;
 | 
			
		||||
  if (rowIds.length <= MAXIMUM_INLINE_ROWS || tableId.startsWith("_grist_")) {
 | 
			
		||||
    rows = [...rowIds.entries()];
 | 
			
		||||
  } else {
 | 
			
		||||
    // if many rows, just take some from start and one from end as examples
 | 
			
		||||
    rows = [...rowIds.slice(0, MAXIMUM_INLINE_ROWS - 1).entries()];
 | 
			
		||||
    rows.push([rowIds.length - 1, rowIds[rowIds.length - 1]]);
 | 
			
		||||
  }
 | 
			
		||||
class ActionSummarizer {
 | 
			
		||||
 | 
			
		||||
  for (const [colId, colChanges] of toPairs(colValues)) {
 | 
			
		||||
    rows.forEach(([idx, rowId]) => {
 | 
			
		||||
      const cell = _forCell(td, rowId, colId);
 | 
			
		||||
      cell[direction] = [colChanges[idx]];
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
  constructor(private _options?: ActionSummaryOptions) {}
 | 
			
		||||
 | 
			
		||||
/** add a rename to a list, avoiding duplicates */
 | 
			
		||||
function _addRename(renames: LabelDelta[], rename: LabelDelta) {
 | 
			
		||||
  if (renames.find(r => r[0] === rename[0] && r[1] === rename[1])) { return; }
 | 
			
		||||
  renames.push(rename);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** add information about an action based on the forward direction */
 | 
			
		||||
function _addForwardAction(summary: ActionSummary, act: DocAction) {
 | 
			
		||||
  const tableId = act[1];
 | 
			
		||||
  if (Action.isAddTable(act)) {
 | 
			
		||||
    summary.tableRenames.push([null, tableId]);
 | 
			
		||||
    for (const info of act[2]) {
 | 
			
		||||
      _forTable(summary, tableId).columnRenames.push([null, info.id]);
 | 
			
		||||
  /** add information about an action based on the forward direction */
 | 
			
		||||
  public addForwardAction(summary: ActionSummary, act: DocAction) {
 | 
			
		||||
    const tableId = act[1];
 | 
			
		||||
    if (Action.isAddTable(act)) {
 | 
			
		||||
      summary.tableRenames.push([null, tableId]);
 | 
			
		||||
      for (const info of act[2]) {
 | 
			
		||||
        this._forTable(summary, tableId).columnRenames.push([null, info.id]);
 | 
			
		||||
      }
 | 
			
		||||
    } else if (Action.isRenameTable(act)) {
 | 
			
		||||
      this._addRename(summary.tableRenames, [tableId, act[2]]);
 | 
			
		||||
    } else if (Action.isRenameColumn(act)) {
 | 
			
		||||
      this._addRename(this._forTable(summary, tableId).columnRenames, [act[2], act[3]]);
 | 
			
		||||
    } else if (Action.isAddColumn(act)) {
 | 
			
		||||
      this._forTable(summary, tableId).columnRenames.push([null, act[2]]);
 | 
			
		||||
    } else if (Action.isRemoveColumn(act)) {
 | 
			
		||||
      this._forTable(summary, tableId).columnRenames.push([act[2], null]);
 | 
			
		||||
    } else if (Action.isAddRecord(act)) {
 | 
			
		||||
      const td = this._forTable(summary, tableId);
 | 
			
		||||
      td.addRows.push(act[2]);
 | 
			
		||||
      this._addRow(td, act[2], act[3], 1);
 | 
			
		||||
    } else if (Action.isUpdateRecord(act)) {
 | 
			
		||||
      const td = this._forTable(summary, tableId);
 | 
			
		||||
      td.updateRows.push(act[2]);
 | 
			
		||||
      this._addRow(td, act[2], act[3], 1);
 | 
			
		||||
    } else if (Action.isBulkAddRecord(act)) {
 | 
			
		||||
      const td = this._forTable(summary, tableId);
 | 
			
		||||
      arrayExtend(td.addRows, act[2]);
 | 
			
		||||
      this._addRows(tableId, td, act[2], act[3], 1);
 | 
			
		||||
    } else if (Action.isBulkUpdateRecord(act)) {
 | 
			
		||||
      const td = this._forTable(summary, tableId);
 | 
			
		||||
      arrayExtend(td.updateRows, act[2]);
 | 
			
		||||
      this._addRows(tableId, td, act[2], act[3], 1);
 | 
			
		||||
    } else if (Action.isReplaceTableData(act)) {
 | 
			
		||||
      const td = this._forTable(summary, tableId);
 | 
			
		||||
      arrayExtend(td.addRows, act[2]);
 | 
			
		||||
      this._addRows(tableId, td, act[2], act[3], 1);
 | 
			
		||||
    }
 | 
			
		||||
  } else if (Action.isRenameTable(act)) {
 | 
			
		||||
    _addRename(summary.tableRenames, [tableId, act[2]]);
 | 
			
		||||
  } else if (Action.isRenameColumn(act)) {
 | 
			
		||||
    _addRename(_forTable(summary, tableId).columnRenames, [act[2], act[3]]);
 | 
			
		||||
  } else if (Action.isAddColumn(act)) {
 | 
			
		||||
    _forTable(summary, tableId).columnRenames.push([null, act[2]]);
 | 
			
		||||
  } else if (Action.isRemoveColumn(act)) {
 | 
			
		||||
    _forTable(summary, tableId).columnRenames.push([act[2], null]);
 | 
			
		||||
  } else if (Action.isAddRecord(act)) {
 | 
			
		||||
    const td = _forTable(summary, tableId);
 | 
			
		||||
    td.addRows.push(act[2]);
 | 
			
		||||
    _addRow(td, act[2], act[3], 1);
 | 
			
		||||
  } else if (Action.isUpdateRecord(act)) {
 | 
			
		||||
    const td = _forTable(summary, tableId);
 | 
			
		||||
    td.updateRows.push(act[2]);
 | 
			
		||||
    _addRow(td, act[2], act[3], 1);
 | 
			
		||||
  } else if (Action.isBulkAddRecord(act)) {
 | 
			
		||||
    const td = _forTable(summary, tableId);
 | 
			
		||||
    arrayExtend(td.addRows, act[2]);
 | 
			
		||||
    _addRows(tableId, td, act[2], act[3], 1);
 | 
			
		||||
  } else if (Action.isBulkUpdateRecord(act)) {
 | 
			
		||||
    const td = _forTable(summary, tableId);
 | 
			
		||||
    arrayExtend(td.updateRows, act[2]);
 | 
			
		||||
    _addRows(tableId, td, act[2], act[3], 1);
 | 
			
		||||
  } else if (Action.isReplaceTableData(act)) {
 | 
			
		||||
    const td = _forTable(summary, tableId);
 | 
			
		||||
    arrayExtend(td.addRows, act[2]);
 | 
			
		||||
    _addRows(tableId, td, act[2], act[3], 1);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** add information about an action based on undo information */
 | 
			
		||||
function _addReverseAction(summary: ActionSummary, act: DocAction) {
 | 
			
		||||
  const tableId = act[1];
 | 
			
		||||
  if (Action.isAddTable(act)) { // undoing, so this is a table removal
 | 
			
		||||
    summary.tableRenames.push([tableId, null]);
 | 
			
		||||
    for (const info of act[2]) {
 | 
			
		||||
      _forTable(summary, tableId).columnRenames.push([info.id, null]);
 | 
			
		||||
  /** add information about an action based on undo information */
 | 
			
		||||
  public addReverseAction(summary: ActionSummary, act: DocAction) {
 | 
			
		||||
    const tableId = act[1];
 | 
			
		||||
    if (Action.isAddTable(act)) { // undoing, so this is a table removal
 | 
			
		||||
      summary.tableRenames.push([tableId, null]);
 | 
			
		||||
      for (const info of act[2]) {
 | 
			
		||||
        this._forTable(summary, tableId).columnRenames.push([info.id, null]);
 | 
			
		||||
      }
 | 
			
		||||
    } else if (Action.isAddRecord(act)) { // undoing, so this is a record removal
 | 
			
		||||
      const td = this._forTable(summary, tableId);
 | 
			
		||||
      td.removeRows.push(act[2]);
 | 
			
		||||
      this._addRow(td, act[2], act[3], 0);
 | 
			
		||||
    } else if (Action.isUpdateRecord(act)) { // undoing, so this is reversal of a record update
 | 
			
		||||
      const td = this._forTable(summary, tableId);
 | 
			
		||||
      this._addRow(td, act[2], act[3], 0);
 | 
			
		||||
    } else if (Action.isBulkAddRecord(act)) { // undoing, this may be reversing a table delete
 | 
			
		||||
      const td = this._forTable(summary, tableId);
 | 
			
		||||
      arrayExtend(td.removeRows, act[2]);
 | 
			
		||||
      this._addRows(tableId, td, act[2], act[3], 0);
 | 
			
		||||
    } else if (Action.isBulkUpdateRecord(act)) { // undoing, so this is reversal of a bulk record update
 | 
			
		||||
      const td = this._forTable(summary, tableId);
 | 
			
		||||
      arrayExtend(td.updateRows, act[2]);
 | 
			
		||||
      this._addRows(tableId, td, act[2], act[3], 0);
 | 
			
		||||
    } else if (Action.isRenameTable(act)) { // undoing - sometimes renames only in undo info
 | 
			
		||||
      this._addRename(summary.tableRenames, [act[2], tableId]);
 | 
			
		||||
    } else if (Action.isRenameColumn(act)) { // undoing - sometimes renames only in undo info
 | 
			
		||||
      this._addRename(this._forTable(summary, tableId).columnRenames, [act[3], act[2]]);
 | 
			
		||||
    } else if (Action.isReplaceTableData(act)) { // undoing
 | 
			
		||||
      const td = this._forTable(summary, tableId);
 | 
			
		||||
      arrayExtend(td.removeRows, act[2]);
 | 
			
		||||
      this._addRows(tableId, td, act[2], act[3], 0);
 | 
			
		||||
    }
 | 
			
		||||
  } else if (Action.isAddRecord(act)) { // undoing, so this is a record removal
 | 
			
		||||
    const td = _forTable(summary, tableId);
 | 
			
		||||
    td.removeRows.push(act[2]);
 | 
			
		||||
    _addRow(td, act[2], act[3], 0);
 | 
			
		||||
  } else if (Action.isUpdateRecord(act)) { // undoing, so this is reversal of a record update
 | 
			
		||||
    const td = _forTable(summary, tableId);
 | 
			
		||||
    _addRow(td, act[2], act[3], 0);
 | 
			
		||||
  } else if (Action.isBulkAddRecord(act)) { // undoing, this may be reversing a table delete
 | 
			
		||||
    const td = _forTable(summary, tableId);
 | 
			
		||||
    arrayExtend(td.removeRows, act[2]);
 | 
			
		||||
    _addRows(tableId, td, act[2], act[3], 0);
 | 
			
		||||
  } else if (Action.isBulkUpdateRecord(act)) { // undoing, so this is reversal of a bulk record update
 | 
			
		||||
    const td = _forTable(summary, tableId);
 | 
			
		||||
    arrayExtend(td.updateRows, act[2]);
 | 
			
		||||
    _addRows(tableId, td, act[2], act[3], 0);
 | 
			
		||||
  } else if (Action.isRenameTable(act)) { // undoing - sometimes renames only in undo info
 | 
			
		||||
    _addRename(summary.tableRenames, [act[2], tableId]);
 | 
			
		||||
  } else if (Action.isRenameColumn(act)) { // undoing - sometimes renames only in undo info
 | 
			
		||||
    _addRename(_forTable(summary, tableId).columnRenames, [act[3], act[2]]);
 | 
			
		||||
  } else if (Action.isReplaceTableData(act)) { // undoing
 | 
			
		||||
    const td = _forTable(summary, tableId);
 | 
			
		||||
    arrayExtend(td.removeRows, act[2]);
 | 
			
		||||
    _addRows(tableId, td, act[2], act[3], 0);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** helper function to access summary changes for a specific table by name */
 | 
			
		||||
  private _forTable(summary: ActionSummary, tableId: string): TableDelta {
 | 
			
		||||
    return summary.tableDeltas[tableId] || (summary.tableDeltas[tableId] = createEmptyTableDelta());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** helper function to access summary changes for a specific cell by rowId and colId */
 | 
			
		||||
  private _forCell(td: TableDelta, rowId: number, colId: string): CellDelta {
 | 
			
		||||
    const cd = td.columnDeltas[colId] || (td.columnDeltas[colId] = {});
 | 
			
		||||
    return cd[rowId] || (cd[rowId] = [null, null]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * helper function to store detailed cell changes for a single row.
 | 
			
		||||
   * Direction parameter is 0 if values are prior values of cells, 1 if values are new values.
 | 
			
		||||
   */
 | 
			
		||||
  private _addRow(td: TableDelta, rowId: number, colValues: Action.ColValues,
 | 
			
		||||
                direction: 0|1) {
 | 
			
		||||
    for (const [colId, colChanges] of toPairs(colValues)) {
 | 
			
		||||
      const cell = this._forCell(td, rowId, colId);
 | 
			
		||||
      cell[direction] = [colChanges];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** helper function to store detailed cell changes for a set of rows */
 | 
			
		||||
  private _addRows(tableId: string, td: TableDelta, rowIds: number[],
 | 
			
		||||
                 colValues: Action.BulkColValues, direction: 0|1) {
 | 
			
		||||
    const maximumInlineRows = this._options?.maximumInlineRows || MAXIMUM_INLINE_ROWS;
 | 
			
		||||
    const limitRows: boolean = rowIds.length > maximumInlineRows && !tableId.startsWith("_grist_");
 | 
			
		||||
    let selectedRows: Array<[number, number]> = [];
 | 
			
		||||
    if (limitRows) {
 | 
			
		||||
      // if many rows, just take some from start and one from end as examples
 | 
			
		||||
      selectedRows = [...rowIds.slice(0, maximumInlineRows - 1).entries()];
 | 
			
		||||
      selectedRows.push([rowIds.length - 1, rowIds[rowIds.length - 1]]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const alwaysPreserveColIds = new Set(this._options?.alwaysPreserveColIds || []);
 | 
			
		||||
    for (const [colId, colChanges] of toPairs(colValues)) {
 | 
			
		||||
      const addCellToSummary = (rowId: number, idx: number) => {
 | 
			
		||||
        const cell = this._forCell(td, rowId, colId);
 | 
			
		||||
        cell[direction] = [colChanges[idx]];
 | 
			
		||||
      };
 | 
			
		||||
      if (!limitRows || alwaysPreserveColIds.has(colId)) {
 | 
			
		||||
        rowIds.forEach(addCellToSummary);
 | 
			
		||||
      } else {
 | 
			
		||||
        selectedRows.forEach(([idx, rowId]) => addCellToSummary(rowId, idx));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** add a rename to a list, avoiding duplicates */
 | 
			
		||||
  private _addRename(renames: LabelDelta[], rename: LabelDelta) {
 | 
			
		||||
    if (renames.find(r => r[0] === rename[0] && r[1] === rename[1])) { return; }
 | 
			
		||||
    renames.push(rename);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -144,13 +165,14 @@ function _addReverseAction(summary: ActionSummary, act: DocAction) {
 | 
			
		||||
 * Summarize the tabular changes that a LocalActionBundle results in, in a form
 | 
			
		||||
 * that will be suitable for composition.
 | 
			
		||||
 */
 | 
			
		||||
export function summarizeAction(body: LocalActionBundle): ActionSummary {
 | 
			
		||||
export function summarizeAction(body: LocalActionBundle, options?: ActionSummaryOptions): ActionSummary {
 | 
			
		||||
  const summarizer = new ActionSummarizer(options);
 | 
			
		||||
  const summary = createEmptyActionSummary();
 | 
			
		||||
  for (const act of getEnvContent(body.stored)) {
 | 
			
		||||
    _addForwardAction(summary, act);
 | 
			
		||||
    summarizer.addForwardAction(summary, act);
 | 
			
		||||
  }
 | 
			
		||||
  for (const act of Array.from(body.undo).reverse()) {
 | 
			
		||||
    _addReverseAction(summary, act);
 | 
			
		||||
    summarizer.addReverseAction(summary, act);
 | 
			
		||||
  }
 | 
			
		||||
  // Name tables consistently, by their ultimate name, now we know it.
 | 
			
		||||
  for (const renames of summary.tableRenames) {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user