Classify and Quarantine Equipment Records

MongoDBBeginner
Practice Now

Introduction

FIELDNOTE rents small equipment kits. Its inherited MongoDB catalog contains useful stock counts alongside old strings and uncertain values. A known count of zero means a kit is unavailable; a missing count means the shop does not know. Turning both into zero would hide a data problem. Your job is to separate these cases and preserve unreliable records for a later supplier review.

You will save a reusable classification policy and a quarantine script that keeps the complete original document before removing it from the active collection. You need basic terminal navigation and familiarity with MongoDB documents, nested fields, find and ordinary updates. The JavaScript constructs, schema rules and BSON details used here are explained before use. MongoDB, its shell and synthetic catalog data are prepared. Use Terminal throughout. The duration is an author estimate including reading and ordinary mistakes.

Author policy.js and quarantine.js under /home/labex/project/catalog-desk. Preserve every original ID, complete original field and type, unrelated extension, collection identity and supplied tool. Keep valid and repairable records unchanged in this lab. Do not drop/rebuild collections or overwrite an existing quarantine original. Work on this course catalog only. This is a controlled maintenance window with no concurrent writers; the copy and deletion of a record are separate operations, not a transaction covering the whole catalog.

Read and Classify the Inherited Catalog

In this step, you will read an ordinary usable record, distinguish stored count types and save an explicit classification policy. Begin in Terminal. cd changes directory; this absolute path also recovers from entering the wrong directory:

cd /home/labex/project/catalog-desk

./catalog is the supplied connection runner in this directory; ./ selects the local executable. Its show command reads actual stored documents. Give it one document ID:

./catalog show KIT-101

Look for name: "Tripod", stock.rack: "A" and stock.available containing "$numberInt": "12". The output uses canonical Extended JSON: a text representation with explicit BSON type tags. BSON is MongoDB's stored document format. $numberInt identifies a 32-bit integer; $numberLong identifies a 64-bit integer. These tags in displayed output describe stored numbers, not ordinary nested stock fields you should add.

The shop accepts either integer type for an available count between 0 and 50 inclusive. The original stock.rack and supplier fields are useful information and must survive. _id identifies the record. Two records named Tripod are still different records.

Inspect the server's actual type for every count:

./catalog types

This reader uses MongoDB's aggregation $type expression on stock.available. Its actualType column distinguishes int, long, string, null, missing, array and double. double is a floating-point BSON type; even an integral-looking Double value of 12 is outside this shop's integer-only contract. missing means the field does not exist; null means it exists with an explicit null value. An array such as [12] is not one scalar count.

Read the complete catalog to connect the types with the original records:

./catalog show

Use this decision table before writing a query:

Stored count Decision in this lab Reason
Int32 12 or Long 0 Keep usable Accepted scalar integer, including known zero
String "12" or "0" Keep for approved repair Canonical decimal representation in the agreed range
"12 kits", "012" or "51" Quarantine Unknown meaning, unapproved representation or outside range
Null, missing, array or Double 12 Quarantine Does not satisfy the agreed scalar integer contract

A repairable string must contain only the canonical ASCII digits for a number from 0 through 50: no sign, spaces, leading zero, decimal point or unit suffix. This is an explicit agreement for these inherited data, not a promise that every numeric-looking string is safe to convert. Quarantine means retain a complete original for review; it does not mean invent a replacement count.

All subsequent commands stay in this Terminal and directory. If a filename is mistyped, ls lists the real local names. Correct the spelling and rerun the command; a file error is not a database refusal and does not require resetting the environment.

Save the Classification Policy

A MongoDB selector is a document describing which records match. $jsonSchema checks a document's shape and BSON types. required names fields that must exist; it does not by itself make their values non-null. properties describes each field, bsonType constrains its BSON type, and minimum/maximum include their boundary values. The nested stock rule separately requires available. Fields not mentioned remain allowed, so rack, supplier and later extensions can survive.

