diff --git a/frontend/src/metabase-lib/metadata/Database.ts b/frontend/src/metabase-lib/metadata/Database.ts index d5eed628b784ae1cd7847610c7b9d794b8d39aa8..678ada5795dd3fca7e49f6758f297fcb109d42fe 100644 --- a/frontend/src/metabase-lib/metadata/Database.ts +++ b/frontend/src/metabase-lib/metadata/Database.ts @@ -1,6 +1,10 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-nocheck -import { Database as IDatabase, NativePermissions } from "metabase-types/api"; +import { + Database as IDatabase, + NativePermissions, + StructuredQuery, +} from "metabase-types/api"; import { generateSchemaId } from "metabase-lib/metadata/utils/schema"; import { createLookupByProperty, memoizeClass } from "metabase-lib/utils"; import Question from "../Question"; @@ -127,7 +131,7 @@ class DatabaseInner extends Base { } question( - query = { + query: StructuredQuery = { "source-table": null, }, ) { diff --git a/frontend/src/metabase-lib/metadata/Field.ts b/frontend/src/metabase-lib/metadata/Field.ts index 348cecc0c7962f77be4ff2df6f3c2bc7bd8e411d..df429c930041682ef2d88ac53024a6d62bde2a8a 100644 --- a/frontend/src/metabase-lib/metadata/Field.ts +++ b/frontend/src/metabase-lib/metadata/Field.ts @@ -4,7 +4,11 @@ import _ from "underscore"; import moment from "moment-timezone"; import { formatField, stripId } from "metabase/lib/formatting"; -import type { FieldFingerprint } from "metabase-types/api/field"; +import type { + DatasetColumn, + Field as IField, + FieldFingerprint, +} from "metabase-types/api"; import type { Field as FieldRef } from "metabase-types/types/Query"; import { isDate, @@ -71,6 +75,10 @@ class FieldInner extends Base { // 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]; @@ -436,7 +444,7 @@ class FieldInner extends Base { return this.isString(); } - column(extra = {}) { + column(extra = {}): DatasetColumn { return this.dimension().column({ source: "fields", ...extra, diff --git a/frontend/src/metabase-lib/parameters/utils/targets.unit.spec.ts b/frontend/src/metabase-lib/parameters/utils/targets.unit.spec.ts index f86e9e2ddf26a87f0fb647832fc6a2304ccc0337..d8270673e655d59c7a4908e3a230a1061547f0ed 100644 --- a/frontend/src/metabase-lib/parameters/utils/targets.unit.spec.ts +++ b/frontend/src/metabase-lib/parameters/utils/targets.unit.spec.ts @@ -72,7 +72,6 @@ describe("parameters/utils/targets", () => { describe("getParameterTargetField", () => { it("should return null when the target is not a dimension", () => { - // @ts-expect-error - SAMPLE_DATABASE is defined const question = SAMPLE_DATABASE.nativeQuestion({ query: "select * from PRODUCTS where CATEGORY = {{foo}}", "template-tags": { @@ -96,7 +95,6 @@ describe("parameters/utils/targets", () => { "dimension", ["template-tag", "foo"], ]; - // @ts-expect-error - SAMPLE_DATABASE is defined const question = SAMPLE_DATABASE.nativeQuestion({ query: "select * from PRODUCTS where {{foo}}", "template-tags": { @@ -119,7 +117,6 @@ describe("parameters/utils/targets", () => { "dimension", ["field", PRODUCTS.CATEGORY.id, null], ]; - // @ts-expect-error - SAMPLE_DATABASE is defined const question = SAMPLE_DATABASE.question({ "source-table": PRODUCTS.id, }); diff --git a/frontend/src/metabase-lib/queries/utils/structured-query-table.unit.spec.ts b/frontend/src/metabase-lib/queries/utils/structured-query-table.unit.spec.ts index 0c80d5aaafc0cfdccbfcaa63d24cd4bb15f071ed..bf8613e276604569f15fbc40bdda36f920095c9c 100644 --- a/frontend/src/metabase-lib/queries/utils/structured-query-table.unit.spec.ts +++ b/frontend/src/metabase-lib/queries/utils/structured-query-table.unit.spec.ts @@ -137,7 +137,9 @@ describe("metabase-lib/queries/utils/structured-query-table", () => { metadata.tables[ORDERS_DATASET_TABLE.id] = ORDERS_DATASET_TABLE; - const table = getStructuredQueryTable(ORDERS_DATASET.query()); + const table = getStructuredQueryTable( + ORDERS_DATASET.query() as StructuredQuery, + ); it("should return a nested card table using the given query's question", () => { expect(table?.getPlainObject()).toEqual( expect.objectContaining({ diff --git a/frontend/src/metabase-lib/queries/utils/virtual-table.unit.spec.ts b/frontend/src/metabase-lib/queries/utils/virtual-table.unit.spec.ts index 041a8c151fc9dfed0b56255c46f272d53890c5d5..43b88d04458dfd65e1c1cb607fefb45972d7134e 100644 --- a/frontend/src/metabase-lib/queries/utils/virtual-table.unit.spec.ts +++ b/frontend/src/metabase-lib/queries/utils/virtual-table.unit.spec.ts @@ -1,10 +1,13 @@ import { metadata, PRODUCTS } from "__support__/sample_database_fixture"; + +import StructuredQuery from "metabase-lib/queries/StructuredQuery"; import Field from "metabase-lib/metadata/Field"; import Table from "metabase-lib/metadata/Table"; + import { createVirtualField, createVirtualTable } from "./virtual-table"; describe("metabase-lib/queries/utils/virtual-table", () => { - const query = PRODUCTS.newQuestion().query(); + const query = PRODUCTS.newQuestion().query() as StructuredQuery; const field = createVirtualField({ id: 123, metadata, @@ -28,7 +31,7 @@ describe("metabase-lib/queries/utils/virtual-table", () => { }); describe("createVirtualTable", () => { - const query = PRODUCTS.newQuestion().query(); + const query = PRODUCTS.newQuestion().query() as StructuredQuery; const field1 = createVirtualField({ id: 1, metadata, diff --git a/frontend/src/metabase-types/api/field.ts b/frontend/src/metabase-types/api/field.ts index 851383d7de00a055dbff41a74b6c0cfc5570c547..caeba662f68264e1f4c9de1d70050ac17017102d 100644 --- a/frontend/src/metabase-types/api/field.ts +++ b/frontend/src/metabase-types/api/field.ts @@ -50,8 +50,8 @@ export type FieldDimension = { name: string; }; -export interface Field { - id?: FieldId; +export interface ConcreteField { + id: FieldId; table_id: TableId; name: string; @@ -85,3 +85,7 @@ export interface Field { created_at: string; updated_at: string; } + +export type Field = Omit<ConcreteField, "id"> & { + id?: FieldId; +}; diff --git a/frontend/src/metabase-types/store/entities.ts b/frontend/src/metabase-types/store/entities.ts index 28afe08c047f8e488e6c07c25742b45f465e9d11..5726a5f5b8f81543bcaab581262f059ec6cb4cc4 100644 --- a/frontend/src/metabase-types/store/entities.ts +++ b/frontend/src/metabase-types/store/entities.ts @@ -2,6 +2,8 @@ import { Collection, CollectionId, Database, + Field, + FieldId, NativeQuerySnippet, NativeQuerySnippetId, Table, @@ -12,6 +14,7 @@ import { export interface EntitiesState { collections?: Record<CollectionId, Collection>; databases?: Record<number, Database>; + fields?: Record<FieldId, Field>; tables?: Record<number | string, Table>; snippets?: Record<NativeQuerySnippetId, NativeQuerySnippet>; users?: Record<UserId, User>; diff --git a/frontend/src/metabase/components/MetadataInfo/TableInfo/TableInfo.unit.spec.tsx b/frontend/src/metabase/components/MetadataInfo/TableInfo/TableInfo.unit.spec.tsx index cdab9a3e361ea298628805506956d2777c938a68..5fb7e4e8321662a46a227f09b7c9c5ceb1fd9198 100644 --- a/frontend/src/metabase/components/MetadataInfo/TableInfo/TableInfo.unit.spec.tsx +++ b/frontend/src/metabase/components/MetadataInfo/TableInfo/TableInfo.unit.spec.tsx @@ -107,7 +107,9 @@ describe("TableInfo", () => { }); it("should display the given table's description", () => { - expect(screen.getByText(PRODUCTS.description)).toBeInTheDocument(); + expect( + screen.getByText(PRODUCTS.description as string), + ).toBeInTheDocument(); }); it("should show a count of columns on the table", () => { diff --git a/frontend/src/metabase/query_builder/actions/core/initializeQB.unit.spec.ts b/frontend/src/metabase/query_builder/actions/core/initializeQB.unit.spec.ts index b3d83c067a8b75639efa1dd77640cf76267fe0ec..bc28c8ab22f750cc83bccb5db0e46f77beec7a4e 100644 --- a/frontend/src/metabase/query_builder/actions/core/initializeQB.unit.spec.ts +++ b/frontend/src/metabase/query_builder/actions/core/initializeQB.unit.spec.ts @@ -9,7 +9,7 @@ import Databases from "metabase/entities/databases"; import Snippets from "metabase/entities/snippets"; import { setErrorPage } from "metabase/redux/app"; -import { User } from "metabase-types/api"; +import { DatabaseId, TableId, User } from "metabase-types/api"; import { createMockUser } from "metabase-types/api/mocks"; import { Card, NativeDatasetQuery } from "metabase-types/types/Card"; import { TemplateTag } from "metabase-types/types/Query"; @@ -648,8 +648,8 @@ describe("QB Actions > initializeQB", () => { describe("blank question", () => { type BlankSetupOpts = Omit<BaseSetupOpts, "location" | "params"> & { - db?: number; - table?: number; + db?: DatabaseId; + table?: TableId; segment?: number; metric?: number; }; diff --git a/frontend/src/metabase/query_builder/actions/core/updateQuestion.unit.spec.ts b/frontend/src/metabase/query_builder/actions/core/updateQuestion.unit.spec.ts index ee0c842bf0ea94941dfc94a7080b80a1b518e4d7..60679a9fb05f7d3dffe03597296e29fc8a616af1 100644 --- a/frontend/src/metabase/query_builder/actions/core/updateQuestion.unit.spec.ts +++ b/frontend/src/metabase/query_builder/actions/core/updateQuestion.unit.spec.ts @@ -19,7 +19,6 @@ import Question from "metabase-lib/Question"; import NativeQuery from "metabase-lib/queries/NativeQuery"; import StructuredQuery from "metabase-lib/queries/StructuredQuery"; import Join from "metabase-lib/queries/structured/Join"; -import Field from "metabase-lib/metadata/Field"; import { getAdHocQuestion, getSavedStructuredQuestion, @@ -76,7 +75,7 @@ async function setup({ const queryResult = createMockDataset({ data: { - cols: ORDERS.fields.map((field: Field) => field.column()), + cols: ORDERS.fields.map(field => field.column()), }, }); diff --git a/frontend/src/metabase/query_builder/components/DataSelector/DataSelectorFieldPicker/DataSelectorFieldPicker.tsx b/frontend/src/metabase/query_builder/components/DataSelector/DataSelectorFieldPicker/DataSelectorFieldPicker.tsx index 86547965968c656a24005587825ac5cb190088f6..cece699a26c5a8803fef43f668da40b5a57ca335 100644 --- a/frontend/src/metabase/query_builder/components/DataSelector/DataSelectorFieldPicker/DataSelectorFieldPicker.tsx +++ b/frontend/src/metabase/query_builder/components/DataSelector/DataSelectorFieldPicker/DataSelectorFieldPicker.tsx @@ -3,8 +3,8 @@ import { t } from "ttag"; import AccordionList from "metabase/core/components/AccordionList"; import Icon from "metabase/components/Icon"; -import type { Field } from "metabase-types/api/field"; import type { Table } from "metabase-types/api/table"; +import type Field from "metabase-lib/metadata/Field"; import DataSelectorLoading from "../DataSelectorLoading"; import { @@ -31,10 +31,7 @@ type HeaderProps = { type FieldWithName = { name: string; - field: { - id: number; - dimension: () => any; - }; + field: Field; }; const DataSelectorFieldPicker = ({ @@ -57,7 +54,7 @@ const DataSelectorFieldPicker = ({ { name: header, items: fields.map(field => ({ - name: field.display_name, + name: field.displayName(), field: field, })), }, diff --git a/frontend/src/metabase/query_builder/components/DataSelector/DataSelectorFieldPicker/DataSelectorFieldPicker.unit.spec.tsx b/frontend/src/metabase/query_builder/components/DataSelector/DataSelectorFieldPicker/DataSelectorFieldPicker.unit.spec.tsx index c49075731f68fcee52c73f70cc968a75bff259f4..82822d3a119e26d7387454968a5a2007217c3832 100644 --- a/frontend/src/metabase/query_builder/components/DataSelector/DataSelectorFieldPicker/DataSelectorFieldPicker.unit.spec.tsx +++ b/frontend/src/metabase/query_builder/components/DataSelector/DataSelectorFieldPicker/DataSelectorFieldPicker.unit.spec.tsx @@ -57,13 +57,11 @@ describe("DataSelectorFieldPicker", () => { display_name: tableDisplayName, }; - const fields = [ORDERS.PRODUCT_ID]; - render( <DataSelectorFieldPicker {...props} selectedTable={selectedTable as Table} - fields={fields} + fields={[ORDERS.PRODUCT_ID]} />, ); diff --git a/frontend/src/metabase/query_builder/components/view/sidebars/ModelCacheManagementSection/ModelCacheManagementSection.unit.spec.tsx b/frontend/src/metabase/query_builder/components/view/sidebars/ModelCacheManagementSection/ModelCacheManagementSection.unit.spec.tsx index ebdaabf25713a100075617822ae8cffa1cbbf309..066a5bfb6cf15b371320bcf73bc8d7a64fd70cfe 100644 --- a/frontend/src/metabase/query_builder/components/view/sidebars/ModelCacheManagementSection/ModelCacheManagementSection.unit.spec.tsx +++ b/frontend/src/metabase/query_builder/components/view/sidebars/ModelCacheManagementSection/ModelCacheManagementSection.unit.spec.tsx @@ -35,7 +35,7 @@ async function setup({ const modelCacheInfo = getMockModelCacheInfo({ ...cacheInfo, card_id: model.id(), - card_name: model.displayName(), + card_name: model.displayName() as string, }); const onRefreshMock = jest diff --git a/frontend/test/__support__/sample_database_fixture.js b/frontend/test/__support__/sample_database_fixture.ts similarity index 55% rename from frontend/test/__support__/sample_database_fixture.js rename to frontend/test/__support__/sample_database_fixture.ts index 442dc28fbdd0f5ed147f2ccbe97aee754b233768..10df7712d7aeb171e66e6b9be20f0dec77197eb2 100644 --- a/frontend/test/__support__/sample_database_fixture.js +++ b/frontend/test/__support__/sample_database_fixture.ts @@ -1,15 +1,22 @@ -/* eslint-disable react/prop-types */ -import React from "react"; -import { Provider } from "react-redux"; import { normalize } from "normalizr"; import { chain } from "icepick"; -import { getStore } from "metabase/store"; import { getMetadata } from "metabase/selectors/metadata"; import { FieldSchema } from "metabase/schema"; -import state from "./sample_database_fixture.json"; -export { default as state } from "./sample_database_fixture.json"; +import type { Field as IField, FieldId } from "metabase-types/api"; +import type { State } from "metabase-types/store"; + +import type Database from "metabase-lib/metadata/Database"; +import type Field from "metabase-lib/metadata/Field"; +import type Metadata from "metabase-lib/metadata/Metadata"; +import type Table from "metabase-lib/metadata/Table"; + +import stateFixture from "./sample_database_fixture.json"; + +export const state = stateFixture as unknown as State; + +export default state; export const SAMPLE_DATABASE_ID = 1; export const ANOTHER_DATABASE_ID = 2; @@ -19,31 +26,45 @@ export const OTHER_MULTI_SCHEMA_DATABASE_ID = 5; export const MAIN_METRIC_ID = 1; -function aliasTablesAndFields(metadata) { - // alias DATABASE.TABLE.FIELD for convienence in tests +function aliasTablesAndFields(metadata: Metadata) { + // alias DATABASE.TABLE.FIELD for convenience in tests // NOTE: this assume names don't conflict with other properties in Database/Table which I think is safe for Sample Database + /* eslint-disable @typescript-eslint/ban-ts-comment */ for (const database of Object.values(metadata.databases)) { for (const table of database.tables) { if (!(table.name in database)) { + // @ts-ignore database[table.name] = table; } for (const field of table.fields) { if (!(field.name in table)) { + // @ts-ignore table[field.name] = field; } } } } + /* eslint-enable @typescript-eslint/ban-ts-comment */ } -function normalizeFields(fields) { +function normalizeFields(fields: Record<string, IField>) { return normalize(fields, [FieldSchema]).entities.fields || {}; } -export function createMetadata(updateState = state => state) { +// Icepick doesn't expose it's IcepickWrapper type, +// so this trick pulls it out of the return type of chain() +// `icepickChainWrapper` is needed because typeof chain<State> doesn't work +// See: https://stackoverflow.com/questions/50321419/typescript-returntype-of-generic-function +const icepickChainWrapper = (state: State) => chain(state); +type EnhancedState = ReturnType<typeof icepickChainWrapper>; + +export function createMetadata(updateState = (state: EnhancedState) => state) { + // This allows to use icepick helpers inside custom `updateState` functions + // Example: const metadata = createMetadata(state => state.assocIn(...)) const stateModified = updateState(chain(state)).thaw().value(); + stateModified.entities.fields = normalizeFields( - stateModified.entities.fields, + stateModified.entities.fields || {}, ); const metadata = getMetadata(stateModified); @@ -53,22 +74,55 @@ export function createMetadata(updateState = state => state) { export const metadata = createMetadata(); -export const SAMPLE_DATABASE = metadata.database(SAMPLE_DATABASE_ID); -export const ANOTHER_DATABASE = metadata.database(ANOTHER_DATABASE_ID); -export const MONGO_DATABASE = metadata.database(MONGO_DATABASE_ID); +/* eslint-disable @typescript-eslint/no-non-null-assertion */ + +/** + * In the wild, fields might not have a concrete ID + * (e.g. when coming from a native query) + * But for our sample data we can be sure that they're always concrete. + */ +type SimpleField = Omit<Field, "id"> & { + id: FieldId; +}; + +type AliasedTable = Table & { + [fieldName: string]: SimpleField; +}; + +/** + * Databases below are extended with table aliases. + * So it's possible to do SAMPLE_DATABASE.ORDERS or SAMPLE_DATABASE.ORDERS.TOTAL + * to retrieve tables and field instances. + */ +type AliasedSampleDatabase = Database & { + ORDERS: AliasedTable; + PRODUCTS: AliasedTable; + PEOPLE: AliasedTable; + REVIEWS: AliasedTable; +}; + +export const SAMPLE_DATABASE = metadata.database( + SAMPLE_DATABASE_ID, +) as AliasedSampleDatabase; + +export const ANOTHER_DATABASE = metadata.database(ANOTHER_DATABASE_ID)!; +export const MONGO_DATABASE = metadata.database(MONGO_DATABASE_ID)!; export const MULTI_SCHEMA_DATABASE = metadata.database( MULTI_SCHEMA_DATABASE_ID, -); +)!; export const OTHER_MULTI_SCHEMA_DATABASE = metadata.database( OTHER_MULTI_SCHEMA_DATABASE_ID, -); +)!; +/* eslint-enable @typescript-eslint/no-non-null-assertion */ export const ORDERS = SAMPLE_DATABASE.ORDERS; export const PRODUCTS = SAMPLE_DATABASE.PRODUCTS; export const PEOPLE = SAMPLE_DATABASE.PEOPLE; export const REVIEWS = SAMPLE_DATABASE.REVIEWS; -export function makeMetadata(metadata) { +export function makeMetadata( + metadata: Record<string, Record<string, any>>, +): Metadata { metadata = { databases: { 1: { name: "database", tables: [] }, @@ -90,7 +144,8 @@ export function makeMetadata(metadata) { questions: {}, ...metadata, }; - // convienence for filling in missing bits + + // convenience for filling in missing bits for (const objects of Object.values(metadata)) { for (const [id, object] of Object.entries(objects)) { object.id = /^\d+$/.test(id) ? parseInt(id) : id; @@ -99,6 +154,7 @@ export function makeMetadata(metadata) { } } } + // linking to default db for (const table of Object.values(metadata.tables)) { if (table.db == null) { @@ -107,6 +163,7 @@ export function makeMetadata(metadata) { (db0.tables = db0.tables || []).push(table.id); } } + // linking to default table for (const childType of ["fields", "segments", "metrics"]) { for (const child of Object.values(metadata[childType])) { @@ -122,12 +179,3 @@ export function makeMetadata(metadata) { return getMetadata({ entities: metadata }); } - -const nopEntitiesReducer = (s = state.entities, a) => s; - -// simple provider which only supports static metadata defined above, no actions will take effect -export const StaticEntitiesProvider = ({ children }) => ( - <Provider store={getStore({ entities: nopEntitiesReducer }, null, state)}> - {children} - </Provider> -); diff --git a/frontend/test/metabase/visualizations/components/settings/ChartSettingOrderedColumns.unit.spec.js b/frontend/test/metabase/visualizations/components/settings/ChartSettingOrderedColumns.unit.spec.js index 9d0a98511862d9663500ac72499284153659a88e..62b97a34a879312639c70e2e37a433f0b2467c33 100644 --- a/frontend/test/metabase/visualizations/components/settings/ChartSettingOrderedColumns.unit.spec.js +++ b/frontend/test/metabase/visualizations/components/settings/ChartSettingOrderedColumns.unit.spec.js @@ -1,8 +1,7 @@ import React from "react"; import { render, screen, fireEvent } from "@testing-library/react"; - +import { ORDERS } from "__support__/sample_database_fixture"; import ChartSettingOrderedColumns from "metabase/visualizations/components/settings/ChartSettingOrderedColumns"; -import { ORDERS } from "__support__/sample_database_fixture.js"; function renderChartSettingOrderedColumns(props) { render( diff --git a/tsconfig.json b/tsconfig.json index 660542786b1853aa662f85ba9f4a0daf7afaf125..1b838e9bf0f8af59253a6dee8dbcdee623416972 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,7 +20,8 @@ "allowJs": true, "esModuleInterop": true, "experimentalDecorators": true, - "forceConsistentCasingInFileNames": true + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true }, "include": [ "frontend/src/**/*.ts",