New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
#1480 Output key validation #1628
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
0a24f35
traceError validators
BALEHOK 2005ab0
Output key validator
BALEHOK 226cc75
unit tests
BALEHOK 4e5b748
OutputKey validator refactoring
BALEHOK 77d15f6
Memoization
BALEHOK 3379f3b
Removing unnecessary string convertions
BALEHOK ae22c6d
Adding number to PipelineErrors keys
BALEHOK e3e4edd
Blocks map
BALEHOK a0e32bd
AllBlocks map
BALEHOK a155366
Explanation on PipelineErrors
BALEHOK 0007f9c
small fixes
BALEHOK b0d574b
renamings
BALEHOK File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,70 +21,49 @@ import { useCallback } from "react"; | |
import { BlockPipeline } from "@/blocks/types"; | ||
import { useField, useFormikContext, setNestedObjectValues } from "formik"; | ||
import { TraceError } from "@/telemetry/trace"; | ||
import { isInputValidationError } from "@/blocks/errors"; | ||
import { OutputUnit } from "@cfworker/json-schema"; | ||
import { useAsyncEffect } from "use-async-effect"; | ||
import { joinName } from "@/utils"; | ||
import { set } from "lodash"; | ||
import validateOutputKey from "@/devTools/editor/validation/validateOutputKey"; | ||
import applyTraceError from "@/devTools/editor/validation/applyTraceError"; | ||
import { isEmpty } from "lodash"; | ||
import { BlocksMap } from "@/devTools/editor/tabs/editTab/editTabTypes"; | ||
|
||
const REQUIRED_FIELD_REGEX = /^Instance does not have required property "(?<property>.+)"\.$/; | ||
/* | ||
* PipelineErrors is Formik error... thing. | ||
* It can be a string, a record of strings, or a record of records... i.e. it is dynamic and depends on the level of the state tree where the error happens. | ||
* Speaking about `PipelineErrors[0]`, the error state normally is not an array, but since the pipeline is an array we use numbers (index) to get the error related to a block. | ||
* Despite it looks like an array (the top-level may look like an array - have numbers for property names), it is an object. | ||
* For instance, it doesn't have a `length` property. | ||
*/ | ||
export type PipelineErrors = string | Record<string | number, unknown>; | ||
|
||
const pipelineBlocksFieldName = "extension.blockPipeline"; | ||
|
||
function usePipelineField(): { | ||
function usePipelineField( | ||
allBlocks: BlocksMap | ||
): { | ||
blockPipeline: BlockPipeline; | ||
blockPipelineErrors: unknown[]; | ||
setBlockPipeline: (value: BlockPipeline, shouldValidate: boolean) => void; | ||
traceError: TraceError; | ||
blockPipelineErrors: PipelineErrors; | ||
errorTraceEntry: TraceError; | ||
} { | ||
const traceError = useSelector(selectTraceError); | ||
const errorTraceEntry = useSelector(selectTraceError); | ||
|
||
const validatePipelineBlocks = useCallback( | ||
(pipeline: BlockPipeline) => { | ||
if (!traceError) { | ||
return; | ||
} | ||
(pipeline: BlockPipeline): void | PipelineErrors => { | ||
const formikErrors: Record<string, unknown> = {}; | ||
|
||
const { error, blockInstanceId } = traceError; | ||
const blockIndex = pipeline.findIndex( | ||
(block) => block.instanceId === blockInstanceId | ||
); | ||
if (blockIndex === -1) { | ||
return; | ||
} | ||
|
||
const errors: Record<string, unknown> = {}; | ||
if (isInputValidationError(error)) { | ||
for (const unit of (error.errors as unknown) as OutputUnit[]) { | ||
let propertyNameInPipeline: string; | ||
let errorMessage: string; | ||
|
||
const property = REQUIRED_FIELD_REGEX.exec(unit.error)?.groups | ||
.property; | ||
if (property) { | ||
propertyNameInPipeline = joinName( | ||
String(blockIndex), | ||
"config", | ||
property | ||
); | ||
errorMessage = "Error from the last run: This field is required"; | ||
} else { | ||
propertyNameInPipeline = String(blockIndex); | ||
errorMessage = error.message; | ||
} | ||
|
||
set(errors, propertyNameInPipeline, errorMessage); | ||
} | ||
} else { | ||
// eslint-disable-next-line security/detect-object-injection | ||
errors[blockIndex] = error.message; | ||
} | ||
validateOutputKey(formikErrors, pipeline, allBlocks); | ||
applyTraceError(formikErrors, errorTraceEntry, pipeline); | ||
Comment on lines
+54
to
+55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice, I like these names a lot more 👍 |
||
|
||
return errors; | ||
return isEmpty(formikErrors) ? undefined : formikErrors; | ||
}, | ||
[traceError] | ||
[errorTraceEntry, allBlocks] | ||
); | ||
|
||
const formikField = useField<BlockPipeline>({ | ||
name: "extension.blockPipeline", | ||
const [ | ||
{ value: blockPipeline }, | ||
{ error: blockPipelineErrors }, | ||
] = useField<BlockPipeline>({ | ||
name: pipelineBlocksFieldName, | ||
// @ts-expect-error working with nested errors | ||
validate: validatePipelineBlocks, | ||
}); | ||
|
@@ -101,14 +80,13 @@ function usePipelineField(): { | |
formikContext.setTouched(setNestedObjectValues(validationErrors, true)); | ||
} | ||
}, | ||
[traceError] | ||
[errorTraceEntry] | ||
); | ||
|
||
return { | ||
blockPipeline: formikField[0].value, | ||
blockPipelineErrors: (formikField[1].error as unknown) as unknown[], | ||
setBlockPipeline: formikField[2].setValue, | ||
traceError, | ||
blockPipeline, | ||
blockPipelineErrors, | ||
errorTraceEntry, | ||
}; | ||
} | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { BlockType } from "@/blocks/util"; | ||
import { IBlock, RegistryId } from "@/core"; | ||
|
||
export type BlocksMap = Record< | ||
RegistryId, | ||
{ | ||
block: IBlock; | ||
type: BlockType; | ||
} | ||
>; |
47 changes: 47 additions & 0 deletions
47
src/devTools/editor/validation/applyTraceBlockError.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
/* | ||
* Copyright (C) 2021 PixieBrix, Inc. | ||
* | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU Affero 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 Affero General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU Affero General Public License | ||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
import { traceErrorFactory } from "@/tests/factories"; | ||
import applyTraceBlockError from "./applyTraceBlockError"; | ||
|
||
test("sets block error", () => { | ||
const pipelineErrors: Record<string, unknown> = {}; | ||
const errorTraceEntry = traceErrorFactory(); | ||
|
||
applyTraceBlockError(pipelineErrors, errorTraceEntry, 0); | ||
|
||
expect(pipelineErrors[0]).toBe(errorTraceEntry.error.message); | ||
}); | ||
|
||
test("doesn't override nested error", () => { | ||
const errorTraceEntry = traceErrorFactory(); | ||
const blockIndex = 5; | ||
|
||
const nestedBlockError = { | ||
config: { | ||
name: "Nested Error", | ||
}, | ||
}; | ||
const pipelineErrors = { | ||
[blockIndex]: nestedBlockError, | ||
}; | ||
|
||
applyTraceBlockError(pipelineErrors, errorTraceEntry, blockIndex); | ||
|
||
// eslint-disable-next-line security/detect-object-injection | ||
expect(pipelineErrors[blockIndex]).toBe(nestedBlockError); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
/* | ||
* Copyright (C) 2021 PixieBrix, Inc. | ||
* | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU Affero 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 Affero General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU Affero General Public License | ||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
import { TraceError } from "@/telemetry/trace"; | ||
|
||
function applyTraceBlockError( | ||
pipelineErrors: Record<string, unknown>, | ||
errorTraceEntry: TraceError, | ||
blockIndex: number | ||
) { | ||
// eslint-disable-next-line security/detect-object-injection | ||
if (!pipelineErrors[blockIndex]) { | ||
// eslint-disable-next-line security/detect-object-injection | ||
pipelineErrors[blockIndex] = errorTraceEntry.error.message; | ||
} | ||
} | ||
|
||
export default applyTraceBlockError; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clarifying my performance idea from the outdated pull request:
You can add a useMemo here to convert allBlocks into a Map for faster lookups in the validation. The blockMap variable can then be referenced in the useCallback
const blockMap = useMemo(() => new Map(allBlocks.map(x => [x.id, x]), [allBlocks])