-
Notifications
You must be signed in to change notification settings - Fork 4.2k
feat(api-service,framework): validate liquidjs filter args for the variables #8178
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
Conversation
✅ Deploy Preview for dashboard-v2-novu-staging ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
ef89a0b to
8273085
Compare
commit: |
| if ('filterMessage' in error) { | ||
| return { | ||
| message: `Variable ${error.output} filter ${error.filterMessage}`, | ||
| issueType: StepContentIssueEnum.INVALID_FILTER_ARG_IN_VARIABLE, | ||
| variableName: error.output, | ||
| }; | ||
| } |
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.
The LiquidJS filter errors have the filterMessage field so this way we differentiate it from the variable errors.
In the future, there will be more fields like filterBegin: number; filterEnd: number, which could tell the exact place in the template where the error is.
| const { validVariables, invalidVariables } = extractLiquidTemplateVariables({ | ||
| template: JSON.stringify(variableControlValue), | ||
| variableSchema, | ||
| }); |
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.
We now have only one place where we create valid/invalid variables.
This change also simplifies the future extension for the additional invalid variables information, like a start, end positions in the template, which the FE could then use to inform the user.
| }; | ||
| } | ||
|
|
||
| function isPropertyAllowed(schema: Record<string, unknown>, propertyPath: string) { |
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.
this function is used in the another file
| export function extractLiquidTemplateVariables(template: string): TemplateVariables { | ||
| export function extractLiquidTemplateVariables({ | ||
| template, | ||
| variableSchema, |
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.
pass the variableSchema to be able to validate the variables against it
| const validVariables: Variable[] = []; | ||
| const invalidVariables: Variable[] = []; | ||
| const parsed = parserEngine.parse(rawOutput) as unknown as Template[]; | ||
| function isPropertyAllowed(schema: Record<string, unknown> | undefined, propertyPath: string) { |
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.
reused
| const filters = extractFilters(template); | ||
| const filterIssues = validateFilters(filters, isDigestEventsVariable); |
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.
Extract filters from the parsed liquid template, and then validate the filters.
The single filter has the information about the filter name and args, also includes the position in the template.
| const argsSchema = z.object({ | ||
| keyPath: requireKeyPath ? z.string().min(1, 'must be non-empty') : z.string().optional().default(DEFAULT_KEY_PATH), | ||
| limit: z | ||
| .number() | ||
| .optional() | ||
| .default(DEFAULT_LIMIT) | ||
| .refine((val) => { | ||
| return val >= 0; | ||
| }, 'must be greater than or equal to 0'), | ||
| overflowSuffix: z.string().optional().default(DEFAULT_OVERFLOW_SUFFIX), | ||
| wordsConnector: z.string().optional().default(DEFAULT_WORDS_CONNECTOR), | ||
| twoWordsConnector: z.string().optional().default(DEFAULT_TWO_WORDS_CONNECTOR), | ||
| lastWordConnector: z.string().optional().default(DEFAULT_LAST_WORD_CONNECTOR), | ||
| }); |
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.
Zod schema for the filter args
| args.forEach((arg, index) => { | ||
| if (!Array.isArray(arg)) { | ||
| let value: string | number = arg.getText(); | ||
| if (arg.kind === TokenKind.Quoted) { | ||
| value = (arg as QuotedToken).content; | ||
| } else if (arg.kind === TokenKind.Number) { | ||
| value = (arg as NumberToken).content; | ||
| } | ||
| const argName = ARG_INDEX_TO_ARG_NAME[index]; | ||
| argsObject[argName] = value; | ||
| } | ||
| }); |
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.
convert args array to object that will be validated
| for (const error of result.error.issues) { | ||
| let type = 'string'; | ||
| if ('type' in error) { | ||
| type = error.type; | ||
| } | ||
|
|
||
| const path = error.path[0]; | ||
| const argIndexToArgName = Object.entries(ARG_INDEX_TO_ARG_NAME).find(([_, argName]) => argName === path); | ||
| const argIndex = argIndexToArgName ? parseInt(argIndexToArgName[0], 10) : null; | ||
| const token = typeof argIndex === 'number' ? args[argIndex] : null; | ||
|
|
||
| if (token && !Array.isArray(token)) { | ||
| issues.push({ | ||
| message: `"toSentence" expects a ${type}${error.message ? ` that ${error.message}` : ''} for argument "${path}"`, | ||
| begin: token.begin, | ||
| end: token.end, | ||
| value: token.getText(), | ||
| }); | ||
| } | ||
| } |
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.
build the validation errors
apps/api/src/app/workflows-v2/util/template-parser/liquid-parser.ts
Outdated
Show resolved
Hide resolved
apps/api/src/app/workflows-v2/usecases/build-step-issues/build-step-issues.usecase.ts
Outdated
Show resolved
Hide resolved
| output: string; | ||
| }; | ||
|
|
||
| type InvalidContentVariable = Variable & { |
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.
I am not sure I prefer having two separate types for this. It introduces too many types and I think it makes the code more cumbersome that it should be. Variables are still variables, regardless if they appear in filters or in content, right?
Alternatively, I believe extending the Variable object with extra optional attributes is a better idea. For now, the filterMessage is only used here to define the error message.
My suggestion is just populate the existing variable.message field for now with the following copywriting:
Filter ${error.filterMessage} in ${variable.name}.
Notice that I used the name not the full output of the variable for a shorter and friendlier message.
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.
I've separated the types because in the future there could be details about the filter start and end positions. Also we don't have a way to differentiate these validation errors to build the different messages, that's why I introduced the filterMessage field.
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.
So I'll revert to one type but with two fields for message, unless we want the type.
| ): LiquidFilterIssue[] { | ||
| const { requireKeyPath = false } = options; | ||
| const issues: LiquidFilterIssue[] = []; | ||
| if (args.length < 1) { |
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.
@LetItRock I am not sure I follow the logic here. Can't we verify args only using zod by converting the filter arguyments arguments to an object without having to do all this pre and post processing?
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.
I'm not sure what you mean by pre/post processing. Can you clarify or pseudo code?
converting the filter arguyments arguments to an object
This is what I do at line 121
But if it's about the args, there are a few reasons it takes an args array:
- the LiquidJS engine produces the output tokens for variables where the filters have
argsas an array - we don't need to have the conversion logic on the API side for every filter, so the code is generic there
- it's aligned with the order of defined args in the filter function
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.
My bad, I wanted to request changes :)
What changed? Why was the change needed?
Implemented the validation logic for the liquid variable filters, which is built on top of the step issues.
The filter args validation function is defined close to the LiquidJS filter, meaning that in the Framework. We can later decide if we want to do similar validation in the Framework.
Screenshots
Screen.Recording.2025-04-22.at.18.01.22.mov