Skip to content

feat(api-service): context aware subscription preferences admin API fixes NV-7059#9855

Merged
ChmaraX merged 11 commits intonextfrom
nv-7059-context-support-in-topics-v2-admin-api
Jan 19, 2026
Merged

feat(api-service): context aware subscription preferences admin API fixes NV-7059#9855
ChmaraX merged 11 commits intonextfrom
nv-7059-context-support-in-topics-v2-admin-api

Conversation

@ChmaraX
Copy link
Contributor

@ChmaraX ChmaraX commented Jan 19, 2026

What changed? Why was the change needed?

This pull request adds support for context-aware topic subscriptions, allowing subscriptions to be scoped and filtered by context keys (such as tenant or project). This enables more granular management and querying of topic subscriptions based on contextual information. The changes include updates to DTOs, API request/response validation, filtering logic, and comprehensive end-to-end tests for context-aware behaviors.

The most important changes are:

Context-aware subscription support:

  • Added contextKeys field to relevant DTOs (SubscriptionDetailsResponseDto, SubscriptionResponseDto, SubscriptionDto, TopicSubscriptionResponseDto) to represent the context that scopes a subscription. This is now included in API responses and documented via Swagger decorators. [1] [2] [3] [4]
  • Updated subscription creation logic to accept a context payload, validate it, and map it to contextKeys for storage and downstream use. [1] [2] [3] [4] [5] [6]

Filtering and querying by context:

  • Enhanced query DTOs (ListSubscriberSubscriptionsQueryDto, ListTopicSubscriptionsQueryDto) to support filtering subscriptions by exact contextKeys (order-insensitive), with appropriate transformation and validation. [1] [2]
  • Updated the subscription retrieval logic to handle context-aware filtering: if contextKeys is undefined, all subscriptions are returned; if an empty array, only subscriptions with no context are matched; otherwise, only exact context matches are returned for security.
  • Modified controller and query handling to pass contextKeys from API requests to the business logic layer.

End-to-end tests for context features:

  • Added comprehensive E2E tests covering creation of subscriptions with and without context, ensuring separate subscriptions for different contexts, and verifying retrieval and filtering behaviors.
  • Added E2E tests for listing topic subscriptions, including context-aware filtering and order-insensitive matching of context keys.

@linear
Copy link

linear bot commented Jan 19, 2026

@netlify
Copy link

netlify bot commented Jan 19, 2026

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

Name Link
🔨 Latest commit 97cb840
🔍 Latest deploy log https://app.netlify.com/projects/dashboard-v2-novu-staging/deploys/696e4f4da2127f0008fddad1

@github-actions github-actions bot changed the title feat(api-service): context aware subscription preferences admin API feat(api-service): context aware subscription preferences admin API fixes NV-7059 Jan 19, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 19, 2026

Walkthrough

This pull request introduces context-aware subscription support across the API and internal SDK. The change adds optional contextKeys fields to subscription-related DTOs and introduces a context mechanism for creating subscriptions. List operations are extended with contextKeys filtering parameters, and subscription retrieval logic is updated to filter results based on provided context keys. Repository methods are augmented to support context-based filtering during pagination. Additionally, end-to-end tests are added to verify context-aware subscription creation, retrieval, and filtering behavior across multiple scenarios.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: adding context-aware subscription preferences support to the admin API and fixing a specific issue (NV-7059).
Description check ✅ Passed The pull request description is clearly related to the changeset and provides meaningful detail about the context-aware subscription feature implementation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
libs/dal/src/repositories/topic/topic-subscribers.repository.ts (1)

250-285: Handle empty contextKeys to avoid unfiltered cross-context results.

if (contextKeys) skips empty arrays, but callers can pass [] (e.g., via query transform) to mean “no context”. That currently drops the filter and returns all contexts.

