@molecule/api-resource-inventory
v1.0.1
Published
Inventory resource with stock tracking, reservations, low-stock alerts, movement history, and bulk updates.
Downloads
523
Maintainers
Readme
@molecule/api-resource-inventory
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.
Inventory resource for molecule.dev.
Provides stock tracking with reservations, low-stock alerts, movement history, and bulk update support.
Quick Start
import { routes, requestHandlerMap } from '@molecule/api-resource-inventory'Type
resource
Installation
npm install @molecule/api-resource-inventory @molecule/api-database @molecule/api-i18n @molecule/api-logger @molecule/api-resourceAPI
Interfaces
BulkStockAdjustment
Input for a single item in a bulk stock update.
interface BulkStockAdjustment {
/** The product to adjust. */
productId: string
/** Optional variant identifier. */
variantId?: string
/** Quantity to adjust. */
quantity: number
/** Type of adjustment. */
type: StockAdjustmentType
/** Reason for the adjustment. */
reason?: string
}BulkUpdateItemResult
Result of a single item in a bulk update.
interface BulkUpdateItemResult {
/** The product that was adjusted. */
productId: string
/** Optional variant identifier. */
variantId?: string
/** Whether this individual adjustment succeeded. */
success: boolean
/** Error message if the adjustment failed. */
error?: string
/** Updated stock info if successful. */
stock?: StockInfo
}BulkUpdateResult
Result of a bulk stock update operation.
interface BulkUpdateResult {
/** Total number of adjustments attempted. */
total: number
/** Number of successful adjustments. */
succeeded: number
/** Number of failed adjustments. */
failed: number
/** Per-item results. */
results: BulkUpdateItemResult[]
}InventorySession
Structural view of the fields on res.locals.session this resource inspects to
decide authorization. All fields are optional — a standard molecule session
carries only userId; apps that model roles may also set isAdmin/role/
roles/permissions claims, which are honored here.
interface InventorySession {
/** Authenticated user id (set by the global auth middleware). */
userId?: string
/** Optional boolean admin claim. */
isAdmin?: boolean
/** Optional single-role claim. */
role?: string
/** Optional multi-role claim. */
roles?: string[]
/** Optional permission strings claim. */
permissions?: string[]
}LowStockAlert
A low-stock alert entry.
interface LowStockAlert {
/** The product that is low on stock. */
productId: string
/** Optional variant identifier. */
variantId?: string
/** Current available quantity. */
available: number
/** The low-stock threshold. */
threshold: number
}PaginatedResult
A paginated result set.
interface PaginatedResult<T> {
/** The items in this page. */
data: T[]
/** Total number of items matching the query. */
total: number
/** Current page number (1-based). */
page: number
/** Number of items per page. */
limit: number
}PaginationOptions
Pagination options for list queries.
interface PaginationOptions {
/** Page number (1-based). */
page?: number
/** Number of items per page. */
limit?: number
}Reservation
A stock reservation tied to an order.
interface Reservation {
/** Unique reservation identifier. */
id: string
/** The product being reserved. */
productId: string
/** Optional variant identifier. */
variantId?: string
/** Quantity reserved. */
quantity: number
/** The order this reservation belongs to. */
orderId: string
/** The authenticated user who created this reservation, if known. */
userId?: string
/** When the reservation was created. */
createdAt: string
}ReservationRow
Internal database row for a stock reservation.
interface ReservationRow {
/** Unique reservation identifier. */
id: string
/** The product being reserved. */
productId: string
/** Optional variant identifier. */
variantId: string | null
/** Quantity reserved. */
quantity: number
/** The order this reservation belongs to. */
orderId: string
/** The authenticated user who created this reservation (null for legacy rows). */
userId: string | null
/** Creation timestamp. */
createdAt: string
}ReserveStockInput
Input for creating a stock reservation.
interface ReserveStockInput {
/** Quantity to reserve. */
quantity: number
/** The order to associate this reservation with. */
orderId: string
}StockAdjustment
Input for adjusting stock levels.
interface StockAdjustment {
/** Optional variant identifier. */
variantId?: string
/** Quantity to adjust by (or absolute value for 'set'). */
quantity: number
/** Type of adjustment: add, remove, or set to absolute value. */
type: StockAdjustmentType
/** Human-readable reason for the adjustment. */
reason?: string
}StockInfo
Current stock information for a product or variant.
interface StockInfo {
/** The product this stock belongs to. */
productId: string
/** Optional variant identifier (size, color, etc.). */
variantId?: string
/** Quantity available for purchase (total − reserved). */
available: number
/** Quantity currently reserved by pending orders. */
reserved: number
/** Total quantity in stock (available + reserved). */
total: number
/** Threshold below which the product is considered low-stock. */
lowStockThreshold: number
/** Whether current available stock is at or below the threshold. */
isLowStock: boolean
}StockMovement
A record of a stock movement (adjustment, reservation, release, or confirmation).
interface StockMovement {
/** Unique movement identifier. */
id: string
/** The product affected. */
productId: string
/** Optional variant identifier. */
variantId?: string
/** Type of movement. */
type: StockMovementType
/** Quantity change (positive for additions, negative for removals). */
quantity: number
/** Reason or description for this movement. */
reason?: string
/** Associated order or reservation identifier. */
referenceId?: string
/** When this movement occurred. */
createdAt: string
}StockMovementRow
Internal database row for a stock movement.
interface StockMovementRow {
/** Unique movement identifier. */
id: string
/** The product affected. */
productId: string
/** Optional variant identifier. */
variantId: string | null
/** Type of movement. */
type: StockMovementType
/** Quantity change. */
quantity: number
/** Reason for this movement. */
reason: string | null
/** Associated reference identifier. */
referenceId: string | null
/** Creation timestamp. */
createdAt: string
}StockRow
Internal database row for an inventory stock record.
interface StockRow {
/** Unique stock record identifier. */
id: string
/** The product this stock belongs to. */
productId: string
/** Optional variant identifier. */
variantId: string | null
/** Total quantity in stock. */
total: number
/** Quantity currently reserved. */
reserved: number
/** Low-stock threshold. */
lowStockThreshold: number
/** Creation timestamp. */
createdAt: string
/** Last modification timestamp. */
updatedAt: string
}Types
StockAdjustmentType
Types of stock adjustment operations.
type StockAdjustmentType = 'add' | 'remove' | 'set'StockMovementType
Types of stock movements recorded in the movement history.
type StockMovementType = 'adjustment' | 'reservation' | 'release' | 'confirmation'Functions
assertInventoryAdmin(res)
In-handler admin guard for the admin-only inventory mutations. Writes the
appropriate JSON error and returns false when the caller is not an
authorized admin — 401 when unauthenticated, 403 when authenticated but
not an admin. Returns true (and writes nothing) when the caller is an admin.
Call this at the top of every admin handler so protection holds independently of the route middleware (defense-in-depth, fail-closed).
function assertInventoryAdmin(res: MoleculeResponse): booleanres— The response, whoselocals.sessionis inspected and onto which an error is written when access is denied.
Returns: true when the caller is an authorized admin, otherwise false.
assertReservationActor(res, reservationUserId)
In-handler guard for reservation-lifecycle mutations (release, confirm).
A reservation is owned by the user who created it; only that owner — or an
inventory admin — may release or confirm it. Writes the appropriate JSON
error and returns false when access is denied (401 when unauthenticated,
403 when authenticated but neither the owner nor an admin); returns true
(writing nothing) when the caller may act.
Fail-closed: a reservation with no recorded owner (null — e.g. a legacy row
created before ownership binding) is accessible only to admins.
function assertReservationActor(res: MoleculeResponse, reservationUserId: string | null): booleanres— The response, whoselocals.sessionis inspected and onto which an error is written when access is denied.reservationUserId— TheuserIdrecorded on the reservation, ornull.
Returns: true when the caller is the owner or an admin, otherwise false.
bulkUpdate(req, res)
Processes multiple stock adjustments in a single request.
function bulkUpdate(req: MoleculeRequest, res: MoleculeResponse): Promise<void>req— The request withadjustmentsarray body.res— The response object.
confirm(req, res)
Confirms a reservation, permanently removing the reserved quantity from total stock.
function confirm(req: MoleculeRequest, res: MoleculeResponse): Promise<void>req— The request withreservationIdparam.res— The response object.
getAlerts(req, res)
Returns all products whose available stock is at or below their low-stock threshold.
Accepts an optional threshold query parameter to override the per-product threshold.
function getAlerts(req: MoleculeRequest, res: MoleculeResponse): Promise<void>req— The request with optionalthresholdquery parameter.res— The response object.
getInventorySession(res)
Reads the structural session off res.locals.
function getInventorySession(res: MoleculeResponse): InventorySession | undefinedres— The response whoselocals.sessionis inspected.
Returns: The session, or undefined when unauthenticated.
getMovements(req, res)
Returns paginated stock movement history for the given product.
function getMovements(req: MoleculeRequest, res: MoleculeResponse): Promise<void>req— The request withproductIdparam and optional pagination query params.res— The response object.
getStock(req, res)
Returns stock information for the given product (and optional variant).
function getStock(req: MoleculeRequest, res: MoleculeResponse): Promise<void>req— The request withproductIdparam and optionalvariantIdquery.res— The response object.
isInventoryAdmin(res)
Resolves whether the current request's session belongs to an actor authorized
to administer inventory (rewrite/bulk-update stock, release/confirm any
reservation). Fail-closed: returns false when there is no authenticated
session, and otherwise only true when the session carries an admin claim —
isAdmin === true, role === 'admin', roles containing 'admin', or
permissions containing 'admin' / 'inventory:manage'.
Use this for in-handler defense-in-depth (it does not depend on the route middleware being preserved by the injector).
function isInventoryAdmin(res: MoleculeResponse): booleanres— The response whoselocals.sessionis inspected.
Returns: true when the session is an authorized inventory admin.
release(req, res)
Releases a stock reservation, returning the reserved quantity to available stock.
function release(req: MoleculeRequest, res: MoleculeResponse): Promise<void>req— The request withreservationIdparam.res— The response object.
requireInventoryAdmin()
Route middleware that gates the admin-only inventory routes (updateStock,
bulkUpdate). Calls next() only for an authenticated admin; otherwise
forwards an error to the framework error handler — Unauthorized when no
session is present, Forbidden when the session is authenticated but not an
admin.
Exposed as a requestHandlerMap key so the injector's route scanner keeps it
(unlike the inert global 'authenticate' string, which is dropped).
function requireInventoryAdmin(): MoleculeRequestHandlerReturns: An Express-compatible middleware function.
reserve(req, res)
Reserves stock for the given product and order.
function reserve(req: MoleculeRequest, res: MoleculeResponse): Promise<void>req— The request withproductIdparam and {@link ReserveStockInput} body.res— The response object.
toLowStockAlert(row)
Converts a database stock row into a typed {@link LowStockAlert}.
function toLowStockAlert(row: StockRow): LowStockAlertrow— The raw database row.
Returns: The low-stock alert.
toReservation(row)
Converts a database reservation row into a typed {@link Reservation}.
function toReservation(row: ReservationRow): Reservationrow— The raw database row.
Returns: The deserialized reservation.
toStockInfo(row)
Converts a database stock row into a typed {@link StockInfo}.
function toStockInfo(row: StockRow): StockInforow— The raw database row.
Returns: The deserialized stock info.
toStockMovement(row)
Converts a database stock movement row into a typed {@link StockMovement}.
function toStockMovement(row: StockMovementRow): StockMovementrow— The raw database row.
Returns: The deserialized stock movement.
updateStock(req, res)
Updates stock for the given product. Creates the stock record if it doesn't exist.
function updateStock(req: MoleculeRequest, res: MoleculeResponse): Promise<void>req— The request withproductIdparam and {@link StockAdjustment} body.res— The response object.
Constants
i18nRegistered
Whether i18n registration has been attempted. Always true; this module is
a placeholder for symmetry with locale-bonded resources.
const i18nRegistered: trueINVENTORY_ADMIN_PERMISSION
Session-claim permission string ('inventory:manage') that, when present in a
session's permissions array, grants inventory administration.
const INVENTORY_ADMIN_PERMISSION: 'inventory:manage'INVENTORY_PERMISSION_ACTION
Permission action describing inventory administration, e.g. for an app's own
@molecule/api-permissions wiring.
const INVENTORY_PERMISSION_ACTION: 'manage'INVENTORY_PERMISSION_RESOURCE
Permission resource describing inventory administration, e.g. for an app's own
@molecule/api-permissions wiring.
const INVENTORY_PERMISSION_RESOURCE: 'inventory'requestHandlerMap
Handler map for the inventory resource routes.
requireInventoryAdmin is the admin authorizer middleware referenced by the
updateStock/bulkUpdate routes. It must live here (as a real handler-map
key) so the mlcl injector's route scanner preserves it — a bare middleware
string that isn't a handler-map key is silently dropped, which is exactly how
the previous bare 'authenticate' gate became inert.
const requestHandlerMap: {
readonly getStock: typeof getStock
readonly updateStock: typeof updateStock
readonly reserve: typeof reserve
readonly release: typeof release
readonly confirm: typeof confirm
readonly getAlerts: typeof getAlerts
readonly getMovements: typeof getMovements
readonly bulkUpdate: typeof bulkUpdate
readonly requireInventoryAdmin: MoleculeRequestHandler
}routes
Inventory resource routes.
All routes require authentication. The destructive admin-side mutations
(updateStock, bulkUpdate) are additionally gated by the
requireInventoryAdmin middleware — a real requestHandlerMap key (see
{@link requireInventoryAdmin}) so the injector preserves it; the previously
declared bare 'authenticate' string was silently dropped by the route
scanner, leaving stock open to rewrite by any authenticated user. Each
admin/reservation handler additionally re-checks authorization internally, so
the gate holds even if a consumer wires the routes without these middlewares.
const routes: readonly [
{
readonly method: 'get'
readonly path: '/inventory/:productId'
readonly handler: 'getStock'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'put'
readonly path: '/inventory/:productId'
readonly handler: 'updateStock'
readonly middlewares: readonly ['requireInventoryAdmin']
},
{
readonly method: 'post'
readonly path: '/inventory/:productId/reserve'
readonly handler: 'reserve'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'delete'
readonly path: '/inventory/reservations/:reservationId'
readonly handler: 'release'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'post'
readonly path: '/inventory/reservations/:reservationId/confirm'
readonly handler: 'confirm'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'get'
readonly path: '/inventory/alerts'
readonly handler: 'getAlerts'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'get'
readonly path: '/inventory/:productId/movements'
readonly handler: 'getMovements'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'post'
readonly path: '/inventory/bulk'
readonly handler: 'bulkUpdate'
readonly middlewares: readonly ['requireInventoryAdmin']
},
]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-resource
Tables: src/__setup__/inventory.sql creates inventory_stock,
inventory_reservations, and inventory_movements. An mlcl-scaffolded API
replays __setup__/*.sql automatically on migrate; anywhere else run it
once — nothing at runtime creates them.
Stock rows are keyed by productId — SHARED app-wide state, not per-user
rows. Writing stock (PUT /inventory/:productId) and
POST /inventory/bulk are role-gated and DENY BY DEFAULT (admin session
claim or an @molecule/api-permissions grant), enforced both as the
requireInventoryAdmin route middleware and inside the handlers
(fail-closed). Out of the box no one can mutate stock — grant the role
first; never "fix" the 403 by removing the gate.
Reservation flow: POST /inventory/:productId/reserve holds quantity →
POST /inventory/reservations/:id/confirm deducts it,
DELETE /inventory/reservations/:id releases the hold. All handlers read
the authenticated user from res.locals.session (mount behind your global
auth middleware; 401 otherwise).
E2E Tests
Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual stock/admin screens, and check every box off one by one. Correctness is the whole point here: a box you can't check is a bug to fix, not a skip.
- [ ] Setting stock (
PUT /inventory/:productId,type:'set') then reloadingGET /inventory/:productIdshows the exacttotal/availableyou set — it persisted, not just optimistic UI.available=total-reserved. - [ ]
type:'add' Nraisestotalby exactly N andtype:'remove' Nlowers it by exactly N — verify the arithmetic on the specificproductId/variantId; variant stock is tracked independently, so adjusting one variant must not move another. - [ ] Stock never goes negative: removing or
setting below the currently reserved quantity is rejected with a visible 409 error (inventory.error.insufficientStock) and the storedtotalis unchanged — never persisted as a negative; reserving more thanavailableis likewise rejected (409insufficientAvailable). - [ ] Low-stock crossing flags the item: when
availablefalls to or belowlowStockThreshold(default 10) it readsisLowStock:trueand appears inGET /inventory/alerts; raising stock back above the threshold clears it. - [ ] Every mutation appends an
inventory_movementsrow shown inGET /inventory/:productId/movementswith the signed delta (+N/-N), type (adjustment/reservation/confirmation), timestamp, andreferenceId(orderId/reservationId); the acting user is recorded on the reservation. The movement log must reconstruct the current total. - [ ] Concurrency: two reservations or removals fired at once that together
exceed
availabledon't double-spend the last unit — exactly one succeeds, and finaltotal/reservedstay consistent (availablenever goes negative). - [ ] AUTHORIZATION — stock mutation is admin-only and denies by default:
PUT /inventory/:productIdandPOST /inventory/bulkreturn 403 for a normal signed-in user (noisAdmin/role:'admin'/roles/permissionsclaim) and 401 when signed out; only an admin session can change stock. A customer cannot mutate the shared catalog stock through any endpoint, and one user cannot release/confirm another user's reservation (403reservationForbidden).
