36-Nodejs Course 2023: Database Master Mind
Hasan Zohdy
Posted on November 4, 2022
Now we've seen how to operate with database CRUD operations, where we used the _id
field to identify the document. But what if we want to use a different field to identify to have an auto incremented id with an integer value? In this article, we will see how to do that.
Auto Incremented Id
So how this works? We'll create a database collection (table), i'll call it MasterMinder
, that collection we'll use as our base collection to store for now two columns, the collection name that we want to auto increment the id and the last id that we used.
How this works
When we create a new record in the database using create
method we've created, we'll add a column internally called id
, what we'll need here is to get the last id of the current Model's collection name from the MasterMind
collection, if that collection was not created yet, we'll create it and set the last id to 1
or to equal the initialValue
that we'll add as a feature later, then we'll increment the last id by 1 and save it in the MasterMind
collection.
Implementation
So let's start by creating a new file called master-mind.ts
inside src/core/database/model
directory, and add the following code:
// src/core/database/model/master-mind.tsimport connection, { Connection } from "./../connection";
export class MasterMind {
/**
* Database Connection
*/
public connection: Connection = connection;
/**
* Get the last id of the collection
*/
public async getLastId(collectionName: string): Promise<number> {
// get the collection query
const masterMind = await this.connection.database.collection("MasterMind");
// find the record of the given collection name
const masterMindData = await masterMind.findOne({ collectionName });
// if the record exists, then return it
if (masterMindData) {
return masterMindData.id;
}
// if the record does not exist, then return zero
return 0;
}
/**
* Increment and return the last updated id for the given collection
* If the collection does not exist, then create it and return the initial value
*/
public async generateNextId(
collectionName: string,
incrementBy = 1,
initialValue = 1,
): Promise<number> {
// get the collection query
const masterMind = await this.connection.database.collection("MasterMind");
// find the record of the given collection name
const masterMindData = await masterMind.findOne({ collectionName });
// if the record exists, then increment the id by the given incrementBy value
if (masterMindData) {
const lastId = masterMindData.id + incrementBy;
// update the record with the new id
await masterMind.updateOne({ collectionName }, { $set: { id: lastId } });
// return the new id
return lastId;
}
// if the record does not exist, then create it with the given initial value
await masterMind.insertOne({ collectionName, id: initialValue });
// return the initial value
return initialValue;
}
}
const masterMind = new MasterMind();
export default masterMind;
So we have created a class that will operate with two operations, one just to getting the last id, which will return 0
if the collection does not exist, this case is not usually happens but it's good to have it, and the second operation is to generate the next id, which will return the initial value if the collection does not exist, and will increment the last id by the given incrementBy
value.
Now let's go to our model and apply the changes.
// src/core/database/model/model.ts
import masterMind from './master-mind';
export default abstract class Model {
// ...
/**
* Define the initial value for the auto incremented id
*/
public static initialId = 1;
/**
* define the increment by value for the auto incremented id
*/
public static incrementBy = 1;
/**
* Generate and return next id of current collection
*/
public static async generateNextId() {
return await masterMind.generateNextId(
this.collectionName,
this.incrementBy,
this.initialId,
);
}
/**
* Create a new record in the database for the current model (child class of this one)
* and return a new instance of it with the created data and the new generated id
*/
public static async create<T>(
this: BaseModel<T>,
data: Record<string, any>,
): Promise<T> {
// 1- get the query of the collection
const query = this.query();
const modelData = { ...data };
// check if the data does not have an id column
if (!modelData.id) {
modelData.id = await this.generateNextId();
}
// perform the insertion
const result = await query.insertOne(data);
// associate the mongodb _id as well
modelData._id = result.insertedId;
return this.self(modelData);
}
}
What we did here is we added two static variables, one for the initial value and the other for the increment by value, and we used them in the create
method, but we checked first if the data that is coming does not have an id column, as in some rare situations the id is being generated outside the model, and we don't want to override it, so if there is no id, then we'll generate one.
Now let's give it a try.
// src/app/users/routes.ts
import User from "./models/user";
setTimeout(async () => {
const user = await User.create({
name: "hasan",
});
console.log(user.data);
}, 4000);
Now you will see something like this:
If you just saved the file again, it will generate a new record with a new id, and if you check the MasterMind
collection, you will see that the id has been incremented.
Updating methods to use the new id
As you can tell, we were using MongoDB's _id
as the id of the record, but now we are using our own id, so we need to update the methods to use the new id.
You know, actually we can make it more advanced, we can allow the user to define what is the primary key that we use as id
in the finding methods, but for now, we'll just use the new id.
// src/core/database/model/model.ts
export default abstract class Model {
// ....
/**
* Primary column
*/
public static primaryColumn = "id";
}
We defined a new static variable called primaryColumn
, and we set it to id
by default, so now we can use it in any method that deals with the id, and also we'll change the type of the id to accept both string
and number
.
// src/core/database/model/model.ts
// update method
/**
* Update model by the given id
*/
public static async update<T>(
this: BaseModel<T>,
// 👇🏻 replace the type of string to be string | number or ObjectId
id: string | ObjectId | number,
data: Record<string, any>,
): Promise<T> {
// get the query of the current collection
const query = this.query();
// execute the update operation
// 👇🏻 replace the _id column with the primary column
const filter = {
[this.primaryColumn]: id,
};
const result = await query.findOneAndUpdate(
filter,
{
$set: data,
},
{
returnDocument: "after",
},
);
return this.self(result.value as Record<string, any>);
}
// replace method
/**
* Replace the entire document for the given document id with the given new data
*/
public static async replace<T>(
this: BaseModel<T>,
// 👇🏻 replace the type of string to be string | number or ObjectId
id: string | ObjectId | number,
data: Record<string, any>,
): Promise<T> {
const query = this.query();
// 👇🏻 replace the _id column with the primary column
const filter = {
[this.primaryColumn]: id,
};
const result = await query.findOneAndReplace(filter, data, {
returnDocument: "after",
});
return this.self(result.value as Record<string, any>);
}
// find method
/**
* Find document by id
*/
// 👇🏻 replace the type of string to be string | number or ObjectId
public static async find<T>(this: BaseModel<T>, id: string | ObjectId | number) {
// 👇🏻 replace the _id column with the primary column
return this.findBy(this.primaryColumn, id);
}
Now regarding the delete method, the type we were using using is ObjectId or an object, so we'll add also the number type.
In the check part, we'll check if the given value is ObjectId
string
or number
, then we'll perform a filter by multiple columns, otherwise, we'll perform a filter by the primary column.
// src/core/database/model/model.ts
/**
* Delete single document if the given filter is an ObjectId of mongodb
* Otherwise, delete multiple documents based on the given filter object
*/
public static async delete<T>(
this: BaseModel<T>,
filter: string | ObjectId | number | Record<string, any>,
): Promise<number> {
const query = this.query();
if (
filter instanceof ObjectId ||
typeof filter === "number" ||
typeof filter === "string"
) {
const result = await query.deleteOne({
[this.primaryColumn]: filter,
});
return result.deletedCount;
}
const result = await query.deleteMany(filter);
return result.deletedCount;
}
Our last remaining question
What if the user's id now is 2
then the next one will be 3
, what happens if the user with id 3
is deleted, what will be the next id that will be generated?, of course it will be 4
as the id will not stop at the deleted ones but will go from the last saved value in the MasterMind
collection.
🎨 Conclusion
In this article, we learned how to generate an auto-increment id for our MongoDB documents, and we also learned how to use it in our models, and we also learned how to update the methods to use the new id.
In our next article, it will be another refactoring article, we'll refactor the Model
class to be more generic and more reusable, and we'll also add some new features to it.
🚀 Project Repository
You can find the latest updates of this project on Github
😍 Join our community
Join our community on Discord to get help and support (Node Js 2023 Channel).
🎞️ Video Course (Arabic Voice)
If you want to learn this course in video format, you can find it on Youtube, the course is in Arabic language.
💰 Bonus Content 💰
You may have a look at these articles, it will definitely boost your knowledge and productivity.
General Topics
- Event Driven Architecture: A Practical Guide in Javascript
- Best Practices For Case Styles: Camel, Pascal, Snake, and Kebab Case In Node And Javascript
- After 6 years of practicing MongoDB, Here are my thoughts on MongoDB vs MySQL
Packages & Libraries
- Collections: Your ultimate Javascript Arrays Manager
- Supportive Is: an elegant utility to check types of values in JavaScript
- Localization: An agnostic i18n package to manage localization in your project
React Js Packages
Courses (Articles)
Posted on November 4, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.