Skip to content

api-news

Vue d'ensemble

Le service api-news est le système de gestion de contenu (CMS) de la plateforme Roomee. Il gère les pages hiérarchiques, les commentaires, les réactions, les catégories et toutes les fonctionnalités de contenu collaboratif.

CaractéristiqueDétail
Port3004
StackTypeScript, Express.js, Prisma, AMQP, Cron
Base de donnéesMongoDB (roomee_news) + roomee_members
ArchitectureModerne (TS + Modules)
Complexité⭐⭐⭐⭐

Responsabilités

mermaid
graph TB
    NEWS[api-news]

    subgraph "Domaines"
        PAGES[Pages Hiérarchiques]
        COMMENTS[Commentaires]
        LIKES[Réactions & Likes]
        CATEGORIES[Catégories]
        TEMPLATES[Templates]
        SCHEDULED[Publications Programmées]
        VIEWS[Vues & Stats]
        PINS[Épinglage]
    end

    NEWS --> PAGES
    NEWS --> COMMENTS
    NEWS --> LIKES
    NEWS --> CATEGORIES
    NEWS --> TEMPLATES
    NEWS --> SCHEDULED
    NEWS --> VIEWS
    NEWS --> PINS

Structure du Projet

api-news/
├── src/
│   ├── app.ts                      # Configuration Express
│   ├── server.ts                   # Point d'entrée
│   ├── modules/
│   │   ├── page/                   # Module pages
│   │   │   ├── controllers/
│   │   │   │   └── page.controller.ts
│   │   │   ├── services/
│   │   │   │   ├── page.service.ts
│   │   │   │   └── pageEvent.service.ts
│   │   │   ├── routes/
│   │   │   │   └── page.routes.ts
│   │   │   └── types/
│   │   │       └── page.types.ts
│   │   ├── comment/                # Module commentaires
│   │   ├── like/                   # Module réactions
│   │   ├── category/               # Module catégories
│   │   ├── template/               # Module templates
│   │   ├── pin/                    # Module épinglage
│   │   ├── view/                   # Module vues
│   │   └── preference/             # Module préférences
│   ├── core/
│   │   ├── cron/                   # Tâches planifiées
│   │   │   └── scheduledPages.cron.ts
│   │   └── events/                 # Event services
│   │       ├── pageEvent.service.ts
│   │       ├── commentEvent.service.ts
│   │       └── likeEvent.service.ts
│   ├── prisma-client/              # Prisma clients
│   │   ├── newsClient.ts           # Client news
│   │   └── memberClient.ts         # Client members
│   ├── config/
│   │   └── config.ts
│   ├── middleware/
│   │   ├── auth.middleware.ts
│   │   ├── workspace.middleware.ts
│   │   └── validation.middleware.ts
│   └── utils/
│       ├── ApiError.ts
│       └── catchAsync.ts
├── prisma/
│   └── schema.prisma               # Schéma 14 modèles
└── __tests/

Modèles de Données

Page

prisma
model Page {
  id                String    @id @default(auto()) @map("_id") @db.ObjectId
  title             String
  content           Json      // Editor.js format
  excerpt           String?

  // Hiérarchie
  parentId          String?   @db.ObjectId
  parent            Page?     @relation("PageHierarchy", fields: [parentId], references: [id], onDelete: NoAction, onUpdate: NoAction)
  children          Page[]    @relation("PageHierarchy")

  // Type de page
  pageType          String    // "feed", "article", "event", "poll"

  // Catégorie
  categoryId        String?   @db.ObjectId
  category          Category? @relation(fields: [categoryId], references: [id])

  // Workspace et Auteur
  workspaceId       String
  createdById       String    // memberId
  updatedById       String?   // memberId

  // Médias
  medias            Json[]    // [{ fileUrl, thumbnailUrl, type }]

  // Statut
  status            String    @default("draft") // draft, published, archived
  publishedAt       DateTime?
  scheduledAt       DateTime? // Publication programmée

  // Interactions
  views             View[]
  likes             Like[]
  comments          Comment[]
  pins              Pin[]

  // Compteurs (denormalized)
  viewCount         Int       @default(0)
  likeCount         Int       @default(0)
  commentCount      Int       @default(0)

  // Paramètres
  enableComments    Boolean   @default(true)
  enableLikes       Boolean   @default(true)
  enableNotifications Boolean @default(true)
  isPinned          Boolean   @default(false)

  // Metadata
  deleted_at        DateTime?
  createdAt         DateTime  @default(now())
  updatedAt         DateTime  @updatedAt

  @@index([workspaceId, deleted_at])
  @@index([categoryId])
  @@index([createdById])
  @@index([parentId])
  @@index([status])
  @@index([publishedAt])
  @@index([scheduledAt])
}

