mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
faec8177ab
Summary: Add more method overrides to MetaTableData for extra type safety. Use MetaTableData, MetaRowRecord, and getMetaTable in more places. Test Plan: Mostly it just has to compile. Tested manually that types are being checked more strictly now, e.g. by adding a typo to property names. Some type casting has also been removed. Reviewers: dsagal Reviewed By: dsagal Subscribers: dsagal Differential Revision: https://phab.getgrist.com/D3168
207 lines
7.4 KiB
TypeScript
207 lines
7.4 KiB
TypeScript
import {BulkColValues, ColValues, DocAction, isSchemaAction, TableDataAction, UserAction} from 'app/common/DocActions';
|
|
import {DocData} from 'app/common/DocData';
|
|
import {TableData} from 'app/common/TableData';
|
|
import {IndexColumns} from 'app/server/lib/DocStorage';
|
|
|
|
const ACTION_TYPES = new Set(['AddRecord', 'BulkAddRecord', 'UpdateRecord', 'BulkUpdateRecord',
|
|
'RemoveRecord', 'BulkRemoveRecord']);
|
|
|
|
export interface ProcessedAction {
|
|
stored: DocAction[];
|
|
undo: DocAction[];
|
|
retValues: any;
|
|
}
|
|
|
|
export interface OnDemandStorage {
|
|
getNextRowId(tableId: string): Promise<number>;
|
|
fetchActionData(tableId: string, rowIds: number[], colIds?: string[]): Promise<TableDataAction>;
|
|
}
|
|
|
|
/**
|
|
* Handle converting UserActions to DocActions for onDemand tables.
|
|
*/
|
|
export class OnDemandActions {
|
|
|
|
private _tablesMeta: TableData = this._docData.getMetaTable('_grist_Tables');
|
|
private _columnsMeta: TableData = this._docData.getMetaTable('_grist_Tables_column');
|
|
|
|
constructor(private _storage: OnDemandStorage, private _docData: DocData) {}
|
|
|
|
// TODO: Ideally a faster data structure like an index by tableId would be used to decide whether
|
|
// the table is onDemand.
|
|
public isOnDemand(tableId: string): boolean {
|
|
const tableRef = this._tablesMeta.findRow('tableId', tableId);
|
|
// OnDemand tables must have a record in the _grist_Tables metadata table.
|
|
return tableRef ? Boolean(this._tablesMeta.getValue(tableRef, 'onDemand')) : false;
|
|
}
|
|
|
|
/**
|
|
* Convert a UserAction into stored and undo DocActions as well as return values.
|
|
*/
|
|
public processUserAction(action: UserAction): Promise<ProcessedAction> {
|
|
const a = action.map(item => item as any);
|
|
switch (a[0]) {
|
|
case "ApplyUndoActions": return this._doApplyUndoActions(a[1]);
|
|
case "AddRecord": return this._doAddRecord (a[1], a[2], a[3]);
|
|
case "BulkAddRecord": return this._doBulkAddRecord (a[1], a[2], a[3]);
|
|
case "UpdateRecord": return this._doUpdateRecord (a[1], a[2], a[3]);
|
|
case "BulkUpdateRecord": return this._doBulkUpdateRecord(a[1], a[2], a[3]);
|
|
case "RemoveRecord": return this._doRemoveRecord (a[1], a[2]);
|
|
case "BulkRemoveRecord": return this._doBulkRemoveRecord(a[1], a[2]);
|
|
default: throw new Error(`Received unknown action ${action[0]}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Splits an array of UserActions into two separate arrays of normal and onDemand actions.
|
|
*/
|
|
public splitByOnDemand(actions: UserAction[]): [UserAction[], UserAction[]] {
|
|
const normal: UserAction[] = [];
|
|
const onDemand: UserAction[] = [];
|
|
actions.forEach(a => {
|
|
// Check that the actionType can be applied without the sandbox and also that the action
|
|
// is on a data table.
|
|
const isOnDemandAction = ACTION_TYPES.has(a[0] as string);
|
|
const isDataTableAction = typeof a[1] === 'string' && !a[1].startsWith('_grist_');
|
|
if (a[0] === 'ApplyUndoActions') {
|
|
// Split actions inside the undo action array.
|
|
const [undoNormal, undoOnDemand] = this.splitByOnDemand(a[1] as UserAction[]);
|
|
if (undoNormal.length > 0) {
|
|
normal.push(['ApplyUndoActions', undoNormal]);
|
|
}
|
|
if (undoOnDemand.length > 0) {
|
|
onDemand.push(['ApplyUndoActions', undoOnDemand]);
|
|
}
|
|
} else if (isDataTableAction && isOnDemandAction && this.isOnDemand(a[1] as string)) {
|
|
// Check whether the tableId belongs to an onDemand table.
|
|
onDemand.push(a);
|
|
} else {
|
|
normal.push(a);
|
|
}
|
|
});
|
|
return [normal, onDemand];
|
|
}
|
|
|
|
/**
|
|
* Compute the indexes we would like to have, given the current schema.
|
|
*/
|
|
public getDesiredIndexes(): IndexColumns[] {
|
|
const desiredIndexes: IndexColumns[] = [];
|
|
for (const c of this._columnsMeta.getRecords()) {
|
|
const t = this._tablesMeta.getRecord(c.parentId as number);
|
|
if (t && t.onDemand && c.type && (c.type as string).startsWith('Ref:')) {
|
|
desiredIndexes.push({tableId: t.tableId as string, colId: c.colId as string});
|
|
}
|
|
}
|
|
return desiredIndexes;
|
|
}
|
|
|
|
/**
|
|
* Check if an action represents a schema change on an onDemand table.
|
|
*/
|
|
public isSchemaAction(docAction: DocAction): boolean {
|
|
return isSchemaAction(docAction) && this.isOnDemand(docAction[1]);
|
|
}
|
|
|
|
private async _doApplyUndoActions(actions: DocAction[]) {
|
|
const undo: DocAction[] = [];
|
|
for (const a of actions) {
|
|
const converted = await this.processUserAction(a);
|
|
undo.concat(converted.undo);
|
|
}
|
|
return {
|
|
stored: actions,
|
|
undo,
|
|
retValues: null
|
|
};
|
|
}
|
|
|
|
private async _doAddRecord(
|
|
tableId: string,
|
|
rowId: number|null,
|
|
colValues: ColValues
|
|
): Promise<ProcessedAction> {
|
|
if (rowId === null) {
|
|
rowId = await this._storage.getNextRowId(tableId);
|
|
}
|
|
// Set the manualSort to be the same as the rowId. This forces new rows to always be added
|
|
// at the end of the table.
|
|
colValues.manualSort = rowId;
|
|
return {
|
|
stored: [['AddRecord', tableId, rowId, colValues]],
|
|
undo: [['RemoveRecord', tableId, rowId]],
|
|
retValues: rowId
|
|
};
|
|
}
|
|
|
|
private async _doBulkAddRecord(
|
|
tableId: string,
|
|
rowIds: Array<number|null>,
|
|
colValues: BulkColValues
|
|
): Promise<ProcessedAction> {
|
|
|
|
// When unset, we will set the rowId values to count up from the greatest
|
|
// values already in the table.
|
|
if (rowIds[0] === null) {
|
|
const nextRowId = await this._storage.getNextRowId(tableId);
|
|
for (let i = 0; i < rowIds.length; i++) {
|
|
rowIds[i] = nextRowId + i;
|
|
}
|
|
}
|
|
// Set the manualSort values to be the same as the rowIds. This forces new rows to always be
|
|
// added at the end of the table.
|
|
colValues.manualSort = rowIds;
|
|
return {
|
|
stored: [['BulkAddRecord', tableId, rowIds as number[], colValues]],
|
|
undo: [['BulkRemoveRecord', tableId, rowIds as number[]]],
|
|
retValues: rowIds
|
|
};
|
|
}
|
|
|
|
private async _doUpdateRecord(
|
|
tableId: string,
|
|
rowId: number,
|
|
colValues: ColValues
|
|
): Promise<ProcessedAction> {
|
|
const [, , oldRowIds, oldColValues] =
|
|
await this._storage.fetchActionData(tableId, [rowId], Object.keys(colValues));
|
|
return {
|
|
stored: [['UpdateRecord', tableId, rowId, colValues]],
|
|
undo: [['BulkUpdateRecord', tableId, oldRowIds, oldColValues]],
|
|
retValues: null
|
|
};
|
|
}
|
|
|
|
private async _doBulkUpdateRecord(
|
|
tableId: string,
|
|
rowIds: number[],
|
|
colValues: BulkColValues
|
|
): Promise<ProcessedAction> {
|
|
const [, , oldRowIds, oldColValues] =
|
|
await this._storage.fetchActionData(tableId, rowIds, Object.keys(colValues));
|
|
return {
|
|
stored: [['BulkUpdateRecord', tableId, rowIds, colValues]],
|
|
undo: [['BulkUpdateRecord', tableId, oldRowIds, oldColValues]],
|
|
retValues: null
|
|
};
|
|
}
|
|
|
|
private async _doRemoveRecord(tableId: string, rowId: number): Promise<ProcessedAction> {
|
|
const [, , oldRowIds, oldColValues] = await this._storage.fetchActionData(tableId, [rowId]);
|
|
return {
|
|
stored: [['RemoveRecord', tableId, rowId]],
|
|
undo: [['BulkAddRecord', tableId, oldRowIds, oldColValues]],
|
|
retValues: null
|
|
};
|
|
}
|
|
|
|
private async _doBulkRemoveRecord(tableId: string, rowIds: number[]): Promise<ProcessedAction> {
|
|
const [, , oldRowIds, oldColValues] = await this._storage.fetchActionData(tableId, rowIds);
|
|
return {
|
|
stored: [['BulkRemoveRecord', tableId, rowIds]],
|
|
undo: [['BulkAddRecord', tableId, oldRowIds, oldColValues]],
|
|
retValues: null
|
|
};
|
|
}
|
|
}
|