Skip to content

Commit

Permalink
feat(api): add preferences entity and schema (#6396)
Browse files Browse the repository at this point in the history
  • Loading branch information
davidsoderberg authored Sep 5, 2024
1 parent 070fde1 commit 0269ddc
Show file tree
Hide file tree
Showing 41 changed files with 1,874 additions and 54 deletions.
2 changes: 2 additions & 0 deletions apps/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { ProductFeatureInterceptor } from './app/shared/interceptors/product-fea
import { AnalyticsModule } from './app/analytics/analytics.module';
import { InboxModule } from './app/inbox/inbox.module';
import { BridgeModule } from './app/bridge/bridge.module';
import { PreferencesModule } from './app/preferences';

const enterpriseImports = (): Array<Type | DynamicModule | Promise<DynamicModule> | ForwardReference> => {
const modules: Array<Type | DynamicModule | Promise<DynamicModule> | ForwardReference> = [];
Expand Down Expand Up @@ -101,6 +102,7 @@ const baseModules: Array<Type | DynamicModule | Promise<DynamicModule> | Forward
ProfilingModule.register(packageJson.name),
TracingModule.register(packageJson.name, packageJson.version),
BridgeModule,
PreferencesModule,
];

const enterpriseModules = enterpriseImports();
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/app/bridge/bridge.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ import {

import { UserSessionData, ControlVariablesLevelEnum, WorkflowTypeEnum } from '@novu/shared';
import { AnalyticsService, ExternalApiAccessible, UserAuthGuard, UserSession } from '@novu/application-generic';

import { EnvironmentRepository, NotificationTemplateRepository, ControlVariablesRepository } from '@novu/dal';

import { ApiExcludeController } from '@nestjs/swagger';

import { StoreControlVariables, StoreControlVariablesCommand } from './usecases/store-control-variables';
import { PreviewStep, PreviewStepCommand } from './usecases/preview-step';
import { SyncCommand } from './usecases/sync';
Expand Down
8 changes: 5 additions & 3 deletions apps/api/src/app/bridge/bridge.module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';

import {
CreateChange,
CreateMessageTemplate,
Expand All @@ -8,11 +7,12 @@ import {
UpdateChange,
UpdateMessageTemplate,
UpdateWorkflow,
UpsertPreferences,
} from '@novu/application-generic';

import { PreferencesRepository } from '@novu/dal';
import { SharedModule } from '../shared/shared.module';
import { BridgeController } from './bridge.controller';
import { USECASES } from './usecases';
import { SharedModule } from '../shared/shared.module';

const PROVIDERS = [
CreateWorkflow,
Expand All @@ -22,6 +22,8 @@ const PROVIDERS = [
DeleteMessageTemplate,
CreateChange,
UpdateChange,
PreferencesRepository,
UpsertPreferences,
];

@Module({
Expand Down
23 changes: 20 additions & 3 deletions apps/api/src/app/bridge/usecases/sync/sync.usecase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
EnvironmentRepository,
NotificationGroupRepository,
NotificationTemplateEntity,
PreferencesActorEnum,
} from '@novu/dal';
import {
AnalyticsService,
Expand All @@ -14,6 +15,8 @@ import {
UpdateWorkflow,
UpdateWorkflowCommand,
ExecuteBridgeRequest,
UpsertPreferences,
UpsertWorkflowPreferencesCommand,
} from '@novu/application-generic';
import { WorkflowTypeEnum } from '@novu/shared';
import { DiscoverOutput, DiscoverStepOutput, DiscoverWorkflowOutput, GetActionEnum } from '@novu/framework';
Expand All @@ -32,7 +35,8 @@ export class Sync {
private notificationGroupRepository: NotificationGroupRepository,
private environmentRepository: EnvironmentRepository,
private executeBridgeRequest: ExecuteBridgeRequest,
private analyticsService: AnalyticsService
private analyticsService: AnalyticsService,
private upsertPreferences: UpsertPreferences
) {}
async execute(command: SyncCommand): Promise<CreateBridgeResponseDto> {
const environment = await this.environmentRepository.findOne({ _id: command.environmentId });
Expand Down Expand Up @@ -126,8 +130,10 @@ export class Sync {
workflow.workflowId
);

let savedWorkflow: NotificationTemplateEntity | undefined;

if (workflowExist) {
return await this.updateWorkflowUsecase.execute(
savedWorkflow = await this.updateWorkflowUsecase.execute(
UpdateWorkflowCommand.create({
id: workflowExist._id,
environmentId: command.environmentId,
Expand Down Expand Up @@ -165,7 +171,7 @@ export class Sync {
}
const isWorkflowActive = this.castToAnyNotSupportedParam(workflow.options)?.active ?? true;

return this.createWorkflowUsecase.execute(
savedWorkflow = await this.createWorkflowUsecase.execute(
CreateWorkflowCommand.create({
notificationGroupId,
draft: !isWorkflowActive,
Expand Down Expand Up @@ -197,6 +203,17 @@ export class Sync {
})
);
}

await this.upsertPreferences.upsertWorkflowPreferences(
UpsertWorkflowPreferencesCommand.create({
environmentId: savedWorkflow._environmentId,
organizationId: savedWorkflow._organizationId,
templateId: savedWorkflow._id,
preferences: workflow.preferences,
})
);

return savedWorkflow;
})
);
}
Expand Down
12 changes: 6 additions & 6 deletions apps/api/src/app/inbox/inbox.module.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { Module } from '@nestjs/common';

import { USE_CASES } from './usecases';
import { InboxController } from './inbox.controller';
import { SharedModule } from '../shared/shared.module';
import { AuthModule } from '../auth/auth.module';
import { SubscribersModule } from '../subscribers/subscribers.module';
import { IntegrationModule } from '../integrations/integrations.module';
import { SharedModule } from '../shared/shared.module';
import { SubscribersModule } from '../subscribers/subscribers.module';
import { InboxController } from './inbox.controller';
import { USE_CASES } from './usecases';
import { PreferencesModule } from '../preferences';

@Module({
imports: [SharedModule, SubscribersModule, AuthModule, IntegrationModule],
imports: [SharedModule, SubscribersModule, AuthModule, IntegrationModule, PreferencesModule],
providers: [...USE_CASES],
exports: [...USE_CASES],
controllers: [InboxController],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import {
GetSubscriberGlobalPreferenceCommand,
GetSubscriberTemplatePreference,
GetSubscriberTemplatePreferenceCommand,
UpsertPreferences,
UpsertSubscriberWorkflowPreferencesCommand,
UpsertSubscriberGlobalPreferencesCommand,
} from '@novu/application-generic';
import {
ChannelTypeEnum,
Expand All @@ -15,11 +18,14 @@ import {
SubscriberPreferenceRepository,
SubscriberRepository,
} from '@novu/dal';
import { IPreferenceChannels } from '@novu/shared';
import { ApiException } from '../../../shared/exceptions/api.exception';
import { AnalyticsEventsEnum } from '../../utils';
import { InboxPreference } from '../../utils/types';
import { UpdatePreferencesCommand } from './update-preferences.command';

const PREFERENCE_DEFAULT_VALUE = true;

@Injectable()
export class UpdatePreferences {
constructor(
Expand All @@ -28,7 +34,8 @@ export class UpdatePreferences {
private subscriberRepository: SubscriberRepository,
private analyticsService: AnalyticsService,
private getSubscriberGlobalPreference: GetSubscriberGlobalPreference,
private getSubscriberTemplatePreferenceUsecase: GetSubscriberTemplatePreference
private getSubscriberTemplatePreferenceUsecase: GetSubscriberTemplatePreference,
private upsertPreferences: UpsertPreferences
) {}

async execute(command: UpdatePreferencesCommand): Promise<InboxPreference> {
Expand Down Expand Up @@ -133,6 +140,15 @@ export class UpdatePreferences {
})
);

await this.storePreferences({
enabled: preference.enabled,
channels: preference.channels,
organizationId: command.organizationId,
environmentId: command.environmentId,
subscriberId: command.subscriberId,
templateId: workflow._id,
});

return {
level: PreferenceLevelEnum.TEMPLATE,
enabled: preference.enabled,
Expand All @@ -155,6 +171,14 @@ export class UpdatePreferences {
})
);

await this.storePreferences({
enabled: preference.enabled,
channels: preference.channels,
organizationId: command.organizationId,
environmentId: command.environmentId,
subscriberId: command.subscriberId,
});

return {
level: PreferenceLevelEnum.GLOBAL,
enabled: preference.enabled,
Expand All @@ -171,4 +195,63 @@ export class UpdatePreferences {
...(command.level === PreferenceLevelEnum.TEMPLATE && command.workflowId && { _templateId: command.workflowId }),
};
}

private async storePreferences(item: {
enabled: boolean;
channels: IPreferenceChannels;
organizationId: string;
subscriberId: string;
environmentId: string;
templateId?: string;
}) {
const preferences = {
workflow: {
defaultValue: item.enabled || PREFERENCE_DEFAULT_VALUE,
readOnly: false,
},
channels: {
in_app: {
defaultValue: item.channels.in_app || PREFERENCE_DEFAULT_VALUE,
readOnly: false,
},
sms: {
defaultValue: item.channels.sms || PREFERENCE_DEFAULT_VALUE,
readOnly: false,
},
email: {
defaultValue: item.channels.email || PREFERENCE_DEFAULT_VALUE,
readOnly: false,
},
push: {
defaultValue: item.channels.push || PREFERENCE_DEFAULT_VALUE,
readOnly: false,
},
chat: {
defaultValue: item.channels.chat || PREFERENCE_DEFAULT_VALUE,
readOnly: false,
},
},
};

if (item.templateId) {
return await this.upsertPreferences.upsertSubscriberWorkflowPreferences(
UpsertSubscriberWorkflowPreferencesCommand.create({
environmentId: item.environmentId,
organizationId: item.organizationId,
subscriberId: item.subscriberId,
templateId: item.templateId,
preferences,
})
);
}

return await this.upsertPreferences.upsertSubscriberGlobalPreferences(
UpsertSubscriberGlobalPreferencesCommand.create({
preferences,
environmentId: item.environmentId,
organizationId: item.organizationId,
subscriberId: item.subscriberId,
})
);
}
}
43 changes: 43 additions & 0 deletions apps/api/src/app/preferences/dtos/preferences.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ChannelTypeEnum } from '@novu/shared';
import { Type } from 'class-transformer';
import { IsBoolean, ValidateNested } from 'class-validator';

export class Preference {
@IsBoolean()
defaultValue: boolean;

@IsBoolean()
readOnly: boolean;
}

export class Channels {
@ValidateNested({ each: true })
@Type(() => Preference)
[ChannelTypeEnum.IN_APP]: Preference;

@ValidateNested({ each: true })
@Type(() => Preference)
[ChannelTypeEnum.EMAIL]: Preference;

@ValidateNested({ each: true })
@Type(() => Preference)
[ChannelTypeEnum.SMS]: Preference;

@ValidateNested({ each: true })
@Type(() => Preference)
[ChannelTypeEnum.CHAT]: Preference;

@ValidateNested({ each: true })
@Type(() => Preference)
[ChannelTypeEnum.PUSH]: Preference;
}

export class PreferencesDto {
@ValidateNested({ each: true })
@Type(() => Preference)
workflow: Preference;

@ValidateNested({ each: true })
@Type(() => Channels)
channels: Channels;
}
12 changes: 12 additions & 0 deletions apps/api/src/app/preferences/dtos/upsert-preferences.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Type } from 'class-transformer';
import { IsString, ValidateNested } from 'class-validator';
import { PreferencesDto } from './preferences.dto';

export class UpsertPreferencesDto {
@IsString()
workflowId: string;

@ValidateNested({ each: true })
@Type(() => PreferencesDto)
preferences: PreferencesDto;
}
1 change: 1 addition & 0 deletions apps/api/src/app/preferences/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { PreferencesModule } from './preferences.module';
Loading

0 comments on commit 0269ddc

Please sign in to comment.