diff --git a/package.json b/package.json index ef642a0ba..54cde6f58 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "@types/js-yaml": "^3.10.1", "@types/lodash": "^4.14.68", "@types/long": "^3.0.32", + "@types/lossless-json": "^1.0.0", "@types/memoize-one": "^4.1.0", "@types/memory-fs": "^0.3.0", "@types/node": "^9.6.6", @@ -144,6 +145,7 @@ "intersection-observer": "^0.7.0", "jest": "^24.9.0", "lint-staged": "^7.0.4", + "lossless-json": "^1.0.3", "memoize-one": "^5.0.0", "moment": "^2.18.1", "object-hash": "^1.3.1", diff --git a/src/components/Launch/LaunchWorkflowForm/CollectionInput.tsx b/src/components/Launch/LaunchWorkflowForm/CollectionInput.tsx new file mode 100644 index 000000000..703686f32 --- /dev/null +++ b/src/components/Launch/LaunchWorkflowForm/CollectionInput.tsx @@ -0,0 +1,55 @@ +import { TextField } from '@material-ui/core'; +import * as React from 'react'; +import { InputChangeHandler, InputProps, InputType } from './types'; +import { UnsupportedInput } from './UnsupportedInput'; + +function stringChangeHandler(onChange: InputChangeHandler) { + return ({ target: { value } }: React.ChangeEvent) => { + onChange(value); + }; +} + +/** Handles rendering of the input component for a Collection of SimpleType values*/ +export const CollectionInput: React.FC = props => { + const { + label, + helperText, + onChange, + typeDefinition: { subtype }, + value = '' + } = props; + if (!subtype) { + console.error( + 'Unexpected missing subtype for collection input', + props.typeDefinition + ); + return ; + } + switch (subtype.type) { + case InputType.Blob: + case InputType.Boolean: + case InputType.Collection: + case InputType.Datetime: + case InputType.Duration: + case InputType.Error: + case InputType.Float: + case InputType.Integer: + case InputType.Map: + case InputType.String: + case InputType.Struct: + return ( + + ); + default: + return ; + } +}; diff --git a/src/components/Launch/LaunchWorkflowForm/LaunchWorkflowForm.tsx b/src/components/Launch/LaunchWorkflowForm/LaunchWorkflowForm.tsx index decfeee25..c1fe92b67 100644 --- a/src/components/Launch/LaunchWorkflowForm/LaunchWorkflowForm.tsx +++ b/src/components/Launch/LaunchWorkflowForm/LaunchWorkflowForm.tsx @@ -18,6 +18,7 @@ import { workflowSortFields } from 'models'; import * as React from 'react'; +import { CollectionInput } from './CollectionInput'; import { SearchableSelector } from './SearchableSelector'; import { SimpleInput } from './SimpleInput'; import { InputProps, InputType, LaunchWorkflowFormProps } from './types'; @@ -54,6 +55,7 @@ const useStyles = makeStyles((theme: Theme) => ({ function getComponentForInput(input: InputProps) { switch (input.typeDefinition.type) { case InputType.Collection: + return ; case InputType.Map: case InputType.Schema: case InputType.Unknown: diff --git a/src/components/Launch/LaunchWorkflowForm/__stories__/LaunchWorkflowForm.stories.tsx b/src/components/Launch/LaunchWorkflowForm/__stories__/LaunchWorkflowForm.stories.tsx index a44ff00ec..a27eb8d38 100644 --- a/src/components/Launch/LaunchWorkflowForm/__stories__/LaunchWorkflowForm.stories.tsx +++ b/src/components/Launch/LaunchWorkflowForm/__stories__/LaunchWorkflowForm.stories.tsx @@ -1,55 +1,88 @@ +import { action } from '@storybook/addon-actions'; import { storiesOf } from '@storybook/react'; import { resolveAfter } from 'common/promiseUtils'; import { mockAPIContextValue } from 'components/data/__mocks__/apiContext'; import { APIContext } from 'components/data/apiContext'; -import { Workflow } from 'models'; +import { Admin } from 'flyteidl'; +import { mapValues } from 'lodash'; +import { Variable, Workflow } from 'models'; import { createMockLaunchPlan } from 'models/__mocks__/launchPlanData'; import { createMockWorkflow, createMockWorkflowClosure, createMockWorkflowVersions } from 'models/__mocks__/workflowData'; +import { mockExecution } from 'models/Execution/__mocks__/mockWorkflowExecutionsData'; import * as React from 'react'; import { LaunchWorkflowForm } from '../LaunchWorkflowForm'; -import { mockParameterMap, mockWorkflowInputsInterface } from './mockInputs'; +import { + createMockWorkflowInputsInterface, + mockCollectionVariables, + mockNestedCollectionVariables, + mockSimpleVariables +} from './mockInputs'; -const mockWorkflow = createMockWorkflow('MyWorkflow'); -const mockLaunchPlan = createMockLaunchPlan( - mockWorkflow.id.name, - mockWorkflow.id.version -); +const submitAction = action('createWorkflowExecution'); -const mockWorkflowVersions = createMockWorkflowVersions( - mockWorkflow.id.name, - 10 -); +const renderForm = (variables: Record) => { + const mockWorkflow = createMockWorkflow('MyWorkflow'); + const mockLaunchPlan = createMockLaunchPlan( + mockWorkflow.id.name, + mockWorkflow.id.version + ); + + const mockWorkflowVersions = createMockWorkflowVersions( + mockWorkflow.id.name, + 10 + ); -mockLaunchPlan.closure!.expectedInputs = mockParameterMap; + const parameterMap = { + parameters: mapValues(variables, v => ({ var: v })) + }; -const mockApi = mockAPIContextValue({ - getLaunchPlan: () => resolveAfter(500, mockLaunchPlan), - getWorkflow: id => { - const workflow: Workflow = { - id - }; - workflow.closure = createMockWorkflowClosure(); - workflow.closure!.compiledWorkflow!.primary.template.interface = mockWorkflowInputsInterface; + mockLaunchPlan.closure!.expectedInputs = parameterMap; - return resolveAfter(500, workflow); - }, - listWorkflows: () => resolveAfter(500, { entities: mockWorkflowVersions }), - listLaunchPlans: () => resolveAfter(500, { entities: [mockLaunchPlan] }) -}); + const mockApi = mockAPIContextValue({ + createWorkflowExecution: input => { + console.log(input); + submitAction('See console for data'); + return Promise.reject('Not implemented'); + }, + getLaunchPlan: () => resolveAfter(500, mockLaunchPlan), + getWorkflow: id => { + const workflow: Workflow = { + id + }; + workflow.closure = createMockWorkflowClosure(); + workflow.closure!.compiledWorkflow!.primary.template.interface = createMockWorkflowInputsInterface( + variables + ); -const onClose = () => console.log('Close'); + return resolveAfter(500, workflow); + }, + listWorkflows: () => + resolveAfter(500, { entities: mockWorkflowVersions }), + listLaunchPlans: () => resolveAfter(500, { entities: [mockLaunchPlan] }) + }); + + const onClose = () => console.log('Close'); + + return ( + +
+ +
+
+ ); +}; const stories = storiesOf('Launch/LaunchWorkflowForm', module); -stories.addDecorator(story => ( - -
{story()}
-
-)); - -stories.add('Basic', () => ( - -)); + +stories.add('Simple', () => renderForm(mockSimpleVariables)); +stories.add('Collections', () => renderForm(mockCollectionVariables)); +stories.add('Nested Collections', () => + renderForm(mockNestedCollectionVariables) +); diff --git a/src/components/Launch/LaunchWorkflowForm/__stories__/mockInputs.ts b/src/components/Launch/LaunchWorkflowForm/__stories__/mockInputs.ts index adab9b11a..13f54400e 100644 --- a/src/components/Launch/LaunchWorkflowForm/__stories__/mockInputs.ts +++ b/src/components/Launch/LaunchWorkflowForm/__stories__/mockInputs.ts @@ -15,7 +15,7 @@ function simpleType(primitiveType: SimpleType, description?: string): Variable { }; } -export const mockVariables: Record = { +export const mockSimpleVariables: Record = { simpleString: simpleType(SimpleType.STRING, 'a simple string value'), stringNoLabel: simpleType(SimpleType.STRING), simpleInteger: simpleType(SimpleType.INTEGER, 'a simple integer value'), @@ -32,12 +32,28 @@ export const mockVariables: Record = { // blob: {} }; -export const mockWorkflowInputsInterface: TypedInterface = { - inputs: { - variables: { ...mockVariables } - } -}; +export const mockCollectionVariables: Record = mapValues( + mockSimpleVariables, + v => ({ + description: `A collection of: ${v.description}`, + type: { collectionType: v.type } + }) +); -export const mockParameterMap: ParameterMap = { - parameters: mapValues(mockVariables, v => ({ var: v })) -}; +export const mockNestedCollectionVariables: Record< + string, + Variable +> = mapValues(mockCollectionVariables, v => ({ + description: `${v.description} (nested)`, + type: { collectionType: v.type } +})); + +export function createMockWorkflowInputsInterface( + variables: Record +): TypedInterface { + return { + inputs: { + variables: { ...variables } + } + }; +} diff --git a/src/components/Launch/LaunchWorkflowForm/inputConverters.ts b/src/components/Launch/LaunchWorkflowForm/inputConverters.ts index f8701b18a..b354b6981 100644 --- a/src/components/Launch/LaunchWorkflowForm/inputConverters.ts +++ b/src/components/Launch/LaunchWorkflowForm/inputConverters.ts @@ -1,62 +1,166 @@ import { dateToTimestamp, millisecondsToDuration } from 'common/utils'; import { Core } from 'flyteidl'; import * as Long from 'long'; +import * as LossLessJSON from 'lossless-json'; import { utc as moment } from 'moment'; -import { InputType } from './types'; +import { + InputProps, + InputType, + InputTypeDefinition, + InputValue +} from './types'; -function booleanToLiteral(value: string): Core.ILiteral { - return { scalar: { primitive: { boolean: Boolean(value) } } }; +function losslessReviver(key: string, value: any) { + if (value && value.isLosslessNumber) { + // *All* numbers will be converted to LossLessNumber, but we only want + // to use a Long if it would overflow + try { + return value.valueOf(); + } catch { + return Long.fromString(value.toString()); + } + } + return value; } -function stringToLiteral(stringValue: string): Core.ILiteral { +function parseJSON(value: string) { + return LossLessJSON.parse(value, losslessReviver); +} + +interface ConverterInput { + value: InputValue; + typeDefinition: InputTypeDefinition; +} + +function literalNone(): Core.ILiteral { + return { scalar: { noneType: {} } }; +} + +/** Converts any of our acceptable values to an actual boolean. */ +function parseBoolean(value: InputValue) { + if (typeof value === 'boolean') { + return value; + } + if (typeof value === 'string') { + return ['f', 'false', '0'].includes(value.toLowerCase()) ? false : true; + } + return !!value; +} + +function booleanToLiteral({ value }: ConverterInput): Core.ILiteral { + return { scalar: { primitive: { boolean: parseBoolean(value) } } }; +} + +function stringToLiteral({ value }: ConverterInput): Core.ILiteral { + const stringValue = typeof value === 'string' ? value : value.toString(); return { scalar: { primitive: { stringValue } } }; } -function integerToLiteral(value: string): Core.ILiteral { +function integerToLiteral({ value }: ConverterInput): Core.ILiteral { + const integer = + value instanceof Long ? value : Long.fromString(value.toString()); return { - scalar: { primitive: { integer: Long.fromString(value) } } + scalar: { primitive: { integer } } }; } -function floatToLiteral(value: string): Core.ILiteral { +function floatToLiteral({ value }: ConverterInput): Core.ILiteral { + const floatValue = + typeof value === 'number' ? value : parseFloat(value.toString()); return { - scalar: { primitive: { floatValue: parseFloat(value) } } + scalar: { primitive: { floatValue } } }; } -function dateToLiteral(value: string): Core.ILiteral { - const datetime = dateToTimestamp(moment(value).toDate()); +function dateToLiteral({ value }: ConverterInput): Core.ILiteral { + const parsed = + value instanceof Date ? value : moment(value.toString()).toDate(); + const datetime = dateToTimestamp(parsed); return { scalar: { primitive: { datetime } } }; } -function durationToLiteral(value: string): Core.ILiteral { - const duration = millisecondsToDuration(parseInt(value, 10)); +function durationToLiteral({ value }: ConverterInput): Core.ILiteral { + const parsed = + typeof value === 'number' ? value : parseInt(value.toString(), 10); + const duration = millisecondsToDuration(parsed); return { scalar: { primitive: { duration } } }; } -const unsupportedInput = () => undefined; +function parseCollection(list: string) { + const parsed = parseJSON(list); + if (!Array.isArray(parsed)) { + throw new Error('Value did not parse to an array'); + } + return parsed; +} + +function collectionToLiteral({ + value, + typeDefinition: { subtype } +}: ConverterInput): Core.ILiteral { + if (!subtype) { + throw new Error('Unexpected missing subtype for collection'); + } + let parsed: any[]; + // If we're processing a nested collection, it may already have been parsed + if (Array.isArray(value)) { + parsed = value; + } else { + const stringValue = + typeof value === 'string' ? value : value.toString(); + if (!stringValue.length) { + return literalNone(); + } + parsed = parseCollection(stringValue); + if (!parsed.length) { + return literalNone(); + } + } + + const converter = inputTypeConverters[subtype.type]; + const literals = parsed.map(value => + converter({ value, typeDefinition: subtype }) + ); + + return { + collection: { + literals + } + }; +} -type ConverterFn = (value: string) => Core.ILiteral | undefined; +const unsupportedInput = literalNone; +type ConverterFn = (input: ConverterInput) => Core.ILiteral; /** Maps an `InputType` to a function which will convert its value into a `Literal` */ export const inputTypeConverters: Record = { - [InputType.Binary]: unsupportedInput, + [InputType.Binary]: literalNone, [InputType.Blob]: unsupportedInput, [InputType.Boolean]: booleanToLiteral, - [InputType.Collection]: unsupportedInput, + [InputType.Collection]: collectionToLiteral, [InputType.Datetime]: dateToLiteral, [InputType.Duration]: durationToLiteral, [InputType.Error]: unsupportedInput, [InputType.Float]: floatToLiteral, [InputType.Integer]: integerToLiteral, [InputType.Map]: unsupportedInput, - [InputType.None]: unsupportedInput, + [InputType.None]: literalNone, [InputType.Schema]: unsupportedInput, [InputType.String]: stringToLiteral, [InputType.Struct]: unsupportedInput, [InputType.Unknown]: unsupportedInput }; + +export function inputToLiteral(input: InputProps): Core.ILiteral { + if (input.value == null) { + return literalNone(); + } + const { typeDefinition, value } = input; + + const converter = inputTypeConverters[typeDefinition.type]; + return converter({ value, typeDefinition }); +} diff --git a/src/components/Launch/LaunchWorkflowForm/test/inputConverters.test.ts b/src/components/Launch/LaunchWorkflowForm/test/inputConverters.test.ts new file mode 100644 index 000000000..f2659727b --- /dev/null +++ b/src/components/Launch/LaunchWorkflowForm/test/inputConverters.test.ts @@ -0,0 +1,174 @@ +import { dateToTimestamp, millisecondsToDuration } from 'common/utils'; +import { Core } from 'flyteidl'; +import * as Long from 'long'; +import { inputToLiteral } from '../inputConverters'; +import { InputProps, InputType } from '../types'; + +const baseInputProps: InputProps = { + description: 'test', + label: 'test', + name: '', + onChange: () => {}, + required: false, + typeDefinition: { type: InputType.Unknown } +}; + +function makeSimpleInput(type: InputType, value: any): InputProps { + return { ...baseInputProps, value, typeDefinition: { type } }; +} + +function makeCollectionInput(type: InputType, value: string): InputProps { + return { + ...baseInputProps, + value, + typeDefinition: { type: InputType.Collection, subtype: { type } } + }; +} + +function makeNestedCollectionInput(type: InputType, value: string): InputProps { + return { + ...baseInputProps, + value, + typeDefinition: { + type: InputType.Collection, + subtype: { type: InputType.Collection, subtype: { type } } + } + }; +} + +// Defines type of value, input, and expected value of innermost `IScalar` +type PrimitiveTestParams = [InputType, any, Core.IPrimitive]; + +const validDateString = '2019-01-10T00:00:00.000Z'; // Dec 1, 2019 + +describe('inputToLiteral', () => { + const simpleTestCases: PrimitiveTestParams[] = [ + [InputType.Boolean, true, { boolean: true }], + [InputType.Boolean, 'true', { boolean: true }], + [InputType.Boolean, 't', { boolean: true }], + [InputType.Boolean, '1', { boolean: true }], + [InputType.Boolean, 1, { boolean: true }], + [InputType.Boolean, false, { boolean: false }], + [InputType.Boolean, 'false', { boolean: false }], + [InputType.Boolean, 'f', { boolean: false }], + [InputType.Boolean, '0', { boolean: false }], + [InputType.Boolean, 0, { boolean: false }], + [ + InputType.Datetime, + new Date('2019-01-10T00:00:00.000Z'), + { datetime: dateToTimestamp(new Date(validDateString)) } + ], + [ + InputType.Datetime, + validDateString, + { datetime: dateToTimestamp(new Date(validDateString)) } + ], + [InputType.Duration, 0, { duration: millisecondsToDuration(0) }], + [ + InputType.Duration, + 10000, + { duration: millisecondsToDuration(10000) } + ], + [InputType.Float, 0, { floatValue: 0 }], + [InputType.Float, '0', { floatValue: 0 }], + [InputType.Float, -1.5, { floatValue: -1.5 }], + [InputType.Float, '-1.5', { floatValue: -1.5 }], + [InputType.Float, 1.5, { floatValue: 1.5 }], + [InputType.Float, '1.5', { floatValue: 1.5 }], + [InputType.Float, 1.25e10, { floatValue: 1.25e10 }], + [InputType.Float, '1.25e10', { floatValue: 1.25e10 }], + [InputType.Integer, 0, { integer: Long.fromNumber(0) }], + [ + InputType.Integer, + Long.fromNumber(0), + { integer: Long.fromNumber(0) } + ], + [InputType.Integer, '0', { integer: Long.fromNumber(0) }], + [InputType.Integer, 1, { integer: Long.fromNumber(1) }], + [ + InputType.Integer, + Long.fromNumber(1), + { integer: Long.fromNumber(1) } + ], + [InputType.Integer, '1', { integer: Long.fromNumber(1) }], + [InputType.Integer, -1, { integer: Long.fromNumber(-1) }], + [ + InputType.Integer, + Long.fromNumber(-1), + { integer: Long.fromNumber(-1) } + ], + [InputType.Integer, '-1', { integer: Long.fromNumber(-1) }], + [ + InputType.Integer, + Long.MAX_VALUE.toString(), + { integer: Long.MAX_VALUE } + ], + [InputType.Integer, Long.MAX_VALUE, { integer: Long.MAX_VALUE }], + [ + InputType.Integer, + Long.MIN_VALUE.toString(), + { integer: Long.MIN_VALUE } + ], + [InputType.Integer, Long.MIN_VALUE, { integer: Long.MIN_VALUE }], + [InputType.String, '', { stringValue: '' }], + [InputType.String, 'abcdefg', { stringValue: 'abcdefg' }] + ]; + describe('Primitives', () => { + simpleTestCases.map(([type, input, output]) => + it(`Should correctly convert ${type}: ${input} (${typeof input})`, () => { + const result = inputToLiteral(makeSimpleInput(type, input)); + expect(result.scalar!.primitive).toEqual(output); + }) + ); + }); + + describe('Collections', () => { + simpleTestCases.map(([type, input, output]) => { + let value: any; + if (['boolean', 'number'].includes(typeof input)) { + value = input; + } else if (input instanceof Date) { + value = `"${input.toISOString()}"`; + } else { + value = `"${input}"`; + } + + it(`Should correctly convert collection of type ${type}: [${value}] (${typeof input})`, () => { + const result = inputToLiteral( + makeCollectionInput(type, `[${value}]`) + ); + expect( + result.collection!.literals![0].scalar!.primitive + ).toEqual(output); + }); + + it(`Should correctly convert nested collection of type ${type}: [[${value}]] (${typeof input})`, () => { + const result = inputToLiteral( + makeNestedCollectionInput(type, `[[${value}]]`) + ); + expect( + result.collection!.literals![0].collection!.literals![0] + .scalar!.primitive + ).toEqual(output); + }); + }); + }); + describe('Unsupported Types', () => { + [ + InputType.Binary, + InputType.Blob, + InputType.Error, + InputType.Map, + InputType.None, + InputType.Schema, + InputType.Struct, + InputType.Unknown + ].map(type => + it(`Should return empty value for type: ${type}`, () => { + expect( + inputToLiteral(makeSimpleInput(type, '')).scalar + ).toEqual({ noneType: {} }); + }) + ); + }); +}); diff --git a/src/components/Launch/LaunchWorkflowForm/useLaunchWorkflowFormState.ts b/src/components/Launch/LaunchWorkflowForm/useLaunchWorkflowFormState.ts index 6a2ee3761..e24f562d3 100644 --- a/src/components/Launch/LaunchWorkflowForm/useLaunchWorkflowFormState.ts +++ b/src/components/Launch/LaunchWorkflowForm/useLaunchWorkflowFormState.ts @@ -6,6 +6,7 @@ import { useWorkflows, waitForAllFetchables } from 'components/hooks'; +import { ParameterError, ValidationError } from 'errors'; import { FilterOperationName, LaunchPlan, @@ -29,7 +30,7 @@ import { LaunchWorkflowFormState } from './types'; import { - convertFormInputsToLiteralMap, + convertFormInputsToLiterals, formatLabelWithType, getInputDefintionForLiteralType, getWorkflowInputs, @@ -221,11 +222,20 @@ export function useLaunchWorkflowFormState({ } const launchPlanId = launchPlanData.id; const { domain, project } = workflowId; + + const { errors, literals } = convertFormInputsToLiterals(inputs); + if (Object.keys(errors).length) { + throw new ValidationError( + Object.keys(errors).map( + name => new ParameterError(name, errors[name]) + ) + ); + } const response = await createWorkflowExecution({ domain, launchPlanId, project, - inputs: convertFormInputsToLiteralMap(inputs) + inputs: { literals } }); const newExecutionId = response.id as WorkflowExecutionIdentifier; if (!newExecutionId) { diff --git a/src/components/Launch/LaunchWorkflowForm/utils.ts b/src/components/Launch/LaunchWorkflowForm/utils.ts index ace6e6b21..379511d1a 100644 --- a/src/components/Launch/LaunchWorkflowForm/utils.ts +++ b/src/components/Launch/LaunchWorkflowForm/utils.ts @@ -1,4 +1,5 @@ import { timestampToDate } from 'common/utils'; +import { ParameterError, ValidationError } from 'errors'; import { LaunchPlan, Literal, @@ -10,7 +11,7 @@ import { } from 'models'; import * as moment from 'moment'; import { simpleTypeToInputType, typeLabels } from './constants'; -import { inputTypeConverters } from './inputConverters'; +import { inputToLiteral, inputTypeConverters } from './inputConverters'; import { SearchableSelectorOption } from './SearchableSelector'; import { InputProps, InputType, InputTypeDefinition } from './types'; @@ -88,30 +89,26 @@ export function launchPlansToSearchableSelectorOptions( })); } -function inputToLiteral(input: InputProps) { - if (!input.value) { - return undefined; - } - const converter = inputTypeConverters[input.typeDefinition.type]; - const value = input.value.toString(); - return converter(value); +interface ConversionResult { + literals: Record; + errors: Record; } - /** Converts a list of Launch form inputs to values that can be submitted with * a CreateExecutionRequest. */ -export function convertFormInputsToLiteralMap( +export function convertFormInputsToLiterals( inputs: InputProps[] -): LiteralMap { - const literals = inputs.reduce>((out, input) => { - const converted = inputToLiteral(input); - return converted - ? Object.assign(out, { [input.name]: converted }) - : out; - }, {}); - return { - literals - }; +): ConversionResult { + const result: ConversionResult = { literals: {}, errors: {} }; + return inputs.reduce((out, input) => { + try { + const converted = inputToLiteral(input); + Object.assign(out.literals, { [input.name]: converted }); + } catch (error) { + Object.assign(out.errors, { [input.name]: `${error}` }); + } + return result; + }, result); } /** Converts a `LiteralType` to an `InputTypeDefintion` to assist with rendering diff --git a/yarn.lock b/yarn.lock index 0e3a86569..8c90a9ae0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2930,6 +2930,11 @@ resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.0.tgz#719551d2352d301ac8b81db732acb6bdc28dbdef" integrity sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q== +"@types/lossless-json@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/lossless-json/-/lossless-json-1.0.0.tgz#6493f813b9421f45bfef9b30e99242652cbd025e" + integrity sha512-knKgXT5I1x87nKLuwCKWi7nfwwYrmyi51ss7O8kAnbj8c116wBm86Laj9yguoN+Ju1S8jkjasam/OdearnQKRw== + "@types/memoize-one@^4.1.0": version "4.1.0" resolved "https://registry.yarnpkg.com/@types/memoize-one/-/memoize-one-4.1.0.tgz#62119f26055b3193ae43ca1882c5b29b88b71ece" @@ -9710,6 +9715,11 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3 dependencies: js-tokens "^3.0.0 || ^4.0.0" +lossless-json@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/lossless-json/-/lossless-json-1.0.3.tgz#f9ce7daeb79e4a0f38a3c0340177654b434bc2a2" + integrity sha512-r4w0WrhIHV1lOTVGbTg4Toqwso5x6C8pM7Q/Nto2vy4c7yUSdTYVYlj16uHVX3MT1StpSELDv8yrqGx41MBsDA== + lower-case@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac"