(core) Parse string cell values in Doc API and Imports

Summary:
- Adds a function `parseUserAction` for parsing strings in UserActions to `ValueParser.ts`
- Adds a boolean option `parseStrings` to use `parseUserAction` in `ActiveDoc.applyUserActions`, off by default.
- Uses `parseStrings` by default in DocApi (set `?noparse=true` in a request to disable) when adding/updating records through the `/data` or `/records` endpoints or in general with the `/apply` endpoint.
- Uses `parseStrings` for various actions in `ActiveDocImport`. Since most types are parsed in Python before these actions are constructed, this only affects references, which still look like errors in the import preview. Importing references can also easily still run into more complicated problems discussed in https://grist.slack.com/archives/C0234CPPXPA/p1639514844028200

Test Plan:
- Added tests to DocApi to compare behaviour with and without string parsing.
- Added a new browser test, fixture doc, and fixture CSV to test importing a file containing references.

Reviewers: georgegevoian

Reviewed By: georgegevoian

Differential Revision: https://phab.getgrist.com/D3183
This commit is contained in:
Alex Hall
2021-12-16 15:45:05 +02:00
parent 9d62e67369
commit d1a848b44a
5 changed files with 104 additions and 21 deletions

View File

@@ -11,6 +11,7 @@ export interface ApplyUAOptions {
otherId?: number; // For undo/redo; the actionNum of the original action to which it applies.
linkId?: number; // For bundled actions, actionNum of the previous action in the bundle.
bestEffort?: boolean; // If set, action may be applied in part if it cannot be applied completely.
parseStrings?: boolean; // If true, parses string values in some actions based on the column
}
export interface ApplyUAResult {

View File

@@ -1,8 +1,9 @@
import {csvDecodeRow} from 'app/common/csvFormat';
import {BulkColValues, CellValue, ColValues, UserAction} from 'app/common/DocActions';
import {DocData} from 'app/common/DocData';
import {DocumentSettings} from 'app/common/DocumentSettings';
import {getReferencedTableId, isFullReferencingType} from 'app/common/gristTypes';
import * as gristTypes from 'app/common/gristTypes';
import {getReferencedTableId, isFullReferencingType} from 'app/common/gristTypes';
import * as gutil from 'app/common/gutil';
import {safeJsonParse} from 'app/common/gutil';
import {getCurrency, NumberFormatOptions} from 'app/common/NumberFormat';
@@ -11,6 +12,7 @@ import {parseDateStrict, parseDateTime} from 'app/common/parseDate';
import {MetaRowRecord, TableData} from 'app/common/TableData';
import {DateFormatOptions, DateTimeFormatOptions, formatDecoded, FormatOptions} from 'app/common/ValueFormatter';
import flatMap = require('lodash/flatMap');
import mapValues = require('lodash/mapValues');
export class ValueParser {
@@ -213,6 +215,7 @@ export const valueParserClasses: { [type: string]: typeof ValueParser } = {
RefList: ReferenceListParser,
};
const identity = (value: string) => value;
/**
* Returns a function which can parse strings into values appropriate for
@@ -228,7 +231,7 @@ export function createParserRaw(
const parser = new cls(type, widgetOpts, docSettings);
return parser.cleanParse.bind(parser);
}
return value => value;
return identity;
}
/**
@@ -270,3 +273,68 @@ export function createParser(
return createParserRaw(type, widgetOpts, docSettings);
}
/**
* Returns a copy of `colValues` with string values parsed according to the type and options of each column.
* `bulk` should be `true` if `colValues` is of type `BulkColValues`.
*/
function parseColValues<T extends ColValues | BulkColValues>(
tableId: string, colValues: T, docData: DocData, bulk: boolean
): T {
const columnsTable = docData.getMetaTable('_grist_Tables_column');
const tablesTable = docData.getMetaTable('_grist_Tables');
const tableRef = tablesTable.findRow('tableId', tableId);
if (!tableRef) {
return colValues;
}
return mapValues(colValues, (values, colId) => {
const colRef = columnsTable.findMatchingRowId({colId, parentId: tableRef});
if (!colRef) {
// Column not found - let something else deal with that
return values;
}
const parser = createParser(docData, colRef);
// Optimisation: If there's no special parser for this column type, do nothing
if (parser === identity) {
return values;
}
function parseIfString(val: any) {
return typeof val === "string" ? parser(val) : val;
}
if (bulk) {
if (!Array.isArray(values)) { // in case of bad input
return values;
}
// `colValues` is of type `BulkColValues`
return (values as CellValue[]).map(parseIfString);
} else {
// `colValues` is of type `ColValues`, `values` is just one value
return parseIfString(values);
}
});
}
export function parseUserAction(ua: UserAction, docData: DocData): UserAction {
const actionType = ua[0] as string;
let parseBulk: boolean;
if (['AddRecord', 'UpdateRecord'].includes(actionType)) {
parseBulk = false;
} else if (['BulkAddRecord', 'BulkUpdateRecord', 'ReplaceTableData'].includes(actionType)) {
parseBulk = true;
} else {
return ua;
}
ua = ua.slice();
const tableId = ua[1] as string;
const lastIndex = ua.length - 1;
const colValues = ua[lastIndex] as ColValues | BulkColValues;
ua[lastIndex] = parseColValues(tableId, colValues, docData, parseBulk);
return ua;
}