strongoose
v0.1.5
Published
Mongoose meets Typescript
Downloads
4
Readme
Strongoose
Mongoose is great and all, but it makes for a subpar experience in Typescript. You need schemas, interfaces, and plain classes just for getters or instance methods. Strongoose streamlines all of that in single classes with just a few decorators. This project was inspired and makes heavy use of the implementation of Typegoose, however builds upon its own ideas.
Simply put, you'll be defining intuitive models as these in no time:
class User extends Strongoose {
@field({ required: true })
firstName: string
@field({ required: true })
lastName: string
@field({ required: true, unique: true })
email: string
@field()
password: string
@field({ ref: Team })
teams: Team[]
@field()
settings: Settings
get fullName() {
return `${this.firstName} ${this.lastName}`
}
}
class Team extends Strongoose {
@field()
name: string
}
class Settings {
@field()
receiveEmails: boolean
@field()
receiveNotifications: boolean
}
const UserModel = new User().getModel(User)
const TeamModel = new Team().getModel(Team)
Installation
Before installing, make sure you have Mongoose and Reflect Metadata installed, both listed as peerDependencies
.
$ npm install --save strongoose
Field
The @field
decorator accepts a plain object with any of the Mongoose options listed in here. As Strongoose automatically infers type information, there's no need to set a type manually. It works even for Mongoose-specific types, such as ObjectId or Decimal128.
@field()
id: mongoose.Schema.Types.ObjectId
An overly complicated field would look like:
@field({ required: true, unique: true, default: '[email protected]', index: true, lowercase: true, validate: /(.+)@(.+)/ })
email: string
Schema
The @schema
decorator is an optional object of schema-wide settings, listed in here. Typically, it may look like this:
@schema({ collection: 'docs', timestamps: true, autoIndex: false })
class Document extends Strongoose {
@field()
title: string
}
Virtual, Methods, and Statics
Mongoose supports them either via schema methods, or by passing a plain class to Schema.loadClass. The latter is specifically problematic for Typescript, which has no idea where the field names are coming from. Strongoose makes it as easy as defining methods.
class Book extends Strongoose {
@field()
title: string
@field()
author: string
get whole() {
return `${this.author} - ${this.title}`
}
addIsbn(isbn: string) {
return `${this.title}: ${isbn}`
}
static findByAuthor(author: string) {
return this.find({ author })
}
}
References
References are handled by passing in a model as type, which automatically builds the correct schema. Returning to the firstmost example, the code below adds a reference to an array of Team
on User.teams
.
class User extends Strongoose {
@field({ ref: Team })
teams: Team[]
}
class Team extends Strongoose {
@field()
name: string
}
The only ceveat is the ref
option passed to @field
, as it informs Strongoose that you're trying to build a reference, not a subdocument.
The above code is equivalent to these schemas in Mongoose:
const team = new mongoose.Schema({
name: string
})
const user = new mongoose.Schema({
teams: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Team' }]
})
Subdocuments
Subdocuments are handled as a simplified case of references. It still infers the schema from the type, but it doesn't expect a ref
option and the subschema classes don't need to extend Strongoose or be initialized as a model. They're simply used to build the schema and aren't evaluated.
class User extends Strongoose {
@field()
settings: Settings
}
class Settings {
@field()
receiveEmails: boolean
@field()
receiveNotifications: boolean
}
That's equivalent to these schemas in Mongoose:
const settingsSchema = new mongoose.Schema({
receiveEmails: Boolean,
receiveNotifications: Boolean
})
const userSchema = new mongoose.Schema({
settings: settingsSchema
})
In the case of the settings
, we don't really need _id
s generated for the subdocument, something Mongoose does by default. We just need a plain object.
class User extends Strongoose {
@field({ _id: false })
settings: Settings
}
class Settings {
@field()
receiveEmails: boolean
@field()
receiveNotifications: boolean
}
Inheritance
Class inheritance can be exploited to compose schemas using shared fields that are built into the children. All the children will receive them as if they were originally declared into them. Just be aware of the implications of such strategy, or you'll end up with model definitions that are difficult to reason about. For simple and common fields, it makes total sense though.
class Person extends Strongoose {
@field({ required: true })
name: string
@field({ required: true, unique: true, index: true })
email: string
}
class User extends Person {
@field()
avatar: string
}
class Friend extends Person {
@field()
private: boolean
}
Base classes need to extend Strongoose, even if they're not going to be used as concrete models. That way, children can inherit methods like getModel()
or setModel()
.
Initializing Models
Before being usable as Mongoose models, Strongoose classes need to be initialized. This is done using the getModel()
method on instances:
class User extends Strongoose {
@field()
name: string
}
const UserModel = new User().getModel(User)
The only parameter to getModel
is the class itself, which helps in informing Typescript of static methods.
Roadmap
Before going for version 1.0, I plan on at least these features:
- Support Middleware.
- Support Plugins.
- Support schema-wide indexes.
- Support Enums.
- Automated tests.
- Real-world testing on my own projects.
- ~~Allow using an existing connection or mongoose instance.~~