← All @molecule/* packages · App templates

@molecule/api-resource-thread

API resource · threads · API (Node) · v1.0.1 · Apache-2.0

Threaded discussions with messages, read-tracking, and unread counts for any resource

npm install @molecule/api-resource-thread

npm · Source on GitHub

How it works

@molecule/api-resource-thread is an API resource: the routes, validation and storage for threads, built on the database and auth cores so it runs on whichever providers your app has bonded.

import { routes, requestHandlerMap } from '@molecule/api-resource-thread'

// Wire routes into your Express app via mlcl inject
// POST   /threads
// GET    /threads
// GET    /threads/unread
// GET    /threads/:threadId
// PATCH  /threads/:threadId
// DELETE /threads/:threadId
// GET    /threads/:threadId/messages
// POST   /threads/:threadId/messages
// PUT    /threads/messages/:messageId
// DELETE /threads/messages/:messageId
// POST   /threads/:threadId/read

Reference

Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit src/index.ts JSDoc, not this file.

Threaded discussion resource for molecule.dev.

Conversation threads with messages, read-tracking, and unread counts. Threads can optionally attach to any resource via resourceType/resourceId.

Quick Start

import { routes, requestHandlerMap } from '@molecule/api-resource-thread'

// Wire routes into your Express app via mlcl inject
// POST   /threads
// GET    /threads
// GET    /threads/unread
// GET    /threads/:threadId
// PATCH  /threads/:threadId
// DELETE /threads/:threadId
// GET    /threads/:threadId/messages
// POST   /threads/:threadId/messages
// PUT    /threads/messages/:messageId
// DELETE /threads/messages/:messageId
// POST   /threads/:threadId/read

Type

resource

Installation

npm install @molecule/api-resource-thread @molecule/api-database @molecule/api-i18n @molecule/api-logger @molecule/api-resource zod

API

Interfaces

CreateMessageInput

Input for creating a new message in a thread.

interface CreateMessageInput {
  /** The message body text. */
  body: string
}

CreateThreadInput

Input for creating a new thread.

interface CreateThreadInput {
  /** Display title for the thread. */
  title: string
  /** Optional resource type to attach the thread to. */
  resourceType?: string
  /** Optional resource ID to attach the thread to. */
  resourceId?: string
}

PaginatedResult

A paginated result set.

interface PaginatedResult<T> {
  /** The result items for the current page. */
  data: T[]
  /** Total number of matching items across all pages. */
  total: number
  /** Maximum number of results per page. */
  limit: number
  /** Number of results skipped. */
  offset: number
}

PaginationOptions

Options for paginated queries.

interface PaginationOptions {
  /** Maximum number of results to return. */
  limit?: number
  /** Number of results to skip. */
  offset?: number
}

Thread

A discussion thread containing messages between participants.

interface Thread {
  /** Unique thread identifier. */
  id: string
  /** Display title for the thread. */
  title: string
  /** The ID of the user who created the thread. */
  creatorId: string
  /** Optional resource type this thread is attached to (e.g. 'project', 'order'). */
  resourceType: string | null
  /** Optional resource ID this thread is attached to. */
  resourceId: string | null
  /** Whether the thread is closed for new messages. */
  closed: boolean
  /** When the thread was created (ISO 8601). */
  createdAt: string
  /** When the thread was last updated (ISO 8601). */
  updatedAt: string
}

ThreadMessage

A message within a thread.

interface ThreadMessage {
  /** Unique message identifier. */
  id: string
  /** The ID of the thread this message belongs to. */
  threadId: string
  /** The ID of the user who sent the message. */
  userId: string
  /** The message body text. */
  body: string
  /** When the message was last edited, or `null` if never edited. */
  editedAt: string | null
  /** When the message was created (ISO 8601). */
  createdAt: string
  /** When the message was last updated (ISO 8601). */
  updatedAt: string
}

ThreadQuery

Options for querying threads.

interface ThreadQuery extends PaginationOptions {
  /** Filter to only threads attached to this resource type. */
  resourceType?: string
  /** Filter to only threads attached to this resource ID. */
  resourceId?: string
  /** Filter to only open or closed threads. */
  closed?: boolean
}

ThreadReadStatus

Tracks the last-read position for a user in a thread.

interface ThreadReadStatus {
  /** The thread ID. */
  threadId: string
  /** The user ID. */
  userId: string
  /** The ID of the last message read by this user. */
  lastReadMessageId: string
  /** When this read status was last updated (ISO 8601). */
  updatedAt: string
}

UpdateMessageInput

Input for updating an existing message.

interface UpdateMessageInput {
  /** The updated message body text. */
  body: string
}

UpdateThreadInput

Input for updating an existing thread.

interface UpdateThreadInput {
  /** Updated title for the thread. */
  title?: string
  /** Whether to close or reopen the thread. */
  closed?: boolean
}

Functions

addMessage(threadId, userId, data)

Adds a message to a thread.

