UNPKG

18.6 kBMarkdownView Raw
1# Camo
2
3## Supporters
4
5<a href="https://pingbot.dev?ref=github-camo">
6 PingBot.dev
7 <img alt="Monitoring for your servers, vendors, and infrastructure." src="https://s3.pingbot.dev/images/landing-header.png" />
8</a>
9
10## Jump To
11* <a href="#why-do-we-need-another-odm">Why do we need another ODM?</a>
12* <a href="#advantages">Advantages</a>
13* <a href="#install-and-run">Install and Run</a>
14* <a href="#quick-start">Quick Start</a>
15 * <a href="#connect-to-the-database">Connect to the Database</a>
16 * <a href="#declaring-your-document">Declaring Your Document</a>
17 * <a href="#embedded-documents">Embedded Documents</a>
18 * <a href="#creating-and-saving">Creating and Saving</a>
19 * <a href="#loading">Loading</a>
20 * <a href="#deleting">Deleting</a>
21 * <a href="#counting">Counting</a>
22 * <a href="#hooks">Hooks</a>
23 * <a href="#misc">Misc.</a>
24* <a href="#transpiler-support">Transpiler Support</a>
25* <a href="#contributing">Contributing</a>
26* <a href="#contact">Contact</a>
27* <a href="#copyright-license">Copyright & License</a>
28
29## Why do we need another ODM?
30Short answer, we probably don't. Camo was created for two reasons: to bring traditional-style classes to [MongoDB](https://www.mongodb.com/) JavaScript, and to support [NeDB](https://github.com/louischatriot/nedb) as a backend (which is much like the SQLite-alternative to Mongo).
31
32Throughout development this eventually turned in to a library full of [ES6](https://github.com/lukehoban/es6features) features. Coming from a Java background, its easier for me to design and write code in terms of classes, and I suspect this is true for many JavaScript beginners. While ES6 classes don't bring any new functionality to the language, they certainly do make it much easier to jump in to OOP with JavaScript, which is reason enough to warrent a new library, IMO.
33
34## Advantages
35So, why use Camo?
36
37- **ES6**: ES6 features are quickly being added to Node, especially now that it has merged with io.js. With all of these new features being released, Camo is getting a head start in writing tested and proven ES6 code. This also means that native Promises are built-in to Camo, so no more `promisify`-ing your ODM or waiting for Promise support to be added natively.
38- **Easy to use**: While JavaScript is a great language overall, it isn't always the easiest for beginners to pick up. Camo aims to ease that transition by providing familiar-looking classes and a simple interface. Also, there is no need to install a full MongoDB instance to get started thanks to the support of NeDB.
39- **Multiple backends**: Camo was designed and built with multiple Mongo-like backends in mind, like NeDB, LokiJS\*, and TaffyDB\*. With NeDB support, for example, you don't need to install a full MongoDB instance for development or for smaller projects. This also allows you to use Camo in the browser, since databases like NeDB supports in-memory storage.
40- **Lightweight**: Camo is just a very thin wrapper around the backend databases, which mean you won't be sacrificing performance.
41
42\* Support coming soon.
43
44## Install and Run
45To use Camo, you must first have installed **Node >2.0.x**, then run the following commands:
46
47 npm install camo --save
48
49And at least ONE of the following:
50
51 npm install nedb --save
52
53 OR
54
55 npm install mongodb --save
56
57## Quick Start
58Camo was built with ease-of-use and ES6 in mind, so you might notice it has more of an OOP feel to it than many existing libraries and ODMs. Don't worry, focusing on object-oriented design doesn't mean we forgot about functional techniques or asynchronous programming. Promises are built-in to the API. Just about every call you make interacting with the database (find, save, delete, etc) will return a Promise. No more callback hell :)
59
60For a short tutorial on using Camo, check out [this](http://stackabuse.com/getting-started-with-camo/) article.
61
62### Connect to the Database
63Before using any document methods, you must first connect to your underlying database. All supported databases have their own unique URI string used for connecting. The URI string usually describes the network location or file location of the database. However, some databases support more than just network or file locations. NeDB, for example, supports storing data in-memory, which can be specified to Camo via `nedb://memory`. See below for details:
64
65- MongoDB:
66 - Format: mongodb://[username:password@]host[:port][/db-name]
67 - Example: `var uri = 'mongodb://scott:abc123@localhost:27017/animals';`
68- NeDB:
69 - Format: nedb://[directory-path] OR nedb://memory
70 - Example: `var uri = 'nedb:///Users/scott/data/animals';`
71
72So to connect to an NeDB database, use the following:
73
74```javascript
75var connect = require('camo').connect;
76
77var database;
78var uri = 'nedb:///Users/scott/data/animals';
79connect(uri).then(function(db) {
80 database = db;
81});
82```
83
84### Declaring Your Document
85All models must inherit from the `Document` class, which handles much of the interface to your backend NoSQL database.
86
87```javascript
88var Document = require('camo').Document;
89
90class Company extends Document {
91 constructor() {
92 super();
93
94 this.name = String;
95 this.valuation = {
96 type: Number,
97 default: 10000000000,
98 min: 0
99 };
100 this.employees = [String];
101 this.dateFounded = {
102 type: Date,
103 default: Date.now
104 };
105 }
106
107 static collectionName() {
108 return 'companies';
109 }
110}
111```
112
113Notice how the schema is declared right in the constructor as member variables. All _public_ member variables (variables that don't start with an underscore [_]) are added to the schema.
114
115The name of the collection can be set by overriding the `static collectionName()` method, which should return the desired collection name as a string. If one isn't given, then Camo uses the name of the class and naively appends an 's' to the end to make it plural.
116
117Schemas can also be defined using the `this.schema()` method. For example, in the `constructor()` method you could use:
118
119```javascript
120this.schema({
121 name: String,
122 valuation: {
123 type: Number,
124 default: 10000000000,
125 min: 0
126 },
127 employees: [String],
128 dateFounded: {
129 type: Date,
130 default: Date.now
131 }
132});
133```
134
135Currently supported variable types are:
136
137- `String`
138- `Number`
139- `Boolean`
140- `Buffer`
141- `Date`
142- `Object`
143- `Array`
144- `EmbeddedDocument`
145- Document Reference
146
147Arrays can either be declared as either un-typed (using `Array` or `[]`), or typed (using the `[TYPE]` syntax, like `[String]`). Typed arrays are enforced by Camo on `.save()` and an `Error` will be thrown if a value of the wrong type is saved in the array. Arrays of references are also supported.
148
149To declare a member variable in the schema, either directly assign it one of the types listed above, or assign it an object with options, like this:
150
151```javascript
152this.primeNumber = {
153 type: Number,
154 default: 2,
155 min: 0,
156 max: 25,
157 choices: [2, 3, 5, 7, 11, 13, 17, 19, 23],
158 unique: true
159}
160```
161
162The `default` option supports both values and no-argument functions (like `Date.now`). Currently the supported options/validators are:
163
164- `type`: The value's type *(required)*
165- `default`: The value to be assigned if none is provided *(optional)*
166- `min`: The minimum value a Number can be *(optional)*
167- `max`: The maximum value a Number can be *(optional)*
168- `choices`: A list of possible values *(optional)*
169- `match`: A regex string that should match the value *(optional)*
170- `validate`: A 1-argument function that returns `false` if the value is invalid *(optional)*
171- `unique`: A boolean value indicating if a 'unique' index should be set *(optional)*
172- `required`: A boolean value indicating if a key value is required *(optional)*
173
174To reference another document, just use its class name as the type.
175
176```javascript
177class Dog extends Document {
178 constructor() {
179 super();
180
181 this.name = String;
182 this.breed = String;
183 }
184}
185
186class Person extends Document {
187 constructor() {
188 super();
189
190 this.pet = Dog;
191 this.name = String;
192 this.age = String;
193 }
194
195 static collectionName() {
196 return 'people';
197 }
198}
199```
200
201#### Embedded Documents
202Embedded documents can also be used within `Document`s. You must declare them separately from the main `Document` that it is being used in. `EmbeddedDocument`s are good for when you need an `Object`, but also need enforced schemas, validation, defaults, hooks, and member functions. All of the options (type, default, min, etc) mentioned above work on `EmbeddedDocument`s as well.
203
204```javascript
205var Document = require('camo').Document;
206var EmbeddedDocument = require('camo').EmbeddedDocument;
207
208class Money extends EmbeddedDocument {
209 constructor() {
210 super();
211
212 this.value = {
213 type: Number,
214 choices: [1, 5, 10, 20, 50, 100]
215 };
216
217 this.currency = {
218 type: String,
219 default: 'usd'
220 }
221 }
222}
223
224class Wallet extends Document {
225 constructor() {
226 super();
227 this.contents = [Money];
228 }
229}
230
231var wallet = Wallet.create();
232wallet.contents.push(Money.create());
233wallet.contents[0].value = 5;
234wallet.contents.push(Money.create());
235wallet.contents[1].value = 100;
236
237wallet.save().then(function() {
238 console.log('Both Wallet and Money objects were saved!');
239});
240````
241
242### Creating and Saving
243To create a new instance of our document, we need to use the `.create()` method, which handles all of the construction for us.
244
245```javascript
246var lassie = Dog.create({
247 name: 'Lassie',
248 breed: 'Collie'
249});
250
251lassie.save().then(function(l) {
252 console.log(l._id);
253});
254```
255
256Once a document is saved, it will automatically be assigned a unique identifier by the backend database. This ID can be accessed by the `._id` property.
257
258If you specified a default value (or function) for a schema variable, that value will be assigned on creation of the object.
259
260An alternative to `.save()` is `.findOneAndUpdate(query, update, options)`. This static method will find and update (or insert) a document in one atomic operation (atomicity is guaranteed in MongoDB only). Using the `{upsert: true}` option will return a new document if one is not found with the given query.
261
262### Loading
263Both the find and delete methods following closely (but not always exactly) to the MongoDB API, so it should feel fairly familiar.
264
265If querying an object by `id`, you _must_ use `_id` and **not** `id`.
266
267To retrieve an object, you have a few methods available to you.
268
269- `.findOne(query, options)` (static method)
270- `.find(query, options)` (static method)
271
272The `.findOne()` method will return the first document found, even if multiple documents match the query. `.find()` will return all documents matching the query. Each should be called as static methods on the document type you want to load.
273
274```javascript
275Dog.findOne({ name: 'Lassie' }).then(function(l) {
276 console.log('Got Lassie!');
277 console.log('Her unique ID is', l._id);
278});
279```
280
281`.findOne()` currently accepts the following option:
282
283- `populate`: Boolean value to load all or no references. Pass an array of field names to only populate the specified references
284 - `Person.findOne({name: 'Billy'}, {populate: true})` populates all references in `Person` object
285 - `Person.findOne({name: 'Billy'}, {populate: ['address', 'spouse']})` populates only 'address' and 'spouse' in `Person` object
286
287`.find()` currently accepts the following options:
288
289- `populate`: Boolean value to load all or no references. Pass an array of field names to only populate the specified references
290 - `Person.find({lastName: 'Smith'}, {populate: true})` populates all references in `Person` object
291 - `Person.find({lastName: 'Smith'}, {populate: ['address', 'spouse']})` populates only 'address' and 'spouse' in `Person` object
292- `sort`: Sort the documents by the given field(s)
293 - `Person.find({}, {sort: '-age'})` sorts by age in descending order
294 - `Person.find({}, {sort: ['age', 'name']})` sorts by ascending age and then name, alphabetically
295- `limit`: Limits the number of documents returned
296 - `Person.find({}, {limit: 5})` returns a maximum of 5 `Person` objects
297- `skip`: Skips the given number of documents and returns the rest
298 - `Person.find({}, {skip: 5})` skips the first 5 `Person` objects and returns all others
299
300### Deleting
301To remove documents from the database, use one of the following:
302
303- `.delete()`
304- `.deleteOne(query, options)` (static method)
305- `.deleteMany(query, options)` (static method)
306- `.findOneAndDelete(query, options)` (static method)
307
308The `.delete()` method should only be used on an instantiated document with a valid `id`. The other three methods should be used on the class of the document(s) you want to delete.
309
310```javascript
311Dog.deleteMany({ breed: 'Collie' }).then(function(numDeleted) {
312 console.log('Deleted', numDeleted, 'Collies from the database.');
313});
314```
315
316### Counting
317To get the number of matching documents for a query without actually retrieving all of the data, use the `.count()` method.
318
319```javascript
320Dog.count({ breed: 'Collie' }).then(function(count) {
321 console.log('Found', count, 'Collies.');
322});
323```
324
325### Hooks
326Camo provides hooks for you to execute code before and after critical parts of your database interactions. For each hook you use, you may return a value (which, as of now, will be discarded) or a Promise for executing asynchronous code. Using Promises throughout Camo allows us to not have to provide separate async and sync hooks, thus making your code simpler and easier to understand.
327
328Hooks can be used not only on `Document` objects, but `EmbeddedDocument` objects as well. The embedded object's hooks will be called when it's parent `Document` is saved/validated/deleted (depending on the hook you provide).
329
330In order to create a hook, you must override a class method. The hooks currently provided, and their corresponding methods, are:
331
332- pre-validate: `preValidate()`
333- post-validate: `postValidate()`
334- pre-save: `preSave()`
335- post-save: `postSave()`
336- pre-delete: `preDelete()`
337- post-delete: `postDelete()`
338
339Here is an example of using a hook (pre-delete, in this case):
340```javascript
341class Company extends Document {
342 constructor() {
343 super();
344
345 this.employees = [Person]
346 }
347
348 static collectionName() {
349 return 'companies';
350 }
351
352 preDelete() {
353 var deletes = [];
354 this.employees.forEach(function(e) {
355 var p = new Promise(function(resolve, reject) {
356 resolve(e.delete());
357 });
358
359 deletes.push(p);
360 });
361
362 return Promise.all(deletes);
363 }
364}
365```
366
367The code above shows a pre-delete hook that deletes all the employees of the company before it itself is deleted. As you can see, this is much more convenient than needing to always remember to delete referenced employees in the application code.
368
369**Note**: The `.preDelete()` and `.postDelete()` hooks are _only_ called when calling `.delete()` on a Document instance. Calling `.deleteOne()` or `.deleteMany()` will **not** trigger the hook methods.
370
371### Misc.
372- `camo.getClient()`: Retrieves the Camo database client
373- `camo.getClient().driver()`: Retrieves the underlying database driver (`MongoClient` or a map of NeDB collections)
374- `Document.toJSON()`: Serializes the given document to just the data, which includes nested and referenced data
375
376## Transpiler Support
377While many transpilers won't have any problem with Camo, some need extra resources/plugins to work correctly:
378
379- Babel
380 - [babel-preset-camo](https://github.com/scottwrobinson/babel-preset-camo): Babel preset for all es2015 plugins supported by Camo
381- TypeScript
382 - [DefinitelyTyped/camo](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/camo): Camo declaration file (h/t [lucasmciruzzi](https://github.com/lucasmciruzzi))
383 - [IndefinitivelyTyped/camo](https://github.com/IndefinitivelyTyped/camo): Typings support for Camo (h/t [WorldMaker](https://github.com/WorldMaker))
384
385## Contributing
386Feel free to open new issues or submit pull requests for Camo. If you'd like to contact me before doing so, feel free to get in touch (see Contact section below).
387
388Before opening an issue or submitting a PR, I ask that you follow these guidelines:
389
390**Issues**
391- Please state whether your issue is a question, feature request, or bug report.
392- Always try the latest version of Camo before opening an issue.
393- If the issue is a bug, be sure to clearly state your problem, what you expected to happen, and what all you have tried to resolve it.
394- Always try to post simplified code that shows the problem. Use Gists for longer examples.
395
396**Pull Requests**
397- If your PR is a new feature, please consult with me first.
398- Any PR should contain only one feature or bug fix. If you have more than one, please submit them as separate PRs.
399- Always try to include relevant tests with your PRs. If you aren't sure where a test should go or how to create one, feel free to ask.
400- Include updates to the README when needed.
401- Do not update the package version or CHANGELOG. I'll handle that for each release.
402
403## Contact
404You can contact me with questions, issues, or ideas at either of the following:
405
406- Email: [s.w.robinson+camo@gmail.com](mailto:s.w.robinson+camo@gmail.com)
407- Twitter: [@ScottWRobinson](https://twitter.com/ScottWRobinson)
408
409For short questions and faster responses, try Twitter.
410
411## Copyright & License
412Copyright (c) 2021 Scott Robinson
413
414Permission is hereby granted, free of charge, to any person obtaining a copy
415of this software and associated documentation files (the "Software"), to deal
416in the Software without restriction, including without limitation the rights
417to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
418copies of the Software, and to permit persons to whom the Software is
419furnished to do so, subject to the following conditions:
420
421The above copyright notice and this permission notice shall be included in
422all copies or substantial portions of the Software.
423
424THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.