-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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
feat(api-service): polish action steps #7389
Changes from 2 commits
2c0dfe2
106ff08
802f726
597c717
0b508ec
0ba3b78
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,23 @@ | ||
import { flatten } from 'flat'; | ||
import type { | ||
ContentIssue, | ||
StepCreateDto, | ||
StepIssuesDto, | ||
StepTypeEnum, | ||
StepUpdateDto, | ||
UpdateWorkflowDto, | ||
WorkflowResponseDto, | ||
} from '@novu/shared'; | ||
import { Step } from '@/utils/types'; | ||
import { STEP_TYPE_LABELS } from '@/utils/constants'; | ||
import { StepTypeEnum } from '@novu/shared'; | ||
import { | ||
DEFAULT_CONTROL_DELAY_AMOUNT, | ||
DEFAULT_CONTROL_DELAY_TYPE, | ||
DEFAULT_CONTROL_DELAY_UNIT, | ||
DEFAULT_CONTROL_DIGEST_AMOUNT, | ||
DEFAULT_CONTROL_DIGEST_CRON, | ||
DEFAULT_CONTROL_DIGEST_DIGEST_KEY, | ||
DEFAULT_CONTROL_DIGEST_UNIT, | ||
STEP_TYPE_LABELS, | ||
} from '@/utils/constants'; | ||
|
||
export const getFirstBodyErrorMessage = (issues?: StepIssuesDto) => { | ||
const stepIssuesArray = Object.entries({ ...issues?.body }); | ||
|
@@ -58,10 +67,25 @@ export const updateStepInWorkflow = ( | |
}; | ||
}; | ||
|
||
export const createStep = (type: StepTypeEnum): Step => ({ | ||
name: STEP_TYPE_LABELS[type] + ' Step', | ||
stepId: '', | ||
slug: '_st_', | ||
type, | ||
_id: crypto.randomUUID(), | ||
}); | ||
export const createStep = (type: StepTypeEnum): StepCreateDto => { | ||
const controlValue: Record<string, unknown> = {}; | ||
|
||
if (type === StepTypeEnum.DIGEST) { | ||
controlValue.amount = DEFAULT_CONTROL_DIGEST_AMOUNT; | ||
controlValue.unit = DEFAULT_CONTROL_DIGEST_UNIT; | ||
controlValue.digestKey = DEFAULT_CONTROL_DIGEST_DIGEST_KEY; | ||
controlValue.cron = DEFAULT_CONTROL_DIGEST_CRON; | ||
} | ||
|
||
if (type === StepTypeEnum.DELAY) { | ||
controlValue.amount = DEFAULT_CONTROL_DELAY_AMOUNT; | ||
controlValue.unit = DEFAULT_CONTROL_DELAY_UNIT; | ||
controlValue.type = DEFAULT_CONTROL_DELAY_TYPE; | ||
} | ||
|
||
return { | ||
name: STEP_TYPE_LABELS[type] + ' Step', | ||
type, | ||
controlValues: controlValue, | ||
}; | ||
}; | ||
Comment on lines
+70
to
+91
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. The summary of the findings of the best approach, why the change is necessary, and what is happening in the current implementation. The main reason for this change is that we cannot create a step with control values during step creation. This limitation causes unexpected behavior, like the bug we are experiencing. Currently, we cannot move schemas with default values to the The solution we originally wanted—to move default values from the This solution is reducing the amount of code. In addition once we store the default values during step creation, we wouldn't need to handle parsing them (placeholders/defaults) in the Next Steps
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,9 +5,9 @@ import { OrganizationLevelCommand } from '../../commands'; | |
|
||
export class TierRestrictionsValidateCommand extends OrganizationLevelCommand { | ||
@IsOptional() | ||
@Transform(({ value }) => (value ? String(value) : value)) | ||
@IsString() | ||
amount?: string; | ||
@Transform(({ value }) => (value ? Number(value) : value)) | ||
@IsNumber() | ||
amount?: number; | ||
Comment on lines
+8
to
+10
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. this should be a number as we use it as number in the usecase |
||
|
||
@IsString() | ||
@IsOptional() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,10 +9,8 @@ import { | |
SmsControlType, | ||
InAppRedirectType, | ||
PushControlType, | ||
isDigestTimedControl, | ||
DigestTimedControlType, | ||
DigestControlSchemaType, | ||
isDigestRegularControl, | ||
DigestRegularControlType, | ||
LookBackWindowType, | ||
DelayControlType, | ||
|
@@ -49,7 +47,7 @@ function sanitizeAction(action: InAppActionType) { | |
function sanitizeInApp(controlValues: InAppControlType) { | ||
const normalized: InAppControlType = { | ||
subject: controlValues.subject || undefined, | ||
// Cast to string to trigger Ajv validation errors | ||
// Cast to string to trigger Ajv validation errors - possible undefined | ||
body: isEmpty(controlValues.body) | ||
? (undefined as unknown as string) | ||
: controlValues.body, | ||
|
@@ -126,7 +124,7 @@ function sanitizeChat(controlValues: ChatControlType) { | |
} | ||
|
||
function sanitizeDigest(controlValues: DigestControlSchemaType) { | ||
if (isDigestTimedControl(controlValues)) { | ||
if (isTimedDigestControl(controlValues)) { | ||
const mappedValues: DigestTimedControlType = { | ||
cron: controlValues.cron || '', | ||
digestKey: controlValues.digestKey || '', | ||
|
@@ -136,19 +134,20 @@ function sanitizeDigest(controlValues: DigestControlSchemaType) { | |
return filterNullishValues(mappedValues); | ||
} | ||
|
||
if (isDigestRegularControl(controlValues)) { | ||
if (isRegularDigestControl(controlValues)) { | ||
const lookBackAmount = (controlValues.lookBackWindow as LookBackWindowType) | ||
?.amount; | ||
const mappedValues: DigestRegularControlType = { | ||
amount: controlValues.amount || 0, | ||
unit: controlValues.unit || TimeUnitEnum.SECONDS, | ||
digestKey: controlValues.digestKey || '', | ||
// Cast to trigger Ajv validation errors - possible undefined | ||
...(parseAmount(controlValues.amount) as { amount?: number }), | ||
unit: controlValues.unit, | ||
digestKey: controlValues.digestKey, | ||
skip: controlValues.skip || undefined, | ||
lookBackWindow: controlValues.lookBackWindow | ||
? { | ||
amount: | ||
(controlValues.lookBackWindow as LookBackWindowType).amount || 0, | ||
unit: | ||
(controlValues.lookBackWindow as LookBackWindowType).unit || | ||
TimeUnitEnum.SECONDS, | ||
// Cast to trigger Ajv validation errors - possible undefined | ||
...(parseAmount(lookBackAmount) as { amount?: number }), | ||
unit: (controlValues.lookBackWindow as LookBackWindowType).unit, | ||
} | ||
: undefined, | ||
}; | ||
|
@@ -177,15 +176,31 @@ function sanitizeDigest(controlValues: DigestControlSchemaType) { | |
|
||
function sanitizeDelay(controlValues: DelayControlType) { | ||
const mappedValues: DelayControlType = { | ||
type: controlValues.type || 'regular', | ||
amount: controlValues.amount || 0, | ||
unit: controlValues.unit || TimeUnitEnum.SECONDS, | ||
// Cast to trigger Ajv validation errors - possible undefined | ||
...(parseAmount(controlValues.amount) as { amount?: number }), | ||
type: controlValues.type, | ||
unit: controlValues.unit, | ||
skip: controlValues.skip || undefined, | ||
}; | ||
|
||
return filterNullishValues(mappedValues); | ||
} | ||
|
||
function parseAmount(amount?: unknown) { | ||
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. safe parse just in case |
||
try { | ||
if (!isNumber(amount)) { | ||
return {}; | ||
} | ||
|
||
const numberAmount = | ||
typeof amount === 'string' ? parseInt(amount, 10) : amount; | ||
|
||
return { amount: numberAmount }; | ||
} catch (error) { | ||
return amount; | ||
} | ||
} | ||
|
||
function filterNullishValues<T extends Record<string, unknown>>(obj: T): T { | ||
if (typeof obj === 'object' && obj !== null) { | ||
return Object.fromEntries( | ||
|
@@ -222,37 +237,63 @@ export function dashboardSanitizeControlValues( | |
controlValues: Record<string, unknown>, | ||
stepType: StepTypeEnum | unknown, | ||
): (Record<string, unknown> & { skip?: Record<string, unknown> }) | null { | ||
if (!controlValues) { | ||
return null; | ||
} | ||
let normalizedValues: Record<string, unknown>; | ||
switch (stepType) { | ||
case StepTypeEnum.IN_APP: | ||
normalizedValues = sanitizeInApp(controlValues as InAppControlType); | ||
break; | ||
case StepTypeEnum.EMAIL: | ||
normalizedValues = sanitizeEmail(controlValues as EmailControlType); | ||
break; | ||
case StepTypeEnum.SMS: | ||
normalizedValues = sanitizeSms(controlValues as SmsControlType); | ||
break; | ||
case StepTypeEnum.PUSH: | ||
normalizedValues = sanitizePush(controlValues as PushControlType); | ||
break; | ||
case StepTypeEnum.CHAT: | ||
normalizedValues = sanitizeChat(controlValues as ChatControlType); | ||
break; | ||
case StepTypeEnum.DIGEST: | ||
normalizedValues = sanitizeDigest( | ||
controlValues as DigestControlSchemaType, | ||
); | ||
break; | ||
case StepTypeEnum.DELAY: | ||
normalizedValues = sanitizeDelay(controlValues as DelayControlType); | ||
break; | ||
default: | ||
normalizedValues = filterNullishValues(controlValues); | ||
try { | ||
if (!controlValues) { | ||
return null; | ||
} | ||
|
||
console.log('controlValues 333222 ', controlValues); | ||
djabarovgeorge marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let normalizedValues: Record<string, unknown>; | ||
switch (stepType) { | ||
case StepTypeEnum.IN_APP: | ||
normalizedValues = sanitizeInApp(controlValues as InAppControlType); | ||
break; | ||
case StepTypeEnum.EMAIL: | ||
normalizedValues = sanitizeEmail(controlValues as EmailControlType); | ||
break; | ||
case StepTypeEnum.SMS: | ||
normalizedValues = sanitizeSms(controlValues as SmsControlType); | ||
break; | ||
case StepTypeEnum.PUSH: | ||
normalizedValues = sanitizePush(controlValues as PushControlType); | ||
break; | ||
case StepTypeEnum.CHAT: | ||
normalizedValues = sanitizeChat(controlValues as ChatControlType); | ||
break; | ||
case StepTypeEnum.DIGEST: | ||
normalizedValues = sanitizeDigest( | ||
controlValues as DigestControlSchemaType, | ||
); | ||
break; | ||
case StepTypeEnum.DELAY: | ||
normalizedValues = sanitizeDelay(controlValues as DelayControlType); | ||
break; | ||
default: | ||
normalizedValues = filterNullishValues(controlValues); | ||
} | ||
|
||
console.log('normalizedValues 333222 ', normalizedValues); | ||
djabarovgeorge marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
return normalizedValues; | ||
} catch (error) { | ||
console.error('Error sanitizing control values', error); | ||
djabarovgeorge marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
return controlValues; | ||
} | ||
} | ||
|
||
function isNumber(value: unknown): value is number { | ||
return !Number.isNaN(Number.parseInt(value as string, 10)); | ||
} | ||
|
||
function isTimedDigestControl( | ||
controlValues: unknown, | ||
): controlValues is DigestTimedControlType { | ||
return !isEmpty((controlValues as DigestTimedControlType)?.cron); | ||
} | ||
|
||
return normalizedValues; | ||
function isRegularDigestControl( | ||
controlValues: unknown, | ||
): controlValues is DigestRegularControlType { | ||
return !isTimedDigestControl(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 used the wrong type here, this is why we passed redundant data to the API such as slug prefix