Comment

prisma
model Comment {
  id              String    @id @default(auto()) @map("_id") @db.ObjectId
  content         String
  pageId          String    @db.ObjectId
  page            Page      @relation(fields: [pageId], references: [id], onDelete: Cascade)

  // Auteur
  memberId        String    // ID du membre
  workspaceId     String

  // Hiérarchie (réponses)
  parentId        String?   @db.ObjectId
  parent          Comment?  @relation("CommentReplies", fields: [parentId], references: [id], onDelete: NoAction, onUpdate: NoAction)
  replies         Comment[] @relation("CommentReplies")

  // Réactions
  likes           CommentLike[]
  likeCount       Int       @default(0)

  // Métadonnées
  isEdited        Boolean   @default(false)
  editedAt        DateTime?
  deleted_at      DateTime?
  createdAt       DateTime  @default(now())
  updatedAt       DateTime  @updatedAt

  @@index([pageId])
  @@index([memberId])
  @@index([workspaceId])
  @@index([parentId])
}

Like

prisma
model Like {
  id              String    @id @default(auto()) @map("_id") @db.ObjectId
  pageId          String    @db.ObjectId
  page            Page      @relation(fields: [pageId], references: [id], onDelete: Cascade)

  memberId        String    // ID du membre
  workspaceId     String

  // Type de réaction
  reactionType    String    @default("like") // like, love, haha, wow, sad, angry

  createdAt       DateTime  @default(now())

  @@unique([pageId, memberId])
  @@index([pageId])
  @@index([memberId])
  @@index([workspaceId])
}

Category

prisma
model Category {
  id              String    @id @default(auto()) @map("_id") @db.ObjectId
  name            String
  slug            String
  description     String?
  icon            String?   // Emoji ou icon name
  color           String?   // Couleur hex

  workspaceId     String

  // Relations
  pages           Page[]

  // Ordre d'affichage
  order           Int       @default(0)

  isActive        Boolean   @default(true)
  deleted_at      DateTime?
  createdAt       DateTime  @default(now())
  updatedAt       DateTime  @updatedAt

  @@unique([workspaceId, slug])
  @@index([workspaceId])
}

Template

prisma
model Template {
  id              String    @id @default(auto()) @map("_id") @db.ObjectId
  name            String
  description     String?
  content         Json      // Editor.js content template

  workspaceId     String

  // Type de template
  pageType        String    // "article", "event", "poll"

  // Usage
  usageCount      Int       @default(0)

  isActive        Boolean   @default(true)
  deleted_at      DateTime?
  createdAt       DateTime  @default(now())
  updatedAt       DateTime  @updatedAt

  @@index([workspaceId])
  @@index([pageType])
}

View

prisma
model View {
  id              String    @id @default(auto()) @map("_id") @db.ObjectId
  pageId          String    @db.ObjectId
  page            Page      @relation(fields: [pageId], references: [id], onDelete: Cascade)

  memberId        String    // ID du membre
  workspaceId     String

  // Durée de lecture
  duration        Int?      // Secondes

  // Device info
  deviceType      String?   // mobile, desktop, tablet
  userAgent       String?

  createdAt       DateTime  @default(now())

  @@index([pageId])
  @@index([memberId])
  @@index([workspaceId])
  @@index([createdAt])
}