Use JSON Schema's scalar type rule here. An ordinary query predicate such as {"stock.available": {$type: "int"}} can match an array containing an integer. That would not establish the scalar count needed by this catalog.

You will write JavaScript for mongosh, the MongoDB shell. const gives a value a name; = assigns its initial value, braces form an object, square brackets form an array, and commas separate entries. Quoted text is a string; 0 and 50 without quotes are numbers. A semicolon ends a statement. schema describes an entire accepted record and valid wraps it as a MongoDB selector.

The approved-string policy has the same shape, with only the count rule changed. JSON.stringify turns this plain policy object into JSON text and JSON.parse reads a separate copy; changing the copy then leaves the original policy alone. This copying method is for the plain policy, not for preserving stored BSON records. stringSchema.properties.stock.properties.available reaches the nested rule being replaced.

The pattern is a regular expression for allowed text. ^ anchors the start and \z means the absolute end of the string; | separates alternatives, [1-9] allows one digit from 1 to 9, and [1-4][0-9] allows two digits from 10 through 49. Together with 0 and 50, these alternatives cover exactly the approved range. JavaScript strings use a backslash as an escape, so the source writes \\z to pass \z to MongoDB. This absolute-end anchor also rejects a final newline; an ordinary $ end anchor can allow a match before that newline. $nor: [valid, repairable] selects records matching neither selector.

Create the policy file with a here-document. cat > policy.js writes the following text to the file and replaces its previous contents. The quoted <<'POLICY' delimiter keeps dollar signs and other JavaScript text literal in the shell. Finish with POLICY alone on a line. Copy the complete block:

cat > policy.js <<'POLICY'
const schema = {
  bsonType: "object",
  required: ["_id", "name", "stock"],
  properties: {
    _id: { bsonType: "string", minLength: 1 },
    name: { bsonType: "string", minLength: 1 },
    stock: {
      bsonType: "object",
      required: ["available"],
      properties: {
        available: { bsonType: ["int", "long"], minimum: 0, maximum: 50 }
      }
    }
  }
};
const valid = { $jsonSchema: schema };
const stringSchema = JSON.parse(JSON.stringify(schema));
stringSchema.properties.stock.properties.available = {
  bsonType: "string",
  pattern: "^(0|[1-9]|[1-4][0-9]|50)\\z"
};
const repairable = { $jsonSchema: stringSchema };
const bad = { $nor: [valid, repairable] };
POLICY

The supplied classifier evaluates this saved file unchanged using a read-only database identity. It then uses your valid, repairable and bad selectors in real MongoDB queries:

./catalog classify policy.js

Inspect the actual document groups, not just their sizes. valid should contain KIT-101 and KIT-102, including the original Long zero. repairable should contain KIT-103 and KIT-104, with their original strings unchanged. outside should contain KIT-105 through KIT-111, including the original null, missing field, array and Double. The complete supplier-review extension on KIT-105 must still be visible.

If a JavaScript syntax error appears, the file has not been accepted as a working policy. Rerun the complete here-document to replace a damaged file, then rerun the classifier. A syntax error names a file/expression problem; an unavailable database connection is a different error and does not mean the classification returned no matches. No data has been repaired or removed by this classifier.

Preserve Originals Before Removing Uncertain Records

In this step, you will save a quarantine script, observe a real interruption after its first copy, and rerun the unchanged script to finish. Begin in Terminal, in /home/labex/project/catalog-desk.

Each quarantine document will have the original _id, an original containing the complete stored record, and reason: "outside-approved-catalog-contract". The reason describes why supplier review is needed; it does not assert that the stock is zero. An original count may be missing, null, an array or a different type, so copying only the displayed value or just the name would lose evidence.

There is another boundary: the shell can convert stored BSON numeric values into ordinary JavaScript numbers when reading. Saving an integral-looking JavaScript number can then produce Int32 even when the original was Double. For every original copy and comparison, use the third find/findOne argument {promoteValues: false, bsonRegExp: true}. It keeps BSON numeric wrappers and regular-expression values rather than converting them to JavaScript equivalents. The second argument {} is an empty projection, retaining every field. EJSON.stringify(value, {relaxed: false}) produces canonical Extended JSON from these preserved types, so a Double 12 and an Int32 12 remain different during comparison.

