Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 

README.md

@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-realtime for live updates

Installation

pnpm add @objectstack/service-feed

Basic 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 subscriptions

UI 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

  1. Enable Selective Tracking: Track only important fields to reduce noise
  2. Use Pagination: Always paginate feed queries to avoid performance issues
  3. Subscribe Sparingly: Don't auto-subscribe users to too many records
  4. Moderate Content: Implement moderation for user-generated comments
  5. Archive Old Data: Periodically archive old feed items
  6. 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

See Also