Pin

prisma
model Pin {
  id              String    @id @default(auto()) @map("_id") @db.ObjectId
  pageId          String    @db.ObjectId
  page            Page      @relation(fields: [pageId], references: [id], onDelete: Cascade)

  workspaceId     String
  pinnedBy        String    // memberId

  // Position
  order           Int       @default(0)

  // Expiration
  expiresAt       DateTime?

  createdAt       DateTime  @default(now())
  updatedAt       DateTime  @updatedAt

  @@unique([pageId, workspaceId])
  @@index([workspaceId])
  @@index([order])
}

PagePreference

prisma
model PagePreference {
  id              String    @id @default(auto()) @map("_id") @db.ObjectId
  pageId          String    @db.ObjectId

  memberId        String    // ID du membre
  workspaceId     String

  // Préférences
  isFavorite      Boolean   @default(false)
  isBookmarked    Boolean   @default(false)
  isFollowing     Boolean   @default(false) // Suivre les commentaires

  // Notifications
  notifyOnComment Boolean   @default(true)
  notifyOnLike    Boolean   @default(false)

  createdAt       DateTime  @default(now())
  updatedAt       DateTime  @updatedAt

  @@unique([pageId, memberId])
  @@index([memberId])
  @@index([workspaceId])
}

Routes API (Sélection)

📄 Pages (/api/v1/pages)

POST /api/v1/pages

Créer une nouvelle page.

Request :

json
{
  "title": "Nouvelle politique RH 2025",
  "content": {
    "blocks": [
      {
        "type": "paragraph",
        "data": {
          "text": "Contenu de la page..."
        }
      }
    ]
  },
  "excerpt": "Résumé de la page",
  "pageType": "article",
  "categoryId": "65f1234567890abcdef12345",
  "parentId": "65f9876543210fedcba98765",
  "workspaceId": "65faaaaaaaaaaaaaaaaaaaaa",
  "medias": [
    {
      "fileUrl": "https://storage.googleapis.com/.../photo.jpg",
      "thumbnailUrl": "https://storage.googleapis.com/.../photo-thumb.jpg",
      "type": "image"
    }
  ],
  "enableComments": true,
  "enableNotifications": true,
  "status": "published"
}

Response :

json
{
  "id": "65fbbbbbbbbbbbbbbbbbbbbb",
  "title": "Nouvelle politique RH 2025",
  "content": { ... },
  "pageType": "article",
  "status": "published",
  "publishedAt": "2025-01-21T10:00:00.000Z",
  "createdBy": {
    "id": "65fccccccccccccccccccccc",
    "firstName": "Marie",
    "lastName": "Dubois",
    "photo_url": "https://..."
  },
  "category": {
    "id": "65f1234567890abcdef12345",
    "name": "Ressources Humaines",
    "icon": "👥"
  },
  "viewCount": 0,
  "likeCount": 0,
  "commentCount": 0
}

Logique :

  1. Validation du contenu (Zod schema)
  2. Création de la page dans la base
  3. Si status === 'published', émettre événement AMQP page.created
  4. api-notification reçoit l'événement et crée des notifications

GET /api/v1/pages

Récupérer toutes les pages (avec filtres et pagination).

Query Params :

  • workspaceId : Requis - ID du workspace
  • page : Numéro de page (défaut: 1)
  • limit : Nombre d'éléments (défaut: 20)
  • pageType : Filtrer par type (article, feed, event)
  • categoryId : Filtrer par catégorie
  • status : Filtrer par statut (draft, published, archived)
  • search : Recherche dans titre et excerpt
  • sortBy : Tri (createdAt, publishedAt, viewCount, likeCount)
  • sortOrder : asc ou desc

Response :

