mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-20 01:02:22 +00:00
a64fb105e3
Summary: Makes type checking a bit stronger Test Plan: it just has to compile Reviewers: jarek Reviewed By: jarek Differential Revision: https://phab.getgrist.com/D3065
348 lines
12 KiB
TypeScript
348 lines
12 KiB
TypeScript
import { CellValue, CellVersions } from 'app/common/DocActions';
|
|
import { GristObjCode } from 'app/plugin/GristData';
|
|
import isString = require('lodash/isString');
|
|
import { removePrefix } from "./gutil";
|
|
|
|
// tslint:disable:object-literal-key-quotes
|
|
|
|
export type GristType = 'Any' | 'Attachments' | 'Blob' | 'Bool' | 'Choice' | 'ChoiceList' |
|
|
'Date' | 'DateTime' |
|
|
'Id' | 'Int' | 'ManualSortPos' | 'Numeric' | 'PositionNumber' | 'Ref' | 'RefList' | 'Text';
|
|
|
|
export type GristTypeInfo =
|
|
{type: 'DateTime', timezone: string} |
|
|
{type: 'Ref', tableId: string} |
|
|
{type: 'RefList', tableId: string} |
|
|
{type: Exclude<GristType, 'DateTime'|'Ref'|'RefList'>};
|
|
|
|
export const MANUALSORT = 'manualSort';
|
|
|
|
// Whether a column is internal and should be hidden.
|
|
export function isHiddenCol(colId: string): boolean {
|
|
return colId.startsWith('gristHelper_') || colId === 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': [ '', "''" ],
|
|
'ChoiceList': [ null, "NULL" ],
|
|
'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];
|
|
}
|
|
|
|
/**
|
|
* Convert a type like 'Numeric', 'DateTime:America/New_York', or 'Ref:Table1' to a GristTypeInfo
|
|
* object.
|
|
*/
|
|
export function extractInfoFromColType(colType: string): GristTypeInfo {
|
|
const colon = colType.indexOf(':');
|
|
const [type, arg] = (colon === -1) ? [colType] : [colType.slice(0, colon), colType.slice(colon + 1)];
|
|
return (type === 'Ref') ? {type, tableId: String(arg)} :
|
|
(type === 'RefList') ? {type, tableId: String(arg)} :
|
|
(type === 'DateTime') ? {type, timezone: String(arg)} :
|
|
{type} as GristTypeInfo;
|
|
}
|
|
|
|
/**
|
|
* Re-encodes a CellValue of a given Grist type as a value suitable to use in an Any column. E.g.
|
|
* reencodeAsAny(123, 'Numeric') -> 123
|
|
* reencodeAsAny(123, 'Date') -> ['d', 123]
|
|
* reencodeAsAny(123, 'Reference', 'Table1') -> ['R', 'Table1', 123]
|
|
*/
|
|
export function reencodeAsAny(value: CellValue, typeInfo: GristTypeInfo): CellValue {
|
|
if (typeof value === 'number') {
|
|
switch (typeInfo.type) {
|
|
case 'Date': return [GristObjCode.Date, value];
|
|
case 'DateTime': return [GristObjCode.DateTime, value, typeInfo.timezone];
|
|
case 'Ref': return [GristObjCode.Reference, typeInfo.tableId, value];
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns whether a value (as received in a DocAction) represents a custom object.
|
|
*/
|
|
export function isObject(value: CellValue): value is [GristObjCode, any?] {
|
|
return Array.isArray(value);
|
|
}
|
|
|
|
/**
|
|
* Returns GristObjCode of the value if the value is an object, or null otherwise.
|
|
* The return type includes any string, since we should not assume we can only get valid codes.
|
|
*/
|
|
export function getObjCode(value: CellValue): GristObjCode|string|null {
|
|
return Array.isArray(value) ? value[0] : null;
|
|
}
|
|
|
|
/**
|
|
* Returns whether a value (as received in a DocAction) represents a raised exception.
|
|
*/
|
|
export function isRaisedException(value: CellValue): boolean {
|
|
return getObjCode(value) === GristObjCode.Exception;
|
|
}
|
|
|
|
/**
|
|
* Returns whether a value (as received in a DocAction) represents a group of versions for
|
|
* a comparison or conflict.
|
|
*/
|
|
export function isVersions(value: CellValue): value is [GristObjCode.Versions, CellVersions] {
|
|
return getObjCode(value) === GristObjCode.Versions;
|
|
}
|
|
|
|
export function isSkip(value: CellValue): value is [GristObjCode.Skip] {
|
|
return getObjCode(value) === GristObjCode.Skip;
|
|
}
|
|
|
|
export function isCensored(value: CellValue): value is [GristObjCode.Censored] {
|
|
return getObjCode(value) === GristObjCode.Censored;
|
|
}
|
|
|
|
/**
|
|
* Returns whether a value (as received in a DocAction) represents a list.
|
|
*/
|
|
export function isList(value: CellValue): value is [GristObjCode.List, ...CellValue[]] {
|
|
return Array.isArray(value) && value[0] === GristObjCode.List;
|
|
}
|
|
|
|
/**
|
|
* Returns whether a value (as received in a DocAction) represents a reference to a record.
|
|
*/
|
|
export function isReference(value: CellValue): value is [GristObjCode.Reference, string, number] {
|
|
return Array.isArray(value) && value[0] === GristObjCode.Reference;
|
|
}
|
|
|
|
/**
|
|
* Returns whether a value (as received in a DocAction) represents a reference list (RecordSet).
|
|
*/
|
|
export function isReferenceList(value: CellValue): value is [GristObjCode.ReferenceList, string, number[]] {
|
|
return Array.isArray(value) && value[0] === GristObjCode.ReferenceList;
|
|
}
|
|
|
|
/**
|
|
* Returns whether a value (as received in a DocAction) represents a reference or reference list.
|
|
*/
|
|
export function isReferencing(value: CellValue):
|
|
value is [GristObjCode.ReferenceList|GristObjCode.Reference, string, number[]|number]
|
|
{
|
|
return Array.isArray(value) &&
|
|
(value[0] === GristObjCode.ReferenceList || value[0] === GristObjCode.Reference);
|
|
}
|
|
|
|
/**
|
|
* 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 || isList(value);
|
|
}
|
|
|
|
/**
|
|
* 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] === GristObjCode.List;
|
|
}
|
|
|
|
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; }
|
|
|
|
// These values are not regular cell values, even in a column of type Any.
|
|
const abnormalValueTypes: string[] = [GristObjCode.Exception, GristObjCode.Pending, GristObjCode.Skip,
|
|
GristObjCode.Unmarshallable, GristObjCode.Versions];
|
|
|
|
function isNormalValue(value: CellValue) {
|
|
return !abnormalValueTypes.includes(getObjCode(value)!);
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
},
|
|
ChoiceList: isListOrNull,
|
|
};
|
|
|
|
export function isRightType(type: string): undefined | ((value: CellValue, options?: any) => 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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enum for values of columns' recalcWhen property, corresponding to Python definitions in
|
|
* schema.py.
|
|
*/
|
|
export enum RecalcWhen {
|
|
DEFAULT = 0, // Calculate on new records or when any field in recalcDeps changes.
|
|
NEVER = 1, // Don't calculate automatically (but user can trigger manually)
|
|
MANUAL_UPDATES = 2, // Calculate on new records and on manual updates to any data field.
|
|
}
|
|
|
|
|
|
/**
|
|
* 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 + '`');
|
|
}
|
|
}
|
|
|
|
export function getReferencedTableId(type: string) {
|
|
return removePrefix(type, "Ref:") || removePrefix(type, "RefList:");
|
|
}
|
|
|
|
export function isRefListType(type: string) {
|
|
return type.startsWith('RefList:');
|
|
}
|
|
|
|
export function isFullReferencingType(type: string) {
|
|
return type.startsWith('Ref:') || isRefListType(type);
|
|
}
|