A High Performance, IDE Friendly ODM for MongoDB
Iridium is designed to offer a high performance, easy to use and above all, editor friendly ODM for MongoDB on Node.js. Rather than adopting the "re-implement everything" approach often favoured by ODMs like Mongoose and friends, requiring you to learn an entirely new API and locking you into a specific coding style, Iridium tries to offer an incredibly lightweight implementation which makes your life easier where it counts and gets out of your way when you want to do anything more complex.
It also means that, if you're familiar with the MongoDB CLI you should find working with Iridium very natural, with all database methods returning promises for their results and sensible, type annotated results being provided if you wish to make use of them.
Iridium makes use of the latest set of @types
TypeScript definitions files, allowing you to install everything using just a
simple npm install
.
npm install iridium --save
Iridium is built on top of a number of very modern technologies, including TypeScript 2.8, JavaScript ES6 and the latest MongoDB Node.js Driver (version 3.0). You'll need to be running Node.js 6.x run version 8 of Iridium.
For starters, you will need to be running MongoDB 2.6 or later in order to use Iridium - however we recommend you use MongoDB 3.1 due to the various performance improvements they've made. If you're working with TypeScript, you will also need to use the 2.8 compiler or risk having the Iridium type definitions break your project.
Rather than opt of the usual "Look how quickly you can do something" approach, we thought it might be useful to see an example which covers most of the stuff you'd need to do in Iridium. We're going to put together a small database example which takes advantage of nested objects, transforms, renames and some other nice features offered by Iridium.
If you're looking for Iridium's API documentation, then you can find that here.
Our example is a simple database which stores the list of cars that individual drivers own and describes those cars
with a make
, model
and colour
. For the sake of showing off some of Iridium's more complex features, we're being
overly verbose in how we implement some components, however you'll often find that the simpler examples (like Address
)
work perfectly for most use cases.
Colour
objectThe colour
object is used in our DB to represent the colour of a car.
While we could use a simple interface and schema, we have opted to include
a custom class as well so that we can provide application-level methods to
represent it as a hex string (for example).
In many cases you won't have need for this and a simple schema+interface
will suffice. Take a look at the Address
object for an example of how that
can be done.
/**
* Schemas are used to provide automatic data validation before you
* save stuff to the database. We are using a custom "ubyte" validator
* which we define on our model later.
*/
const ColourSchema = {
r: "ubyte",
g: "ubyte",
b: "ubyte"
}
/**
* We also define a document interface which describes how the
* colour is stored in the database.
*/
interface ColourDoc {
r: number;
g: number;
b: number;
}
/**
* The Colour class is then used within our application to describe
* a colour. It provides useful utility functions that our app will
* utilize to perform its duties.
*/
class Colour {
constructor(doc: ColourDoc) {
this.r = doc.r
this.g = doc.g
this.b = doc.b
}
r: number;
g: number;
b: number;
/**
* The toDB() method is used to convert this class back into a DB document
*/
toDB(): ColourDoc {
return {
r: this.r,
g: this.g,
b: this.b
}
}
toString() {
return `#${this.toHex(this.r)}${this.toHex(this.g)}${this.toHex(this.b)}`
}
/**
* Who needs left-pad?
*/
private toHex(num: number, padding = 2) {
let hex = num.toString(16)
while (hex.length < 2)
hex = `0${hex}`
return hex
}
}
Car
objectOur car
object shows how you can nest DB objects with the toDB()
method
and their constructors. It also shows how you can implement helpful methods
on these classes to make writing code with them easier.
const CarSchema = {
make: String,
model: String,
colour: ColourSchema
}
interface CarDoc {
make: string
model: string
colour: ColourDoc
}
class Car {
constructor(doc: CarDoc) {
this.make = doc.make
this.model = doc.model
this.colour = new Colour(doc.colour)
}
make: string;
model: string;
colour: Colour;
repaint(r: number, g: number, b: number) {
this.colour = new Colour({ r, g, b })
}
toDB(): CarDoc() {
return {
make: this.make,
model: this.model,
colour: this.colour.toDB()
}
}
}
Address
objectIn this case, we don't need anything complex to represent an address or to interact with it, so we're just going to use a basic schema and interface to describe it - no need for fancy classes or transforms.
const AddressSchema = {
// Address lines, at least one and no more than 3
lines: [String, 1, 3],
country: String
}
interface AddressDoc {
lines: string[];
country: string;
}
Driver
modelHere is where we start interacting with Iridium. We're defining a model's Instance type
which is the class that Iridium will use to represent a DB document. Unlike many other
ORMs/ODMs, Iridium allows you to implement your own instance type if you so desire and
simply provides a useful base class for your average use case. You can find out more
about the methods that Iridium.Instance
provides by reading the API Documentation.
In this example we are leveraging that default base class and using Iridium's various
@decorators
to configure things like our indexes, schema validation rules, transforms
and renames. You can also bypass these decorators and use static fields on the instance
type itself if you prefer, however the decorators are usually a nicer way to manage things.
import * as Iridium from 'iridium';
import {ObjectId} from "mongodb";
// The DriverDocument describes the format of our database
// document.
interface DriverDocument {
_id?: ObjectId;
name: string;
address: AddressDoc;
// We want to let people create a driver without specifying
// their cars, so we make this optional.
cars?: CarDoc[];
}
// Finding a driver by name turns out to be a really common use case,
// so we've added an index on that field to reduce the number of full
// collection scans performed by our app.
@Iridium.Index({ name: 1 })
// Let's also add an index on the cars that someone owns so that we can
// quickly find out who drives that black Audi...
@Iridium.Index({ "cars.model": 1, "cars.make": 1 }, { background: true })
// Driver objects should be stored in the `drivers` collection in our DB
@Iridium.Collection('drivers')
// We want to add support for our "ubyte" custom datatype to the validation
// engine, so we'll do that here.
@Iridium.Validate("ubyte", (value) => {
return this.assert(
typeof value === "number"
&& value >= 0
&& value < 255
&& Math.floor(value) === value,
"Should be an integer between 0 and 255"
)
})
class Driver extends Instance<DriverDocument, Driver> {
// We'd prefer to work with a string ObjectID, so ask Iridium to take
// care of converting it for us.
@Iridium.ObjectID
_id: string;
// We'd like to force drivers to have very specific types of names,
// in this case, only westernized names like J.P. Pietersen or Bob McAffee.
@Iridium.Property(/^[A-Z][\w\-\.]*(\s[A-Z][\w\-\.])+/)
// Our DB originally called this full_name, so we're renaming it
// here for backwards compatibility, but we really prefer working
// with `name` in the app.
@Iridium.Rename("full_name")
name: string;
// This is a really simple nested document which doesn't use transforms
// or classes.
@Iridium.Property(AddressSchema)
address: AddressDoc;
// We have a list of cars that each driver owns. The schema is an array of
// CarSchema objects, which we defined above.
@Property([CarSchema])
// We also want Iridium to automatically transform our DB's CarDoc objects into
// our much more useful Car class, but also be smart about putting the values back
// into the DB in the right format.
@Iridium.TransformClassList(Car)
cars: Car[];
// When we first create a Driver document we would like to make sure that cars
// is defined, but empty, so we use this onCreating hook to achieve that.
static onCreating(doc: HouseDocument) {
doc.cars = doc.cars || [];
}
// We'd like to quickly be able to add a car to a driver without needing to
// manually instantiate the object, so we've provided this nice helper method.
addCar(make: string, model: string, colour: Colour) {
this.cars.push(new Car({
make: make,
model: model,
colour: colour.toDB()
}));
}
// We also would like to be able to access a driver's `numberOfCars` by a nice
// alias, so we've added a getter for that.
get numberOfCars() {
return this.cars.length;
}
}
Core
implementationOnce we've defined all of our model types, it's time to define our database connection
class. To do so, simply extend Iridium's Core
class and register your DB models there.
This pattern is, as with every other aspect of Iridium, optional and if you prefer you can
simply instantiate a new Iridium.Core
and manage your models separately. This might make more sense for applications which use
Dependency Injection and are interested in de-coupling their models.
import * as Iridium from 'iridium';
class MyDatabase extends Core {
Drivers = new Model<DriverDocument, Driver>(this, Driver);
// We've got a few housekeeping tasks we'd like to take
// care of automatically when we start our database connection,
// so we use the `onConnected` hook to run them.
protected onConnected() {
return super.onConnected().then(() => Promise.all([
// One of those tasks is creating all our indexes
// for the Drivers collection.
this.Drivers.ensureIndexes(),
this.seed()
]))
}
// This is our own custom method which we call from the onConnected hook
// to seed the DB with some example data.
protected seed() {
return this.Drivers.count().then(count => {
if (!count)
return this.Drivers.insert({
name: "Example Driver",
cars: [{
make: 'Audi',
model: 'A4',
colour: { r: 0, g: 0, b: 0 }
}]
})
})
}
}
When you have defined all of your database components, you'll probably want to use it.
To do so, simply instantiate your MyDatabase
type and provide the connection configuration
for your environment; call connect()
and then do whatever you need to do.
For more information on the various connection options you can provide, take a look at the API documentation.
var myDb = new MyDatabase({ database: 'drivers_test' });
myDb.connect()
.then(() => myDb.Drivers.get({ name: "Example Driver" }))
.then((driver) => {
driver.addCar('Audi', 'S4', { r: 255, g: 255, b: 255 });
return driver.save();
})
.then(() => myDb.close());
Generated using TypeDoc