json
{
  "data": [
    {
      "id": "65fbbbbbbbbbbbbbbbbbbbbb",
      "title": "Nouvelle politique RH 2025",
      "excerpt": "Résumé de la page",
      "pageType": "article",
      "status": "published",
      "publishedAt": "2025-01-21T10:00:00.000Z",
      "createdBy": {
        "firstName": "Marie",
        "lastName": "Dubois"
      },
      "category": {
        "name": "Ressources Humaines",
        "icon": "👥"
      },
      "medias": [...],
      "viewCount": 125,
      "likeCount": 45,
      "commentCount": 12,
      "isPinned": false
    }
  ],
  "meta": {
    "total": 150,
    "page": 1,
    "limit": 20,
    "totalPages": 8
  }
}

GET /api/v1/pages/:id

Récupérer une page par son ID (avec détails complets).

Response :

json
{
  "id": "65fbbbbbbbbbbbbbbbbbbbbb",
  "title": "Nouvelle politique RH 2025",
  "content": { ... },
  "excerpt": "Résumé",
  "pageType": "article",
  "status": "published",
  "publishedAt": "2025-01-21T10:00:00.000Z",

  "parent": {
    "id": "65f9876543210fedcba98765",
    "title": "Actualités RH"
  },

  "children": [
    {
      "id": "65fddddddddddddddddddddd",
      "title": "FAQ Politique RH"
    }
  ],

  "category": {
    "id": "65f1234567890abcdef12345",
    "name": "Ressources Humaines",
    "icon": "👥",
    "color": "#3B82F6"
  },

  "createdBy": {
    "id": "65fccccccccccccccccccccc",
    "firstName": "Marie",
    "lastName": "Dubois",
    "photo_url": "https://..."
  },

  "medias": [...],

  "viewCount": 125,
  "likeCount": 45,
  "commentCount": 12,

  "userInteractions": {
    "hasLiked": false,
    "hasViewed": true,
    "isFavorite": false,
    "isBookmarked": false
  },

  "isPinned": false,
  "enableComments": true,
  "enableLikes": true
}

Logique :

  1. Récupérer la page avec relations
  2. Incrémenter le compteur de vues (View)
  3. Récupérer les interactions de l'utilisateur

PATCH /api/v1/pages/:id

Mettre à jour une page.


DELETE /api/v1/pages/:id

Supprimer une page (soft delete).


POST /api/v1/pages/:id/publish

Publier une page en draft.

Response :

json
{
  "id": "65fbbbbbbbbbbbbbbbbbbbbb",
  "status": "published",
  "publishedAt": "2025-01-21T11:00:00.000Z"
}

Logique :

  1. Mettre à jour status = 'published' et publishedAt = NOW()
  2. Émettre événement AMQP page.created
  3. Notifications envoyées

POST /api/v1/pages/:id/schedule

Programmer une publication.

Request :

json
{
  "scheduledAt": "2025-01-25T09:00:00.000Z"
}

Logique :

  1. Mettre à jour scheduledAt
  2. Cron job vérifie toutes les minutes les pages à publier
  3. Quand scheduledAt <= NOW(), publier automatiquement

GET /api/v1/pages/:id/hierarchy

Récupérer la hiérarchie complète d'une page.

Response :

json
{
  "breadcrumb": [
    {
      "id": "65f0000000000000000000001",
      "title": "Accueil"
    },
    {
      "id": "65f9876543210fedcba98765",
      "title": "Actualités RH"
    },
    {
      "id": "65fbbbbbbbbbbbbbbbbbbbbb",
      "title": "Nouvelle politique RH 2025"
    }
  ],
  "children": [...]
}

💬 Commentaires (/api/v1/comments)

POST /api/v1/comments

Ajouter un commentaire à une page.

Request :

json
{
  "pageId": "65fbbbbbbbbbbbbbbbbbbbbb",
  "content": "Super article, merci pour le partage !",
  "parentId": null
}

Response :

json
{
  "id": "65feeeeeeeeeeeeeeeeeeeeeee",
  "content": "Super article, merci pour le partage !",
  "pageId": "65fbbbbbbbbbbbbbbbbbbbbb",
  "member": {
    "id": "65fccccccccccccccccccccc",
    "firstName": "Paul",
    "lastName": "Martin",
    "photo_url": "https://..."
  },
  "likeCount": 0,
  "replies": [],
  "createdAt": "2025-01-21T11:30:00.000Z"
}

