@brightchain/brightchain-api-lib 0.23.25 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/lib/__tests__/fixtures/mock-backend-brightchain-member.d.ts.map +1 -1
- package/src/lib/__tests__/fixtures/mock-backend-brightchain-member.js +11 -0
- package/src/lib/__tests__/fixtures/mock-backend-brightchain-member.js.map +1 -1
- package/src/lib/__tests__/fixtures/mocked-model.js +63 -60
- package/src/lib/__tests__/fixtures/mocked-model.js.map +1 -1
- package/src/lib/application.d.ts +1 -1
- package/src/lib/application.d.ts.map +1 -1
- package/src/lib/application.js +103 -34
- package/src/lib/application.js.map +1 -1
- package/src/lib/auth/aclEnforcedAvailability.js +4 -0
- package/src/lib/auth/aclEnforcedAvailability.js.map +1 -1
- package/src/lib/auth/aclEnforcedBlockStore.js +7 -0
- package/src/lib/auth/aclEnforcedBlockStore.js.map +1 -1
- package/src/lib/auth/ecdsaNodeAuthenticator.js +1 -0
- package/src/lib/auth/ecdsaNodeAuthenticator.js.map +1 -1
- package/src/lib/auth/poolAclBootstrap.js +2 -0
- package/src/lib/auth/poolAclBootstrap.js.map +1 -1
- package/src/lib/auth/poolAclStore.js +2 -1
- package/src/lib/auth/poolAclStore.js.map +1 -1
- package/src/lib/auth/poolAclUpdater.js +4 -0
- package/src/lib/auth/poolAclUpdater.js.map +1 -1
- package/src/lib/availability/availabilityMetrics.js +43 -45
- package/src/lib/availability/availabilityMetrics.js.map +1 -1
- package/src/lib/availability/availabilityService.js +26 -20
- package/src/lib/availability/availabilityService.js.map +1 -1
- package/src/lib/availability/blockRegistry.js +46 -25
- package/src/lib/availability/blockRegistry.js.map +1 -1
- package/src/lib/availability/discoveryProtocol.js +10 -8
- package/src/lib/availability/discoveryProtocol.js.map +1 -1
- package/src/lib/availability/gossipService.js +56 -45
- package/src/lib/availability/gossipService.js.map +1 -1
- package/src/lib/availability/heartbeatMonitor.js +22 -20
- package/src/lib/availability/heartbeatMonitor.js.map +1 -1
- package/src/lib/availability/poolDiscoveryService.js +7 -1
- package/src/lib/availability/poolDiscoveryService.js.map +1 -1
- package/src/lib/availability/quorumGossipHandler.js +13 -6
- package/src/lib/availability/quorumGossipHandler.js.map +1 -1
- package/src/lib/availability/reconciliationService.js +18 -12
- package/src/lib/availability/reconciliationService.js.map +1 -1
- package/src/lib/blockFetch/blockFetcher.js +14 -8
- package/src/lib/blockFetch/blockFetcher.js.map +1 -1
- package/src/lib/blockFetch/fetchQueue.js +9 -7
- package/src/lib/blockFetch/fetchQueue.js.map +1 -1
- package/src/lib/blockFetch/httpBlockFetchTransport.js +2 -0
- package/src/lib/blockFetch/httpBlockFetchTransport.js.map +1 -1
- package/src/lib/browserKeyring.js +5 -2
- package/src/lib/browserKeyring.js.map +1 -1
- package/src/lib/controllers/api/blocks.d.ts.map +1 -1
- package/src/lib/controllers/api/blocks.js +9 -3
- package/src/lib/controllers/api/blocks.js.map +1 -1
- package/src/lib/controllers/api/brighthub/connectionController.d.ts +80 -0
- package/src/lib/controllers/api/brighthub/connectionController.d.ts.map +1 -0
- package/src/lib/controllers/api/brighthub/connectionController.js +890 -0
- package/src/lib/controllers/api/brighthub/connectionController.js.map +1 -0
- package/src/lib/controllers/api/brighthub/index.d.ts +9 -0
- package/src/lib/controllers/api/brighthub/index.d.ts.map +1 -0
- package/src/lib/controllers/api/brighthub/index.js +12 -0
- package/src/lib/controllers/api/brighthub/index.js.map +1 -0
- package/src/lib/controllers/api/brighthub/messagingController.d.ts +84 -0
- package/src/lib/controllers/api/brighthub/messagingController.d.ts.map +1 -0
- package/src/lib/controllers/api/brighthub/messagingController.js +1077 -0
- package/src/lib/controllers/api/brighthub/messagingController.js.map +1 -0
- package/src/lib/controllers/api/brighthub/notificationController.d.ts +89 -0
- package/src/lib/controllers/api/brighthub/notificationController.d.ts.map +1 -0
- package/src/lib/controllers/api/brighthub/notificationController.js +444 -0
- package/src/lib/controllers/api/brighthub/notificationController.js.map +1 -0
- package/src/lib/controllers/api/brighthub/postController.d.ts +75 -0
- package/src/lib/controllers/api/brighthub/postController.d.ts.map +1 -0
- package/src/lib/controllers/api/brighthub/postController.js +388 -0
- package/src/lib/controllers/api/brighthub/postController.js.map +1 -0
- package/src/lib/controllers/api/brighthub/timelineController.d.ts +74 -0
- package/src/lib/controllers/api/brighthub/timelineController.d.ts.map +1 -0
- package/src/lib/controllers/api/brighthub/timelineController.js +418 -0
- package/src/lib/controllers/api/brighthub/timelineController.js.map +1 -0
- package/src/lib/controllers/api/brightpass.d.ts.map +1 -1
- package/src/lib/controllers/api/brightpass.js +46 -4
- package/src/lib/controllers/api/brightpass.js.map +1 -1
- package/src/lib/controllers/api/cbl.js +1 -0
- package/src/lib/controllers/api/cbl.js.map +1 -1
- package/src/lib/controllers/api/channels.js +2 -2
- package/src/lib/controllers/api/channels.js.map +1 -1
- package/src/lib/controllers/api/conversations.js +1 -1
- package/src/lib/controllers/api/conversations.js.map +1 -1
- package/src/lib/controllers/api/docs.js +2 -1
- package/src/lib/controllers/api/docs.js.map +1 -1
- package/src/lib/controllers/api/emails.d.ts.map +1 -1
- package/src/lib/controllers/api/emails.js +16 -2
- package/src/lib/controllers/api/emails.js.map +1 -1
- package/src/lib/controllers/api/explodingMessages.js +2 -4
- package/src/lib/controllers/api/explodingMessages.js.map +1 -1
- package/src/lib/controllers/api/groups.js +2 -2
- package/src/lib/controllers/api/groups.js.map +1 -1
- package/src/lib/controllers/api/health.d.ts +9 -2
- package/src/lib/controllers/api/health.d.ts.map +1 -1
- package/src/lib/controllers/api/health.js +48 -6
- package/src/lib/controllers/api/health.js.map +1 -1
- package/src/lib/controllers/api/index.d.ts +1 -0
- package/src/lib/controllers/api/index.d.ts.map +1 -1
- package/src/lib/controllers/api/index.js +1 -0
- package/src/lib/controllers/api/index.js.map +1 -1
- package/src/lib/controllers/api/introspection.js +2 -0
- package/src/lib/controllers/api/introspection.js.map +1 -1
- package/src/lib/controllers/api/messages.js +1 -1
- package/src/lib/controllers/api/messages.js.map +1 -1
- package/src/lib/controllers/api/nodes.js +6 -6
- package/src/lib/controllers/api/nodes.js.map +1 -1
- package/src/lib/controllers/api/quorum.d.ts +1 -1
- package/src/lib/controllers/api/quorum.d.ts.map +1 -1
- package/src/lib/controllers/api/quorum.js +3 -2
- package/src/lib/controllers/api/quorum.js.map +1 -1
- package/src/lib/controllers/api/scbl.js +3 -0
- package/src/lib/controllers/api/scbl.js.map +1 -1
- package/src/lib/controllers/api/sessions.d.ts +18 -2
- package/src/lib/controllers/api/sessions.d.ts.map +1 -1
- package/src/lib/controllers/api/sessions.js +60 -3
- package/src/lib/controllers/api/sessions.js.map +1 -1
- package/src/lib/controllers/api/sync.js +4 -4
- package/src/lib/controllers/api/sync.js.map +1 -1
- package/src/lib/controllers/api/user.d.ts +2 -0
- package/src/lib/controllers/api/user.d.ts.map +1 -1
- package/src/lib/controllers/api/user.js +95 -3
- package/src/lib/controllers/api/user.js.map +1 -1
- package/src/lib/controllers/identity/deviceController.js +2 -2
- package/src/lib/controllers/identity/deviceController.js.map +1 -1
- package/src/lib/controllers/identity/directoryController.js +1 -1
- package/src/lib/controllers/identity/directoryController.js.map +1 -1
- package/src/lib/controllers/identity/identityProofController.js +4 -6
- package/src/lib/controllers/identity/identityProofController.js.map +1 -1
- package/src/lib/databaseInit.d.ts +3 -3
- package/src/lib/databaseInit.js +5 -5
- package/src/lib/databaseInit.js.map +1 -1
- package/src/lib/datastore/block-document-store.d.ts.map +1 -1
- package/src/lib/datastore/block-document-store.js +18 -6
- package/src/lib/datastore/block-document-store.js.map +1 -1
- package/src/lib/datastore/document-store.d.ts.map +1 -1
- package/src/lib/datastore/memory-document-store.js +5 -2
- package/src/lib/datastore/memory-document-store.js.map +1 -1
- package/src/lib/encryption/encryptedMetadataService.js +2 -0
- package/src/lib/encryption/encryptedMetadataService.js.map +1 -1
- package/src/lib/encryption/encryptionAwareReplication.js +3 -0
- package/src/lib/encryption/encryptionAwareReplication.js.map +1 -1
- package/src/lib/encryption/errors.d.ts.map +1 -1
- package/src/lib/encryption/errors.js +8 -0
- package/src/lib/encryption/errors.js.map +1 -1
- package/src/lib/encryption/poolKeyManager.js +2 -0
- package/src/lib/encryption/poolKeyManager.js.map +1 -1
- package/src/lib/environment.js +10 -0
- package/src/lib/environment.js.map +1 -1
- package/src/lib/errors/express-validation.js +1 -0
- package/src/lib/errors/express-validation.js.map +1 -1
- package/src/lib/errors/invalid-backup-code-version.js +1 -0
- package/src/lib/errors/invalid-backup-code-version.js.map +1 -1
- package/src/lib/errors/memberIndexSchemaValidationError.js +1 -0
- package/src/lib/errors/memberIndexSchemaValidationError.js.map +1 -1
- package/src/lib/errors/missing-validated-data.js +2 -0
- package/src/lib/errors/missing-validated-data.js.map +1 -1
- package/src/lib/errors/token-not-found.js +1 -0
- package/src/lib/errors/token-not-found.js.map +1 -1
- package/src/lib/errors/typed-error-local.js +3 -0
- package/src/lib/errors/typed-error-local.js.map +1 -1
- package/src/lib/interfaces/brightpass/index.d.ts +1 -1
- package/src/lib/interfaces/brightpass/index.d.ts.map +1 -1
- package/src/lib/interfaces/environment.d.ts +1 -1
- package/src/lib/interfaces/environment.d.ts.map +1 -1
- package/src/lib/interfaces/member/memberProfileResponse.d.ts.map +1 -1
- package/src/lib/interfaces/member/operational.d.ts.map +1 -1
- package/src/lib/interfaces/member-init-config.d.ts +1 -1
- package/src/lib/interfaces/member-init-config.d.ts.map +1 -1
- package/src/lib/interfaces/responses/api-backup-codes-response.d.ts.map +1 -1
- package/src/lib/interfaces/responses/api-challenge-response.d.ts.map +1 -1
- package/src/lib/interfaces/responses/api-code-count-response.d.ts.map +1 -1
- package/src/lib/interfaces/responses/api-detailed-health-response.d.ts.map +1 -1
- package/src/lib/interfaces/responses/api-discover-block-response.d.ts.map +1 -1
- package/src/lib/interfaces/responses/api-express-validation-error.d.ts.map +1 -1
- package/src/lib/interfaces/responses/api-get-block-response.d.ts.map +1 -1
- package/src/lib/interfaces/responses/api-get-node-response.d.ts.map +1 -1
- package/src/lib/interfaces/responses/api-health-response.d.ts.map +1 -1
- package/src/lib/interfaces/responses/api-list-nodes-response.d.ts.map +1 -1
- package/src/lib/interfaces/responses/api-login-response.d.ts.map +1 -1
- package/src/lib/interfaces/responses/api-members-response.d.ts.map +1 -1
- package/src/lib/interfaces/responses/api-mnemonic-response.d.ts.map +1 -1
- package/src/lib/interfaces/responses/api-reconcile-response.d.ts.map +1 -1
- package/src/lib/interfaces/responses/api-register-node-response.d.ts.map +1 -1
- package/src/lib/interfaces/responses/api-registration-response.d.ts.map +1 -1
- package/src/lib/interfaces/responses/api-replicate-block-response.d.ts.map +1 -1
- package/src/lib/interfaces/responses/api-request-user-response.d.ts.map +1 -1
- package/src/lib/interfaces/responses/api-store-block-response.d.ts.map +1 -1
- package/src/lib/interfaces/responses/api-store-cbl-response.d.ts.map +1 -1
- package/src/lib/interfaces/responses/api-sync-request-response.d.ts.map +1 -1
- package/src/lib/interfaces/responses/brighthub/api-connection-response.d.ts +21 -0
- package/src/lib/interfaces/responses/brighthub/api-connection-response.d.ts.map +1 -0
- package/src/lib/interfaces/responses/brighthub/api-connection-response.js +3 -0
- package/src/lib/interfaces/responses/brighthub/api-connection-response.js.map +1 -0
- package/src/lib/interfaces/responses/brighthub/api-messaging-response.d.ts +39 -0
- package/src/lib/interfaces/responses/brighthub/api-messaging-response.d.ts.map +1 -0
- package/src/lib/interfaces/responses/brighthub/api-messaging-response.js +3 -0
- package/src/lib/interfaces/responses/brighthub/api-messaging-response.js.map +1 -0
- package/src/lib/interfaces/responses/brighthub/api-notification-response.d.ts +30 -0
- package/src/lib/interfaces/responses/brighthub/api-notification-response.d.ts.map +1 -0
- package/src/lib/interfaces/responses/brighthub/api-notification-response.js +3 -0
- package/src/lib/interfaces/responses/brighthub/api-notification-response.js.map +1 -0
- package/src/lib/interfaces/responses/brighthub/api-post-response.d.ts +21 -0
- package/src/lib/interfaces/responses/brighthub/api-post-response.d.ts.map +1 -0
- package/src/lib/interfaces/responses/brighthub/api-post-response.js +3 -0
- package/src/lib/interfaces/responses/brighthub/api-post-response.js.map +1 -0
- package/src/lib/interfaces/responses/brighthub/api-timeline-response.d.ts +14 -0
- package/src/lib/interfaces/responses/brighthub/api-timeline-response.d.ts.map +1 -0
- package/src/lib/interfaces/responses/brighthub/api-timeline-response.js +3 -0
- package/src/lib/interfaces/responses/brighthub/api-timeline-response.js.map +1 -0
- package/src/lib/interfaces/responses/brighthub/api-user-profile-response.d.ts +10 -0
- package/src/lib/interfaces/responses/brighthub/api-user-profile-response.d.ts.map +1 -0
- package/src/lib/interfaces/responses/brighthub/api-user-profile-response.js +3 -0
- package/src/lib/interfaces/responses/brighthub/api-user-profile-response.js.map +1 -0
- package/src/lib/interfaces/responses/brighthub/index.d.ts +13 -0
- package/src/lib/interfaces/responses/brighthub/index.d.ts.map +1 -0
- package/src/lib/interfaces/responses/brighthub/index.js +9 -0
- package/src/lib/interfaces/responses/brighthub/index.js.map +1 -0
- package/src/lib/interfaces/responses/index.d.ts +1 -0
- package/src/lib/interfaces/responses/index.d.ts.map +1 -1
- package/src/lib/interfaces/storage/storedDocumentTypes.d.ts +1 -1
- package/src/lib/interfaces/storage/storedDocumentTypes.js +1 -1
- package/src/lib/interfaces/storage/userRoleSchema.d.ts.map +1 -1
- package/src/lib/interfaces/storage/userRoleSchema.js +1 -3
- package/src/lib/interfaces/storage/userRoleSchema.js.map +1 -1
- package/src/lib/middlewares.js +31 -31
- package/src/lib/middlewares.js.map +1 -1
- package/src/lib/nodeKeyring.js +2 -0
- package/src/lib/nodeKeyring.js.map +1 -1
- package/src/lib/plugins/brightchain-database-plugin.d.ts +14 -14
- package/src/lib/plugins/brightchain-database-plugin.d.ts.map +1 -1
- package/src/lib/plugins/brightchain-database-plugin.js +36 -34
- package/src/lib/plugins/brightchain-database-plugin.js.map +1 -1
- package/src/lib/routers/api.d.ts +62 -1
- package/src/lib/routers/api.d.ts.map +1 -1
- package/src/lib/routers/api.js +123 -2
- package/src/lib/routers/api.js.map +1 -1
- package/src/lib/routers/app.d.ts.map +1 -1
- package/src/lib/routers/app.js +10 -1
- package/src/lib/routers/app.js.map +1 -1
- package/src/lib/routers/base.js +2 -0
- package/src/lib/routers/base.js.map +1 -1
- package/src/lib/secureEnclaveKeyring.js +4 -2
- package/src/lib/secureEnclaveKeyring.js.map +1 -1
- package/src/lib/services/auth.d.ts.map +1 -1
- package/src/lib/services/auth.js +11 -2
- package/src/lib/services/auth.js.map +1 -1
- package/src/lib/services/base.js +1 -0
- package/src/lib/services/base.js.map +1 -1
- package/src/lib/services/blockServiceFactory.js +2 -0
- package/src/lib/services/blockServiceFactory.js.map +1 -1
- package/src/lib/services/blockStore.js +3 -2
- package/src/lib/services/blockStore.js.map +1 -1
- package/src/lib/services/blocks.js +1 -0
- package/src/lib/services/blocks.js.map +1 -1
- package/src/lib/services/brightChainBackupCodeService.d.ts +114 -0
- package/src/lib/services/brightChainBackupCodeService.d.ts.map +1 -0
- package/src/lib/services/brightChainBackupCodeService.js +303 -0
- package/src/lib/services/brightChainBackupCodeService.js.map +1 -0
- package/src/lib/services/brightchain-authentication-provider.d.ts.map +1 -1
- package/src/lib/services/brightchain-authentication-provider.js +40 -9
- package/src/lib/services/brightchain-authentication-provider.js.map +1 -1
- package/src/lib/services/brightchain-member-init.service.d.ts +17 -17
- package/src/lib/services/brightchain-member-init.service.d.ts.map +1 -1
- package/src/lib/services/brightchain-member-init.service.js +12 -9
- package/src/lib/services/brightchain-member-init.service.js.map +1 -1
- package/src/lib/services/brighthub/connectionService.d.ts +286 -0
- package/src/lib/services/brighthub/connectionService.d.ts.map +1 -0
- package/src/lib/services/brighthub/connectionService.js +1887 -0
- package/src/lib/services/brighthub/connectionService.js.map +1 -0
- package/src/lib/services/brighthub/discoveryService.d.ts +110 -0
- package/src/lib/services/brighthub/discoveryService.d.ts.map +1 -0
- package/src/lib/services/brighthub/discoveryService.js +528 -0
- package/src/lib/services/brighthub/discoveryService.js.map +1 -0
- package/src/lib/services/brighthub/feedService.d.ts +141 -0
- package/src/lib/services/brighthub/feedService.d.ts.map +1 -0
- package/src/lib/services/brighthub/feedService.js +418 -0
- package/src/lib/services/brighthub/feedService.js.map +1 -0
- package/src/lib/services/brighthub/index.d.ts +11 -0
- package/src/lib/services/brighthub/index.d.ts.map +1 -0
- package/src/lib/services/brighthub/index.js +14 -0
- package/src/lib/services/brighthub/index.js.map +1 -0
- package/src/lib/services/brighthub/messagingService.d.ts +109 -0
- package/src/lib/services/brighthub/messagingService.d.ts.map +1 -0
- package/src/lib/services/brighthub/messagingService.js +947 -0
- package/src/lib/services/brighthub/messagingService.js.map +1 -0
- package/src/lib/services/brighthub/messagingService.test-helpers.d.ts +75 -0
- package/src/lib/services/brighthub/messagingService.test-helpers.d.ts.map +1 -0
- package/src/lib/services/brighthub/messagingService.test-helpers.js +237 -0
- package/src/lib/services/brighthub/messagingService.test-helpers.js.map +1 -0
- package/src/lib/services/brighthub/notificationService.d.ts +172 -0
- package/src/lib/services/brighthub/notificationService.d.ts.map +1 -0
- package/src/lib/services/brighthub/notificationService.js +768 -0
- package/src/lib/services/brighthub/notificationService.js.map +1 -0
- package/src/lib/services/brighthub/notificationService.test-helpers.d.ts +75 -0
- package/src/lib/services/brighthub/notificationService.test-helpers.d.ts.map +1 -0
- package/src/lib/services/brighthub/notificationService.test-helpers.js +230 -0
- package/src/lib/services/brighthub/notificationService.test-helpers.js.map +1 -0
- package/src/lib/services/brighthub/postService.d.ts +129 -0
- package/src/lib/services/brighthub/postService.d.ts.map +1 -0
- package/src/lib/services/brighthub/postService.js +470 -0
- package/src/lib/services/brighthub/postService.js.map +1 -0
- package/src/lib/services/brighthub/postService.test-helpers.d.ts +40 -0
- package/src/lib/services/brighthub/postService.test-helpers.d.ts.map +1 -0
- package/src/lib/services/brighthub/postService.test-helpers.js +84 -0
- package/src/lib/services/brighthub/postService.test-helpers.js.map +1 -0
- package/src/lib/services/brighthub/textFormatter.d.ts +64 -0
- package/src/lib/services/brighthub/textFormatter.d.ts.map +1 -0
- package/src/lib/services/brighthub/textFormatter.js +256 -0
- package/src/lib/services/brighthub/textFormatter.js.map +1 -0
- package/src/lib/services/brighthub/threadService.d.ts +79 -0
- package/src/lib/services/brighthub/threadService.d.ts.map +1 -0
- package/src/lib/services/brighthub/threadService.js +246 -0
- package/src/lib/services/brighthub/threadService.js.map +1 -0
- package/src/lib/services/brighthub/userProfileService.d.ts +203 -0
- package/src/lib/services/brighthub/userProfileService.d.ts.map +1 -0
- package/src/lib/services/brighthub/userProfileService.js +868 -0
- package/src/lib/services/brighthub/userProfileService.js.map +1 -0
- package/src/lib/services/brighthub/userProfileService.test-helpers.d.ts +86 -0
- package/src/lib/services/brighthub/userProfileService.test-helpers.d.ts.map +1 -0
- package/src/lib/services/brighthub/userProfileService.test-helpers.js +169 -0
- package/src/lib/services/brighthub/userProfileService.test-helpers.js.map +1 -0
- package/src/lib/services/brighthub/webSocketServer.d.ts +68 -0
- package/src/lib/services/brighthub/webSocketServer.d.ts.map +1 -0
- package/src/lib/services/brighthub/webSocketServer.js +194 -0
- package/src/lib/services/brighthub/webSocketServer.js.map +1 -0
- package/src/lib/services/brightpass/auditLogger.js +10 -5
- package/src/lib/services/brightpass/auditLogger.js.map +1 -1
- package/src/lib/services/brightpass/vaultEncryption.js +8 -8
- package/src/lib/services/brightpass/vaultEncryption.js.map +1 -1
- package/src/lib/services/brightpass.js +16 -8
- package/src/lib/services/brightpass.js.map +1 -1
- package/src/lib/services/cliOperatorPrompt.js +5 -2
- package/src/lib/services/cliOperatorPrompt.js.map +1 -1
- package/src/lib/services/clientWebSocketServer.d.ts +69 -4
- package/src/lib/services/clientWebSocketServer.d.ts.map +1 -1
- package/src/lib/services/clientWebSocketServer.js +188 -10
- package/src/lib/services/clientWebSocketServer.js.map +1 -1
- package/src/lib/services/contentAwareBlocksService.js +2 -0
- package/src/lib/services/contentAwareBlocksService.js.map +1 -1
- package/src/lib/services/contentIngestionService.d.ts.map +1 -1
- package/src/lib/services/contentIngestionService.js +2 -0
- package/src/lib/services/contentIngestionService.js.map +1 -1
- package/src/lib/services/diskQuorumService.js +3 -0
- package/src/lib/services/diskQuorumService.js.map +1 -1
- package/src/lib/services/email.js +5 -0
- package/src/lib/services/email.js.map +1 -1
- package/src/lib/services/eventNotificationSystem.d.ts +51 -10
- package/src/lib/services/eventNotificationSystem.d.ts.map +1 -1
- package/src/lib/services/eventNotificationSystem.js +76 -23
- package/src/lib/services/eventNotificationSystem.js.map +1 -1
- package/src/lib/services/expirationScheduler.js +6 -3
- package/src/lib/services/expirationScheduler.js.map +1 -1
- package/src/lib/services/fakeEmailService.js +3 -4
- package/src/lib/services/fakeEmailService.js.map +1 -1
- package/src/lib/services/fec.js +1 -3
- package/src/lib/services/fec.js.map +1 -1
- package/src/lib/services/fecServiceFactory.js +2 -2
- package/src/lib/services/fecServiceFactory.js.map +1 -1
- package/src/lib/services/fecUsageExample.js +1 -3
- package/src/lib/services/fecUsageExample.js.map +1 -1
- package/src/lib/services/identityExpirationScheduler.d.ts.map +1 -1
- package/src/lib/services/identityExpirationScheduler.js +7 -2
- package/src/lib/services/identityExpirationScheduler.js.map +1 -1
- package/src/lib/services/index.d.ts +3 -2
- package/src/lib/services/index.d.ts.map +1 -1
- package/src/lib/services/index.js +3 -2
- package/src/lib/services/index.js.map +1 -1
- package/src/lib/services/messageEventsWebSocketHandler.js +1 -0
- package/src/lib/services/messageEventsWebSocketHandler.js.map +1 -1
- package/src/lib/services/messagePassingService.js +5 -0
- package/src/lib/services/messagePassingService.js.map +1 -1
- package/src/lib/services/nativeRsFecService.js +3 -5
- package/src/lib/services/nativeRsFecService.js.map +1 -1
- package/src/lib/services/presenceService.js +5 -4
- package/src/lib/services/presenceService.js.map +1 -1
- package/src/lib/services/quorum.js +3 -2
- package/src/lib/services/quorum.js.map +1 -1
- package/src/lib/services/quorumDatabaseAdapter.d.ts +5 -5
- package/src/lib/services/quorumDatabaseAdapter.d.ts.map +1 -1
- package/src/lib/services/quorumDatabaseAdapter.js +5 -3
- package/src/lib/services/quorumDatabaseAdapter.js.map +1 -1
- package/src/lib/services/rbac-input-builder.js +5 -0
- package/src/lib/services/rbac-input-builder.js.map +1 -1
- package/src/lib/services/secureKeyStorage.js +3 -0
- package/src/lib/services/secureKeyStorage.js.map +1 -1
- package/src/lib/services/sessionAdapter.d.ts +4 -4
- package/src/lib/services/sessionAdapter.d.ts.map +1 -1
- package/src/lib/services/sessionAdapter.js +3 -2
- package/src/lib/services/sessionAdapter.js.map +1 -1
- package/src/lib/services/webSocketMessageServer.js +7 -4
- package/src/lib/services/webSocketMessageServer.js.map +1 -1
- package/src/lib/services/webSocketPeerProvider.js +2 -1
- package/src/lib/services/webSocketPeerProvider.js.map +1 -1
- package/src/lib/services/websocketHandler.js +9 -1
- package/src/lib/services/websocketHandler.js.map +1 -1
- package/src/lib/shared-types.d.ts +1 -2
- package/src/lib/shared-types.d.ts.map +1 -1
- package/src/lib/stores/__tests__/helpers/mockCloudBlockStore.d.ts +63 -0
- package/src/lib/stores/__tests__/helpers/mockCloudBlockStore.d.ts.map +1 -0
- package/src/lib/stores/__tests__/helpers/mockCloudBlockStore.js +160 -0
- package/src/lib/stores/__tests__/helpers/mockCloudBlockStore.js.map +1 -0
- package/src/lib/stores/availabilityAwareBlockStore.js +34 -5
- package/src/lib/stores/availabilityAwareBlockStore.js.map +1 -1
- package/src/lib/stores/cloudBlockStoreBase.d.ts +121 -0
- package/src/lib/stores/cloudBlockStoreBase.d.ts.map +1 -0
- package/src/lib/stores/cloudBlockStoreBase.js +1165 -0
- package/src/lib/stores/cloudBlockStoreBase.js.map +1 -0
- package/src/lib/stores/diskBlockAsyncStore.js +9 -5
- package/src/lib/stores/diskBlockAsyncStore.js.map +1 -1
- package/src/lib/stores/diskBlockMetadataStore.js +2 -0
- package/src/lib/stores/diskBlockMetadataStore.js.map +1 -1
- package/src/lib/stores/diskBlockStore.js +10 -8
- package/src/lib/stores/diskBlockStore.js.map +1 -1
- package/src/lib/stores/diskCBLStore.d.ts.map +1 -1
- package/src/lib/stores/diskCBLStore.js +8 -0
- package/src/lib/stores/diskCBLStore.js.map +1 -1
- package/src/lib/stores/index.d.ts +1 -0
- package/src/lib/stores/index.d.ts.map +1 -1
- package/src/lib/stores/index.js +1 -0
- package/src/lib/stores/index.js.map +1 -1
- package/src/lib/systemKeyring.d.ts.map +1 -1
- package/src/lib/systemKeyring.js +5 -4
- package/src/lib/systemKeyring.js.map +1 -1
- package/src/lib/transforms/checksumTransform.js +1 -0
- package/src/lib/transforms/checksumTransform.js.map +1 -1
- package/src/lib/transforms/memoryWritableStream.js +1 -0
- package/src/lib/transforms/memoryWritableStream.js.map +1 -1
- package/src/lib/transforms/xorMultipleTransform.js +3 -0
- package/src/lib/transforms/xorMultipleTransform.js.map +1 -1
- package/src/lib/utils/rehydration.d.ts +1 -1
- package/src/lib/utils/rehydration.js +1 -1
- package/src/lib/utils/serialization.d.ts +1 -1
- package/src/lib/utils/serialization.js +1 -1
- package/src/lib/services/backupCodeService.d.ts +0 -35
- package/src/lib/services/backupCodeService.d.ts.map +0 -1
- package/src/lib/services/backupCodeService.js +0 -109
- package/src/lib/services/backupCodeService.js.map +0 -1
|
@@ -0,0 +1,1887 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConnectionService = void 0;
|
|
4
|
+
exports.createConnectionService = createConnectionService;
|
|
5
|
+
const brighthub_lib_1 = require("@brightchain/brighthub-lib");
|
|
6
|
+
const crypto_1 = require("crypto");
|
|
7
|
+
/**
|
|
8
|
+
* Default pagination limit
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_PAGE_LIMIT = 20;
|
|
11
|
+
/**
|
|
12
|
+
* Maximum pagination limit
|
|
13
|
+
*/
|
|
14
|
+
const MAX_PAGE_LIMIT = 100;
|
|
15
|
+
/**
|
|
16
|
+
* Connection_Service implementation (list management)
|
|
17
|
+
* Handles connection lists, membership, and list lifecycle
|
|
18
|
+
* @see Requirements: 19.1-19.12
|
|
19
|
+
*/
|
|
20
|
+
class ConnectionService {
|
|
21
|
+
listsCollection;
|
|
22
|
+
listMembersCollection;
|
|
23
|
+
userProfilesCollection;
|
|
24
|
+
categoriesCollection;
|
|
25
|
+
categoryAssignmentsCollection;
|
|
26
|
+
notesCollection;
|
|
27
|
+
followsCollection;
|
|
28
|
+
metadataCollection;
|
|
29
|
+
temporaryMutesCollection;
|
|
30
|
+
hubsCollection;
|
|
31
|
+
hubMembersCollection;
|
|
32
|
+
blocksCollection;
|
|
33
|
+
listFollowersCollection;
|
|
34
|
+
/** Tracks last import timestamp per user for rate limiting */
|
|
35
|
+
lastImportTimestamps = new Map();
|
|
36
|
+
/** Cache for mutual connection counts with TTL of 5 minutes */
|
|
37
|
+
mutualConnectionCache = new Map();
|
|
38
|
+
/** TTL for mutual connection cache entries in milliseconds (5 minutes) */
|
|
39
|
+
static MUTUAL_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
40
|
+
constructor(application) {
|
|
41
|
+
this.listsCollection = application.getModel('brighthub_connection_lists');
|
|
42
|
+
this.listMembersCollection =
|
|
43
|
+
application.getModel('brighthub_connection_list_members');
|
|
44
|
+
this.userProfilesCollection = application.getModel('brighthub_user_profiles');
|
|
45
|
+
this.categoriesCollection = application.getModel('brighthub_connection_categories');
|
|
46
|
+
this.categoryAssignmentsCollection =
|
|
47
|
+
application.getModel('brighthub_connection_category_assignments');
|
|
48
|
+
this.notesCollection = application.getModel('brighthub_connection_notes');
|
|
49
|
+
this.followsCollection =
|
|
50
|
+
application.getModel('brighthub_follows');
|
|
51
|
+
this.metadataCollection = application.getModel('brighthub_connection_metadata');
|
|
52
|
+
this.temporaryMutesCollection = application.getModel('brighthub_temporary_mutes');
|
|
53
|
+
this.hubsCollection = application.getModel('brighthub_hubs');
|
|
54
|
+
this.hubMembersCollection = application.getModel('brighthub_hub_members');
|
|
55
|
+
this.blocksCollection =
|
|
56
|
+
application.getModel('brighthub_blocks');
|
|
57
|
+
this.listFollowersCollection = application.getModel('brighthub_connection_list_followers');
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Convert a database record to the API response format
|
|
61
|
+
*/
|
|
62
|
+
recordToList(record) {
|
|
63
|
+
return {
|
|
64
|
+
_id: record._id,
|
|
65
|
+
ownerId: record.ownerId,
|
|
66
|
+
name: record.name,
|
|
67
|
+
description: record.description,
|
|
68
|
+
visibility: record.visibility,
|
|
69
|
+
memberCount: record.memberCount,
|
|
70
|
+
followerCount: record.followerCount,
|
|
71
|
+
createdAt: record.createdAt,
|
|
72
|
+
updatedAt: record.updatedAt,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Convert a user profile record to the API response format
|
|
77
|
+
*/
|
|
78
|
+
recordToProfile(record) {
|
|
79
|
+
return {
|
|
80
|
+
_id: record._id,
|
|
81
|
+
username: record.username,
|
|
82
|
+
displayName: record.displayName,
|
|
83
|
+
bio: record.bio,
|
|
84
|
+
profilePictureUrl: record.profilePictureUrl,
|
|
85
|
+
headerImageUrl: record.headerImageUrl,
|
|
86
|
+
location: record.location,
|
|
87
|
+
websiteUrl: record.websiteUrl,
|
|
88
|
+
followerCount: record.followerCount,
|
|
89
|
+
followingCount: record.followingCount,
|
|
90
|
+
postCount: record.postCount,
|
|
91
|
+
isVerified: record.isVerified,
|
|
92
|
+
isProtected: record.isProtected,
|
|
93
|
+
approveFollowersMode: record.approveFollowersMode,
|
|
94
|
+
privacySettings: record.privacySettings,
|
|
95
|
+
createdAt: record.createdAt,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Convert a category database record to the API response format
|
|
100
|
+
*/
|
|
101
|
+
recordToCategory(record) {
|
|
102
|
+
return {
|
|
103
|
+
_id: record._id,
|
|
104
|
+
ownerId: record.ownerId,
|
|
105
|
+
name: record.name,
|
|
106
|
+
color: record.color,
|
|
107
|
+
icon: record.icon,
|
|
108
|
+
isDefault: record.isDefault,
|
|
109
|
+
createdAt: record.createdAt,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Get effective pagination limit
|
|
114
|
+
*/
|
|
115
|
+
getLimit(options) {
|
|
116
|
+
const limit = options?.limit ?? DEFAULT_PAGE_LIMIT;
|
|
117
|
+
return Math.min(Math.max(1, limit), MAX_PAGE_LIMIT);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Validate and retrieve a list, ensuring the caller is the owner
|
|
121
|
+
*/
|
|
122
|
+
async getOwnedList(listId, ownerId) {
|
|
123
|
+
const list = await this.listsCollection
|
|
124
|
+
.findOne({ _id: listId })
|
|
125
|
+
.exec();
|
|
126
|
+
if (!list) {
|
|
127
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.ListNotFound, `List with ID '${listId}' not found`);
|
|
128
|
+
}
|
|
129
|
+
if (list.ownerId !== ownerId) {
|
|
130
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.ListNotAuthorized, 'Not authorized to modify this list');
|
|
131
|
+
}
|
|
132
|
+
return list;
|
|
133
|
+
}
|
|
134
|
+
// ═══════════════════════════════════════════════════════
|
|
135
|
+
// List CRUD Operations
|
|
136
|
+
// ═══════════════════════════════════════════════════════
|
|
137
|
+
async createList(ownerId, name, options) {
|
|
138
|
+
// Validate name
|
|
139
|
+
if (!name || !name.trim()) {
|
|
140
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.InvalidListName, 'List name cannot be empty');
|
|
141
|
+
}
|
|
142
|
+
// Enforce 100 lists per user limit
|
|
143
|
+
const existingLists = await this.listsCollection
|
|
144
|
+
.find({ ownerId })
|
|
145
|
+
.exec();
|
|
146
|
+
if (existingLists.length >= brighthub_lib_1.MAX_LISTS_PER_USER) {
|
|
147
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.ListLimitExceeded, `Cannot create more than ${brighthub_lib_1.MAX_LISTS_PER_USER} lists`);
|
|
148
|
+
}
|
|
149
|
+
const now = new Date().toISOString();
|
|
150
|
+
const record = {
|
|
151
|
+
_id: (0, crypto_1.randomUUID)(),
|
|
152
|
+
ownerId,
|
|
153
|
+
name: name.trim(),
|
|
154
|
+
description: options?.description,
|
|
155
|
+
visibility: options?.visibility ?? brighthub_lib_1.ConnectionVisibility.Private,
|
|
156
|
+
memberCount: 0,
|
|
157
|
+
followerCount: 0,
|
|
158
|
+
createdAt: now,
|
|
159
|
+
updatedAt: now,
|
|
160
|
+
};
|
|
161
|
+
await this.listsCollection.create(record);
|
|
162
|
+
return this.recordToList(record);
|
|
163
|
+
}
|
|
164
|
+
async updateList(listId, ownerId, updates) {
|
|
165
|
+
const list = await this.getOwnedList(listId, ownerId);
|
|
166
|
+
// Validate name if being updated
|
|
167
|
+
if (updates.name !== undefined && (!updates.name || !updates.name.trim())) {
|
|
168
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.InvalidListName, 'List name cannot be empty');
|
|
169
|
+
}
|
|
170
|
+
const now = new Date().toISOString();
|
|
171
|
+
const updateFields = {
|
|
172
|
+
updatedAt: now,
|
|
173
|
+
};
|
|
174
|
+
if (updates.name !== undefined) {
|
|
175
|
+
updateFields.name = updates.name.trim();
|
|
176
|
+
}
|
|
177
|
+
if (updates.description !== undefined) {
|
|
178
|
+
updateFields.description = updates.description;
|
|
179
|
+
}
|
|
180
|
+
if (updates.visibility !== undefined) {
|
|
181
|
+
updateFields.visibility = updates.visibility;
|
|
182
|
+
}
|
|
183
|
+
await this.listsCollection
|
|
184
|
+
.updateOne({ _id: listId }, updateFields)
|
|
185
|
+
.exec();
|
|
186
|
+
// When visibility changes to Private, remove all external followers
|
|
187
|
+
if (updates.visibility === brighthub_lib_1.ConnectionVisibility.Private &&
|
|
188
|
+
list.visibility !== brighthub_lib_1.ConnectionVisibility.Private) {
|
|
189
|
+
await this.removeNonOwnerFollowersOnPrivate(listId, ownerId);
|
|
190
|
+
}
|
|
191
|
+
// Return updated list
|
|
192
|
+
return this.recordToList({
|
|
193
|
+
...list,
|
|
194
|
+
...updateFields,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
async deleteList(listId, ownerId) {
|
|
198
|
+
await this.getOwnedList(listId, ownerId);
|
|
199
|
+
// Cascade: remove all memberships for this list
|
|
200
|
+
const members = await this.listMembersCollection
|
|
201
|
+
.find({ listId })
|
|
202
|
+
.exec();
|
|
203
|
+
for (const member of members) {
|
|
204
|
+
await this.listMembersCollection
|
|
205
|
+
.deleteOne({ _id: member._id })
|
|
206
|
+
.exec();
|
|
207
|
+
}
|
|
208
|
+
// Cascade: remove all followers for this list
|
|
209
|
+
const followers = await this.listFollowersCollection
|
|
210
|
+
.find({ listId })
|
|
211
|
+
.exec();
|
|
212
|
+
for (const follower of followers) {
|
|
213
|
+
await this.listFollowersCollection
|
|
214
|
+
.deleteOne({ _id: follower._id })
|
|
215
|
+
.exec();
|
|
216
|
+
}
|
|
217
|
+
// Delete the list itself
|
|
218
|
+
await this.listsCollection
|
|
219
|
+
.deleteOne({ _id: listId })
|
|
220
|
+
.exec();
|
|
221
|
+
}
|
|
222
|
+
// ═══════════════════════════════════════════════════════
|
|
223
|
+
// List Membership Operations
|
|
224
|
+
// ═══════════════════════════════════════════════════════
|
|
225
|
+
async addMembersToList(listId, ownerId, userIds) {
|
|
226
|
+
const list = await this.getOwnedList(listId, ownerId);
|
|
227
|
+
if (userIds.length === 0) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
// Check member limit
|
|
231
|
+
const newTotal = list.memberCount + userIds.length;
|
|
232
|
+
if (newTotal > brighthub_lib_1.MAX_MEMBERS_PER_LIST) {
|
|
233
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.ListMemberLimitExceeded, `Cannot exceed ${brighthub_lib_1.MAX_MEMBERS_PER_LIST} members per list`);
|
|
234
|
+
}
|
|
235
|
+
const now = new Date().toISOString();
|
|
236
|
+
let addedCount = 0;
|
|
237
|
+
for (const userId of userIds) {
|
|
238
|
+
// Check if already a member (skip duplicates)
|
|
239
|
+
const existing = await this.listMembersCollection
|
|
240
|
+
.findOne({
|
|
241
|
+
listId,
|
|
242
|
+
userId,
|
|
243
|
+
})
|
|
244
|
+
.exec();
|
|
245
|
+
if (!existing) {
|
|
246
|
+
await this.listMembersCollection.create({
|
|
247
|
+
_id: (0, crypto_1.randomUUID)(),
|
|
248
|
+
listId,
|
|
249
|
+
userId,
|
|
250
|
+
addedAt: now,
|
|
251
|
+
});
|
|
252
|
+
addedCount++;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Update member count
|
|
256
|
+
if (addedCount > 0) {
|
|
257
|
+
await this.listsCollection
|
|
258
|
+
.updateOne({ _id: listId }, {
|
|
259
|
+
memberCount: list.memberCount + addedCount,
|
|
260
|
+
updatedAt: now,
|
|
261
|
+
})
|
|
262
|
+
.exec();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async removeMembersFromList(listId, ownerId, userIds) {
|
|
266
|
+
const list = await this.getOwnedList(listId, ownerId);
|
|
267
|
+
if (userIds.length === 0) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
let removedCount = 0;
|
|
271
|
+
for (const userId of userIds) {
|
|
272
|
+
const result = await this.listMembersCollection
|
|
273
|
+
.deleteOne({
|
|
274
|
+
listId,
|
|
275
|
+
userId,
|
|
276
|
+
})
|
|
277
|
+
.exec();
|
|
278
|
+
if (result.deletedCount > 0) {
|
|
279
|
+
removedCount++;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// Update member count
|
|
283
|
+
if (removedCount > 0) {
|
|
284
|
+
const newCount = Math.max(0, list.memberCount - removedCount);
|
|
285
|
+
await this.listsCollection
|
|
286
|
+
.updateOne({ _id: listId }, {
|
|
287
|
+
memberCount: newCount,
|
|
288
|
+
updatedAt: new Date().toISOString(),
|
|
289
|
+
})
|
|
290
|
+
.exec();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async getListMembers(listId, options) {
|
|
294
|
+
// Verify list exists
|
|
295
|
+
const list = await this.listsCollection
|
|
296
|
+
.findOne({ _id: listId })
|
|
297
|
+
.exec();
|
|
298
|
+
if (!list) {
|
|
299
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.ListNotFound, `List with ID '${listId}' not found`);
|
|
300
|
+
}
|
|
301
|
+
const limit = this.getLimit(options);
|
|
302
|
+
// Fetch one extra to determine hasMore
|
|
303
|
+
const queryLimit = limit + 1;
|
|
304
|
+
let query = this.listMembersCollection.find({
|
|
305
|
+
listId,
|
|
306
|
+
});
|
|
307
|
+
if (query.sort) {
|
|
308
|
+
query = query.sort({ addedAt: -1 });
|
|
309
|
+
}
|
|
310
|
+
// Apply cursor-based pagination (cursor is the addedAt timestamp)
|
|
311
|
+
const allMembers = await query.exec();
|
|
312
|
+
let filteredMembers = allMembers;
|
|
313
|
+
if (options?.cursor) {
|
|
314
|
+
filteredMembers = allMembers.filter((m) => m.addedAt < options.cursor);
|
|
315
|
+
}
|
|
316
|
+
const paginatedMembers = filteredMembers.slice(0, queryLimit);
|
|
317
|
+
const hasMore = paginatedMembers.length > limit;
|
|
318
|
+
const resultMembers = hasMore
|
|
319
|
+
? paginatedMembers.slice(0, limit)
|
|
320
|
+
: paginatedMembers;
|
|
321
|
+
// Look up user profiles for each member
|
|
322
|
+
const profiles = [];
|
|
323
|
+
for (const member of resultMembers) {
|
|
324
|
+
const userRecord = await this.userProfilesCollection
|
|
325
|
+
.findOne({ _id: member.userId })
|
|
326
|
+
.exec();
|
|
327
|
+
if (userRecord) {
|
|
328
|
+
profiles.push(this.recordToProfile(userRecord));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const lastMember = resultMembers[resultMembers.length - 1];
|
|
332
|
+
return {
|
|
333
|
+
items: profiles,
|
|
334
|
+
cursor: hasMore && lastMember ? lastMember.addedAt : undefined,
|
|
335
|
+
hasMore,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
// ═══════════════════════════════════════════════════════
|
|
339
|
+
// List Query Operations
|
|
340
|
+
// ═══════════════════════════════════════════════════════
|
|
341
|
+
async getUserLists(ownerId, options) {
|
|
342
|
+
const limit = this.getLimit(options);
|
|
343
|
+
const queryLimit = limit + 1;
|
|
344
|
+
let query = this.listsCollection.find({
|
|
345
|
+
ownerId,
|
|
346
|
+
});
|
|
347
|
+
if (query.sort) {
|
|
348
|
+
query = query.sort({ createdAt: -1 });
|
|
349
|
+
}
|
|
350
|
+
const allLists = await query.exec();
|
|
351
|
+
let filteredLists = allLists;
|
|
352
|
+
if (options?.cursor) {
|
|
353
|
+
filteredLists = allLists.filter((l) => l.createdAt < options.cursor);
|
|
354
|
+
}
|
|
355
|
+
const paginatedLists = filteredLists.slice(0, queryLimit);
|
|
356
|
+
const hasMore = paginatedLists.length > limit;
|
|
357
|
+
const resultLists = hasMore
|
|
358
|
+
? paginatedLists.slice(0, limit)
|
|
359
|
+
: paginatedLists;
|
|
360
|
+
const lastList = resultLists[resultLists.length - 1];
|
|
361
|
+
return {
|
|
362
|
+
items: resultLists.map((r) => this.recordToList(r)),
|
|
363
|
+
cursor: hasMore && lastList ? lastList.createdAt : undefined,
|
|
364
|
+
hasMore,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
// ═══════════════════════════════════════════════════════
|
|
368
|
+
// Category Helper Methods
|
|
369
|
+
// ═══════════════════════════════════════════════════════
|
|
370
|
+
/**
|
|
371
|
+
* Validate and retrieve a category, ensuring the caller is the owner
|
|
372
|
+
*/
|
|
373
|
+
async getOwnedCategory(categoryId, ownerId) {
|
|
374
|
+
const category = await this.categoriesCollection
|
|
375
|
+
.findOne({ _id: categoryId })
|
|
376
|
+
.exec();
|
|
377
|
+
if (!category) {
|
|
378
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.CategoryNotFound, `Category with ID '${categoryId}' not found`);
|
|
379
|
+
}
|
|
380
|
+
if (category.ownerId !== ownerId) {
|
|
381
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.CategoryNotAuthorized, 'Not authorized to modify this category');
|
|
382
|
+
}
|
|
383
|
+
return category;
|
|
384
|
+
}
|
|
385
|
+
// ═══════════════════════════════════════════════════════
|
|
386
|
+
// Category CRUD Operations (Requirements 20.1-20.8)
|
|
387
|
+
// ═══════════════════════════════════════════════════════
|
|
388
|
+
async createCategory(ownerId, name, options) {
|
|
389
|
+
// Validate name
|
|
390
|
+
if (!name || !name.trim()) {
|
|
391
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.InvalidCategoryName, 'Category name cannot be empty');
|
|
392
|
+
}
|
|
393
|
+
// Check for duplicate name
|
|
394
|
+
const existingByName = await this.categoriesCollection
|
|
395
|
+
.find({ ownerId })
|
|
396
|
+
.exec();
|
|
397
|
+
const duplicateName = existingByName.find((c) => c.name.toLowerCase() === name.trim().toLowerCase());
|
|
398
|
+
if (duplicateName) {
|
|
399
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.DuplicateCategoryName, `A category named '${name.trim()}' already exists`);
|
|
400
|
+
}
|
|
401
|
+
// Enforce 20 custom categories per user limit (default categories don't count)
|
|
402
|
+
const customCategories = existingByName.filter((c) => !c.isDefault);
|
|
403
|
+
if (customCategories.length >= brighthub_lib_1.MAX_CATEGORIES_PER_USER) {
|
|
404
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.CategoryLimitExceeded, `Cannot create more than ${brighthub_lib_1.MAX_CATEGORIES_PER_USER} custom categories`);
|
|
405
|
+
}
|
|
406
|
+
const now = new Date().toISOString();
|
|
407
|
+
const record = {
|
|
408
|
+
_id: (0, crypto_1.randomUUID)(),
|
|
409
|
+
ownerId,
|
|
410
|
+
name: name.trim(),
|
|
411
|
+
color: options?.color,
|
|
412
|
+
icon: options?.icon,
|
|
413
|
+
isDefault: false,
|
|
414
|
+
createdAt: now,
|
|
415
|
+
};
|
|
416
|
+
await this.categoriesCollection.create(record);
|
|
417
|
+
return this.recordToCategory(record);
|
|
418
|
+
}
|
|
419
|
+
async assignCategory(connectionId, categoryId, ownerId) {
|
|
420
|
+
// Verify category exists and belongs to the owner
|
|
421
|
+
await this.getOwnedCategory(categoryId, ownerId);
|
|
422
|
+
// Check if already assigned
|
|
423
|
+
const existing = await this.categoryAssignmentsCollection
|
|
424
|
+
.findOne({
|
|
425
|
+
connectionId,
|
|
426
|
+
categoryId,
|
|
427
|
+
})
|
|
428
|
+
.exec();
|
|
429
|
+
if (existing) {
|
|
430
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.CategoryAlreadyAssigned, 'Category is already assigned to this connection');
|
|
431
|
+
}
|
|
432
|
+
const now = new Date().toISOString();
|
|
433
|
+
await this.categoryAssignmentsCollection.create({
|
|
434
|
+
_id: (0, crypto_1.randomUUID)(),
|
|
435
|
+
ownerId,
|
|
436
|
+
connectionId,
|
|
437
|
+
categoryId,
|
|
438
|
+
assignedAt: now,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
async unassignCategory(connectionId, categoryId, ownerId) {
|
|
442
|
+
// Verify category exists and belongs to the owner
|
|
443
|
+
await this.getOwnedCategory(categoryId, ownerId);
|
|
444
|
+
const result = await this.categoryAssignmentsCollection
|
|
445
|
+
.deleteOne({
|
|
446
|
+
connectionId,
|
|
447
|
+
categoryId,
|
|
448
|
+
})
|
|
449
|
+
.exec();
|
|
450
|
+
if (result.deletedCount === 0) {
|
|
451
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.CategoryNotAssigned, 'Category is not assigned to this connection');
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
async bulkAssignCategory(connectionIds, categoryId, ownerId) {
|
|
455
|
+
if (connectionIds.length === 0) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
// Verify category exists and belongs to the owner
|
|
459
|
+
await this.getOwnedCategory(categoryId, ownerId);
|
|
460
|
+
const now = new Date().toISOString();
|
|
461
|
+
for (const connectionId of connectionIds) {
|
|
462
|
+
// Skip if already assigned
|
|
463
|
+
const existing = await this.categoryAssignmentsCollection
|
|
464
|
+
.findOne({
|
|
465
|
+
connectionId,
|
|
466
|
+
categoryId,
|
|
467
|
+
})
|
|
468
|
+
.exec();
|
|
469
|
+
if (!existing) {
|
|
470
|
+
await this.categoryAssignmentsCollection.create({
|
|
471
|
+
_id: (0, crypto_1.randomUUID)(),
|
|
472
|
+
ownerId,
|
|
473
|
+
connectionId,
|
|
474
|
+
categoryId,
|
|
475
|
+
assignedAt: now,
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
async getConnectionsByCategory(ownerId, categoryId, options) {
|
|
481
|
+
// Verify category exists and belongs to the owner
|
|
482
|
+
await this.getOwnedCategory(categoryId, ownerId);
|
|
483
|
+
const limit = this.getLimit(options);
|
|
484
|
+
const queryLimit = limit + 1;
|
|
485
|
+
// Get all assignments for this category
|
|
486
|
+
let query = this.categoryAssignmentsCollection.find({
|
|
487
|
+
ownerId,
|
|
488
|
+
categoryId,
|
|
489
|
+
});
|
|
490
|
+
if (query.sort) {
|
|
491
|
+
query = query.sort({ assignedAt: -1 });
|
|
492
|
+
}
|
|
493
|
+
const allAssignments = await query.exec();
|
|
494
|
+
let filteredAssignments = allAssignments;
|
|
495
|
+
if (options?.cursor) {
|
|
496
|
+
filteredAssignments = allAssignments.filter((a) => a.assignedAt < options.cursor);
|
|
497
|
+
}
|
|
498
|
+
const paginatedAssignments = filteredAssignments.slice(0, queryLimit);
|
|
499
|
+
const hasMore = paginatedAssignments.length > limit;
|
|
500
|
+
const resultAssignments = hasMore
|
|
501
|
+
? paginatedAssignments.slice(0, limit)
|
|
502
|
+
: paginatedAssignments;
|
|
503
|
+
// Look up user profiles for each connection
|
|
504
|
+
const profiles = [];
|
|
505
|
+
for (const assignment of resultAssignments) {
|
|
506
|
+
const userRecord = await this.userProfilesCollection
|
|
507
|
+
.findOne({
|
|
508
|
+
_id: assignment.connectionId,
|
|
509
|
+
})
|
|
510
|
+
.exec();
|
|
511
|
+
if (userRecord) {
|
|
512
|
+
profiles.push(this.recordToProfile(userRecord));
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
const lastAssignment = resultAssignments[resultAssignments.length - 1];
|
|
516
|
+
return {
|
|
517
|
+
items: profiles,
|
|
518
|
+
cursor: hasMore && lastAssignment ? lastAssignment.assignedAt : undefined,
|
|
519
|
+
hasMore,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
async deleteCategory(categoryId, ownerId) {
|
|
523
|
+
const category = await this.getOwnedCategory(categoryId, ownerId);
|
|
524
|
+
// Prevent deleting default categories
|
|
525
|
+
if (category.isDefault) {
|
|
526
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.CannotDeleteDefaultCategory, 'Cannot delete a default category');
|
|
527
|
+
}
|
|
528
|
+
// Remove all assignments for this category
|
|
529
|
+
const assignments = await this.categoryAssignmentsCollection
|
|
530
|
+
.find({ categoryId })
|
|
531
|
+
.exec();
|
|
532
|
+
for (const assignment of assignments) {
|
|
533
|
+
await this.categoryAssignmentsCollection
|
|
534
|
+
.deleteOne({
|
|
535
|
+
_id: assignment._id,
|
|
536
|
+
})
|
|
537
|
+
.exec();
|
|
538
|
+
}
|
|
539
|
+
// Delete the category itself
|
|
540
|
+
await this.categoriesCollection
|
|
541
|
+
.deleteOne({ _id: categoryId })
|
|
542
|
+
.exec();
|
|
543
|
+
}
|
|
544
|
+
async getDefaultCategories(ownerId) {
|
|
545
|
+
// Check if default categories already exist for this user
|
|
546
|
+
const existingCategories = await this.categoriesCollection
|
|
547
|
+
.find({ ownerId, isDefault: true })
|
|
548
|
+
.exec();
|
|
549
|
+
if (existingCategories.length > 0) {
|
|
550
|
+
return existingCategories.map((r) => this.recordToCategory(r));
|
|
551
|
+
}
|
|
552
|
+
// Create default categories
|
|
553
|
+
const now = new Date().toISOString();
|
|
554
|
+
const created = [];
|
|
555
|
+
for (const def of brighthub_lib_1.DEFAULT_CONNECTION_CATEGORIES) {
|
|
556
|
+
const record = {
|
|
557
|
+
_id: (0, crypto_1.randomUUID)(),
|
|
558
|
+
ownerId,
|
|
559
|
+
name: def.name,
|
|
560
|
+
color: def.color,
|
|
561
|
+
icon: def.icon,
|
|
562
|
+
isDefault: true,
|
|
563
|
+
createdAt: now,
|
|
564
|
+
};
|
|
565
|
+
await this.categoriesCollection.create(record);
|
|
566
|
+
created.push(this.recordToCategory(record));
|
|
567
|
+
}
|
|
568
|
+
return created;
|
|
569
|
+
}
|
|
570
|
+
// ═══════════════════════════════════════════════════════
|
|
571
|
+
// Connection Note Operations (Requirements 21.1-21.7)
|
|
572
|
+
// ═══════════════════════════════════════════════════════
|
|
573
|
+
/**
|
|
574
|
+
* Validate note content
|
|
575
|
+
* @throws ConnectionServiceError if note is empty or exceeds limit
|
|
576
|
+
*/
|
|
577
|
+
validateNoteContent(note) {
|
|
578
|
+
if (!note || !note.trim()) {
|
|
579
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.InvalidNoteContent, 'Note content cannot be empty');
|
|
580
|
+
}
|
|
581
|
+
if (note.length > brighthub_lib_1.MAX_NOTE_LENGTH) {
|
|
582
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.NoteTooLong, `Note cannot exceed ${brighthub_lib_1.MAX_NOTE_LENGTH} characters`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Convert a note record to the API response format
|
|
587
|
+
*/
|
|
588
|
+
recordToNote(record) {
|
|
589
|
+
return {
|
|
590
|
+
_id: record._id,
|
|
591
|
+
userId: record.userId,
|
|
592
|
+
connectionId: record.connectionId,
|
|
593
|
+
note: record.note,
|
|
594
|
+
createdAt: record.createdAt,
|
|
595
|
+
updatedAt: record.updatedAt,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
async addNote(userId, connectionId, note) {
|
|
599
|
+
this.validateNoteContent(note);
|
|
600
|
+
// Check if a note already exists for this connection (unique constraint)
|
|
601
|
+
const existing = await this.notesCollection
|
|
602
|
+
.findOne({
|
|
603
|
+
userId,
|
|
604
|
+
connectionId,
|
|
605
|
+
})
|
|
606
|
+
.exec();
|
|
607
|
+
if (existing) {
|
|
608
|
+
// Update existing note instead of creating duplicate
|
|
609
|
+
const now = new Date().toISOString();
|
|
610
|
+
await this.notesCollection
|
|
611
|
+
.updateOne({ _id: existing._id }, {
|
|
612
|
+
note: note.trim(),
|
|
613
|
+
updatedAt: now,
|
|
614
|
+
})
|
|
615
|
+
.exec();
|
|
616
|
+
return this.recordToNote({
|
|
617
|
+
...existing,
|
|
618
|
+
note: note.trim(),
|
|
619
|
+
updatedAt: now,
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
const now = new Date().toISOString();
|
|
623
|
+
const record = {
|
|
624
|
+
_id: (0, crypto_1.randomUUID)(),
|
|
625
|
+
userId,
|
|
626
|
+
connectionId,
|
|
627
|
+
note: note.trim(),
|
|
628
|
+
createdAt: now,
|
|
629
|
+
updatedAt: now,
|
|
630
|
+
};
|
|
631
|
+
await this.notesCollection.create(record);
|
|
632
|
+
return this.recordToNote(record);
|
|
633
|
+
}
|
|
634
|
+
async updateNote(userId, connectionId, note) {
|
|
635
|
+
this.validateNoteContent(note);
|
|
636
|
+
const existing = await this.notesCollection
|
|
637
|
+
.findOne({
|
|
638
|
+
userId,
|
|
639
|
+
connectionId,
|
|
640
|
+
})
|
|
641
|
+
.exec();
|
|
642
|
+
if (!existing) {
|
|
643
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.NoteNotFound, 'No note found for this connection');
|
|
644
|
+
}
|
|
645
|
+
const now = new Date().toISOString();
|
|
646
|
+
await this.notesCollection
|
|
647
|
+
.updateOne({ _id: existing._id }, {
|
|
648
|
+
note: note.trim(),
|
|
649
|
+
updatedAt: now,
|
|
650
|
+
})
|
|
651
|
+
.exec();
|
|
652
|
+
return this.recordToNote({
|
|
653
|
+
...existing,
|
|
654
|
+
note: note.trim(),
|
|
655
|
+
updatedAt: now,
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
async deleteNote(userId, connectionId) {
|
|
659
|
+
const result = await this.notesCollection
|
|
660
|
+
.deleteOne({
|
|
661
|
+
userId,
|
|
662
|
+
connectionId,
|
|
663
|
+
})
|
|
664
|
+
.exec();
|
|
665
|
+
if (result.deletedCount === 0) {
|
|
666
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.NoteNotFound, 'No note found for this connection');
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
async searchConnectionsByNote(userId, searchTerm, options) {
|
|
670
|
+
const limit = this.getLimit(options);
|
|
671
|
+
const skip = options?.cursor ? parseInt(options.cursor, 10) : 0;
|
|
672
|
+
// Find all notes for this user (we filter in-memory for text matching
|
|
673
|
+
// since the mock collection doesn't support regex/text search)
|
|
674
|
+
const allNotes = await this.notesCollection
|
|
675
|
+
.find({ userId })
|
|
676
|
+
.exec();
|
|
677
|
+
// Filter by search term (case-insensitive)
|
|
678
|
+
const searchLower = searchTerm.toLowerCase();
|
|
679
|
+
const matchingNotes = allNotes.filter((n) => n.note.toLowerCase().includes(searchLower));
|
|
680
|
+
// Apply pagination
|
|
681
|
+
const paginatedNotes = matchingNotes.slice(skip, skip + limit + 1);
|
|
682
|
+
const hasMore = paginatedNotes.length > limit;
|
|
683
|
+
const results = paginatedNotes.slice(0, limit);
|
|
684
|
+
return {
|
|
685
|
+
items: results.map((r) => this.recordToNote(r)),
|
|
686
|
+
cursor: hasMore ? String(skip + limit) : undefined,
|
|
687
|
+
hasMore,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
// ═══════════════════════════════════════════════════════
|
|
691
|
+
// Import/Export Operations (Requirements 22.1-22.7)
|
|
692
|
+
// ═══════════════════════════════════════════════════════
|
|
693
|
+
/**
|
|
694
|
+
* Check and enforce rate limiting for import operations
|
|
695
|
+
* @throws ConnectionServiceError if rate limited
|
|
696
|
+
*/
|
|
697
|
+
checkImportRateLimit(userId) {
|
|
698
|
+
const lastImport = this.lastImportTimestamps.get(userId);
|
|
699
|
+
if (lastImport !== undefined) {
|
|
700
|
+
const elapsed = Date.now() - lastImport;
|
|
701
|
+
if (elapsed < brighthub_lib_1.IMPORT_RATE_LIMIT_MS) {
|
|
702
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.ImportRateLimited, `Import rate limited. Please wait ${Math.ceil((brighthub_lib_1.IMPORT_RATE_LIMIT_MS - elapsed) / 1000)} seconds before importing again.`);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Parse CSV data into an array of usernames (one per line)
|
|
708
|
+
*/
|
|
709
|
+
parseCsvUsernames(data) {
|
|
710
|
+
return data
|
|
711
|
+
.split(/\r?\n/)
|
|
712
|
+
.map((line) => line.trim())
|
|
713
|
+
.filter((line) => line.length > 0 && !line.startsWith('#'));
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Look up a user profile by username
|
|
717
|
+
*/
|
|
718
|
+
async findUserByUsername(username) {
|
|
719
|
+
return this.userProfilesCollection
|
|
720
|
+
.findOne({ username })
|
|
721
|
+
.exec();
|
|
722
|
+
}
|
|
723
|
+
async exportConnections(userId) {
|
|
724
|
+
// Get all follows for this user
|
|
725
|
+
const follows = await this.followsCollection
|
|
726
|
+
.find({ followerId: userId })
|
|
727
|
+
.exec();
|
|
728
|
+
const followedIds = follows.map((f) => f.followedId);
|
|
729
|
+
// Get all category assignments for this user
|
|
730
|
+
const allAssignments = await this.categoryAssignmentsCollection
|
|
731
|
+
.find({ ownerId: userId })
|
|
732
|
+
.exec();
|
|
733
|
+
// Get all categories for this user
|
|
734
|
+
const allCategories = await this.categoriesCollection
|
|
735
|
+
.find({ ownerId: userId })
|
|
736
|
+
.exec();
|
|
737
|
+
const categoryMap = new Map(allCategories.map((c) => [c._id, c.name]));
|
|
738
|
+
// Get all notes for this user
|
|
739
|
+
const allNotes = await this.notesCollection
|
|
740
|
+
.find({ userId })
|
|
741
|
+
.exec();
|
|
742
|
+
const noteMap = new Map(allNotes.map((n) => [n.connectionId, n.note]));
|
|
743
|
+
// Get all list memberships for lists owned by this user
|
|
744
|
+
const userLists = await this.listsCollection
|
|
745
|
+
.find({ ownerId: userId })
|
|
746
|
+
.exec();
|
|
747
|
+
const listNameMap = new Map(userLists.map((l) => [l._id, l.name]));
|
|
748
|
+
// Build a map of connectionId -> list names
|
|
749
|
+
const connectionListNames = new Map();
|
|
750
|
+
for (const list of userLists) {
|
|
751
|
+
const members = await this.listMembersCollection
|
|
752
|
+
.find({ listId: list._id })
|
|
753
|
+
.exec();
|
|
754
|
+
for (const member of members) {
|
|
755
|
+
const existing = connectionListNames.get(member.userId) || [];
|
|
756
|
+
const listName = listNameMap.get(list._id);
|
|
757
|
+
if (listName) {
|
|
758
|
+
existing.push(listName);
|
|
759
|
+
}
|
|
760
|
+
connectionListNames.set(member.userId, existing);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
// Build connection entries
|
|
764
|
+
const connections = [];
|
|
765
|
+
for (const followedId of followedIds) {
|
|
766
|
+
const profile = await this.userProfilesCollection
|
|
767
|
+
.findOne({ _id: followedId })
|
|
768
|
+
.exec();
|
|
769
|
+
if (!profile)
|
|
770
|
+
continue;
|
|
771
|
+
// Get categories for this connection
|
|
772
|
+
const assignments = allAssignments.filter((a) => a.connectionId === followedId);
|
|
773
|
+
const categories = assignments
|
|
774
|
+
.map((a) => categoryMap.get(a.categoryId))
|
|
775
|
+
.filter((name) => name !== undefined);
|
|
776
|
+
connections.push({
|
|
777
|
+
username: profile.username,
|
|
778
|
+
displayName: profile.displayName,
|
|
779
|
+
categories,
|
|
780
|
+
note: noteMap.get(followedId),
|
|
781
|
+
lists: connectionListNames.get(followedId) || [],
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
return {
|
|
785
|
+
version: '1.0',
|
|
786
|
+
exportedAt: new Date().toISOString(),
|
|
787
|
+
userId,
|
|
788
|
+
connectionCount: connections.length,
|
|
789
|
+
connections,
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
async exportList(listId, ownerId) {
|
|
793
|
+
const list = await this.getOwnedList(listId, ownerId);
|
|
794
|
+
// Get all members
|
|
795
|
+
const memberRecords = await this.listMembersCollection
|
|
796
|
+
.find({ listId })
|
|
797
|
+
.exec();
|
|
798
|
+
const members = [];
|
|
799
|
+
for (const memberRecord of memberRecords) {
|
|
800
|
+
const profile = await this.userProfilesCollection
|
|
801
|
+
.findOne({ _id: memberRecord.userId })
|
|
802
|
+
.exec();
|
|
803
|
+
if (profile) {
|
|
804
|
+
members.push({
|
|
805
|
+
username: profile.username,
|
|
806
|
+
displayName: profile.displayName,
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return {
|
|
811
|
+
version: '1.0',
|
|
812
|
+
exportedAt: new Date().toISOString(),
|
|
813
|
+
listName: list.name,
|
|
814
|
+
description: list.description,
|
|
815
|
+
visibility: list.visibility,
|
|
816
|
+
memberCount: members.length,
|
|
817
|
+
members,
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
async importConnections(userId, data, format) {
|
|
821
|
+
this.checkImportRateLimit(userId);
|
|
822
|
+
let usernames;
|
|
823
|
+
if (format === 'csv') {
|
|
824
|
+
usernames = this.parseCsvUsernames(data);
|
|
825
|
+
}
|
|
826
|
+
else {
|
|
827
|
+
try {
|
|
828
|
+
const parsed = JSON.parse(data);
|
|
829
|
+
if (parsed.connections && Array.isArray(parsed.connections)) {
|
|
830
|
+
usernames = parsed.connections
|
|
831
|
+
.map((c) => c.username || '')
|
|
832
|
+
.filter((u) => u.length > 0);
|
|
833
|
+
}
|
|
834
|
+
else if (Array.isArray(parsed)) {
|
|
835
|
+
usernames = parsed
|
|
836
|
+
.map((entry) => typeof entry === 'string' ? entry : entry.username || '')
|
|
837
|
+
.filter((u) => u.length > 0);
|
|
838
|
+
}
|
|
839
|
+
else {
|
|
840
|
+
throw new Error('Unrecognized JSON structure');
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
catch {
|
|
844
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.InvalidImportFormat, 'Invalid JSON format. Expected { connections: [{ username }] } or an array of usernames.');
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
// Deduplicate
|
|
848
|
+
const uniqueUsernames = [...new Set(usernames)];
|
|
849
|
+
const result = {
|
|
850
|
+
successCount: 0,
|
|
851
|
+
skippedCount: 0,
|
|
852
|
+
totalCount: uniqueUsernames.length,
|
|
853
|
+
skippedUsernames: [],
|
|
854
|
+
errors: [],
|
|
855
|
+
};
|
|
856
|
+
for (const username of uniqueUsernames) {
|
|
857
|
+
const profile = await this.findUserByUsername(username);
|
|
858
|
+
if (!profile) {
|
|
859
|
+
result.skippedCount++;
|
|
860
|
+
result.skippedUsernames.push(username);
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
// Don't follow yourself
|
|
864
|
+
if (profile._id === userId) {
|
|
865
|
+
result.skippedCount++;
|
|
866
|
+
result.errors.push(`Cannot follow yourself (${username})`);
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
// Skip blocked users (Requirement 32.5)
|
|
870
|
+
const isBlocked = await this.isBlockedFromList(profile._id, userId);
|
|
871
|
+
const isBlockedBy = await this.blocksCollection
|
|
872
|
+
.findOne({
|
|
873
|
+
blockerId: profile._id,
|
|
874
|
+
blockedId: userId,
|
|
875
|
+
})
|
|
876
|
+
.exec();
|
|
877
|
+
if (isBlocked || isBlockedBy) {
|
|
878
|
+
result.skippedCount++;
|
|
879
|
+
result.errors.push(`Skipped blocked user: ${username}`);
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
// Check if already following
|
|
883
|
+
const existingFollow = await this.followsCollection
|
|
884
|
+
.findOne({
|
|
885
|
+
followerId: userId,
|
|
886
|
+
followedId: profile._id,
|
|
887
|
+
})
|
|
888
|
+
.exec();
|
|
889
|
+
if (existingFollow) {
|
|
890
|
+
// Already following, count as success (idempotent)
|
|
891
|
+
result.successCount++;
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
// Create follow relationship
|
|
895
|
+
await this.followsCollection.create({
|
|
896
|
+
_id: (0, crypto_1.randomUUID)(),
|
|
897
|
+
followerId: userId,
|
|
898
|
+
followedId: profile._id,
|
|
899
|
+
createdAt: new Date().toISOString(),
|
|
900
|
+
});
|
|
901
|
+
result.successCount++;
|
|
902
|
+
}
|
|
903
|
+
// Record import timestamp for rate limiting
|
|
904
|
+
this.lastImportTimestamps.set(userId, Date.now());
|
|
905
|
+
return result;
|
|
906
|
+
}
|
|
907
|
+
async importList(userId, data, format) {
|
|
908
|
+
this.checkImportRateLimit(userId);
|
|
909
|
+
let listName;
|
|
910
|
+
let description;
|
|
911
|
+
let visibility = brighthub_lib_1.ConnectionVisibility.Private;
|
|
912
|
+
let usernames;
|
|
913
|
+
if (format === 'csv') {
|
|
914
|
+
// CSV: first line is list name, rest are usernames
|
|
915
|
+
const lines = data
|
|
916
|
+
.split(/\r?\n/)
|
|
917
|
+
.map((line) => line.trim())
|
|
918
|
+
.filter((line) => line.length > 0 && !line.startsWith('#'));
|
|
919
|
+
if (lines.length === 0) {
|
|
920
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.InvalidImportFormat, 'CSV data is empty. Expected at least a list name on the first line.');
|
|
921
|
+
}
|
|
922
|
+
listName = lines[0];
|
|
923
|
+
usernames = lines.slice(1);
|
|
924
|
+
}
|
|
925
|
+
else {
|
|
926
|
+
try {
|
|
927
|
+
const parsed = JSON.parse(data);
|
|
928
|
+
listName = parsed.listName || parsed.name;
|
|
929
|
+
if (!listName || typeof listName !== 'string') {
|
|
930
|
+
throw new Error('Missing list name');
|
|
931
|
+
}
|
|
932
|
+
description = parsed.description;
|
|
933
|
+
if (parsed.visibility &&
|
|
934
|
+
Object.values(brighthub_lib_1.ConnectionVisibility).includes(parsed.visibility)) {
|
|
935
|
+
visibility = parsed.visibility;
|
|
936
|
+
}
|
|
937
|
+
if (parsed.members && Array.isArray(parsed.members)) {
|
|
938
|
+
usernames = parsed.members
|
|
939
|
+
.map((m) => typeof m === 'string' ? m : m.username || '')
|
|
940
|
+
.filter((u) => u.length > 0);
|
|
941
|
+
}
|
|
942
|
+
else {
|
|
943
|
+
usernames = [];
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
catch {
|
|
947
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.InvalidImportFormat, 'Invalid JSON format. Expected { listName, members: [{ username }] }.');
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
// Create the list
|
|
951
|
+
const list = await this.createList(userId, listName, {
|
|
952
|
+
description,
|
|
953
|
+
visibility,
|
|
954
|
+
});
|
|
955
|
+
// Deduplicate
|
|
956
|
+
const uniqueUsernames = [...new Set(usernames)];
|
|
957
|
+
const result = {
|
|
958
|
+
successCount: 0,
|
|
959
|
+
skippedCount: 0,
|
|
960
|
+
totalCount: uniqueUsernames.length,
|
|
961
|
+
skippedUsernames: [],
|
|
962
|
+
errors: [],
|
|
963
|
+
};
|
|
964
|
+
const validUserIds = [];
|
|
965
|
+
for (const username of uniqueUsernames) {
|
|
966
|
+
const profile = await this.findUserByUsername(username);
|
|
967
|
+
if (!profile) {
|
|
968
|
+
result.skippedCount++;
|
|
969
|
+
result.skippedUsernames.push(username);
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
// Skip blocked users during list import (Requirement 32.5)
|
|
973
|
+
const isBlocked = await this.isBlockedFromList(profile._id, userId);
|
|
974
|
+
if (isBlocked) {
|
|
975
|
+
result.skippedCount++;
|
|
976
|
+
result.errors.push(`Skipped blocked user: ${username}`);
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
// Check if this user is a connection (followed by the importer)
|
|
980
|
+
const existingFollow = await this.followsCollection
|
|
981
|
+
.findOne({
|
|
982
|
+
followerId: userId,
|
|
983
|
+
followedId: profile._id,
|
|
984
|
+
})
|
|
985
|
+
.exec();
|
|
986
|
+
if (!existingFollow) {
|
|
987
|
+
result.skippedCount++;
|
|
988
|
+
result.errors.push(`${username} is not a connection; only existing connections can be added to lists`);
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
validUserIds.push(profile._id);
|
|
992
|
+
result.successCount++;
|
|
993
|
+
}
|
|
994
|
+
// Add valid users to the list in bulk
|
|
995
|
+
if (validUserIds.length > 0) {
|
|
996
|
+
await this.addMembersToList(list._id, userId, validUserIds);
|
|
997
|
+
}
|
|
998
|
+
// Record import timestamp for rate limiting
|
|
999
|
+
this.lastImportTimestamps.set(userId, Date.now());
|
|
1000
|
+
return result;
|
|
1001
|
+
}
|
|
1002
|
+
// ═══════════════════════════════════════════════════════
|
|
1003
|
+
// Priority Connection Operations (Requirements 23.1-23.5)
|
|
1004
|
+
// ═══════════════════════════════════════════════════════
|
|
1005
|
+
async setPriority(userId, connectionId, isPriority) {
|
|
1006
|
+
// If setting priority=true, enforce the 50 limit
|
|
1007
|
+
if (isPriority) {
|
|
1008
|
+
const allMetadata = await this.metadataCollection
|
|
1009
|
+
.find({ userId, isPriority: true })
|
|
1010
|
+
.exec();
|
|
1011
|
+
// Check if this connection is already priority (don't count it against the limit)
|
|
1012
|
+
const existingForConnection = allMetadata.find((m) => m.connectionId === connectionId);
|
|
1013
|
+
const currentPriorityCount = allMetadata.length;
|
|
1014
|
+
if (!existingForConnection?.isPriority &&
|
|
1015
|
+
currentPriorityCount >= brighthub_lib_1.MAX_PRIORITY_CONNECTIONS) {
|
|
1016
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.PriorityLimitExceeded, `Cannot have more than ${brighthub_lib_1.MAX_PRIORITY_CONNECTIONS} priority connections`);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
// Check if metadata record already exists for this user+connection
|
|
1020
|
+
const existing = await this.metadataCollection
|
|
1021
|
+
.findOne({
|
|
1022
|
+
userId,
|
|
1023
|
+
connectionId,
|
|
1024
|
+
})
|
|
1025
|
+
.exec();
|
|
1026
|
+
const now = new Date().toISOString();
|
|
1027
|
+
if (existing) {
|
|
1028
|
+
// Update existing record
|
|
1029
|
+
await this.metadataCollection
|
|
1030
|
+
.updateOne({ _id: existing._id }, {
|
|
1031
|
+
isPriority,
|
|
1032
|
+
updatedAt: now,
|
|
1033
|
+
})
|
|
1034
|
+
.exec();
|
|
1035
|
+
}
|
|
1036
|
+
else {
|
|
1037
|
+
// Create new metadata record (upsert)
|
|
1038
|
+
await this.metadataCollection.create({
|
|
1039
|
+
_id: (0, crypto_1.randomUUID)(),
|
|
1040
|
+
userId,
|
|
1041
|
+
connectionId,
|
|
1042
|
+
isPriority,
|
|
1043
|
+
isQuiet: false,
|
|
1044
|
+
createdAt: now,
|
|
1045
|
+
updatedAt: now,
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
async getPriorityConnections(userId, options) {
|
|
1050
|
+
const limit = this.getLimit(options);
|
|
1051
|
+
const queryLimit = limit + 1;
|
|
1052
|
+
// Get all priority metadata records for this user
|
|
1053
|
+
let query = this.metadataCollection.find({
|
|
1054
|
+
userId,
|
|
1055
|
+
isPriority: true,
|
|
1056
|
+
});
|
|
1057
|
+
if (query.sort) {
|
|
1058
|
+
query = query.sort({ updatedAt: -1 });
|
|
1059
|
+
}
|
|
1060
|
+
const allRecords = await query.exec();
|
|
1061
|
+
let filteredRecords = allRecords;
|
|
1062
|
+
if (options?.cursor) {
|
|
1063
|
+
filteredRecords = allRecords.filter((r) => r.updatedAt < options.cursor);
|
|
1064
|
+
}
|
|
1065
|
+
const paginatedRecords = filteredRecords.slice(0, queryLimit);
|
|
1066
|
+
const hasMore = paginatedRecords.length > limit;
|
|
1067
|
+
const resultRecords = hasMore
|
|
1068
|
+
? paginatedRecords.slice(0, limit)
|
|
1069
|
+
: paginatedRecords;
|
|
1070
|
+
// Look up user profiles for each priority connection
|
|
1071
|
+
const profiles = [];
|
|
1072
|
+
for (const record of resultRecords) {
|
|
1073
|
+
const userRecord = await this.userProfilesCollection
|
|
1074
|
+
.findOne({
|
|
1075
|
+
_id: record.connectionId,
|
|
1076
|
+
})
|
|
1077
|
+
.exec();
|
|
1078
|
+
if (userRecord) {
|
|
1079
|
+
profiles.push(this.recordToProfile(userRecord));
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
const lastRecord = resultRecords[resultRecords.length - 1];
|
|
1083
|
+
return {
|
|
1084
|
+
items: profiles,
|
|
1085
|
+
cursor: hasMore && lastRecord ? lastRecord.updatedAt : undefined,
|
|
1086
|
+
hasMore,
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
async setQuietMode(userId, connectionId, isQuiet) {
|
|
1090
|
+
// Check if metadata record already exists for this user+connection
|
|
1091
|
+
const existing = await this.metadataCollection
|
|
1092
|
+
.findOne({
|
|
1093
|
+
userId,
|
|
1094
|
+
connectionId,
|
|
1095
|
+
})
|
|
1096
|
+
.exec();
|
|
1097
|
+
const now = new Date().toISOString();
|
|
1098
|
+
if (existing) {
|
|
1099
|
+
// Update existing record
|
|
1100
|
+
await this.metadataCollection
|
|
1101
|
+
.updateOne({ _id: existing._id }, {
|
|
1102
|
+
isQuiet,
|
|
1103
|
+
updatedAt: now,
|
|
1104
|
+
})
|
|
1105
|
+
.exec();
|
|
1106
|
+
}
|
|
1107
|
+
else {
|
|
1108
|
+
// Create new metadata record (upsert)
|
|
1109
|
+
await this.metadataCollection.create({
|
|
1110
|
+
_id: (0, crypto_1.randomUUID)(),
|
|
1111
|
+
userId,
|
|
1112
|
+
connectionId,
|
|
1113
|
+
isPriority: false,
|
|
1114
|
+
isQuiet,
|
|
1115
|
+
createdAt: now,
|
|
1116
|
+
updatedAt: now,
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
async getQuietConnections(userId, options) {
|
|
1121
|
+
const limit = this.getLimit(options);
|
|
1122
|
+
const queryLimit = limit + 1;
|
|
1123
|
+
// Get all quiet metadata records for this user
|
|
1124
|
+
let query = this.metadataCollection.find({
|
|
1125
|
+
userId,
|
|
1126
|
+
isQuiet: true,
|
|
1127
|
+
});
|
|
1128
|
+
if (query.sort) {
|
|
1129
|
+
query = query.sort({ updatedAt: -1 });
|
|
1130
|
+
}
|
|
1131
|
+
const allRecords = await query.exec();
|
|
1132
|
+
let filteredRecords = allRecords;
|
|
1133
|
+
if (options?.cursor) {
|
|
1134
|
+
filteredRecords = allRecords.filter((r) => r.updatedAt < options.cursor);
|
|
1135
|
+
}
|
|
1136
|
+
const paginatedRecords = filteredRecords.slice(0, queryLimit);
|
|
1137
|
+
const hasMore = paginatedRecords.length > limit;
|
|
1138
|
+
const resultRecords = hasMore
|
|
1139
|
+
? paginatedRecords.slice(0, limit)
|
|
1140
|
+
: paginatedRecords;
|
|
1141
|
+
// Look up user profiles for each quiet connection
|
|
1142
|
+
const profiles = [];
|
|
1143
|
+
for (const record of resultRecords) {
|
|
1144
|
+
const userRecord = await this.userProfilesCollection
|
|
1145
|
+
.findOne({
|
|
1146
|
+
_id: record.connectionId,
|
|
1147
|
+
})
|
|
1148
|
+
.exec();
|
|
1149
|
+
if (userRecord) {
|
|
1150
|
+
profiles.push(this.recordToProfile(userRecord));
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
const lastRecord = resultRecords[resultRecords.length - 1];
|
|
1154
|
+
return {
|
|
1155
|
+
items: profiles,
|
|
1156
|
+
cursor: hasMore && lastRecord ? lastRecord.updatedAt : undefined,
|
|
1157
|
+
hasMore,
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Calculate the expiration date for a mute duration.
|
|
1162
|
+
* @param duration The mute duration
|
|
1163
|
+
* @returns ISO string of the expiration date
|
|
1164
|
+
*/
|
|
1165
|
+
calculateExpiresAt(duration) {
|
|
1166
|
+
const durationToMs = {
|
|
1167
|
+
'1h': 3600000,
|
|
1168
|
+
'8h': 28800000,
|
|
1169
|
+
'24h': 86400000,
|
|
1170
|
+
'7d': 604800000,
|
|
1171
|
+
'30d': 2592000000,
|
|
1172
|
+
// ~100 years for permanent mutes
|
|
1173
|
+
permanent: 100 * 365.25 * 24 * 60 * 60 * 1000,
|
|
1174
|
+
};
|
|
1175
|
+
const ms = durationToMs[duration];
|
|
1176
|
+
return new Date(Date.now() + ms).toISOString();
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Set a temporary mute on a connection with a specified duration.
|
|
1180
|
+
* Creates or updates a mute record with a calculated expiration timestamp.
|
|
1181
|
+
* @see Requirements: 25.1, 25.2
|
|
1182
|
+
*/
|
|
1183
|
+
async setTemporaryMute(userId, connectionId, duration) {
|
|
1184
|
+
const now = new Date().toISOString();
|
|
1185
|
+
const expiresAt = this.calculateExpiresAt(duration);
|
|
1186
|
+
// Check if a mute already exists for this user+connection
|
|
1187
|
+
const existing = await this.temporaryMutesCollection
|
|
1188
|
+
.findOne({
|
|
1189
|
+
userId,
|
|
1190
|
+
connectionId,
|
|
1191
|
+
})
|
|
1192
|
+
.exec();
|
|
1193
|
+
if (existing) {
|
|
1194
|
+
// Update existing mute with new duration and expiration
|
|
1195
|
+
await this.temporaryMutesCollection
|
|
1196
|
+
.updateOne({ _id: existing._id }, {
|
|
1197
|
+
duration,
|
|
1198
|
+
expiresAt,
|
|
1199
|
+
createdAt: now,
|
|
1200
|
+
})
|
|
1201
|
+
.exec();
|
|
1202
|
+
}
|
|
1203
|
+
else {
|
|
1204
|
+
// Create new mute record
|
|
1205
|
+
await this.temporaryMutesCollection.create({
|
|
1206
|
+
_id: (0, crypto_1.randomUUID)(),
|
|
1207
|
+
userId,
|
|
1208
|
+
connectionId,
|
|
1209
|
+
duration,
|
|
1210
|
+
expiresAt,
|
|
1211
|
+
createdAt: now,
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* Remove a temporary mute before it expires (early unmute).
|
|
1217
|
+
* @see Requirements: 25.7
|
|
1218
|
+
*/
|
|
1219
|
+
async removeTemporaryMute(userId, connectionId) {
|
|
1220
|
+
await this.temporaryMutesCollection
|
|
1221
|
+
.deleteOne({
|
|
1222
|
+
userId,
|
|
1223
|
+
connectionId,
|
|
1224
|
+
})
|
|
1225
|
+
.exec();
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* Convert a temporary mute to a permanent mute.
|
|
1229
|
+
* @see Requirements: 25.6
|
|
1230
|
+
*/
|
|
1231
|
+
async convertToPermanentMute(userId, connectionId) {
|
|
1232
|
+
const existing = await this.temporaryMutesCollection
|
|
1233
|
+
.findOne({
|
|
1234
|
+
userId,
|
|
1235
|
+
connectionId,
|
|
1236
|
+
})
|
|
1237
|
+
.exec();
|
|
1238
|
+
if (!existing) {
|
|
1239
|
+
// No existing mute — create a permanent one
|
|
1240
|
+
await this.setTemporaryMute(userId, connectionId, brighthub_lib_1.MuteDuration.Permanent);
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
const permanentExpiresAt = this.calculateExpiresAt(brighthub_lib_1.MuteDuration.Permanent);
|
|
1244
|
+
await this.temporaryMutesCollection
|
|
1245
|
+
.updateOne({ _id: existing._id }, {
|
|
1246
|
+
duration: brighthub_lib_1.MuteDuration.Permanent,
|
|
1247
|
+
expiresAt: permanentExpiresAt,
|
|
1248
|
+
})
|
|
1249
|
+
.exec();
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Get all muted connections for a user with their expiration info.
|
|
1253
|
+
* Only returns currently active (non-expired) mutes.
|
|
1254
|
+
* @see Requirements: 25.5
|
|
1255
|
+
*/
|
|
1256
|
+
async getMutedConnections(userId, options) {
|
|
1257
|
+
const limit = this.getLimit(options);
|
|
1258
|
+
const queryLimit = limit + 1;
|
|
1259
|
+
const now = new Date().toISOString();
|
|
1260
|
+
// Get all mute records for this user
|
|
1261
|
+
let query = this.temporaryMutesCollection.find({
|
|
1262
|
+
userId,
|
|
1263
|
+
});
|
|
1264
|
+
if (query.sort) {
|
|
1265
|
+
query = query.sort({ expiresAt: 1 });
|
|
1266
|
+
}
|
|
1267
|
+
const allRecords = await query.exec();
|
|
1268
|
+
// Filter to only active (non-expired) mutes
|
|
1269
|
+
let filteredRecords = allRecords.filter((r) => r.expiresAt > now);
|
|
1270
|
+
// Apply cursor-based pagination
|
|
1271
|
+
if (options?.cursor) {
|
|
1272
|
+
filteredRecords = filteredRecords.filter((r) => r.expiresAt > options.cursor);
|
|
1273
|
+
}
|
|
1274
|
+
const paginatedRecords = filteredRecords.slice(0, queryLimit);
|
|
1275
|
+
const hasMore = paginatedRecords.length > limit;
|
|
1276
|
+
const resultRecords = hasMore
|
|
1277
|
+
? paginatedRecords.slice(0, limit)
|
|
1278
|
+
: paginatedRecords;
|
|
1279
|
+
// Look up user profiles and attach mute info
|
|
1280
|
+
const results = [];
|
|
1281
|
+
for (const record of resultRecords) {
|
|
1282
|
+
const userRecord = await this.userProfilesCollection
|
|
1283
|
+
.findOne({
|
|
1284
|
+
_id: record.connectionId,
|
|
1285
|
+
})
|
|
1286
|
+
.exec();
|
|
1287
|
+
if (userRecord) {
|
|
1288
|
+
results.push({
|
|
1289
|
+
...this.recordToProfile(userRecord),
|
|
1290
|
+
expiresAt: record.expiresAt,
|
|
1291
|
+
duration: record.duration,
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
const lastRecord = resultRecords[resultRecords.length - 1];
|
|
1296
|
+
return {
|
|
1297
|
+
items: results,
|
|
1298
|
+
cursor: hasMore && lastRecord ? lastRecord.expiresAt : undefined,
|
|
1299
|
+
hasMore,
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
// ═══════════════════════════════════════════════════════
|
|
1303
|
+
// Hub Operations (Requirements 30.1-30.10)
|
|
1304
|
+
// ═══════════════════════════════════════════════════════
|
|
1305
|
+
/**
|
|
1306
|
+
* Convert a hub database record to the API response format
|
|
1307
|
+
*/
|
|
1308
|
+
recordToHub(record) {
|
|
1309
|
+
return {
|
|
1310
|
+
_id: record._id,
|
|
1311
|
+
ownerId: record.ownerId,
|
|
1312
|
+
name: record.name,
|
|
1313
|
+
memberCount: record.memberCount,
|
|
1314
|
+
isDefault: record.isDefault,
|
|
1315
|
+
createdAt: record.createdAt,
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* Validate hub name
|
|
1320
|
+
*/
|
|
1321
|
+
validateHubName(name) {
|
|
1322
|
+
if (!name || name.trim().length === 0) {
|
|
1323
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.InvalidHubName, 'Hub name cannot be empty');
|
|
1324
|
+
}
|
|
1325
|
+
if (name.trim().length > 50) {
|
|
1326
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.InvalidHubName, 'Hub name cannot exceed 50 characters');
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Get an owned hub or throw
|
|
1331
|
+
*/
|
|
1332
|
+
async getOwnedHub(hubId, ownerId) {
|
|
1333
|
+
const hub = await this.hubsCollection
|
|
1334
|
+
.findOne({ _id: hubId })
|
|
1335
|
+
.exec();
|
|
1336
|
+
if (!hub) {
|
|
1337
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.HubNotFound, `Hub ${hubId} not found`);
|
|
1338
|
+
}
|
|
1339
|
+
if (hub.ownerId !== ownerId) {
|
|
1340
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.HubNotAuthorized, 'Not authorized to modify this hub');
|
|
1341
|
+
}
|
|
1342
|
+
return hub;
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* Create a new hub
|
|
1346
|
+
* @see Requirements: 30.1, 30.9
|
|
1347
|
+
*/
|
|
1348
|
+
async createHub(ownerId, name) {
|
|
1349
|
+
this.validateHubName(name);
|
|
1350
|
+
// Check hub limit
|
|
1351
|
+
const existingHubs = await this.hubsCollection
|
|
1352
|
+
.find({ ownerId })
|
|
1353
|
+
.exec();
|
|
1354
|
+
if (existingHubs.length >= brighthub_lib_1.MAX_HUBS_PER_USER) {
|
|
1355
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.HubLimitExceeded, `Cannot create more than ${brighthub_lib_1.MAX_HUBS_PER_USER} hubs`);
|
|
1356
|
+
}
|
|
1357
|
+
const now = new Date().toISOString();
|
|
1358
|
+
const record = {
|
|
1359
|
+
_id: (0, crypto_1.randomUUID)(),
|
|
1360
|
+
ownerId,
|
|
1361
|
+
name: name.trim(),
|
|
1362
|
+
memberCount: 0,
|
|
1363
|
+
isDefault: false,
|
|
1364
|
+
createdAt: now,
|
|
1365
|
+
};
|
|
1366
|
+
const created = await this.hubsCollection.create(record);
|
|
1367
|
+
return this.recordToHub(created);
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* Delete a hub and all its memberships.
|
|
1371
|
+
* Cannot delete the default "Close Friends" hub.
|
|
1372
|
+
* @see Requirements: 30.1
|
|
1373
|
+
*/
|
|
1374
|
+
async deleteHub(hubId, ownerId) {
|
|
1375
|
+
const hub = await this.getOwnedHub(hubId, ownerId);
|
|
1376
|
+
if (hub.isDefault) {
|
|
1377
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.CannotDeleteDefaultHub, 'Cannot delete the default hub');
|
|
1378
|
+
}
|
|
1379
|
+
// Delete all memberships
|
|
1380
|
+
const members = await this.hubMembersCollection
|
|
1381
|
+
.find({ hubId })
|
|
1382
|
+
.exec();
|
|
1383
|
+
for (const member of members) {
|
|
1384
|
+
await this.hubMembersCollection
|
|
1385
|
+
.deleteOne({ _id: member._id })
|
|
1386
|
+
.exec();
|
|
1387
|
+
}
|
|
1388
|
+
// Delete the hub
|
|
1389
|
+
await this.hubsCollection
|
|
1390
|
+
.deleteOne({ _id: hubId })
|
|
1391
|
+
.exec();
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Add members to a hub (bulk support)
|
|
1395
|
+
* @see Requirements: 30.2, 30.10
|
|
1396
|
+
*/
|
|
1397
|
+
async addToHub(hubId, ownerId, userIds) {
|
|
1398
|
+
const hub = await this.getOwnedHub(hubId, ownerId);
|
|
1399
|
+
if (hub.memberCount + userIds.length > brighthub_lib_1.MAX_MEMBERS_PER_HUB) {
|
|
1400
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.HubMemberLimitExceeded, `Cannot exceed ${brighthub_lib_1.MAX_MEMBERS_PER_HUB} members per hub`);
|
|
1401
|
+
}
|
|
1402
|
+
const now = new Date().toISOString();
|
|
1403
|
+
let addedCount = 0;
|
|
1404
|
+
for (const userId of userIds) {
|
|
1405
|
+
// Check if already a member
|
|
1406
|
+
const existing = await this.hubMembersCollection
|
|
1407
|
+
.findOne({
|
|
1408
|
+
hubId,
|
|
1409
|
+
userId,
|
|
1410
|
+
})
|
|
1411
|
+
.exec();
|
|
1412
|
+
if (existing) {
|
|
1413
|
+
// Skip duplicates silently for bulk operations
|
|
1414
|
+
continue;
|
|
1415
|
+
}
|
|
1416
|
+
await this.hubMembersCollection.create({
|
|
1417
|
+
_id: (0, crypto_1.randomUUID)(),
|
|
1418
|
+
hubId,
|
|
1419
|
+
userId,
|
|
1420
|
+
addedAt: now,
|
|
1421
|
+
});
|
|
1422
|
+
addedCount++;
|
|
1423
|
+
}
|
|
1424
|
+
// Update member count
|
|
1425
|
+
if (addedCount > 0) {
|
|
1426
|
+
await this.hubsCollection
|
|
1427
|
+
.updateOne({ _id: hubId }, {
|
|
1428
|
+
memberCount: hub.memberCount + addedCount,
|
|
1429
|
+
})
|
|
1430
|
+
.exec();
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
/**
|
|
1434
|
+
* Remove members from a hub (bulk support)
|
|
1435
|
+
* @see Requirements: 30.2
|
|
1436
|
+
*/
|
|
1437
|
+
async removeFromHub(hubId, ownerId, userIds) {
|
|
1438
|
+
const hub = await this.getOwnedHub(hubId, ownerId);
|
|
1439
|
+
let removedCount = 0;
|
|
1440
|
+
for (const userId of userIds) {
|
|
1441
|
+
const result = await this.hubMembersCollection
|
|
1442
|
+
.deleteOne({
|
|
1443
|
+
hubId,
|
|
1444
|
+
userId,
|
|
1445
|
+
})
|
|
1446
|
+
.exec();
|
|
1447
|
+
if (result.deletedCount > 0) {
|
|
1448
|
+
removedCount++;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
// Update member count
|
|
1452
|
+
if (removedCount > 0) {
|
|
1453
|
+
const newCount = Math.max(0, hub.memberCount - removedCount);
|
|
1454
|
+
await this.hubsCollection
|
|
1455
|
+
.updateOne({ _id: hubId }, { memberCount: newCount })
|
|
1456
|
+
.exec();
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* Get all hubs owned by a user
|
|
1461
|
+
* @see Requirements: 30.1
|
|
1462
|
+
*/
|
|
1463
|
+
async getHubs(ownerId) {
|
|
1464
|
+
const records = await this.hubsCollection
|
|
1465
|
+
.find({ ownerId })
|
|
1466
|
+
.exec();
|
|
1467
|
+
return records.map((r) => this.recordToHub(r));
|
|
1468
|
+
}
|
|
1469
|
+
/**
|
|
1470
|
+
* Get members of a hub with pagination
|
|
1471
|
+
* @see Requirements: 30.2
|
|
1472
|
+
*/
|
|
1473
|
+
async getHubMembers(hubId, options) {
|
|
1474
|
+
const limit = this.getLimit(options);
|
|
1475
|
+
let query = this.hubMembersCollection.find({
|
|
1476
|
+
hubId,
|
|
1477
|
+
});
|
|
1478
|
+
if (options?.cursor && query.sort) {
|
|
1479
|
+
// Use cursor-based pagination by filtering addedAt
|
|
1480
|
+
query = this.hubMembersCollection.find({
|
|
1481
|
+
hubId,
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
if (query.sort) {
|
|
1485
|
+
query = query.sort({ addedAt: -1 });
|
|
1486
|
+
}
|
|
1487
|
+
if (query.skip && options?.cursor) {
|
|
1488
|
+
// Simple offset approach: count records before cursor
|
|
1489
|
+
}
|
|
1490
|
+
if (query.limit) {
|
|
1491
|
+
query = query.limit(limit + 1);
|
|
1492
|
+
}
|
|
1493
|
+
const memberRecords = await query.exec();
|
|
1494
|
+
// Filter by cursor if provided
|
|
1495
|
+
const filteredRecords = options?.cursor
|
|
1496
|
+
? memberRecords.filter((r) => r.addedAt < options.cursor)
|
|
1497
|
+
: memberRecords;
|
|
1498
|
+
const hasMore = filteredRecords.length > limit;
|
|
1499
|
+
const resultRecords = hasMore
|
|
1500
|
+
? filteredRecords.slice(0, limit)
|
|
1501
|
+
: filteredRecords;
|
|
1502
|
+
// Look up user profiles
|
|
1503
|
+
const results = [];
|
|
1504
|
+
for (const record of resultRecords) {
|
|
1505
|
+
const userRecord = await this.userProfilesCollection
|
|
1506
|
+
.findOne({ _id: record.userId })
|
|
1507
|
+
.exec();
|
|
1508
|
+
if (userRecord) {
|
|
1509
|
+
results.push(this.recordToProfile(userRecord));
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
const lastRecord = resultRecords[resultRecords.length - 1];
|
|
1513
|
+
return {
|
|
1514
|
+
items: results,
|
|
1515
|
+
cursor: hasMore && lastRecord ? lastRecord.addedAt : undefined,
|
|
1516
|
+
hasMore,
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
/**
|
|
1520
|
+
* Get or create the default "Close Friends" hub for a user
|
|
1521
|
+
* @see Requirements: 30.6
|
|
1522
|
+
*/
|
|
1523
|
+
async getOrCreateDefaultHub(ownerId) {
|
|
1524
|
+
// Try to find existing default hub
|
|
1525
|
+
const existing = await this.hubsCollection
|
|
1526
|
+
.findOne({
|
|
1527
|
+
ownerId,
|
|
1528
|
+
isDefault: true,
|
|
1529
|
+
})
|
|
1530
|
+
.exec();
|
|
1531
|
+
if (existing) {
|
|
1532
|
+
return this.recordToHub(existing);
|
|
1533
|
+
}
|
|
1534
|
+
// Create the default hub
|
|
1535
|
+
const now = new Date().toISOString();
|
|
1536
|
+
const record = {
|
|
1537
|
+
_id: (0, crypto_1.randomUUID)(),
|
|
1538
|
+
ownerId,
|
|
1539
|
+
name: brighthub_lib_1.DEFAULT_HUB_NAME,
|
|
1540
|
+
memberCount: 0,
|
|
1541
|
+
isDefault: true,
|
|
1542
|
+
createdAt: now,
|
|
1543
|
+
};
|
|
1544
|
+
const created = await this.hubsCollection.create(record);
|
|
1545
|
+
return this.recordToHub(created);
|
|
1546
|
+
}
|
|
1547
|
+
// ── Mutual Connections ──────────────────────────────────────────────
|
|
1548
|
+
/**
|
|
1549
|
+
* Build a sorted cache key so that (A,B) and (B,A) share the same entry.
|
|
1550
|
+
*/
|
|
1551
|
+
mutualCacheKey(userId, otherUserId) {
|
|
1552
|
+
return [userId, otherUserId].sort().join(':');
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1555
|
+
* Get the set of user IDs that a given user follows.
|
|
1556
|
+
*/
|
|
1557
|
+
async getFollowedIds(userId) {
|
|
1558
|
+
const follows = await this.followsCollection
|
|
1559
|
+
.find({ followerId: userId })
|
|
1560
|
+
.exec();
|
|
1561
|
+
return new Set(follows.map((f) => f.followedId));
|
|
1562
|
+
}
|
|
1563
|
+
/**
|
|
1564
|
+
* Get mutual connections between two users with pagination.
|
|
1565
|
+
* Mutual connections are users that both userId AND otherUserId follow.
|
|
1566
|
+
* @see Requirements: 28.1, 28.2
|
|
1567
|
+
*/
|
|
1568
|
+
async getMutualConnections(userId, otherUserId, options) {
|
|
1569
|
+
const limit = this.getLimit(options);
|
|
1570
|
+
// Get follows for both users
|
|
1571
|
+
const [userFollows, otherFollows] = await Promise.all([
|
|
1572
|
+
this.getFollowedIds(userId),
|
|
1573
|
+
this.getFollowedIds(otherUserId),
|
|
1574
|
+
]);
|
|
1575
|
+
// Compute intersection
|
|
1576
|
+
const mutualIds = [];
|
|
1577
|
+
for (const id of userFollows) {
|
|
1578
|
+
if (otherFollows.has(id)) {
|
|
1579
|
+
mutualIds.push(id);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
// Sort for deterministic pagination
|
|
1583
|
+
mutualIds.sort();
|
|
1584
|
+
// Apply cursor-based pagination
|
|
1585
|
+
let startIndex = 0;
|
|
1586
|
+
if (options?.cursor) {
|
|
1587
|
+
const cursorIdx = mutualIds.indexOf(options.cursor);
|
|
1588
|
+
if (cursorIdx !== -1) {
|
|
1589
|
+
startIndex = cursorIdx + 1;
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
const page = mutualIds.slice(startIndex, startIndex + limit);
|
|
1593
|
+
const hasMore = startIndex + limit < mutualIds.length;
|
|
1594
|
+
// Look up profiles for the page
|
|
1595
|
+
const profiles = [];
|
|
1596
|
+
for (const id of page) {
|
|
1597
|
+
const record = await this.userProfilesCollection
|
|
1598
|
+
.findOne({ _id: id })
|
|
1599
|
+
.exec();
|
|
1600
|
+
if (record) {
|
|
1601
|
+
profiles.push(this.recordToProfile(record));
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
return {
|
|
1605
|
+
items: profiles,
|
|
1606
|
+
cursor: hasMore && page.length > 0 ? page[page.length - 1] : undefined,
|
|
1607
|
+
hasMore,
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
/**
|
|
1611
|
+
* Get the count of mutual connections between two users.
|
|
1612
|
+
* Uses an in-memory cache with a 5-minute TTL for performance.
|
|
1613
|
+
* @see Requirements: 28.1, 28.5
|
|
1614
|
+
*/
|
|
1615
|
+
async getMutualConnectionCount(userId, otherUserId) {
|
|
1616
|
+
const key = this.mutualCacheKey(userId, otherUserId);
|
|
1617
|
+
const cached = this.mutualConnectionCache.get(key);
|
|
1618
|
+
const now = Date.now();
|
|
1619
|
+
if (cached &&
|
|
1620
|
+
now - cached.timestamp < ConnectionService.MUTUAL_CACHE_TTL_MS) {
|
|
1621
|
+
return cached.count;
|
|
1622
|
+
}
|
|
1623
|
+
// Cache miss – compute intersection count
|
|
1624
|
+
const [userFollows, otherFollows] = await Promise.all([
|
|
1625
|
+
this.getFollowedIds(userId),
|
|
1626
|
+
this.getFollowedIds(otherUserId),
|
|
1627
|
+
]);
|
|
1628
|
+
let count = 0;
|
|
1629
|
+
for (const id of userFollows) {
|
|
1630
|
+
if (otherFollows.has(id)) {
|
|
1631
|
+
count++;
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
this.mutualConnectionCache.set(key, { count, timestamp: now });
|
|
1635
|
+
return count;
|
|
1636
|
+
}
|
|
1637
|
+
// ═══════════════════════════════════════════════════════
|
|
1638
|
+
// Block/Mute Inheritance for Lists (Requirements 32.1-32.5)
|
|
1639
|
+
// ═══════════════════════════════════════════════════════
|
|
1640
|
+
/**
|
|
1641
|
+
* Remove a blocked user from ALL lists owned by the blocker.
|
|
1642
|
+
* Finds every list owned by ownerId, removes blockedUserId from each,
|
|
1643
|
+
* and decrements member counts accordingly.
|
|
1644
|
+
* @see Requirements: 32.1
|
|
1645
|
+
*/
|
|
1646
|
+
async removeBlockedUserFromLists(ownerId, blockedUserId) {
|
|
1647
|
+
// Get all lists owned by the blocker
|
|
1648
|
+
const lists = await this.listsCollection
|
|
1649
|
+
.find({ ownerId })
|
|
1650
|
+
.exec();
|
|
1651
|
+
for (const list of lists) {
|
|
1652
|
+
// Try to remove the blocked user from this list's members
|
|
1653
|
+
const result = await this.listMembersCollection
|
|
1654
|
+
.deleteOne({
|
|
1655
|
+
listId: list._id,
|
|
1656
|
+
userId: blockedUserId,
|
|
1657
|
+
})
|
|
1658
|
+
.exec();
|
|
1659
|
+
// Decrement member count if a record was actually removed
|
|
1660
|
+
if (result.deletedCount > 0) {
|
|
1661
|
+
const newCount = Math.max(0, list.memberCount - 1);
|
|
1662
|
+
await this.listsCollection
|
|
1663
|
+
.updateOne({ _id: list._id }, {
|
|
1664
|
+
memberCount: newCount,
|
|
1665
|
+
updatedAt: new Date().toISOString(),
|
|
1666
|
+
})
|
|
1667
|
+
.exec();
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
/**
|
|
1672
|
+
* Check if a user is blocked by the list owner.
|
|
1673
|
+
* Used to prevent blocked users from viewing or following lists.
|
|
1674
|
+
* @see Requirements: 32.2, 32.3
|
|
1675
|
+
*/
|
|
1676
|
+
async isBlockedFromList(userId, listOwnerId) {
|
|
1677
|
+
const block = await this.blocksCollection
|
|
1678
|
+
.findOne({
|
|
1679
|
+
blockerId: listOwnerId,
|
|
1680
|
+
blockedId: userId,
|
|
1681
|
+
})
|
|
1682
|
+
.exec();
|
|
1683
|
+
return block !== null;
|
|
1684
|
+
}
|
|
1685
|
+
// ═══════════════════════════════════════════════════════
|
|
1686
|
+
// List Following Operations
|
|
1687
|
+
// ═══════════════════════════════════════════════════════
|
|
1688
|
+
/**
|
|
1689
|
+
* Follow a public list. Verifies the list exists, is public,
|
|
1690
|
+
* the user is not blocked, and is not already following.
|
|
1691
|
+
* Increments the list's followerCount.
|
|
1692
|
+
* @see Requirements: 33.1, 33.3
|
|
1693
|
+
*/
|
|
1694
|
+
async followList(userId, listId) {
|
|
1695
|
+
// Verify list exists
|
|
1696
|
+
const list = await this.listsCollection
|
|
1697
|
+
.findOne({ _id: listId })
|
|
1698
|
+
.exec();
|
|
1699
|
+
if (!list) {
|
|
1700
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.ListNotFound, 'List not found');
|
|
1701
|
+
}
|
|
1702
|
+
// Only public lists can be followed
|
|
1703
|
+
if (list.visibility !== brighthub_lib_1.ConnectionVisibility.Public) {
|
|
1704
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.ListNotPublic, 'Only public lists can be followed');
|
|
1705
|
+
}
|
|
1706
|
+
// Check if user is blocked by the list owner
|
|
1707
|
+
const isBlocked = await this.isBlockedFromList(userId, list.ownerId);
|
|
1708
|
+
if (isBlocked) {
|
|
1709
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.ListNotFound, 'List not found');
|
|
1710
|
+
}
|
|
1711
|
+
// Check if already following
|
|
1712
|
+
const existing = await this.listFollowersCollection
|
|
1713
|
+
.findOne({
|
|
1714
|
+
listId,
|
|
1715
|
+
userId,
|
|
1716
|
+
})
|
|
1717
|
+
.exec();
|
|
1718
|
+
if (existing) {
|
|
1719
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.AlreadyFollowingList, 'Already following this list');
|
|
1720
|
+
}
|
|
1721
|
+
// Create follower record
|
|
1722
|
+
const now = new Date().toISOString();
|
|
1723
|
+
await this.listFollowersCollection.create({
|
|
1724
|
+
_id: (0, crypto_1.randomUUID)(),
|
|
1725
|
+
listId,
|
|
1726
|
+
userId,
|
|
1727
|
+
followedAt: now,
|
|
1728
|
+
});
|
|
1729
|
+
// Increment followerCount
|
|
1730
|
+
await this.listsCollection
|
|
1731
|
+
.updateOne({ _id: listId }, {
|
|
1732
|
+
followerCount: list.followerCount + 1,
|
|
1733
|
+
updatedAt: now,
|
|
1734
|
+
})
|
|
1735
|
+
.exec();
|
|
1736
|
+
}
|
|
1737
|
+
/**
|
|
1738
|
+
* Unfollow a list, removing the subscription and decrementing followerCount.
|
|
1739
|
+
* @see Requirements: 33.5
|
|
1740
|
+
*/
|
|
1741
|
+
async unfollowList(userId, listId) {
|
|
1742
|
+
// Verify the follower record exists
|
|
1743
|
+
const existing = await this.listFollowersCollection
|
|
1744
|
+
.findOne({
|
|
1745
|
+
listId,
|
|
1746
|
+
userId,
|
|
1747
|
+
})
|
|
1748
|
+
.exec();
|
|
1749
|
+
if (!existing) {
|
|
1750
|
+
throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.NotFollowingList, 'Not following this list');
|
|
1751
|
+
}
|
|
1752
|
+
// Delete follower record
|
|
1753
|
+
await this.listFollowersCollection
|
|
1754
|
+
.deleteOne({ _id: existing._id })
|
|
1755
|
+
.exec();
|
|
1756
|
+
// Decrement followerCount
|
|
1757
|
+
const list = await this.listsCollection
|
|
1758
|
+
.findOne({ _id: listId })
|
|
1759
|
+
.exec();
|
|
1760
|
+
if (list) {
|
|
1761
|
+
const newCount = Math.max(0, list.followerCount - 1);
|
|
1762
|
+
await this.listsCollection
|
|
1763
|
+
.updateOne({ _id: listId }, {
|
|
1764
|
+
followerCount: newCount,
|
|
1765
|
+
updatedAt: new Date().toISOString(),
|
|
1766
|
+
})
|
|
1767
|
+
.exec();
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
/**
|
|
1771
|
+
* Get all followers of a list with pagination.
|
|
1772
|
+
* @see Requirements: 33.6
|
|
1773
|
+
*/
|
|
1774
|
+
async getListFollowers(listId, options) {
|
|
1775
|
+
const limit = this.getLimit(options);
|
|
1776
|
+
const queryLimit = limit + 1;
|
|
1777
|
+
let query = this.listFollowersCollection.find({
|
|
1778
|
+
listId,
|
|
1779
|
+
});
|
|
1780
|
+
if (query.sort) {
|
|
1781
|
+
query = query.sort({ followedAt: -1 });
|
|
1782
|
+
}
|
|
1783
|
+
const allFollowers = await query.exec();
|
|
1784
|
+
let filteredFollowers = allFollowers;
|
|
1785
|
+
if (options?.cursor) {
|
|
1786
|
+
filteredFollowers = allFollowers.filter((f) => f.followedAt < options.cursor);
|
|
1787
|
+
}
|
|
1788
|
+
const paginatedFollowers = filteredFollowers.slice(0, queryLimit);
|
|
1789
|
+
const hasMore = paginatedFollowers.length > limit;
|
|
1790
|
+
const resultFollowers = hasMore
|
|
1791
|
+
? paginatedFollowers.slice(0, limit)
|
|
1792
|
+
: paginatedFollowers;
|
|
1793
|
+
// Resolve user profiles
|
|
1794
|
+
const items = [];
|
|
1795
|
+
for (const record of resultFollowers) {
|
|
1796
|
+
const profile = await this.userProfilesCollection
|
|
1797
|
+
.findOne({ _id: record.userId })
|
|
1798
|
+
.exec();
|
|
1799
|
+
if (profile) {
|
|
1800
|
+
items.push(this.recordToProfile(profile));
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
const lastFollower = resultFollowers[resultFollowers.length - 1];
|
|
1804
|
+
return {
|
|
1805
|
+
items,
|
|
1806
|
+
cursor: hasMore && lastFollower ? lastFollower.followedAt : undefined,
|
|
1807
|
+
hasMore,
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1811
|
+
* Get all lists a user follows with pagination.
|
|
1812
|
+
* @see Requirements: 33.6
|
|
1813
|
+
*/
|
|
1814
|
+
async getFollowedLists(userId, options) {
|
|
1815
|
+
const limit = this.getLimit(options);
|
|
1816
|
+
const queryLimit = limit + 1;
|
|
1817
|
+
let query = this.listFollowersCollection.find({
|
|
1818
|
+
userId,
|
|
1819
|
+
});
|
|
1820
|
+
if (query.sort) {
|
|
1821
|
+
query = query.sort({ followedAt: -1 });
|
|
1822
|
+
}
|
|
1823
|
+
const allFollowed = await query.exec();
|
|
1824
|
+
let filteredFollowed = allFollowed;
|
|
1825
|
+
if (options?.cursor) {
|
|
1826
|
+
filteredFollowed = allFollowed.filter((f) => f.followedAt < options.cursor);
|
|
1827
|
+
}
|
|
1828
|
+
const paginatedFollowed = filteredFollowed.slice(0, queryLimit);
|
|
1829
|
+
const hasMore = paginatedFollowed.length > limit;
|
|
1830
|
+
const resultFollowed = hasMore
|
|
1831
|
+
? paginatedFollowed.slice(0, limit)
|
|
1832
|
+
: paginatedFollowed;
|
|
1833
|
+
// Resolve list details
|
|
1834
|
+
const items = [];
|
|
1835
|
+
for (const record of resultFollowed) {
|
|
1836
|
+
const list = await this.listsCollection
|
|
1837
|
+
.findOne({ _id: record.listId })
|
|
1838
|
+
.exec();
|
|
1839
|
+
if (list) {
|
|
1840
|
+
items.push(this.recordToList(list));
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
const lastFollowed = resultFollowed[resultFollowed.length - 1];
|
|
1844
|
+
return {
|
|
1845
|
+
items,
|
|
1846
|
+
cursor: hasMore && lastFollowed ? lastFollowed.followedAt : undefined,
|
|
1847
|
+
hasMore,
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
/**
|
|
1851
|
+
* Remove all non-owner followers when a list becomes private.
|
|
1852
|
+
* Deletes all follower records and resets followerCount to 0.
|
|
1853
|
+
* @see Requirements: 33.7
|
|
1854
|
+
*/
|
|
1855
|
+
async removeNonOwnerFollowersOnPrivate(listId, ownerId) {
|
|
1856
|
+
// Get all followers for this list
|
|
1857
|
+
const followers = await this.listFollowersCollection
|
|
1858
|
+
.find({ listId })
|
|
1859
|
+
.exec();
|
|
1860
|
+
// Delete all follower records (owner wouldn't normally follow their own list,
|
|
1861
|
+
// but skip them just in case)
|
|
1862
|
+
for (const follower of followers) {
|
|
1863
|
+
if (follower.userId !== ownerId) {
|
|
1864
|
+
await this.listFollowersCollection
|
|
1865
|
+
.deleteOne({ _id: follower._id })
|
|
1866
|
+
.exec();
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
// Reset followerCount to 0
|
|
1870
|
+
await this.listsCollection
|
|
1871
|
+
.updateOne({ _id: listId }, {
|
|
1872
|
+
followerCount: 0,
|
|
1873
|
+
updatedAt: new Date().toISOString(),
|
|
1874
|
+
})
|
|
1875
|
+
.exec();
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
exports.ConnectionService = ConnectionService;
|
|
1879
|
+
/**
|
|
1880
|
+
* Factory function to create a ConnectionService instance
|
|
1881
|
+
* @param application Application with database collection access
|
|
1882
|
+
* @returns A new ConnectionService instance
|
|
1883
|
+
*/
|
|
1884
|
+
function createConnectionService(application) {
|
|
1885
|
+
return new ConnectionService(application);
|
|
1886
|
+
}
|
|
1887
|
+
//# sourceMappingURL=connectionService.js.map
|