MongoDB Kit Lending Project
Combine validated input, a request identity, a guarded stock decrement and a loan event in one bounded workflow. Refused attempts must preserve stock and request state.
How it works
The project uses a new loan_requests collection and a function for sequential requests from one fictional member M1. It accepts only known scalar ID formats and a small positive integer quantity. Within a session transaction it checks the request, reserves stock and creates the loan together. Expected business refusals are returned explicitly; unexpected database failures are rethrown. Concurrent duplicate requests and production retries require an additional conflict/retry policy, not a claim of exactly-once service behavior.
Run in a separate MongoDB lab
Start a disposable lab automatically: in a separate empty learning folder, save package.json, run-lab.mjs and this lesson’s source with its displayed filename. Run npm install --ignore-scripts, then node run-lab.mjs lending-project.mongosh.js. mongosh lessons also need mongosh installed on PATH. The launcher downloads the pinned MongoDB server on first use, starts one authenticated loopback replica-set member, supplies private child-process credentials, runs the lesson and stops that server. Its cache, generated data and diagnostic files stay in the private .bhylu-mongodb-lab folder; it installs no service. Review downloaded code before executing it.
The following manual connection route is an alternative for readers who already have a suitable separate lab instance.
The reviewed baseline is MongoDB 8.0.12, mongosh 2.14.0, Node.js 22.15.1 and driver 6.17.0. These are tested versions, not a newest-release claim. Use a private authenticated learning server on 127.0.0.1 and an explicit non-default port, not your normal database or an Atlas project. These scripts intentionally refuse port 27017 and remote hosts. Bhylu has no MongoDB browser runner; a plain JavaScript playground cannot provide this database.
Supply your own lab connection through the child process environment variable BHYLU_MONGO_URI and set BHYLU_MONGO_LAB=YES. Keep credentials private; do not save them in source or public commands. The lab role needs permission to create and drop its generated databases and collections. No PHP, Apache or normal XAMPP service change is needed.
Save the complete download as lending-project.mongosh.js and run mongosh --nodb --norc --quiet --file lending-project.mongosh.js in a separate child process. It reads its private connection from the environment. The file uses actual mongosh APIs; it is not a Node.js module.
Every run generates a new bhylu_mongo_ database name, refuses an existing collection set, writes its own ownership marker, seeds the complete fictional dataset and finally removes only that owned database after checking the marker. Do not change the generated name to a valuable existing database. Closing the child process releases its client connections.
Replica-set requirement: this lesson requires a supporting local replica set. The review uses one authenticated member for transaction/change-stream capabilities; it does not test replication redundancy, elections or failover.
Example
// Bhylu MongoDB 8.0.12 lab: Kit Lending Project
// Run only against your separate authenticated loopback learning instance.
async function main() {
if (process.env.BHYLU_MONGO_LAB !== "YES")
throw new Error("Set BHYLU_MONGO_LAB=YES for a private lab");
const raw = process.env.BHYLU_MONGO_URI;
if (!raw)
throw new Error("Set BHYLU_MONGO_URI privately; do not commit credentials");
const address = new URL(raw);
if (
address.protocol !== "mongodb:" ||
address.hostname !== "127.0.0.1" ||
!address.port ||
address.port === "27017"
)
throw new Error("Use a private loopback MongoDB on a non-default port");
const marker = new ObjectId().toString(),
dbName = "bhylu_mongo_" + marker;
const connection = new Mongo(raw);
const db = connection.getDB(dbName);
let owned = false;
const emit = (value) => print(JSON.stringify(value));
try {
if ((await db.getCollectionNames()).length)
throw new Error("Refusing an existing database");
await db.createCollection("_bhylu_owner");
await db.getCollection("_bhylu_owner").insertOne({ _id: marker });
owned = true;
await db.getCollection("kits").insertMany([
{
_id: "K1",
label: "Birdwatch pack",
category: "observation",
available: 3,
condition: "ready",
location: { room: "North", shelf: 2 },
tags: ["outdoor", "compact"],
notes: null,
checks: [
{ kind: "lens", passed: true },
{ kind: "strap", passed: false },
],
},
{
_id: "K2",
label: "Stream sampler",
category: "sampling",
available: 1,
condition: "ready",
location: { room: "South", shelf: 1 },
tags: ["outdoor", "washable"],
checks: [
{ kind: "lens", passed: false },
{ kind: "strap", passed: true },
],
},
{
_id: "K3",
label: "Soil survey set",
category: "sampling",
available: 0,
condition: "repair",
location: { room: "North", shelf: 3 },
tags: ["washable"],
notes: "Replace pouch",
checks: [],
},
{
_id: "K4",
label: "Weather logger",
category: "observation",
available: 2,
condition: "ready",
location: { room: "South", shelf: 2 },
tags: [],
notes: null,
checks: [],
},
]);
await db.getCollection("members").insertMany([
{ _id: "M1", name: "Asha", area: "North" },
{ _id: "M2", name: "Dev", area: "South" },
{ _id: "M3", name: "Noor", area: "West" },
]);
await db.getCollection("loans").insertMany([
{
_id: "L1",
kitId: "K1",
memberId: "M1",
quantity: 1,
status: "returned",
openedAt: new Date("2026-01-10T09:00:00Z"),
},
{
_id: "L2",
kitId: "K1",
memberId: "M2",
quantity: 2,
status: "open",
openedAt: new Date("2026-01-11T09:00:00Z"),
},
{
_id: "L3",
kitId: "K2",
memberId: "M1",
quantity: 1,
status: "open",
openedAt: new Date("2026-01-11T10:00:00Z"),
},
{
_id: "L4",
kitId: "K3",
memberId: "M2",
quantity: 1,
status: "returned",
openedAt: new Date("2026-01-10T11:00:00Z"),
},
{
_id: "L5",
kitId: "K4",
memberId: null,
quantity: 1,
status: "planned",
openedAt: new Date("2026-01-12T09:00:00Z"),
},
]);
const kits = db.getCollection("kits"),
loans = db.getCollection("loans"),
members = db.getCollection("members");
// Lesson commands begin here.
await db.createCollection("loan_requests");
class RequestRefusal extends Error {}
async function borrowOnce(requestId, kitId, quantity) {
if (typeof requestId !== "string" || !/^R[0-9]{1,4}$/.test(requestId))
return { kind: "rejected", reason: "Invalid request" };
if (typeof kitId !== "string" || !/^K[1-9][0-9]*$/.test(kitId))
return { kind: "rejected", reason: "Invalid kit" };
if (!Number.isSafeInteger(quantity) || quantity < 1 || quantity > 3)
return { kind: "rejected", reason: "Invalid quantity" };
const session = connection.startSession();
try {
session.startTransaction({
readConcern: { level: "snapshot" },
writeConcern: { w: "majority" },
});
const tx = session.getDatabase(dbName),
requests = tx.getCollection("loan_requests");
if (await requests.findOne({ _id: requestId }))
throw new RequestRefusal("Duplicate request");
await requests.insertOne({ _id: requestId, kitId, quantity });
const result = await tx
.getCollection("kits")
.updateOne(
{ _id: kitId, condition: "ready", available: { $gte: quantity } },
{ $inc: { available: -quantity } },
);
if (result.modifiedCount !== 1) throw new RequestRefusal("Unavailable");
await tx.getCollection("loans").insertOne({
_id: "request:" + requestId,
requestId,
kitId,
memberId: "M1",
quantity,
status: "open",
});
await session.commitTransaction();
return { kind: "accepted", requestId };
} catch (error) {
await session.abortTransaction();
if (error instanceof RequestRefusal)
return { kind: "rejected", reason: error.message };
throw error;
} finally {
await session.endSession();
}
}
const accepted = await borrowOnce("R1", "K1", 2);
const duplicate = await borrowOnce("R1", "K1", 2);
const unavailable = await borrowOnce("R2", "K1", 2);
const invalid = await borrowOnce("R3", "K1", -1);
emit({
accepted: accepted.kind,
duplicate: duplicate.reason,
unavailable: unavailable.reason,
invalid: invalid.reason,
available: (await kits.findOne({ _id: "K1" })).available,
requests: await db.getCollection("loan_requests").countDocuments({}),
});
} finally {
if (owned) {
const owner = await db
.getCollection("_bhylu_owner")
.findOne({ _id: marker });
if (!owner || !/^bhylu_mongo_[a-f0-9]{24}$/.test(dbName))
throw new Error("Refusing cleanup without matching ownership marker");
await db.dropDatabase();
}
// The noninteractive mongosh process closes its connection when it exits.
}
}
main().catch((error) => {
console.error(error.message);
exit(1);
});
Focus on the lesson operations
This excerpt omits connection guards, seed and cleanup. Use the complete download to run it.
await db.createCollection('loan_requests');
class RequestRefusal extends Error {}
async function borrowOnce(requestId,kitId,quantity){
if(typeof requestId!=='string'||!/^R[0-9]{1,4}$/.test(requestId))return {kind:'rejected',reason:'Invalid request'};
if(typeof kitId!=='string'||!/^K[1-9][0-9]*$/.test(kitId))return {kind:'rejected',reason:'Invalid kit'};
if(!Number.isSafeInteger(quantity)||quantity<1||quantity>3)return {kind:'rejected',reason:'Invalid quantity'};
const session=connection.startSession();
try{
session.startTransaction({readConcern:{level:'snapshot'},writeConcern:{w:'majority'}});
const tx=session.getDatabase(dbName),requests=tx.getCollection('loan_requests');
if(await requests.findOne({_id:requestId}))throw new RequestRefusal('Duplicate request');
await requests.insertOne({_id:requestId,kitId,quantity});
const result=await tx.getCollection('kits').updateOne({_id:kitId,condition:'ready',available:{$gte:quantity}},{$inc:{available:-quantity}});
if(result.modifiedCount!==1)throw new RequestRefusal('Unavailable');
await tx.getCollection('loans').insertOne({_id:'request:'+requestId,requestId,kitId,memberId:'M1',quantity,status:'open'});
await session.commitTransaction();return {kind:'accepted',requestId};
}catch(error){
await session.abortTransaction();
if(error instanceof RequestRefusal)return {kind:'rejected',reason:error.message};
throw error;
}finally{await session.endSession();}
}
const accepted=await borrowOnce('R1','K1',2);
const duplicate=await borrowOnce('R1','K1',2);
const unavailable=await borrowOnce('R2','K1',2);
const invalid=await borrowOnce('R3','K1',-1);
emit({accepted:accepted.kind,duplicate:duplicate.reason,unavailable:unavailable.reason,invalid:invalid.reason,
available:(await kits.findOne({_id:'K1'})).available,requests:await db.getCollection('loan_requests').countDocuments({})});
Expected result
{"accepted":"accepted","duplicate":"Duplicate request","unavailable":"Unavailable","invalid":"Invalid quantity","available":1,"requests":1}The script emits JSON so these small results are unambiguous. Interactive shell formatting may look different. Object IDs and owned database names are not hard-coded expected values.
One accepted request leaves one free unit and one request record. Duplicate, insufficient and invalid attempts leave that state intact. The review runs additional operations against this same function and an independent expected-state model. This lab has no user accounts, actual lending authorization, durable external receipt delivery or public deployment.