From 984ad7a74d5ba4a1bfeda5478ef61baf98e7a863 Mon Sep 17 00:00:00 2001 From: Ian MacFarland Date: Tue, 17 Nov 2020 15:57:29 -0800 Subject: [PATCH 1/2] Metric class POC --- spotlight-client/package.json | 4 +- .../src/datastores/Metric.test.ts | 222 ++++++++++++ spotlight-client/src/datastores/Metric.ts | 321 ++++++++++++++++++ .../src/datastores/contentSources/us_nd.ts | 42 +++ .../src/datastores/retrieveContent.ts | 40 +++ spotlight-client/src/datastores/types.ts | 29 ++ .../src/metricsApi/metricsClient.ts | 56 +++ spotlight-client/src/setupTests.ts | 5 + yarn.lock | 30 +- 9 files changed, 743 insertions(+), 6 deletions(-) create mode 100644 spotlight-client/src/datastores/Metric.test.ts create mode 100644 spotlight-client/src/datastores/Metric.ts create mode 100644 spotlight-client/src/datastores/contentSources/us_nd.ts create mode 100644 spotlight-client/src/datastores/retrieveContent.ts create mode 100644 spotlight-client/src/datastores/types.ts create mode 100644 spotlight-client/src/metricsApi/metricsClient.ts diff --git a/spotlight-client/package.json b/spotlight-client/package.json index 738cfc04..ebc07769 100644 --- a/spotlight-client/package.json +++ b/spotlight-client/package.json @@ -21,7 +21,8 @@ "react-app-polyfill": "^1.0.6", "react-dom": "^16.13.1", "react-scripts": "3.4.3", - "typescript": "^4.0.0" + "typescript": "^4.0.0", + "utility-types": "^3.10.0" }, "devDependencies": { "@testing-library/jest-dom": "^4.2.4", @@ -32,6 +33,7 @@ "@typescript-eslint/eslint-plugin": "^4.4.0", "@typescript-eslint/parser": "^4.4.0", "eslint-import-resolver-typescript": "^2.3.0", + "jest-fetch-mock": "^3.0.3", "lint-staged": ">=10" }, "browserslist": { diff --git a/spotlight-client/src/datastores/Metric.test.ts b/spotlight-client/src/datastores/Metric.test.ts new file mode 100644 index 00000000..5d12dce6 --- /dev/null +++ b/spotlight-client/src/datastores/Metric.test.ts @@ -0,0 +1,222 @@ +// Recidiviz - a data platform for criminal justice reform +// Copyright (C) 2020 Recidiviz, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// ============================================================================= + +import fetchMock from "jest-fetch-mock"; +import { createMetric, MetricTypes } from "./Metric"; + +const metadataSource = { + name: "test metric", + description: "this is a test metric", + methodology: "test methodology description", +}; + +afterEach(() => { + fetchMock.resetMocks(); +}); + +test("base metadata", () => { + const metric = createMetric({ + metricType: MetricTypes.SentencePopulationCurrent, + initOptions: metadataSource, + }); + + expect(metric.name).toBe(metadataSource.name); + expect(metric.description).toBe(metadataSource.description); + expect(metric.methodology).toBe(metadataSource.methodology); +}); + +test("fetches its data file", () => { + const metric = createMetric({ + metricType: MetricTypes.SentencePopulationCurrent, + initOptions: metadataSource, + }); + + metric.fetch(); + expect(fetchMock.mock.calls.length).toBe(1); + const [requestUrl, config] = fetchMock.mock.calls[0]; + expect(requestUrl).toBe(`${process.env.REACT_APP_API_URL}/api/public`); + if (typeof config?.body === "string") { + expect(JSON.parse(config?.body)).toEqual({ + files: ["sentence_type_by_district_by_demographics.json"], + }); + } else { + throw new Error("unexpected request body type"); + } +}); + +test("file loading state", async () => { + const metric = createMetric({ + metricType: MetricTypes.SentencePopulationCurrent, + initOptions: metadataSource, + }); + expect(metric.isLoading).toBe(true); + + fetchMock.once( + JSON.stringify({ sentence_type_by_district_by_demographics: [] }) + ); + await metric.fetch(); + expect(metric.isLoading).toBe(false); +}); + +test.todo("fetch error state"); + +test("locality filter", async () => { + const fileContents = [ + { + district: "ALL", + gender: "ALL", + race_or_ethnicity: "ALL", + state_code: "US_ND", + age_bucket: "ALL", + incarceration_count: "6193", + probation_count: "3399", + total_population_count: "11648", + dual_sentence_count: "2056", + }, + { + district: "NORTHEAST", + gender: "ALL", + race_or_ethnicity: "ALL", + state_code: "US_ND", + age_bucket: "ALL", + incarceration_count: "1318", + probation_count: "722", + total_population_count: "2478", + dual_sentence_count: "438", + }, + { + district: "SOUTHEAST", + gender: "ALL", + race_or_ethnicity: "ALL", + state_code: "US_ND", + age_bucket: "ALL", + incarceration_count: "689", + probation_count: "378", + total_population_count: "1296", + dual_sentence_count: "229", + }, + ]; + fetchMock.once( + JSON.stringify({ sentence_type_by_district_by_demographics: fileContents }) + ); + + const metric = createMetric({ + metricType: MetricTypes.SentencePopulationCurrent, + initOptions: metadataSource, + }); + + await metric.fetch(); + // defaults to all localities + expect(metric.records).toEqual([ + expect.objectContaining({ locality: fileContents[0].district }), + ]); + + // set a filter + metric.localityId = fileContents[1].district; + expect(metric.records).toEqual([ + expect.objectContaining({ locality: fileContents[1].district }), + ]); +}); + +test("demographic filter", async () => { + const fileContents = [ + { + district: "ALL", + gender: "ALL", + race_or_ethnicity: "ALL", + state_code: "US_ND", + age_bucket: "ALL", + incarceration_count: "6193", + probation_count: "3399", + total_population_count: "11648", + dual_sentence_count: "2056", + }, + { + district: "ALL", + gender: "ALL", + race_or_ethnicity: "ALL", + state_code: "US_ND", + age_bucket: "<25", + incarceration_count: "1147", + probation_count: "96", + total_population_count: "1510", + dual_sentence_count: "267", + }, + { + district: "ALL", + gender: "ALL", + race_or_ethnicity: "ALL", + state_code: "US_ND", + age_bucket: "25-29", + incarceration_count: "1262", + probation_count: "1248", + total_population_count: "3048", + dual_sentence_count: "538", + }, + { + district: "ALL", + gender: "MALE", + race_or_ethnicity: "ALL", + state_code: "US_ND", + age_bucket: "ALL", + incarceration_count: "6034", + probation_count: "3112", + total_population_count: "11107", + dual_sentence_count: "1961", + }, + { + district: "ALL", + gender: "FEMALE", + race_or_ethnicity: "ALL", + state_code: "US_ND", + age_bucket: "ALL", + incarceration_count: "159", + probation_count: "285", + total_population_count: "540", + dual_sentence_count: "96", + }, + ]; + fetchMock.once( + JSON.stringify({ sentence_type_by_district_by_demographics: fileContents }) + ); + + const metric = createMetric({ + metricType: MetricTypes.SentenceTypesCurrent, + initOptions: metadataSource, + }); + + await metric.fetch(); + // defaults to total + expect(metric.records).toEqual([ + expect.objectContaining({ + raceOrEthnicity: "ALL", + gender: "ALL", + ageBucket: "ALL", + }), + ]); + + metric.demographicView = "age"; + expect(metric.records.length).toBeGreaterThan(0); + metric.records.forEach((record) => { + expect(record).toEqual( + expect.objectContaining({ raceOrEthnicity: "ALL", gender: "ALL" }) + ); + expect(["<25", "25-29", "30-34", "35-39", "40<"]).toContain( + record.ageBucket + ); + }); +}); diff --git a/spotlight-client/src/datastores/Metric.ts b/spotlight-client/src/datastores/Metric.ts new file mode 100644 index 00000000..edae0d2e --- /dev/null +++ b/spotlight-client/src/datastores/Metric.ts @@ -0,0 +1,321 @@ +// Recidiviz - a data platform for criminal justice reform +// Copyright (C) 2020 Recidiviz, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// ============================================================================= + +import { ValuesType } from "utility-types"; +import { callMetricsApi, DataFile } from "../metricsApi/metricsClient"; +import { NamedEntity } from "./types"; + +export enum MetricTypes { + SentencePopulationCurrent = "SentencePopulationCurrent", + SentenceTypesCurrent = "SentenceTypesCurrent", +} + +type MetricsContent = NamedEntity & { methodology: string }; + +type MetricFactoryOptionsBase = MetricsContent; + +type TotalIdentifier = "ALL"; +const TOTAL_KEY: TotalIdentifier = "ALL"; + +type DemographicView = "total" | "race" | "gender" | "age"; + +const DIMENSION_DATA_KEYS: Record< + Exclude, + keyof DemographicFields +> = { + age: "ageBucket", + gender: "gender", + race: "raceOrEthnicity", +}; + +// TODO: should these be enums? +type RaceIdentifier = + | TotalIdentifier + | "AMERICAN_INDIAN_ALASKAN_NATIVE" + | "ASIAN" + | "BLACK" + | "HISPANIC" + | "NATIVE_HAWAIIAN_PACIFIC_ISLANDER" + | "WHITE" + | "OTHER"; +type GenderIdentifier = TotalIdentifier | "FEMALE" | "MALE"; +type AgeIdentifier = + | TotalIdentifier + | "<25" + | "25-29" + | "30-34" + | "35-39" + | "40<"; + +type BaseMetricFields = { + stateCode: string; +}; + +type DemographicFields = { + raceOrEthnicity: RaceIdentifier; + gender: GenderIdentifier; + ageBucket: AgeIdentifier; +}; + +type LocalityFields = { + locality: string; +}; + +type RawRecord = Record; + +export type AnyMetric = Metric; + +type PopulationBreakdownByLocationRecord = BaseMetricFields & + DemographicFields & + LocalityFields & { + population: number; + }; + +// TODO: should these even be different types? why not just PopulationSnapshot? +type PopulationSnapshot = Metric; + +type SentenceTypeByLocationRecord = BaseMetricFields & + DemographicFields & + LocalityFields & { + dualSentenceCount: number; + incarcerationCount: number; + probationCount: number; + }; +type SentenceTypesCurrent = Metric; + +type DataTransformer = (rawData: DataFile) => RecordFormat[]; + +type InitParams = { + contentSource: MetricsContent; + dataTransformer: DataTransformer; + sourceFileName: string; + // NOTE: these fields can only be populated if the RecordFormat contains the requisite fields; + // however, they are not REQUIRED to be populated in those cases (e.g., population snapshots + // contain demographic fields but do not need to support demographic filters). + // I think this means it's possible to inadvertently break a filter by setting it to `undefined` + // without triggering a compiler error. Maybe that can be fixed but I have not done so here + defaultLocalityId?: RecordFormat extends LocalityFields ? string : undefined; + defaultDemographicView?: RecordFormat extends DemographicFields + ? DemographicView + : undefined; +}; + +class Metric { + // metadata properties + readonly description: string; + + readonly methodology: string; + + readonly name: string; + + // data properties + private dataTransformer: DataTransformer; + + private sourceFileName: string; + + isLoading: boolean; + + private allRecords?: RecordFormat[]; + + error?: Error; + + // filter properties + localityId?: RecordFormat extends LocalityFields ? string : undefined; + + demographicView?: RecordFormat extends DemographicFields + ? DemographicView + : undefined; + + constructor({ + contentSource, + dataTransformer, + defaultLocalityId, + defaultDemographicView, + sourceFileName, + }: InitParams) { + // initialize metadata + this.description = contentSource.description; + this.methodology = contentSource.methodology; + this.name = contentSource.name; + + // initialize data fetching + this.isLoading = true; + this.dataTransformer = dataTransformer; + this.sourceFileName = sourceFileName; + + // initialize filters + this.localityId = defaultLocalityId; + this.demographicView = defaultDemographicView; + } + + async fetch(): Promise { + // TODO: map metric type to file name(s) in factory function? + const apiResponse = await callMetricsApi({ + files: [this.sourceFileName], + }); + // TODO: call the proper data transformation function for the metric type + if (apiResponse) { + const metricFileData = apiResponse[this.sourceFileName]; + if (metricFileData) { + this.allRecords = this.dataTransformer(metricFileData); + } + this.isLoading = false; + } + } + + get records(): RecordFormat[] { + let recordsToReturn = this.allRecords || []; + + if (this.localityId) { + recordsToReturn = recordsToReturn.filter( + (record) => record.locality === this.localityId + ); + } + + if (this.demographicView) { + recordsToReturn = recordsToReturn.filter( + recordIsTotalByDimension(this.demographicView) + ); + } + + return recordsToReturn; + } +} + +export default Metric; + +/** + * Returns a filter predicate for the specified demographic view + * that will exclude totals and breakdowns for all other views + */ +function recordIsTotalByDimension( + demographicView: DemographicView +): (record: RawRecord) => boolean { + const keysEnum = { ...DIMENSION_DATA_KEYS }; + + if (demographicView !== "total") { + delete keysEnum[demographicView]; + } + + const otherDataKeys = Object.values(keysEnum); + + return (record) => { + let match = true; + if (demographicView !== "total") { + // filter out totals + match = + match && record[DIMENSION_DATA_KEYS[demographicView]] !== TOTAL_KEY; + } + + // filter out subset permutations + match = match && otherDataKeys.every((key) => record[key] === TOTAL_KEY); + + return match; + }; +} + +export type MetricsMapping = { + [MetricTypes.SentencePopulationCurrent]?: PopulationSnapshot; + [MetricTypes.SentenceTypesCurrent]?: SentenceTypesCurrent; +}; + +function extractBaseFields(record: ValuesType): BaseMetricFields { + return { + stateCode: record.state_code, + }; +} + +function extractDemographicFields( + record: ValuesType +): DemographicFields { + return { + // we are trusting the API to return valid strings here, not validating them + raceOrEthnicity: record.race_or_ethnicity as RaceIdentifier, + gender: record.gender as GenderIdentifier, + ageBucket: record.age_bucket as AgeIdentifier, + }; +} + +function transformSentencePopulationCurrent( + rawRecords: DataFile +): PopulationBreakdownByLocationRecord[] { + return rawRecords.map((record) => { + return { + locality: record.district, + population: Number(record.total_population_count), + ...extractBaseFields(record), + ...extractDemographicFields(record), + }; + }); +} + +function transformSentenceTypes( + rawRecords: DataFile +): SentenceTypeByLocationRecord[] { + return rawRecords.map((record) => { + return { + dualSentenceCount: Number(record.dual_sentence_count), + incarcerationCount: Number(record.incarceration_count), + locality: record.district, + probationCount: Number(record.probation_count), + ...extractBaseFields(record), + ...extractDemographicFields(record), + }; + }); +} + +export function createMetric(opts: { + metricType: MetricTypes.SentencePopulationCurrent; + initOptions: MetricFactoryOptionsBase; +}): PopulationSnapshot; +export function createMetric(opts: { + metricType: MetricTypes.SentenceTypesCurrent; + initOptions: MetricFactoryOptionsBase; +}): SentenceTypesCurrent; +// TODO: like 20 more overloads for all the different metric types -_- +/** + * Factory function for creating Metric instances. + * Returned Metric instance will be a the subtype of Metric + * indicated by the metricType argument. + */ +export function createMetric({ + metricType, + initOptions, +}: { + metricType: keyof typeof MetricTypes; + initOptions: MetricFactoryOptionsBase; +}): AnyMetric { + switch (metricType) { + case MetricTypes.SentencePopulationCurrent: + return new Metric({ + contentSource: initOptions, + dataTransformer: transformSentencePopulationCurrent, + defaultLocalityId: "ALL", + sourceFileName: "sentence_type_by_district_by_demographics", + }); + case MetricTypes.SentenceTypesCurrent: + return new Metric({ + contentSource: initOptions, + dataTransformer: transformSentenceTypes, + defaultLocalityId: "ALL", + defaultDemographicView: "total", + sourceFileName: "sentence_type_by_district_by_demographics", + }); + default: + throw new Error("unsupported metric type"); + } +} diff --git a/spotlight-client/src/datastores/contentSources/us_nd.ts b/spotlight-client/src/datastores/contentSources/us_nd.ts new file mode 100644 index 00000000..366c5ccb --- /dev/null +++ b/spotlight-client/src/datastores/contentSources/us_nd.ts @@ -0,0 +1,42 @@ +// Recidiviz - a data platform for criminal justice reform +// Copyright (C) 2020 Recidiviz, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// ============================================================================= + +import { TenantContent } from "../retrieveContent"; + +const content: TenantContent = { + name: "North Dakota", + description: + 'The North Dakota Department of Corrections and Rehabilitation (DOCR) provides correctional services for the state of North Dakota. Our mission is to transform lives, influence change, and strengthen community. Transparency is a critical element of our mission; sharing information builds greater accountability between the DOCR and the communities we serve. To this end, this collection of data visualizations is built to answer important questions that the public may have about the state of our correctional system in North Dakota. The data represented here is updated every day.', + metrics: { + SentencePopulationCurrent: { + name: "Who is being sentenced?", + description: + "After being convicted of a Class A misdemeanor or greater offense by a district court, a person may be sentenced to time in prison or probation, at which point they come under the jurisdiction of the Department of Corrections and Rehabilitation (DOCR). These charts show everyone currently involved with the North Dakota DOCR.", + methodology: + "This includes all individuals that are currently incarcerated, on parole, or on probation in North Dakota.", + }, + SentenceTypesCurrent: { + name: "What types of sentences do people receive?", + description: + "Sentences that lead to individuals coming under DOCR jurisdiction fall broadly into two categories: Probation and Incarceration.", + methodology: + "Incarceration includes any sentence that begins with a period of incarceration in a ND DOCR facility. Probation includes any sentence that begins with a period of probation under the supervision of a ND DOCR probation officer.

Of note, individuals’ current status (incarcerated or on supervision) may differ from their sentence category (incarceration or probation). Individuals now on parole after being incarcerated are still counted in the incarceration sentence category. Individuals who have had their probation revoked and are now in prison are likewise included in the probation sentence category because their sentence was first to probation.

It is possible for an individual to be serving both incarceration and probation sentences simultaneously. These individuals are counted in the “Both” category.

", + }, + }, +}; + +export default content; diff --git a/spotlight-client/src/datastores/retrieveContent.ts b/spotlight-client/src/datastores/retrieveContent.ts new file mode 100644 index 00000000..91200915 --- /dev/null +++ b/spotlight-client/src/datastores/retrieveContent.ts @@ -0,0 +1,40 @@ +// Recidiviz - a data platform for criminal justice reform +// Copyright (C) 2020 Recidiviz, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// ============================================================================= + +import US_ND from "./contentSources/us_nd"; +import { MetricTypes } from "./Metric"; +import { NamedEntity, TenantIds } from "./types"; + +export type TenantContent = NamedEntity & { + metrics: { + [key in MetricTypes]?: MetricsContent; + }; +}; + +type MetricsContent = NamedEntity & { methodology: string }; + +const CONTENT_SOURCES = { US_ND }; + +type RetrieveContentParams = { + tenantId: TenantIds; +}; + +export default function retrieveContent({ + tenantId, +}: RetrieveContentParams): TenantContent { + return CONTENT_SOURCES[tenantId]; +} diff --git a/spotlight-client/src/datastores/types.ts b/spotlight-client/src/datastores/types.ts new file mode 100644 index 00000000..4210dee9 --- /dev/null +++ b/spotlight-client/src/datastores/types.ts @@ -0,0 +1,29 @@ +// Recidiviz - a data platform for criminal justice reform +// Copyright (C) 2020 Recidiviz, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// ============================================================================= + +/** + * Abstract types + */ +export type NamedEntity = { + name: string; + description: string; +}; + +/** + * Tenant types + */ +export type TenantIds = "US_ND"; // TODO: union more IDs as they are added diff --git a/spotlight-client/src/metricsApi/metricsClient.ts b/spotlight-client/src/metricsApi/metricsClient.ts new file mode 100644 index 00000000..7cdced3d --- /dev/null +++ b/spotlight-client/src/metricsApi/metricsClient.ts @@ -0,0 +1,56 @@ +// Recidiviz - a data platform for criminal justice reform +// Copyright (C) 2020 Recidiviz, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// ============================================================================= + +export type DataFile = Record[]; + +type MetricsApiResponse = Record; + +/** + * An asynchronous function that returns a promise which will eventually return the results from + * invoking the given API endpoint. + */ +export async function callMetricsApi({ + files, +}: { + // TODO: should/can this signature be narrowed? + files: string[]; +}): Promise { + const response = await fetch( + // TODO: this endpoint is still somewhat imaginary; consider it a realistic placeholder + `${process.env.REACT_APP_API_URL}/api/public`, + { + body: JSON.stringify({ + files: files.map((filename) => `${filename}.json`), + }), + headers: { + "Content-Type": "application/json", + }, + method: "POST", + } + ); + + const responseData: MetricsApiResponse = await response.json(); + return responseData; +} + +/** + * A convenience function returning whether or not the client is still awaiting what it needs to + * display results to the user. + */ +export function awaitingResults(awaitingApi: boolean): boolean { + return awaitingApi; +} diff --git a/spotlight-client/src/setupTests.ts b/spotlight-client/src/setupTests.ts index 4a2546cd..bed73f6a 100644 --- a/spotlight-client/src/setupTests.ts +++ b/spotlight-client/src/setupTests.ts @@ -21,3 +21,8 @@ // learn more: https://github.com/testing-library/jest-dom import "@testing-library/jest-dom/extend-expect"; +import fetchMock from "jest-fetch-mock"; + +// we are globally mocking all fetch calls; +// tests should import `fetchMock` to provide test responses +fetchMock.enableMocks(); diff --git a/yarn.lock b/yarn.lock index 2aed72b3..d6488063 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4164,6 +4164,13 @@ create-react-context@0.3.0: gud "^1.0.0" warning "^4.0.3" +cross-fetch@^3.0.4: + version "3.0.6" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.0.6.tgz#3a4040bc8941e653e0e9cf17f29ebcd177d3365c" + integrity sha512-KBPUbqgFjzWlVcURG+Svp9TlhA5uliYtiNx/0r8nv0pdypeQCRJ9IaSIc3q/x3q8t3F75cHuwxVql1HFGHCNJQ== + dependencies: + node-fetch "2.6.1" + cross-spawn@7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.1.tgz#0ab56286e0f7c24e153d04cc2aa027e43a9a5d14" @@ -7783,6 +7790,14 @@ jest-environment-node@^24.9.0: jest-mock "^24.9.0" jest-util "^24.9.0" +jest-fetch-mock@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz#31749c456ae27b8919d69824f1c2bd85fe0a1f3b" + integrity sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw== + dependencies: + cross-fetch "^3.0.4" + promise-polyfill "^8.1.3" + jest-get-type@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e" @@ -9049,6 +9064,11 @@ nocache@2.1.0: resolved "https://registry.yarnpkg.com/nocache/-/nocache-2.1.0.tgz#120c9ffec43b5729b1d5de88cd71aa75a0ba491f" integrity sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q== +node-fetch@2.6.1, node-fetch@^2.3.0, node-fetch@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" + integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== + node-fetch@^1.0.1: version "1.7.3" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" @@ -9057,11 +9077,6 @@ node-fetch@^1.0.1: encoding "^0.1.11" is-stream "^1.0.1" -node-fetch@^2.3.0, node-fetch@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" - integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== - node-forge@^0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" @@ -13089,6 +13104,11 @@ utila@^0.4.0, utila@~0.4: resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw= +utility-types@^3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.10.0.tgz#ea4148f9a741015f05ed74fd615e1d20e6bed82b" + integrity sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg== + utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" From e925d3cc23a3d8ac77ac6a1c1f225ba3524eb412 Mon Sep 17 00:00:00 2001 From: Ian MacFarland Date: Tue, 17 Nov 2020 16:05:56 -0800 Subject: [PATCH 2/2] update some TS lint rules --- spotlight-client/.eslintrc.json | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/spotlight-client/.eslintrc.json b/spotlight-client/.eslintrc.json index 5fc9ffbe..f92109ff 100644 --- a/spotlight-client/.eslintrc.json +++ b/spotlight-client/.eslintrc.json @@ -11,6 +11,21 @@ // From eslint-config-prettier "prettier/@typescript-eslint" ], + "overrides": [ + { + "files": ["**.ts", "**.tsx"], + "rules": { + // these bare ESLint rules are superseded by TS equivalents + "no-use-before-define": "off", + "@typescript-eslint/no-use-before-define": ["error"], + "no-shadow": "off", + "@typescript-eslint/no-shadow": "error", + + // TypeScript is largely redundant with PropTypes + "react/prop-types": "off" + } + } + ], "parser": "@typescript-eslint/parser", "plugins": ["@typescript-eslint"], "rules": { @@ -18,11 +33,6 @@ // add it below to override it. Write a comment above each rule explaining // why the exception is made, so we know whether to keep it in the future. - // the bare eslint rule breaks in typescript - // https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-use-before-define.md#how-to-use - "no-use-before-define": "off", - "@typescript-eslint/no-use-before-define": ["error"], - // don't require extensions for typescript modules "import/extensions": [ "error", @@ -31,14 +41,7 @@ ], // support typescript as well as javascript file extensions - "react/jsx-filename-extension": [ - "error", - { "extensions": [".tsx", ".js"] } - ], - - // We use TypeScript props interfaces, which is mostly redundant with prop - // types. - "react/prop-types": "off" + "react/jsx-filename-extension": ["error", { "extensions": [".tsx", ".js"] }] }, "settings": { "import/resolver": {