Logique :

  1. Créer le commentaire
  2. Incrémenter page.commentCount
  3. Émettre 2 événements AMQP :
    • comment.created (channels: ['socket']) → Tous les membres du workspace
    • comment.created (channels: ['push', 'socket']) → Auteur de la page uniquement

GET /api/v1/comments/page/:pageId

Récupérer tous les commentaires d'une page (hiérarchie avec réponses).

Response :

json
{
  "data": [
    {
      "id": "65feeeeeeeeeeeeeeeeeeeeeee",
      "content": "Super article !",
      "member": {...},
      "likeCount": 5,
      "replies": [
        {
          "id": "65ffffffffffffffffffffffe",
          "content": "Merci !",
          "member": {...},
          "likeCount": 2,
          "createdAt": "2025-01-21T12:00:00.000Z"
        }
      ],
      "createdAt": "2025-01-21T11:30:00.000Z"
    }
  ],
  "total": 12
}

PATCH /api/v1/comments/:id

Modifier un commentaire.


DELETE /api/v1/comments/:id

Supprimer un commentaire (soft delete).


POST /api/v1/comments/:id/like

Aimer un commentaire.


❤️ Réactions (/api/v1/likes)

POST /api/v1/likes

Ajouter une réaction à une page.

Request :

json
{
  "pageId": "65fbbbbbbbbbbbbbbbbbbbbb",
  "reactionType": "love"
}

Types disponibles : like, love, haha, wow, sad, angry

Response :

json
{
  "id": "65fffffffffffffffffffff",
  "pageId": "65fbbbbbbbbbbbbbbbbbbbbb",
  "reactionType": "love",
  "createdAt": "2025-01-21T11:45:00.000Z"
}

Logique :

  1. Créer ou mettre à jour la réaction (upsert)
  2. Incrémenter page.likeCount
  3. Émettre événement AMQP like.created

DELETE /api/v1/likes/:pageId

Retirer sa réaction d'une page.


GET /api/v1/likes/page/:pageId

Récupérer toutes les réactions d'une page.

Response :

json
{
  "total": 45,
  "breakdown": {
    "like": 30,
    "love": 10,
    "wow": 5
  },
  "members": [
    {
      "member": {
        "firstName": "Marie",
        "lastName": "Dubois"
      },
      "reactionType": "love"
    }
  ]
}

🏷️ Catégories (/api/v1/categories)

POST /api/v1/categories

Créer une catégorie.

Request :

json
{
  "name": "Ressources Humaines",
  "slug": "ressources-humaines",
  "description": "Tout sur les RH",
  "icon": "👥",
  "color": "#3B82F6",
  "workspaceId": "65faaaaaaaaaaaaaaaaaaaaa",
  "order": 1
}

GET /api/v1/categories/workspace/:workspaceId

Récupérer toutes les catégories d'un workspace.


PATCH /api/v1/categories/:id

Mettre à jour une catégorie.


DELETE /api/v1/categories/:id

Supprimer une catégorie.


📋 Templates (/api/v1/templates)

POST /api/v1/templates

Créer un template.

Request :

json
{
  "name": "Template Article",
  "description": "Template pour les articles",
  "pageType": "article",
  "content": {
    "blocks": [...]
  },
  "workspaceId": "65faaaaaaaaaaaaaaaaaaaaa"
}

GET /api/v1/templates/workspace/:workspaceId

Récupérer tous les templates.


POST /api/v1/pages/from-template/:templateId

Créer une page depuis un template.


📌 Épinglage (/api/v1/pins)

POST /api/v1/pins

Épingler une page.

Request :

json
{
  "pageId": "65fbbbbbbbbbbbbbbbbbbbbb",
  "workspaceId": "65faaaaaaaaaaaaaaaaaaaaa",
  "order": 1,
  "expiresAt": "2025-02-01T00:00:00.000Z"
}

