@objectstack/service-feed
v4.0.4
Published
Feed/Chatter Service for ObjectStack — implements IFeedService with in-memory adapter for comments, reactions, field changes, and record subscriptions
Readme
@objectstack/service-feed
Feed/Chatter Service for ObjectStack — implements IFeedService with in-memory adapter for comments, reactions, field changes, and record subscriptions.
Features
- Activity Feed: Track all record changes and user activities
- Comments & Mentions: Add comments with @mentions support
- Reactions: Like, upvote, or react to records and comments
- Field Change Tracking: Automatic history of field value changes
- Subscriptions: Subscribe to records for notifications
- Rich Content: Support for markdown, attachments, and embeds
- Real-time Updates: Integrates with
@objectstack/service-realtimefor live updates
Installation
pnpm add @objectstack/service-feedBasic Usage
import { defineStack } from '@objectstack/spec';
import { ServiceFeed } from '@objectstack/service-feed';
const stack = defineStack({
services: [
ServiceFeed.configure({
enableFieldTracking: true,
enableMentions: true,
}),
],
});Configuration
interface FeedServiceConfig {
/** Enable automatic field change tracking */
enableFieldTracking?: boolean;
/** Enable @mention support in comments */
enableMentions?: boolean;
/** Enable reactions (likes, upvotes, etc.) */
enableReactions?: boolean;
/** Maximum feed items per page */
pageSize?: number;
}Service API
// Get feed service
const feed = kernel.getService<IFeedService>('feed');Comments
// Add a comment to a record
await feed.addComment({
object: 'opportunity',
recordId: '123',
userId: 'user:456',
body: 'Great progress on this deal! @john can you follow up?',
});
// Get comments for a record
const comments = await feed.getComments({
object: 'opportunity',
recordId: '123',
limit: 20,
});
// Update a comment
await feed.updateComment({
commentId: 'comment:789',
body: 'Updated comment text',
});
// Delete a comment
await feed.deleteComment('comment:789');Reactions
// Add a reaction
await feed.addReaction({
targetType: 'record', // or 'comment'
targetId: '123',
userId: 'user:456',
type: 'like', // 'like', 'love', 'upvote', 'celebrate'
});
// Remove a reaction
await feed.removeReaction({
targetType: 'record',
targetId: '123',
userId: 'user:456',
type: 'like',
});
// Get reactions for a record
const reactions = await feed.getReactions({
targetType: 'record',
targetId: '123',
});
// Returns: { like: 12, love: 5, upvote: 8 }Activity Feed
// Get feed for a specific record
const recordFeed = await feed.getFeed({
object: 'account',
recordId: '123',
types: ['comment', 'field_change', 'record_created'],
limit: 50,
});
// Get user's personalized feed (records they follow)
const userFeed = await feed.getUserFeed({
userId: 'user:456',
limit: 100,
});
// Example feed item:
// {
// id: 'feed:abc',
// type: 'field_change',
// object: 'opportunity',
// recordId: '123',
// userId: 'user:456',
// timestamp: '2024-01-15T10:30:00Z',
// data: {
// field: 'stage',
// oldValue: 'prospecting',
// newValue: 'proposal'
// }
// }Subscriptions
// Subscribe to a record
await feed.subscribe({
object: 'opportunity',
recordId: '123',
userId: 'user:456',
});
// Unsubscribe from a record
await feed.unsubscribe({
object: 'opportunity',
recordId: '123',
userId: 'user:456',
});
// Check if user is subscribed
const isSubscribed = await feed.isSubscribed({
object: 'opportunity',
recordId: '123',
userId: 'user:456',
});
// Get all subscriptions for a user
const subscriptions = await feed.getUserSubscriptions('user:456');Field Change Tracking
Field changes are automatically tracked when enableFieldTracking is enabled:
// Automatically creates feed items like:
// {
// type: 'field_change',
// object: 'opportunity',
// recordId: '123',
// userId: 'user:456',
// data: {
// field: 'amount',
// oldValue: 50000,
// newValue: 75000
// }
// }Customize which fields to track:
ServiceFeed.configure({
enableFieldTracking: true,
trackedObjects: {
opportunity: ['stage', 'amount', 'close_date'],
account: ['status', 'industry'],
},
});Advanced Features
Mentions & Notifications
// Parse mentions from comment body
const mentions = feed.parseMentions('Hey @john and @sarah, check this out!');
// Returns: ['john', 'sarah']
// Get mentions for a user
const userMentions = await feed.getMentions('user:456', {
unreadOnly: true,
});
// Mark mention as read
await feed.markMentionRead({
mentionId: 'mention:abc',
userId: 'user:456',
});Rich Content
await feed.addComment({
object: 'opportunity',
recordId: '123',
userId: 'user:456',
body: '## Great News!\n\nWe closed the deal at **$100k**!',
format: 'markdown',
attachments: [
{
type: 'file',
url: 'https://example.com/contract.pdf',
name: 'contract.pdf',
size: 1024000,
},
],
});Filtering & Search
// Get feed with filters
const feed = await feed.getFeed({
object: 'opportunity',
recordId: '123',
types: ['comment'], // Only comments
userId: 'user:456', // Only from specific user
since: '2024-01-01T00:00:00Z',
until: '2024-01-31T23:59:59Z',
});
// Search comments
const results = await feed.searchComments({
query: 'follow up',
object: 'opportunity',
recordId: '123',
});Integration with Realtime Service
// Subscribe to real-time feed updates
const realtime = kernel.getService<IRealtimeService>('realtime');
realtime.subscribe(`feed:opportunity:123`, (event) => {
if (event.type === 'comment_added') {
console.log('New comment:', event.data);
} else if (event.type === 'field_changed') {
console.log('Field changed:', event.data);
}
});REST API Endpoints
POST /api/v1/feed/comments # Add comment
GET /api/v1/feed/comments/:object/:recordId # Get comments
PATCH /api/v1/feed/comments/:id # Update comment
DELETE /api/v1/feed/comments/:id # Delete comment
POST /api/v1/feed/reactions # Add reaction
DELETE /api/v1/feed/reactions # Remove reaction
GET /api/v1/feed/reactions/:type/:id # Get reactions
GET /api/v1/feed/:object/:recordId # Get record feed
GET /api/v1/feed/user/:userId # Get user feed
POST /api/v1/feed/subscriptions # Subscribe
DELETE /api/v1/feed/subscriptions # Unsubscribe
GET /api/v1/feed/subscriptions/:userId # Get subscriptionsUI Integration
React Hook Example
import { useFeed } from '@objectstack/client-react';
function OpportunityFeed({ recordId }: { recordId: string }) {
const { feed, addComment, loading } = useFeed({
object: 'opportunity',
recordId,
});
return (
<div>
{feed.map((item) => (
<FeedItem key={item.id} item={item} />
))}
<CommentBox onSubmit={addComment} />
</div>
);
}Best Practices
- Enable Selective Tracking: Track only important fields to reduce noise
- Use Pagination: Always paginate feed queries to avoid performance issues
- Subscribe Sparingly: Don't auto-subscribe users to too many records
- Moderate Content: Implement moderation for user-generated comments
- Archive Old Data: Periodically archive old feed items
- Index Efficiently: Ensure database indexes on object/recordId/timestamp
Performance Considerations
- In-Memory Adapter: Current implementation is in-memory only (future: database persistence)
- Pagination: Always use pagination for feed queries
- Filtering: Filter by type and date range to reduce result set
- Caching: Cache recent feed items for frequently accessed records
Contract Implementation
Implements IFeedService from @objectstack/spec/contracts:
interface IFeedService {
addComment(options: AddCommentOptions): Promise<Comment>;
getComments(options: GetCommentsOptions): Promise<Comment[]>;
updateComment(options: UpdateCommentOptions): Promise<void>;
deleteComment(commentId: string): Promise<void>;
addReaction(options: AddReactionOptions): Promise<void>;
removeReaction(options: RemoveReactionOptions): Promise<void>;
getReactions(options: GetReactionsOptions): Promise<ReactionCounts>;
getFeed(options: GetFeedOptions): Promise<FeedItem[]>;
getUserFeed(options: GetUserFeedOptions): Promise<FeedItem[]>;
subscribe(options: SubscribeOptions): Promise<void>;
unsubscribe(options: UnsubscribeOptions): Promise<void>;
isSubscribed(options: IsSubscribedOptions): Promise<boolean>;
}License
Apache-2.0
