Skip to content
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

Merged
merged 14 commits into from
Apr 7, 2025
Merged

Conversation

SokratisVidros
Copy link
Contributor

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.

Screenshot 2025-04-05 at 02 46 39

Copy link

netlify bot commented Apr 4, 2025

Deploy Preview for dashboard-v2-novu-staging canceled.

Name Link
🔨 Latest commit c3661e7
🔍 Latest deploy log https://app.netlify.com/sites/dashboard-v2-novu-staging/deploys/67f39b396933be0008125e1d

@@ -52,7 +52,25 @@ jobs:
shell: bash
env:
NOVU_ENTERPRISE: ${{ inputs.ee }}
run: cd apps/worker && pnpm start:test 2>&1 &
run: |
Copy link
Contributor Author

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

Copy link
Contributor

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.

Copy link
Contributor

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()) {
Copy link
Contributor Author

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
Copy link
Contributor Author

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;
Copy link
Contributor Author

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.

@SokratisVidros SokratisVidros changed the title Refactor workflows v2 chore(api): Refactor workflows v2 Apr 4, 2025

async function patchStepWithControlValues(workflowSlug: string, stepSlug: string, controlValues: InAppControlType) {
Copy link
Contributor Author

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')
Copy link
Contributor Author

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()
Copy link
Contributor Author

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.

Copy link
Contributor

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} />;
Copy link
Contributor Author

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
Copy link
Contributor Author

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';
Copy link
Contributor Author

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
Copy link
Contributor Author

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.

Copy link
Contributor

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 {
Copy link
Contributor Author

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,
Copy link
Contributor Author

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> {
Copy link
Contributor Author

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.

Copy link
Contributor

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.
})
);
}
// TODO: use transaction to ensure that the workflows, steps and controls are upserted atomically
Copy link
Contributor

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?

Copy link
Contributor Author

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,
Copy link
Contributor

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);
Copy link
Contributor

@djabarovgeorge djabarovgeorge Apr 7, 2025

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 },
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

@djabarovgeorge djabarovgeorge left a 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: |
Copy link
Contributor

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

Copy link
Contributor

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.

Copy link
Contributor Author

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.

@SokratisVidros SokratisVidros merged commit 526d8f1 into next Apr 7, 2025
32 of 33 checks passed
@SokratisVidros SokratisVidros deleted the refactor_workflows_v2 branch April 7, 2025 09:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants