1 | <h1 align="center">
|
2 | <br>
|
3 | <img width="400" src="media/logo.png">
|
4 | <br>
|
5 | <br>
|
6 | </h1>
|
7 |
|
8 | > Lightweight and flexible MongoDB ODM for Node.js apps based on Redux.
|
9 |
|
10 | [![Build Status](https://travis-ci.org/vadimdemedes/mongorito.svg?branch=master)](https://travis-ci.org/vadimdemedes/mongorito) [![Coverage Status](https://coveralls.io/repos/vadimdemedes/mongorito/badge.svg?branch=master&service=github)](https://coveralls.io/github/vadimdemedes/mongorito?branch=master)
|
11 |
|
12 |
|
13 | ## Features
|
14 |
|
15 | **Flexible**
|
16 |
|
17 | Mongorito is based on [Redux](https://github.com/reactjs/redux), which opens the doors for customizing literally everything - from model's state (reducers) to the behavior of core methods, like `set()`, `save()` or `find()`.
|
18 |
|
19 | Each model instance has a separate Redux store, which ensures isolation between other models and easy extensibility.
|
20 |
|
21 | **No schemas**
|
22 |
|
23 | If MongoDB doesn't enforce schemas, why would Mongorito do? Enjoy the schema-free data management with Mongorito the same way you do in `mongo` console.
|
24 |
|
25 | **Lightweight**
|
26 |
|
27 | Mongorito is betting on 3rd-party plugins to deliver extra functionality to developers. Mongorito ships with a barebones model with basic get/set, save/remove and querying functionality and let's you be in control of what's included and what's not.
|
28 |
|
29 | Mongorito is basically a tiny Redux-based application, which uses the official MongoDB driver and [mquery](https://github.com/aheckmann/mquery) for querying. Not that amount of lines are relevant when measuring complexity, but each file (module) is less than 300 lines. Check out the source code and see for yourself!
|
30 |
|
31 |
|
32 | ## Quick overview
|
33 |
|
34 | ```js
|
35 | const {Database, Model} = require('mongorito');
|
36 |
|
37 | const db = new Database('localhost/blog');
|
38 | await db.connect();
|
39 |
|
40 | class Post extends Model {}
|
41 |
|
42 | db.register(Post);
|
43 |
|
44 | const post = new Post({
|
45 | title: 'Steve Angello rocks',
|
46 | author: {
|
47 | name: 'Emma'
|
48 | }
|
49 | });
|
50 |
|
51 | await post.save();
|
52 |
|
53 | post.set('author.name', 'Rick');
|
54 | await post.save();
|
55 | ```
|
56 |
|
57 | *Note*: `await` won't work at top level, it's used to reduce the complexity of an example.
|
58 |
|
59 |
|
60 | ## Installation
|
61 |
|
62 | ```
|
63 | $ npm install --save mongorito
|
64 | ```
|
65 |
|
66 |
|
67 | ## Contents
|
68 |
|
69 | - [Connection](#connection)
|
70 | - [Models](#models)
|
71 | - - [Creating a model](#creating-a-model)
|
72 | - - [Working with fields](#working-with-fields)
|
73 | - - [Saving or removing documents](#saving-or-removing-documents)
|
74 | - - [Incrementing fields](#incrementing-fields)
|
75 | - - [Embedding other models](#embedding-other-models)
|
76 | - - [Configuration](#configuration)
|
77 | - [Queries](#queries)
|
78 | - [Plugins](#plugins)
|
79 | - - [Using plugins](#using-plugins)
|
80 | - - [Writing plugins](#writing-plugins)
|
81 | - - [Extending model with new methods](#extending-model-with-new-methods)
|
82 | - - [Modifying model's state](#modifying-models-state)
|
83 | - - [Changing behavior using middleware](#changing-behavior-using-middleware)
|
84 | - [Migrating from legacy version](#migrating-from-legacy-version)
|
85 |
|
86 |
|
87 | ## Connection
|
88 |
|
89 | Mongorito exports several own classes, as well as a few properties from the MongoDB driver:
|
90 |
|
91 | ```js
|
92 | const {
|
93 | Database,
|
94 | Model,
|
95 | Timestamp,
|
96 | ObjectId,
|
97 | MinKey,
|
98 | MaxKey,
|
99 | DBRef,
|
100 | Long
|
101 | } = require('mongorito');
|
102 | ```
|
103 |
|
104 | `Database` and `Model` are Mongorito's own exports, all the other ones are exported straight from [`mongodb`](https://github.com/mongodb/node-mongodb-native) package for convenience. Normally, you'd need only `Database`, `Model` and `ObjectId`.
|
105 |
|
106 | To connect, initialize a `Database`, which accepts a MongoDB connection string and use `connect()` method, which returns a Promise.
|
107 |
|
108 | For convenience, `await` will be used in all examples below, even though it doesn't work at top level.
|
109 |
|
110 | ```js
|
111 | const {Database, Model} = require('mongorito');
|
112 |
|
113 | const db = new Database('localhost/blog');
|
114 | await db.connect();
|
115 | ```
|
116 |
|
117 | You don't have to wait until connection establishes to perform operations. Mongorito automatically executes pending operations once connection is up.
|
118 |
|
119 | ## Models
|
120 |
|
121 | ### Creating a model
|
122 |
|
123 | Model is the connection between your data and a database. Each model represents a single collection. Model is a simple class, which doesn't even need to have any properties or methods.
|
124 |
|
125 | ```js
|
126 | class Post extends Model {}
|
127 | ```
|
128 |
|
129 | For `Post` model to work and be aware of the database it's connected to, make sure to register it in the database we created earlier.
|
130 |
|
131 | ```js
|
132 | db.register(Post);
|
133 | ```
|
134 |
|
135 | That's it, the `Post` model is good to go!
|
136 |
|
137 | ### Working with fields
|
138 |
|
139 | To create a new document, create an instance of `Post` model.
|
140 |
|
141 | ```js
|
142 | const post = new Post();
|
143 | ```
|
144 |
|
145 | Model's constructor also accepts an object of fields to instantiate the document with:
|
146 |
|
147 | ```js
|
148 | const post = new Post({
|
149 | title: 'Great post',
|
150 | author: {
|
151 | name: 'Sarah'
|
152 | }
|
153 | });
|
154 | ```
|
155 |
|
156 | Note, documents can contain nested fields and even models, just like in MongoDB.
|
157 |
|
158 | To get one or all fields from the `post` document, use a `get()` method.
|
159 |
|
160 | ```js
|
161 | const title = post.get('title');
|
162 | //=> "Great post"
|
163 |
|
164 | const author = post.get('author.name');
|
165 | //=> "Sarah"
|
166 |
|
167 | const data = post.get();
|
168 | //=>
|
169 | // {
|
170 | // title: "Great post"
|
171 | // author: {
|
172 | // name: "Sarah"
|
173 | // }
|
174 | // }
|
175 | ```
|
176 |
|
177 | Similarly, use `set()` to update fields:
|
178 |
|
179 | ```js
|
180 | // update fields one by one
|
181 | post.set('title', 'Amazing post');
|
182 | post.set('author.name', 'Monica');
|
183 |
|
184 | // or all at once
|
185 | post.set({
|
186 | title: 'Amazing post',
|
187 | author: {
|
188 | name: 'Monica'
|
189 | }
|
190 | });
|
191 | ```
|
192 |
|
193 | To remove a field, use `unset()`:
|
194 |
|
195 | ```js
|
196 | // unset single fields
|
197 | post.unset('title');
|
198 | post.unset('author.name');
|
199 |
|
200 | // or multiple fields at once
|
201 | post.unset(['title', 'author.name']);
|
202 | ```
|
203 |
|
204 | ### Saving or removing documents
|
205 |
|
206 | To create or update documents, simply call `save()`. Even though Mongorito differentiates these two operations internally, you don't have to care about that! Mongorito also infers the collection name from the model, so the instances of the model `Post` will be saved to `posts` collection.
|
207 |
|
208 | ```js
|
209 | await post.save();
|
210 | ```
|
211 |
|
212 | When a document is saved, an `_id` field is automatically added.
|
213 |
|
214 | ```js
|
215 | post.get('_id');
|
216 | //=> ObjectId("5905cb6b543c3a50e03e810d")
|
217 | ```
|
218 |
|
219 | To remove a document, use `remove()`.
|
220 |
|
221 | ```js
|
222 | await post.remove();
|
223 | ```
|
224 |
|
225 | To remove multiple documents, use `remove()` on the model itself with a query as an argument.
|
226 |
|
227 | ```js
|
228 | await Post.remove({good: false});
|
229 | ```
|
230 |
|
231 | ### Incrementing fields
|
232 |
|
233 | Mongorito also provides a handy `increment()` method to increment or decrement numerical fields:
|
234 |
|
235 | ```js
|
236 | const post = new Post({
|
237 | views: 0
|
238 | });
|
239 |
|
240 | await post.increment('views');
|
241 |
|
242 | post.get('views');
|
243 | //=> 1
|
244 | ```
|
245 |
|
246 | You can also supply a value to increment a field by a specific amount.
|
247 |
|
248 | ```js
|
249 | await post.increment('views', 2);
|
250 |
|
251 | post.get('views');
|
252 | //=> 3
|
253 | ```
|
254 |
|
255 | Multiple fields can be incremented at once, too.
|
256 |
|
257 | ```js
|
258 | const post = new Post({
|
259 | views: 10,
|
260 | comments: 10
|
261 | });
|
262 |
|
263 | await post.increment({
|
264 | views: 2,
|
265 | comments: 5
|
266 | });
|
267 |
|
268 | post.get('views');
|
269 | //=> 12
|
270 |
|
271 | post.get('comments');
|
272 | //=> 15
|
273 | ```
|
274 |
|
275 | ### Embedding other models
|
276 |
|
277 | Just like MongoDB, Mongorito allows to effortlessly embed other models. They're transparently converted between JSON and Mongorito models.
|
278 |
|
279 | To embed models, use `embeds()` method on the model itself to help Mongorito with the model serialization when saving/reading from the database. `embeds()` method accepts a field name, where the embedded document (or array of documents) resides.
|
280 |
|
281 | Here's the quick overview on how it works. Note, that model registering via `register()` is skipped in the following example.
|
282 |
|
283 | ```js
|
284 | class Post extends Model {}
|
285 | class Author extends Model {}
|
286 | class Comment extends Model {}
|
287 |
|
288 | Post.embeds('author', Author);
|
289 | Post.embeds('comments', Comment);
|
290 |
|
291 | const post = new Post({
|
292 | title: 'Great post',
|
293 | author: new Author({name: 'Steve'}),
|
294 | comments: [new Comment({body: 'Interesting!'})]
|
295 | });
|
296 |
|
297 | await post.save();
|
298 | ```
|
299 |
|
300 | The above post will be saved to the database as:
|
301 |
|
302 | ```json
|
303 | {
|
304 | "title": "Great post",
|
305 | "author": {
|
306 | "name": "Steve"
|
307 | },
|
308 | "comments": [
|
309 | {
|
310 | "body": "Interesting!"
|
311 | }
|
312 | ]
|
313 | }
|
314 | ```
|
315 |
|
316 | You can also just pass objects instead of model instances and Mongorito will take care of that too.
|
317 |
|
318 | ```js
|
319 | const post = new Post({
|
320 | title: 'Great post',
|
321 | author: {
|
322 | name: 'Steve'
|
323 | },
|
324 | comments: [{
|
325 | body: 'Interesting!'
|
326 | }]
|
327 | });
|
328 | ```
|
329 |
|
330 | When that document will be retrieved from the database next time, all embedded documents will be wrapped with their corresponding models.
|
331 |
|
332 | ```js
|
333 | const post = await Post.findOne();
|
334 |
|
335 | const author = post.get('author');
|
336 | //=> Author { name: "Steve" }
|
337 |
|
338 | author.get('name');
|
339 | //=> "Steve"
|
340 | ```
|
341 |
|
342 | ### Configuration
|
343 |
|
344 | #### Using a different collection name
|
345 |
|
346 | In case you need to store documents in a custom collection, you can override the default one using `collection()` method.
|
347 |
|
348 | ```js
|
349 | class Post extends Model {
|
350 | collection() {
|
351 | return 'awesome_posts';
|
352 | }
|
353 | }
|
354 | ```
|
355 |
|
356 | ## Queries
|
357 |
|
358 | Mongorito uses [mquery](https://github.com/aheckmann/mquery) to provide a simple and comfortable API for querying. It inherits all the methods from `mquery` with a few exceptions, which will be documented below. For documentation, please check out mquery's API - https://github.com/aheckmann/mquery.
|
359 |
|
360 | Here's a quick overview of how querying works in Mongorito. All documents returned from queries are automatically wrapped into their models.
|
361 |
|
362 | ```js
|
363 | // find all posts
|
364 | await Post.find();
|
365 |
|
366 | // find all amazing posts
|
367 | await Post.find({amazing: true});
|
368 | await Post.where('amazing', true).find();
|
369 |
|
370 | // find 5 recent posts
|
371 | await Post
|
372 | .limit(5)
|
373 | .sort('created_at', 'desc')
|
374 | .find();
|
375 |
|
376 | // find one post
|
377 | await Post.findOne({incredible: 'yes'});
|
378 |
|
379 | // count posts
|
380 | await Post.count({super: false});
|
381 | ```
|
382 |
|
383 | ## Plugins
|
384 |
|
385 | ### Using plugins
|
386 |
|
387 | To use a 3rd-party plugin, all you have to do is to call `use()` method.
|
388 |
|
389 | ```js
|
390 | const timestamps = require('mongorito-timestamps');
|
391 |
|
392 | db.use(timestamps);
|
393 | ```
|
394 |
|
395 | This will apply [mongorito-timestamps](https://github.com/vadimdemedes/mongorito-timestamps) to models registered after that.
|
396 |
|
397 | If you want to apply the plugin to a specific model only, call it on the model itself.
|
398 |
|
399 | ```js
|
400 | Post.use(timestamps);
|
401 | ```
|
402 |
|
403 | ### Writing plugins
|
404 |
|
405 | A plugin is simply a function that accepts a model. A familiarity with Redux and its concepts will help you tremendously with writing plugins.
|
406 |
|
407 | ```js
|
408 | const myPlugin = model => {
|
409 | // do anything with model (Post, in this case)
|
410 | };
|
411 |
|
412 | Post.use(myPlugin);
|
413 | ```
|
414 |
|
415 | Feel free to assign new methods to the model or instances, add new middleware, modify the model's state and anything that comes to your mind.
|
416 |
|
417 | ### Extending model with new methods
|
418 |
|
419 | Here's an example of adding a class method and an instance method to a `Post` model.
|
420 |
|
421 | ```js
|
422 | const extendPost = Post => {
|
423 | Post.findRecent = function () {
|
424 | return this
|
425 | .limit(5)
|
426 | .sort('created_at', 'desc')
|
427 | .find();
|
428 | };
|
429 |
|
430 | Post.prototype.makeAmazing = function () {
|
431 | this.set('amazing', true);
|
432 | };
|
433 | };
|
434 |
|
435 | Post.use(extendPost);
|
436 |
|
437 | const post = new Post();
|
438 | post.makeAmazing();
|
439 | post.get('amazing');
|
440 | //=> true
|
441 |
|
442 | const posts = await Post.findRecent();
|
443 | //=> [Post, Post, Post]
|
444 | ```
|
445 |
|
446 | ### Modifying model's state
|
447 |
|
448 | If you plugin needs to have its own state, you can modify the model's reducer using `modifyReducer()` method. It accepts a function, which receives the existing reducer shape as an argument and should return a new object with added reducers.
|
449 |
|
450 | ```js
|
451 | const customReducer = (state = null, action) => {
|
452 | // reducer code...
|
453 | };
|
454 |
|
455 | const extendReducer = model => {
|
456 | model.modifyReducer(reducer => {
|
457 | return {
|
458 | ...reducer,
|
459 | customState: customReducer
|
460 | }
|
461 | });
|
462 | };
|
463 | ```
|
464 |
|
465 | ### Changing behavior using middleware
|
466 |
|
467 | Middleware can be used to change or modify the behavior of model's operations. You can interact with everything, from get/set operations to queries.
|
468 |
|
469 | To add plugin's custom middleware to the default middleware stack, return it from the plugin function.
|
470 |
|
471 | ```js
|
472 | const myPlugin = () => {
|
473 | return store => next => action => {
|
474 | // middleware code...
|
475 | };
|
476 | };
|
477 | ```
|
478 |
|
479 | Obviously, to detect what kind of action is being handled, you need to be aware of Mongorito's action types.
|
480 |
|
481 | ```js
|
482 | const {ActionTypes} = require('mongorito');
|
483 |
|
484 | const myPlugin = () => {
|
485 | return store => next => action => {
|
486 | if (action.type === ActionTypes.SET) {
|
487 | // alter set() behavior
|
488 | }
|
489 |
|
490 | return next(action);
|
491 | };
|
492 | };
|
493 | ```
|
494 |
|
495 | Again, the middleware is identical to the middleware you're used to when writing apps with Redux. There are only 2 new properties added to the `store`:
|
496 |
|
497 | - `model` - instance of the model (document) the middleware is currently running in. If middleware is running at the model level (without instantiated model), it will be `undefined`.
|
498 | - `modelClass` - model class (`Post`, for example).
|
499 |
|
500 | Here's an example on how to access all props of the store:
|
501 |
|
502 | ```js
|
503 | const myPlugin = () => {
|
504 | return ({getState, dispatch, model, modelClass}) => next => action => {
|
505 | // `getState()` and `dispatch()` are from Redux itself
|
506 | // `model` is `post`
|
507 | // `modelClass` is `Post`
|
508 |
|
509 | return next(action);
|
510 | };
|
511 | };
|
512 |
|
513 | Post.use(myPlugin);
|
514 |
|
515 | const post = new Post();
|
516 | await post.save();
|
517 | ```
|
518 |
|
519 | For examples on how to write middleware, check out Mongorito's native ones - https://github.com/vadimdemedes/mongorito/tree/master/lib/middleware.
|
520 |
|
521 |
|
522 | ## Migrating from legacy version
|
523 |
|
524 | ### Connection
|
525 |
|
526 | Before:
|
527 |
|
528 | ```js
|
529 | const mongorito = require('mongorito');
|
530 |
|
531 | mongorito.connect('localhost/blog');
|
532 | ```
|
533 |
|
534 | After:
|
535 |
|
536 | ```js
|
537 | const {Database} = require('mongorito');
|
538 |
|
539 | const db = new Database('localhost/blog');
|
540 | await db.connect();
|
541 | ```
|
542 |
|
543 |
|
544 | ## License
|
545 |
|
546 | MIT © [Vadim Demedes](https://github.com/vadimdemedes)
|