diff --git a/self-host-data/attachments.mdx b/self-host-data/attachments.mdx index 98b56592..bc77264a 100644 --- a/self-host-data/attachments.mdx +++ b/self-host-data/attachments.mdx @@ -18,26 +18,11 @@ Velt supports self-hosting your comments file attachments data: - This gives you full control over attachment data while maintaining the file attachment features. # How does it work? -When users upload or delete attachments: - -1. The SDK uses your configured [`AttachmentDataProvider`](/api-reference/sdk/models/data-models#attachmentdataprovider) to handle storage -2. Your data provider implements two key methods: - - `save`: Stores the file and returns its URL - - `delete`: Removes the file from storage - -**The process works as follows:** - -When an attachment operation occurs: - -1. The SDK first attempts to save/delete the file on your storage infrastructure -2. If successful: - - The SDK updates Velt's servers with minimal metadata - - The [`PartialComment`](/api-reference/sdk/models/data-models#partialcomment) object is updated to reference the attachment including the attachment url, name and metadata. - - When the comment is saved, this information is stored on your end. - - Velt servers only store necessary identifiers, not the actual files or URLs -3. If the operation fails, no changes are made to Velt's servers and the operation is retried if you have configured retries. - - +- When attachments are uploaded or deleted, the SDK uses your configured [`AttachmentDataProvider`](/api-reference/sdk/models/data-models#attachmentdataprovider) to handle storage +- The data provider implements `save` and `delete` methods to interact with your storage backend +- Velt handles the data mapping and realtime synchronization while delegating persistence of actual files to your infrastructure +- For write requests (save, delete), the operation is first performed on your storage backend and only if we get a success response, the SDK will perform the operation on the Velt server. If the operation fails on your storage backend, the SDK will not perform the operation on the Velt server. +- You can configure retries, timeouts, etc. for the data provider. Here are the methods that you need to implement on the data provider: ## save @@ -45,15 +30,213 @@ Save attachments to your storage backend. Return the url with a success or error - Param: [`SaveAttachmentResolverRequest`](/api-reference/sdk/models/data-models#saveattachmentresolverrequest) - Return: [`Promise>`](/api-reference/sdk/models/data-models#resolverresponse) + + +```json +{ + "file": "File object or Blob", + "metadata": { + "apiKey": "your-api-key", + "documentId": "doc-123", + "organizationId": "your-org-id", + "folderId": "folder-789" + }, + "fileName": "screenshot.png", + "fileType": "image/png" +} +``` + + + +```json +{ + "success": true, + "statusCode": 200, + "data": { + "url": "https://your-storage.com/files/screenshot.png" + } +} +``` + + + +```json +{ + "success": false, + "statusCode": 500, + "message": "Failed to upload attachment to storage" +} +``` + + + ## delete Delete attachments from your storage backend. Return a success or error response. On error we will retry. - Param: [`DeleteAttachmentResolverRequest`](/api-reference/sdk/models/data-models#deleteattachmentresolverrequest) - Return: [`Promise>`](/api-reference/sdk/models/data-models#resolverresponse) + + +```json +{ + "url": "https://your-storage.com/files/screenshot.png", + "metadata": { + "apiKey": "your-api-key", + "documentId": "doc-123", + "organizationId": "your-org-id", + "folderId": "folder-789" + } +} +``` + + + +```json +{ + "success": true, + "statusCode": 200, + "message": "Attachment deleted successfully" +} +``` + + + +```json +{ + "success": false, + "statusCode": 500, + "message": "Failed to delete attachment from storage" +} +``` + + + ## config Configuration for the attachment data provider. - Type: [`ResolverConfig`](/api-reference/sdk/models/data-models#resolverconfig) +# AWS S3 Storage Implementation Example + +Here's how to implement the data provider methods with AWS S3: + + + +```typescript +import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; + +const s3Client = new S3Client({ region: 'us-east-1' }); + +const saveAttachmentToS3 = async (request: SaveAttachmentResolverRequest) => { + try { + const { file, fileName, metadata } = request; + + // Generate unique file path + const fileKey = `attachments/${metadata.organizationId}/${metadata.documentId}/${Date.now()}-${fileName}`; + + // Convert file to buffer + const fileBuffer = await file.arrayBuffer(); + + // Upload to S3 + const command = new PutObjectCommand({ + Bucket: 'your-bucket-name', + Key: fileKey, + Body: Buffer.from(fileBuffer), + ContentType: request.fileType + }); + + await s3Client.send(command); + + // Construct public URL + const url = `https://your-bucket-name.s3.amazonaws.com/${fileKey}`; + + return { + success: true, + statusCode: 200, + data: { url } + }; + } catch (error) { + console.error('Error uploading attachment:', error); + return { + success: false, + statusCode: 500, + message: 'Failed to upload attachment to storage' + }; + } +}; +``` + + + +```typescript +import { S3Client, DeleteObjectCommand } from '@aws-sdk/client-s3'; + +const deleteAttachmentFromS3 = async (request: DeleteAttachmentResolverRequest) => { + try { + const { url } = request; + + // Extract file key from URL + const fileKey = url.replace('https://your-bucket-name.s3.amazonaws.com/', ''); + + // Delete from S3 + const command = new DeleteObjectCommand({ + Bucket: 'your-bucket-name', + Key: fileKey + }); + + await s3Client.send(command); + + return { + success: true, + statusCode: 200, + message: 'Attachment deleted successfully' + }; + } catch (error) { + console.error('Error deleting attachment:', error); + return { + success: false, + statusCode: 500, + message: 'Failed to delete attachment from storage' + }; + } +}; +``` + + + +```typescript +import { S3Client } from '@aws-sdk/client-s3'; + +// Initialize S3 client +const s3Client = new S3Client({ + region: 'us-east-1', + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY + } +}); + +// Configure the data provider +const attachmentResolverConfig: ResolverConfig = { + resolveTimeout: 5000, + saveRetryConfig: { + retryCount: 3, + retryDelay: 2000 + }, + deleteRetryConfig: { + retryCount: 3, + retryDelay: 2000 + } +}; + +const attachmentDataProvider: AttachmentDataProvider = { + save: saveAttachmentToS3, + delete: deleteAttachmentFromS3, + config: attachmentResolverConfig +}; +``` + + + # Example Implementation @@ -62,7 +245,7 @@ Configuration for the attachment data provider. const saveAttachmentsToDB = async (request: SaveAttachmentResolverRequest) => { const result = await __saveAttachmentsToYourDB__(request) .then((response) => { - return { success: true, statusCode: 200 }; + return { success: true, statusCode: 200, data: { url: response.url } }; }) .catch((error) => { return { success: false, statusCode: 500 }; @@ -83,10 +266,6 @@ const deleteAttachmentsFromDB = async (request: DeleteAttachmentResolverRequest) const attachmentResolverConfig: ResolverConfig = { resolveTimeout: 2000, - getRetryConfig: { - retryCount: 3, - retryDelay: 2000 - }, saveRetryConfig: { retryCount: 3, retryDelay: 2000 @@ -104,7 +283,7 @@ const attachmentDataProvider: AttachmentDataProvider = { config: attachmentResolverConfig }; - -``` jsx +```js const saveAttachmentsToDB = async (request) => { const result = await __saveAttachmentsToYourDB__(request) .then((response) => { - return { success: true, statusCode: 200 }; + return { success: true, statusCode: 200, data: { url: response.url } }; }) .catch((error) => { return { success: false, statusCode: 500 }; @@ -164,4 +343,44 @@ Velt.setDataProviders({ ``` - \ No newline at end of file + + +# Sample Data + + + +```json +{ + "fileUrl": "https://your-storage.com/attachments/org-123/doc-456/1234567890-screenshot.png", + "fileName": "screenshot.png", + "fileType": "image/png", + "metadata": { + "organizationId": "org-123", + "documentId": "doc-456", + "folderId": "folder-789", + "uploadedAt": "2025-07-16T04:00:19.770Z" + } +} +``` + + +```json +{ + "attachmentId": "attachment-abc", + "fileName": "screenshot.png", + "metadata": { + "apiKey": "API_KEY", + "documentId": "DOCUMENT_ID", + "organizationId": "ORGANIZATION_ID", + "folderId": "FOLDER_ID" + }, + "from": { + "userId": "USER_ID" + }, + "createdAt": 1752638412635, + "lastUpdated": 1752638419745 +} + +``` + + diff --git a/self-host-data/comments.mdx b/self-host-data/comments.mdx index 69df07f0..c07f06e2 100644 --- a/self-host-data/comments.mdx +++ b/self-host-data/comments.mdx @@ -37,6 +37,70 @@ Method to fetch comments from your database. On error we will retry. - Param: [`GetCommentResolverRequest`](/api-reference/sdk/models/data-models#getcommentresolverrequest) - Return: [`Promise>>`](/api-reference/sdk/models/data-models#resolverresponse) + + +```json +{ + "organizationId": "your-org-id", + "documentIds": ["doc-123", "doc-456"], + "folderId": "folder-789" +} +``` + +Or fetch specific annotations: + +```json +{ + "organizationId": "your-org-id", + "commentAnnotationIds": ["annotation-abc", "annotation-xyz"] +} +``` + + + +```json +{ + "success": true, + "statusCode": 200, + "data": { + "annotation-abc": { + "annotationId": "annotation-abc", + "metadata": { + "apiKey": "your-api-key", + "documentId": "doc-123", + "organizationId": "your-org-id", + "folderId": "folder-789" + }, + "comments": { + "comment-1": { + "commentId": 1, + "commentHtml": "