🐛 Proposed fix
-    if (contextKeys) {
+    if (contextKeys !== undefined) {
       Object.assign(query, this.buildContextExactMatchQuery(contextKeys));
     }
apps/api/src/app/topics-v2/e2e/create-topic-subscriptions.e2e.ts (1)

16-29: Restore IS_CONTEXT_PREFERENCES_ENABLED after the suite to prevent leakage.

The env var is set in before but never reset, which can make unrelated tests context-aware unexpectedly.

🔧 Suggested fix
 describe('Create topic subscriptions - /v2/topics/:topicKey/subscriptions (POST) `#novu-v2`', async () => {
   let session: UserSession;
   let novuClient: Novu;
   let subscriber1: SubscriberEntity;
   let subscriber2: SubscriberEntity;
   let subscriber3: SubscriberEntity;
   let topicSubscribersRepository: TopicSubscribersRepository;
+  let previousContextPreferencesEnabled: string | undefined;

   before(async () => {
-    (process.env as Record<string, string>).IS_CONTEXT_PREFERENCES_ENABLED = 'true';
+    previousContextPreferencesEnabled = process.env.IS_CONTEXT_PREFERENCES_ENABLED;
+    process.env.IS_CONTEXT_PREFERENCES_ENABLED = 'true';
     ...
   });
+
+  after(() => {
+    if (previousContextPreferencesEnabled === undefined) {
+      delete process.env.IS_CONTEXT_PREFERENCES_ENABLED;
+      return;
+    }
+    process.env.IS_CONTEXT_PREFERENCES_ENABLED = previousContextPreferencesEnabled;
+  });
🧹 Nitpick comments (10)
apps/dashboard/src/components/header-navigation/no-changes-modal.tsx (1)

6-10: Consider removing unused targetEnvironment prop.

The targetEnvironment prop is defined but never used in the component. If it's not needed for future functionality, consider removing it to keep the interface clean.

♻️ Proposed refactor
 type NoChangesModalProps = {
   isOpen: boolean;
   onClose: () => void;
-  targetEnvironment?: IEnvironment;
 };
libs/internal-sdk/src/models/operations/subscriberscontrollerlistsubscribertopics.ts (1)

60-63: Consider validating the documented contextKeys format.

Line 61 documents "type:id", but the schema currently accepts any string. Adding a light regex/refine would fail fast on obvious bad inputs and improve error messages. Since this file is generated, the change should be applied in the generator/spec template.

Proposed schema refinement
-    contextKeys: z.array(z.string()).optional(),
+    contextKeys: z
+      .array(z.string().regex(/.+:.+/, 'Expected "type:id"'))
+      .optional(),

Also applies to: 90-111

apps/api/src/app/topics-v2/dtos/delete-topic-subscriptions-response.dto.ts (1)

109-115: Consider adding validation decorators for consistency.

The contextKeys field lacks validation decorators (@IsOptional, @IsArray, @IsString({ each: true })) that are present on other optional fields like identifier (lines 92-94). While this DTO appears to be used primarily for response serialization, adding validation decorators would maintain consistency with the codebase patterns and provide defense-in-depth.

Suggested improvement
+  `@IsOptional`()
+  `@IsArray`()
+  `@IsString`({ each: true })
   `@ApiProperty`({
     description: 'Context keys that scope this subscription (e.g., tenant:org-a, project:proj-123)',
     example: ['tenant:org-a', 'project:proj-123'],
     type: [String],
     required: false,
   })
   contextKeys?: string[];
apps/api/src/app/subscriptions/usecases/get-subscription/get-subscription.usecase.ts (1)

178-186: Consider reusing the feature flag result to avoid duplicate async calls.

The IS_CONTEXT_PREFERENCES_ENABLED flag is checked twice per request—once in execute() (line 40) and again in buildContextExactMatchQuery(). Consider passing the flag value as a parameter to avoid the redundant async call.

♻️ Suggested refactor
- private async buildContextExactMatchQuery(
+ private buildContextExactMatchQuery(
    contextKeys: string[] | undefined,
-   organizationId: string
+   isContextEnabled: boolean
  ): Promise<Record<string, unknown>> {
-   const useContextFiltering = await this.featureFlagsService.getFlag({
-     key: FeatureFlagsKeysEnum.IS_CONTEXT_PREFERENCES_ENABLED,
-     defaultValue: false,
-     organization: { _id: organizationId },
-   });
-
-   if (!useContextFiltering) {
+   if (!isContextEnabled) {
      return {}; // FF OFF: no context filtering (pre-feature behavior)
    }
    // ... rest of method
  }

Then call it with:

const contextQuery = this.buildContextExactMatchQuery(subscription.contextKeys, isContextEnabled);
apps/api/src/app/shared/dtos/subscriptions/create-subscriptions-response.dto.ts (1)

178-183: Consider adding validation decorators for consistency.

Other optional array fields in this DTO (e.g., preferences at lines 174-176) use @IsArray() and @IsOptional() decorators. For consistency, consider adding them to contextKeys as well.

♻️ Suggested addition
  `@ApiPropertyOptional`({
    description: 'Context keys that scope this subscription (e.g., tenant:org-a, project:proj-123)',
    example: ['tenant:org-a', 'project:proj-123'],
    type: [String],
  })
+ `@IsArray`()
+ `@IsOptional`()
  contextKeys?: string[];
apps/api/src/app/subscriptions/usecases/update-subscription/update-subscription.usecase.ts (1)

140-165: Consider caching the feature flag result to reduce redundant calls.

The IS_CONTEXT_PREFERENCES_ENABLED flag is checked multiple times per request (in execute(), updatePreferencesForSubscription(), and fetchPreferencesForSubscription()). Consider fetching it once in execute() and passing it down to avoid redundant async calls.

apps/api/src/app/shared/dtos/subscription-details-response.dto.ts (1)

38-44: Consider adding validation decorators for consistency with other fields.

Other fields in this DTO use validation decorators (e.g., preferences has @IsArray, @IsOptional). For consistency, consider adding them to contextKeys.

♻️ Suggested addition
  `@ApiPropertyOptional`({
    description: 'Context keys that scope this subscription (e.g., tenant:org-a, project:proj-123)',
    example: ['tenant:org-a', 'project:proj-123'],
    type: [String],
  })
+ `@IsArray`()
+ `@IsOptional`()
  contextKeys?: string[];
apps/api/src/app/subscriptions/usecases/create-subscriptions/create-subscriptions.usecase.ts (1)

622-648: Consider consolidating redundant feature flag checks.

The IS_CONTEXT_PREFERENCES_ENABLED flag is checked both in the execute method (lines 59-68) and again here in resolveContexts. The second check is effectively unreachable when the first check returns undefined for contextKeys, making this check redundant.

While not incorrect, consolidating to a single check point would improve clarity.

apps/api/src/app/topics-v2/e2e/list-topic-subscriptions.e2e.ts (1)

16-18: Consider cleaning up the environment variable after tests.

Setting process.env.IS_CONTEXT_PREFERENCES_ENABLED directly without cleanup in an after hook could potentially leak to other test files running in the same process. Consider adding cleanup:

♻️ Suggested cleanup
 before(async () => {
   (process.env as Record<string, string>).IS_CONTEXT_PREFERENCES_ENABLED = 'true';
   // ... rest of setup
 });
+
+ after(() => {
+   delete (process.env as Record<string, string>).IS_CONTEXT_PREFERENCES_ENABLED;
+ });
libs/internal-sdk/src/react-query/topicsSubscriptionsList.core.ts (1)

58-72: Normalize contextKeys order in the query key

contextKeys are documented as order-insensitive, so different permutations would currently generate different cache keys. Sorting before constructing the key avoids cache fragmentation for equivalent requests.

♻️ Proposed update to normalize contextKeys
 export function queryKeyTopicsSubscriptionsList(
   topicKey: string,
   parameters: {
     after?: string | undefined;
     before?: string | undefined;
     limit?: number | undefined;
     orderDirection?: operations.TopicsControllerListTopicSubscriptionsQueryParamOrderDirection | undefined;
     orderBy?: string | undefined;
     includeCursor?: boolean | undefined;
     subscriberId?: string | undefined;
     contextKeys?: Array<string> | undefined;
     idempotencyKey?: string | undefined;
   }
 ): QueryKey {
-  return ['@novu/api', 'Subscriptions', 'list', topicKey, parameters];
+  const normalizedContextKeys = parameters.contextKeys
+    ? [...parameters.contextKeys].sort()
+    : undefined;
+
+  return [
+    '@novu/api',
+    'Subscriptions',
+    'list',
+    topicKey,
+    {
+      ...parameters,
+      contextKeys: normalizedContextKeys,
+    },
+  ];
 }

@ChmaraX ChmaraX merged commit 60b3658 into next Jan 19, 2026
35 checks passed
@ChmaraX ChmaraX deleted the nv-7059-context-support-in-topics-v2-admin-api branch January 19, 2026 15:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant