Skip to content
Snippets Groups Projects
Field.ts 13.1 KiB
Newer Older
  • Learn to ignore specific revisions
  • // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-nocheck
    
    import moment from "moment-timezone"; // eslint-disable-line no-restricted-imports -- deprecated usage
    
    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";
    
    Tom Robinson's avatar
    Tom Robinson committed
    import {
    
    Tom Robinson's avatar
    Tom Robinson committed
      isBoolean,
      isCategory,
    
    Tom Robinson's avatar
    Tom Robinson committed
      isCoordinate,
    
    Tom Robinson's avatar
    Tom Robinson committed
      isDimension,
    
    Tom Robinson's avatar
    Tom Robinson committed
      isMetric,
    
    Tom Robinson's avatar
    Tom Robinson committed
      isPK,
    
    } 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 Base from "./Base";
    
    import type Metadata from "./Metadata";
    
    import type Table from "./Table";
    
    import { getIconForField, getUniqueFieldId } from "./utils/fields";
    
     * @typedef { import("./Metadata").FieldValues } FieldValues
    
    Tom Robinson's avatar
    Tom Robinson committed
    
    
    /**
     * Wrapper class for field metadata objects. Belongs to a Table.
     */
    
    /**
     * @deprecated use RTK Query endpoints and plain api objects from metabase-types/api
     */
    
    class FieldInner extends Base {
    
      description: string | null;
    
      semantic_type: string | null;
    
      fingerprint?: FieldFingerprint;
    
      base_type: string | null;
    
      table_id?: Table["id"];
    
      remapping?: unknown;
    
      has_more_values?: boolean;
    
      values: any[];
    
      json_unfolding: boolean | null;
      coercion_strategy: string | null;
    
      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);
    
    Tom Robinson's avatar
    Tom Robinson committed
      parent() {
    
        return this.metadata ? this.metadata.field(this.parent_id) : null;
    
    Tom Robinson's avatar
    Tom Robinson committed
      path() {
        const path = [];
        let field = this;
    
    Tom Robinson's avatar
    Tom Robinson committed
        do {
          path.unshift(field);
        } while ((field = field.parent()));
    
    Tom Robinson's avatar
    Tom Robinson committed
        return path;
      }
    
    
    Tom Robinson's avatar
    Tom Robinson committed
        let displayName = "";
    
        // 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,
            }) + "";
    
    Tom Robinson's avatar
    Tom Robinson committed
        }
    
    Tom Robinson's avatar
    Tom Robinson committed
        if (includePath) {
    
          displayName += this.path().map(formatField).join(": ");
    
    Tom Robinson's avatar
    Tom Robinson committed
        } else {
          displayName += formatField(this);
        }
    
    Tom Robinson's avatar
    Tom Robinson committed
        return displayName;
    
      /**
       * 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());
    
    Tom Robinson's avatar
    Tom Robinson committed
      isDate() {
        return isDate(this);
      }
    
      isDateWithoutTime() {
        return isDateWithoutTime(this);
      }
    
    
    Tom Robinson's avatar
    Tom Robinson committed
      isTime() {
        return isTime(this);
      }
    
    Tom Robinson's avatar
    Tom Robinson committed
      isNumber() {
        return isNumber(this);
      }
    
    Tom Robinson's avatar
    Tom Robinson committed
      isNumeric() {
        return isNumeric(this);
      }
    
    Tom Robinson's avatar
    Tom Robinson committed
      isBoolean() {
        return isBoolean(this);
      }
    
    Tom Robinson's avatar
    Tom Robinson committed
      isString() {
        return isString(this);
      }
    
      isCity() {
        return isCity(this);
      }
    
      isZipCode() {
        return isZipCode(this);
      }
    
    Tom Robinson's avatar
    Tom Robinson committed
      isState() {
        return isState(this);
      }
    
    Tom Robinson's avatar
    Tom Robinson committed
      isCountry() {
        return isCountry(this);
      }
    
    Tom Robinson's avatar
    Tom Robinson committed
      isCoordinate() {
        return isCoordinate(this);
      }
    
    Tom Robinson's avatar
    Tom Robinson committed
      isSummable() {
        return isSummable(this);
      }
    
    Tom Robinson's avatar
    Tom Robinson committed
      isCategory() {
        return isCategory(this);
      }
    
    Tom Robinson's avatar
    Tom Robinson committed
      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);
      }
    
    Tom Robinson's avatar
    Tom Robinson committed
      isID() {
        return isPK(this) || isFK(this);
      }
    
    Tom Robinson's avatar
    Tom Robinson committed
      isPK() {
        return isPK(this);
      }
    
    Tom Robinson's avatar
    Tom Robinson committed
      isFK() {
        return isFK(this);
      }
    
    Tom Robinson's avatar
    Tom Robinson committed
      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) {
    
    Tom Robinson's avatar
    Tom Robinson committed
        return (
          this.isDate() === field.isDate() ||
          this.isNumeric() === field.isNumeric() ||
          this.id === field.id
        );
    
      /**
       * @returns {FieldValues}
       */
      fieldValues() {
    
        return getFieldValues(this._plainObject);
    
      hasFieldValues() {
        return !_.isEmpty(this.fieldValues());
      }
    
    
    Tom Robinson's avatar
    Tom Robinson committed
      icon() {
        return getIconForField(this);
      }
    
    
        if (Array.isArray(this.id)) {
    
          // if ID is an array, it's a MBQL field reference, typically "field"
    
        } else if (this.field_ref) {
          return this.field_ref;
    
    Tom Robinson's avatar
    Tom Robinson committed
        }
    
      // 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...
    
        const ref = this.reference();
        const fieldDimension = new FieldDimension(
          ref[1],
          ref[2],
          this.metadata,
          this.query,
          {
            _fieldInstance: this,
          },
        );
    
        return fieldDimension;
    
    Tom Robinson's avatar
    Tom Robinson committed
      sourceField() {
        const d = this.dimension().sourceDimension();
        return d && d.field();
      }
    
    
      filterOperators(selected) {
        return getFilterOperators(this, this.table, selected);
    
      filterOperatorsLookup = _.once(() => {
    
        return createLookupByProperty(this.filterOperators(), "name");
    
      filterOperator(operatorName) {
    
        return this.filterOperatorsLookup()[operatorName];
    
      aggregationOperators = _.once(() => {
    
          ? 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
    
    
    Tom Robinson's avatar
    Tom Robinson committed
      /**
       * 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";
    
    Tom Robinson's avatar
    Tom Robinson committed
      /**
       * Returns the remapped field, if any
    
    Tom Robinson's avatar
    Tom Robinson committed
       */
    
    Cam Saul's avatar
    Cam Saul committed
        const displayFieldId = this.dimensions?.[0]?.human_readable_field_id;
    
    Tom Robinson's avatar
    Tom Robinson committed
        if (displayFieldId != null) {
    
          return this.metadata.field(displayFieldId);
    
    Tom Robinson's avatar
    Tom Robinson committed
        // 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;
    
    Tom Robinson's avatar
    Tom Robinson committed
        return null;
      }
    
      /**
       * Returns the human readable remapped value, if any
    
    Tom Robinson's avatar
    Tom Robinson committed
       */
    
    Tom Robinson's avatar
    Tom Robinson committed
        // TODO: Ugh. Should this be handled further up by the parameter widget?
        if (this.isNumeric() && typeof value !== "number") {
          value = parseFloat(value);
        }
    
    Tom Robinson's avatar
    Tom Robinson committed
        return this.remapping && this.remapping.get(value);
      }
    
      /**
       * Returns whether the field has a human readable remapped value for this value
    
    Tom Robinson's avatar
    Tom Robinson committed
       */
    
    Tom Robinson's avatar
    Tom Robinson committed
        // TODO: Ugh. Should this be handled further up by the parameter widget?
        if (this.isNumeric() && typeof value !== "number") {
          value = parseFloat(value);
        }
    
    Tom Robinson's avatar
    Tom Robinson committed
        return this.remapping && this.remapping.has(value);
      }
    
      /**
       * Returns true if this field can be searched, e.x. in filter or parameter widgets
    
    Tom Robinson's avatar
    Tom Robinson committed
       */
    
    Tom Robinson's avatar
    Tom Robinson committed
        // 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;
      }
    
    
      column(extra = {}): DatasetColumn {
    
        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}
    
        return this.dimension().foreign(foreignField.dimension());
    
      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
       */
    
      _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,
    ) {}