@molecule/api-resource-follow
v1.0.1
Published
Follow/unfollow users or any resource with polymorphic targeting
Maintainers
Readme
@molecule/api-resource-follow
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.
Follow/unfollow resource for molecule.dev.
Polymorphic follow system for users or any resource type. Supports followers list, following list, and follow status checks.
Quick Start
import { routes, requestHandlerMap } from '@molecule/api-resource-follow'
// Wire routes into your Express app via mlcl inject
// POST /follow/:targetType/:targetId
// DELETE /follow/:targetType/:targetId
// GET /:targetType/:targetId/followers
// GET /following
// GET /follow/check/:targetType/:targetIdType
resource
Installation
npm install @molecule/api-resource-follow @molecule/api-database @molecule/api-i18n @molecule/api-logger @molecule/api-resourceAPI
Interfaces
Follow
A follow relationship between a user and a target resource.
interface Follow {
/** Unique follow identifier. */
id: string
/** The ID of the user who is following. */
followerId: string
/** The type of target being followed (e.g. 'user', 'project'). */
targetType: string
/** The ID of the target being followed. */
targetId: string
/** When the follow was created (ISO 8601). */
createdAt: string
/** When the follow was last updated (ISO 8601). */
updatedAt: 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
}Functions
checkFollowing(req, res)
Checks if the current user is following a target.
function checkFollowing(req: MoleculeRequest, res: MoleculeResponse): Promise<void>req— The request withtargetTypeandtargetIdparams.res— The response object.
create(req, res)
Creates a follow relationship. Idempotent.
function create(req: MoleculeRequest, res: MoleculeResponse): Promise<void>req— The request withtargetTypeandtargetIdparams.res— The response object.
del(req, res)
Removes a follow relationship.
function del(req: MoleculeRequest, res: MoleculeResponse): Promise<void>req— The request withtargetTypeandtargetIdparams.res— The response object.
follow(followerId, targetType, targetId)
Creates a follow relationship. Idempotent — returns existing follow if already following.
function follow(followerId: string, targetType: string, targetId: string): Promise<Follow>followerId— The ID of the follower.targetType— The type of target being followed.targetId— The ID of the target being followed.
Returns: The created or existing follow.
following(req, res)
Lists targets the current user is following.
function following(req: MoleculeRequest, res: MoleculeResponse): Promise<void>req— The request with pagination query params.res— The response object.
getFollowerCount(targetType, targetId)
Gets the follower count for a target.
function getFollowerCount(targetType: string, targetId: string): Promise<number>targetType— The type of target.targetId— The ID of the target.
Returns: The number of followers.
getFollowers(targetType, targetId, options)
Gets paginated followers of a target.
function getFollowers(
targetType: string,
targetId: string,
options?: PaginationOptions,
): Promise<PaginatedResult<Follow>>targetType— The type of target.targetId— The ID of the target.options— Pagination options.
Returns: Paginated followers.
getFollowing(userId, options)
Gets paginated targets a user is following.
function getFollowing(userId: string, options?: PaginationOptions): Promise<PaginatedResult<Follow>>userId— The follower's user ID.options— Pagination options.
Returns: Paginated follows.
getFollowingCount(userId)
Gets the following count for a user.
function getFollowingCount(userId: string): Promise<number>userId— The user ID.
Returns: The number of targets the user is following.
isFollowing(followerId, targetType, targetId)
Checks if a user is following a target.
function isFollowing(followerId: string, targetType: string, targetId: string): Promise<boolean>followerId— The follower's user ID.targetType— The type of target.targetId— The ID of the target.
Returns: true if following.
list(req, res)
Lists paginated followers of a target.
function list(req: MoleculeRequest, res: MoleculeResponse): Promise<void>req— The request withtargetTypeandtargetIdparams.res— The response object.
unfollow(followerId, targetType, targetId)
Removes a follow relationship.
function unfollow(followerId: string, targetType: string, targetId: string): Promise<void>followerId— The ID of the follower.targetType— The type of target.targetId— The ID of the target.
Constants
requestHandlerMap
Handler map for follow routes.
const requestHandlerMap: {
readonly create: typeof create
readonly del: typeof del
readonly list: typeof list
readonly following: typeof following
readonly checkFollowing: typeof checkFollowing
}routes
Routes for follow/unfollow, followers, following, and status check.
const routes: readonly [
{
readonly method: 'post'
readonly path: '/follow/:targetType/:targetId'
readonly handler: 'create'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'delete'
readonly path: '/follow/:targetType/:targetId'
readonly handler: 'del'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'get'
readonly path: '/:targetType/:targetId/followers'
readonly handler: 'list'
},
{
readonly method: 'get'
readonly path: '/following'
readonly handler: 'following'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'get'
readonly path: '/follow/check/:targetType/:targetId'
readonly handler: 'checkFollowing'
readonly middlewares: readonly ['authenticate']
},
]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
Runtime Dependencies
@molecule/api-database@molecule/api-i18n@molecule/api-logger@molecule/api-resourceList 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. Table:src/__setup__/follows.sqlcreates the singlefollowstable. An mlcl-scaffolded API replays__setup__/*.sqlautomatically on migrate; anywhere else run it once — nothing at runtime creates it.
The follower is ALWAYS the authenticated user: handlers read
res.locals.session (populated by your global auth middleware) and 401
without it — never accept a follower userId from the body or params.
GET /:targetType/:targetId/followers is deliberately PUBLIC; gate it
yourself if follower lists are private in your app.
targetType is a free-form string (user, post, …) — the package does
not validate it against your schema, so constrain accepted values in your
app if arbitrary types would be a problem.
E2E Tests
Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:
- [ ] User A follows user B (
POST /follow/user/:B): B's follower count and A's following count each increment by exactly one, B appears in A's following list (GET /following) and A appears in B's followers list (GET /user/:B/followers). Reload — the edge and both counts persist (it's a realfollowsrow, not local UI state). - [ ] Following is IDEMPOTENT: A following B a second time (double-tap Follow
or replay the POST) creates NO duplicate edge and does NOT double-count —
exactly one
followsrow exists for (A → B) and both counts are unchanged. - [ ] Unfollow (
DELETE /follow/user/:B) removes the edge: A's following count and B's follower count each decrement back, B leaves A's following list, A leaves B's followers, andGET /follow/check/user/:Bnow returns{ following: false }. - [ ] You cannot follow yourself: the UI never offers Follow on your own
profile, and following your own id never inflates your own counts. This
package's
follow()does not rejectfollowerId === targetId, so the app must guard it — verify the guard exists, don't assume it. - [ ] AUTHORIZATION — the follower is ALWAYS the session user: handlers read
res.locals.sessionand 401 without it, so no UI or endpoint lets you follow/unfollow on behalf of another user by passing their id, and follow/unfollow act only on your own edges. Signed in as A you can never make B follow or unfollow anyone.
