-
Notifications
You must be signed in to change notification settings - Fork 4k
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
chore(api): Refactor workflows v2 #8069
Conversation
✅ Deploy Preview for dashboard-v2-novu-staging canceled.
|
@@ -52,7 +52,25 @@ jobs: | |||
shell: bash | |||
env: | |||
NOVU_ENTERPRISE: ${{ inputs.ee }} | |||
run: cd apps/worker && pnpm start:test 2>&1 & | |||
run: | |
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.
An attempt to make E2E more robust when the worker hangs
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.
feels a bit hacky i wonder if there is a more robust way to fix it within the worker service.
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 would maybe put it to a bash script than having it in yaml directly, so the workflow is less overwhelming.
pnpm start:worker:test 2>&1 & ./monitor-worker.sh
})), | ||
}; | ||
|
||
const { body } = await session.testAgent.post('/v2/workflows').send(createWorkflowDto); | ||
const workflow = body.data; | ||
|
||
for (const [index, step] of steps.entries()) { |
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 endpoint doesn't exist anymore. So the data generator of the E2E had to be updated
@@ -1,8 +1,9 @@ | |||
// TODO: Move this file under e2e folder and merge it with the one that has the same name |
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 didn't do it as part of this PR to avoid losing the diff.
|
||
@IsBoolean() | ||
@IsOptional() | ||
skipControlValueIssues?: boolean; |
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.
Control if control value issues should be enforced or not.
|
||
async function patchStepWithControlValues(workflowSlug: string, stepSlug: string, controlValues: InAppControlType) { |
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.
Ditto, there is no patch step endpoint anymore.
@@ -208,24 +223,8 @@ export class WorkflowController { | |||
); | |||
} | |||
|
|||
@Patch('/:workflowId/steps/:stepId') |
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 PATCH step endpoint wasn't used in the Dashboard anymore. Most importantly, it's a problematic endpoint that couldn't survive our modelling.
Steps depend on each other through variables. So, PATCHing a single step without considering the other steps shouldn't be allowed. Imagine patching a digest step that affects all the other steps that follow.
private patchWorkflowUsecase: PatchWorkflowUsecase, | ||
private duplicateWorkflowUseCase: DuplicateWorkflowUseCase | ||
) {} | ||
|
||
@Post('') | ||
@ExternalApiAccessible() |
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.
Expose the basic CRUD endpoints for API access. We won't document them yet.
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.
im not against this, but as far as i remember, we agreed that this route would be internal.
when needed, we would introduce a lean route specifically for api usage.
@@ -32,5 +32,5 @@ type SidebarFooterProps = HTMLAttributes<HTMLDivElement>; | |||
|
|||
export const SidebarFooter = (props: SidebarFooterProps) => { | |||
const { className, ...rest } = props; | |||
return <div className={cn('mt-auto space-y-2.5 px-2 py-3.5', className)} {...rest} />; | |||
return <div className={cn('border-t-border-weak mt-auto space-y-2.5 border-t p-2', className)} {...rest} />; |
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.
Minor UI fix for the Step editor footer to ensure the border has 100% width.
@@ -202,7 +202,7 @@ export const WorkflowsPage = () => { | |||
}} | |||
> | |||
<RiFileMarkedLine /> | |||
View Workflow Gallery |
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.
Copywriting fix: It's the Workflow Template store, we don't use the term Gallery.
@@ -1,5 +1,4 @@ | |||
import { RedirectToSignIn, SignedIn, SignedOut } from '@clerk/clerk-react'; | |||
import { ROUTES } from '@/utils/routes'; |
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.
Oops, I forge this in next
@@ -21,6 +21,7 @@ function sanitizeEmptyInput<T_Type>(input: T_Type, defaultValue: T_Type = undefi | |||
} | |||
|
|||
export function sanitizeRedirect(redirect: InAppRedirectType | undefined) { | |||
// TODO: There is a bug here, if the redirect doesn't contain both a url and a target it is removed from the new controlValues |
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 must fix this so as not to have issues if the endpoints are used by the API and not the Dashboard that always sends a full object for redirectURls and actions.
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.
can you please elaborate on the exact problem we’re facing here?
and yes, this sanitization was explicitly designed to sanitize only dashboard values; it’s even in the name: dashboardSanitizeControlValues
.
this was intentional because we decided that the v2 api would be client-first.
@@ -104,7 +100,7 @@ export class UiSchema { | |||
properties?: Record<string, UiSchemaProperty>; | |||
} | |||
|
|||
export class ControlsMetadata { | |||
export class Controls { |
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.
Just a shorter type name.
commandWorkflowSteps: Array<StepCreateDto | StepUpdateDto>, | ||
persistedWorkflow?: NotificationTemplateEntity | undefined | ||
private async buildSteps( | ||
command: UpsertWorkflowCommand, |
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.
Notice that too avoid multiple arguments for each private function and ad-hoc types, the command object is passe to all of them so that each function can pick whatever it needs from it.
private notificationGroupRepository: NotificationGroupRepository, | ||
private getWorkflowByIdsUseCase: GetWorkflowByIdsUseCase, | ||
private getWorkflowUseCase: GetWorkflowUseCase, | ||
private buildStepIssuesUsecase: BuildStepIssuesUsecase, | ||
private controlValuesRepository: ControlValuesRepository, | ||
private upsertControlValuesUseCase: UpsertControlValuesUseCase, | ||
private analyticsService: AnalyticsService, | ||
private notificationTemplateRepository: NotificationTemplateRepository | ||
private analyticsService: AnalyticsService | ||
) {} | ||
|
||
@InstrumentUsecase() | ||
async execute(command: UpsertWorkflowCommand): Promise<WorkflowResponseDto> { |
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 primary execute function's goal is to contain all the logic in one go without too much indirection. Moreover, the use case is symmetrical, one path for create, another path for update with identical function names.
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.
Love it!
Oh boy, this started as a small UI improvement but then I just couldn't stop. So here is the list of the fixes: - Expose the CRUD endpoints for API Key access - Remove the PATCH step endpoint as it can be used autonomously since workflow steps depend on each other. For example, you can't patch a digest step as it will affect all the following steps that use its outputs. - Simplify drastically the upsert workflow usecase to make it more robust and readable. - Simplify drastically the workflow E2E tests - Ensure that step content issues are not computed immediately after a step is created when the request comes from the Dashboard. This results in better UX upon step creation, as the step form opens without step issues initially.
Worker often hangs in CI during E2E execution. This is an attempt to restart it when that happens.
Nest.js keeps us hostages with Next.js until we upgrade to a Node version that allows require (ESM). So we had to use a much-ordered version of the get-port, which is CJS and not ESM.
69e2a7c
to
ba6d9b2
Compare
}) | ||
); | ||
} | ||
// TODO: use transaction to ensure that the workflows, steps and controls are upserted atomically |
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.
dont we have the Idempotency header key for this?
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.
Let's take this offline.
@@ -2,26 +2,24 @@ import { BadRequestException, Injectable } from '@nestjs/common'; | |||
|
|||
import { | |||
AnalyticsService, | |||
CreateWorkflow as CreateWorkflowGeneric, | |||
CreateWorkflow as CreateWorkflowV0Usecase, |
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.
👏
existingWorkflow: NotificationTemplateEntity | ||
): Promise<UpdateWorkflowCommand> { | ||
const steps = await this.mapSteps(workflowDto.origin, user, workflowDto.steps, existingWorkflow); | ||
const { workflowDto, user } = command; | ||
const steps = await this.buildSteps(command, existingWorkflow); |
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.
sorry i dont get it.
wont we create issues in here for the newly created workflow? (in this.buildSteps => this.buildStepIssuesUsecase.execute)
never mind i just saw the Dashboard implementation
}) | ||
}), | ||
{ | ||
state: { hideValidationErrorsOnFirstRender: true }, |
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.
ohhhhh i see, so we create issues on create but not render them.
i thought the ticket was about not creating the issues on create.
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.
Correct! Issues must be there in the modelling but this is mostly a UX fix.
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.
Looks awesome on my end, left a couple of comments with my concerns.
@@ -52,7 +52,25 @@ jobs: | |||
shell: bash | |||
env: | |||
NOVU_ENTERPRISE: ${{ inputs.ee }} | |||
run: cd apps/worker && pnpm start:test 2>&1 & | |||
run: | |
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 would maybe put it to a bash script than having it in yaml directly, so the workflow is less overwhelming.
pnpm start:worker:test 2>&1 & ./monitor-worker.sh
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.
Not related to this PR but as a sidenote, its really tough to work with this test suite cause it has like 800 lines and many functions do the same but due to scale its probably hard to notice that something already exists.
I suggest we should do unit test in each usecase folder and then for actual E2E we should just briefly test if the endpoint is reachable and returns something, just a happy path scenario. Because we always use 1 entry usecase per 1 controller anyway.
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 agree 100%. It needs to be split in a file per controller endpoint.
What changed? Why was the change needed?
This PR started as an attempt to improve the UX when a step was created. The goal was to create a step, open the step template form, and not show issues immediately until the first edit.
Then, one thing led to another, and a major cleanup was conducted.
Please refer to the commit messages.