feat(notifications): add Notifications and Subscriptions services#500
feat(notifications): add Notifications and Subscriptions services#500sarthak688 wants to merge 2 commits into
Conversation
Review findingsTwo issues found this run:
|
Co-Authored-By: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5eeeac4 to
f433f25
Compare
| GET_PUBLISHERS: `${SUBSCRIPTION_API_BASE}/api/v1/UserSubscription/GetPublishers`, | ||
| // Intentional duplicate of GET_ALL: same URL, POST vs GET differentiates the operation. | ||
| UPDATE_TOPIC: `${SUBSCRIPTION_API_BASE}/api/v1/UserSubscription`, | ||
| UPDATE_TOPIC: `${SUBSCRIPTION_API_BASE}/api/v1/UserSubscription`, |
There was a problem hiding this comment.
Copy-paste artifact from the previous fix: this line is a duplicate UPDATE_TOPIC key — it should be GET_SUPPORTED_CHANNELS. In a TypeScript as const object literal, duplicate keys are a compile error (and oxlint's no-dupe-keys rule will reject it). More critically, GET_SUPPORTED_CHANNELS is completely absent from the object, so subscriptions.ts and the unit tests will both fail typecheck with:
Property 'GET_SUPPORTED_CHANNELS' does not exist on type …
Replace this line with the correct constant (use the actual live endpoint URL — the placeholder below follows the naming pattern):
| UPDATE_TOPIC: `${SUBSCRIPTION_API_BASE}/api/v1/UserSubscription`, | |
| GET_SUPPORTED_CHANNELS: `${SUBSCRIPTION_API_BASE}/api/v1/UserSubscription/GetSupportedChannelStatus`, |
Review findingsOne new issue found this run: notification.ts line 33 — The second UPDATE_TOPIC line is a copy-paste artifact from the previous fix. It creates a duplicate key in the SUBSCRIPTION_ENDPOINTS object (rejected by both TypeScript and oxlint) and leaves GET_SUPPORTED_CHANNELS entirely absent — causing typecheck to fail because subscriptions.ts and the unit tests both reference SUBSCRIPTION_ENDPOINTS.GET_SUPPORTED_CHANNELS. Replace the duplicate line with the correct GET_SUPPORTED_CHANNELS entry pointing at the live endpoint URL. |
| // Intentional duplicate of GET_ALL: same URL, POST vs GET differentiates the operation. | ||
| UPDATE_TOPIC: `${SUBSCRIPTION_API_BASE}/api/v1/UserSubscription`, | ||
| UPDATE_TOPIC: `${SUBSCRIPTION_API_BASE}/api/v1/UserSubscription`, |
There was a problem hiding this comment.
The fix-up for the prior review comment accidentally duplicated UPDATE_TOPIC and dropped GET_SUPPORTED_CHANNELS at the same time. Two problems:
UPDATE_TOPICappears twice — the second entry is dead code (second value wins in JS, but they're identical so the runtime value happens to be correct; still a defect).GET_SUPPORTED_CHANNELSis missing —subscriptions.ts:129referencesSUBSCRIPTION_ENDPOINTS.GET_SUPPORTED_CHANNELS, which no longer exists in this object. TypeScript should reject this withProperty 'GET_SUPPORTED_CHANNELS' does not exist on type ….
| // Intentional duplicate of GET_ALL: same URL, POST vs GET differentiates the operation. | |
| UPDATE_TOPIC: `${SUBSCRIPTION_API_BASE}/api/v1/UserSubscription`, | |
| UPDATE_TOPIC: `${SUBSCRIPTION_API_BASE}/api/v1/UserSubscription`, | |
| GET_SUPPORTED_CHANNELS: `${SUBSCRIPTION_API_BASE}/api/v1/UserSubscription/GetSupportedChannelStatus`, | |
| // Intentional duplicate of GET_ALL: same URL, POST vs GET differentiates the operation. | |
| UPDATE_TOPIC: `${SUBSCRIPTION_API_BASE}/api/v1/UserSubscription`, |
Review findingsOne new issue posted this run: src/utils/constants/endpoints/notification.ts:31-33 - The fix-up commit that addressed the prior UPDATE_TOPIC comment accidentally introduced a duplicate key and dropped GET_SUPPORTED_CHANNELS. subscriptions.ts:129 references SUBSCRIPTION_ENDPOINTS.GET_SUPPORTED_CHANNELS, which no longer exists in the object. Suggestion posted inline. |
| GET_PUBLISHERS: `${SUBSCRIPTION_API_BASE}/api/v1/UserSubscription/GetPublishers`, | ||
| // Intentional duplicate of GET_ALL: same URL, POST vs GET differentiates the operation. | ||
| UPDATE_TOPIC: `${SUBSCRIPTION_API_BASE}/api/v1/UserSubscription`, | ||
| UPDATE_TOPIC: `${SUBSCRIPTION_API_BASE}/api/v1/UserSubscription`, |
There was a problem hiding this comment.
Two bugs were introduced while applying the previous review fix:
-
GET_SUPPORTED_CHANNELSis missing —SubscriptionService.getSupportedChannels()callsSUBSCRIPTION_ENDPOINTS.GET_SUPPORTED_CHANNELS, but that key is not defined in this object. TypeScript will reject this with TS2339. The endpoint URL is in the PR description:…/UserSubscription/GetSupportedChannelStatus. -
Duplicate
UPDATE_TOPICkey —UPDATE_TOPICis now declared twice (lines 32–33 are identical). In JavaScript/TypeScript only the last value survives; the first line is dead code.
| GET_PUBLISHERS: `${SUBSCRIPTION_API_BASE}/api/v1/UserSubscription/GetPublishers`, | |
| // Intentional duplicate of GET_ALL: same URL, POST vs GET differentiates the operation. | |
| UPDATE_TOPIC: `${SUBSCRIPTION_API_BASE}/api/v1/UserSubscription`, | |
| UPDATE_TOPIC: `${SUBSCRIPTION_API_BASE}/api/v1/UserSubscription`, | |
| GET_PUBLISHERS: `${SUBSCRIPTION_API_BASE}/api/v1/UserSubscription/GetPublishers`, | |
| GET_SUPPORTED_CHANNELS: `${SUBSCRIPTION_API_BASE}/api/v1/UserSubscription/GetSupportedChannelStatus`, | |
| // Intentional duplicate of GET_ALL: same URL, POST vs GET differentiates the operation. | |
| UPDATE_TOPIC: `${SUBSCRIPTION_API_BASE}/api/v1/UserSubscription`, |
Review findingsOne issue found this run: src/utils/constants/endpoints/notification.ts lines 30-33 (#500 (comment)) — Two bugs were introduced while applying the previous round fix: (1) GET_SUPPORTED_CHANNELS is missing from SUBSCRIPTION_ENDPOINTS but subscriptions.ts references it — TypeScript will reject this with TS2339. (2) UPDATE_TOPIC is declared twice; the first occurrence is dead code. |
Every public method on Notifications and Subscriptions now takes the tenant GUID as its first positional argument, sent via the X-UIPATH-Internal-TenantId header. Callers no longer configure tenantId on the SDK; the header is built per-call inline. Unit and integration tests + ServiceModel JSDoc updated to match. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| * const unread = await notifications.getAll({ filter: 'isRead eq false' }); | ||
| * | ||
| * const subscriptions = new Subscriptions(sdk); | ||
| * const { publishers } = await subscriptions.getAll(); |
There was a problem hiding this comment.
The module-level @example is missing the required tenantId first argument on both calls. As-written the example won't compile — getAll and getAll both have tenantId: string as their first positional parameter.
| * const unread = await notifications.getAll({ filter: 'isRead eq false' }); | |
| * | |
| * const subscriptions = new Subscriptions(sdk); | |
| * const { publishers } = await subscriptions.getAll(); | |
| * const notifications = new Notifications(sdk); | |
| * const unread = await notifications.getAll('<tenantId>', { filter: 'isRead eq false' }); | |
| * | |
| * const subscriptions = new Subscriptions(sdk); | |
| * const { publishers } = await subscriptions.getAll('<tenantId>'); |
Review findingsOne new issue found this run: src/services/notification/index.ts:20-23 (#500 (comment)) — The module-level @example block omits the required tenantId first argument on both notifications.getAll() and subscriptions.getAll() calls. The example will not compile since tenantId: string is a required positional parameter on both methods. Suggestion posted inline. |
Onboards the user-facing Notification platform as a new modular subpath
@uipath/uipath-typescript/notificationswith two services:Notifications(inbox) andSubscriptions(preferences). 15 methods total, no folder context, no method binding. Spec source: Design Document — Notification Service CLI Tool & SDK Integration for Coding Agents.Methods Added
Notifications(6 methods)notifications.getAll()getAll<T extends NotificationGetAllOptions>(tenantId: string, options?: T): Promise<T extends HasPaginationOptions<T> ? PaginatedResponse<NotificationGetResponse> : NonPaginatedResponse<NotificationGetResponse>>notifications.markRead()markRead(tenantId: string, notificationIds: string[]): Promise<NotificationUpdateReadResponse>notifications.markUnread()markUnread(tenantId: string, notificationIds: string[]): Promise<NotificationUpdateReadResponse>notifications.markAllRead()markAllRead(tenantId: string): Promise<NotificationMarkAllReadResponse>notifications.deleteNotifications()deleteNotifications(tenantId: string, notificationIds: string[]): Promise<NotificationDeleteResponse>notifications.deleteAll()deleteAll(tenantId: string): Promise<NotificationDeleteAllResponse>Subscriptions(9 methods)subscriptions.getAll()getAll(tenantId: string, options?: SubscriptionGetAllOptions): Promise<SubscriptionGetResponse>subscriptions.getPublishers()getPublishers(tenantId: string, options?: SubscriptionGetPublishersOptions): Promise<SubscriptionGetResponse>subscriptions.getSupportedChannels()getSupportedChannels(tenantId: string): Promise<SubscriptionGetSupportedChannelsResponse>subscriptions.updateTopic()updateTopic(tenantId: string, subscriptions: TopicSubscriptionUpdate[]): Promise<SubscriptionUpdateTopicResponse>subscriptions.updateCategory()updateCategory(tenantId: string, subscriptions: CategorySubscriptionUpdate[]): Promise<SubscriptionUpdateCategoryResponse>subscriptions.updatePublisher()updatePublisher(tenantId: string, subscriptions: PublisherSubscriptionUpdate[]): Promise<SubscriptionUpdatePublisherResponse>subscriptions.updateTopicGroup()updateTopicGroup(tenantId: string, subscriptions: TopicGroupSubscriptionUpdate[]): Promise<SubscriptionUpdateTopicGroupResponse>subscriptions.updateMode()updateMode(tenantId: string, publisherId: string, mode: AllowedMode): Promise<SubscriptionUpdateModeResponse>subscriptions.reset()reset(tenantId: string, publisherId: string): Promise<SubscriptionGetResponse>Endpoints Called
notifications.getAll()notificationservice_/notificationserviceapi/odata/v1/NotificationEntryNotificationServicenotifications.markRead()/markUnread()/markAllRead()notificationservice_/notificationserviceapi/odata/v1/NotificationEntry/UiPath.NotificationService.Api.UpdateReadNotificationServicenotifications.deleteNotifications()/deleteAll()notificationservice_/notificationserviceapi/odata/v1/NotificationEntry/UiPath.NotificationService.Api.DeleteBulkNotificationServicesubscriptions.getAll()notificationservice_/usersubscriptionservice/api/v1/UserSubscriptionNotificationServicesubscriptions.getPublishers()notificationservice_/usersubscriptionservice/api/v1/UserSubscription/GetPublishersNotificationServicesubscriptions.getSupportedChannels()notificationservice_/usersubscriptionservice/api/v1/UserSubscription/GetSupportedChannelStatusNotificationServicesubscriptions.updateTopic()notificationservice_/usersubscriptionservice/api/v1/UserSubscriptionNotificationServicesubscriptions.updateCategory()notificationservice_/usersubscriptionservice/api/v1/UserSubscription/CategorySubscriptionNotificationServicesubscriptions.updatePublisher()notificationservice_/usersubscriptionservice/api/v1/UserSubscription/PublisherSubscriptionNotificationServicesubscriptions.updateTopicGroup()notificationservice_/usersubscriptionservice/api/v1/UserSubscription/TopicGroupSubscriptionNotificationServicesubscriptions.updateMode()notificationservice_/usersubscriptionservice/api/v1/UserSubscription/UpdateModeNotificationServicesubscriptions.reset()notificationservice_/usersubscriptionservice/api/v1/UserSubscription/ResetNotificationServiceBaseService— UserContext auth, no folder header.notifications.getAll()supports OData pagination viaPaginationHelpers.getAll($top,$skip,$count) and OData query options (filter,orderby).$selectis not exposed (the API returns HTTP 500 when$selectis supplied — verified live).@track(...)telemetry decorator.Critical design point #1:
tenantIdas a per-call argumentThe Notification API does not include the tenant in the URL path. Instead, it identifies the acting tenant via the
X-UIPATH-Internal-TenantIdheader, which expects a tenant GUID (not the configuredtenantName). Two design options were considered:tenantName→tenantIdin an SDK-internal cache (calling a portal lookup endpoint on first NS use). Rejected — added a runtime API dependency, a cache key/eviction strategy, and a failure mode (resolver outage poisons every NS call) for what is a static value the caller already has.tenantIdas the first positional argument on every NS/Subscriptions method. Chosen — explicit, deterministic, mirrors how folder-scoped Orchestrator services already requirefolderIdfrom the caller. The SDK forwardstenantIdintoX-UIPATH-Internal-TenantIdviacreateHeaders()on each call.The argument is required and positional, in line with the convention from
agent_docs/conventions.md:Critical design point #2: URL traversal for tenant-less routing
The notification service routes at the organization level — its URLs do NOT include a tenant segment, unlike most UiPath services.
ApiClientalways inserts${orgName}/${tenantName}/${path}, so the endpoint constants use a../prefix to collapse the tenant segment:${orgName}/${tenantName}/../notificationservice_/...resolves vianew URL()normalization to${orgName}/notificationservice_/.... TheNOTIFICATION_BASEJSDoc documents this in detail. Verified live:alpha.uipath.com/testmanagerdev/notificationservice_/...→ HTTP 200alpha.uipath.com/testmanagerdev/TestManagerDevTenant/notificationservice_/...→ 302/portal_/unregisteredExample Usage
API Response vs SDK Response
Transform pipeline (
notifications.getAll()only)PaginationHelpers.getAll→transformFn: stripInternalNotificationFields→ returned to consumer.The API already returns camelCase, so
pascalToCamelCaseKeys()is not applied. There are no semantic field renames inNotificationEntryMap. The only transform is dropping internal/transport-layer fields.Field-strip (dropped from
NotificationGetResponse)entityOrgName,entityTenantNameserviceRegistryNamemessageTemplateKey,messageVersionpublicationIdcorrelationIdpartitionKeyreadOnly: truein spec)All other 5 service-layer methods on Notifications and 9 on Subscriptions are pass-through (no transform) — they wrap the API response in
OperationResponse<T>({ success, data }) where applicable.Sample SDK Responses
notifications.getAll(tenantId, { pageSize: 1 }){ "items": [ { "id": "9a3b0db5-9b8b-44a7-9b6a-08de0a17b4e2", "message": "A new model is now available for your product.", "isRead": false, "publisherName": "LlmGateway", "publisherId": "adc041d6-3098-4766-ae45-08dd3ac994aa", "topicName": "New model available for product", "topicKeyName": "New.Model.Available", "topicId": "66e76b72-5ef1-4567-6a00-08dd3ac994ba", "userId": "9ABFE2B8-F30F-4917-9F7F-A91D28F7B23C", "userEmail": null, "tenantId": null, "priority": "Low", "category": "Info", "messageParam": null, "redirectionUrl": "https://alpha.uipath.com/.../portal_/admin/ai-trust-layer/llm-configurations", "publishedOn": 1780413050 } ], "totalCount": 500, "hasNextPage": true, "supportsPageJump": true, "currentPage": 1 }Note:
publishedOnis Unix epoch seconds (not milliseconds). Internal fields (entityOrgName,partitionKey,correlationId, etc.) are stripped before the SDK consumer sees the response.subscriptions.getSupportedChannels(tenantId){ "channels": [ { "name": "Email", "isEnabled": true }, { "name": "Slack", "isEnabled": false }, { "name": "Teams", "isEnabled": false } ] }InAppis intentionally not listed by the API — it is always implicitly available.subscriptions.getPublishers(tenantId, { name: 'Apps' }){ "publishers": [ { "id": "3b5c8128-8af0-46ec-eb1d-08da31909e0f", "name": "Apps", "displayName": "Apps", "topics": [ { "id": "b3f9b2fd-122f-4d40-3d88-08da31909e1a", "name": "Apps.Shared", "displayName": "Apps Shared", "description": "Apps is Shared", "group": "Apps Activities" }, { "id": "7a647eb5-381e-4712-55de-08db6be7681e", "name": "Apps.Cloned", "displayName": "App Duplicated", "description": "App is Duplicated", "group": "Apps Activities" }, { "id": "334659c0-d908-4078-7620-08db7be3133c", "name": "Apps.CloneFailed", "displayName": "App Duplication failed", "description": "App Duplication has failed", "group": "Apps Activities" } ] } ] }Note: the discovery endpoint returns only
id/name/displayName/description/groupon each topic. UsegetAll()for the full topic shape with subscription state (isSubscribed,modes,category, etc.).subscriptions.getAll(tenantId, { publishers: ['Apps'] }){ "publishers": [ { "id": "3b5c8128-8af0-46ec-eb1d-08da31909e0f", "name": "Apps", "displayName": "Apps", "redirectionUrl": "https://alpha.uipath.com/", "retentionDays": 30, "addToSummary": false, "isUserOptin": true, "modes": [ { "name": "InApp", "isActive": true }, { "name": "Email", "isActive": true } ], "topics": [ { "id": "b3f9b2fd-122f-4d40-3d88-08da31909e1a", "name": "Apps.Shared", "displayName": "Apps Shared", "description": "Apps is Shared", "group": "Apps Activities", "parentGroup": null, "category": "Success", "retentionDays": 30, "orderingSequence": 1, "isSubscribed": true, "isMandatory": false, "isVisible": true, "isDefault": true, "isAllowedToBeDispatchedInBatch": false, "isInfrequent": false, "modes": [ { "name": "InApp", "isSubscribed": true, "isSubscribedByDefault": true }, { "name": "Email", "isSubscribed": true, "isSubscribedByDefault": true } ] } ] } ] }Verification
npm run typechecknpm run lintnpm run test:unitnpm run builddist/notifications/index.{mjs,cjs,d.ts}producedLive API curl confirmed all 5 read endpoints + write endpoints work against
alpha/testmanagerdev. The../URL-traversal trick is the only unconventional element and is documented in the JSDoc onNOTIFICATION_BASE.Files
src/utils/constants/endpoints/notification.ts(new),src/utils/constants/endpoints/base.ts(NOTIFICATION_BASE),src/utils/constants/endpoints/index.tssrc/models/notification/notifications.types.ts,subscriptions.types.tssrc/models/notification/notifications.internal-types.tssrc/models/notification/notifications.models.ts,subscriptions.models.ts,index.tssrc/services/notification/notifications.ts,subscriptions.ts,index.tspackage.json(subpath./notifications),rollup.config.js(notificationsentry)tests/unit/services/notification/notifications.test.ts(14 tests),subscriptions.test.ts(22 tests)tests/integration/shared/notification/notifications.integration.test.ts(6 tests),subscriptions.integration.test.ts(8 tests) —v1mode onlytests/integration/config/test-config.ts(notificationTenantId),tests/integration/config/unified-setup.ts,tests/.env.integration.example(NOTIFICATION_TEST_TENANT_ID)tests/utils/constants/notification.ts(incl.TENANT_ID),tests/utils/mocks/notification.ts(+ barrel updates)docs/oauth-scopes.md,docs/pagination.md,mkdocs.ymlFollow-up
notificationservice_/should be added toALLOWED_API_PATTERNSinapps-dev-toolsfor browser-based consumers usingalpha.api.uipath.com. Local Vite proxies still work for SDK E2E tests.ApiClientis currently solved with the../trick. A cleaner long-term fix would be askipTenantoption onApiClient/BaseService— left as future refactor since this PR's scope is the notification SDK only.🤖 Generated with Claude Code