Newer
Older
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import moment from "moment-timezone"; // eslint-disable-line no-restricted-imports -- deprecated usage
Dalton
committed
import _ from "underscore";
import { is_coerceable, coercions_for_type } from "cljs/metabase.types";
import { formatField, stripId } from "metabase/lib/formatting";
import { getFilterOperators } from "metabase-lib/v1/operators/utils";
import type NativeQuery from "metabase-lib/v1/queries/NativeQuery";
import type StructuredQuery from "metabase-lib/v1/queries/StructuredQuery";
import {
getFieldValues,
getRemappings,
} from "metabase-lib/v1/queries/utils/field";
import { TYPE } from "metabase-lib/v1/types/constants";
isa,
Alexander Polyankin
committed
isAddress,
Alexander Polyankin
committed
isComment,
Alexander Polyankin
committed
isCountry,
isCurrency,
Alexander Polyankin
committed
isDate,
isDateWithoutTime,
Alexander Polyankin
committed
isEntityName,
isFK,
isLocation,
Alexander Polyankin
committed
isNumber,
isNumeric,
Alexander Polyankin
committed
isScope,
isState,
isString,
isSummable,
isTime,
isTypeFK,
Alexander Polyankin
committed
isZipCode,
} from "metabase-lib/v1/types/utils/isa";
import { createLookupByProperty, memoizeClass } from "metabase-lib/v1/utils";
import type {
DatasetColumn,
FieldReference,
FieldFingerprint,
FieldId,
FieldFormattingSettings,
FieldVisibilityType,
FieldValuesType,
} from "metabase-types/api";
import { FieldDimension } from "../Dimension";
import type Metadata from "./Metadata";
import type Table from "./Table";
import { getIconForField, getUniqueFieldId } from "./utils/fields";
const LONG_TEXT_MIN = 80;
* @typedef { import("./Metadata").FieldValues } FieldValues
/**
* Wrapper class for field metadata objects. Belongs to a Table.
*/
/**
* @deprecated use RTK Query endpoints and plain api objects from metabase-types/api
*/
id: FieldId | FieldReference;
Alexander Polyankin
committed
display_name: string;
semantic_type: string | null;
Denis Berezin
committed
effective_type?: string | null;
table_id?: Table["id"];
name_field?: Field;
has_field_values?: FieldValuesType;
Alexander Polyankin
committed
position: number;
metadata?: Metadata;
source?: string;
Alexander Polyankin
committed
nfc_path?: string[];
json_unfolding: boolean | null;
coercion_strategy: string | null;
Alexander Polyankin
committed
fk_target_field_id: FieldId | null;
settings?: FieldFormattingSettings;
visibility_type: FieldVisibilityType;
// added when creating "virtual fields" that are associated with a given query
query?: StructuredQuery | NativeQuery;
getPlainObject(): IField {
return this._plainObject;
}
getId() {
if (Array.isArray(this.id)) {
return this.id[1];
}
return this.id;
}
// `uniqueId` is set by our normalizr schema so it is not always available,
// if the Field instance was instantiated outside of an entity
getUniqueId() {
if (this.uniqueId) {
return this.uniqueId;
}
const uniqueId = getUniqueFieldId(this);
this.uniqueId = uniqueId;
return uniqueId;
}
return this.metadata ? this.metadata.field(this.parent_id) : null;
path() {
const path = [];
let field = this;
do {
path.unshift(field);
} while ((field = field.parent()));
displayName({
includeSchema = false,
includeTable = false,
includePath = true,
} = {}) {
// It is possible that the table doesn't exist or
// that it does, but its `displayName` resolves to an empty string.
if (includeTable && this.table?.displayName?.()) {
displayName +=
this.table.displayName({
includeSchema,
}) + " → ";
displayName += this.path().map(formatField).join(": ");
} else {
displayName += formatField(this);
}
/**
* The name of the object type this field points to.
* Currently we try to guess this by stripping trailing `ID` from `display_name`, but ideally it would be configurable in metadata
* See also `table.objectName()`
*/
targetObjectName() {
return stripId(this.displayName());
isDateWithoutTime() {
return isDateWithoutTime(this);
}
isCurrency() {
return isCurrency(this);
}
Tom Robinson
committed
isAddress() {
return isAddress(this);
}
isCity() {
return isCity(this);
}
isZipCode() {
return isZipCode(this);
}
isCoordinate() {
return isCoordinate(this);
}
Tom Robinson
committed
isLocation() {
return isLocation(this);
}
isScope() {
return isScope(this);
}
isMetric() {
return isMetric(this);
}
/**
* Tells if this column can be used in a breakout
* Currently returns `true` for everything expect for aggregation columns
*/
isDimension() {
return isDimension(this);
}
isID() {
return isPK(this) || isFK(this);
}
isEntityName() {
return isEntityName(this);
}
isLongText() {
return (
isString(this) &&
(isComment(this) ||
isDescription(this) ||
this?.fingerprint?.type?.["type/Text"]?.["average-length"] >=
LONG_TEXT_MIN)
);
}
/**
* @param {Field} field
*/
isCompatibleWith(field) {
return (
this.isDate() === field.isDate() ||
this.isNumeric() === field.isNumeric() ||
this.id === field.id
);
/**
* @returns {FieldValues}
*/
fieldValues() {
return getFieldValues(this._plainObject);
Dalton
committed
hasFieldValues() {
return !_.isEmpty(this.fieldValues());
}
remappedValues() {
return getRemappings(this);
}
icon() {
return getIconForField(this);
}
Anton Kulyk
committed
reference() {
if (Array.isArray(this.id)) {
// if ID is an array, it's a MBQL field reference, typically "field"
Anton Kulyk
committed
return this.id;
} else if (this.field_ref) {
return this.field_ref;
Anton Kulyk
committed
return ["field", this.id, null];
// 1. `_fieldInstance` is passed in so that we can shortwire any subsequent calls to `field()` form the dimension instance
// 2. The distinction between "fields" and "dimensions" is fairly fuzzy, and this method is "wrong" in the sense that
// The `ref` of this Field instance MIGHT be something like ["aggregation", "count"] which means that we should
// instantiate an AggregationDimension, not a FieldDimension, but there are bugs with that route, and this seems to work for now...
Anton Kulyk
committed
dimension() {
Dalton
committed
const ref = this.reference();
const fieldDimension = new FieldDimension(
ref[1],
ref[2],
this.metadata,
this.query,
{
_fieldInstance: this,
},
);
return fieldDimension;
Anton Kulyk
committed
}
sourceField() {
const d = this.dimension().sourceDimension();
return d && d.field();
}
filterOperators(selected) {
return getFilterOperators(this, this.table, selected);
return createLookupByProperty(this.filterOperators(), "name");
return this.filterOperatorsLookup()[operatorName];
return this.table
? this.table
.aggregationOperators()
.filter(
aggregation =>
aggregation.validFieldsFilters[0] &&
aggregation.validFieldsFilters[0]([this]).length === 1,
)
aggregationOperatorsLookup = _.once(() => {
return createLookupByProperty(this.aggregationOperators(), "short");
aggregationOperator(short) {
return this.aggregationOperatorsLookup()[short];
}
// BREAKOUTS
/**
* Returns a default breakout MBQL clause for this field
*/
getDefaultBreakout() {
return this.dimension().defaultBreakout();
}
/**
* Returns a default date/time unit for this field
*/
getDefaultDateTimeUnit() {
try {
const fingerprint = this.fingerprint.type["type/DateTime"];
const days = moment(fingerprint.latest).diff(
moment(fingerprint.earliest),
"day",
);
if (Number.isNaN(days) || this.isTime()) {
return "hour";
}
if (days < 1) {
return "minute";
} else if (days < 31) {
return "day";
} else if (days < 365) {
return "week";
} else {
return "month";
}
} catch (e) {
return "day";
* @return {?Field}
remappedField() {
const displayFieldId = this.dimensions?.[0]?.human_readable_field_id;
return this.metadata.field(displayFieldId);
// this enables "implicit" remappings from type/PK to type/Name on the same table,
// used in FieldValuesWidget, but not table/object detail listings
if (this.name_field) {
return this.name_field;
return null;
}
/**
* Returns the human readable remapped value, if any
* @returns {?string}
remappedValue(value) {
// TODO: Ugh. Should this be handled further up by the parameter widget?
if (this.isNumeric() && typeof value !== "number") {
value = parseFloat(value);
}
return this.remapping && this.remapping.get(value);
}
/**
* Returns whether the field has a human readable remapped value for this value
* @returns {?string}
hasRemappedValue(value) {
// TODO: Ugh. Should this be handled further up by the parameter widget?
if (this.isNumeric() && typeof value !== "number") {
value = parseFloat(value);
}
return this.remapping && this.remapping.has(value);
}
/**
* Returns true if this field can be searched, e.x. in filter or parameter widgets
* @returns {boolean}
isSearchable() {
// TODO: ...?
return this.isString();
}
searchField(disablePKRemapping = false): Field | null {
if (disablePKRemapping && this.isPK()) {
return this.isSearchable() ? this : null;
}
const remappedField = this.remappedField();
if (remappedField && remappedField.isSearchable()) {
return remappedField;
}
return this.isSearchable() ? this : null;
}
return this.dimension().column({
source: "fields",
...extra,
});
remappingOptions = () => {
const table = this.table;
if (!table) {
return [];
}
const { fks } = table
.legacyQuery({ useStructuredQuery: true })
.fieldOptions();
return fks
.filter(({ field }) => field.id === this.id)
.map(({ field, dimension, dimensions }) => ({
field,
dimension,
dimensions: dimensions.filter(d => d.isValidFKRemappingTarget()),
}));
};
clone(fieldMetadata?: FieldMetadata) {
if (fieldMetadata instanceof Field) {
throw new Error("`fieldMetadata` arg must be a plain object");
}
const plainObject = this.getPlainObject();
const newField = new Field({ ...this, ...fieldMetadata });
newField._plainObject = { ...plainObject, ...fieldMetadata };
return newField;
}
/**
* Returns a FKDimension for this field and the provided field
* @param {Field} foreignField
* @return {Dimension}
foreign(foreignField) {
return this.dimension().foreign(foreignField.dimension());
Alexander Polyankin
committed
isVirtual() {
return typeof this.id !== "number";
}
isJsonUnfolded() {
const database = this.table?.database;
return this.json_unfolding ?? database?.details["json-unfolding"] ?? true;
}
canUnfoldJson() {
const database = this.table?.database;
return (
isa(this.base_type, TYPE.JSON) &&
database != null &&
database.hasFeature("nested-field-columns")
);
}
canCoerceType() {
return !isTypeFK(this.semantic_type) && is_coerceable(this.base_type);
}
coercionStrategyOptions(): string[] {
return coercions_for_type(this.base_type);
}
/**
* @private
* @param {number} id
* @param {string} name
* @param {string} display_name
* @param {string} description
* @param {Table} table
* @param {?Field} name_field
* @param {Metadata} metadata
*/
/* istanbul ignore next */
_constructor(
id,
name,
display_name,
description,
table,
name_field,
metadata,
) {
this.id = id;
this.name = name;
this.display_name = display_name;
this.description = description;
this.table = table;
this.name_field = name_field;
this.metadata = metadata;
}
}
// eslint-disable-next-line import/no-default-export -- deprecated usage
export default class Field extends memoizeClass<FieldInner>("filterOperators")(
FieldInner,
) {}