diff --git a/.cspell.json b/.cspell.json index ad07ccd8fa1..fbeff20890e 100644 --- a/.cspell.json +++ b/.cspell.json @@ -851,11 +851,11 @@ "*.riv", "*/**/.vscode/settings.json", "*/**/CHANGELOG.md", - "apps/api/src/app/workflows-v2/e2e/generate-preview.e2e.ts", + "**/*.e2e.ts", + "**/*.spec.ts", "angular.json", "apps/api/src/.env.test", "apps/api/src/app/analytics/usecases/hubspot-identify-form/hubspot-identify-form.usecase.ts", - "apps/api/src/app/translations/e2e/v2/**/*", "apps/api/src/app/workflows-v2/maily-test-data.ts", "apps/api/src/app/workflows-v2/usecases/validate-content/validate-placeholders/validate-placeholder.usecase.ts", "apps/api/src/app/workflows-v2/util/json-schema-mock.ts", @@ -887,7 +887,7 @@ "apps/dashboard/src/components/variable/constants.ts", "apps/dashboard/src/utils/locales.ts", "apps/dashboard/src/components/primitives/constants.ts", - "enterprise/workers/socket/worker-configuration.d.ts", - "enterprise/workers/scheduler/worker-configuration.d.ts", + "enterprise/workers/socket/worker-configuration.d.ts", + "enterprise/workers/scheduler/worker-configuration.d.ts" ] } diff --git a/apps/api/src/app/inbox/usecases/bulk-update-preferences/bulk-update-preferences.command.ts b/apps/api/src/app/inbox/usecases/bulk-update-preferences/bulk-update-preferences.command.ts index 88b25abf731..4ce4835e93d 100644 --- a/apps/api/src/app/inbox/usecases/bulk-update-preferences/bulk-update-preferences.command.ts +++ b/apps/api/src/app/inbox/usecases/bulk-update-preferences/bulk-update-preferences.command.ts @@ -1,7 +1,9 @@ // apps/api/src/app/inbox/usecases/bulk-update-preferences/bulk-update-preferences.command.ts +import { IsValidContextPayload } from '@novu/application-generic'; +import { ContextPayload } from '@novu/shared'; import { Type } from 'class-transformer'; -import { IsArray, IsDefined } from 'class-validator'; +import { IsArray, IsDefined, IsOptional } from 'class-validator'; import { EnvironmentWithSubscriber } from '../../../shared/commands/project.command'; import { BulkUpdatePreferenceItemDto } from '../../dtos/bulk-update-preferences-request.dto'; @@ -11,4 +13,8 @@ export class BulkUpdatePreferencesCommand extends EnvironmentWithSubscriber { @IsArray() @Type(() => BulkUpdatePreferenceItemDto) readonly preferences: BulkUpdatePreferenceItemDto[]; + + @IsOptional() + @IsValidContextPayload({ maxCount: 5 }) + readonly context?: ContextPayload; } diff --git a/apps/api/src/app/inbox/usecases/bulk-update-preferences/bulk-update-preferences.spec.ts b/apps/api/src/app/inbox/usecases/bulk-update-preferences/bulk-update-preferences.spec.ts index 1e300cce141..564ecb4468b 100644 --- a/apps/api/src/app/inbox/usecases/bulk-update-preferences/bulk-update-preferences.spec.ts +++ b/apps/api/src/app/inbox/usecases/bulk-update-preferences/bulk-update-preferences.spec.ts @@ -1,6 +1,11 @@ import { BadRequestException, NotFoundException, UnprocessableEntityException } from '@nestjs/common'; -import { AnalyticsService } from '@novu/application-generic'; -import { EnvironmentRepository, NotificationTemplateRepository, SubscriberRepository } from '@novu/dal'; +import { AnalyticsService, FeatureFlagsService } from '@novu/application-generic'; +import { + ContextRepository, + EnvironmentRepository, + NotificationTemplateRepository, + SubscriberRepository, +} from '@novu/dal'; import { PreferenceLevelEnum, TriggerTypeEnum } from '@novu/shared'; import { expect } from 'chai'; import sinon from 'sinon'; @@ -80,18 +85,26 @@ describe('BulkUpdatePreferences', () => { let notificationTemplateRepositoryMock: sinon.SinonStubbedInstance; let updatePreferencesUsecaseMock: sinon.SinonStubbedInstance; let environmentRepositoryMock: sinon.SinonStubbedInstance; + let contextRepositoryMock: sinon.SinonStubbedInstance; + let featureFlagsServiceMock: sinon.SinonStubbedInstance; + beforeEach(() => { subscriberRepositoryMock = sinon.createStubInstance(SubscriberRepository); analyticsServiceMock = sinon.createStubInstance(AnalyticsService); notificationTemplateRepositoryMock = sinon.createStubInstance(NotificationTemplateRepository); updatePreferencesUsecaseMock = sinon.createStubInstance(UpdatePreferences); environmentRepositoryMock = sinon.createStubInstance(EnvironmentRepository); + contextRepositoryMock = sinon.createStubInstance(ContextRepository); + featureFlagsServiceMock = sinon.createStubInstance(FeatureFlagsService); + bulkUpdatePreferences = new BulkUpdatePreferences( notificationTemplateRepositoryMock as any, subscriberRepositoryMock as any, analyticsServiceMock as any, updatePreferencesUsecaseMock as any, - environmentRepositoryMock as any + environmentRepositoryMock as any, + contextRepositoryMock as any, + featureFlagsServiceMock as any ); }); diff --git a/apps/api/src/app/inbox/usecases/bulk-update-preferences/bulk-update-preferences.usecase.ts b/apps/api/src/app/inbox/usecases/bulk-update-preferences/bulk-update-preferences.usecase.ts index 5dc6309a477..b0422328839 100644 --- a/apps/api/src/app/inbox/usecases/bulk-update-preferences/bulk-update-preferences.usecase.ts +++ b/apps/api/src/app/inbox/usecases/bulk-update-preferences/bulk-update-preferences.usecase.ts @@ -1,13 +1,14 @@ import { BadRequestException, Injectable, NotFoundException, UnprocessableEntityException } from '@nestjs/common'; -import { AnalyticsService, InstrumentUsecase } from '@novu/application-generic'; +import { AnalyticsService, FeatureFlagsService, InstrumentUsecase } from '@novu/application-generic'; import { BaseRepository, + ContextRepository, EnvironmentRepository, NotificationTemplateEntity, NotificationTemplateRepository, SubscriberRepository, } from '@novu/dal'; -import { PreferenceLevelEnum } from '@novu/shared'; +import { ContextPayload, FeatureFlagsKeysEnum, PreferenceLevelEnum } from '@novu/shared'; import { BulkUpdatePreferenceItemDto } from '../../dtos/bulk-update-preferences-request.dto'; import { AnalyticsEventsEnum } from '../../utils'; import { InboxPreference } from '../../utils/types'; @@ -24,11 +25,15 @@ export class BulkUpdatePreferences { private subscriberRepository: SubscriberRepository, private analyticsService: AnalyticsService, private updatePreferencesUsecase: UpdatePreferences, - private environmentRepository: EnvironmentRepository + private environmentRepository: EnvironmentRepository, + private contextRepository: ContextRepository, + private featureFlagsService: FeatureFlagsService ) {} @InstrumentUsecase() async execute(command: BulkUpdatePreferencesCommand): Promise { + const contextKeys = await this.resolveContexts(command.environmentId, command.organizationId, command.context); + const subscriber = await this.subscriberRepository.findBySubscriberId(command.environmentId, command.subscriberId); if (!subscriber) throw new NotFoundException(`Subscriber with id: ${command.subscriberId} is not found`); @@ -102,7 +107,7 @@ export class BulkUpdatePreferences { organizationId: command.organizationId, subscriberId: command.subscriberId, environmentId: command.environmentId, - contextKeys: command.contextKeys, + contextKeys, level: PreferenceLevelEnum.TEMPLATE, subscriptionIdentifier: preference.subscriptionIdentifier, ...(isUpdatingSubscriptionPreference && { @@ -131,4 +136,33 @@ export class BulkUpdatePreferences { return updatedPreferences; } + + private async resolveContexts( + environmentId: string, + organizationId: string, + context?: ContextPayload + ): Promise { + // Check if context preferences feature is enabled + const isEnabled = await this.featureFlagsService.getFlag({ + key: FeatureFlagsKeysEnum.IS_CONTEXT_PREFERENCES_ENABLED, + defaultValue: false, + organization: { _id: organizationId }, + }); + + if (!isEnabled) { + return undefined; // Ignore context when FF is off + } + + if (!context) { + return []; + } + + const contexts = await this.contextRepository.findOrCreateContextsFromPayload( + environmentId, + organizationId, + context + ); + + return contexts.map((ctx) => ctx.key); + } } diff --git a/apps/api/src/app/subscribers-v2/dtos/bulk-update-subscriber-preferences.dto.ts b/apps/api/src/app/subscribers-v2/dtos/bulk-update-subscriber-preferences.dto.ts index 948f83df583..36d1258abdf 100644 --- a/apps/api/src/app/subscribers-v2/dtos/bulk-update-subscriber-preferences.dto.ts +++ b/apps/api/src/app/subscribers-v2/dtos/bulk-update-subscriber-preferences.dto.ts @@ -1,7 +1,9 @@ import { ApiProperty } from '@nestjs/swagger'; -import { parseSlugId } from '@novu/application-generic'; +import { IsValidContextPayload, parseSlugId } from '@novu/application-generic'; +import { ContextPayload } from '@novu/shared'; import { Transform, Type } from 'class-transformer'; -import { ArrayMaxSize, IsArray, IsDefined, IsString, ValidateNested } from 'class-validator'; +import { ArrayMaxSize, IsArray, IsDefined, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { ApiContextPayload } from '../../shared/framework/swagger'; import { PatchPreferenceChannelsDto } from './patch-subscriber-preferences.dto'; export class BulkUpdateSubscriberPreferenceItemDto { @@ -30,4 +32,9 @@ export class BulkUpdateSubscriberPreferencesDto { @Type(() => BulkUpdateSubscriberPreferenceItemDto) @ValidateNested({ each: true }) readonly preferences: BulkUpdateSubscriberPreferenceItemDto[]; + + @ApiContextPayload() + @IsOptional() + @IsValidContextPayload({ maxCount: 5 }) + context?: ContextPayload; } diff --git a/apps/api/src/app/subscribers-v2/dtos/get-subscriber-preferences-request.dto.ts b/apps/api/src/app/subscribers-v2/dtos/get-subscriber-preferences-request.dto.ts index 2bc1dfadda9..a001d2b54d4 100644 --- a/apps/api/src/app/subscribers-v2/dtos/get-subscriber-preferences-request.dto.ts +++ b/apps/api/src/app/subscribers-v2/dtos/get-subscriber-preferences-request.dto.ts @@ -1,6 +1,7 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; import { WorkflowCriticalityEnum } from '@novu/shared'; -import { IsEnum, IsOptional } from 'class-validator'; +import { Transform } from 'class-transformer'; +import { IsArray, IsEnum, IsOptional, IsString } from 'class-validator'; export class GetSubscriberPreferencesRequestDto { @IsEnum(WorkflowCriticalityEnum) @@ -10,4 +11,25 @@ export class GetSubscriberPreferencesRequestDto { default: WorkflowCriticalityEnum.NON_CRITICAL, }) criticality?: WorkflowCriticalityEnum = WorkflowCriticalityEnum.NON_CRITICAL; + + @IsOptional() + @Transform(({ value }) => { + // No parameter = no filter + if (value === undefined) return undefined; + + // Empty string = filter for records with no (default) context + if (value === '') return []; + + // Normalize to array and remove empty strings + const array = Array.isArray(value) ? value : [value]; + return array.filter((v) => v !== ''); + }) + @IsArray() + @IsString({ each: true }) + @ApiPropertyOptional({ + description: 'Context keys for filtering preferences (e.g., ["tenant:acme"])', + type: [String], + example: ['tenant:acme'], + }) + contextKeys?: string[]; } diff --git a/apps/api/src/app/subscribers-v2/dtos/patch-subscriber-preferences.dto.ts b/apps/api/src/app/subscribers-v2/dtos/patch-subscriber-preferences.dto.ts index 3cc89f47556..c6aa33bc2bf 100644 --- a/apps/api/src/app/subscribers-v2/dtos/patch-subscriber-preferences.dto.ts +++ b/apps/api/src/app/subscribers-v2/dtos/patch-subscriber-preferences.dto.ts @@ -1,9 +1,10 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { parseSlugId } from '@novu/application-generic'; -import { IPreferenceChannels } from '@novu/shared'; +import { IsValidContextPayload, parseSlugId } from '@novu/application-generic'; +import { ContextPayload, IPreferenceChannels } from '@novu/shared'; import { Transform, Type } from 'class-transformer'; import { IsOptional, ValidateNested } from 'class-validator'; import { ScheduleDto } from '../../shared/dtos/schedule'; +import { ApiContextPayload } from '../../shared/framework/swagger'; export class PatchPreferenceChannelsDto implements IPreferenceChannels { @ApiProperty({ description: 'Email channel preference' }) @@ -41,4 +42,9 @@ export class PatchSubscriberPreferencesDto { @ValidateNested() @Type(() => ScheduleDto) schedule?: ScheduleDto; + + @ApiContextPayload() + @IsOptional() + @IsValidContextPayload({ maxCount: 5 }) + context?: ContextPayload; } diff --git a/apps/api/src/app/subscribers-v2/e2e/get-subscriber-preferences.e2e.ts b/apps/api/src/app/subscribers-v2/e2e/get-subscriber-preferences.e2e.ts index 90acd26d8e1..37401624071 100644 --- a/apps/api/src/app/subscribers-v2/e2e/get-subscriber-preferences.e2e.ts +++ b/apps/api/src/app/subscribers-v2/e2e/get-subscriber-preferences.e2e.ts @@ -14,6 +14,7 @@ describe('Get Subscriber Preferences - /subscribers/:subscriberId/preferences (G let workflow: NotificationTemplateEntity; beforeEach(async () => { + (process.env as any).IS_CONTEXT_PREFERENCES_ENABLED = 'true'; const uuid = randomBytes(4).toString('hex'); session = new UserSession(); await session.initialize(); @@ -24,8 +25,12 @@ describe('Get Subscriber Preferences - /subscribers/:subscriberId/preferences (G }); }); + afterEach(() => { + delete (process.env as any).IS_CONTEXT_PREFERENCES_ENABLED; + }); + it('should fetch subscriber preferences with default values', async () => { - const response = await novuClient.subscribers.preferences.list(subscriber.subscriberId); + const response = await novuClient.subscribers.preferences.list({ subscriberId: subscriber.subscriberId }); const { global, workflows } = response.result; @@ -37,7 +42,7 @@ describe('Get Subscriber Preferences - /subscribers/:subscriberId/preferences (G it('should return 404 if subscriber does not exist', async () => { const invalidSubscriberId = `non-existent-${randomBytes(2).toString('hex')}`; const { error } = await expectSdkExceptionGeneric(() => - novuClient.subscribers.preferences.list(invalidSubscriberId) + novuClient.subscribers.preferences.list({ subscriberId: invalidSubscriberId }) ); expect(error?.statusCode).to.equal(404); @@ -48,7 +53,7 @@ describe('Get Subscriber Preferences - /subscribers/:subscriberId/preferences (G const workflow2 = await session.createTemplate({ noFeedId: true }); const workflow3 = await session.createTemplate({ noFeedId: true }); - const response = await novuClient.subscribers.preferences.list(subscriber.subscriberId); + const response = await novuClient.subscribers.preferences.list({ subscriberId: subscriber.subscriberId }); const { workflows } = response.result; @@ -75,7 +80,7 @@ describe('Get Subscriber Preferences - /subscribers/:subscriberId/preferences (G const newWorkflow = await session.createTemplate({ noFeedId: true }); // Check preferences - const response = await novuClient.subscribers.preferences.list(subscriber.subscriberId); + const response = await novuClient.subscribers.preferences.list({ subscriberId: subscriber.subscriberId }); const { workflows } = response.result; @@ -85,6 +90,106 @@ describe('Get Subscriber Preferences - /subscribers/:subscriberId/preferences (G // New workflow should inherit global settings expect(newWorkflowPreferences?.channels).to.deep.equal({ email: false, inApp: true }); }); + + it('should filter preferences by contextKeys', async () => { + // Create preference for context A + await novuClient.subscribers.preferences.update( + { + workflowId: workflow._id, + channels: { email: false }, + context: { tenant: 'acme' }, + }, + subscriber.subscriberId + ); + + // Create preference for context B + const workflow2 = await session.createTemplate({ noFeedId: true }); + await novuClient.subscribers.preferences.update( + { + workflowId: workflow2._id, + channels: { email: false }, + context: { tenant: 'globex' }, + }, + subscriber.subscriberId + ); + + // List with context A filter + const responseA = await novuClient.subscribers.preferences.list({ + subscriberId: subscriber.subscriberId, + contextKeys: ['tenant:acme'], + }); + + // Should return BOTH workflows (all workflows always returned regardless of context) + const workflowIdentifiers = responseA.result.workflows.map((w) => w.workflow.identifier); + expect(workflowIdentifiers).to.include(workflow.triggers[0].identifier); + expect(workflowIdentifiers).to.include(workflow2.triggers[0].identifier); + + // workflow1 uses tenant:acme preference (email: false) + const wf1 = responseA.result.workflows.find((w) => w.workflow.identifier === workflow.triggers[0].identifier); + expect(wf1?.channels.email).to.equal(false); + + // workflow2 falls back to global/default (email: true by default) + const wf2 = responseA.result.workflows.find((w) => w.workflow.identifier === workflow2.triggers[0].identifier); + expect(wf2?.channels.email).to.equal(true); + }); + + it('should return default preferences when no context-specific preference exists', async () => { + // Create workflow preference for context A + await novuClient.subscribers.preferences.update( + { + workflowId: workflow._id, + channels: { email: false }, + context: { tenant: 'acme' }, + }, + subscriber.subscriberId + ); + + // List with different context B (no specific preference exists) + const response = await novuClient.subscribers.preferences.list({ + subscriberId: subscriber.subscriberId, + contextKeys: ['tenant:globex'], + }); + + // Should return workflow with default/inherited settings + expect(response.result.workflows).to.have.lengthOf(1); + // Default should be enabled + expect(response.result.workflows[0].channels.email).to.equal(true); + }); + + it('should isolate preferences per context', async () => { + // Set global preference + await novuClient.subscribers.preferences.update( + { + channels: { email: false, inApp: false }, + }, + subscriber.subscriberId + ); + + // Create workflow preference for context A (override email) + await novuClient.subscribers.preferences.update( + { + workflowId: workflow._id, + channels: { email: true }, // Override to true + context: { tenant: 'acme' }, + }, + subscriber.subscriberId + ); + + // List with context A + const responseA = await novuClient.subscribers.preferences.list({ + subscriberId: subscriber.subscriberId, + contextKeys: ['tenant:acme'], + }); + expect(responseA.result.workflows[0].channels.email).to.equal(true); + expect(responseA.result.global.channels.email).to.equal(false); // Global unchanged + + // List with context B (should see global) + const responseB = await novuClient.subscribers.preferences.list({ + subscriberId: subscriber.subscriberId, + contextKeys: ['tenant:globex'], + }); + expect(responseB.result.workflows[0].channels.email).to.equal(false); // Inherits global + }); }); async function createSubscriberAndValidate(id: string = '') { diff --git a/apps/api/src/app/subscribers-v2/e2e/patch-subscriber-preferences.e2e.ts b/apps/api/src/app/subscribers-v2/e2e/patch-subscriber-preferences.e2e.ts index 953a768b361..4f0f7823af7 100644 --- a/apps/api/src/app/subscribers-v2/e2e/patch-subscriber-preferences.e2e.ts +++ b/apps/api/src/app/subscribers-v2/e2e/patch-subscriber-preferences.e2e.ts @@ -24,6 +24,7 @@ describe('Patch Subscriber Preferences - /subscribers/:subscriberId/preferences let workflow: NotificationTemplateEntity; beforeEach(async () => { + (process.env as any).IS_CONTEXT_PREFERENCES_ENABLED = 'true'; const uuid = randomBytes(4).toString('hex'); session = new UserSession(); await session.initialize(); @@ -34,6 +35,10 @@ describe('Patch Subscriber Preferences - /subscribers/:subscriberId/preferences }); }); + afterEach(() => { + delete (process.env as any).IS_CONTEXT_PREFERENCES_ENABLED; + }); + it('should patch workflow channel preferences', async () => { // Patch with workflow id const workflowId = workflow._id; @@ -259,6 +264,101 @@ describe('Patch Subscriber Preferences - /subscribers/:subscriberId/preferences expect(error?.statusCode).to.equal(404); expect(error?.message).to.include('Workflows with ids: non-existent-workflow-id not found'); }); + + it('should create workflow preference with context', async () => { + const patchData: PatchSubscriberPreferencesDto = { + workflowId: workflow._id, + channels: { + email: false, + inApp: true, + }, + context: { tenant: 'acme' }, + }; + + const response = await novuClient.subscribers.preferences.update(patchData, subscriber.subscriberId); + + expect(response.result.workflows).to.have.lengthOf(1); + expect(response.result.workflows[0].channels).to.deep.equal({ inApp: true, email: false }); + }); + + it('should create separate preferences for different contexts', async () => { + // Create preference for context A + await novuClient.subscribers.preferences.update( + { + workflowId: workflow._id, + channels: { email: false }, + context: { tenant: 'acme' }, + }, + subscriber.subscriberId + ); + + // Create preference for context B + await novuClient.subscribers.preferences.update( + { + workflowId: workflow._id, + channels: { email: true }, + context: { tenant: 'globex' }, + }, + subscriber.subscriberId + ); + + // Both should coexist - verify by listing with different contextKeys + const responseA = await novuClient.subscribers.preferences.list({ + subscriberId: subscriber.subscriberId, + contextKeys: ['tenant:acme'], + }); + expect(responseA.result.workflows[0].channels.email).to.equal(false); + + const responseB = await novuClient.subscribers.preferences.list({ + subscriberId: subscriber.subscriberId, + contextKeys: ['tenant:globex'], + }); + expect(responseB.result.workflows[0].channels.email).to.equal(true); + }); + + it('should reject context for global preferences', async () => { + const patchData: PatchSubscriberPreferencesDto = { + // No workflowId = global preference + channels: { + email: false, + }, + context: { tenant: 'acme' }, // Should be rejected + }; + + const { error } = await expectSdkExceptionGeneric(() => + novuClient.subscribers.preferences.update(patchData, subscriber.subscriberId) + ); + + expect(error?.statusCode).to.equal(400); + expect(error?.message).to.include('Context cannot be used with global preferences'); + }); + + it('should bulk update with context', async () => { + const bulkUpdateData: BulkUpdateSubscriberPreferencesDto = { + context: { tenant: 'acme' }, + preferences: [ + { + workflowId: workflow._id, + channels: { + email: false, + inApp: true, + }, + }, + ], + }; + + const response = await novuClient.subscribers.preferences.bulkUpdate(bulkUpdateData, subscriber.subscriberId); + + expect(response.result).to.have.lengthOf(1); + expect(response.result[0].channels.email).to.equal(false); + + // Verify it's stored with context + const listResponse = await novuClient.subscribers.preferences.list({ + subscriberId: subscriber.subscriberId, + contextKeys: ['tenant:acme'], + }); + expect(listResponse.result.workflows[0].channels.email).to.equal(false); + }); }); async function createSubscriberAndValidate(id: string = '') { diff --git a/apps/api/src/app/subscribers-v2/subscribers.controller.ts b/apps/api/src/app/subscribers-v2/subscribers.controller.ts index e82d8e0de31..4a79e7b0f0d 100644 --- a/apps/api/src/app/subscribers-v2/subscribers.controller.ts +++ b/apps/api/src/app/subscribers-v2/subscribers.controller.ts @@ -260,6 +260,7 @@ export class SubscribersController { organizationId: user.organizationId, subscriberId, criticality: query.criticality, + contextKeys: query.contextKeys, }) ); } @@ -322,6 +323,7 @@ export class SubscribersController { subscriberId, environmentId: user.environmentId, preferences, + context: body.context, }) ); } @@ -351,6 +353,7 @@ export class SubscribersController { workflowIdOrInternalId: body.workflowId, channels: body.channels, schedule: body.schedule, + context: body.context, }) ); } diff --git a/apps/api/src/app/subscribers-v2/subscribers.module.ts b/apps/api/src/app/subscribers-v2/subscribers.module.ts index 8ac84359ce2..1f334965838 100644 --- a/apps/api/src/app/subscribers-v2/subscribers.module.ts +++ b/apps/api/src/app/subscribers-v2/subscribers.module.ts @@ -15,6 +15,7 @@ import { } from '@novu/application-generic'; import { CommunityOrganizationRepository, + ContextRepository, EnvironmentRepository, IntegrationRepository, MessageRepository, @@ -71,6 +72,7 @@ const DAL_MODELS = [ WorkflowOverrideRepository, TenantRepository, MessageRepository, + ContextRepository, ]; @Module({ diff --git a/apps/api/src/app/subscribers-v2/usecases/get-subscriber-preferences/get-subscriber-preferences.usecase.ts b/apps/api/src/app/subscribers-v2/usecases/get-subscriber-preferences/get-subscriber-preferences.usecase.ts index 9f688b7bfd9..846f1e32c14 100644 --- a/apps/api/src/app/subscribers-v2/usecases/get-subscriber-preferences/get-subscriber-preferences.usecase.ts +++ b/apps/api/src/app/subscribers-v2/usecases/get-subscriber-preferences/get-subscriber-preferences.usecase.ts @@ -57,6 +57,7 @@ export class GetSubscriberPreferences { organizationId: command.organizationId, includeInactiveChannels: false, criticality: command.criticality ?? WorkflowCriticalityEnum.NON_CRITICAL, + contextKeys: command.contextKeys, }) ); diff --git a/apps/api/src/app/subscribers-v2/usecases/update-subscriber-preferences/update-subscriber-preferences.command.ts b/apps/api/src/app/subscribers-v2/usecases/update-subscriber-preferences/update-subscriber-preferences.command.ts index 0cf8130dc36..4705780a375 100644 --- a/apps/api/src/app/subscribers-v2/usecases/update-subscriber-preferences/update-subscriber-preferences.command.ts +++ b/apps/api/src/app/subscribers-v2/usecases/update-subscriber-preferences/update-subscriber-preferences.command.ts @@ -1,3 +1,5 @@ +import { IsValidContextPayload } from '@novu/application-generic'; +import { ContextPayload } from '@novu/shared'; import { Type } from 'class-transformer'; import { IsOptional, IsString } from 'class-validator'; import { EnvironmentWithSubscriber } from '../../../shared/commands/project.command'; @@ -16,4 +18,8 @@ export class UpdateSubscriberPreferencesCommand extends EnvironmentWithSubscribe @IsOptional() @Type(() => ScheduleDto) readonly schedule?: ScheduleDto; + + @IsOptional() + @IsValidContextPayload({ maxCount: 5 }) + readonly context?: ContextPayload; } diff --git a/apps/api/src/app/subscribers-v2/usecases/update-subscriber-preferences/update-subscriber-preferences.usecase.ts b/apps/api/src/app/subscribers-v2/usecases/update-subscriber-preferences/update-subscriber-preferences.usecase.ts index 5d67a5593ee..fda0f235948 100644 --- a/apps/api/src/app/subscribers-v2/usecases/update-subscriber-preferences/update-subscriber-preferences.usecase.ts +++ b/apps/api/src/app/subscribers-v2/usecases/update-subscriber-preferences/update-subscriber-preferences.usecase.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@nestjs/common'; -import { GetWorkflowByIdsCommand, GetWorkflowByIdsUseCase } from '@novu/application-generic'; -import { PreferenceLevelEnum, WorkflowCriticalityEnum } from '@novu/shared'; +import { BadRequestException, Injectable } from '@nestjs/common'; +import { FeatureFlagsService, GetWorkflowByIdsCommand, GetWorkflowByIdsUseCase } from '@novu/application-generic'; +import { ContextRepository } from '@novu/dal'; +import { ContextPayload, FeatureFlagsKeysEnum, PreferenceLevelEnum, WorkflowCriticalityEnum } from '@novu/shared'; import { plainToInstance } from 'class-transformer'; import { UpdatePreferencesCommand } from '../../../inbox/usecases/update-preferences/update-preferences.command'; import { UpdatePreferences } from '../../../inbox/usecases/update-preferences/update-preferences.usecase'; @@ -13,10 +14,16 @@ export class UpdateSubscriberPreferences { constructor( private updatePreferencesUsecase: UpdatePreferences, private getSubscriberPreferences: GetSubscriberPreferences, - private getWorkflowByIdsUseCase: GetWorkflowByIdsUseCase + private getWorkflowByIdsUseCase: GetWorkflowByIdsUseCase, + private contextRepository: ContextRepository, + private featureFlagsService: FeatureFlagsService ) {} async execute(command: UpdateSubscriberPreferencesCommand): Promise { + this.validateContextRequiresWorkflow(command); + + const contextKeys = await this.resolveContexts(command.environmentId, command.organizationId, command.context); + let workflowId: string | undefined; if (command.workflowIdOrInternalId) { const workflowEntity = await this.getWorkflowByIdsUseCase.execute( @@ -39,6 +46,7 @@ export class UpdateSubscriberPreferences { includeInactiveChannels: false, ...command.channels, schedule: command.schedule, + contextKeys, }) ); @@ -47,6 +55,7 @@ export class UpdateSubscriberPreferences { organizationId: command.organizationId, subscriberId: command.subscriberId, criticality: WorkflowCriticalityEnum.NON_CRITICAL, + contextKeys, }); return plainToInstance(GetSubscriberPreferencesDto, { @@ -54,4 +63,41 @@ export class UpdateSubscriberPreferences { workflows: subscriberPreferences.workflows, }); } + + private validateContextRequiresWorkflow(command: UpdateSubscriberPreferencesCommand): void { + if (command.context && !command.workflowIdOrInternalId) { + throw new BadRequestException( + 'Context cannot be used with global preferences. Please provide a workflowId to update workflow preferences with context.' + ); + } + } + + private async resolveContexts( + environmentId: string, + organizationId: string, + context?: ContextPayload + ): Promise { + // Check if context preferences feature is enabled + const isEnabled = await this.featureFlagsService.getFlag({ + key: FeatureFlagsKeysEnum.IS_CONTEXT_PREFERENCES_ENABLED, + defaultValue: false, + organization: { _id: organizationId }, + }); + + if (!isEnabled) { + return undefined; // Ignore context when FF is off + } + + if (!context) { + return []; + } + + const contexts = await this.contextRepository.findOrCreateContextsFromPayload( + environmentId, + organizationId, + context + ); + + return contexts.map((ctx) => ctx.key); + } } diff --git a/libs/dal/src/repositories/preferences/preferences.schema.ts b/libs/dal/src/repositories/preferences/preferences.schema.ts index b41fbb564f2..ffce7e00fb5 100644 --- a/libs/dal/src/repositories/preferences/preferences.schema.ts +++ b/libs/dal/src/repositories/preferences/preferences.schema.ts @@ -107,7 +107,8 @@ preferencesSchema.index( ); // Subscriber Workflow Preferences -// Ensures one workflow preference per subscriber per template (SUBSCRIBER_WORKFLOW type) +// Ensures one workflow preference per subscriber per template per context (SUBSCRIBER_WORKFLOW type) +// Includes contextKeys to allow multiple preferences for different contexts // Partial filter ensures this only applies to SUBSCRIBER_WORKFLOW type, // preventing conflicts with other preference types preferencesSchema.index( @@ -116,6 +117,7 @@ preferencesSchema.index( _subscriberId: 1, _templateId: 1, type: 1, + contextKeys: 1, }, { unique: true, @@ -143,7 +145,8 @@ preferencesSchema.index( } ); -// Ensures one workflow preference per subscriber per template per topic subscription (SUBSCRIPTION_SUBSCRIBER_WORKFLOW type) +// Ensures one workflow preference per subscriber per template per topic subscription per context (SUBSCRIPTION_SUBSCRIBER_WORKFLOW type) +// Includes contextKeys to allow multiple preferences for different contexts // Only for this type (via partial filter). preferencesSchema.index( { @@ -152,6 +155,7 @@ preferencesSchema.index( _topicSubscriptionId: 1, _templateId: 1, type: 1, + contextKeys: 1, }, { unique: true, diff --git a/libs/internal-sdk/.speakeasy/gen.yaml b/libs/internal-sdk/.speakeasy/gen.yaml index c7661901093..c6d6a6aa0cd 100755 --- a/libs/internal-sdk/.speakeasy/gen.yaml +++ b/libs/internal-sdk/.speakeasy/gen.yaml @@ -22,7 +22,8 @@ generation: schemas: allOfMergeStrategy: shallowMerge requestBodyFieldName: "" - persistentEdits: {} + persistentEdits: + enabled: never tests: generateTests: true generateNewTests: false @@ -91,6 +92,7 @@ typescript: outputModelSuffix: output packageName: '@novu/api' preApplyUnionDiscriminators: true + preserveModelFieldNames: false responseFormat: flat sseFlatResponse: false templateVersion: v2 diff --git a/libs/internal-sdk/src/funcs/subscribersPreferencesList.ts b/libs/internal-sdk/src/funcs/subscribersPreferencesList.ts index f853b0d01f7..8b61c7750f6 100644 --- a/libs/internal-sdk/src/funcs/subscribersPreferencesList.ts +++ b/libs/internal-sdk/src/funcs/subscribersPreferencesList.ts @@ -2,28 +2,28 @@ * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ -import { NovuCore } from "../core.js"; -import { encodeFormQuery, encodeSimple } from "../lib/encodings.js"; -import * as M from "../lib/matchers.js"; -import { compactMap } from "../lib/primitives.js"; -import { safeParse } from "../lib/schemas.js"; -import { RequestOptions } from "../lib/sdks.js"; -import { extractSecurity, resolveGlobalSecurity } from "../lib/security.js"; -import { pathToFunc } from "../lib/url.js"; +import { NovuCore } from '../core.js'; +import { encodeFormQuery, encodeSimple } from '../lib/encodings.js'; +import * as M from '../lib/matchers.js'; +import { compactMap } from '../lib/primitives.js'; +import { safeParse } from '../lib/schemas.js'; +import { RequestOptions } from '../lib/sdks.js'; +import { extractSecurity, resolveGlobalSecurity } from '../lib/security.js'; +import { pathToFunc } from '../lib/url.js'; import { ConnectionError, InvalidRequestError, RequestAbortedError, RequestTimeoutError, UnexpectedClientError, -} from "../models/errors/httpclienterrors.js"; -import * as errors from "../models/errors/index.js"; -import { NovuError } from "../models/errors/novuerror.js"; -import { ResponseValidationError } from "../models/errors/responsevalidationerror.js"; -import { SDKValidationError } from "../models/errors/sdkvalidationerror.js"; -import * as operations from "../models/operations/index.js"; -import { APICall, APIPromise } from "../types/async.js"; -import { Result } from "../types/fp.js"; +} from '../models/errors/httpclienterrors.js'; +import * as errors from '../models/errors/index.js'; +import { NovuError } from '../models/errors/novuerror.js'; +import { ResponseValidationError } from '../models/errors/responsevalidationerror.js'; +import { SDKValidationError } from '../models/errors/sdkvalidationerror.js'; +import * as operations from '../models/operations/index.js'; +import { APICall, APIPromise } from '../types/async.js'; +import { Result } from '../types/fp.js'; /** * Retrieve subscriber preferences @@ -34,10 +34,8 @@ import { Result } from "../types/fp.js"; */ export function subscribersPreferencesList( client: NovuCore, - subscriberId: string, - criticality?: operations.Criticality | undefined, - idempotencyKey?: string | undefined, - options?: RequestOptions, + request: operations.SubscribersControllerGetSubscriberPreferencesRequest, + options?: RequestOptions ): APIPromise< Result< operations.SubscribersControllerGetSubscriberPreferencesResponse, @@ -53,21 +51,13 @@ export function subscribersPreferencesList( | SDKValidationError > > { - return new APIPromise($do( - client, - subscriberId, - criticality, - idempotencyKey, - options, - )); + return new APIPromise($do(client, request, options)); } async function $do( client: NovuCore, - subscriberId: string, - criticality?: operations.Criticality | undefined, - idempotencyKey?: string | undefined, - options?: RequestOptions, + request: operations.SubscribersControllerGetSubscriberPreferencesRequest, + options?: RequestOptions ): Promise< [ Result< @@ -86,67 +76,56 @@ async function $do( APICall, ] > { - const input: operations.SubscribersControllerGetSubscriberPreferencesRequest = - { - subscriberId: subscriberId, - criticality: criticality, - idempotencyKey: idempotencyKey, - }; - const parsed = safeParse( - input, - (value) => - operations - .SubscribersControllerGetSubscriberPreferencesRequest$outboundSchema - .parse(value), - "Input validation failed", + request, + (value) => operations.SubscribersControllerGetSubscriberPreferencesRequest$outboundSchema.parse(value), + 'Input validation failed' ); if (!parsed.ok) { - return [parsed, { status: "invalid" }]; + return [parsed, { status: 'invalid' }]; } const payload = parsed.value; const body = null; const pathParams = { - subscriberId: encodeSimple("subscriberId", payload.subscriberId, { + subscriberId: encodeSimple('subscriberId', payload.subscriberId, { explode: false, - charEncoding: "percent", + charEncoding: 'percent', }), }; - const path = pathToFunc("/v2/subscribers/{subscriberId}/preferences")( - pathParams, - ); + const path = pathToFunc('/v2/subscribers/{subscriberId}/preferences')(pathParams); const query = encodeFormQuery({ - "criticality": payload.criticality, + contextKeys: payload.contextKeys, + criticality: payload.criticality, }); - const headers = new Headers(compactMap({ - Accept: "application/json", - "idempotency-key": encodeSimple( - "idempotency-key", - payload["idempotency-key"], - { explode: false, charEncoding: "none" }, - ), - })); + const headers = new Headers( + compactMap({ + Accept: 'application/json', + 'idempotency-key': encodeSimple('idempotency-key', payload['idempotency-key'], { + explode: false, + charEncoding: 'none', + }), + }) + ); const securityInput = await extractSecurity(client._options.security); const requestSecurity = resolveGlobalSecurity(securityInput); const context = { options: client._options, - baseURL: options?.serverURL ?? client._baseURL ?? "", - operationID: "SubscribersController_getSubscriberPreferences", + baseURL: options?.serverURL ?? client._baseURL ?? '', + operationID: 'SubscribersController_getSubscriberPreferences', oAuth2Scopes: null, resolvedSecurity: requestSecurity, securitySource: client._options.security, - retryConfig: options?.retries - || client._options.retryConfig - || { - strategy: "backoff", + retryConfig: options?.retries || + client._options.retryConfig || { + strategy: 'backoff', backoff: { initialInterval: 1000, maxInterval: 30000, @@ -154,51 +133,54 @@ async function $do( maxElapsedTime: 3600000, }, retryConnectionErrors: true, - } - || { strategy: "none" }, - retryCodes: options?.retryCodes || ["408", "409", "429", "5XX"], + } || { strategy: 'none' }, + retryCodes: options?.retryCodes || ['408', '409', '429', '5XX'], }; - const requestRes = client._createRequest(context, { - security: requestSecurity, - method: "GET", - baseURL: options?.serverURL, - path: path, - headers: headers, - query: query, - body: body, - userAgent: client._options.userAgent, - timeoutMs: options?.timeoutMs || client._options.timeoutMs || -1, - }, options); + const requestRes = client._createRequest( + context, + { + security: requestSecurity, + method: 'GET', + baseURL: options?.serverURL, + path: path, + headers: headers, + query: query, + body: body, + userAgent: client._options.userAgent, + timeoutMs: options?.timeoutMs || client._options.timeoutMs || -1, + }, + options + ); if (!requestRes.ok) { - return [requestRes, { status: "invalid" }]; + return [requestRes, { status: 'invalid' }]; } const req = requestRes.value; const doResult = await client._do(req, { context, errorCodes: [ - "400", - "401", - "403", - "404", - "405", - "409", - "413", - "414", - "415", - "422", - "429", - "4XX", - "500", - "503", - "5XX", + '400', + '401', + '403', + '404', + '405', + '409', + '413', + '414', + '415', + '422', + '429', + '4XX', + '500', + '503', + '5XX', ], retryConfig: context.retryConfig, retryCodes: context.retryCodes, }); if (!doResult.ok) { - return [doResult, { status: "request-error", request: req }]; + return [doResult, { status: 'request-error', request: req }]; } const response = doResult.value; @@ -219,28 +201,22 @@ async function $do( | UnexpectedClientError | SDKValidationError >( - M.json( - 200, - operations - .SubscribersControllerGetSubscriberPreferencesResponse$inboundSchema, - { hdrs: true, key: "Result" }, - ), + M.json(200, operations.SubscribersControllerGetSubscriberPreferencesResponse$inboundSchema, { + hdrs: true, + key: 'Result', + }), M.jsonErr(414, errors.ErrorDto$inboundSchema), - M.jsonErr( - [400, 401, 403, 404, 405, 409, 413, 415], - errors.ErrorDto$inboundSchema, - { hdrs: true }, - ), + M.jsonErr([400, 401, 403, 404, 405, 409, 413, 415], errors.ErrorDto$inboundSchema, { hdrs: true }), M.jsonErr(422, errors.ValidationErrorDto$inboundSchema, { hdrs: true }), M.fail(429), M.jsonErr(500, errors.ErrorDto$inboundSchema, { hdrs: true }), M.fail(503), - M.fail("4XX"), - M.fail("5XX"), + M.fail('4XX'), + M.fail('5XX') )(response, req, { extraFields: responseFields }); if (!result.ok) { - return [result, { status: "complete", request: req, response }]; + return [result, { status: 'complete', request: req, response }]; } - return [result, { status: "complete", request: req, response }]; + return [result, { status: 'complete', request: req, response }]; } diff --git a/libs/internal-sdk/src/lib/config.ts b/libs/internal-sdk/src/lib/config.ts index 1bfe131b974..e2b8c0b84c5 100644 --- a/libs/internal-sdk/src/lib/config.ts +++ b/libs/internal-sdk/src/lib/config.ts @@ -59,8 +59,8 @@ export function serverURLFromOptions(options: SDKOptions): URL | null { export const SDK_METADATA = { language: 'typescript', - openapiDocVersion: '3.11.0', + openapiDocVersion: '3.12.0', sdkVersion: '0.1.21', - genVersion: '2.791.1', - userAgent: 'speakeasy-sdk/typescript 0.1.21 2.791.1 3.11.0 @novu/api', + genVersion: '2.792.3', + userAgent: 'speakeasy-sdk/typescript 0.1.21 2.792.3 3.12.0 @novu/api', } as const; diff --git a/libs/internal-sdk/src/lib/sdks.ts b/libs/internal-sdk/src/lib/sdks.ts index fb7e9ac575a..d8808ff2732 100644 --- a/libs/internal-sdk/src/lib/sdks.ts +++ b/libs/internal-sdk/src/lib/sdks.ts @@ -2,19 +2,19 @@ * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ -import { SDKHooks } from "../hooks/hooks.js"; -import { HookContext } from "../hooks/types.js"; +import { SDKHooks } from '../hooks/hooks.js'; +import { HookContext } from '../hooks/types.js'; import { ConnectionError, InvalidRequestError, RequestAbortedError, RequestTimeoutError, UnexpectedClientError, -} from "../models/errors/httpclienterrors.js"; -import { ERR, OK, Result } from "../types/fp.js"; -import { stringToBase64 } from "./base64.js"; -import { SDK_METADATA, SDKOptions, serverURLFromOptions } from "./config.js"; -import { encodeForm } from "./encodings.js"; +} from '../models/errors/httpclienterrors.js'; +import { ERR, OK, Result } from '../types/fp.js'; +import { stringToBase64 } from './base64.js'; +import { SDK_METADATA, SDKOptions, serverURLFromOptions } from './config.js'; +import { encodeForm } from './encodings.js'; import { HTTPClient, isAbortError, @@ -22,10 +22,10 @@ import { isTimeoutError, matchContentType, matchStatusCode, -} from "./http.js"; -import { Logger } from "./logger.js"; -import { retry, RetryConfig } from "./retries.js"; -import { SecurityState } from "./security.js"; +} from './http.js'; +import { Logger } from './logger.js'; +import { RetryConfig, retry } from './retries.js'; +import { SecurityState } from './security.js'; export type RequestOptions = { /** @@ -52,15 +52,15 @@ export type RequestOptions = { * * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Request/Request#options|Request} */ - fetchOptions?: Omit; -} & Omit; + fetchOptions?: Omit; +} & Omit; type RequestConfig = { method: string; path: string; baseURL?: string | URL | undefined; query?: string; - body?: RequestInit["body"]; + body?: RequestInit['body']; headers?: HeadersInit; security?: SecurityState | null; uaHeader?: string; @@ -68,14 +68,13 @@ type RequestConfig = { timeoutMs?: number; }; -const gt: unknown = typeof globalThis === "undefined" ? null : globalThis; -const webWorkerLike = typeof gt === "object" - && gt != null - && "importScripts" in gt - && typeof gt["importScripts"] === "function"; -const isBrowserLike = webWorkerLike - || (typeof navigator !== "undefined" && "serviceWorker" in navigator) - || (typeof window === "object" && typeof window.document !== "undefined"); +const gt: unknown = typeof globalThis === 'undefined' ? null : globalThis; +const webWorkerLike = + typeof gt === 'object' && gt != null && 'importScripts' in gt && typeof gt['importScripts'] === 'function'; +const isBrowserLike = + webWorkerLike || + (typeof navigator !== 'undefined' && 'serviceWorker' in navigator) || + (typeof window === 'object' && typeof window.document !== 'undefined'); export class ClientSDK { readonly #httpClient: HTTPClient; @@ -86,12 +85,7 @@ export class ClientSDK { constructor(options: SDKOptions = {}) { const opt = options as unknown; - if ( - typeof opt === "object" - && opt != null - && "hooks" in opt - && opt.hooks instanceof SDKHooks - ) { + if (typeof opt === 'object' && opt != null && 'hooks' in opt && opt.hooks instanceof SDKHooks) { this.#hooks = opt.hooks; } else { this.#hooks = new SDKHooks(); @@ -102,7 +96,7 @@ export class ClientSDK { const url = serverURLFromOptions(options); if (url) { - url.pathname = url.pathname.replace(/\/+$/, "") + "/"; + url.pathname = url.pathname.replace(/\/+$/, '') + '/'; } this._baseURL = url; this.#httpClient = options.httpClient || defaultHttpClient; @@ -115,37 +109,37 @@ export class ClientSDK { public _createRequest( context: HookContext, conf: RequestConfig, - options?: RequestOptions, + options?: RequestOptions ): Result { const { method, path, query, headers: opHeaders, security } = conf; const base = conf.baseURL ?? this._baseURL; if (!base) { - return ERR(new InvalidRequestError("No base URL provided for operation")); + return ERR(new InvalidRequestError('No base URL provided for operation')); } const reqURL = new URL(base); const inputURL = new URL(path, reqURL); if (path) { - reqURL.pathname += reqURL.pathname.endsWith("/") ? "" : "/"; - reqURL.pathname += inputURL.pathname.replace(/^\/+/, ""); + reqURL.pathname += reqURL.pathname.endsWith('/') ? '' : '/'; + reqURL.pathname += inputURL.pathname.replace(/^\/+/, ''); } - let finalQuery = query || ""; + let finalQuery = query || ''; const secQuery: string[] = []; for (const [k, v] of Object.entries(security?.queryParams || {})) { - const q = encodeForm(k, v, { charEncoding: "percent" }); - if (typeof q !== "undefined") { + const q = encodeForm(k, v, { charEncoding: 'percent' }); + if (typeof q !== 'undefined') { secQuery.push(q); } } if (secQuery.length) { - finalQuery += `&${secQuery.join("&")}`; + finalQuery += `&${secQuery.join('&')}`; } if (finalQuery) { - const q = finalQuery.startsWith("&") ? finalQuery.slice(1) : finalQuery; + const q = finalQuery.startsWith('&') ? finalQuery.slice(1) : finalQuery; reqURL.search = `?${q}`; } @@ -154,10 +148,8 @@ export class ClientSDK { const username = security?.basic.username; const password = security?.basic.password; if (username != null || password != null) { - const encoded = stringToBase64( - [username || "", password || ""].join(":"), - ); - headers.set("Authorization", `Basic ${encoded}`); + const encoded = stringToBase64([username || '', password || ''].join(':')); + headers.set('Authorization', `Basic ${encoded}`); } const securityHeaders = new Headers(security?.headers || {}); @@ -165,16 +157,14 @@ export class ClientSDK { headers.set(k, v); } - let cookie = headers.get("cookie") || ""; + let cookie = headers.get('cookie') || ''; for (const [k, v] of Object.entries(security?.cookies || {})) { cookie += `; ${k}=${v}`; } - cookie = cookie.startsWith("; ") ? cookie.slice(2) : cookie; - headers.set("cookie", cookie); + cookie = cookie.startsWith('; ') ? cookie.slice(2) : cookie; + headers.set('cookie', cookie); - const userHeaders = new Headers( - options?.headers ?? options?.fetchOptions?.headers, - ); + const userHeaders = new Headers(options?.headers ?? options?.fetchOptions?.headers); for (const [k, v] of userHeaders) { headers.set(k, v); } @@ -182,13 +172,10 @@ export class ClientSDK { // Only set user agent header in non-browser-like environments since CORS // policy disallows setting it in browsers e.g. Chrome throws an error. if (!isBrowserLike) { - headers.set( - conf.uaHeader ?? "user-agent", - conf.userAgent ?? SDK_METADATA.userAgent, - ); + headers.set(conf.uaHeader ?? 'user-agent', conf.userAgent ?? SDK_METADATA.userAgent); } - const fetchOptions: Omit = { + const fetchOptions: Omit = { ...options?.fetchOptions, ...options, }; @@ -198,7 +185,7 @@ export class ClientSDK { } if (conf.body instanceof ReadableStream) { - Object.assign(fetchOptions, { duplex: "half" }); + Object.assign(fetchOptions, { duplex: 'half' }); } let input; @@ -214,9 +201,9 @@ export class ClientSDK { }); } catch (err: unknown) { return ERR( - new UnexpectedClientError("Create request hook failed to execute", { + new UnexpectedClientError('Create request hook failed to execute', { cause: err, - }), + }) ); } @@ -230,34 +217,20 @@ export class ClientSDK { errorCodes: number | string | (number | string)[]; retryConfig: RetryConfig; retryCodes: string[]; - }, - ): Promise< - Result< - Response, - | RequestAbortedError - | RequestTimeoutError - | ConnectionError - | UnexpectedClientError - > - > { + } + ): Promise> { const { context, errorCodes } = options; return retry( async () => { const req = await this.#hooks.beforeRequest(context, request.clone()); - await logRequest(this.#logger, req).catch((e) => - this.#logger?.log("Failed to log request:", e) - ); + await logRequest(this.#logger, req).catch((e) => this.#logger?.log('Failed to log request:', e)); let response = await this.#httpClient.request(req); try { if (matchStatusCode(response, errorCodes)) { - const result = await this.#hooks.afterError( - context, - response, - null, - ); + const result = await this.#hooks.afterError(context, response, null); if (result.error) { throw result.error; } @@ -266,74 +239,68 @@ export class ClientSDK { response = await this.#hooks.afterSuccess(context, response); } } finally { - await logResponse(this.#logger, response, req) - .catch(e => this.#logger?.log("Failed to log response:", e)); + await logResponse(this.#logger, response, req).catch((e) => this.#logger?.log('Failed to log response:', e)); } return response; }, - { config: options.retryConfig, statusCodes: options.retryCodes }, + { config: options.retryConfig, statusCodes: options.retryCodes } ).then( (r) => OK(r), (err) => { switch (true) { case isAbortError(err): return ERR( - new RequestAbortedError("Request aborted by client", { + new RequestAbortedError('Request aborted by client', { cause: err, - }), + }) ); case isTimeoutError(err): - return ERR( - new RequestTimeoutError("Request timed out", { cause: err }), - ); + return ERR(new RequestTimeoutError('Request timed out', { cause: err })); case isConnectionError(err): - return ERR( - new ConnectionError("Unable to make request", { cause: err }), - ); + return ERR(new ConnectionError('Unable to make request', { cause: err })); default: return ERR( - new UnexpectedClientError("Unexpected HTTP client error", { + new UnexpectedClientError('Unexpected HTTP client error', { cause: err, - }), + }) ); } - }, + } ); } } -const jsonLikeContentTypeRE = /(application|text)\/.*?\+*json.*/; -const jsonlLikeContentTypeRE = - /(application|text)\/(.*?\+*\bjsonl\b.*|.*?\+*\bx-ndjson\b.*)/; +const jsonLikeContentTypeRE = /(application|text)\/([^+]+\+)*json.*/; +const jsonlLikeContentTypeRE = /(application|text)\/(([^+]+\+)*jsonl\b.*|([^+]+\+)*x-ndjson\b.*)/; async function logRequest(logger: Logger | undefined, req: Request) { if (!logger) { return; } - const contentType = req.headers.get("content-type"); - const ct = contentType?.split(";")[0] || ""; + const contentType = req.headers.get('content-type'); + const ct = contentType?.split(';')[0] || ''; logger.group(`> Request: ${req.method} ${req.url}`); - logger.group("Headers:"); + logger.group('Headers:'); for (const [k, v] of req.headers.entries()) { logger.log(`${k}: ${v}`); } logger.groupEnd(); - logger.group("Body:"); + logger.group('Body:'); switch (true) { case jsonLikeContentTypeRE.test(ct): logger.log(await req.clone().json()); break; - case ct.startsWith("text/"): + case ct.startsWith('text/'): logger.log(await req.clone().text()); break; - case ct === "multipart/form-data": { + case ct === 'multipart/form-data': { const body = await req.clone().formData(); for (const [k, v] of body) { - const vlabel = v instanceof Blob ? "" : v; + const vlabel = v instanceof Blob ? '' : v; logger.log(`${k}: ${vlabel}`); } break; @@ -347,47 +314,42 @@ async function logRequest(logger: Logger | undefined, req: Request) { logger.groupEnd(); } -async function logResponse( - logger: Logger | undefined, - res: Response, - req: Request, -) { +async function logResponse(logger: Logger | undefined, res: Response, req: Request) { if (!logger) { return; } - const contentType = res.headers.get("content-type"); - const ct = contentType?.split(";")[0] || ""; + const contentType = res.headers.get('content-type'); + const ct = contentType?.split(';')[0] || ''; logger.group(`< Response: ${req.method} ${req.url}`); - logger.log("Status Code:", res.status, res.statusText); + logger.log('Status Code:', res.status, res.statusText); - logger.group("Headers:"); + logger.group('Headers:'); for (const [k, v] of res.headers.entries()) { logger.log(`${k}: ${v}`); } logger.groupEnd(); - logger.group("Body:"); + logger.group('Body:'); switch (true) { - case matchContentType(res, "application/json") - || jsonLikeContentTypeRE.test(ct) && !jsonlLikeContentTypeRE.test(ct): + case matchContentType(res, 'application/json') || + (jsonLikeContentTypeRE.test(ct) && !jsonlLikeContentTypeRE.test(ct)): logger.log(await res.clone().json()); break; - case matchContentType(res, "application/jsonl") - || jsonlLikeContentTypeRE.test(ct): + case matchContentType(res, 'application/jsonl') || jsonlLikeContentTypeRE.test(ct): logger.log(await res.clone().text()); break; - case matchContentType(res, "text/event-stream"): + case matchContentType(res, 'text/event-stream'): logger.log(`<${contentType}>`); break; - case matchContentType(res, "text/*"): + case matchContentType(res, 'text/*'): logger.log(await res.clone().text()); break; - case matchContentType(res, "multipart/form-data"): { + case matchContentType(res, 'multipart/form-data'): { const body = await res.clone().formData(); for (const [k, v] of body) { - const vlabel = v instanceof Blob ? "" : v; + const vlabel = v instanceof Blob ? '' : v; logger.log(`${k}: ${vlabel}`); } break; diff --git a/libs/internal-sdk/src/models/components/bulkupdatesubscriberpreferencesdto.ts b/libs/internal-sdk/src/models/components/bulkupdatesubscriberpreferencesdto.ts index 415f7aef321..09a318b3bf3 100644 --- a/libs/internal-sdk/src/models/components/bulkupdatesubscriberpreferencesdto.ts +++ b/libs/internal-sdk/src/models/components/bulkupdatesubscriberpreferencesdto.ts @@ -2,23 +2,72 @@ * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ -import * as z from "zod/v3"; +import * as z from 'zod/v3'; import { BulkUpdateSubscriberPreferenceItemDto, BulkUpdateSubscriberPreferenceItemDto$Outbound, BulkUpdateSubscriberPreferenceItemDto$outboundSchema, -} from "./bulkupdatesubscriberpreferenceitemdto.js"; +} from './bulkupdatesubscriberpreferenceitemdto.js'; + +/** + * Rich context object with id and optional data + */ +export type Context2 = { + id: string; + /** + * Optional additional context data + */ + data?: { [k: string]: any } | undefined; +}; + +export type BulkUpdateSubscriberPreferencesDtoContext = Context2 | string; export type BulkUpdateSubscriberPreferencesDto = { /** * Array of workflow preferences to update (maximum 100 items) */ preferences: Array; + context?: { [k: string]: Context2 | string } | undefined; }; +/** @internal */ +export type Context2$Outbound = { + id: string; + data?: { [k: string]: any } | undefined; +}; + +/** @internal */ +export const Context2$outboundSchema: z.ZodType = z.object({ + id: z.string(), + data: z.record(z.any()).optional(), +}); + +export function context2ToJSON(context2: Context2): string { + return JSON.stringify(Context2$outboundSchema.parse(context2)); +} + +/** @internal */ +export type BulkUpdateSubscriberPreferencesDtoContext$Outbound = Context2$Outbound | string; + +/** @internal */ +export const BulkUpdateSubscriberPreferencesDtoContext$outboundSchema: z.ZodType< + BulkUpdateSubscriberPreferencesDtoContext$Outbound, + z.ZodTypeDef, + BulkUpdateSubscriberPreferencesDtoContext +> = z.union([z.lazy(() => Context2$outboundSchema), z.string()]); + +export function bulkUpdateSubscriberPreferencesDtoContextToJSON( + bulkUpdateSubscriberPreferencesDtoContext: BulkUpdateSubscriberPreferencesDtoContext +): string { + return JSON.stringify( + BulkUpdateSubscriberPreferencesDtoContext$outboundSchema.parse(bulkUpdateSubscriberPreferencesDtoContext) + ); +} + /** @internal */ export type BulkUpdateSubscriberPreferencesDto$Outbound = { preferences: Array; + context?: { [k: string]: Context2$Outbound | string } | undefined; }; /** @internal */ @@ -28,14 +77,11 @@ export const BulkUpdateSubscriberPreferencesDto$outboundSchema: z.ZodType< BulkUpdateSubscriberPreferencesDto > = z.object({ preferences: z.array(BulkUpdateSubscriberPreferenceItemDto$outboundSchema), + context: z.record(z.union([z.lazy(() => Context2$outboundSchema), z.string()])).optional(), }); export function bulkUpdateSubscriberPreferencesDtoToJSON( - bulkUpdateSubscriberPreferencesDto: BulkUpdateSubscriberPreferencesDto, + bulkUpdateSubscriberPreferencesDto: BulkUpdateSubscriberPreferencesDto ): string { - return JSON.stringify( - BulkUpdateSubscriberPreferencesDto$outboundSchema.parse( - bulkUpdateSubscriberPreferencesDto, - ), - ); + return JSON.stringify(BulkUpdateSubscriberPreferencesDto$outboundSchema.parse(bulkUpdateSubscriberPreferencesDto)); } diff --git a/libs/internal-sdk/src/models/components/createchannelconnectionrequestdto.ts b/libs/internal-sdk/src/models/components/createchannelconnectionrequestdto.ts index 2c146c6cdd4..51873c61c6f 100644 --- a/libs/internal-sdk/src/models/components/createchannelconnectionrequestdto.ts +++ b/libs/internal-sdk/src/models/components/createchannelconnectionrequestdto.ts @@ -2,22 +2,14 @@ * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ -import * as z from "zod/v3"; -import { - AuthDto, - AuthDto$Outbound, - AuthDto$outboundSchema, -} from "./authdto.js"; -import { - WorkspaceDto, - WorkspaceDto$Outbound, - WorkspaceDto$outboundSchema, -} from "./workspacedto.js"; +import * as z from 'zod/v3'; +import { AuthDto, AuthDto$Outbound, AuthDto$outboundSchema } from './authdto.js'; +import { WorkspaceDto, WorkspaceDto$Outbound, WorkspaceDto$outboundSchema } from './workspacedto.js'; /** * Rich context object with id and optional data */ -export type Context2 = { +export type CreateChannelConnectionRequestDtoContext2 = { id: string; /** * Optional additional context data @@ -25,7 +17,7 @@ export type Context2 = { data?: { [k: string]: any } | undefined; }; -export type CreateChannelConnectionRequestDtoContext = Context2 | string; +export type CreateChannelConnectionRequestDtoContext = CreateChannelConnectionRequestDtoContext2 | string; export type CreateChannelConnectionRequestDto = { /** @@ -36,7 +28,7 @@ export type CreateChannelConnectionRequestDto = { * The subscriber ID to link the channel connection to */ subscriberId?: string | undefined; - context?: { [k: string]: Context2 | string } | undefined; + context?: { [k: string]: CreateChannelConnectionRequestDtoContext2 | string } | undefined; /** * The identifier of the integration to use for this channel connection. */ @@ -46,28 +38,32 @@ export type CreateChannelConnectionRequestDto = { }; /** @internal */ -export type Context2$Outbound = { +export type CreateChannelConnectionRequestDtoContext2$Outbound = { id: string; data?: { [k: string]: any } | undefined; }; /** @internal */ -export const Context2$outboundSchema: z.ZodType< - Context2$Outbound, +export const CreateChannelConnectionRequestDtoContext2$outboundSchema: z.ZodType< + CreateChannelConnectionRequestDtoContext2$Outbound, z.ZodTypeDef, - Context2 + CreateChannelConnectionRequestDtoContext2 > = z.object({ id: z.string(), data: z.record(z.any()).optional(), }); -export function context2ToJSON(context2: Context2): string { - return JSON.stringify(Context2$outboundSchema.parse(context2)); +export function createChannelConnectionRequestDtoContext2ToJSON( + createChannelConnectionRequestDtoContext2: CreateChannelConnectionRequestDtoContext2 +): string { + return JSON.stringify( + CreateChannelConnectionRequestDtoContext2$outboundSchema.parse(createChannelConnectionRequestDtoContext2) + ); } /** @internal */ export type CreateChannelConnectionRequestDtoContext$Outbound = - | Context2$Outbound + | CreateChannelConnectionRequestDtoContext2$Outbound | string; /** @internal */ @@ -75,16 +71,13 @@ export const CreateChannelConnectionRequestDtoContext$outboundSchema: z.ZodType< CreateChannelConnectionRequestDtoContext$Outbound, z.ZodTypeDef, CreateChannelConnectionRequestDtoContext -> = z.union([z.lazy(() => Context2$outboundSchema), z.string()]); +> = z.union([z.lazy(() => CreateChannelConnectionRequestDtoContext2$outboundSchema), z.string()]); export function createChannelConnectionRequestDtoContextToJSON( - createChannelConnectionRequestDtoContext: - CreateChannelConnectionRequestDtoContext, + createChannelConnectionRequestDtoContext: CreateChannelConnectionRequestDtoContext ): string { return JSON.stringify( - CreateChannelConnectionRequestDtoContext$outboundSchema.parse( - createChannelConnectionRequestDtoContext, - ), + CreateChannelConnectionRequestDtoContext$outboundSchema.parse(createChannelConnectionRequestDtoContext) ); } @@ -92,7 +85,11 @@ export function createChannelConnectionRequestDtoContextToJSON( export type CreateChannelConnectionRequestDto$Outbound = { identifier?: string | undefined; subscriberId?: string | undefined; - context?: { [k: string]: Context2$Outbound | string } | undefined; + context?: + | { + [k: string]: CreateChannelConnectionRequestDtoContext2$Outbound | string; + } + | undefined; integrationIdentifier: string; workspace: WorkspaceDto$Outbound; auth: AuthDto$Outbound; @@ -106,20 +103,16 @@ export const CreateChannelConnectionRequestDto$outboundSchema: z.ZodType< > = z.object({ identifier: z.string().optional(), subscriberId: z.string().optional(), - context: z.record( - z.union([z.lazy(() => Context2$outboundSchema), z.string()]), - ).optional(), + context: z + .record(z.union([z.lazy(() => CreateChannelConnectionRequestDtoContext2$outboundSchema), z.string()])) + .optional(), integrationIdentifier: z.string(), workspace: WorkspaceDto$outboundSchema, auth: AuthDto$outboundSchema, }); export function createChannelConnectionRequestDtoToJSON( - createChannelConnectionRequestDto: CreateChannelConnectionRequestDto, + createChannelConnectionRequestDto: CreateChannelConnectionRequestDto ): string { - return JSON.stringify( - CreateChannelConnectionRequestDto$outboundSchema.parse( - createChannelConnectionRequestDto, - ), - ); + return JSON.stringify(CreateChannelConnectionRequestDto$outboundSchema.parse(createChannelConnectionRequestDto)); } diff --git a/libs/internal-sdk/src/models/components/patchsubscriberpreferencesdto.ts b/libs/internal-sdk/src/models/components/patchsubscriberpreferencesdto.ts index ecee4508e7f..0638169ad6e 100644 --- a/libs/internal-sdk/src/models/components/patchsubscriberpreferencesdto.ts +++ b/libs/internal-sdk/src/models/components/patchsubscriberpreferencesdto.ts @@ -2,17 +2,26 @@ * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ -import * as z from "zod/v3"; +import * as z from 'zod/v3'; import { PatchPreferenceChannelsDto, PatchPreferenceChannelsDto$Outbound, PatchPreferenceChannelsDto$outboundSchema, -} from "./patchpreferencechannelsdto.js"; -import { - ScheduleDto, - ScheduleDto$Outbound, - ScheduleDto$outboundSchema, -} from "./scheduledto.js"; +} from './patchpreferencechannelsdto.js'; +import { ScheduleDto, ScheduleDto$Outbound, ScheduleDto$outboundSchema } from './scheduledto.js'; + +/** + * Rich context object with id and optional data + */ +export type Two = { + id: string; + /** + * Optional additional context data + */ + data?: { [k: string]: any } | undefined; +}; + +export type Context = Two | string; export type PatchSubscriberPreferencesDto = { /** @@ -27,13 +36,44 @@ export type PatchSubscriberPreferencesDto = { * Subscriber schedule */ schedule?: ScheduleDto | undefined; + context?: { [k: string]: Two | string } | undefined; }; +/** @internal */ +export type Two$Outbound = { + id: string; + data?: { [k: string]: any } | undefined; +}; + +/** @internal */ +export const Two$outboundSchema: z.ZodType = z.object({ + id: z.string(), + data: z.record(z.any()).optional(), +}); + +export function twoToJSON(two: Two): string { + return JSON.stringify(Two$outboundSchema.parse(two)); +} + +/** @internal */ +export type Context$Outbound = Two$Outbound | string; + +/** @internal */ +export const Context$outboundSchema: z.ZodType = z.union([ + z.lazy(() => Two$outboundSchema), + z.string(), +]); + +export function contextToJSON(context: Context): string { + return JSON.stringify(Context$outboundSchema.parse(context)); +} + /** @internal */ export type PatchSubscriberPreferencesDto$Outbound = { channels?: PatchPreferenceChannelsDto$Outbound | undefined; workflowId?: string | undefined; schedule?: ScheduleDto$Outbound | undefined; + context?: { [k: string]: Two$Outbound | string } | undefined; }; /** @internal */ @@ -45,14 +85,11 @@ export const PatchSubscriberPreferencesDto$outboundSchema: z.ZodType< channels: PatchPreferenceChannelsDto$outboundSchema.optional(), workflowId: z.string().optional(), schedule: ScheduleDto$outboundSchema.optional(), + context: z.record(z.union([z.lazy(() => Two$outboundSchema), z.string()])).optional(), }); export function patchSubscriberPreferencesDtoToJSON( - patchSubscriberPreferencesDto: PatchSubscriberPreferencesDto, + patchSubscriberPreferencesDto: PatchSubscriberPreferencesDto ): string { - return JSON.stringify( - PatchSubscriberPreferencesDto$outboundSchema.parse( - patchSubscriberPreferencesDto, - ), - ); + return JSON.stringify(PatchSubscriberPreferencesDto$outboundSchema.parse(patchSubscriberPreferencesDto)); } diff --git a/libs/internal-sdk/src/models/components/previewpayloaddto.ts b/libs/internal-sdk/src/models/components/previewpayloaddto.ts index 5e09c3ceb8b..e88d8f673ec 100644 --- a/libs/internal-sdk/src/models/components/previewpayloaddto.ts +++ b/libs/internal-sdk/src/models/components/previewpayloaddto.ts @@ -2,21 +2,21 @@ * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ -import * as z from "zod/v3"; -import { safeParse } from "../../lib/schemas.js"; -import { Result as SafeParseResult } from "../../types/fp.js"; -import { SDKValidationError } from "../errors/sdkvalidationerror.js"; +import * as z from 'zod/v3'; +import { safeParse } from '../../lib/schemas.js'; +import { Result as SafeParseResult } from '../../types/fp.js'; +import { SDKValidationError } from '../errors/sdkvalidationerror.js'; import { SubscriberResponseDtoOptional, SubscriberResponseDtoOptional$inboundSchema, SubscriberResponseDtoOptional$Outbound, SubscriberResponseDtoOptional$outboundSchema, -} from "./subscriberresponsedtooptional.js"; +} from './subscriberresponsedtooptional.js'; /** * Rich context object with id and optional data */ -export type Two = { +export type PreviewPayloadDtoContext2 = { id: string; /** * Optional additional context data @@ -24,7 +24,7 @@ export type Two = { data?: { [k: string]: any } | undefined; }; -export type Context = Two | string; +export type PreviewPayloadDtoContext = PreviewPayloadDtoContext2 | string; export type PreviewPayloadDto = { /** @@ -39,113 +39,101 @@ export type PreviewPayloadDto = { * Steps data */ steps?: { [k: string]: any } | undefined; - context?: { [k: string]: Two | string } | undefined; + context?: { [k: string]: PreviewPayloadDtoContext2 | string } | undefined; }; /** @internal */ -export const Two$inboundSchema: z.ZodType = z - .object({ +export const PreviewPayloadDtoContext2$inboundSchema: z.ZodType = + z.object({ id: z.string(), data: z.record(z.any()).optional(), }); /** @internal */ -export type Two$Outbound = { +export type PreviewPayloadDtoContext2$Outbound = { id: string; data?: { [k: string]: any } | undefined; }; /** @internal */ -export const Two$outboundSchema: z.ZodType = z - .object({ - id: z.string(), - data: z.record(z.any()).optional(), - }); +export const PreviewPayloadDtoContext2$outboundSchema: z.ZodType< + PreviewPayloadDtoContext2$Outbound, + z.ZodTypeDef, + PreviewPayloadDtoContext2 +> = z.object({ + id: z.string(), + data: z.record(z.any()).optional(), +}); -export function twoToJSON(two: Two): string { - return JSON.stringify(Two$outboundSchema.parse(two)); +export function previewPayloadDtoContext2ToJSON(previewPayloadDtoContext2: PreviewPayloadDtoContext2): string { + return JSON.stringify(PreviewPayloadDtoContext2$outboundSchema.parse(previewPayloadDtoContext2)); } -export function twoFromJSON( - jsonString: string, -): SafeParseResult { +export function previewPayloadDtoContext2FromJSON( + jsonString: string +): SafeParseResult { return safeParse( jsonString, - (x) => Two$inboundSchema.parse(JSON.parse(x)), - `Failed to parse 'Two' from JSON`, + (x) => PreviewPayloadDtoContext2$inboundSchema.parse(JSON.parse(x)), + `Failed to parse 'PreviewPayloadDtoContext2' from JSON` ); } /** @internal */ -export const Context$inboundSchema: z.ZodType = - z.union([z.lazy(() => Two$inboundSchema), z.string()]); +export const PreviewPayloadDtoContext$inboundSchema: z.ZodType = + z.union([z.lazy(() => PreviewPayloadDtoContext2$inboundSchema), z.string()]); /** @internal */ -export type Context$Outbound = Two$Outbound | string; +export type PreviewPayloadDtoContext$Outbound = PreviewPayloadDtoContext2$Outbound | string; /** @internal */ -export const Context$outboundSchema: z.ZodType< - Context$Outbound, +export const PreviewPayloadDtoContext$outboundSchema: z.ZodType< + PreviewPayloadDtoContext$Outbound, z.ZodTypeDef, - Context -> = z.union([z.lazy(() => Two$outboundSchema), z.string()]); + PreviewPayloadDtoContext +> = z.union([z.lazy(() => PreviewPayloadDtoContext2$outboundSchema), z.string()]); -export function contextToJSON(context: Context): string { - return JSON.stringify(Context$outboundSchema.parse(context)); +export function previewPayloadDtoContextToJSON(previewPayloadDtoContext: PreviewPayloadDtoContext): string { + return JSON.stringify(PreviewPayloadDtoContext$outboundSchema.parse(previewPayloadDtoContext)); } -export function contextFromJSON( - jsonString: string, -): SafeParseResult { +export function previewPayloadDtoContextFromJSON( + jsonString: string +): SafeParseResult { return safeParse( jsonString, - (x) => Context$inboundSchema.parse(JSON.parse(x)), - `Failed to parse 'Context' from JSON`, + (x) => PreviewPayloadDtoContext$inboundSchema.parse(JSON.parse(x)), + `Failed to parse 'PreviewPayloadDtoContext' from JSON` ); } /** @internal */ -export const PreviewPayloadDto$inboundSchema: z.ZodType< - PreviewPayloadDto, - z.ZodTypeDef, - unknown -> = z.object({ +export const PreviewPayloadDto$inboundSchema: z.ZodType = z.object({ subscriber: SubscriberResponseDtoOptional$inboundSchema.optional(), payload: z.record(z.any()).optional(), steps: z.record(z.any()).optional(), - context: z.record(z.union([z.lazy(() => Two$inboundSchema), z.string()])) - .optional(), + context: z.record(z.union([z.lazy(() => PreviewPayloadDtoContext2$inboundSchema), z.string()])).optional(), }); /** @internal */ export type PreviewPayloadDto$Outbound = { subscriber?: SubscriberResponseDtoOptional$Outbound | undefined; payload?: { [k: string]: any } | undefined; steps?: { [k: string]: any } | undefined; - context?: { [k: string]: Two$Outbound | string } | undefined; + context?: { [k: string]: PreviewPayloadDtoContext2$Outbound | string } | undefined; }; /** @internal */ -export const PreviewPayloadDto$outboundSchema: z.ZodType< - PreviewPayloadDto$Outbound, - z.ZodTypeDef, - PreviewPayloadDto -> = z.object({ - subscriber: SubscriberResponseDtoOptional$outboundSchema.optional(), - payload: z.record(z.any()).optional(), - steps: z.record(z.any()).optional(), - context: z.record(z.union([z.lazy(() => Two$outboundSchema), z.string()])) - .optional(), -}); +export const PreviewPayloadDto$outboundSchema: z.ZodType = + z.object({ + subscriber: SubscriberResponseDtoOptional$outboundSchema.optional(), + payload: z.record(z.any()).optional(), + steps: z.record(z.any()).optional(), + context: z.record(z.union([z.lazy(() => PreviewPayloadDtoContext2$outboundSchema), z.string()])).optional(), + }); -export function previewPayloadDtoToJSON( - previewPayloadDto: PreviewPayloadDto, -): string { - return JSON.stringify( - PreviewPayloadDto$outboundSchema.parse(previewPayloadDto), - ); +export function previewPayloadDtoToJSON(previewPayloadDto: PreviewPayloadDto): string { + return JSON.stringify(PreviewPayloadDto$outboundSchema.parse(previewPayloadDto)); } -export function previewPayloadDtoFromJSON( - jsonString: string, -): SafeParseResult { +export function previewPayloadDtoFromJSON(jsonString: string): SafeParseResult { return safeParse( jsonString, (x) => PreviewPayloadDto$inboundSchema.parse(JSON.parse(x)), - `Failed to parse 'PreviewPayloadDto' from JSON`, + `Failed to parse 'PreviewPayloadDto' from JSON` ); } diff --git a/libs/internal-sdk/src/models/operations/subscriberscontrollergetsubscriberpreferences.ts b/libs/internal-sdk/src/models/operations/subscriberscontrollergetsubscriberpreferences.ts index 0f02dbac26d..723fe204c65 100644 --- a/libs/internal-sdk/src/models/operations/subscriberscontrollergetsubscriberpreferences.ts +++ b/libs/internal-sdk/src/models/operations/subscriberscontrollergetsubscriberpreferences.ts @@ -2,24 +2,28 @@ * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ -import * as z from "zod/v3"; -import { remap as remap$ } from "../../lib/primitives.js"; -import { safeParse } from "../../lib/schemas.js"; -import { ClosedEnum } from "../../types/enums.js"; -import { Result as SafeParseResult } from "../../types/fp.js"; -import * as components from "../components/index.js"; -import { SDKValidationError } from "../errors/sdkvalidationerror.js"; +import * as z from 'zod/v3'; +import { remap as remap$ } from '../../lib/primitives.js'; +import { safeParse } from '../../lib/schemas.js'; +import { ClosedEnum } from '../../types/enums.js'; +import { Result as SafeParseResult } from '../../types/fp.js'; +import * as components from '../components/index.js'; +import { SDKValidationError } from '../errors/sdkvalidationerror.js'; export const Criticality = { - Critical: "critical", - NonCritical: "nonCritical", - All: "all", + Critical: 'critical', + NonCritical: 'nonCritical', + All: 'all', } as const; export type Criticality = ClosedEnum; export type SubscribersControllerGetSubscriberPreferencesRequest = { subscriberId: string; criticality?: Criticality | undefined; + /** + * Context keys for filtering preferences (e.g., ["tenant:acme"]) + */ + contextKeys?: Array | undefined; /** * A header for idempotency purposes */ @@ -32,71 +36,67 @@ export type SubscribersControllerGetSubscriberPreferencesResponse = { }; /** @internal */ -export const Criticality$outboundSchema: z.ZodNativeEnum = z - .nativeEnum(Criticality); +export const Criticality$outboundSchema: z.ZodNativeEnum = z.nativeEnum(Criticality); /** @internal */ export type SubscribersControllerGetSubscriberPreferencesRequest$Outbound = { subscriberId: string; criticality: string; - "idempotency-key"?: string | undefined; + contextKeys?: Array | undefined; + 'idempotency-key'?: string | undefined; }; /** @internal */ -export const SubscribersControllerGetSubscriberPreferencesRequest$outboundSchema: - z.ZodType< - SubscribersControllerGetSubscriberPreferencesRequest$Outbound, - z.ZodTypeDef, - SubscribersControllerGetSubscriberPreferencesRequest - > = z.object({ +export const SubscribersControllerGetSubscriberPreferencesRequest$outboundSchema: z.ZodType< + SubscribersControllerGetSubscriberPreferencesRequest$Outbound, + z.ZodTypeDef, + SubscribersControllerGetSubscriberPreferencesRequest +> = z + .object({ subscriberId: z.string(), - criticality: Criticality$outboundSchema.default("nonCritical"), + criticality: Criticality$outboundSchema.default('nonCritical'), + contextKeys: z.array(z.string()).optional(), idempotencyKey: z.string().optional(), - }).transform((v) => { + }) + .transform((v) => { return remap$(v, { - idempotencyKey: "idempotency-key", + idempotencyKey: 'idempotency-key', }); }); export function subscribersControllerGetSubscriberPreferencesRequestToJSON( - subscribersControllerGetSubscriberPreferencesRequest: - SubscribersControllerGetSubscriberPreferencesRequest, + subscribersControllerGetSubscriberPreferencesRequest: SubscribersControllerGetSubscriberPreferencesRequest ): string { return JSON.stringify( SubscribersControllerGetSubscriberPreferencesRequest$outboundSchema.parse( - subscribersControllerGetSubscriberPreferencesRequest, - ), + subscribersControllerGetSubscriberPreferencesRequest + ) ); } /** @internal */ -export const SubscribersControllerGetSubscriberPreferencesResponse$inboundSchema: - z.ZodType< - SubscribersControllerGetSubscriberPreferencesResponse, - z.ZodTypeDef, - unknown - > = z.object({ +export const SubscribersControllerGetSubscriberPreferencesResponse$inboundSchema: z.ZodType< + SubscribersControllerGetSubscriberPreferencesResponse, + z.ZodTypeDef, + unknown +> = z + .object({ Headers: z.record(z.array(z.string())).default({}), Result: components.GetSubscriberPreferencesDto$inboundSchema, - }).transform((v) => { + }) + .transform((v) => { return remap$(v, { - "Headers": "headers", - "Result": "result", + Headers: 'headers', + Result: 'result', }); }); export function subscribersControllerGetSubscriberPreferencesResponseFromJSON( - jsonString: string, -): SafeParseResult< - SubscribersControllerGetSubscriberPreferencesResponse, - SDKValidationError -> { + jsonString: string +): SafeParseResult { return safeParse( jsonString, - (x) => - SubscribersControllerGetSubscriberPreferencesResponse$inboundSchema.parse( - JSON.parse(x), - ), - `Failed to parse 'SubscribersControllerGetSubscriberPreferencesResponse' from JSON`, + (x) => SubscribersControllerGetSubscriberPreferencesResponse$inboundSchema.parse(JSON.parse(x)), + `Failed to parse 'SubscribersControllerGetSubscriberPreferencesResponse' from JSON` ); } diff --git a/libs/internal-sdk/src/react-query/subscribersPreferencesList.core.ts b/libs/internal-sdk/src/react-query/subscribersPreferencesList.core.ts index 4bf231bacf2..b1f4d63f344 100644 --- a/libs/internal-sdk/src/react-query/subscribersPreferencesList.core.ts +++ b/libs/internal-sdk/src/react-query/subscribersPreferencesList.core.ts @@ -2,77 +2,49 @@ * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ -import { - QueryClient, - QueryFunctionContext, - QueryKey, -} from "@tanstack/react-query"; -import { NovuCore } from "../core.js"; -import { subscribersPreferencesList } from "../funcs/subscribersPreferencesList.js"; -import { combineSignals } from "../lib/primitives.js"; -import { RequestOptions } from "../lib/sdks.js"; -import * as operations from "../models/operations/index.js"; -import { unwrapAsync } from "../types/fp.js"; -export type SubscribersPreferencesListQueryData = - operations.SubscribersControllerGetSubscriberPreferencesResponse; +import { QueryClient, QueryFunctionContext, QueryKey } from '@tanstack/react-query'; +import { NovuCore } from '../core.js'; +import { subscribersPreferencesList } from '../funcs/subscribersPreferencesList.js'; +import { combineSignals } from '../lib/primitives.js'; +import { RequestOptions } from '../lib/sdks.js'; +import * as operations from '../models/operations/index.js'; +import { unwrapAsync } from '../types/fp.js'; +export type SubscribersPreferencesListQueryData = operations.SubscribersControllerGetSubscriberPreferencesResponse; export function prefetchSubscribersPreferencesList( queryClient: QueryClient, client$: NovuCore, - subscriberId: string, - criticality?: operations.Criticality | undefined, - idempotencyKey?: string | undefined, - options?: RequestOptions, + request: operations.SubscribersControllerGetSubscriberPreferencesRequest, + options?: RequestOptions ): Promise { return queryClient.prefetchQuery({ - ...buildSubscribersPreferencesListQuery( - client$, - subscriberId, - criticality, - idempotencyKey, - options, - ), + ...buildSubscribersPreferencesListQuery(client$, request, options), }); } export function buildSubscribersPreferencesListQuery( client$: NovuCore, - subscriberId: string, - criticality?: operations.Criticality | undefined, - idempotencyKey?: string | undefined, - options?: RequestOptions, + request: operations.SubscribersControllerGetSubscriberPreferencesRequest, + options?: RequestOptions ): { queryKey: QueryKey; - queryFn: ( - context: QueryFunctionContext, - ) => Promise; + queryFn: (context: QueryFunctionContext) => Promise; } { return { - queryKey: queryKeySubscribersPreferencesList(subscriberId, { - criticality, - idempotencyKey, + queryKey: queryKeySubscribersPreferencesList(request.subscriberId, { + criticality: request.criticality, + contextKeys: request.contextKeys, + idempotencyKey: request.idempotencyKey, }), - queryFn: async function subscribersPreferencesListQueryFn( - ctx, - ): Promise { - const sig = combineSignals( - ctx.signal, - options?.signal, - options?.fetchOptions?.signal, - ); + queryFn: async function subscribersPreferencesListQueryFn(ctx): Promise { + const sig = combineSignals(ctx.signal, options?.signal, options?.fetchOptions?.signal); const mergedOptions = { ...options?.fetchOptions, ...options, signal: sig, }; - return unwrapAsync(subscribersPreferencesList( - client$, - subscriberId, - criticality, - idempotencyKey, - mergedOptions, - )); + return unwrapAsync(subscribersPreferencesList(client$, request, mergedOptions)); }, }; } @@ -81,8 +53,9 @@ export function queryKeySubscribersPreferencesList( subscriberId: string, parameters: { criticality?: operations.Criticality | undefined; + contextKeys?: Array | undefined; idempotencyKey?: string | undefined; - }, + } ): QueryKey { - return ["@novu/api", "Preferences", "list", subscriberId, parameters]; + return ['@novu/api', 'Preferences', 'list', subscriberId, parameters]; } diff --git a/libs/internal-sdk/src/react-query/subscribersPreferencesList.ts b/libs/internal-sdk/src/react-query/subscribersPreferencesList.ts index 16ef4ced8ef..938e09ddff8 100644 --- a/libs/internal-sdk/src/react-query/subscribersPreferencesList.ts +++ b/libs/internal-sdk/src/react-query/subscribersPreferencesList.ts @@ -5,24 +5,20 @@ import { InvalidateQueryFilters, QueryClient, - useQuery, UseQueryResult, - useSuspenseQuery, UseSuspenseQueryResult, -} from "@tanstack/react-query"; -import * as operations from "../models/operations/index.js"; -import { useNovuContext } from "./_context.js"; -import { - QueryHookOptions, - SuspenseQueryHookOptions, - TupleToPrefixes, -} from "./_types.js"; + useQuery, + useSuspenseQuery, +} from '@tanstack/react-query'; +import * as operations from '../models/operations/index.js'; +import { useNovuContext } from './_context.js'; +import { QueryHookOptions, SuspenseQueryHookOptions, TupleToPrefixes } from './_types.js'; import { buildSubscribersPreferencesListQuery, prefetchSubscribersPreferencesList, queryKeySubscribersPreferencesList, SubscribersPreferencesListQueryData, -} from "./subscribersPreferencesList.core.js"; +} from './subscribersPreferencesList.core.js'; export { buildSubscribersPreferencesListQuery, prefetchSubscribersPreferencesList, @@ -38,20 +34,12 @@ export { * This API returns all five channels preferences for all workflows and global preferences. */ export function useSubscribersPreferencesList( - subscriberId: string, - criticality?: operations.Criticality | undefined, - idempotencyKey?: string | undefined, - options?: QueryHookOptions, + request: operations.SubscribersControllerGetSubscriberPreferencesRequest, + options?: QueryHookOptions ): UseQueryResult { const client = useNovuContext(); return useQuery({ - ...buildSubscribersPreferencesListQuery( - client, - subscriberId, - criticality, - idempotencyKey, - options, - ), + ...buildSubscribersPreferencesListQuery(client, request, options), ...options, }); } @@ -64,20 +52,12 @@ export function useSubscribersPreferencesList( * This API returns all five channels preferences for all workflows and global preferences. */ export function useSubscribersPreferencesListSuspense( - subscriberId: string, - criticality?: operations.Criticality | undefined, - idempotencyKey?: string | undefined, - options?: SuspenseQueryHookOptions, + request: operations.SubscribersControllerGetSubscriberPreferencesRequest, + options?: SuspenseQueryHookOptions ): UseSuspenseQueryResult { const client = useNovuContext(); return useSuspenseQuery({ - ...buildSubscribersPreferencesListQuery( - client, - subscriberId, - criticality, - idempotencyKey, - options, - ), + ...buildSubscribersPreferencesListQuery(client, request, options), ...options, }); } @@ -88,10 +68,11 @@ export function setSubscribersPreferencesListData( subscriberId: string, parameters: { criticality?: operations.Criticality | undefined; + contextKeys?: Array | undefined; idempotencyKey?: string | undefined; }, ], - data: SubscribersPreferencesListQueryData, + data: SubscribersPreferencesListQueryData ): SubscribersPreferencesListQueryData | undefined { const key = queryKeySubscribersPreferencesList(...queryKeyBase); @@ -105,24 +86,25 @@ export function invalidateSubscribersPreferencesList( subscriberId: string, parameters: { criticality?: operations.Criticality | undefined; + contextKeys?: Array | undefined; idempotencyKey?: string | undefined; }, ] >, - filters?: Omit, + filters?: Omit ): Promise { return client.invalidateQueries({ ...filters, - queryKey: ["@novu/api", "Preferences", "list", ...queryKeyBase], + queryKey: ['@novu/api', 'Preferences', 'list', ...queryKeyBase], }); } export function invalidateAllSubscribersPreferencesList( client: QueryClient, - filters?: Omit, + filters?: Omit ): Promise { return client.invalidateQueries({ ...filters, - queryKey: ["@novu/api", "Preferences", "list"], + queryKey: ['@novu/api', 'Preferences', 'list'], }); } diff --git a/libs/internal-sdk/src/sdk/preferences.ts b/libs/internal-sdk/src/sdk/preferences.ts index ad035f18fe5..a111a93b756 100644 --- a/libs/internal-sdk/src/sdk/preferences.ts +++ b/libs/internal-sdk/src/sdk/preferences.ts @@ -2,13 +2,13 @@ * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ -import { subscribersPreferencesBulkUpdate } from "../funcs/subscribersPreferencesBulkUpdate.js"; -import { subscribersPreferencesList } from "../funcs/subscribersPreferencesList.js"; -import { subscribersPreferencesUpdate } from "../funcs/subscribersPreferencesUpdate.js"; -import { ClientSDK, RequestOptions } from "../lib/sdks.js"; -import * as components from "../models/components/index.js"; -import * as operations from "../models/operations/index.js"; -import { unwrapAsync } from "../types/fp.js"; +import { subscribersPreferencesBulkUpdate } from '../funcs/subscribersPreferencesBulkUpdate.js'; +import { subscribersPreferencesList } from '../funcs/subscribersPreferencesList.js'; +import { subscribersPreferencesUpdate } from '../funcs/subscribersPreferencesUpdate.js'; +import { ClientSDK, RequestOptions } from '../lib/sdks.js'; +import * as components from '../models/components/index.js'; +import * as operations from '../models/operations/index.js'; +import { unwrapAsync } from '../types/fp.js'; export class Preferences extends ClientSDK { /** @@ -19,18 +19,10 @@ export class Preferences extends ClientSDK { * This API returns all five channels preferences for all workflows and global preferences. */ async list( - subscriberId: string, - criticality?: operations.Criticality | undefined, - idempotencyKey?: string | undefined, - options?: RequestOptions, + request: operations.SubscribersControllerGetSubscriberPreferencesRequest, + options?: RequestOptions ): Promise { - return unwrapAsync(subscribersPreferencesList( - this, - subscriberId, - criticality, - idempotencyKey, - options, - )); + return unwrapAsync(subscribersPreferencesList(this, request, options)); } /** @@ -45,17 +37,11 @@ export class Preferences extends ClientSDK { patchSubscriberPreferencesDto: components.PatchSubscriberPreferencesDto, subscriberId: string, idempotencyKey?: string | undefined, - options?: RequestOptions, - ): Promise< - operations.SubscribersControllerUpdateSubscriberPreferencesResponse - > { - return unwrapAsync(subscribersPreferencesUpdate( - this, - patchSubscriberPreferencesDto, - subscriberId, - idempotencyKey, - options, - )); + options?: RequestOptions + ): Promise { + return unwrapAsync( + subscribersPreferencesUpdate(this, patchSubscriberPreferencesDto, subscriberId, idempotencyKey, options) + ); } /** @@ -66,20 +52,13 @@ export class Preferences extends ClientSDK { * This API allows updating multiple workflow preferences in a single request. */ async bulkUpdate( - bulkUpdateSubscriberPreferencesDto: - components.BulkUpdateSubscriberPreferencesDto, + bulkUpdateSubscriberPreferencesDto: components.BulkUpdateSubscriberPreferencesDto, subscriberId: string, idempotencyKey?: string | undefined, - options?: RequestOptions, - ): Promise< - operations.SubscribersControllerBulkUpdateSubscriberPreferencesResponse - > { - return unwrapAsync(subscribersPreferencesBulkUpdate( - this, - bulkUpdateSubscriberPreferencesDto, - subscriberId, - idempotencyKey, - options, - )); + options?: RequestOptions + ): Promise { + return unwrapAsync( + subscribersPreferencesBulkUpdate(this, bulkUpdateSubscriberPreferencesDto, subscriberId, idempotencyKey, options) + ); } }