A lightweight TypeScript MongoDB ODM with standout convenience features like CollectionFactory
and findWithPagination
Read the excerpt from Angular for Enterprise on Understanding DocumentTS on the Wiki.
Looking to containerize MongoDB? Checkout excellalabs/mongo for a fully featured Mongo container (with Auth & SSL) inherited from the official Mongo Docker image and instructions on how to deploy it on AWS.
Major Features
DocumentTS is an ODM (Object Document Mapper) for MongoDB.
-
connect()
MongoDB async connection harness
It can be a challenge to ensure that database connectivity exists, when writing a fully async web application.
connect()
makes it easy to connect to a MongoDB instance and makes it safe to be called simultaneously from multiple threads starting up simultaneously. -
Document
andIDocument
Base Class and Interface to help define your own models
-
CollectionFactory
Define collections, organize indexes, and aggregate queries alongside collection implementation. Below are the convenience features of a DocumentTS collection
-
get collection
returns the native MongoDB collection, so you can directly operate on it
get collection(): ICollectionProvider<TDocument>
-
aggregate
allows you to run a MongoDB aggregation pipeline
aggregate(pipeline: object[]): AggregationCursor<TDocument>
-
findOne
andfindOneAndUpdate
simplifies the operation of commonly used database functionality, automatically hydrating the models it returns
async findOne(filter: FilterQuery<TDocument>, options?: FindOneOptions) async findOneAndUpdate( filter: FilterQuery<TDocument>, update: TDocument | UpdateQuery<TDocument>, options?: FindOneAndReplaceOption ): Promise<TDocument | null>
-
findWithPagination
is by far the best feature of DocumentTS, allowing you to filter, sort, and paginate large collections of data. This function is geared towards use with data tables, so you specify searchable properties, turn off hydration, and use a debug feature to fine-tune your queries.
async findWithPagination<TReturnType extends IDbRecord>( queryParams: Partial<IQueryParameters> & object, aggregationCursorFunc?: Func<AggregationCursor<TReturnType>>, query?: string | object, searchableProperties?: string[], hydrate = true, debugQuery = false ): Promise<IPaginationResult<TReturnType>>
-
Quick Start
Supports MongoDB v4+, Mongo Driver 3.3+ and TypeScript 3.7+
- Add DocumentTS to your project with
npm install document-ts mongodb
- Connect to your Mongo database using
connect()
- Connect will retry connecting to the database 10 times every 2 seconds
- Set
connectionRetryWait
(in seconds) andconnectionRetryMax
to modify this behavior
- Set
- Specify
isProd
andcertFileUri
to connect using an SSL certificate
import { connect } from 'document-ts'
async function start() {
// If isProd is set to true and a .pem file is provided, SSL will be used to connect: i.e. connect(config.mongoUri, isProd, 'server/compose-ca.pem')
await connect(process.env.MONGO_URI)
}
start()
- If you use
connect()
then you don't have to worry about having your Database Instance initialized during an asynchronous start-up sequence.getDbInstance
gives you access to the native MongoDB driver to perform custom functions like creating indexes.
import { getDbInstance } from 'document-ts'
// assuming this is called within an async function
await dbInstance.collection('users').createIndexes([
{
key: {
displayName: 1,
},
},
{
key: {
email: 1,
},
unique: true,
},
])
- Define the interface for your first model
See
tests\user.ts
for sample Model implementation
import { IDocument } from 'document-ts'
export interface IUser extends IDocument {
email: string;
firstName: string;
lastName: string;
role: string;
}
- Define the class for your model
See
tests\user.ts
for sample Model implementation
import { Document } from 'document-ts'
export class User extends Document<IUser> implements IUser {
static collectionName = 'users'
private password: string
public email: string
public firstName: string
public lastName: string
public role: string
constructor(user?: IUser) {
super(User.collectionName, user)
}
...
}
- Implement
getCalculatedPropertiesToInclude()
which will ensure that yourget
properties that are "calculate" on the fly will be serialized when sending the model down to the client, but it will not be saved in the database.
getCalculatedPropertiesToInclude(): string[]{
return ['fullName']
}
- Implement
getPropertiesToExclude()
which will ensure that certain properties like passwords will not be serialized when sending the model down to the client, but it will still be saved in the database.
getPropertiesToExclude(): string[]{
return ['password']
}
- Implement the
CollectionFactory
class, so that you can run Mongo queries without having to callgetDbInstance
or specify the collection and TypeScript type name every time you run a query. CollectionFactory provides convenience functions likefind
,findOne
,findOneAndUpdate
,findWithPagination
, and similar, while also handlinghydration
tasks, such as serializing getters and child documents.
import { CollectionFactory } from 'document-ts'
class UserCollectionFactory extends CollectionFactory<User> {
constructor(docType: typeof User) {
super(User.collectionName, docType, ['firstName', 'lastName', 'email'])
}
}
export let UserCollection = new UserCollectionFactory(User)
-
CollectionFactory
is powerful and flexible. In your custom class, you can implement MongoDB aggregate queries to run advanced join-like queries, geo queries, and whatever MongoDB supports.findWithPagination
itself is very powerful and will enable you to implement paginated dashboards with ease. -
findWithPagination
leverage query parameters for pagination and configuration
export interface IQueryParameters {
filter?: string
skip?: number
limit?: number
sortKeyOrList?: string | Object[] | Object
projectionKeyOrList?: string | Object[] | Object
}
- Optionally implement
toJSON()
to customize serialization/hydration behavior or extendISerializable
toJSON() {
let keys = Object.keys(this).concat(['fullAddress', 'localAddress'])
return Serialize(SerializationStrategy.JSON, this, keys)
}
- Optionally implement
toBSON()
to customize database serialization behavior or extendISerializable
toBSON() {
let keys = Object.keys(this).concat(['fullAddress', 'localAddress'])
return Serialize(SerializationStrategy.BSON, this, keys)
}
- To debug the default serialization behavior, implement
toJSON() {
// drop a breakpoint here or console.log(this)
return super.toJSON()
}
toBSON() {
return super.toBSON()
}
See the Lemon Mart Server sample project for usage - https://github.com/duluca/lemon-mart-server
Goals
- Reliable
- Rely on the rock-solid Native Node.js MongoDB drivers
- Don't inject custom code into DB calls without explicit intent by the developer
- Don't hide new MongoDB features, so you don't have to wait for DocumentTS to be updated before you can use them
- Optional
- Stays out of the way, so developers can slowly transition
- If performance becomes a concern easily switch to native MongoDB calls for the best performance
- Async
- Ensure developers can write simpler and more reliable code by surfacing promises and async/await features
- Convenient
- Developers define their own models through simple Interfaces
- Choose fields that you want to automatically hydrate, such as child or related objects
- Serialize calculated fields with every request
- Protect certain fields (like passwords) from serialization, so they aren't accidentally sent across the wire
- Promote Good Patterns
- Suggest/enable easy to understand and implement patterns for developers, so their code can scale and remain organized
- Prevent Bloat
- Leverage TypeScript types, interfaces, generics and inheritance to achieve development-time certainty of proper database access
- Keep the code smart, readable and lean
- Be very selective about any new features
What It Isn't
Not a full-fledged ODM or ORM replacement and doesn't aspire to be one like Mongoose or Camo. Databases are HARD. MongoDB took many years to mature, and Microsoft has been trying for a really long time to build a reliable ORM with Entity Framework, Mongoose and many other ODMs are ridden with bugs (no offense) when you push them beyond the basics. It takes great resources to deliver a solid data access experience, so with DocumentTS you can develop directly against MongoDB while enjoying some conveniences as you choose.
Inspirations
Although DocumentTS doesn't aspire to replace Mongoose or Camo, it most definitely is inspired by them in the way they've solved certain problems such as hydration. Check out the source code for those projects here:
- Mongoose
- Camo
Building This Project
- Run
npm install
- Run
npm test