function addMessage(
  threadId: string,
  userId: string,
  data: CreateMessageInput,
): Promise<ThreadMessage | null>
  • threadId — The thread ID to add the message to.
  • userId — The ID of the user sending the message.
  • data — The message creation input.

Returns: The created message, or null if the thread is closed or not found.

create(req, res)

Creates a new discussion thread.

function create(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
  • req — The request with thread creation body.
  • res — The response object.

createMessage(req, res)

Adds a new message to a thread.

function createMessage(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
  • req — The request with threadId param and message body.
  • res — The response object.

createThread(creatorId, data)

Creates a new thread.

function createThread(creatorId: string, data: CreateThreadInput): Promise<Thread>
  • creatorId — The ID of the user creating the thread.
  • data — The thread creation input.

Returns: The created thread.

del(req, res)

Deletes a thread. Only the thread creator can delete.

function del(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
  • req — The request with threadId param.
  • res — The response object.

deleteMessage(messageId, userId)

Deletes a message. Only the message author can delete.

function deleteMessage(messageId: string, userId: string): Promise<boolean>
  • messageId — The message ID to delete.
  • userId — The requesting user's ID (must match message author).

Returns: true if deleted, false if not found or unauthorized.

deleteMsg(req, res)

Deletes a message. Only the message author can delete.

function deleteMsg(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
  • req — The request with messageId param.
  • res — The response object.

deleteThread(threadId, userId)

Deletes a thread. Only the thread creator can delete.

function deleteThread(threadId: string, userId: string): Promise<boolean>
  • threadId — The thread ID to delete.
  • userId — The requesting user's ID (must match thread creator).

Returns: true if deleted, false if not found or unauthorized.

getMessages(threadId, options)

Retrieves paginated messages for a thread, ordered by creation date ascending.

function getMessages(
  threadId: string,
  options?: PaginationOptions,
): Promise<PaginatedResult<ThreadMessage>>
  • threadId — The thread ID to get messages for.
  • options — Pagination options.

Returns: A paginated result of messages.

getThreadById(threadId)

Retrieves a single thread by ID.

function getThreadById(threadId: string): Promise<Thread | null>
  • threadId — The thread ID to look up.

Returns: The thread or null if not found.

getThreads(userId, options)

Retrieves paginated threads for a user (threads where the user has posted messages).

function getThreads(userId: string, options?: ThreadQuery): Promise<PaginatedResult<Thread>>
  • userId — The user ID to find threads for.
  • options — Query and pagination options.

Returns: A paginated result of threads.

getUnreadCount(userId)

Returns the number of unread threads for a user. A thread is unread if it has messages newer than the user's last-read position, or if the user has never read it and it contains messages.

function getUnreadCount(userId: string): Promise<number>
  • userId — The user ID.

Returns: The count of threads with unread messages.

list(req, res)

Lists paginated threads for the authenticated user.

function list(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
  • req — The request with optional query params for filtering and pagination.
  • res — The response object.

listMessages(req, res)

Lists paginated messages in a thread.

function listMessages(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
  • req — The request with threadId param.
  • res — The response object.

markRead(threadId, userId, lastReadMessageId)

Marks a thread as read up to a specific message for a user.

function markRead(threadId: string, userId: string, lastReadMessageId: string): Promise<void>
  • threadId — The thread ID.
  • userId — The user marking the thread as read.
  • lastReadMessageId — The ID of the last message read.

markThreadRead(req, res)

Marks a thread as read up to a specific message for the authenticated user.

function markThreadRead(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
  • req — The request with threadId param and lastReadMessageId in body.
  • res — The response object.

read(req, res)

Retrieves a single thread by ID.

function read(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
  • req — The request with threadId param.
  • res — The response object.

unread(_req, res)

Returns the number of threads with unread messages for the authenticated user.

function unread(_req: MoleculeRequest, res: MoleculeResponse): Promise<void>
  • _req — The request object (unused).
  • res — The response object.

update(req, res)

Updates an existing thread. Only the thread creator can update.

function update(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
  • req — The request with threadId param and update body.
  • res — The response object.

updateMessage(messageId, userId, data)

Updates a message. Only the message author can update.

function updateMessage(
  messageId: string,
  userId: string,
  data: UpdateMessageInput,
): Promise<ThreadMessage | null>
  • messageId — The message ID to update.
  • userId — The requesting user's ID (must match message author).
  • data — The update input.

Returns: The updated message or null if not found or unauthorized.

updateMsg(req, res)

Updates an existing message. Only the message author can update.

function updateMsg(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
  • req — The request with messageId param and update body.
  • res — The response object.

updateThread(threadId, userId, data)

Updates a thread. Only the thread creator can update.

function updateThread(
  threadId: string,
  userId: string,
  data: UpdateThreadInput,
): Promise<Thread | null>
  • threadId — The thread ID to update.
  • userId — The requesting user's ID (must match thread creator).
  • data — The update input.

Returns: The updated thread or null if not found or unauthorized.

Constants

createMessageSchema

Schema for validating message creation input.

const createMessageSchema: z.ZodObject<{ body: z.ZodString }, z.core.$strip>

createThreadSchema

Schema for validating thread creation input.

const createThreadSchema: z.ZodObject<
  {
    title: z.ZodString
    resourceType: z.ZodOptional<z.ZodString>
    resourceId: z.ZodOptional<z.ZodString>
  },
  z.core.$strip
>

requestHandlerMap

Handler map for thread routes.

const requestHandlerMap: {
  readonly create: typeof create
  readonly list: typeof list
  readonly read: typeof read
  readonly update: typeof update
  readonly del: typeof del
  readonly listMessages: typeof listMessages
  readonly createMessage: typeof createMessage
  readonly updateMsg: typeof updateMsg
  readonly deleteMsg: typeof deleteMsg
  readonly markThreadRead: typeof markThreadRead
  readonly unread: typeof unread
}

routes

Routes for thread CRUD, messages, and read-tracking.

const routes: readonly [
  {
    readonly method: 'post'
    readonly path: '/threads'
    readonly handler: 'create'
    readonly middlewares: readonly ['authenticate']
  },
  {
    readonly method: 'get'
    readonly path: '/threads'
    readonly handler: 'list'
    readonly middlewares: readonly ['authenticate']
  },
  {
    readonly method: 'get'
    readonly path: '/threads/unread'
    readonly handler: 'unread'
    readonly middlewares: readonly ['authenticate']
  },
  { readonly method: 'get'; readonly path: '/threads/:threadId'; readonly handler: 'read' },
  {
    readonly method: 'patch'
    readonly path: '/threads/:threadId'
    readonly handler: 'update'
    readonly middlewares: readonly ['authenticate']
  },
  {
    readonly method: 'delete'
    readonly path: '/threads/:threadId'
    readonly handler: 'del'
    readonly middlewares: readonly ['authenticate']
  },
  {
    readonly method: 'get'
    readonly path: '/threads/:threadId/messages'
    readonly handler: 'listMessages'
  },
  {
    readonly method: 'post'
    readonly path: '/threads/:threadId/messages'
    readonly handler: 'createMessage'
    readonly middlewares: readonly ['authenticate']
  },
  {
    readonly method: 'put'
    readonly path: '/threads/messages/:messageId'
    readonly handler: 'updateMsg'
    readonly middlewares: readonly ['authenticate']
  },
  {
    readonly method: 'delete'
    readonly path: '/threads/messages/:messageId'
    readonly handler: 'deleteMsg'
    readonly middlewares: readonly ['authenticate']
  },
  {
    readonly method: 'post'
    readonly path: '/threads/:threadId/read'
    readonly handler: 'markThreadRead'
    readonly middlewares: readonly ['authenticate']
  },
]

updateMessageSchema

Schema for validating message update input.

const updateMessageSchema: z.ZodObject<{ body: z.ZodString }, z.core.$strip>

updateThreadSchema

Schema for validating thread update input.

const updateThreadSchema: z.ZodObject<
  { title: z.ZodOptional<z.ZodString>; closed: z.ZodOptional<z.ZodBoolean> },
  z.core.$strip
>

Injection Notes

Requirements

Peer dependencies:

  • @molecule/api-database ^1.0.1
  • @molecule/api-i18n ^1.0.1
  • @molecule/api-logger ^1.0.1
  • @molecule/api-resource ^1.0.1
  • zod ^4.0.0

Runtime Dependencies

  • @molecule/api-database

  • @molecule/api-i18n

  • @molecule/api-logger

  • @molecule/api-resource

  • zod

  • List endpoints return a PAGINATED envelope { data, total, limit, offset }, not a bare array — read the rows off result.data (server). On the client, unwrapList(res) from @molecule/app-http normalizes this envelope (pass it the whole HttpResponse), so the rows come back; reading the response as a bare array — or res.data alone (which is the envelope) — yields an EMPTY list. SINGLE-USER BY DESIGN — a thread is PRIVATE to its creator. Every read and write, including GET /threads/:threadId, GET /threads/:threadId/messages, and POSTING a message, is authorized against thread.creatorId === userId; any other (or anonymous) caller gets 404 — existence is not leaked. Out of the box this resource does NOT support multi-participant conversations: for user-to-user messaging use @molecule/api-resource-message (participant model), or put your own participant/role gate (e.g. via @molecule/api-resource-share) in front of these handlers. Messages cannot be added to a closed thread.

Session-auth prerequisite: handlers read the caller from res.locals.session.userId and fail closed with 401 — mount the routes behind your global auth middleware. The two routes declared without an authenticate middleware (read, listMessages) still require a session in-handler (defense-in-depth for scanners that drop bare middleware strings).

Tables: src/__setup__/threads.sql creates threads, thread_messages, and thread_read_status. An mlcl-scaffolded API replays __setup__/*.sql automatically on migrate; anywhere else run it once — nothing at runtime creates them.