This is a comment

", + "commentText": "This is a comment", + "from": { + "userId": "user-1" + } + } + }, + "from": { + "userId": "user-1" + } + } + } +} +``` +
+ + +```json +{ + "success": false, + "statusCode": 500, + "message": "Failed to fetch comments from database" +} +``` + +
+ ## save Save comments to your database. Return a success or error response. On error we will retry. - Param: [`SaveCommentResolverRequest`](/api-reference/sdk/models/data-models#savecommentresolverrequest) @@ -47,15 +111,340 @@ Save comments to your database. Return a success or error response. On error we If you are using REST API to add or update comments, ensure that you set `isCommentResolverUsed` and `isCommentTextAvailable` fields in the request object. [Learn more](/api-reference/rest-apis/v2/comments-feature/comment-annotations/add-comment-annotations) + + +```json +{ + "commentAnnotation": { + "annotation-abc": { + "annotationId": "annotation-abc", + "metadata": { + "apiKey": "your-api-key", + "documentId": "doc-123", + "organizationId": "your-org-id", + "folderId": "folder-789" + }, + "comments": { + "comment-1": { + "commentId": 1, + "commentHtml": "

Updated comment text

", + "commentText": "Updated comment text", + "from": { + "userId": "user-1" + } + } + }, + "from": { + "userId": "user-1" + } + } + }, + "metadata": { + "apiKey": "your-api-key", + "documentId": "doc-123", + "organizationId": "your-org-id" + }, + "event": "onCommentAdd", + "commentId": "comment-1" +} +``` +
+ + +```json +{ + "success": true, + "statusCode": 200, + "message": "Comment saved successfully" +} +``` + + + +```json +{ + "success": false, + "statusCode": 500, + "message": "Failed to save comment to database" +} +``` + +
+ ## delete Delete comments from your database. Return a success or error response. On error we will retry. - Param: [`DeleteCommentResolverRequest`](/api-reference/sdk/models/data-models#deletecommentresolverrequest) - Return: [`Promise>`](/api-reference/sdk/models/data-models#resolverresponse) + + +```json +{ + "commentAnnotationId": "annotation-abc", + "metadata": { + "apiKey": "your-api-key", + "documentId": "doc-123", + "organizationId": "your-org-id", + "folderId": "folder-789" + }, + "event": "onCommentDelete" +} +``` + + + +```json +{ + "success": true, + "statusCode": 200, + "message": "Comment deleted successfully" +} +``` + + + +```json +{ + "success": false, + "statusCode": 500, + "message": "Failed to delete comment from database" +} +``` + + + ## config Configuration for the comment data provider. - Type: [`ResolverConfig`](/api-reference/sdk/models/data-models#resolverconfig) +# MongoDB Backend Implementation Example + +Here's how to implement the data provider methods with MongoDB: + + + +```typescript +// MongoDB Schema for Comment Annotations +interface CommentAnnotationDocument { + annotationId: string; + metadata: { + apiKey: string; + documentId: string; + organizationId: string; + folderId?: string; + }; + comments: { + [commentId: string]: { + commentId: number; + commentHtml: string; + commentText: string; + from: { + userId: string; + }; + }; + }; + from: { + userId: string; + }; + createdAt?: Date; + updatedAt?: Date; +} +``` + + + +```typescript +import { MongoClient, Db } from 'mongodb'; + +const fetchCommentsFromDB = async (request: GetCommentResolverRequest) => { + try { + const db: Db = client.db('your-database'); + const collection = db.collection('commentAnnotations'); + + // Build query based on request parameters + const query: any = { + 'metadata.organizationId': request.organizationId + }; + + // Filter by specific annotation IDs + if (request.commentAnnotationIds?.length) { + query.annotationId = { $in: request.commentAnnotationIds }; + } + + // Filter by document IDs + if (request.documentIds?.length) { + query['metadata.documentId'] = { $in: request.documentIds }; + } + + // Filter by folder ID + if (request.folderId) { + query['metadata.folderId'] = request.folderId; + } + + // Fetch from MongoDB + const annotations = await collection.find(query).toArray(); + + // Transform to required format: Record + const data: Record = {}; + annotations.forEach(annotation => { + data[annotation.annotationId] = { + annotationId: annotation.annotationId, + metadata: annotation.metadata, + comments: annotation.comments, + from: annotation.from + }; + }); + + return { + data, + success: true, + statusCode: 200 + }; + } catch (error) { + console.error('Error fetching comments:', error); + return { + success: false, + statusCode: 500, + message: 'Failed to fetch comments from database' + }; + } +}; +``` + + + +```typescript +const saveCommentsToDB = async (request: SaveCommentResolverRequest) => { + try { + const db: Db = client.db('your-database'); + const collection = db.collection('commentAnnotations'); + + // Get the annotation data from request + const annotationData = Object.values(request.commentAnnotation)[0]; + + if (!annotationData) { + return { + success: false, + statusCode: 400, + message: 'No annotation data provided' + }; + } + + // Upsert the comment annotation + await collection.updateOne( + { annotationId: annotationData.annotationId }, + { + $set: { + ...annotationData, + updatedAt: new Date() + }, + $setOnInsert: { + createdAt: new Date() + } + }, + { upsert: true } + ); + + return { + success: true, + statusCode: 200, + message: 'Comment saved successfully' + }; + } catch (error) { + console.error('Error saving comment:', error); + return { + success: false, + statusCode: 500, + message: 'Failed to save comment to database' + }; + } +}; +``` + + + +```typescript +const deleteCommentsFromDB = async (request: DeleteCommentResolverRequest) => { + try { + const db: Db = client.db('your-database'); + const collection = db.collection('commentAnnotations'); + + // Delete the comment annotation + const result = await collection.deleteOne({ + annotationId: request.commentAnnotationId + }); + + if (result.deletedCount === 0) { + return { + success: false, + statusCode: 404, + message: 'Comment annotation not found' + }; + } + + return { + success: true, + statusCode: 200, + message: 'Comment deleted successfully' + }; + } catch (error) { + console.error('Error deleting comment:', error); + return { + success: false, + statusCode: 500, + message: 'Failed to delete comment from database' + }; + } +}; +``` + + + +```typescript +import { MongoClient } from 'mongodb'; + +// Initialize MongoDB connection +const client = new MongoClient('mongodb://localhost:27017'); +await client.connect(); + +// Create indexes for better query performance +const db = client.db('your-database'); +const collection = db.collection('commentAnnotations'); + +await collection.createIndexes([ + { key: { annotationId: 1 }, unique: true }, + { key: { 'metadata.organizationId': 1 } }, + { key: { 'metadata.documentId': 1 } }, + { key: { 'metadata.folderId': 1 } } +]); + +// Configure the data provider +const commentResolverConfig: ResolverConfig = { + resolveTimeout: 2000, + getRetryConfig: { + retryCount: 3, + retryDelay: 2000 + }, + saveRetryConfig: { + retryCount: 3, + retryDelay: 2000 + }, + deleteRetryConfig: { + retryCount: 3, + retryDelay: 2000 + } +}; + +const commentAnnotationDataProvider: CommentAnnotationDataProvider = { + get: fetchCommentsFromDB, + save: saveCommentsToDB, + delete: deleteCommentsFromDB, + config: commentResolverConfig +}; +``` + + + # Example Implementation @@ -200,45 +589,47 @@ Velt.setDataProviders({ When you self-host content, use Webhooks to trigger emails from your own system: - - +### Step 1: Enable relevant webhooks + Subscribe to comment-related events (e.g., user mentions, replies). See [Webhooks basics](/webhooks/basic). - - +### Step 2: Receive webhook and fetch content from your DB + Your server receives the webhook event. Use IDs from the payload (e.g., `annotationId`, `commentId`) to query your own comment and notification content from your database via your Data Provider. - - +### Step 3: Assemble email content and recipients + Combine the webhook event context with the self-hosted content to build the subject, body, and list of recipients (e.g., mentioned users). - - +### Step 4: Send email via your provider + Use your own email service (SendGrid under your account, SES, Postmark, etc.) to send the email. If you previously configured SendGrid in Velt Console, that configuration will not be used for self-hosted content. Use your own SendGrid account or another email provider from your server. - - - - -```json cURL -POST /webhooks/velt HTTP/1.1 -Content-Type: application/json + + +```json { "actionType": "newlyAdded", - "commentAnnotation": { "annotationId": "ANNOTATION_ID", "metadata": { "documentId": "DOC_ID" } }, - "latestComment": { "commentId": 123, "from": { "userId": "USER_1" } }, + "commentAnnotation": { + "annotationId": "ANNOTATION_ID", + "metadata": { "documentId": "DOC_ID" } + }, + "latestComment": { + "commentId": 123, + "from": { "userId": "USER_1" } + }, "fromUser": { "userId": "USER_1" }, "documentMetadata": { "url": "https://app.example.com/doc/123" } } ``` - + - -```javascript Node.js + +```javascript // Pseudocode: handle webhook, fetch content, send email app.post('/webhooks/velt', async (req, res) => { const evt = req.body; @@ -258,12 +649,17 @@ app.post('/webhooks/velt', async (req, res) => { const pageUrl = evt?.documentMetadata?.url; // 4) Send via your provider - await emailClient.send({ to: recipients, subject, html: renderTemplate({ body, pageUrl }) }); + await emailClient.send({ + to: recipients, + subject, + html: renderTemplate({ body, pageUrl }) + }); res.sendStatus(200); }); ``` - + + You now send email notifications from your own infrastructure while keeping content self-hosted. diff --git a/self-host-data/reactions.mdx b/self-host-data/reactions.mdx index cf8e6a15..0a734144 100644 --- a/self-host-data/reactions.mdx +++ b/self-host-data/reactions.mdx @@ -4,6 +4,7 @@ description: "Self-host your reactions data while using Velt's components. Keep --- + - This feature is currently in beta and is subject to change. - This is currently only compatible with `setDocuments` method. - Ensure that the data providers are set prior to calling `identify` method. - The data provider methods must return the correct status code (e.g. 200 for success, 500 for errors) and success boolean in the response object. This ensures proper error handling and retries. @@ -21,30 +22,407 @@ Velt supports self-hosting your reactions and related data: - When reactions are created, updated, deleted or requested, the SDK uses your configured [`ReactionAnnotationDataProvider`](/api-reference/sdk/models/data-models#reactionannotationdataprovider) to handle storage and retrieval - The data provider implements `get`, `save`, and `delete` methods to interact with your database - Velt handles the data mapping and realtime synchronization while delegating persistence of actual content to your infrastructure +- The data provider works at the Reaction Annotation level. - For write requests (save, delete), the operation is first performed on your database and only if we get a success response, the SDK will perform the operation on the Velt server. If the operation fails on your database, the SDK will not perform the operation on the Velt server. - You can configure retries, timeouts, etc. for the data provider. - Here are the methods that you need to implement on the data provider: ## get Method to fetch reactions from your database. On error we will retry. - Param: [`GetReactionResolverRequest`](/api-reference/sdk/models/data-models#getreactionresolverrequest) - Return: [`Promise>>`](/api-reference/sdk/models/data-models#resolverresponse) + + +```json +{ + "organizationId": "your-org-id", + "documentIds": ["doc-123", "doc-456"], + "folderId": "folder-789" +} +``` + +Or fetch specific reaction annotations: + +```json +{ + "organizationId": "your-org-id", + "reactionAnnotationIds": ["reaction-abc", "reaction-xyz"] +} +``` + + + +```json +{ + "success": true, + "statusCode": 200, + "data": { + "reaction-abc": { + "annotationId": "reaction-abc", + "metadata": { + "apiKey": "your-api-key", + "documentId": "doc-123", + "organizationId": "your-org-id", + "folderId": "folder-789" + }, + "reactions": { + "👍": [ + { + "userId": "user-1", + "reactionId": "reaction-1" + } + ], + "❤️": [ + { + "userId": "user-2", + "reactionId": "reaction-2" + } + ] + } + } + } +} +``` + + + +```json +{ + "success": false, + "statusCode": 500, + "message": "Failed to fetch reactions from database" +} +``` + + + ## save Save reactions to your database. Return a success or error response. On error we will retry. - Param: [`SaveReactionResolverRequest`](/api-reference/sdk/models/data-models#savereactionresolverrequest) + - Note in the `SaveReactionResolverRequest` object, you will receive [the event name](/api-reference/sdk/models/data-models#resolveractions) that triggered the save. - Return: [`Promise>`](/api-reference/sdk/models/data-models#resolverresponse) + + +```json +{ + "reactionAnnotation": { + "reaction-abc": { + "annotationId": "reaction-abc", + "metadata": { + "apiKey": "your-api-key", + "documentId": "doc-123", + "organizationId": "your-org-id", + "folderId": "folder-789" + }, + "reactions": { + "👍": [ + { + "userId": "user-1", + "reactionId": "reaction-1" + } + ] + } + } + }, + "metadata": { + "apiKey": "your-api-key", + "documentId": "doc-123", + "organizationId": "your-org-id" + }, + "event": "onReactionAdd" +} +``` + + + +```json +{ + "success": true, + "statusCode": 200, + "message": "Reaction saved successfully" +} +``` + + + +```json +{ + "success": false, + "statusCode": 500, + "message": "Failed to save reaction to database" +} +``` + + + ## delete Delete reactions from your database. Return a success or error response. On error we will retry. - Param: [`DeleteReactionResolverRequest`](/api-reference/sdk/models/data-models#deletereactionresolverrequest) - Return: [`Promise>`](/api-reference/sdk/models/data-models#resolverresponse) + + +```json +{ + "reactionAnnotationId": "reaction-abc", + "metadata": { + "apiKey": "your-api-key", + "documentId": "doc-123", + "organizationId": "your-org-id", + "folderId": "folder-789" + }, + "event": "onReactionDelete" +} +``` + + + +```json +{ + "success": true, + "statusCode": 200, + "message": "Reaction deleted successfully" +} +``` + + + +```json +{ + "success": false, + "statusCode": 500, + "message": "Failed to delete reaction from database" +} +``` + + + ## config Configuration for the reaction data provider. - Type: [`ResolverConfig`](/api-reference/sdk/models/data-models#resolverconfig) +# MongoDB Backend Implementation Example + +Here's how to implement the data provider methods with MongoDB: + + + +```typescript +// MongoDB Schema for Reaction Annotations +interface ReactionAnnotationDocument { + annotationId: string; + metadata: { + apiKey: string; + documentId: string; + organizationId: string; + folderId?: string; + }; + reactions: { + [emoji: string]: Array<{ + userId: string; + reactionId: string; + }>; + }; + createdAt?: Date; + updatedAt?: Date; +} +``` + + + +```typescript +import { MongoClient, Db } from 'mongodb'; + +const fetchReactionsFromDB = async (request: GetReactionResolverRequest) => { + try { + const db: Db = client.db('your-database'); + const collection = db.collection('reactionAnnotations'); + + // Build query based on request parameters + const query: any = { + 'metadata.organizationId': request.organizationId + }; + + // Filter by specific annotation IDs + if (request.reactionAnnotationIds?.length) { + query.annotationId = { $in: request.reactionAnnotationIds }; + } + + // Filter by document IDs + if (request.documentIds?.length) { + query['metadata.documentId'] = { $in: request.documentIds }; + } + + // Filter by folder ID + if (request.folderId) { + query['metadata.folderId'] = request.folderId; + } + + // Fetch from MongoDB + const annotations = await collection.find(query).toArray(); + + // Transform to required format: Record + const data: Record = {}; + annotations.forEach(annotation => { + data[annotation.annotationId] = { + annotationId: annotation.annotationId, + metadata: annotation.metadata, + reactions: annotation.reactions + }; + }); + + return { + data, + success: true, + statusCode: 200 + }; + } catch (error) { + console.error('Error fetching reactions:', error); + return { + success: false, + statusCode: 500, + message: 'Failed to fetch reactions from database' + }; + } +}; +``` + + + +```typescript +const saveReactionsToDB = async (request: SaveReactionResolverRequest) => { + try { + const db: Db = client.db('your-database'); + const collection = db.collection('reactionAnnotations'); + + // Get the annotation data from request + const annotationData = Object.values(request.reactionAnnotation)[0]; + + if (!annotationData) { + return { + success: false, + statusCode: 400, + message: 'No annotation data provided' + }; + } + + // Upsert the reaction annotation + await collection.updateOne( + { annotationId: annotationData.annotationId }, + { + $set: { + ...annotationData, + updatedAt: new Date() + }, + $setOnInsert: { + createdAt: new Date() + } + }, + { upsert: true } + ); + + return { + success: true, + statusCode: 200, + message: 'Reaction saved successfully' + }; + } catch (error) { + console.error('Error saving reaction:', error); + return { + success: false, + statusCode: 500, + message: 'Failed to save reaction to database' + }; + } +}; +``` + + + +```typescript +const deleteReactionsFromDB = async (request: DeleteReactionResolverRequest) => { + try { + const db: Db = client.db('your-database'); + const collection = db.collection('reactionAnnotations'); + + // Delete the reaction annotation + const result = await collection.deleteOne({ + annotationId: request.reactionAnnotationId + }); + + if (result.deletedCount === 0) { + return { + success: false, + statusCode: 404, + message: 'Reaction annotation not found' + }; + } + + return { + success: true, + statusCode: 200, + message: 'Reaction deleted successfully' + }; + } catch (error) { + console.error('Error deleting reaction:', error); + return { + success: false, + statusCode: 500, + message: 'Failed to delete reaction from database' + }; + } +}; +``` + + + +```typescript +import { MongoClient } from 'mongodb'; + +// Initialize MongoDB connection +const client = new MongoClient('mongodb://localhost:27017'); +await client.connect(); + +// Create indexes for better query performance +const db = client.db('your-database'); +const collection = db.collection('reactionAnnotations'); + +await collection.createIndexes([ + { key: { annotationId: 1 }, unique: true }, + { key: { 'metadata.organizationId': 1 } }, + { key: { 'metadata.documentId': 1 } }, + { key: { 'metadata.folderId': 1 } } +]); + +// Configure the data provider +const reactionResolverConfig: ResolverConfig = { + resolveTimeout: 2000, + getRetryConfig: { + retryCount: 3, + retryDelay: 2000 + }, + saveRetryConfig: { + retryCount: 3, + retryDelay: 2000 + }, + deleteRetryConfig: { + retryCount: 3, + retryDelay: 2000 + } +}; + +const reactionAnnotationDataProvider: ReactionAnnotationDataProvider = { + get: fetchReactionsFromDB, + save: saveReactionsToDB, + delete: deleteReactionsFromDB, + config: reactionResolverConfig +}; +``` + + + # Example Implementation @@ -109,7 +487,7 @@ const reactionAnnotationDataProvider: ReactionAnnotationDataProvider = { config: reactionResolverConfig }; - -``` jsx +```js const fetchReactionsFromDB = async (request) => { // Fetch reaction annotations from your DB const result = await __getReactionsFromYourDB__(request) @@ -158,6 +536,10 @@ const deleteReactionsFromDB = async (request) => { const reactionResolverConfig = { resolveTimeout: 2000, + getRetryConfig: { + retryCount: 3, + retryDelay: 2000 + }, saveRetryConfig: { retryCount: 3, retryDelay: 2000 @@ -183,4 +565,54 @@ Velt.setDataProviders({ ``` - \ No newline at end of file + + +# Sample Data + + + +```json +{ + "REACTION_ANNOTATION_ID": { + "annotationId": "REACTION_ANNOTATION_ID", + "metadata": { + "apiKey": "API_KEY", + "documentId": "DOCUMENT_ID", + "organizationId": "ORGANIZATION_ID", + "folderId": "FOLDER_ID" + }, + "reactions": { + "👍": [ + { + "userId": "USER_ID_1", + "reactionId": "REACTION_ID_1" + } + ], + "❤️": [ + { + "userId": "USER_ID_2", + "reactionId": "REACTION_ID_2" + } + ] + } + } +} +``` + + +```json +{ + "annotationId": "REACTION_ANNOTATION_ID", + "metadata": { + "apiKey": "API_KEY", + "documentId": "DOCUMENT_ID", + "organizationId": "ORGANIZATION_ID", + "folderId": "FOLDER_ID" + }, + "lastUpdated": 1752638419745, + "createdAt": 1752638412635 +} + +``` + + diff --git a/self-host-data/users.mdx b/self-host-data/users.mdx index ba2a07f7..5fffc6be 100644 --- a/self-host-data/users.mdx +++ b/self-host-data/users.mdx @@ -3,48 +3,194 @@ title: "Users" description: "Self-host your users' PII while using Velt's collaboration features. Keep sensitive user data on your infrastructure with only user IDs stored on Velt servers." --- + + - This feature is currently in beta and is subject to change. + - This is currently only compatible with `setDocuments` method. + - Ensure that the data providers are set prior to calling `identify` method. + - The data provider methods must return the correct status code (e.g. 200 for success, 500 for errors) and success boolean in the response object. This ensures proper error handling and retries. + + +# Overview Velt supports self-hosting your users' personally identifiable information (PII): -- Only the userId is stored on Velt servers, keeping sensitive user metadata on your infrastructure -- Velt Components automatically hydrate user details in the frontend by fetching from your configured data provider -- This gives you full control over user data while maintaining all Velt functionality +- Only the userId is stored on Velt servers, keeping sensitive user metadata on your infrastructure. +- Velt Components automatically hydrate user details in the frontend by fetching from your configured data provider. +- This gives you full control over user data while maintaining all Velt functionality. -## How does it work? -- When the SDK is initialized, it will call the [`UserDataProvider`](/api-reference/sdk/models/data-models#userdataprovider) you configure with the list of userIds that it needs to fetch for the currently set user, organization, document, etc. -- The [`UserDataProvider`](/api-reference/sdk/models/data-models#userdataprovider) takes in a list of userIds and returns a Record object with the userIds as keys and the user data as values. +# How does it work? +- When the SDK is initialized or when user data is needed, it will call the [`UserDataProvider`](/api-reference/sdk/models/data-models#userdataprovider) you configure with the list of userIds that it needs to fetch +- The data provider implements a `get` method to retrieve user information from your database +- Velt handles the data mapping and UI updates while delegating user data retrieval to your infrastructure +- The SDK requests user data for the currently set user, organization, document, etc. based on your configuration +- You can configure when user resolver requests are made to optimize performance Here are the methods that you need to implement on the data provider: ## get -Method to fetch users from your database. +Method to fetch users from your database. On error we will retry. - Param: `string[]`: Array of userIds to fetch - Return: `Promise>` + + +```json +[ + "user-1", + "user-2", + "user-3" +] +``` + + + +```json +{ + "user-1": { + "userId": "user-1", + "name": "John Doe", + "email": "john@example.com", + "photoUrl": "https://example.com/photos/john.jpg" + }, + "user-2": { + "userId": "user-2", + "name": "Jane Smith", + "email": "jane@example.com", + "photoUrl": "https://example.com/photos/jane.jpg" + }, + "user-3": { + "userId": "user-3", + "name": "Bob Johnson", + "email": "bob@example.com", + "photoUrl": "https://example.com/photos/bob.jpg" + } +} +``` + + + +```json +{ + "success": false, + "statusCode": 500, + "message": "Failed to fetch users from database" +} +``` + + + ## config Configuration for the user data provider. - Type: [`ResolverConfig`](/api-reference/sdk/models/data-models#resolverconfig). Relevant properties: - `resolveUsersConfig`: [`ResolveUsersConfig`](/api-reference/sdk/models/data-models#resolveusersconfig). Configuration to control when user resolver requests are made. This helps optimize performance by avoiding unnecessary user data requests when you have a large number of users in your organization, document, or folder. You can disable user resolver requests for specific contexts and use [custom autocomplete feature](/async-collaboration/comments/customize-behavior#customautocompletesearch) instead. - `organization`: boolean - Enable/disable user requests for organization users (default: true) - - `document`: boolean - Enable/disable user requests for document users (default: true) + - `document`: boolean - Enable/disable user requests for document users (default: true) - `folder`: boolean - Enable/disable user requests for folder users (default: true) -## Example Implementation +# MongoDB Backend Implementation Example + +Here's how to implement the data provider methods with MongoDB: + + + +```typescript +// MongoDB Schema for Users +interface UserDocument { + userId: string; + name: string; + email: string; + photoUrl?: string; + metadata?: Record; + createdAt?: Date; + updatedAt?: Date; +} +``` + + + +```typescript +import { MongoClient, Db } from 'mongodb'; + +const fetchUsersFromDB = async (userIds: string[]) => { + try { + const db: Db = client.db('your-database'); + const collection = db.collection('users'); + + // Fetch users from MongoDB + const users = await collection.find({ + userId: { $in: userIds } + }).toArray(); + + // Transform to required format: Record + const data: Record = {}; + users.forEach(user => { + data[user.userId] = { + userId: user.userId, + name: user.name, + email: user.email, + photoUrl: user.photoUrl + }; + }); + + return data; + } catch (error) { + console.error('Error fetching users:', error); + throw error; + } +}; +``` + + + +```typescript +import { MongoClient } from 'mongodb'; + +// Initialize MongoDB connection +const client = new MongoClient('mongodb://localhost:27017'); +await client.connect(); + +// Create indexes for better query performance +const db = client.db('your-database'); +const collection = db.collection('users'); + +await collection.createIndexes([ + { key: { userId: 1 }, unique: true }, + { key: { email: 1 } } +]); + +// Configure the data provider +const userDataProvider: UserDataProvider = { + get: fetchUsersFromDB, + config: { + resolveUsersConfig: { + organization: false, // Disable organization user requests + folder: false, // Disable folder user requests + document: true // Enable document user requests + } + } +}; +``` + + + +# Example Implementation -``` jsx {19-29} +``` jsx const formatUsersToRecord = (users) => { // Format users array into a Record object with userId as key and user data as value return users.reduce((record, user) => { record[user.userId] = { userId: user.userId, name: user.name, + email: user.email, + photoUrl: user.photoUrl // any other fields }; return record; }, {}); }; -const fetchUsersFromDB = async (userIds) => { +const fetchUsersFromDB = async (userIds: string[]) => { // Fetch users from your DB const usersData = await __getUsersFromYourDB__(userIds); return formatUsersToRecord(usersData); @@ -61,7 +207,7 @@ const userDataProvider: UserDataProvider = { } }; - -``` js {19-28} +```js const formatUsersToRecord = (users) => { // Format users array into a Record object with userId as key and user data as value return users.reduce((record, user) => { record[user.userId] = { userId: user.userId, name: user.name, + email: user.email, + photoUrl: user.photoUrl // any other fields }; return record; @@ -107,4 +255,43 @@ Velt.setDataProviders({ }); ``` - \ No newline at end of file + + +# Sample Data + + + +```json +{ + "user-1": { + "userId": "user-1", + "name": "John Doe", + "email": "john@example.com", + "photoUrl": "https://example.com/photos/john.jpg", + "metadata": { + "department": "Engineering", + "role": "Senior Developer" + } + }, + "user-2": { + "userId": "user-2", + "name": "Jane Smith", + "email": "jane@example.com", + "photoUrl": "https://example.com/photos/jane.jpg", + "metadata": { + "department": "Product", + "role": "Product Manager" + } + } +} +``` + + +```json +{ + "userId": "user-1" +} + +``` + +