From efe73ba42d10ed0c45c5510b81a2161374cd9157 Mon Sep 17 00:00:00 2001 From: Amine Ben hammou Date: Sat, 5 May 2018 02:09:42 +0100 Subject: [PATCH] initial commit --- .editorconfig | 11 + .gitignore | 3 + .travis.yml | 8 + LICENSE.md | 21 + README.md | 625 +++++++++++++++++++ package-lock.json | 1342 ++++++++++++++++++++++++++++++++++++++++ package.json | 48 ++ src/generate.js | 83 +++ src/index.js | 7 + src/lib.js | 47 ++ src/relation.js | 22 + src/sanctuary.js | 10 + src/schema/basic.js | 60 ++ src/schema/index.js | 4 + src/schema/mongoose.js | 29 + src/seed.js | 131 ++++ src/types/index.js | 6 + src/types/meta.js | 26 + src/types/mongoose.js | 11 + src/types/relation.js | 18 + src/types/schema.js | 78 +++ test/db.js | 95 +++ test/lib.test.js | 174 ++++++ test/schema.test.js | 321 ++++++++++ test/seed.test.js | 161 +++++ types.md | 98 +++ 26 files changed, 3439 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/generate.js create mode 100644 src/index.js create mode 100644 src/lib.js create mode 100644 src/relation.js create mode 100644 src/sanctuary.js create mode 100644 src/schema/basic.js create mode 100644 src/schema/index.js create mode 100644 src/schema/mongoose.js create mode 100644 src/seed.js create mode 100644 src/types/index.js create mode 100644 src/types/meta.js create mode 100644 src/types/mongoose.js create mode 100644 src/types/relation.js create mode 100644 src/types/schema.js create mode 100644 test/db.js create mode 100644 test/lib.test.js create mode 100644 test/schema.test.js create mode 100644 test/seed.test.js create mode 100644 types.md diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f35fc5c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +[**/*.js] +indent_style = space +indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..751f13d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +*.sublime-* +coverage diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..07c924f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +language: node_js +node_js: + - "8.11.0" + - "9.0.0" + - "10.0.0" +script: npm test +services: mongodb +after_success: npm run coveralls diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..d6bb392 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 Amine Ben hammou + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1689a87 --- /dev/null +++ b/README.md @@ -0,0 +1,625 @@ +# Wajez Utils + +[![Build Status](https://travis-ci.org/wajez/utils.svg?branch=master)](https://travis-ci.org/wajez/utils) +[![Coverage Status](https://coveralls.io/repos/github/wajez/utils/badge.svg)](https://coveralls.io/github/wajez/utils) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat)](https://github.com/wajez/utils/blob/master/LICENSE) + +A bunch of handy functions used to build other Wajez packages. + +# Contents + +- [Installation](#installation) + +- [Generating Random Data](#generating-random-data) + +- [Generating Mongoose Model](#generating-mongoose-model) + +- [Seeding a Mongoose Database](#seeding-a-mongoose-database) + +- [Converting Data](#converting-data) + + - [Full Example](#full-example) + + - [Converter can be a Function](#converter-can-be-a-function) + + - [Converter can be an Object](#converter-can-be-an-object) + + - [Converter maps over Arrays](#converter-maps-over-arrays) + + - [An Array of converters is a converter](#an-array-of-converters-is-a-converter) + + - [Converters are recursive](#converters-are-recursive) + +- [Contributing](#contributing) + + +# Installation + +``` +yarn add wajez-utils +``` +or +``` +npm i --save wajez-utils +``` + +# Generating Random Data + +``` +generate :: Schema -> (() -> *) +``` + +The function `generate` takes a `Schema` and returns a function which when called will return some random data based on the given schema. + +Schemas can be defined using the following functions: + +``` +string :: {minLength: Number, maxLength: Number, match: Regex, choices: [String]} -> Schema +number :: {min: Number, max: Number} -> Schema +boolean :: () -> Schema +buffer :: () -> Schema +date :: {min: Date, max: Date} -> Schema +array :: {schema: Schema, minLength: Number, maxLangth: Number} -> Schema +object :: {*: Schema} -> Schema +``` + +Here are some examples: + +```js +const U = require('wajez-utils') + +// Generate Strings +const anyString = U.generate(U.string()) +const yesOrNo = U.generate(U.string({choices: ['yes', 'no']})) +const email = U.generate(U.string({match: /[a-z0-9._+-]{1,20}@[a-z0-9]{3,15}\.[a-z]{2,4}/})) +const lessThen20Chars = U.generate(U.string({maxLength: 20})) + +console.log(anyString()) +// @RAJPF1y#FqM%!U(8ESsnr@PMM*c03NN^GRFwPY6*hhWNuwf +console.log(yesOrNo()) +// no +console.log(email()) +// h8mtue4vwr9@1nop1vu72qg.spjm +console.log(email()) +// fbv46d8d_xvdg6@upuc.cf +console.log(lessThen20Chars()) +// ^CRYr@bP + +// Generate Numbers +const anyNumber = U.generate(U.number()) +const rating = U.generate(U.number({min: 1, max: 5})) + +console.log(anyNumber()) +// -460.3763 +console.log(rating()) +// 4.6537 + +// Generate Booleans +const anyBoolean = U.generate(U.boolean()) +console.log(anyBoolean()) +// true +console.log(anyBoolean()) +// false + +// Generate Dates +const anyDate = U.generate(U.date()) +const inTheFuture = U.generate(U.date({min: new Date()})) + +console.log(anyDate()) +// 2018-10-11T02:09:58.000Z +console.log(inTheFuture()) +// 2018-10-31T01:46:48.000Z + +// Generate Buffers +const anyBuffer = U.generate(U.buffer()) +console.log(anyBuffer()) +// + +// Generate Objects & Arrays +const person = U.object({ + name: U.string({maxLength: 50}), + age: U.number({min: 0}) +}) + +const project = U.object({ + name: U.string({maxLength: 25}), + language: U.string({choices: ['javascript', 'c++', 'php', 'python']}) +}) + +// schemas are composable! +const developer = U.object({ + ... person.fields, + projects: U.array(project, {maxLength: 10}) +}) + +const newDeveloper = U.generate(developer) + +console.log(JSON.stringify(newDeveloper())) +// { +// "name":"9dMK&KKg@&mKPHDr6L]Md6HL$jqGAA]Z", +// "age":-741.6026, +// "projects": [{ +// "name":"xZL2YPl9eW]dsKYOQZB", +// "language":"python" +// },{ +// "name":")jE&17", +// "language":"php" +// }] +// } +``` + + +# Generating Mongoose Model + +The schema of a mongoose model can be defined using the `model` function: + +``` +model :: MongooseModel -> Schema +``` + + +```js +const mongoose = require('mongoose') +const {Schema} = mongoose +const U = require('wajez-utils') + +const Username = { + type: String, + match: /[a-zA-Z0-9-_\.]{5,20}/ +} + +const User = mongoose.model('User', new Schema({ + name: { + type: String, + minLength: 3, + maxLength: 25, + match: /^[a-z ]+$/ + }, + picture: Buffer, + since: { + type: Date, + max: new Date() + }, + rating: { + type: Number, + min: 0, + max: 5 + }, + links: { + facebook: Username, + twitter: Username, + github: Username + } +})) + +const generateUser = U.generate(U.model(User)) + +console.log(generateUser()) +// { +// name: 'gwyvehx raxilunaetkwdzwdwcpwdrpqvp mwpbcxwpku', +// picture: , +// since: 2018-02-12T04:31:15.000Z, +// rating: 1.9242, +// links: { +// facebook: 'VeBL6e-pU', +// twitter: 'gKGLqLV', +// github: 'vhIC6BDLvNf4MuHOj_' +// } +// } +``` + +# Seeding a Mongoose Database + +The function `seed` can be used to fill a mongodb database with random data based on `mongoose` models. + +``` +seed :: {model1: Number, model2: Number, ...} -> [Relation] -> Promise(*) +``` + +The first argument of `seed` is an object associating each model name to the number of records to generate and insert. The second argument is an array of `relations`. + +Relations can be defined using the functions `oneOne`, `oneMany` and `manyMany`. + +```js +oneOne(sourceModel, sourceField, targetModel, targetField) +oneMany(sourceModel, sourceField, targetModel, targetField) +manyMany(sourceModel, sourceField, targetModel, targetField) +``` + +Here is a full example + +```js +const mongoose = require('mongoose') +const {Schema} = mongoose +const {seed, oneOne, oneMany, manyMany} = require('./src') + +mongoose.Promise = global.Promise +mongoose.connect(`mongodb://localhost/wajez-utils`) + +const User = mongoose.model('User', new Schema({ + posts: [{ + type: Schema.Types.ObjectId, + ref: 'Post' + }], + profile: { + type: Schema.Types.ObjectId, + ref: 'Profile' + }, + name: String +})) + +const Profile = mongoose.model('Profile', new Schema({ + picture: Buffer +})) + +const Post = mongoose.model('Post', new Schema({ + writer: { + type: Schema.Types.ObjectId, + ref: 'User' + }, + tags: [{ + type: Schema.Types.ObjectId, + ref: 'Tag' + }], + title: String, + content: String +})) + +const Tag = mongoose.model('Tag', new Schema({ + name: String, + posts: [{ + type: Schema.Types.ObjectId, + ref: 'Post' + }] +})) + +seed({ + User: 3, + Profile: 3, + Post: 5, + Tag: 4 +}, [ + oneOne('User', 'profile', 'Profile', null), + oneMany('User', 'posts', 'Post', 'writer'), + manyMany('Post', 'tags', 'Tag', 'posts') +]) +.then(data => { + console.log(JSON.stringify(data)) +}) + +// { +// "User": [ +// { +// "posts": [ +// "5aed2cd384702a2207c3e2ed", +// "5aed2cd384702a2207c3e2ee", +// "5aed2cd384702a2207c3e2ef" +// ], +// "_id": "5aed2cd384702a2207c3e2e7", +// "profile": "5aed2cd384702a2207c3e2ea", +// "name": "7MthPn7WA)!3y@SjHigl]vFAK&xRo9FfsdMF)dK]#t#HiF@^s^wvFns", +// "__v": 0 +// }, +// { +// "posts": [ +// "5aed2cd384702a2207c3e2f0", +// "5aed2cd384702a2207c3e2f1" +// ], +// "_id": "5aed2cd384702a2207c3e2e8", +// "profile": "5aed2cd384702a2207c3e2eb", +// "name": "B]([^8*k6padMySnAI1MS%FX^LoLXAHbE&S)OmXw", +// "__v": 0 +// }, +// { +// "posts": [], +// "_id": "5aed2cd384702a2207c3e2e9", +// "profile": "5aed2cd384702a2207c3e2ec", +// "name": "!w3I84iTXYtSJdmVJX]c1[d6#*16nd1mqMiDP)g^&o6R053RnNcyxYr0c*5^X18St6RsquCAorFq)KlHWs", +// "__v": 0 +// } +// ], +// "Profile": [ +// { +// "_id": "5aed2cd384702a2207c3e2ea", +// "picture": { +// "type": "Buffer", +// "data": [52, 53, 97, 105, 67, 114, 42, 99, 41, 118, 40, 38, 42, 35, 78] +// }, +// "__v": 0 +// }, +// { +// "_id": "5aed2cd384702a2207c3e2eb", +// "picture": { +// "type": "Buffer", +// "data": [51, 98, 78, 33, 76, 86] +// }, +// "__v": 0 +// }, +// { +// "_id": "5aed2cd384702a2207c3e2ec", +// "picture": { +// "type": "Buffer", +// "data": [69, 69, 48, 37, 66, 87, 33, 83, 93, 89, 38, 67, 82] +// }, +// "__v": 0 +// } +// ], +// "Post": [ +// { +// "tags": [ +// "5aed2cd384702a2207c3e2f2", +// "5aed2cd384702a2207c3e2f4" +// ], +// "_id": "5aed2cd384702a2207c3e2ed", +// "writer": "5aed2cd384702a2207c3e2e7", +// "title": "3F]K8JQnIXsne(whhGn%U*YyrA0vC^pC%pxhKwGU]FAivdhDznMri*Ip&]HT1nY[%DpDegCoBW", +// "content": "rp", +// "__v": 0 +// }, +// { +// "tags": [ +// "5aed2cd384702a2207c3e2f3", +// "5aed2cd384702a2207c3e2f5" +// ], +// "_id": "5aed2cd384702a2207c3e2ee", +// "writer": "5aed2cd384702a2207c3e2e7", +// "title": "^8Nkp8B)JHu(YV0s**InJof@J!JGBhlYy6wuQCc#sb^d[K)C]b8jg)PnEDxF#[JFT#]ABT4x%vgWw8CWssemwvmODFSJVdd", +// "content": "D3cdhdg)hO$@ydIx)T!!St1vy!BfYP1TK2A5$#$g*@o)Qk(xF78a8V5H(QdgokP08&A(mf*tmER6PzoNi(6CeXxn$W%V", +// "__v": 0 +// }, +// { +// "tags": [ +// "5aed2cd384702a2207c3e2f2", +// "5aed2cd384702a2207c3e2f3" +// ], +// "_id": "5aed2cd384702a2207c3e2ef", +// "writer": "5aed2cd384702a2207c3e2e7", +// "title": "C[dbHT!DaP9KTi3qWpbWYLqIMzZOJE9p[](M0cNnDmnc7h%p#)vzcU%O%l2Dq8YIx0f5eLZe(vFFV#72k0dP6", +// "content": "(#q#xY)osPBQOTb27h$jPYh[hq38lm37XO$ZZh&zb!zF!2^", +// "__v": 0 +// }, +// { +// "tags": [ +// "5aed2cd384702a2207c3e2f2", +// "5aed2cd384702a2207c3e2f3", +// "5aed2cd384702a2207c3e2f4" +// ], +// "_id": "5aed2cd384702a2207c3e2f0", +// "writer": "5aed2cd384702a2207c3e2e8", +// "title": "xGVK&Z%Pf%t@kkozDez[VTedKekVqdnHFj]JnuD@FbrW2R9dhLGIu(oShe9ngv]RY0sV4l!u&tXNh%S@Bl8n**)A0ArOblrEh", +// "content": "tcNQ$I4oh#fqv2xrh]ioCMQKYB8eXI#IA9xuz4MVDi4*(HKhBe8SIWxTglIR[DAkwWB", +// "__v": 0 +// }, +// { +// "tags": [ +// "5aed2cd384702a2207c3e2f2", +// "5aed2cd384702a2207c3e2f3", +// "5aed2cd384702a2207c3e2f4" +// ], +// "_id": "5aed2cd384702a2207c3e2f1", +// "writer": "5aed2cd384702a2207c3e2e8", +// "title": "Lsfm!l)QzkeBUnKBnKgrQ4EpeEMPuT@1GRL$(x#2W]WgaS0TQsuuoIgVnJIa3lJIRFWoVXD(dNjn6fDQ0kvRc&1oEqm!09", +// "content": "iN7J[oJh7TDI#&*XX6qK7no!9^OwcQdGlMzcjueDVKwQQrAJIIXENvgHHlx3gfLjo%)&btAqGfLJqz", +// "__v": 0 +// } +// ], +// "Tag": [ +// { +// "posts": [ +// "5aed2cd384702a2207c3e2ed", +// "5aed2cd384702a2207c3e2ef", +// "5aed2cd384702a2207c3e2f0", +// "5aed2cd384702a2207c3e2f1" +// ], +// "_id": "5aed2cd384702a2207c3e2f2", +// "name": "MhT4[w1cVzrNc(%cgJq%A*GknXU%[r7y%#vi$5MZqQTOwKgEAwno76HDoM5V", +// "__v": 0 +// }, +// { +// "posts": [ +// "5aed2cd384702a2207c3e2ee", +// "5aed2cd384702a2207c3e2ef", +// "5aed2cd384702a2207c3e2f0", +// "5aed2cd384702a2207c3e2f1" +// ], +// "_id": "5aed2cd384702a2207c3e2f3", +// "name": "BQl2c9ozL39^J$Re@H*ida&!5]V", +// "__v": 0 +// }, +// { +// "posts": [ +// "5aed2cd384702a2207c3e2ed", +// "5aed2cd384702a2207c3e2f0", +// "5aed2cd384702a2207c3e2f1" +// ], +// "_id": "5aed2cd384702a2207c3e2f4", +// "name": "vgsHTSPoivaAb^)(vE!33G)P8CdHHeNUiA0DdY9a$JiOKHH!5YZCACC3Y&zHbr64xECMdGxf5]dPi[H", +// "__v": 0 +// }, +// { +// "posts": [ +// "5aed2cd384702a2207c3e2ee" +// ], +// "_id": "5aed2cd384702a2207c3e2f5", +// "name": "f#(BSFM)Ez749b7IJW5wyyuu$jVgcMRd3NQ7OX0RYXTYoAy", +// "__v": 0 +// } +// ] +// } +``` + + +# Converting Data + +The function `applyConverter` can be used to apply a converter on data. A converter is an object which specifies what function to apply on each property of the given data. + +We will start with a full example, then explain different ways to use `applyConverter`. + +## Full Example +Let's assume I have the following user infos returned from database + +```js +const user = { + _id: 'xxxxxxx', + _v: 1, + username: 'webneat', + password: 'myVerySecurePassword :P', + profile: { + _id: 'yyyyyyy', + _v: 1, + firstname: 'Amine', + lastname: 'Ben hammou', + picture: 'some-url', + }, + repos: [ + 'webNeat/lumen-generators', + 'tarsana/command', + 'wajez/api' + ] +} +``` + +I want to transform it into something like this + +```js +{ + username: 'webneat', + fullName: 'Amine Ben hammou', + picture: 'some-url', + repos: [ + { + name: 'lumen-generators', + url: 'https://github.com/webNeat/lumen-generators' + }, + { + name: 'command', + url: 'https://github.com/tarsana/command' + }, + { + name: 'api', + url: 'https://github.com/wajez/api' + } + ] +} +``` + +I would simply do the following + +```js +const {applyConverter, I} = require('./src') + +const fullName = ({profile: {firstname, lastname}}) => firstname + ' ' + lastname +const repo = name => ({ + name: name.split('/')[1], + url: `https://github.com/${name}` +}) + +const convert = applyConverter({ + username: I, + fullName: fullName, + picture: _ => _.profile.picture, + repos: repo +}) + +console.log(convert(user)) +// { +// username: 'webneat', +// fullName: 'Amine Ben hammou', +// picture: 'some-url', +// repos: [ +// { name: 'lumen-generators', url: 'https://github.com/webNeat/lumen-generators' }, +// { name: 'command', url: 'https://github.com/tarsana/command' }, +// { name: 'api', url: 'https://github.com/wajez/api' } +// ] +// } + +``` + +## Converter can be a Function + +When given a `Function` as converter, `applyConverter` will simply apply that function on the value and return the result. + +```js +applyConverter(x => x + 1, 5) // 6 +``` + +## Converter can be an Object + +When given an object of functions, it will try to apply each function to the corresponding attribute on the value. + +```js +const addOne = _ => _ + 1 +const triple = _ => _ * 3 + +const value = { x: 1, y: 2, z: 3 } + +applyConverter({ x: triple, y: addOne }, value) // {x: 3, y: 3} +``` + +Note that since the key `z` is not mentioned in the converter, it's not included in the result. + +If we metion an key which does not exist in the value, the function will be applied to the whole value. + +```js +const sum = ({x, y, z}) => x + y + z + +const value = { x: 1, y: 2, z: 3 } + +applyConverter({a: sum}, value) // {a: 6} +``` + +## Converter maps over Arrays + +When applying a converter to an array, it's applied to each item of the array + +```js +const sum = ({x, y}) => x + y + +const values = [ + { x: 1, y: 2 }, + { x: 3, y: 4 }, + { x: 5, y: 6 }, +] + +applyConverter({a: sum}, values) // [{a: 3}, {a: 7}, {a: 11}] +``` + +## An Array of converters is a converter + +```js +const addOne = _ => _ + 1 +const triple = _ => _ * 3 +const sum = ({x, y}) => x + y + +const value = {x: 1, y: 2} + +applyConverter( + [ + {x: addOne, y: triple}, + sum + ], + value +) // 8 +``` + +## Converters are recursive + +When defining a converter as object, are applied as converters, so they can be functions, objects of converters or array of converters. + +```js + +const value = { + x: { + a: 1, + y: { + z: 2 + } + } +} + +applyConverter({x: {y: {z: addOne}}}, value) // {x: {y: {z: 3}}} +``` + +# Contributing + +Feel free to create issues and/or submit Pull Requests! diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c7c04dd --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1342 @@ +{ + "name": "wajez-utils", + "version": "0.0.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "abbrev": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", + "dev": true + }, + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "dev": true, + "requires": { + "co": "4.6.0", + "fast-deep-equal": "1.1.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" + } + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "dev": true, + "requires": { + "kind-of": "3.2.2", + "longest": "1.0.1", + "repeat-string": "1.6.1" + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "1.0.3" + } + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", + "dev": true + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "async": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.1.4.tgz", + "integrity": "sha1-LSFgx3iAMuTdbL4lAvH5osj2zeQ=", + "requires": { + "lodash": "4.17.10" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", + "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "dev": true, + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "bluebird": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz", + "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw=" + }, + "boom": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", + "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", + "dev": true, + "requires": { + "hoek": "4.2.1" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "bson": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.6.tgz", + "integrity": "sha512-D8zmlb46xfuK2gGvKmUjIklQEouN2nQ0LEHHeZ/NoHM2LDiMk2EYzZ5Ntw/Urk+bgMDosOZxaRzXxvhI5TcAVQ==" + }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "dev": true, + "optional": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "dev": true, + "optional": true, + "requires": { + "align-text": "0.1.4", + "lazy-cache": "1.0.4" + } + }, + "chai": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", + "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", + "dev": true, + "requires": { + "assertion-error": "1.1.0", + "check-error": "1.0.2", + "deep-eql": "3.0.1", + "get-func-name": "2.0.0", + "pathval": "1.1.0", + "type-detect": "4.0.8" + } + }, + "chance": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/chance/-/chance-1.0.16.tgz", + "integrity": "sha512-2bgDHH5bVfAXH05SPtjqrsASzZ7h90yCuYT2z4mkYpxxYvJXiIydBFzVieVHZx7wLH1Ag2Azaaej2/zA1XUrNQ==" + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "dev": true, + "optional": true, + "requires": { + "center-align": "0.1.3", + "right-align": "0.1.3", + "wordwrap": "0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "dev": true, + "optional": true + } + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "dev": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, + "commander": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", + "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "coveralls": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-3.0.1.tgz", + "integrity": "sha512-FAzXwiDOYLGDWH+zgoIA+8GbWv50hlx+kpEJyvzLKOdnIBv9uWoVl4DhqGgyUHpiRjAlF8KYZSipWXYtllWH6Q==", + "dev": true, + "requires": { + "js-yaml": "3.11.0", + "lcov-parse": "0.0.10", + "log-driver": "1.2.7", + "minimist": "1.2.0", + "request": "2.85.0" + } + }, + "cryptiles": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", + "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", + "dev": true, + "requires": { + "boom": "5.2.0" + }, + "dependencies": { + "boom": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", + "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", + "dev": true, + "requires": { + "hoek": "4.2.1" + } + } + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "1.0.0" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true, + "optional": true + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "drange": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/drange/-/drange-1.0.1.tgz", + "integrity": "sha512-PVkrAwra9MnIY6QIa9YMlEHkfbcikjK+W/X/O0BNXG14y3O8vqGhzvP8TKlu4sFCJn7V2raugY8SOjDXVHti0g==" + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "dev": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "escodegen": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", + "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", + "dev": true, + "requires": { + "esprima": "2.7.3", + "estraverse": "1.9.3", + "esutils": "2.0.2", + "optionator": "0.8.2", + "source-map": "0.2.0" + }, + "dependencies": { + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + } + } + }, + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", + "dev": true + }, + "estraverse": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", + "dev": true + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "dev": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.6", + "mime-types": "2.1.18" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "1.0.0" + } + }, + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "growl": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz", + "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==", + "dev": true + }, + "handlebars": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", + "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", + "dev": true, + "requires": { + "async": "1.5.2", + "optimist": "0.6.1", + "source-map": "0.4.4", + "uglify-js": "2.8.29" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true + }, + "har-validator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "dev": true, + "requires": { + "ajv": "5.5.2", + "har-schema": "2.0.0" + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "hawk": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", + "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", + "dev": true, + "requires": { + "boom": "4.3.1", + "cryptiles": "3.1.2", + "hoek": "4.2.1", + "sntp": "2.1.0" + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "hoek": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", + "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", + "dev": true + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "jsprim": "1.4.1", + "sshpk": "1.14.1" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "istanbul": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", + "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", + "dev": true, + "requires": { + "abbrev": "1.0.9", + "async": "1.5.2", + "escodegen": "1.8.1", + "esprima": "2.7.3", + "glob": "5.0.15", + "handlebars": "4.0.11", + "js-yaml": "3.11.0", + "mkdirp": "0.5.1", + "nopt": "3.0.6", + "once": "1.4.0", + "resolve": "1.1.7", + "supports-color": "3.2.3", + "which": "1.3.0", + "wordwrap": "1.0.0" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + } + } + }, + "js-yaml": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.11.0.tgz", + "integrity": "sha512-saJstZWv7oNeOyBh3+Dx1qWzhW0+e6/8eDzo7p5rDFqxntSztloLtuKu+Ejhtq82jsilwOIZYsCz+lIjthg1Hw==", + "dev": true, + "requires": { + "argparse": "1.0.10", + "esprima": "4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true, + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "kareem": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.0.7.tgz", + "integrity": "sha512-p8+lEpsNs4N0fvNOC1/zzDO0wDrD3Pb1G+OwfIG+gKVK3MyY5jeaGYh+9Qx6jb4fEG2b3E6U98vaE9MH7Gilsw==" + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "dev": true, + "optional": true + }, + "lcov-parse": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-0.0.10.tgz", + "integrity": "sha1-GwuP+ayceIklBYK3C3ExXZ2m2aM=", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "1.1.2", + "type-check": "0.3.2" + } + }, + "lodash": { + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + }, + "log-driver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", + "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==", + "dev": true + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "dev": true + }, + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "dev": true + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "dev": true, + "requires": { + "mime-db": "1.33.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, + "mocha": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.1.1.tgz", + "integrity": "sha512-kKKs/H1KrMMQIEsWNxGmb4/BGsmj0dkeyotEvbrAuQ01FcWRLssUNXCEUZk6SZtyJBi6EE7SL0zDDtItw1rGhw==", + "dev": true, + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.11.0", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.3", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "4.4.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "supports-color": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", + "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, + "mongodb": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.0.7.tgz", + "integrity": "sha512-n/14kMJEoARXz1qhpNPhUocqy+z5130jhqgEIX1Tsl8UVpHrndQ8et+VmgC4yPK/I8Tcgc93JEMQCHTekBUnNA==", + "requires": { + "mongodb-core": "3.0.7" + } + }, + "mongodb-core": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-3.0.7.tgz", + "integrity": "sha512-z6YufO7s40wLiv2ssFshqoLS4+Kf+huhHq6KZ7gDArsKNzXYjAwTMnhEIJ9GQ8fIfBGs5tBLNPfbIDoCKGPmOw==", + "requires": { + "bson": "1.0.6", + "require_optional": "1.0.1" + } + }, + "mongoose": { + "version": "5.0.17", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.0.17.tgz", + "integrity": "sha512-RV1WBQhzW7oOhStR+s7LQYfgQWTJm4hgmU3TqtgTiBCfnj5/sNliX2/SY+ef7tpIZRUqEBV5xITZdAlwQ6Ymdg==", + "requires": { + "async": "2.1.4", + "bson": "1.0.6", + "kareem": "2.0.7", + "lodash.get": "4.4.2", + "mongodb": "3.0.7", + "mongoose-legacy-pluralize": "1.0.2", + "mpath": "0.4.1", + "mquery": "3.0.0", + "ms": "2.0.0", + "regexp-clone": "0.0.1", + "sliced": "1.0.1" + } + }, + "mongoose-legacy-pluralize": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz", + "integrity": "sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ==" + }, + "mpath": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.4.1.tgz", + "integrity": "sha512-NNY/MpBkALb9jJmjpBlIi6GRoLveLUM0pJzgbp9vY9F7IQEb/HREC/nxrixechcQwd1NevOhJnWWV8QQQRE+OA==" + }, + "mquery": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-3.0.0.tgz", + "integrity": "sha512-WL1Lk8v4l8VFSSwN3yCzY9TXw+fKVYKn6f+w86TRzOLSE8k1yTgGaLBPUByJQi8VcLbOdnUneFV/y3Kv874pnQ==", + "requires": { + "bluebird": "3.5.0", + "debug": "2.6.9", + "regexp-clone": "0.0.1", + "sliced": "0.0.5" + }, + "dependencies": { + "sliced": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/sliced/-/sliced-0.0.5.tgz", + "integrity": "sha1-XtwETKTrb3gW1Qui/GPiXY/kcH8=" + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "requires": { + "abbrev": "1.0.9" + } + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "0.0.10", + "wordwrap": "0.0.3" + }, + "dependencies": { + "minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", + "dev": true + }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true + } + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "0.1.3", + "fast-levenshtein": "2.0.6", + "levn": "0.3.0", + "prelude-ls": "1.1.2", + "type-check": "0.3.2", + "wordwrap": "1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + }, + "randexp": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.9.tgz", + "integrity": "sha512-maAX1cnBkzIZ89O4tSQUOF098xjGMC8N+9vuY/WfHwg87THw6odD2Br35donlj5e6KnB1SB0QBHhTQhhDHuTPQ==", + "requires": { + "drange": "1.0.1", + "ret": "0.2.2" + } + }, + "regexp-clone": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-0.0.1.tgz", + "integrity": "sha1-p8LgmJH9vzj7sQ03b7cwA+aKxYk=" + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "request": { + "version": "2.85.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.85.0.tgz", + "integrity": "sha512-8H7Ehijd4js+s6wuVPLjwORxD4zeuyjYugprdOXlPSqaApmL/QOy+EB/beICHVCHkGMKNh5rvihb5ov+IDw4mg==", + "dev": true, + "requires": { + "aws-sign2": "0.7.0", + "aws4": "1.7.0", + "caseless": "0.12.0", + "combined-stream": "1.0.6", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.3.2", + "har-validator": "5.0.3", + "hawk": "6.0.2", + "http-signature": "1.2.0", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.18", + "oauth-sign": "0.8.2", + "performance-now": "2.1.0", + "qs": "6.5.2", + "safe-buffer": "5.1.2", + "stringstream": "0.0.5", + "tough-cookie": "2.3.4", + "tunnel-agent": "0.6.0", + "uuid": "3.2.1" + } + }, + "require_optional": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", + "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", + "requires": { + "resolve-from": "2.0.0", + "semver": "5.5.0" + } + }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + }, + "resolve-from": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", + "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" + }, + "ret": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", + "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==" + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "dev": true, + "optional": true, + "requires": { + "align-text": "0.1.4" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "sanctuary": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/sanctuary/-/sanctuary-0.14.1.tgz", + "integrity": "sha512-+oWyyObmQUVzx99YdVj4XGKRWGXWYGZM/R/HEO6VtiSZuJ5FpmPa9HHw9NYYk1Gru5Bgu2IB/mlR+vj8IisxNw==", + "requires": { + "sanctuary-def": "0.14.0", + "sanctuary-type-classes": "7.1.1", + "sanctuary-type-identifiers": "2.0.1" + } + }, + "sanctuary-def": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/sanctuary-def/-/sanctuary-def-0.14.0.tgz", + "integrity": "sha512-pn60l2S29xeic7/7bsWgQo9mia+i/jSSDZv2GDdoNnx89GiF5nbefAZM0AbCmXG1PUgXprogMQBxM+4wLX97dQ==", + "requires": { + "sanctuary-type-classes": "7.1.1", + "sanctuary-type-identifiers": "2.0.1" + } + }, + "sanctuary-type-classes": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/sanctuary-type-classes/-/sanctuary-type-classes-7.1.1.tgz", + "integrity": "sha512-1qbKw1lCcdlXv6SSMT3AIUkBdgpF+bsujBIM2/DW/2muWmac0Jg36YyQUUCxLq+z6gMPI/IQWW2YgANSKH7fnQ==", + "requires": { + "sanctuary-type-identifiers": "1.0.0" + }, + "dependencies": { + "sanctuary-type-identifiers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/sanctuary-type-identifiers/-/sanctuary-type-identifiers-1.0.0.tgz", + "integrity": "sha1-6PNZ8AbLXmJM+4RkYD/BFGCL3p8=" + } + } + }, + "sanctuary-type-identifiers": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/sanctuary-type-identifiers/-/sanctuary-type-identifiers-2.0.1.tgz", + "integrity": "sha1-/FJM9t2Szr/LsN2VCe/xkxWaIO0=" + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" + }, + "sliced": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", + "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" + }, + "sntp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", + "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", + "dev": true, + "requires": { + "hoek": "4.2.1" + } + }, + "source-map": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", + "dev": true, + "optional": true, + "requires": { + "amdefine": "1.0.1" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz", + "integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=", + "dev": true, + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + } + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", + "dev": true + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "1.0.0" + } + }, + "tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "dev": true, + "requires": { + "punycode": "1.4.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true, + "optional": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "1.1.2" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "dev": true, + "optional": true, + "requires": { + "source-map": "0.5.7", + "uglify-to-browserify": "1.0.2", + "yargs": "3.10.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, + "optional": true + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "dev": true, + "optional": true + }, + "uuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", + "dev": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + } + }, + "which": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", + "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", + "dev": true, + "requires": { + "isexe": "2.0.0" + } + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "dev": true, + "optional": true + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dev": true, + "optional": true, + "requires": { + "camelcase": "1.2.1", + "cliui": "2.1.0", + "decamelize": "1.2.0", + "window-size": "0.1.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..574d82a --- /dev/null +++ b/package.json @@ -0,0 +1,48 @@ +{ + "name": "wajez-utils", + "version": "0.0.1", + "description": "Handy utils for api development.", + "main": "index.js", + "scripts": { + "test": "mocha --recursive", + "coveralls": "istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- --recursive -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage" + }, + "engines": { + "node": ">=8.11.0" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wajez/utils.git" + }, + "keywords": [ + "utils", + "schema", + "mongodb", + "mongoose", + "seeder", + "generate", + "random", + "data", + "convert", + "transform" + ], + "author": "Amine Ben hammou", + "license": "MIT", + "bugs": { + "url": "https://github.com/wajez/utils/issues" + }, + "homepage": "https://github.com/wajez/utils#readme", + "dependencies": { + "chance": "^1.0.16", + "escape-string-regexp": "^1.0.5", + "mongoose": "^5.0.17", + "randexp": "^0.4.9", + "sanctuary": "^0.14.1" + }, + "devDependencies": { + "chai": "^4.1.2", + "coveralls": "^3.0.1", + "istanbul": "^0.4.5", + "mocha": "^5.1.1" + } +} diff --git a/src/generate.js b/src/generate.js new file mode 100644 index 0000000..99039f5 --- /dev/null +++ b/src/generate.js @@ -0,0 +1,83 @@ +const chance = new (require('chance')) +const {randexp} = require('randexp') +const escapeStringRegexp = require('escape-string-regexp') +const {$, def, S} = require('./sanctuary') +const T = require('./types') + +const string = def('generateString', {}, [T.StringSchema, $.String], + ({choices, match, minLength, maxLength}) => { + if (choices) + match = choices.map(escapeStringRegexp).join('|') + if (match) + return randexp(match) + const length = chance.integer({min: minLength, max: maxLength}) + return chance.string({length}) + } +) + +const number = def('generateNumber', {}, [T.NumberSchema, $.Number], + ({min, max}) => chance.floating({min, max}) +) + +const boolean = def('generateBoolean', {}, [T.BooleanSchema, $.Boolean], + () => chance.bool() +) + +const date = def('generateDate', {}, [T.DateSchema, $.Date], + ({min, max}) => { + if (! min) { + min = new Date() + min.setFullYear(min.getFullYear() - 1) + } + if (! max) { + max = new Date() + max.setFullYear(max.getFullYear() + 1) + } + + min = Math.floor(min.getTime() / 1000) + max = Math.floor(max.getTime() / 1000) + + return new Date(1000 * chance.integer({min, max})) + } +) + +const buffer = def('generateBuffer', {}, [T.BufferSchema, $.Any], + () => Buffer.from(chance.string()) +) + +const object = def('generateObject', {}, [T.ObjectSchema, $.Any], + ({fields}) => { + const result = {} + for(const name in fields) + result[name] = generate(fields[name])() + return result + } +) + +const array = def('generateArray', {}, [T.ArraySchema, $.Array($.Any)], + ({schema, minLength, maxLength}) => { + const length = chance.integer({min: minLength, max: maxLength}) + , items = [] + , get = generate(schema) + if (get() == null) + return [] + let i = 0 + while (i < length) { + items.push(get()) + i ++ + } + return items + } +) + +const generate = schema => () => + (schema.type === 'date') ? date(schema) : + (schema.type === 'array') ? array(schema) : + (schema.type === 'string') ? string(schema) : + (schema.type === 'number') ? number(schema) : + (schema.type === 'buffer') ? buffer(schema) : + (schema.type === 'object') ? object(schema) : + (schema.type === 'boolean') ? boolean(schema) : + null + +module.exports = {generate} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..f37d2e8 --- /dev/null +++ b/src/index.js @@ -0,0 +1,7 @@ +module.exports = { + ...(require('./generate')), + ...(require('./lib')), + ...(require('./relation')), + ...(require('./schema')), + ...(require('./seed')), +} diff --git a/src/lib.js b/src/lib.js new file mode 100644 index 0000000..4084f43 --- /dev/null +++ b/src/lib.js @@ -0,0 +1,47 @@ +const {$, def, S} = require('./sanctuary') +const T = require('./types') + +const I = _ => _ + +const id = _ => (_.id === undefined ? _ : _.id).toString('hex') + +const merge = def('merge', {}, [$.StrMap($.Any), $.StrMap($.Any), $.StrMap($.Any)], + (a, b) => { + const keysA = Object.keys(a) + const keysB = Object.keys(b) + return keysA.concat(keysB).reduce((result, key) => { + const valueA = a[key] + const valueB = b[key] + if (S.is(Object, valueA) && S.is(Object, valueB)) + result[key] = merge(valueA, valueB) + else + result[key] = (valueB === undefined) ? valueA : valueB + return result + }, {}) + } +) + +const mapObj = def('mapObj', {}, [$.AnyFunction, $.Object, $.Object], + (fn, obj) => Object.keys(obj).reduce((result, key) => { + result[key] = fn(obj[key]) + return result + }, {}) +) + +const applySpec = def('applySpec', {}, [$.StrMap($.AnyFunction), $.Any, $.Any], + (spec, value) => S.sequence(Function, spec)(value) +) + +const applyConverter = def('applyConverter', {}, [$.Any, $.Any, $.Any], + (converter, value) => + value === undefined ? undefined : + value.constructor === Array ? value.map(_ => applyConverter(converter, _)) : + converter.constructor === Array ? converter.reduce((v, c) => applyConverter(c, v), value) : + S.is(Function, converter) ? converter(value) : + Object.keys(converter).reduce((result, key) => { + result[key] = applyConverter(converter[key], value[key] || value) + return result + }, {}) +) + +module.exports = {I, id, merge, mapObj, applySpec, applyConverter} diff --git a/src/relation.js b/src/relation.js new file mode 100644 index 0000000..771c7c8 --- /dev/null +++ b/src/relation.js @@ -0,0 +1,22 @@ +const {$, def, S} = require('./sanctuary') +const T = require('./types') + +const relation = def('relation', {}, [T.RelationType, $.String, $.Nullable($.String), $.String, $.Nullable($.String), T.Relation], + (type, sourceName, sourceField, targetName, targetField) => ({ + type, + source: { + name: sourceName, + field: sourceField + }, + target: { + name: targetName, + field: targetField + } + }) +) + +const oneOne = relation('one-one') +const oneMany = relation('one-many') +const manyMany = relation('many-many') + +module.exports = {relation, oneOne, oneMany, manyMany} diff --git a/src/sanctuary.js b/src/sanctuary.js new file mode 100644 index 0000000..aba607c --- /dev/null +++ b/src/sanctuary.js @@ -0,0 +1,10 @@ +const $ = require ('sanctuary-def') +const {create, env} = require('sanctuary') + +const checkTypes = process.env.NODE_ENV !== 'production' + +module.exports = { + $, + S: create({checkTypes, env}), + def: $.create({checkTypes, env: $.env}), +} diff --git a/src/schema/basic.js b/src/schema/basic.js new file mode 100644 index 0000000..d6ba671 --- /dev/null +++ b/src/schema/basic.js @@ -0,0 +1,60 @@ +const {$, def, S} = require('../sanctuary') +const T = require('../types') + +const string = ({choices, match, minLength, maxLength} = {}) => ({ + type: 'string', + choices: choices || null, + match: match || null, + minLength: minLength || 0, + maxLength: maxLength || 100 +}) + +const number = ({min, max} = {}) => ({ + type: 'number', + min: (min !== undefined) ? min : -100, + max: (max !== undefined) ? max : 100 +}) + +const _boolean = {type: 'boolean'} +const boolean = () => _boolean + +const date = ({min, max} = {}) => ({ + type: 'date', + min: min || null, + max: max || null +}) + +const _buffer = {type: 'buffer'} +const buffer = () => _buffer + +const object = fields => ({ + type: 'object', + fields: fields || {} +}) + +const array = (schema, {minLength, maxLength} = {}) => ({ + type: 'array', + schema: schema || object(), + minLength: minLength || 0, + maxLength: maxLength || 100 +}) + +const reference = name => ({ + type: 'reference', + name +}) + +const _unknown = {type: 'unknown'} +const unknown = () => _unknown + +module.exports = { + string, + number, + boolean, + date, + buffer, + object, + array, + reference, + unknown, +} diff --git a/src/schema/index.js b/src/schema/index.js new file mode 100644 index 0000000..489d5dc --- /dev/null +++ b/src/schema/index.js @@ -0,0 +1,4 @@ +module.exports = { + ...(require('./basic')), + ...(require('./mongoose')), +} diff --git a/src/schema/mongoose.js b/src/schema/mongoose.js new file mode 100644 index 0000000..61db990 --- /dev/null +++ b/src/schema/mongoose.js @@ -0,0 +1,29 @@ +const {Schema} = require('mongoose') +const {$, def, S} = require('../sanctuary') +const T = require('../types') +const {merge, mapObj} = require('../lib') +const { + string, number, boolean, date, buffer, + object, array, reference, unknown +} = require('./basic') + +const field = def('field', {}, [$.Any, T.Schema], + value => + (value == undefined) ? unknown() : + (value === String || value.type === String) ? string(value.type != undefined ? merge(value, {choices: value.enum}) : {}) : + (value === Number || value.type === Number) ? number(value.type != undefined ? value : {}) : + (value === Date || value.type === Date) ? date(value.type != undefined ? value : {}) : + (value === Boolean || value.type === Boolean) ? boolean() : + (value === Buffer || value.type === Buffer) ? buffer() : + (value.type === Schema.Types.ObjectId) ? (value.ref !== undefined ? reference(value.ref) : unknown()) : + (value.constructor === Array && value.length > 0) ? array(field(value[0]), {}) : + (value === Object || value.type === Object) ? object({}) : + S.is(Object, value) ? object(mapObj(field, value)) : + unknown() +) + +const model = def('model', {}, [T.MongooseModel, T.Schema], + model => object(mapObj(field, model.schema.obj)) +) + +module.exports = {model} diff --git a/src/seed.js b/src/seed.js new file mode 100644 index 0000000..cd60900 --- /dev/null +++ b/src/seed.js @@ -0,0 +1,131 @@ +const mongoose = require('mongoose') +const {$, def, S} = require('./sanctuary') +const T = require('./types') +const {model} = require('./schema') +const {generate} = require('./generate') + +const generateAll = def('generateAll', {}, [$.StrMap($.Integer), $.StrMap($.Array($.Any))], + counts => Object.keys(counts) + .reduce((data, name) => { + const make = generate(model(mongoose.model(name))) + data[name] = [] + for(let i = 0; i < counts[name]; i++) + data[name].push(make()) + return data + }, {}) +) + +const insertAll = def('insertAll', {}, [$.StrMap($.Array($.Any)), $.Any], + async data => { + const result = {} + const names = Object.keys(data) + for (let i = 0; i < names.length; i++) { + const name = names[i] + const Model = mongoose.model(name) + result[name] = await Model.insertMany(data[name]) + } + return result + } +) + +const applyRelation = def('applyRelation', {}, [T.Relation, $.StrMap($.Array($.Any)), $.StrMap($.Array($.Any))], + (relation, data) => + (relation.type === 'one-one') ? applyOneOneRelation(relation, data) : + (relation.type === 'one-many') ? applyOneManyRelation(relation, data) : + (relation.type === 'many-many') ? applyManyManyRelation(relation, data) : + data +) + +const applyOneOneRelation = def('applyOneOneRelation', {}, [T.Relation, $.StrMap($.Array($.Any)), $.StrMap($.Array($.Any))], + ({source, target}, data) => { + const sources = data[source.name] + const targets = data[target.name] + if (!sources || !targets || (!source.field && !target.field)) + return data + let i = 0, j = 0 + if (source.name == target.name) + j ++ + while (i < sources.length && j < targets.length) { + if (source.field) + sources[i][source.field] = targets[j].id + if (target.field) + targets[j][target.field] = sources[i].id + i ++ + j ++ + } + return data + } +) + +const applyOneManyRelation = def('applyOneManyRelation', {}, [T.Relation, $.StrMap($.Array($.Any)), $.StrMap($.Array($.Any))], + ({source, target}, data) => { + const sources = data[source.name] + const targets = data[target.name] + if (!sources || !targets || (!source.field && !target.field)) + return data + const count = Math.floor(targets.length / Math.min(sources.length, Math.floor(targets.length / 2))) + 1 + let i = 0, j = 0 + if (source.name == target.name) + j = 1 + while (j < targets.length) { + i = Math.floor(j / count) + if (source.field) + sources[i][source.field].push(targets[j].id) + if (target.field) + targets[j][target.field] = sources[i].id + j ++ + } + return data + } +) + +const applyManyManyRelation = def('applyManyManyRelation', {}, [T.Relation, $.StrMap($.Array($.Any)), $.StrMap($.Array($.Any))], + ({source, target}, data) => { + const sources = data[source.name] + const targets = data[target.name] + if (!sources || !targets || (!source.field && !target.field)) + return data + + for(let i = 0; i < sources.length; i++) { + for(let j = 0; j < targets.length; j++) { + if (Math.random() < 0.5) { + if (source.field) + sources[i][source.field].push(targets[j].id) + if (target.field) + targets[j][target.field].push(sources[i].id) + } + } + } + + return data + } +) + +const updateAll = def('updateAll', {}, [$.StrMap($.Array($.Any)), $.Any], + async data => { + const result = {} + const names = Object.keys(data) + for (let i = 0; i < names.length; i++) { + const name = names[i] + const Model = mongoose.model(name) + result[name] = [] + for (let j = 0; j < data[name].length; j++) + result[name].push(await Model.findOneAndUpdate({_id: data[name][j].id}, data[name][j], {new: true})) + } + return result + } +) + +const seed = def('seed', {}, [$.StrMap($.Integer), $.Array(T.Relation), $.Any], + async (counts, relations) => { + let data = generateAll(counts) + data = await insertAll(data) + relations.forEach(relation => { + data = applyRelation(relation, data) + }) + data = await updateAll(data) + return data + } +) + +module.exports = {seed} diff --git a/src/types/index.js b/src/types/index.js new file mode 100644 index 0000000..9fa2386 --- /dev/null +++ b/src/types/index.js @@ -0,0 +1,6 @@ +module.exports = { + ...(require('./meta')), + ...(require('./mongoose')), + ...(require('./relation')), + ...(require('./schema')), +} diff --git a/src/types/meta.js b/src/types/meta.js new file mode 100644 index 0000000..4164f5d --- /dev/null +++ b/src/types/meta.js @@ -0,0 +1,26 @@ +const {$, S} = require('../sanctuary') + +const packageName = 'wajez/utils' +const packageURL = 'https://github.com/wajez/utils/blob/master/types.md' + +const _ = $.RecordType + +const Lazy = (name, type) => $.NullaryType( + `${packageName}/${name}`, + `${packageURL}#${name}`, + x => $.test($.env, type(), x) +) + +const Enum = (name, values) => $.EnumType( + `${packageName}/${name}`, + `${packageURL}#${name}`, + values +) + +const Union = (name, types) => $.NullaryType( + `${packageName}/${name}`, + `${packageURL}#${name}`, + S.anyPass(types.map(type => x => $.test($.env, type, x))) +) + +module.exports = {_, Lazy, Enum, Union} diff --git a/src/types/mongoose.js b/src/types/mongoose.js new file mode 100644 index 0000000..295fe04 --- /dev/null +++ b/src/types/mongoose.js @@ -0,0 +1,11 @@ +const {$, S} = require('../sanctuary') +const {_, Enum, Union} = require('./meta') + +const MongooseModel = _({ + modelName: $.String, + schema: _({ + obj: $.Object, + }), +}) + +module.exports = {MongooseModel} diff --git a/src/types/relation.js b/src/types/relation.js new file mode 100644 index 0000000..355a2bb --- /dev/null +++ b/src/types/relation.js @@ -0,0 +1,18 @@ +const {$, S} = require('../sanctuary') +const {_, Enum, Union} = require('./meta') + +const RelationType = Enum('RelationType', ['one-one', 'one-many', 'many-many']) + +const Relation = _({ + type: RelationType, + source: _({ + name: $.String, + field: $.Nullable($.String), + }), + target: _({ + name: $.String, + field: $.Nullable($.String), + }) +}) + +module.exports = {Relation, RelationType} diff --git a/src/types/schema.js b/src/types/schema.js new file mode 100644 index 0000000..ff90875 --- /dev/null +++ b/src/types/schema.js @@ -0,0 +1,78 @@ +const {$, S} = require('../sanctuary') +const {_, Lazy, Enum, Union} = require('./meta') + +const LazySchema = Lazy('Schema', () => Schema) + +const StringSchema = _({ + type: Enum('StringSchemaType', ['string']), + choices: $.Nullable($.Array($.String)), + match: $.Nullable($.RegExp), + minLength: $.Number, + maxLength: $.Number, +}) + +const NumberSchema = _({ + type: Enum('NumberSchemaType', ['number']), + min: $.Number, + max: $.Number, +}) + +const BooleanSchema = _({ + type: Enum('BooleanSchemaType', ['boolean']), +}) + +const DateSchema = _({ + type: Enum('DateSchemaType', ['date']), + min: $.Nullable($.Date), + max: $.Nullable($.Date), +}) + +const BufferSchema = _({ + type: Enum('BufferSchemaType', ['buffer']), +}) + +const ObjectSchema = _({ + type: Enum('ObjectSchemaType', ['object']), + fields: $.StrMap(LazySchema) +}) + +const ArraySchema = _({ + type: Enum('ArraySchemaType', ['array']), + schema: LazySchema, + minLength: $.Number, + maxLength: $.Number, +}) + +const ReferenceSchema = _({ + type: Enum('ReferenceSchemaType', ['reference']), + name: $.String, +}) + +const UnknownSchema = _({ + type: Enum('UnknownSchemaType', ['unknown']) +}) + +const Schema = Union('Schema', [ + StringSchema, + NumberSchema, + BooleanSchema, + DateSchema, + BufferSchema, + ObjectSchema, + ArraySchema, + ReferenceSchema, + UnknownSchema +]) + +module.exports = { + StringSchema, + NumberSchema, + BooleanSchema, + DateSchema, + BufferSchema, + ObjectSchema, + ArraySchema, + ReferenceSchema, + UnknownSchema, + Schema, +} diff --git a/test/db.js b/test/db.js new file mode 100644 index 0000000..580b811 --- /dev/null +++ b/test/db.js @@ -0,0 +1,95 @@ +const mongoose = require('mongoose') +const {Schema} = mongoose + +const connect = async () => { + mongoose.Promise = global.Promise + return mongoose.connect(`mongodb://localhost/wajez-utils`) +} + +const disconnect = async () => { + mongoose.connection.close() +} + +const User = mongoose.model('User', new Schema({ + posts: [{ + type: Schema.Types.ObjectId, + ref: 'Post' + }], + profile: { + type: Schema.Types.ObjectId, + ref: 'Profile' + }, + name: String, + since: Date, +})) + +const Profile = mongoose.model('Profile', new Schema({ + type: { + type: String, + enum: ['free', 'premium', 'admin'] + }, + picture: Buffer, + rank: Number, + links: { + facebook: String, + twitter: String, + github: String + }, + quotes: [String] +})) + +const Category = mongoose.model('Category', new Schema({ + parent: { + type: Schema.Types.ObjectId, + ref: 'Category' + }, + children: [{ + type: Schema.Types.ObjectId, + ref: 'Category' + }], + posts: [{ + type: Schema.Types.ObjectId, + ref: 'Post' + }], + name: String +})) + +const Post = mongoose.model('Post', new Schema({ + category: { + type: Schema.Types.ObjectId, + ref: 'Category' + }, + writer: { + type: Schema.Types.ObjectId, + ref: 'User' + }, + tags: [{ + type: Schema.Types.ObjectId, + ref: 'Tag' + }], + comments: [{ + type: Schema.Types.ObjectId, + ref: 'Comment' + }], + title: String, + content: String +})) + +const Comment = mongoose.model('Comment', new Schema({ + post: { + type: Schema.Types.ObjectId, + ref: 'Post' + }, + writer: { + type: Schema.Types.ObjectId, + ref: 'User' + }, + content: String +})) + +const Tag = mongoose.model('Tag', new Schema({ + name: String, + hidden: Boolean +})) + +module.exports = {connect, disconnect, User, Profile, Category, Post, Comment, Tag} diff --git a/test/lib.test.js b/test/lib.test.js new file mode 100644 index 0000000..38e4042 --- /dev/null +++ b/test/lib.test.js @@ -0,0 +1,174 @@ +const assert = require('chai').assert +const {id, merge, mapObj, applySpec, applyConverter} = require('../src') + +describe('Lib', () => { + + describe('id', () => { + it('returns the id of the object', () => { + const buf = Buffer.from('1125', 'hex') + assert.equal(id({id: buf}), '1125') + }) + it('returns the object if id is missing', () => { + const buf = Buffer.from('1125', 'hex') + assert.equal(id(buf), '1125') + }) + }) + + describe('merge', () => { + it('merges two objects', () => { + assert.deepEqual(merge({}, {}), {}) + assert.deepEqual(merge({a: 1}, {}), {a: 1}) + assert.deepEqual(merge({a: 1}, {b: 2}), {a: 1, b: 2}) + assert.deepEqual(merge({a: 1, b: 1}, {b: 2}), {a: 1, b: 2}) + assert.deepEqual(merge({a: 1, b: 1}, {b: 2, c: 3}), {a: 1, b: 2, c: 3}) + assert.deepEqual(merge({a: 1, d: {e: 'Hi'}}, {b: 2, c: 3}), {a: 1, b: 2, c: 3, d: {e: 'Hi'}}) + assert.deepEqual(merge({a: 1, d: {e: 'Hi'}}, {b: 2, c: 3, d: {}}), {a: 1, b: 2, c: 3, d: {e: 'Hi'}}) + assert.deepEqual(merge({a: 1, d: {e: 'Hi'}}, {b: 2, c: 3, d: {f: 'Yo'}}), {a: 1, b: 2, c: 3, d: {e: 'Hi', f: 'Yo'}}) + assert.deepEqual(merge({a: 1, d: {e: 'Hi'}}, {b: 2, c: 3, d: {e: {g: true}, f: 'Yo'}}), {a: 1, b: 2, c: 3, d: {e: {g: true}, f: 'Yo'}}) + }) + }) + + describe('mapObj', () => { + const addOne = _ => _ + 1 + it('maps a function over an object', () => { + assert.deepEqual( + mapObj( + _ => _ == 1 ? 10 : _ == 2 ? 'Hey' : addOne, + { x: 1, y: 2, z: 3 } + ), + { x: 10, y: 'Hey', z: addOne } + ) + }) + }) + + describe('applySpec', () => { + it('applies a specification of functions to a value', () => { + assert.deepEqual(applySpec({ + fullName: _ => _.firstName + ' ' + _.lastName, + adult: _ => _.age > 18 + }, { + firstName: 'Amine', + lastName: 'Ben hammou', + age: 27 + }), { + fullName: 'Amine Ben hammou', + adult: true + }) + }) + }) + + describe('applyConverter', () => { + it('applies a function to a value', () => { + assert.deepEqual(applyConverter(_ => _.firstName + ' ' + _.lastName, { + firstName: 'Amine', + lastName: 'Ben hammou', + age: 27 + }), 'Amine Ben hammou') + assert.deepEqual(applyConverter(_ => _.firstName + ' ' + _.lastName, undefined), undefined) + }) + it('applies an object of functions to a value', () => { + assert.deepEqual(applyConverter({ + firstName: _ => _, + lastName: _ => _.toUpperCase(), + age: _ => _ - 6 + }, { + firstName: 'Amine', + lastName: 'Ben hammou', + age: 27 + }), { + firstName: 'Amine', + lastName: 'BEN HAMMOU', + age: 21 + }) + }) + it('ignores missing attributes', () => { + assert.deepEqual(applyConverter({ + firstName: _ => _, + lastName: _ => _.toUpperCase(), + }, { + firstName: 'Amine', + lastName: 'Ben hammou', + age: 27 + }), { + firstName: 'Amine', + lastName: 'BEN HAMMOU' + }) + }) + it('applies multiple converters', () => { + assert.deepEqual(applyConverter([ + { + firstName: _ => _, + lastName: _ => _.toUpperCase(), + }, + { + fullName: _ => _.firstName + ' ' + _.lastName + }, + _ => _.fullName + ], { + firstName: 'Amine', + lastName: 'Ben hammou', + age: 27 + }), + 'Amine BEN HAMMOU') + }) + it('is recursive', () => { + assert.deepEqual(applyConverter({ + infos: { + firstName: _ => _, + lastName: _ => _.toUpperCase(), + }, + age: _ => _ - 6 + }, { + infos: { + firstName: 'Amine', + lastName: 'Ben hammou', + }, + age: 27 + }), { + infos: { + firstName: 'Amine', + lastName: 'BEN HAMMOU' + }, + age: 21 + }) + }) + it('handles arrays', () => { + assert.deepEqual(applyConverter({ + infos: { + firstName: _ => _, + lastName: _ => _.toUpperCase(), + }, + projects: { + name: _ => _.toUpperCase(), + score: _ => _ * 2 + } + }, { + infos: { + firstName: 'Amine', + lastName: 'Ben hammou', + }, + age: 27, + projects: [{ + name: 'tarsana', + score: 100, + }, { + name: 'wajez', + score: 10 + }] + }), { + infos: { + firstName: 'Amine', + lastName: 'BEN HAMMOU' + }, + projects: [{ + name: 'TARSANA', + score: 200 + }, { + name: 'WAJEZ', + score: 20 + }] + }) + }) + }) + +}) diff --git a/test/schema.test.js b/test/schema.test.js new file mode 100644 index 0000000..5f66c14 --- /dev/null +++ b/test/schema.test.js @@ -0,0 +1,321 @@ +const assert = require('chai').assert +const mongoose = require('mongoose') +const {model} = require('../src') + +describe('Schema', () => { + describe('model', () => { + it('gets schema of string fields', () => { + assert.deepEqual(model(mongoose.model('SchemaTest1', new mongoose.Schema({ + name: String, + genre: { + type: String, + enum: ['M', 'F'] + }, + email: { + type: String, + match: /[a-z0-9._+-]{1,20}@[a-z0-9]{3,15}\.[a-z]{2,4}/ + }, + password: { + type: String, + minLength: 8 + } + }))), { + type: 'object', + fields: { + name: { + type: 'string', + choices: null, + match: null, + minLength: 0, + maxLength: 100 + }, + genre: { + type: 'string', + choices: ['M', 'F'], + match: null, + minLength: 0, + maxLength: 100 + }, + email: { + type: 'string', + choices: null, + match: /[a-z0-9._+-]{1,20}@[a-z0-9]{3,15}\.[a-z]{2,4}/, + minLength: 0, + maxLength: 100 + }, + password: { + type: 'string', + choices: null, + match: null, + minLength: 8, + maxLength: 100 + } + } + }) + }) + + it('gets schema of number fields', () => { + assert.deepEqual(model(mongoose.model('SchemaTest2', new mongoose.Schema({ + n1: Number, + n2: { + type: Number, + min: 3 + }, + n3: { + type: Number, + max: 3 + }, + }))), { + type: 'object', + fields: { + + n1: { + type: 'number', + min: -100, + max: 100 + }, + n2: { + type: 'number', + min: 3, + max: 100 + }, + n3: { + type: 'number', + min: -100, + max: 3 + }, + } + }) + }) + + it('gets schema of boolean fields', () => { + assert.deepEqual(model(mongoose.model('SchemaTest3', new mongoose.Schema({ + b1: Boolean, + b2: { + type: Boolean + }, + }))), { + type: 'object', + fields: { + b1: { + type: 'boolean', + }, + b2: { + type: 'boolean', + }, + } + }) + }) + + it('gets schema of buffer fields', () => { + assert.deepEqual(model(mongoose.model('SchemaTest4', new mongoose.Schema({ + b1: Buffer, + b2: { + type: Buffer + }, + }))), { + type: 'object', + fields: { + b1: { + type: 'buffer', + }, + b2: { + type: 'buffer', + }, + } + }) + }) + + it('gets schema of date fields', () => { + const now = new Date() + assert.deepEqual(model(mongoose.model('SchemaTest5', new mongoose.Schema({ + d1: Date, + d2: { + type: Date, + min: now + }, + d3: { + type: Date, + max: now + }, + }))), { + type: 'object', + fields: { + d1: { + type: 'date', + min: null, + max: null + }, + d2: { + type: 'date', + min: now, + max: null + }, + d3: { + type: 'date', + min: null, + max: now + }, + } + }) + }) + + it('gets schema of object fields', () => { + assert.deepEqual(model(mongoose.model('SchemaTest6', new mongoose.Schema({ + foo: Object, + bestFriend: { + name: String, + pet: { + name: String, + type: { + type: String, + enum: ['dog', 'cat'] + } + } + } + }))), { + type: 'object', + fields: { + foo: { + type: 'object', + fields: {} + }, + bestFriend: { + type: 'object', + fields: { + name: { + type: 'string', + choices: null, + match: null, + minLength: 0, + maxLength: 100 + }, + pet: { + type: 'object', + fields: { + name: { + type: 'string', + choices: null, + match: null, + minLength: 0, + maxLength: 100 + }, + type: { + type: 'string', + choices: ['dog', 'cat'], + match: null, + minLength: 0, + maxLength: 100 + } + } + } + } + }, + } + }) + }) + + it('gets schema of array fields', () => { + assert.deepEqual(model(mongoose.model('SchemaTest7', new mongoose.Schema({ + scores: [Number], + matrix: [[Boolean]], + projects: [{ + name: String, + language: { + type: String, + enum: ['JS', 'PHP', 'C++'] + } + }] + }))), { + type: 'object', + fields: { + scores: { + type: 'array', + schema: { + type: 'number', + min: -100, + max: 100 + }, + minLength: 0, + maxLength: 100 + }, + matrix: { + type: 'array', + schema: { + type: 'array', + schema: { + type: 'boolean', + }, + minLength: 0, + maxLength: 100 + }, + minLength: 0, + maxLength: 100 + }, + projects: { + type: 'array', + schema: { + type: 'object', + fields: { + name: { + type: 'string', + choices: null, + match: null, + minLength: 0, + maxLength: 100 + }, + language: { + type: 'string', + choices: ['JS', 'PHP', 'C++'], + match: null, + minLength: 0, + maxLength: 100 + } + } + }, + minLength: 0, + maxLength: 100 + }, + } + }) + }) + + it('gets schema of ref fields', () => { + assert.deepEqual(model(mongoose.model('SchemaTest8', new mongoose.Schema({ + unknown: mongoose.Schema.Types.ObjectId, + anything: mongoose.Schema.Types.Mixed, + parent: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User3', + }, + friends: [{ + type: mongoose.Schema.Types.ObjectId, + ref: 'User8', + }], + }))), { + type: 'object', + fields: { + unknown: { + type: 'unknown' + }, + anything: { + type: 'unknown' + }, + parent: { + type: 'reference', + name: 'User3' + }, + friends: { + type: 'array', + minLength: 0, + maxLength: 100, + schema: { + type: 'reference', + name: 'User8' + } + }, + } + }) + }) + }) +}) diff --git a/test/seed.test.js b/test/seed.test.js new file mode 100644 index 0000000..342178d --- /dev/null +++ b/test/seed.test.js @@ -0,0 +1,161 @@ +const assert = require('chai').assert +const mongoose = require('mongoose') +const {seed, oneOne, oneMany, manyMany} = require('../src') +const {connect, disconnect, User, Profile, Category, Post, Comment, Tag} = require('./db') + +describe('Seed', () => { + before(connect) + beforeEach(() => + User.remove({}) + .then(() => Category.remove({})) + .then(() => Post.remove({})) + .then(() => Comment.remove({})) + .then(() => Tag.remove({})) + ) + + it('seeds one model', () => + Tag.find({}) + .then(tags => { + assert.deepEqual(tags, []) + return seed({Tag: 5}, []) + }) + .then(data => { + assert.isObject(data) + assert.isArray(data.Tag) + assert.lengthOf(data.Tag, 5) + data.Tag.forEach(tag => { + assert.isString(tag.name) + assert.isBoolean(tag.hidden) + }) + }) + ) + + it('seeds one model with recursive relation', () => + seed({Category: 10}, [ + oneMany('Category', 'children', 'Category', 'parent') + ]) + .then(data => { + assert.isObject(data) + assert.isArray(data.Category) + assert.lengthOf(data.Category, 10) + const index = data.Category.reduce((index, c) => { + index[c.id] = c + return index + }, {}) + let noParent = 0 + data.Category.forEach(c => { + assert.isString(c.name) + if (c.parent == null) + noParent ++ + else + assert.include(index[c.parent].children, c.id) + }) + assert.equal(noParent, 1) + }) + ) + + it('seeds many models with multiple relations', () => { + const counts = { + User: 5, Profile: 5, Category: 5, + Post: 10, Comment: 50, Tag: 30 + } + return seed(counts, [ + oneOne('User', 'profile', 'Profile', null), + oneMany('User', 'posts', 'Post', 'writer'), + oneMany('User', null, 'Comment', 'writer'), + oneMany('Category', 'posts', 'Post', 'category'), + oneMany('Category', 'children', 'Category', 'parent'), + oneMany('Post', 'comments', 'Comment', 'post'), + manyMany('Post', 'tags', 'Tag', null) + ]) + .then(data => { + assert.isObject(data) + + Object.keys(counts).forEach(name => { + assert.isArray(data[name]) + assert.lengthOf(data[name], counts[name]) + }) + + const index = {} + const ids = {} + Object.keys(data).forEach(name => { + index[name] = data[name].reduce((index, m) => { + index[m.id] = m + return index + }, {}) + ids[name] = data[name].map(_ => _.id.toString()) + }) + + data.User.forEach(user => { + assert.isString(user.name) + assert.instanceOf(user.since, Date) + assert.include(ids.Profile, user.profile.toString()) + user.posts.forEach(id => + assert.equal(user.id, index.Post[id].writer) + ) + }) + + data.Profile.forEach(profile => { + assert.include(['free', 'premium', 'admin'], profile.type) + assert.instanceOf(profile.picture, Buffer) + assert.isNumber(profile.rank) + assert.isString(profile.links.facebook) + assert.isString(profile.links.twitter) + assert.isString(profile.links.github) + profile.quotes.forEach(q => assert.isString(q)) + }) + + let categoriesWithNoParent = 0 + data.Category.forEach(category => { + assert.isString(category.name) + if (category.parent == null) + categoriesWithNoParent ++ + else + assert.include( + index.Category[category.parent].children, + category.id + ) + category.posts.forEach(id => + assert.equal(category.id, index.Post[id].category) + ) + }) + assert.equal(categoriesWithNoParent, 1) + + data.Post.forEach(post => { + assert.isString(post.title) + assert.isString(post.content) + assert.include( + index.Category[post.category].posts, + post.id + ) + assert.include( + index.User[post.writer].posts, + post.id + ) + post.comments.forEach(id => + assert.equal(post.id, index.Comment[id].post) + ) + post.tags.forEach(id => + assert.include(ids.Tag, id.toString()) + ) + }) + + data.Comment.forEach(comment => { + assert.isString(comment.content) + assert.include( + index.Post[comment.post].comments, + comment.id + ) + assert.include(ids.User, comment.writer.toString()) + }) + + data.Tag.forEach(tag => { + assert.isString(tag.name) + assert.isBoolean(tag.hidden) + }) + + }) + }).timeout(10000) + + after(disconnect) +}) diff --git a/types.md b/types.md new file mode 100644 index 0000000..05943b0 --- /dev/null +++ b/types.md @@ -0,0 +1,98 @@ +# Data Types + +## Schema + +A schema is used to describe the structure of data. It can be one of: + +### StringSchema + +```js +{ + type :: 'string' + choices: Array String | Null, + match: RegExp | Null, + minLength: Number, + maxLength: Number, +} +``` + +### NumberSchema + +```js +{ + type: 'number' + min: Number, + max: Number, +} +``` + +### BooleanSchema + +```js +{ + type: 'boolean' +} +``` + +### BufferSchema + +```js +{ + type: 'buffer' +} +``` + +### DateSchema + +```js +{ + type: 'date', + min: Date || Null, + max: Date || Null +} +``` + +### ArraySchema + +```js +{ + type: 'array', + minLength: Number, + maxLength: Number, + schema: Schema +} +``` + +### ObjectSchema + +```js +{ + type: 'object', + fields: StrMap Schema +} +``` + +## MongooseModel + +Represents a [mongoose](http://mongoosejs.com/) model. + +## RelationType + +One of `'one-one'` `'one-many'` `'many-many'`. + +## Relation + +```js +{ + type: RelationType, + source: { + name: String, + field: String || Null, + }, + target: { + name: String, + field: String || Null, + } +} +``` +