Iridium
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.
Installation
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
Features
- Built with TypeScript and designed for ease of use, you'll be hard pressed to find another Node.js ORM as easy to pick
up and learn when combined with a supporting editor.
- Promises Throughout mean that you can easily start working with your favourite A+ compliant promise library and writing
more readable code. Don't think promises are the way forward? Stick with callbacks, Iridium supports both.
- Fully Tested with over 300 unit tests and over 95% test coverage, helping to ensure that Iridium always behaves the way
it's supposed to.
- Decorator Support helps describe your classes in an easy to understand and maintain manner, embrace ES7 with strong fallbacks
on ES5 compliant approaches.
- Fall into Success with Iridium's carefully designed API - which is structured to help ensure your code remains maintainable
regardless of how large your project becomes.
- Amazing Documentation which covers the full Iridium API, and is always up to date thanks to the brilliant TypeDoc project,
can be found at sierrasoftworks.github.io/Iridium.
Requirements
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.
Using Iridium
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.
Example Description
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.
Example Components
The Colour
object
The 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.
const ColourSchema = {
r: "ubyte",
g: "ubyte",
b: "ubyte"
}
interface ColourDoc {
r: number;
g: number;
b: number;
}
class Colour {
constructor(doc: ColourDoc) {
this.r = doc.r
this.g = doc.g
this.b = doc.b
}
r: number;
g: number;
b: number;
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)}`
}
private toHex(num: number, padding = 2) {
let hex = num.toString(16)
while (hex.length < 2)
hex = `0${hex}`
return hex
}
}
The Car
object
Our 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()
}
}
}
The Address
object
In 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 = {
lines: [String, 1, 3],
country: String
}
interface AddressDoc {
lines: string[];
country: string;
}
The Driver
model
Here 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";
interface DriverDocument {
_id?: ObjectId;
name: string;
address: AddressDoc;
cars?: CarDoc[];
}
@Iridium.Index({ name: 1 })
@Iridium.Index({ "cars.model": 1, "cars.make": 1 }, { background: true })
@Iridium.Collection('drivers')
@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> {
@Iridium.ObjectID
_id: string;
@Iridium.Property(/^[A-Z][\w\-\.]*(\s[A-Z][\w\-\.])+/)
@Iridium.Rename("full_name")
name: string;
@Iridium.Property(AddressSchema)
address: AddressDoc;
@Property([CarSchema])
@Iridium.TransformClassList(Car)
cars: Car[];
static onCreating(doc: HouseDocument) {
doc.cars = doc.cars || [];
}
addCar(make: string, model: string, colour: Colour) {
this.cars.push(new Car({
make: make,
model: model,
colour: colour.toDB()
}));
}
get numberOfCars() {
return this.cars.length;
}
}
Example Core
implementation
Once 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);
protected onConnected() {
return super.onConnected().then(() => Promise.all([
this.Drivers.ensureIndexes(),
this.seed()
]))
}
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 }
}]
})
})
}
}
Example Application
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());