README
🌠 @reflet/mongoose
The best decorators for Mongoose. Have a look at Reflet's philosophy.
- Getting started
- Schema definition
- Model
- Schema options
- Schema retrieval
- Hooks
- Model discriminators
- Embedded discriminators
- Populate virtuals
- Plain helper
- Augmentations
Getting started
Make sure you have decorators enabled. (click for details)
Enable them in your TypeScript compiler options.
"experimentalDecorators": true, "emitDecoratorMetadata": true,
Install
reflect-metadata
shim.yarn add reflect-metadata
Import the shim in your program before everything else.
import 'reflect-metadata'
Install the package along with peer dependencies.
yarn add @reflet/mongoose mongoose && yarn add -D @types/mongoose @types/node
Create your decorated models.
// user.model.ts import { Model, Field } from '@reflet/mongoose' @Model() export class User extends Model.Interface { static findByEmail(email) { return this.findOne({ email }); } @Fied({ type: String, required: true }) email: string getProfileUrl() { return `https://mysite.com/${this.email}`; } }
Connect to MongoDB and save your documents.
// server.ts import 'reflect-metadata' import * as mongoose from 'mongoose' import { User } from './user.model.ts' mongoose.connect('mongodb://localhost/test', { useNewUrlParser: true }) User.create({ email: 'jeremy@example.com' }).then((user) => { console.log(`User ${user._id} has been saved.`) })
The Mongoose way
Connect Mongoose in the way you already know. Models defined with Reflet will simply be attached to the default Mongoose connection. This means you can progressively decorate your Mongoose models. 😉
Schema definition
🔦
@Field(schemaType)
💫 Related Mongoose object: SchemaTypes
Mongoose already allows you to load an ES6 class to attach getters, setters, instance and static methods to a schema.
But what about properties ? Enters the @Field
decorator :
class User {
@Field({ type: String, required: true })
firstname: string
@Field({ type: String, required: true })
lastname: string
@Field(Number)
age?: number
get fullname() {
return `${this.firstname} ${this.lastname}`
}
}
The @Field
API is a direct wrapper of Mongoose SchemaType, you can put in there everything that you used to.
Nested properties
🔦
@Field.Nested(schemaTypes)
class User {
@Field.Nested({
street: { type: String, required: true },
city: { type: String, required: true },
country: { type: String, required: true },
})
address: {
street: string
city: string
country: string
}
}
@Field.Nested
works the same as @Field
at runtime. Its type is simply looser to allow nested objects.
Model
🔦
@Model(collection?, connection?)
💫 Related Mongoose method:model
TypeScript class decorators can modify and even replace class constructors. Reflet takes advantage of this feature and transforms a class directly into a Mongoose Model.
This means you don't have to deal with the procedural and separate creation of both schema and model anymore ! And now your properties and methods are statically typed.
Reflet | Mongoose equivalent |
---|---|
|
|
Your class needs to inherit a special empty class, Model.Interface
or Model.I
, to have Mongoose document properties and methods.
⚠️ @Model
should always be at the top of your class decorators. Why? Because decorators are executed bottom to top, so if @Model
directly compiles your class into a Mongooose Model, other class decorators won't be properly applied. Reflet will warn you with its own decorators.
Custom collection name
By default, Mongoose automatically creates a collection named as the plural, lowercased version of your model name. You can customize the collection name, with the first argument:
@Model('people')
class User extends Model.I {
@Field({ type: String, required: true })
email: string
}
Custom Mongoose connection
You can use a different database by creating a Mongoose connection and passing it as the second argument:
const otherDb = mongoose.createConnection('mongodb://localhost/other', { useNewUrlParser: true })
@Model(undefined, otherDb)
class User extends Model.I {
@Field({ type: String, required: true })
email: string
}
Schema options
🔦
@SchemaOptions(options)
💫 Related Mongoose object: Schema options
Reflet | Mongoose equivalent |
---|---|
|
|
The @SchemaOptions
API is a direct wrapper of Mongoose schema options, you can put in there everything that you used to.
Timestamps
🔦
@CreatedAt
,@UpdatedAt
💫 Related Mongoose option property:timestamps
Reflet | Mongoose equivalent |
---|---|
|
|
Advanced schema manipulation
🔦
@SchemaCallback(callback)
💫 Related Mongoose object:Schema
If you need more advanced schema manipulation before @Model
compiles it, you can use @SchemaCallback
:
Reflet | Mongoose equivalent |
---|---|
|
|
Beware of defining hooks in the callback if your schema has embedded discriminators. Mongoose documentation recommends declaring hooks before embedded discriminators, the callback is applied after them. You should use the dedicated hooks decorators @PreHook
and @PostHook
.
Schema retrieval
🔦
schemaFrom(class)
You can retrieve a schema from any decorated class, for advanced manipulation or embedded use in another schema.
Reflet | Mongoose equivalent |
---|---|
|
|
🗣️ As a good practice, you should make your schema-only classes abstract
, so you don't instantiate them by mistake.
Sub schemas
🔦
Field.Schema(class)
As an alternative for the above use of schemaFrom
inside the Field
decorator, you can do:
@Model()
class City extends Model.I {
@Field.Schema(Location)
location: Location
}
Hooks
Pre hook
🔦
@PreHook(method, callback)
💫 Related Mongoose method:schema.pre
Reflet | Mongoose equivalent |
---|---|
|
|
The @PreHook
API is a direct wrapper of Mongoose Schema.pre method, you can put in there everything that you used to.
Post hook
🔦
@PostHook(method, callback)
💫 Related Mongoose method:schema.post
Reflet | Mongoose equivalent |
---|---|
|
|
The @PostHook
API is a direct wrapper of Mongoose Schema.post method, you can put in there everything that you used to.
Post error handling middleware
To help the compiler accurately infer the error handling middleware signature, pass a second type argument to @PostHook
:
@Model()
@PostHook<User, Error>('save', function(error, doc, next) {
if (error.name === 'MongoError' && error.code === 11000) {
next(new Error('There was a duplicate key error'))
} else {
next()
}
})
class User extends Model.I {
@Field(String)
name: string
}
Model discriminators
🔦
@Model.Discriminator(rootModel)
💫 Related Mongoose method:model.discriminator
@Model()
class User extends Model.I {
@Field(String)
name: string
}
@Model.Discriminator(User)
class Worker extends User {
@Field(String)
job: string
// No need to decorate the default discriminatorKey.
__t: 'Worker'
// You can strictly type the constructor of a discriminator model by using the Plain Helper (see below).
constructor(worker: Plain.Omit<Worker, '_id' | '__t'>) {
super() // required by the compiler.
}
}
const worker = await Worker.create({ name: 'Jeremy', job: 'developer' })
// { _id: '5d023ae14043262bcfd9b384', __t: 'Worker', name: 'Jeremy', job: 'developer' }
As you know, _t
is the default discriminatorKey
, and its value will be the class name. If you want to customize both the key and the value, check out the following @Kind
decorator.
⚠️ @Model.Discriminator
should always be at the top of your class decorators. Why? Because decorators are executed bottom to top, so if @Model.Discriminator
directly compiles your class into a Mongooose Model, other class decorators won't be properly applied. Reflet will warn you with its own decorators.
Kind / DiscriminatorKey
🔦
@Kind(value?)
alias@DiscriminatorKey
💫 Related Mongoose option property:discriminatorKey
Mongoose discriminatorKey
is usually defined in the parent model options, and appears in the children model documents.
@Kind
(or its alias @DiscriminatorKey
) exists to define discriminatorKey
directly on the children class instead of the parent model.
@Model()
class User extends Model.I {
@Field(String)
name: string
}
@Model.Discriminator(User)
class Developer extends User {
@Kind
kind: 'Developer' // Value will be the class name by default.
}
@Model.Discriminator(User)
class Doctor extends User {
@Kind('doctor') // Customize the discriminator value by passing a string.
kind: 'doctor'
}
// This is equivalent to setting `{ discriminatorKey: 'kind' }` on User schema options.
A mecanism will check and prevent you from defining a different @Kind
key on sibling discriminators.
Embedded discriminators
Single nested discriminators
🔦
@Field.Union(classes[], options?)
💫 Related Mongoose method:SingleNestedPath.discriminator
@Field.Union
allows you to embed discriminators on an single property.
abstract class Circle {
@Field(Number)
radius: number
__t: 'Circle'
// __t is the default discriminatorKey (no need to decorate).
// Value will be the class name.
}
abstract class Square {
@Field(Number)
side: number
__t: 'Square'
}
@Model()
class Shape extends Model.I {
@Field.Union([Circle, Square], {
required: true, // Make the field itself `shape` required.
strict: true // Make the discriminator key `__t` required and narrowed to its possible values.
})
shape: Circle | Square
}
const circle = new Shape({ shape: { __t: 'Circle', radius: 5 } })
const square = new Shape({ shape: { __t: 'Square', side: 4 } })
Embedded discriminators in arrays
🔦
@Field.ArrayOfUnion(classes[], options?)
💫 Related Mongoose method:DocumentArrayPath.discriminator
@Field.ArrayOfUnion
allows you to embed discriminators in an array.
@SchemaOptions({ _id: false })
abstract class Clicked {
@Field({ type: String, required: true })
message: string
@Field({ type: String, required: true })
element: string
@Kind
kind: 'Clicked'
}
@SchemaOptions({ _id: false })
abstract class Purchased {
@Field({ type: String, required: true })
message: string
@Field({ type: String, required: true })
product: string
@Kind
kind: 'Purchased'
}
@Model()
class Batch extends Model.I {
@Field.ArrayOfUnion([Clicked, Purchased], {
strict: true // Make the discriminator key `kind` required and narrowed to its possible values.
})
events: (Clicked | Purchased)[]
}
const batch = new Batch({
events: [
{ kind: 'Clicked', element: '#hero', message: 'hello' },
{ kind: 'Purchased', product: 'action-figure', message: 'world' },
]
})
Populate virtuals
🔦
@PopulateVirtual(options)
💫 Related Mongoose method:schema.virtual
@Model()
class Person extends Model.I {
@Field(String)
name: string
@Field(String)
band: string
}
@Model()
@SchemaOptions({
toObject: { virtuals: true },
toJson: { virtuals: true }
})
class Band extends Model.I {
@Field(String)
name: string
@PopulateVirtual<Person, Band>({
ref: 'Person',
foreignField: 'band',
localField: 'name'
})
readonly members: string[]
}
const bands = await Band.find({}).populate('members')
Plain helper
🔦
Plain<class, options?>
Reflet provides a generic type to discard Mongoose properties and methods from a Document.
Plain<T>
removes inherited Mongoose properties (except_id
) and all methods fromT
.Plain.Partial<T>
does the same asPlain
and makes remaining properties ofT
optional.Plain.PartialDeep<T>
does the same asPlain.Partial
recursively.
Plain
also has a second type argument to omit other properties and/or make them optional:
Plain<T, { Omit: keyof T; Optional: keyof T } >
These options exist as standalone generics as well: Plain.Omit<T, keyof T>
and Plain.Optional<T, keyof T>
.
One document, multiple shapes
In each of your models, some fields might be present when you read the document from the database, but optional or even absent when you create it. 😕
Given the following model:
@Model()
class User extends Model.I {
@Field({ type: String, required: true })
firstname: string
@Field({ type: String, required: true })
lastname: string
@Field({ type: Boolean, default: () => false })
activated: boolean
@CreatedAt
createdAt: Date
get fullname() {
return `${this.firstname} ${this.lastname}`
}
}
This is how you can type constructor
and create
parameters:
type NewUser = Plain<User, { Omit: 'fullname' | 'createdAt'; Optional: '_id' | 'activated' }>
@Model()
class User extends Model.I {
// ...
// @ts-ignore implementation
constructor(user: NewUser)
static create(doc: NewUser): Promise<User>
// @ts-ignore implementation
static create(docs: NewUser[]): Promise<User[]>
}
const user = new User({ firstname: 'John', lastname: 'Doe' })
await user.save()
await User.create({ firstname: 'Jeremy', lastname: 'Doe', activated: true })
You can safely use ts-ignore to avoid a useless wrapper of create
or even the constructor
, the compiler will still check NewUser
as we need.
You can also narrow the return type of toObject()
and toJson()
:
const userPlain = user.toObject({ getters: false }) as Plain.Omit<User, 'fullname'>
Augmentations
Reflet comes with a dedicated global namespace RefletMongoose
, so you can augment or narrow specific types.
SchemaType options augmentation
If you use plugins like mongoose-autopopulate,
you can augment the global interface SchemaTypeOptions
to have new options in the @Field
API.
declare global {
namespace RefletMongoose {
interface SchemaTypeOptions {
autopopulate?: boolean
}
}
}
Now you have access to autopopulate
option in your schemas:
@Model()
class User extends Model.I {
@Field({
type: mongoose.Schema.Types.ObjectId,
ref: Company,
autopopulate: true
})
company: Company
}
Model and Document augmentation
You can augment Model
and Document
interfaces with the dedicated global interfaces.
Here is an example with the @casl/mongoose plugin:
declare global {
namespace RefletMongoose {
interface Model {
accessibleBy<T extends mongoose.Document>(ability: AnyMongoAbility, action?: string): mongoose.DocumentQuery<T[], T>
}
interface Document {}
}
}
References narrowing
SchemaType ref
option can be either a Model or a model name. With Reflet, by default, any class can be passed as a Model and any string can be passed as a model name.
By augmenting the global interface Ref
, you can:
- Narrow the class to an union of your models.
- Narrow the string to an union of your models' names.
// company.model.ts
@Model()
class Company extends Model.I {
@Field(String)
name: string
}
// You should augment the global interface in each of your models' files, to keep it close to the model.
declare global {
namespace RefletMongoose {
interface Ref {
Company: Company
}
}
}
// user.model.ts
@Model()
class User extends Model.I {
@Field({
type: mongoose.Schema.Types.ObjectId,
ref: 'Company',
})
company: Company
}
declare global {
namespace RefletMongoose {
interface Ref {
User: User
}
}
}