@molecule/api-resource-thread
API resource · threads · API (Node) · v1.0.5 · Apache-2.0
Threaded discussions with messages, read-tracking, and unread counts for any resource
npm install @molecule/api-resource-threadHow 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/readLive demos
Running apps built with Molecule that install this package — open the app or study the template.
Reference
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.
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.tsJSDoc, not this file.
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
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
}
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
}
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
}
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
}
CreateMessageInput
Input for creating a new message in a thread.
interface CreateMessageInput {
/** The message body text. */
body: string
}
UpdateMessageInput
Input for updating an existing message.
interface UpdateMessageInput {
/** The updated message body text. */
body: string
}
PaginationOptions
Options for paginated queries.
interface PaginationOptions {
/** Maximum number of results to return. */
limit?: number
/** Number of results to skip. */
offset?: number
}
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
}
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
}
Functions
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.
del(req, res)
Deletes a thread. Only the thread creator can delete.
function del(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req— The request withthreadIdparam.res— The response object.
deleteMsg(req, res)
Deletes a message. Only the message author can delete.
function deleteMsg(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req— The request withmessageIdparam.res— The response object.
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.
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 withthreadIdparam andlastReadMessageIdin body.res— The response object.
listMessages(req, res)
Lists paginated messages in a thread.
function listMessages(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req— The request withthreadIdparam.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 withthreadIdparam and message 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 withthreadIdparam.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 withthreadIdparam and update body.res— The response object.
updateMsg(req, res)
Updates an existing message. Only the message author can update.
function updateMsg(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req— The request withmessageIdparam and update 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Constants
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']
},
]
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
>
updateThreadSchema
Schema for validating thread update input.
const updateThreadSchema: z.ZodObject<
{ title: z.ZodOptional<z.ZodString>; closed: z.ZodOptional<z.ZodBoolean> },
z.core.$strip
>
createMessageSchema
Schema for validating message creation input.
const createMessageSchema: z.ZodObject<{ body: z.ZodString }, z.core.$strip>
updateMessageSchema
Schema for validating message update input.
const updateMessageSchema: z.ZodObject<{ body: z.ZodString }, 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.1zod^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 offresult.data(server). On the client,unwrapList(res)from@molecule/app-httpnormalizes this envelope (pass it the whole HttpResponse), so the rows come back; reading the response as a bare array — orres.dataalone (which is the envelope) — yields an EMPTY list. SINGLE-USER BY DESIGN — a thread is PRIVATE to its creator. Every read and write, includingGET /threads/:threadId,GET /threads/:threadId/messages, and POSTING a message, is authorized againstthread.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 aclosedthread.
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
on migrate (a CREATE TABLE IF NOT EXISTS, so a table the app's own migrations already create keeps only its own columns: ALTER in the ones it lacks); anywhere else run it once — nothing at runtime
creates them.