The script's loop reads the outside group once and handles each record separately. sort({_id: 1}) gives readable ascending IDs and toArray() collects the results. for (const doc of ...) repeats the enclosed statements for each result. The upsert operation finds a quarantine record by its original ID; upsert: true creates it if absent. $setOnInsert writes the original and reason only when creating that record, leaving an existing copy untouched. findOne reads back the stored copy.

if tests a condition. !== means not equal and || means either condition is true. A different original or reason must throw Error(...), which stops the script with a nonzero exit before deleting that source record. deleteOne removes only the verified source ID. Previous records may already have completed; these are individual operations, not an all-or-nothing batch. With no concurrent writers, a successful full comparison before each deletion provides the retry path used here.

Write the script. The QUARANTINE here-document uses the same literal-writing syntax as before:

cat > quarantine.js <<'QUARANTINE'
const typed = { promoteValues: false, bsonRegExp: true };
for (const doc of db.equipment.find(bad, {}, typed).sort({_id: 1}).toArray()) {
  db.quarantine.updateOne(
    {_id: doc._id},
    {$setOnInsert: {original: doc, reason: "outside-approved-catalog-contract"}},
    {upsert: true}
  );
  const saved = db.quarantine.findOne({_id: doc._id}, {}, typed);
  if (EJSON.stringify(saved.original, {relaxed: false}) !== EJSON.stringify(doc, {relaxed: false}) ||
      saved.reason !== "outside-approved-catalog-contract") {
    throw Error("Existing quarantine copy differs; keep source and investigate");
  }
  db.equipment.deleteOne({_id: doc._id});
}
QUARANTINE

First use the supplied copy-only connection. This is a real database identity allowed to read and copy but denied deletion. The runner evaluates policy.js and then quarantine.js unchanged in one shell invocation. Expect a native authorization error and a nonzero exit when the script first tries to delete:

./catalog copy-only policy.js quarantine.js

The message includes not authorized and a delete command on equipment. The copy succeeded before that refusal. Read both real records:

./catalog archive KIT-105
./catalog show KIT-105

KIT-105 now exists in both places. The quarantine original must include its stock text and the full extension, including its numeric, date and ObjectId type tags. The active source is still intact. A failed command therefore does not imply that all earlier writes were undone.

Rerun the exact saved files using the ordinary course writer connection:

./catalog run policy.js quarantine.js

No output is required from a successful script. The first existing copy is verified and retained; its corresponding source is removed. The remaining uncertain records are copied, verified and removed in turn. Read the actual results:

./catalog show
./catalog archive

The active catalog should contain only KIT-101 through KIT-104, unchanged, including the two approved strings awaiting repair. The quarantine should contain KIT-105 through KIT-111 with complete original documents and the agreed reason. In particular KIT-109's original Double still displays $numberDouble, and KIT-107's original still has no available field.

Run the migration again:

./catalog run policy.js quarantine.js
./catalog archive

There are no outside records left to move, so the saved originals remain the same. This is an idempotent replay: repeating a completed operation does not change its business result. An interruption after a copy can also be resumed because the archive key prevents a duplicate copy and the comparison refuses an inconsistent existing copy. If that conflict error appears, keep the source and previous archive for investigation; do not “fix” it by overwriting the archive or deleting the source. The script does not establish safety against concurrent edits between its read and delete, nor can it roll back the entire batch.

Summary

You separated usable integer counts, explicitly approved strings and uncertain records with a reusable MongoDB policy. You then preserved complete typed originals before deleting their active sources, observed a real partial failure and resumed the same saved script. The catalog now keeps usable and repairable records while the quarantine retains evidence for supplier review. Conversion and future-write validation are the next independent unit.