Skip to content

Commit

Permalink
Merge branch 'next' into copy-micro-interaction
Browse files Browse the repository at this point in the history
  • Loading branch information
scopsy committed Jan 8, 2025
2 parents 30d29c2 + fd8e082 commit fa9cc8c
Show file tree
Hide file tree
Showing 19 changed files with 272 additions and 151 deletions.
6 changes: 3 additions & 3 deletions apps/api/src/app/environments-v1/novu-bridge.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
HydrateEmailSchemaUseCase,
InAppOutputRendererUsecase,
PushOutputRendererUsecase,
RenderEmailOutputUsecase,
EmailOutputRendererUsecase,
SmsOutputRendererUsecase,
} from './usecases/output-renderers';
import { DelayOutputRendererUsecase } from './usecases/output-renderers/delay-output-renderer.usecase';
Expand All @@ -31,11 +31,11 @@ import { DigestOutputRendererUsecase } from './usecases/output-renderers/digest-
ConstructFrameworkWorkflow,
GetDecryptedSecretKey,
InAppOutputRendererUsecase,
RenderEmailOutputUsecase,
EmailOutputRendererUsecase,
SmsOutputRendererUsecase,
ChatOutputRendererUsecase,
PushOutputRendererUsecase,
RenderEmailOutputUsecase,
EmailOutputRendererUsecase,
ExpandEmailEditorSchemaUsecase,
HydrateEmailSchemaUseCase,
DelayOutputRendererUsecase,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
FullPayloadForRender,
InAppOutputRendererUsecase,
PushOutputRendererUsecase,
RenderEmailOutputUsecase,
EmailOutputRendererUsecase,
SmsOutputRendererUsecase,
} from '../output-renderers';
import { DelayOutputRendererUsecase } from '../output-renderers/delay-output-renderer.usecase';
Expand All @@ -28,7 +28,7 @@ export class ConstructFrameworkWorkflow {
private logger: PinoLogger,
private workflowsRepository: NotificationTemplateRepository,
private inAppOutputRendererUseCase: InAppOutputRendererUsecase,
private emailOutputRendererUseCase: RenderEmailOutputUsecase,
private emailOutputRendererUseCase: EmailOutputRendererUsecase,
private smsOutputRendererUseCase: SmsOutputRendererUsecase,
private chatOutputRendererUseCase: ChatOutputRendererUsecase,
private pushOutputRendererUseCase: PushOutputRendererUsecase,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ import { Instrument, InstrumentUsecase } from '@novu/application-generic';
import { FullPayloadForRender, RenderCommand } from './render-command';
import { ExpandEmailEditorSchemaUsecase } from './expand-email-editor-schema.usecase';

export class RenderEmailOutputCommand extends RenderCommand {}
export class EmailOutputRendererCommand extends RenderCommand {}

@Injectable()
// todo rename to EmailOutputRenderer
export class RenderEmailOutputUsecase {
export class EmailOutputRendererUsecase {
constructor(private expandEmailEditorSchemaUseCase: ExpandEmailEditorSchemaUsecase) {}

@InstrumentUsecase()
async execute(renderCommand: RenderEmailOutputCommand): Promise<EmailRenderOutput> {
async execute(renderCommand: EmailOutputRendererCommand): Promise<EmailRenderOutput> {
const { body, subject } = renderCommand.controlValues;

if (!body || typeof body !== 'string') {
Expand Down Expand Up @@ -43,7 +42,7 @@ export class RenderEmailOutputUsecase {

private async parseTipTapNodeByLiquid(
tiptapNode: TipTapNode,
renderCommand: RenderEmailOutputCommand
renderCommand: EmailOutputRendererCommand
): Promise<TipTapNode> {
const client = new Liquid({
outputEscape: (output) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
/* eslint-disable no-param-reassign */
import { Injectable } from '@nestjs/common';
import { PreviewPayload, TipTapNode } from '@novu/shared';
import { z } from 'zod';
import { processNodeAttrs } from '@novu/application-generic';

import { PreviewPayload, TipTapNode } from '@novu/shared';
import { processNodeAttrs, processNodeMarks } from '@novu/application-generic';

import { HydrateEmailSchemaCommand } from './hydrate-email-schema.command';
import { PlaceholderAggregation } from '../../../workflows-v2/usecases';

Expand All @@ -18,13 +20,13 @@ export class HydrateEmailSchemaUseCase {
};

// TODO: Aligned Zod inferred type and TipTapNode to remove the need of a type assertion
const emailEditorSchema: TipTapNode = TipTapSchema.parse(JSON.parse(command.emailEditor)) as TipTapNode;
if (emailEditorSchema.content) {
this.transformContentInPlace(emailEditorSchema.content, command.fullPayloadForRender, placeholderAggregation);
const emailBody: TipTapNode = TipTapSchema.parse(JSON.parse(command.emailEditor)) as TipTapNode;
if (emailBody) {
this.transformContentInPlace([emailBody], command.fullPayloadForRender, placeholderAggregation);
}

return {
hydratedEmailSchema: emailEditorSchema,
hydratedEmailSchema: emailBody,
placeholderAggregation,
};
}
Expand Down Expand Up @@ -89,6 +91,7 @@ export class HydrateEmailSchemaUseCase {
) {
content.forEach((node, index) => {
processNodeAttrs(node);
processNodeMarks(node);

if (this.isVariableNode(node)) {
this.variableLogic(masterPayload, node, content, index, placeholderAggregation);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export * from './render-command';
export * from './push-output-renderer.usecase';
export * from './sms-output-renderer.usecase';
export * from './in-app-output-renderer.usecase';
export * from './render-email-output.usecase';
export * from './email-output-renderer.usecase';
export * from './hydrate-email-schema.usecase';
export * from './hydrate-email-schema.command';
export * from './expand-email-editor-schema.usecase';
70 changes: 68 additions & 2 deletions apps/api/src/app/subscribers/e2e/update-global-preference.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { ChannelTypeEnum } from '@novu/shared';
import { ChannelTypeEnum, StepTypeEnum } from '@novu/shared';
import { UserSession } from '@novu/testing';
import { expect } from 'chai';

import { updateGlobalPreferences } from './helpers';
import { SubscriberRepository } from '@novu/dal';
import { getPreference, updateGlobalPreferences } from './helpers';

describe('Update Subscribers global preferences - /subscribers/:subscriberId/preferences (PATCH)', function () {
let session: UserSession;
let subscriberRepository: SubscriberRepository;

beforeEach(async () => {
session = new UserSession();
subscriberRepository = new SubscriberRepository();
await session.initialize();
});

Expand Down Expand Up @@ -89,6 +92,69 @@ describe('Update Subscribers global preferences - /subscribers/:subscriberId/pre
});
});

it('should update user global preferences only for the current environment', async function () {
// create a template in dev environment
await session.createTemplate({
steps: [
{
type: StepTypeEnum.IN_APP,
content: 'Hello',
},
],
noFeedId: true,
});

await session.switchToProdEnvironment();
// create a subscriber in prod environment
await subscriberRepository.create({
_environmentId: session.environment._id,
_organizationId: session.organization._id,
subscriberId: session.subscriberId,
});
// create a template in prod environment
await session.createTemplate({
steps: [
{
type: StepTypeEnum.IN_APP,
content: 'Hello',
},
],
noFeedId: true,
});

await session.switchToDevEnvironment();
// update the subscriber global preferences in dev environment
const response = await updateGlobalPreferences(
{
enabled: true,
preferences: [{ type: ChannelTypeEnum.IN_APP, enabled: false }],
},
session
);

expect(response.data.data.preference.enabled).to.eql(true);
expect(response.data.data.preference.channels).to.eql({
[ChannelTypeEnum.EMAIL]: true,
[ChannelTypeEnum.PUSH]: true,
[ChannelTypeEnum.CHAT]: true,
[ChannelTypeEnum.SMS]: true,
[ChannelTypeEnum.IN_APP]: false,
});

// get the subscriber preferences in dev environment
const getDevPreferencesResponse = await getPreference(session);
const devPreferences = getDevPreferencesResponse.data.data;
expect(devPreferences.every((item) => !!item.preference.channels.in_app)).to.be.false;

await session.switchToProdEnvironment();

// get the subscriber preferences in prod environment
session.apiKey = session.environment.apiKeys[0].key;
const getProdPreferencesResponse = await getPreference(session);
const prodPreferences = getProdPreferencesResponse.data.data;
expect(prodPreferences.every((item) => !!item.preference.channels.in_app)).to.be.true;
});

// `enabled` flag is not used anymore. The presence of a preference object means that the subscriber has enabled notifications.
it.skip('should update user global preference and disable the flag for the future channels update', async function () {
const disablePreferenceData = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Injectable, InternalServerErrorException } from '@nestjs/common';
import _ from 'lodash';
import Ajv, { ErrorObject } from 'ajv';
import addFormats from 'ajv-formats';
import { captureException } from '@sentry/node';

import {
ChannelTypeEnum,
createMockObjectFromSchema,
Expand All @@ -22,8 +24,8 @@ import {
PinoLogger,
dashboardSanitizeControlValues,
} from '@novu/application-generic';
import { captureException } from '@sentry/node';
import { channelStepSchemas, actionStepSchemas } from '@novu/framework/internal';

import { PreviewStep, PreviewStepCommand } from '../../../bridge/usecases/preview-step';
import { FrameworkPreviousStepsOutputState } from '../../../bridge/usecases/preview-step/preview-step.command';
import { BuildStepDataUsecase } from '../build-step-data';
Expand Down Expand Up @@ -70,7 +72,7 @@ export class GeneratePreviewUsecase {
if (!sanitizedValidatedControls && workflow.origin === WorkflowOriginEnum.NOVU_CLOUD) {
throw new Error(
// eslint-disable-next-line max-len
'Control values normalization failed: The normalizeControlValues function requires maintenance to sanitize the provided type or data structure correctly'
'Control values normalization failed, normalizeControlValues function requires maintenance to sanitize the provided type or data structure correctly'
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { JSONContent } from '@maily-to/render';
import _ from 'lodash';
import { processNodeAttrs, MailyContentTypeEnum } from '@novu/application-generic';
import { processNodeAttrs, MailyContentTypeEnum, processNodeMarks } from '@novu/application-generic';

/**
* Processes raw Maily JSON editor state by converting variables to Liquid.js output syntax
Expand Down Expand Up @@ -101,7 +101,8 @@ function processForLoopNode(node: JSONContent): JSONContent {
function processNode(node: JSONContent): JSONContent {
if (!node) return node;

const processedNode = processNodeAttrs(node);
let processedNode = processNodeAttrs(node);
processedNode = processNodeMarks(processedNode);

switch (processedNode.type) {
case MailyContentTypeEnum.VARIABLE:
Expand Down
62 changes: 16 additions & 46 deletions apps/api/src/app/workflows-v2/util/build-variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ import { Variable, extractLiquidTemplateVariables, TemplateVariables } from './t
import { transformMailyContentToLiquid } from '../usecases/generate-preview/transform-maily-content-to-liquid';
import { isStringTipTapNode } from './tip-tap.util';

const DOT_PROPERTIES = '.properties';
const DOT_ADDITIONAL_PROPERTIES = '.additionalProperties';

export function buildVariables(
variableSchema: Record<string, unknown> | undefined,
controlValue: unknown | Record<string, unknown>,
Expand Down Expand Up @@ -44,57 +41,30 @@ export function buildVariables(
};
}

type PathValidationResult = {
isValid: boolean;
additionalPropertiesFound?: boolean;
};

function validateVariablePath(variableSchema: Record<string, unknown>, variableName: string): PathValidationResult {
const parts = variableName.split('.');
let currentPath = 'properties';

for (const part of parts) {
currentPath += `.${part}`;
const valueExists = _.get(variableSchema, currentPath) !== undefined;
const propertiesPath = `${currentPath}${DOT_PROPERTIES}`;
const propertiesExist = _.get(variableSchema, propertiesPath) !== undefined;

if (!valueExists && !propertiesExist) {
const additionalPropertiesResult = checkAdditionalProperties(variableSchema, propertiesPath);
if (additionalPropertiesResult.isValid) {
return { isValid: true, additionalPropertiesFound: true };
}

return { isValid: false };
}

currentPath = propertiesPath;
function isPropertyAllowed(schema: Record<string, unknown>, propertyPath: string) {
let currentSchema = { ...schema };
if (!currentSchema || typeof currentSchema !== 'object') {
return false;
}

return { isValid: true };
}

function checkAdditionalProperties(
variableSchema: Record<string, unknown>,
propertiesPath: string
): PathValidationResult {
let currentPath = propertiesPath;
const pathParts = propertyPath.split('.');

while (currentPath.length > 0) {
const additionalPropertiesPath = `${currentPath.slice(0, -DOT_PROPERTIES.length)}${DOT_ADDITIONAL_PROPERTIES}`;
const additionalPropertiesValue = _.get(variableSchema, additionalPropertiesPath);
for (const part of pathParts) {
const { properties, additionalProperties } = currentSchema;

if (additionalPropertiesValue !== undefined) {
return { isValid: additionalPropertiesValue === true };
if (properties?.[part]) {
currentSchema = properties[part];
continue;
}

const pathParts = currentPath.split('.');
if (pathParts.length <= 2) break;
if (additionalProperties === true) {
return true;
}

currentPath = pathParts.slice(0, -2).join('.');
return false;
}

return { isValid: false };
return true;
}

function createInvalidVariable(variable: Variable): Variable {
Expand All @@ -114,7 +84,7 @@ function identifyUnknownVariables(

return variables.reduce<TemplateVariables>(
(acc, variable) => {
const { isValid } = validateVariablePath(variableSchema, variable.name);
const isValid = isPropertyAllowed(variableSchema, variable.name);

if (isValid) {
acc.validVariables.push(variable);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,6 @@ export const EditorBreadcrumbs = () => {
{
label: 'Workflows',
href: workflowsRoute,
node: (
<Badge kind="pill" size="2xs" className="no-underline">
BETA
</Badge>
),
},
];

Expand All @@ -57,11 +52,10 @@ export const EditorBreadcrumbs = () => {
></CompactButton>
<Breadcrumb>
<BreadcrumbList>
{breadcrumbs.map(({ label, href, node }) => (
{breadcrumbs.map(({ label, href }) => (
<React.Fragment key={`${href}_${label}`}>
<BreadcrumbItem className="flex items-center gap-1">
<BreadcrumbLink to={href}>{label}</BreadcrumbLink>
{node}
</BreadcrumbItem>
<BreadcrumbSeparator />
</React.Fragment>
Expand Down
Loading

0 comments on commit fa9cc8c

Please sign in to comment.