GET /api/v1/pins/workspace/:workspaceId

Récupérer les pages épinglées.


DELETE /api/v1/pins/:pageId

Désépingler une page.


📊 Statistiques (/api/v1/analytics)

GET /api/v1/analytics/page/:id

Statistiques d'une page.

Response :

json
{
  "pageId": "65fbbbbbbbbbbbbbbbbbbbbb",
  "views": {
    "total": 125,
    "unique": 98,
    "byDay": [
      { "date": "2025-01-20", "count": 45 },
      { "date": "2025-01-21", "count": 80 }
    ]
  },
  "likes": {
    "total": 45,
    "breakdown": {
      "like": 30,
      "love": 10,
      "wow": 5
    }
  },
  "comments": {
    "total": 12
  },
  "engagement": {
    "rate": 36.8,
    "averageReadTime": 120
  }
}

GET /api/v1/analytics/workspace/:workspaceId

Statistiques globales d'un workspace.

Response :

json
{
  "pages": {
    "total": 150,
    "published": 120,
    "drafts": 30
  },
  "interactions": {
    "views": 12500,
    "likes": 3200,
    "comments": 850
  },
  "topPages": [
    {
      "id": "65fbbbbbbbbbbbbbbbbbbbbb",
      "title": "Nouvelle politique RH",
      "views": 125,
      "engagement": 36.8
    }
  ],
  "topCategories": [
    {
      "id": "65f1234567890abcdef12345",
      "name": "Ressources Humaines",
      "pageCount": 25
    }
  ]
}

Services

Page Event Service

typescript
// services/pageEvent.service.ts
class PageEventService {
  async emitCreatedEvent(
    page: Page,
    req: Request,
    sendPushNotification: boolean = false
  ) {
    const channels = sendPushNotification
      ? ['push', 'socket']
      : ['socket']

    await amqpServer.emit('roomee.notification.page.created', {
      type: 'page.created',
      workspaceId: page.workspaceId,
      workspaceName: workspace.name,
      payload: {
        id: page.id,
        title: page.title,
        parent: page.parent ? {
          id: page.parent.id,
          title: page.parent.title,
          pageType: page.parent.pageType
        } : null,
        medias: page.medias
      },
      channels,
      metadata: {
        timestamp: new Date().toISOString(),
        actor: {
          id: req.user.memberId,
          name: `${member.firstName} ${member.lastName}`,
          type: 'member'
        },
        receiverIds: workspaceMembers.map(m => m.id)
      }
    })
  }

  async emitUpdatedEvent(page: Page, changes: any) {
    await amqpServer.emit('roomee.notification.page.updated', {
      type: 'page.updated',
      workspaceId: page.workspaceId,
      payload: {
        id: page.id,
        title: page.title,
        changes
      },
      channels: ['socket']
    })
  }

  async emitDeletedEvent(page: Page) {
    await amqpServer.emit('roomee.notification.page.deleted', {
      type: 'page.deleted',
      workspaceId: page.workspaceId,
      payload: {
        id: page.id,
        title: page.title
      },
      channels: ['socket']
    })
  }
}

Tâches Cron

Publication Programmée

typescript
// core/cron/scheduledPages.cron.ts
import cron from 'node-cron'

// Toutes les minutes
cron.schedule('* * * * *', async () => {
  const now = new Date()

  // Récupérer les pages à publier
  const pagesToPublish = await prisma.page.findMany({
    where: {
      status: 'draft',
      scheduledAt: {
        lte: now
      },
      deleted_at: null
    }
  })

  for (const page of pagesToPublish) {
    try {
      // Publier la page
      await prisma.page.update({
        where: { id: page.id },
        data: {
          status: 'published',
          publishedAt: now,
          scheduledAt: null
        }
      })

      // Émettre événement
      await pageEventService.emitCreatedEvent(page, null, true)

      logger.info('Scheduled page published', { pageId: page.id })
    } catch (error) {
      logger.error('Failed to publish scheduled page', {
        pageId: page.id,
        error
      })
    }
  }
})

