You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
gristlabs_grist-core/app/common/gristTypes.ts

243 lines
8.5 KiB

import {CellValue} from 'app/common/DocActions';
import isString = require('lodash/isString');
// tslint:disable:object-literal-key-quotes
export type GristType = 'Any' | 'Attachments' | 'Blob' | 'Bool' | 'Choice' | 'Date' | 'DateTime' |
'Id' | 'Int' | 'ManualSortPos' | 'Numeric' | 'PositionNumber' | 'Ref' | 'RefList' | 'Text';
// Letter codes for CellValue types encoded as [code, args...] tuples.
export type GristObjType = 'L' | 'D' | 'd' | 'R' | 'E' | 'P';
export const MANUALSORT = 'manualSort';
// This mapping includes both the default value, and its representation for SQLite.
const _defaultValues: {[key in GristType]: [CellValue, string]} = {
'Any': [ null, "NULL" ],
'Attachments': [ null, "NULL" ],
'Blob': [ null, "NULL" ],
// Bool is only supported by SQLite as 0 and 1 values.
'Bool': [ false, "0" ],
'Choice': [ '', "''" ],
'Date': [ null, "NULL" ],
'DateTime': [ null, "NULL" ],
'Id': [ 0, "0" ],
'Int': [ 0, "0" ],
// Note that "1e999" is a way to store Infinity into SQLite. This is verified by "Defaults"
// tests in DocStorage.js. See also http://sqlite.1065341.n5.nabble.com/Infinity-td55327.html.
'ManualSortPos': [ Number.POSITIVE_INFINITY, "1e999" ],
'Numeric': [ 0, "0" ],
'PositionNumber': [ Number.POSITIVE_INFINITY, "1e999" ],
'Ref': [ 0, "0" ],
'RefList': [ null, "NULL" ],
'Text': [ '', "''" ],
};
/**
* Given a grist column type (e.g Text, Numeric, ...) returns the default value for that type.
* If options.sqlFormatted is true, returns the representation of the value for SQLite.
*/
export function getDefaultForType(colType: string, options: {sqlFormatted?: boolean} = {}) {
const type = extractTypeFromColType(colType);
return (_defaultValues[type as GristType] || _defaultValues.Any)[options.sqlFormatted ? 1 : 0];
}
/**
* Returns whether a value (as received in a DocAction) represents a custom object.
*/
export function isObject(value: CellValue): value is [string, any?] {
return Array.isArray(value);
}
/**
* Returns whether a value (as received in a DocAction) represents a raised exception.
*/
export function isRaisedException(value: CellValue): boolean {
return Array.isArray(value) && value[0] === 'E';
}
/**
* Returns whether a value (as received in a DocAction) represents a list or is null,
* which is a valid value for list types in grist.
*/
export function isListOrNull(value: CellValue): boolean {
return value === null || (Array.isArray(value) && value[0] === 'L');
}
/**
* Returns whether a value (as received in a DocAction) represents an empty list.
*/
export function isEmptyList(value: CellValue): boolean {
return Array.isArray(value) && value.length === 1 && value[0] === 'L';
}
/**
* Returns whether a value (as received in a DocAction) represents a "Pending" value.
*/
export function isPending(value: CellValue): boolean {
return Array.isArray(value) && value[0] === 'P';
}
/**
* Formats a raised exception (a value for which isRaisedException is true) for display in a cell.
* This is designed to look somewhat similar to Excel, e.g. #VALUE or #DIV/0!"
*/
export function formatError(value: [string, ...any[]]): string {
const errName = value[1];
switch (errName) {
case 'ZeroDivisionError': return '#DIV/0!';
case 'UnmarshallableError': return value[3] || ('#' + errName);
case 'InvalidTypedValue': return `#Invalid ${value[2]}: ${value[3]}`;
}
return '#' + errName;
}
function isNumber(v: CellValue) { return typeof v === 'number' || typeof v === 'boolean'; }
function isNumberOrNull(v: CellValue) { return isNumber(v) || v === null; }
function isBoolean(v: CellValue) { return typeof v === 'boolean' || v === 1 || v === 0; }
function isNormalValue(value: CellValue) {
return !(Array.isArray(value) && (value[0] === 'E' || value[0] === 'P'));
}
/**
* Map of Grist type to an "isRightType" checker function, which determines if a given values type
* matches the declared type of the column.
*/
const rightType: {[key in GristType]: (value: CellValue) => boolean} = {
Any: isNormalValue,
Attachments: isListOrNull,
Text: isString,
Blob: isString,
Int: isNumberOrNull,
Bool: isBoolean,
Date: isNumberOrNull,
DateTime: isNumberOrNull,
Numeric: isNumberOrNull,
Id: isNumber,
PositionNumber: isNumber,
ManualSortPos: isNumber,
Ref: isNumber,
RefList: isListOrNull,
Choice: (v: CellValue, options?: any) => {
// TODO widgets options should not be used outside of the client. They are an instance of
// modelUtil.jsonObservable, passed in by FieldBuilder.
if (v === '') {
// Accept empty-string values as valid
return true;
} else if (options) {
const choices = options().choices;
return Array.isArray(choices) && choices.includes(v);
} else {
return false;
}
}
};
export function isRightType(type: string): undefined | ((value: CellValue) => boolean) {
return rightType[type as GristType];
}
export function extractTypeFromColType(type: string): string {
if (!type) { return type; }
const colon = type.indexOf(':');
return (colon === -1 ? type : type.slice(0, colon));
}
/**
* Convert pureType to Grist python type name, e.g. 'Ref' to 'Reference'.
*/
export function getGristType(pureType: string): string {
switch (pureType) {
case 'Ref': return 'Reference';
case 'RefList': return 'ReferenceList';
default: return pureType;
}
}
/**
* Converts SQL type strings produced by the Sequelize library into its corresponding
* Grist type. The list of types is based on an analysis of SQL type string outputs
* produced by the Sequelize library (mostly covered in lib/data-types.js). Some
* additional engine/dialect specific types are detailed in dialect directories.
*
* TODO: A handful of exotic SQL types (mostly from PostgreSQL) will currently throw an
* Error, rather than returning a type. Further testing is required to determine
* whether Grist can manage those data types.
*
* @param {String} sqlType A string produced by Sequelize's describeTable query
* @return {String} The corresponding Grist type string
* @throws {Error} If the sqlType is unrecognized or unsupported
*/
export function sequelizeToGristType(sqlType: string): GristType {
// Sequelize type strings can include parens (e.g., `CHAR(10)`). This function
// ignores those additional details when determining the Grist type.
let endMarker = sqlType.length;
const parensMarker = sqlType.indexOf('(');
endMarker = parensMarker > 0 ? parensMarker : endMarker;
// Type strings might also include a space after the basic type description.
// The type `DOUBLE PRECISION` is one such example, but modifiers or attributes
// relevant to the type might also appear after the type itself (e.g., UNSIGNED,
// NONZERO). These are ignored when determining the Grist type.
const spaceMarker = sqlType.indexOf(' ');
endMarker = spaceMarker > 0 && spaceMarker < endMarker ? spaceMarker : endMarker;
switch (sqlType.substring(0, endMarker)) {
case 'INTEGER':
case 'BIGINT':
case 'SMALLINT':
case 'INT':
return 'Int';
case 'NUMBER':
case 'FLOAT':
case 'DECIMAL':
case 'NUMERIC':
case 'REAL':
case 'DOUBLE':
case 'DOUBLE PRECISION':
return 'Numeric';
case 'BOOLEAN':
case 'TINYINT':
return 'Bool';
case 'STRING':
case 'CHAR':
case 'TEXT':
case 'UUID':
case 'UUIDV1':
case 'UUIDV4':
case 'VARCHAR':
case 'NVARCHAR':
case 'TINYTEXT':
case 'MEDIUMTEXT':
case 'LONGTEXT':
case 'ENUM':
return 'Text';
case 'TIME':
case 'DATE':
case 'DATEONLY':
case 'DATETIME':
case 'NOW':
return 'Text';
case 'BLOB':
case 'TINYBLOB':
case 'MEDIUMBLOB':
case 'LONGBLOB':
// TODO: Passing binary data to the Sandbox is throwing Errors. Proper support
// for these Blob data types requires some more investigation.
throw new Error('SQL type: `' + sqlType + '` is currently unsupported');
case 'NONE':
case 'HSTORE':
case 'JSON':
case 'JSONB':
case 'VIRTUAL':
case 'ARRAY':
case 'RANGE':
case 'GEOMETRY':
throw new Error('SQL type: `' + sqlType + '` is currently untested');
default:
throw new Error('Unrecognized datatype: `' + sqlType + '`');
}
}