Événements AMQP

Événements Émis

Routing KeyChannelsDescription
roomee.notification.page.created['socket'] ou ['push', 'socket']Page créée/publiée
roomee.notification.page.updated['socket']Page mise à jour
roomee.notification.page.deleted['socket']Page supprimée
roomee.notification.comment.created['socket'] → tous, ['push', 'socket'] → auteurCommentaire ajouté
roomee.notification.comment.deleted['socket']Commentaire supprimé
roomee.notification.like.created['socket']Like ajouté

Pattern de notification graduée (commentaires) :

typescript
// 1. Socket pour tous (mise à jour UI)
await commentEventService.emitCreatedEvent(comment, req, false)

// 2. Push + Socket pour l'auteur de la page
if (page.enableNotifications) {
  await commentEventService.emitCreatedEvent(comment, req, true, [page.createdById])
}

Socket.IO Events

typescript
// socket/newsSocket.ts
io.on('connection', (socket) => {
  // Rejoindre workspace
  socket.on('joinWorkspace', (workspaceId) => {
    socket.join(`workspace:${workspaceId}`)
  })

  // S'abonner à des types spécifiques
  socket.on('subscribeToTypes', ({ workspaceId, types }) => {
    types.forEach(type => {
      socket.join(`workspace:${workspaceId}:type:${type}`)
    })
  })

  // Rejoindre une page spécifique
  socket.on('joinPage', (pageId) => {
    socket.join(`page:${pageId}`)
  })
})

// Émissions serveur
io.to(`workspace:${workspaceId}`).emit('page.created', pageData)
io.to(`page:${pageId}`).emit('comment.created', commentData)
io.to(`page:${pageId}`).emit('like.created', likeData)

Synchronisation des Membres

typescript
// Endpoint appelé périodiquement
POST /sync/members

// Logique
const members = await memberClient.member.findMany({
  where: {
    workspaces: {
      some: {
        workspaceId: workspaceId
      }
    }
  }
})

// Mettre à jour le cache local
await updateMemberCache(members)

Configuration

bash
# Server
PORT=3004

# Databases
DATABASE_URL=mongodb://localhost:27017/roomee_news
MEMBER_DATABASE_URL=mongodb://localhost:27017/roomee_members

# JWT
JWT_ACCESS_SECRET=your-jwt-secret

# AMQP
AMQP_GATEWAY_URL=amqp://localhost:5672
AMQP_EXCHANGE_NAME=roomee_events
AMQP_ROUTING_KEY_BASE=roomee.notification.page
AMQP_ROUTING_KEYS=["page.*", "comment.*", "like.*"]

# Service URLs
MEMBER_API_URL=http://localhost:3001

# API Keys
MEMBER_SERVICE_API_KEY=your-member-api-key

Bonnes Pratiques

1. Toujours filtrer par workspaceId

typescript
const pages = await prisma.page.findMany({
  where: {
    workspaceId: req.user.workspaceId,
    deleted_at: null
  }
})

2. Utiliser les compteurs denormalisés

typescript
// Au lieu de compter à chaque fois
const commentCount = await prisma.comment.count({ where: { pageId } })

// Utiliser le compteur denormalisé
const page = await prisma.page.findUnique({
  where: { id },
  select: { commentCount: true }
})

3. Émettre les bons événements

typescript
// Socket pour tous (UI update)
await pageEventService.emitCreatedEvent(page, req, false)

// Push + Socket pour notification complète
if (page.enableNotifications) {
  await pageEventService.emitCreatedEvent(page, req, true)
}

Améliorations Futures

  • [ ] Recherche full-text (Elasticsearch)
  • [ ] Versionning des pages (historique)
  • [ ] Approbation workflow (review before publish)
  • [ ] Mentions dans commentaires (@user)
  • [ ] Hashtags et recherche par tags
  • [ ] Reactions personnalisées par workspace
  • [ ] Export PDF des pages
  • [ ] Analytics avancés (heatmaps, scroll depth)

Documentation technique Roomee Services