├── .gitignore ├── .jscsfilter ├── .jscsrc ├── .jshintignore ├── .jshintrc ├── .travis.yml ├── LICENSE ├── README.md ├── bin └── azul ├── docs ├── scripts │ ├── build │ └── deploy ├── source │ ├── getting-started.md │ ├── guides │ │ ├── backends.md │ │ ├── core.md │ │ ├── express.md │ │ ├── express │ │ │ └── anti-patterns.md │ │ ├── index.md │ │ ├── managers.md │ │ ├── migrations.md │ │ ├── models.md │ │ ├── queries.md │ │ ├── relations.md │ │ └── transactions.md │ ├── index.md │ ├── releases.md │ └── styles │ │ └── application.scss └── templates │ ├── base.html │ ├── guide-page.html │ ├── home.html │ ├── page.html │ └── releases.html ├── index.js ├── jsdoc.json ├── lib ├── cli │ ├── actions.js │ └── index.js ├── compatibility.js ├── database.js ├── index.js ├── migration.js ├── model │ ├── attr.js │ ├── index.js │ ├── manager.js │ └── model.js ├── query │ ├── bound.js │ └── mixins │ │ ├── bound_auto_join.js │ │ ├── bound_core.js │ │ ├── bound_helpers.js │ │ ├── bound_join.js │ │ ├── bound_transform.js │ │ ├── bound_unique.js │ │ ├── bound_util.js │ │ └── bound_with.js ├── relations │ ├── base.js │ ├── belongs_to.js │ ├── belongs_to_associations.js │ ├── belongs_to_config.js │ ├── belongs_to_overrides.js │ ├── belongs_to_prefetch.js │ ├── has_many.js │ ├── has_many_associations.js │ ├── has_many_collection.js │ ├── has_many_config.js │ ├── has_many_hooks.js │ ├── has_many_in_flight.js │ ├── has_many_overrides.js │ ├── has_many_prefetch.js │ ├── has_many_query.js │ ├── has_many_through.js │ ├── has_one.js │ ├── model_save.js │ └── relation_attr.js ├── templates │ ├── azulfile.js.template │ └── migration.js.template └── util │ └── inflection.js ├── package.json └── test ├── .jshintrc ├── cli ├── cli_actions_tests.js ├── cli_helpers.js └── cli_tests.js ├── common ├── index.js └── models.js ├── compatibility_tests.js ├── database_tests.js ├── fixtures ├── cli │ ├── azulfile.json │ ├── help.js │ └── load.js └── migrations │ └── blog │ ├── 20141022202234_create_articles.js │ └── 20141022202634_create_comments.js ├── helpers ├── index.js └── reset.js ├── integration ├── mysql_tests.js ├── pg_tests.js ├── shared_behaviors.js └── sqlite3_tests.js ├── migration_tests.js ├── mocha.opts ├── model └── model_tests.js ├── query └── bound_tests.js ├── relations ├── base_tests.js ├── belongs_to_tests.js ├── has_many_tests.js ├── has_one_join_tests.js ├── has_one_prefetch_tests.js ├── has_one_tests.js ├── has_one_through_tests.js ├── inverse_tests.js ├── many_to_many_tests.js ├── one_to_many_tests.js ├── one_to_one_tests.js ├── relation_config_tests.js ├── save_cycle_test.js ├── self_join_tests.js ├── through_shortcut_tests.js └── through_tests.js └── util └── inflection_tests.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /coverage 3 | /out 4 | /docs/build 5 | -------------------------------------------------------------------------------- /.jscsfilter: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(e) { 4 | var report = true; 5 | var isTestFile = !!e.filename.match(/^\.\/test\//); 6 | if (isTestFile && e.message === 'jsdoc definition required') { 7 | report = false; 8 | } 9 | return report; 10 | }; 11 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "airbnb", 3 | "excludeFiles": [ 4 | "test/**", 5 | "node_modules/**", 6 | "coverage/**", 7 | "out/**", 8 | "docs/build/**" 9 | ], 10 | "errorFilter": "./.jscsfilter", 11 | "disallowEmptyBlocks": null, 12 | "disallowFunctionDeclarations": true, 13 | "disallowKeywordsOnNewLine": null, 14 | "disallowMultipleLineBreaks": null, 15 | "disallowMultipleVarDecl": "exceptUndefined", 16 | "disallowSpacesInAnonymousFunctionExpression": { 17 | "beforeOpeningRoundBrace": true 18 | }, 19 | "disallowSpacesInFunctionExpression": { 20 | "beforeOpeningRoundBrace": true 21 | }, 22 | "disallowTrailingComma": null, 23 | "maximumLineLength": 80, 24 | "maximumNumberOfLines": 1000, 25 | "requireBlocksOnNewline": 2, 26 | "requireKeywordsOnNewLine": ["else"], 27 | "requirePaddingNewLinesAfterBlocks": null, 28 | "requirePaddingNewLinesAfterUseStrict": true, 29 | "requirePaddingNewLinesBeforeExport": true, 30 | "requireSemicolons": true, 31 | "requireSpaceAfterLineComment": true, 32 | "requireSpacesInsideObjectBrackets": "all", 33 | "requireTrailingComma": { 34 | "ignoreSingleLine": true 35 | }, 36 | "safeContextKeyword": "self", 37 | "jsDoc": { 38 | "checkAnnotations": { 39 | "preset": "jsdoc3", 40 | "extra": { 41 | "scope": "some" 42 | } 43 | }, 44 | "checkReturnTypes": true, 45 | "checkTypes": "capitalizedNativeCase", 46 | "enforceExistence": true, 47 | "requireNewlineAfterDescription": true, 48 | "requireParamTypes": true 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | .gitignore -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "bitwise": true, 4 | "camelcase": true, 5 | "curly": true, 6 | "eqeqeq": true, 7 | "immed": true, 8 | "indent": 2, 9 | "latedef": true, 10 | "newcap": true, 11 | "noarg": true, 12 | "quotmark": "single", 13 | "regexp": true, 14 | "undef": true, 15 | "unused": true, 16 | "strict": true, 17 | "trailing": true, 18 | "smarttabs": true, 19 | "globals": { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - '0.12' 5 | - '4.0' 6 | - '4.1' 7 | 8 | script: npm run $ACTION 9 | 10 | before_script: 11 | - psql -c 'create database "azul_test";' -U postgres 12 | - mysql -e 'create database `azul_test`;' 13 | 14 | after_script: 15 | - npm install coveralls@2 && cat ./coverage/lcov.info | coveralls 16 | 17 | env: ACTION=test-travis PG_USER=postgres MYSQL_USER=travis 18 | 19 | matrix: 20 | include: 21 | - env: ACTION=docs 22 | node_js: '4.1' 23 | 24 | fast_finish: true 25 | 26 | sudo: false 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Whitney Young 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Azul 2 | 3 | [![NPM version][npm-image]][npm-url] [![Build status][travis-image]][travis-url] [![Code Climate][codeclimate-image]][codeclimate-url] [![Coverage Status][coverage-image]][coverage-url] [![Dependencies][david-image]][david-url] [![devDependencies][david-dev-image]][david-dev-url] 4 | 5 | Azul has been built from the ground up to be the most expressive ORM for 6 | Node.js. 7 | 8 | ## Get Started 9 | 10 | ```bash 11 | $ npm install azul -g 12 | $ npm install azul 13 | $ azul init 14 | ``` 15 | 16 | The current release of Azul.js is a functional **alpha**. We're looking for 17 | feedback at this point, so please [look at the docs][azul-docs] and give it a 18 | try. 19 | 20 | We're focusing on creating an expressive, well [documented public 21 | API][azul-docs] and reaching feature parity with other ORM tools. If you're 22 | interested in helping, please reach out. 23 | 24 | ### Addons 25 | 26 | A few additional components that you can use with Azul.js are: 27 | 28 | - [Azul.js Express][azul-express] Middleware & decorators for Express 29 | - [Azul.js Logger][azul-logger] Logs queries being executed 30 | - [Azul.js Tracker][azul-tracker] Reports queries that were created, but never 31 | executed 32 | 33 | ### Testing 34 | 35 | Simply run `npm test` to run the full test suite. 36 | 37 | This build currently connects to databases for testing purposes. To replicate this on your machine, do the following: 38 | 39 | #### Postgres 40 | 41 | ```bash 42 | $ createuser -s root 43 | $ psql -U root -d postgres 44 | > CREATE DATABASE azul_test; 45 | > \q 46 | ``` 47 | 48 | #### MySQL 49 | 50 | ```bash 51 | $ mysql -u root 52 | > CREATE DATABASE azul_test; 53 | > exit 54 | ``` 55 | ### Documentation 56 | 57 | Generate and access documentation locally: 58 | 59 | ```bash 60 | $ jsdoc --private -c jsdoc.json 61 | $ open out/index.html 62 | ``` 63 | 64 | ## License 65 | 66 | This project is distributed under the MIT license. 67 | 68 | [azul-docs]: http://www.azuljs.com/ 69 | [azul-express]: https://github.com/wbyoung/azul-express 70 | [azul-logger]: https://github.com/wbyoung/azul-logger 71 | [azul-tracker]: https://github.com/wbyoung/azul-tracker 72 | 73 | [travis-image]: http://img.shields.io/travis/wbyoung/azul.svg?style=flat 74 | [travis-url]: http://travis-ci.org/wbyoung/azul 75 | [npm-image]: http://img.shields.io/npm/v/azul.svg?style=flat 76 | [npm-url]: https://npmjs.org/package/azul 77 | [codeclimate-image]: http://img.shields.io/codeclimate/github/wbyoung/azul.svg?style=flat 78 | [codeclimate-url]: https://codeclimate.com/github/wbyoung/azul 79 | [coverage-image]: http://img.shields.io/coveralls/wbyoung/azul.svg?style=flat 80 | [coverage-url]: https://coveralls.io/r/wbyoung/azul 81 | [david-image]: http://img.shields.io/david/wbyoung/azul.svg?style=flat 82 | [david-url]: https://david-dm.org/wbyoung/azul 83 | [david-dev-image]: http://img.shields.io/david/dev/wbyoung/azul.svg?style=flat 84 | [david-dev-url]: https://david-dm.org/wbyoung/azul#info=devDependencies 85 | -------------------------------------------------------------------------------- /bin/azul: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var _ = require('lodash'); 6 | var Liftoff = require('liftoff'); 7 | var path = require('path'); 8 | var argv = require('minimist')(process.argv.slice(2)); 9 | var interpret = require('interpret'); 10 | var v8flags = require('v8flags'); 11 | var cli = require('../lib/cli'); 12 | 13 | var invoke = function(env) { 14 | if (process.cwd() !== env.cwd) { 15 | process.chdir(env.cwd); 16 | } 17 | // prefer local, but fall back on global install 18 | var modulePath = env.modulePath || '.'; 19 | var cliPath = path.join(modulePath, '../lib/cli'); 20 | var cli = require(cliPath); 21 | cli(env); 22 | }; 23 | 24 | new Liftoff({ 25 | name: 'azul', 26 | v8flags: v8flags, 27 | extensions: interpret.jsVariants 28 | }) 29 | 30 | .on('require', cli.require) 31 | .on('requireFail', cli.requireFail) 32 | .on('respawn', cli.respawn) 33 | 34 | .launch({ 35 | cwd: argv.cwd, 36 | configPath: argv.azulfile, 37 | require: argv.require, 38 | completion: argv.completion 39 | }, invoke); 40 | -------------------------------------------------------------------------------- /docs/scripts/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var _ = require('lodash'); 4 | var Metalsmith = require('metalsmith'); 5 | var templates = require('metalsmith-templates'); 6 | var markdown = require('metalsmith-markdown'); 7 | var sass = require('metalsmith-sass'); 8 | var metallic = require('metalsmith-metallic'); 9 | var serve = require('metalsmith-serve'); 10 | var watch = require('metalsmith-watch'); 11 | var path = require('path'); 12 | var cp = require('child_process'); 13 | var semver = require('semver'); 14 | var cheerio = require('cheerio'); 15 | var commander = require('commander'); 16 | var program = new commander.Command() 17 | .option('-s --serve', 'serve and watch for changes') 18 | 19 | program.parse(process.argv); 20 | 21 | 22 | /** 23 | * Set all known tags as `azulTags`. 24 | * 25 | * @return {MetalsmithPlugin} 26 | */ 27 | var addKnownTags = function() { 28 | var args = ['tag', '-l']; 29 | var extract = function(v) { return v; }; 30 | var filter = function(v) { return v; }; 31 | if (process.env.TRAVIS) { 32 | args = ['ls-remote', '--tags', 'origin'] 33 | extract = function(v) { 34 | var match = v.match(/refs\/tags\/([^^]*)$/); 35 | return match && match[1]; 36 | }; 37 | } 38 | 39 | var tags = cp.spawnSync('git', args, { encoding: 'utf8' }) 40 | .stdout.trim().split('\n') 41 | .map(extract).filter(filter) 42 | .sort(semver.compare) 43 | .reverse(); 44 | 45 | return function addKnownTags(files, metalsmith, done) { 46 | _.forEach(files, function(data, file) { 47 | data.azulTags = tags; 48 | }); 49 | done(); 50 | }; 51 | }; 52 | 53 | /** 54 | * Set current tag as `azulTag`. 55 | * 56 | * @return {MetalsmithPlugin} 57 | */ 58 | var addCurrentTag = function() { 59 | var tag = cp.spawnSync('git', ['describe', '--exact-match'], 60 | { encoding: 'utf8' }).stdout.trim(); 61 | 62 | return function addCurrentTag(files, metalsmith, done) { 63 | _.forEach(files, function(data, file) { 64 | data.azulTag = tag; 65 | }); 66 | done(); 67 | }; 68 | }; 69 | 70 | /** 71 | * Creates version specific documentation `azulTag` has been set. 72 | * 73 | * This moves all documentation pages into a sub-directory for the specific 74 | * tag/version. It prevents the generation of any other pages. It also updates 75 | * all href links so the user remains on the current version documentation. The 76 | * changes made are: 77 | * 78 | * /guides/* -> /version/guides/* 79 | * /docs/* -> /version/docs/* 80 | * 81 | * @return {MetalsmithPlugin} 82 | */ 83 | var version = function() { 84 | return function version(files, metalsmith, done) { 85 | _.forEach(files, function(data, file) { 86 | if (!data.azulTag) { return; } 87 | if (file.match(/^(guides|docs)/)) { 88 | files[path.join(data.azulTag, file)] = data; 89 | 90 | var $ = cheerio.load(data.contents); 91 | $('a[href^="/guides"], a[href^="/docs"]').each(function() { 92 | $(this).attr('href', '/' + data.azulTag + $(this).attr('href')); 93 | }); 94 | data.contents = new Buffer($.html()); 95 | } 96 | delete files[file]; 97 | }); 98 | done(); 99 | }; 100 | }; 101 | 102 | /** 103 | * Moves simple HTML pages into a subdirectory by the same name for pretty links. 104 | * For instance, `page.html` would become `page/index.html`. 105 | * 106 | * @return {MetalsmithPlugin} 107 | */ 108 | var indexify = function() { 109 | return function indexify(files, metalsmith, done) { 110 | _.forEach(files, function(data, file) { 111 | var match = file.match(/(.*)\.html?$/i); 112 | if (match && !file.match(/index\.html?/i)) { 113 | files[match[1] + '/index.html'] = data; 114 | delete files[file]; 115 | } 116 | }); 117 | done(); 118 | }; 119 | }; 120 | 121 | /** 122 | * Generate a table of contents from the page content & make it available to 123 | * templates. 124 | * 125 | * @return {MetalsmithPlugin} 126 | */ 127 | var toc = function() { 128 | return function toc(files, metalsmith, done) { 129 | _.forEach(files, function(data, file) { 130 | if (!data.toc) { return; } 131 | var $ = cheerio.load(data.contents); 132 | var level = 0; 133 | var result = []; 134 | var root = { children: result }; 135 | var nodes = [root]; 136 | 137 | $('h1[id], h2[id], h3[id], h4[id]').each(function() { 138 | var headerLevel = this.tagName.match(/h(\d)/)[1] - 1; 139 | 140 | while (headerLevel < level) { 141 | delete nodes[level]; 142 | level -= 1; 143 | } 144 | 145 | while (headerLevel > level) { 146 | level += 1; 147 | if (!nodes[level]) { 148 | throw new Error('Header level skipped, cannot generate TOC.'); 149 | } 150 | nodes[level].children = nodes[level].children || []; 151 | } 152 | 153 | var parent = nodes[headerLevel]; 154 | var header = $(this); 155 | var title = header.text(); 156 | var id = header.attr('id'); 157 | 158 | // check if this is a method name & if so, fix up the links 159 | var codeTitle = header.find('code:first-of-type').text(); 160 | if (codeTitle) { 161 | title = codeTitle.replace(/^(\w*[#.]?\w+).*/i, '$1'); 162 | id = title.toLowerCase().replace(/[^a-z]/, '-').replace(/^-/, ''); 163 | if (codeTitle.match(/\w+\(.*\)/)) { 164 | title = title + '()'; 165 | } 166 | } 167 | 168 | // change ids so that they are all more unique (based on parent ids) 169 | if (level > 1 && parent && parent.id) { 170 | header.attr('id', [parent.id, id].join('-')); 171 | } 172 | 173 | var node = { 174 | id: header.attr('id'), 175 | title: title 176 | }; 177 | 178 | parent.children.push(node); 179 | nodes[headerLevel+1] = node; 180 | }); 181 | 182 | data.toc = result; 183 | data.contents = new Buffer($.html()); 184 | }); 185 | 186 | done(); 187 | }; 188 | }; 189 | 190 | var metalsmith = Metalsmith(path.join(__dirname, '..')) 191 | .source('./source') 192 | .destination('./build') 193 | .use(addKnownTags()) 194 | .use(addCurrentTag()) 195 | .use(sass()) 196 | .use(metallic()) 197 | .use(markdown()) 198 | .use(toc()) 199 | .use(templates('swig')) 200 | .use(indexify()) 201 | .use(version()); 202 | 203 | if (program.serve) { 204 | metalsmith = metalsmith 205 | .use(watch({})) 206 | .use(serve()); 207 | } 208 | 209 | metalsmith.build(function(err){ 210 | if (err) { throw err; } 211 | }); 212 | -------------------------------------------------------------------------------- /docs/scripts/deploy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # note that $GITHUB_REPO contains an API key in it, so be careful to not allow 4 | # that to end up as part of the output ever. 5 | 6 | set -e 7 | 8 | NAME=`git describe --exact-match 2> /dev/null || true` 9 | DIR="${NAME}" 10 | 11 | if [ -z "${NAME}" ]; then 12 | NAME="master" 13 | DIR="." 14 | fi 15 | 16 | if [ "`git rev-parse HEAD`" != "`git rev-parse ${NAME}~0 2> /dev/null`" ]; then 17 | echo "Not deploying docs for non-master non-tagged commit `git rev-parse --short HEAD`" 18 | exit 0 19 | fi 20 | 21 | mkdir -p deploy 22 | cd deploy 23 | git clone -q --depth 1 --branch gh-pages --single-branch $GITHUB_REPO . 24 | git config user.name $GITHUB_NAME 25 | git config user.email $GITHUB_EMAIL 26 | 27 | # remove all old content before copying over new docs 28 | if [ -d "${DIR}" ]; then 29 | find "${DIR}" \ 30 | -type f \ 31 | -not -name CNAME \ 32 | -not -name README.md \ 33 | -not -path './.*' \ 34 | -not -path './v*/guides/**' \ 35 | -exec rm {} \; 36 | fi 37 | 38 | cp -r ../docs/build/* . 39 | 40 | git add -A . 41 | git commit -m 'Site updated' || echo 'No update required.' 42 | git push -q origin gh-pages 43 | -------------------------------------------------------------------------------- /docs/source/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | header: Getting Started 4 | active: getting_started 5 | template: page.html 6 | --- 7 | 8 | # Getting Started 9 | 10 | Getting started with Azul.js is quick and easy. First, you'll need to install 11 | the library and a database adapter via `npm`. We'll use 12 | [PostgreSQL][node-postgres] in the examples on this page, but Azul.js also 13 | supports [MySQL][node-mysql] and [SQLite3][node-sqlite3]. You'll also want to 14 | install Azul.js globally to have easy access to the `azul` command line tool. 15 | 16 | ```bash 17 | $ npm install azul pg --save 18 | $ npm install azul --global 19 | ``` 20 | 21 | ## Configuration 22 | 23 | An `azulfile` allows your application and the `azul` command line tool to share 24 | connection settings. To create the `azulfile`, simply run: 25 | 26 | ```bash 27 | $ azul init postgresql # or mysql, sqlite 28 | ``` 29 | 30 | This configuration file allows the `azul` command line application to connect 31 | to your database when performing housekeeping operations on your behalf. 32 | 33 | Your application also connects to the database, and will use this file as well 34 | when you [configure your application](#application). 35 | 36 | Azul won't create databases for you automatically, so don't forget to do that: 37 | 38 | ```bash 39 | $ createuser -s root 40 | $ psql -U root -d postgres 41 | > CREATE DATABASE my_database; 42 | > \q 43 | ``` 44 | 45 | Your configuration file contains connection settings for _production_, 46 | _development_, and _test_. The `NODE_ENV` environment variable can then be used 47 | to control the environment & connection settings when running `azul` on the 48 | command line. 49 | 50 | The `azulfile` can be either a _JSON_ or _JavaScript_ file that exports the 51 | configuration. 52 | 53 | ## Application 54 | 55 | With your configuration file in place, a simple application can be built using 56 | that configuration file. 57 | 58 | ```js 59 | // get the database configuration for the current environment 60 | var env = process.env.NODE_ENV || 'development'; 61 | var config = require('./azulfile')[env]; 62 | 63 | var azul = require('azul'); 64 | var db = azul(config); 65 | 66 | var Article = db.model('article', { 67 | name: db.attr() 68 | }); 69 | ``` 70 | 71 | Once your application is configured, you can proceed by setting up 72 | [migrations][azul-migrations] and [models][azul-models]. 73 | 74 | 75 | [node-postgres]: https://github.com/brianc/node-postgres 76 | [node-mysql]: https://github.com/felixge/node-mysql/ 77 | [node-sqlite3]: https://github.com/mapbox/node-sqlite3 78 | 79 | [azul-migrations]: /guides/migrations/ 80 | [azul-models]: /guides/models/ 81 | -------------------------------------------------------------------------------- /docs/source/guides/backends.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Backends 3 | toc: true 4 | template: guide-page.html 5 | --- 6 | 7 | # Backends 8 | 9 | ## PostgreSQL 10 | 11 | All features are known to work with PostgreSQL. 12 | 13 | ### `Query#returning(column)` 14 | 15 | An additional query method, `returning` can be used to return a specific value 16 | from [insert queries][azul-queries#insert]. This method is only fully supported 17 | in PostgreSQL. 18 | 19 | ## MySQL 20 | 21 | All features are known to work with MySQL. 22 | 23 | ### `Query#returning(column)` 24 | 25 | Using MySQL, the only value that can be returned is the auto-incremented 26 | primary key. Requesting any other value will result in undefined behavior. 27 | 28 | ## SQLite3 29 | 30 | There are a few features that are currently not supported in SQLite3. If you'd 31 | like to see these improved, please open [an issue][azul-issues] or send over a 32 | [pull request][azul-pulls] with support. 33 | 34 | ### `Query#returning(column)` 35 | 36 | Using SQLite3, the only value that can be returned is the primary key if it is 37 | aliased to the [`ROWID`][sqlite-autoinc]. Requesting any other value will 38 | result in undefined behavior. 39 | 40 | ### Lookups 41 | 42 | The following lookups are unsupported without 43 | [an extension][node-sqlite-extension] that adds a 44 | [SQL function][sqlite-functions]: 45 | 46 | - `regex` and `iregex` require a `REGEXP(pattern, value)` function 47 | - `year` requires a `YEAR(date)` function 48 | - `month` requires a `MONTH(date)` function 49 | - `day` requires a `DAY(date)` function 50 | - `weekday` requires a `WEEKDAY(date)` function 51 | - `hour` requires a `HOUR(date)` function 52 | - `minute` requires a `MINUTE(date)` function 53 | - `second` requires a `SECOND(date)` function 54 | 55 | ### `date` 56 | 57 | SQLite3 stores `date` as a number & Azul.js does not currently support 58 | distinguishing this type in any way. Data will always be read from the database 59 | as numbers even if a date was used to store the value. 60 | 61 | One way around this is to use [properties][azul-core#properties] to convert 62 | the value as it comes back from the database: 63 | 64 | ```js 65 | var property = azul.Core.property; 66 | 67 | var Article = db.model('article', { 68 | publishedValue: db.attr('published'), 69 | published: property(function() { 70 | return new Date(this.publishedValue); 71 | }, function(value) { 72 | this.publishedValue = value.getTime(); 73 | }) 74 | }); 75 | 76 | var article = Article.create({ published: new Date() }); 77 | ``` 78 | 79 | ### `time` 80 | 81 | SQLite3 stores `time` as a number & Azul.js does not currently support 82 | distinguishing this type in any way. Data will always be read from the database 83 | as numbers even if a date was used to store the value. See the example in the 84 | [section above](#sqlite3-date). 85 | 86 | ### `dateTime` 87 | 88 | SQLite3 stores `dateTime` as a number & Azul.js does not currently support 89 | distinguishing this type in any way. Data will always be read from the database 90 | as numbers even if a date was used to store the value. See the example in the 91 | [section above](#sqlite3-date). 92 | 93 | 94 | [azul-issues]: https://github.com/wbyoung/azul/issues 95 | [azul-pulls]: https://github.com/wbyoung/azul/pulls 96 | [azul-core#properties]: /guides/core/#objects-extending-classes-properties 97 | [azul-queries#insert]: /guides/queries/#data-queries-insert 98 | [sqlite-autoinc]: https://www.sqlite.org/autoinc.html 99 | [sqlite-functions]: https://www.sqlite.org/c3ref/create_function.html 100 | [node-sqlite-extension]: https://github.com/mapbox/node-sqlite3/wiki/Extensions 101 | -------------------------------------------------------------------------------- /docs/source/guides/core.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Core 3 | toc: true 4 | active: guides 5 | template: guide-page.html 6 | --- 7 | 8 | # Azul.js Core 9 | 10 | This page contains documentation for basic core functionality from Azul.js that 11 | is relevant for developers using the library. 12 | 13 | ## Objects 14 | 15 | Azul.js provides methods for creating an object-oriented class hierarchy that 16 | is similar to other JavaScript libraries. It is heavily influenced by 17 | [`Ember.Object`][ember-object]. 18 | 19 | ### Creating Instances 20 | 21 | Instances can be instantiated via the `create` class method and will cause the 22 | `init` method to be called with the same arguments. For instance: 23 | 24 | ```js 25 | var Person = Class.extend({ 26 | init: function(firstName, lastName) { 27 | console.log('creating the person %s %s', firstName, lastName); 28 | } 29 | }); 30 | 31 | var person = Person.create('Whitney', 'Young'); 32 | ``` 33 | 34 | ### Extending Classes 35 | 36 | The base class object, `Class` is accessible for you to extend. For instance: 37 | 38 | ```js 39 | var azul = require('azul'), 40 | Class = azul.core.Class; 41 | 42 | var Person = Class.extend({ 43 | speak: function() { 44 | console.log('hello world'); 45 | } 46 | }); 47 | ``` 48 | 49 | #### Instance Methods 50 | 51 | Instance methods can be added via `reopen`: 52 | 53 | ```js 54 | Person.reopen({ 55 | walk: function() { 56 | console.log('The person is walking'); 57 | } 58 | }); 59 | 60 | var person = Person.create(); 61 | person.walk(); 62 | ``` 63 | 64 | Instance methods can also be added via the first argument to `Class.extend` as 65 | shown above in [Creating Instances](#objects-creating-instances). 66 | 67 | #### Class Methods 68 | 69 | Class methods, or static methods, can be added to a class via `reopenClass`: 70 | 71 | ```js 72 | Person.reopenClass({ 73 | createWoman: function() { 74 | return Person.create({ gender: 'female' }); 75 | }, 76 | createMan: function() { 77 | return Person.create({ gender: 'male' }); 78 | } 79 | }); 80 | ``` 81 | 82 | Class methods can also be added via the second argument to `Class.extend`. 83 | 84 | #### Calling `_super` 85 | 86 | All methods can call `_super` to call the method that was previously defined on 87 | the class or parent class. 88 | 89 | #### Mixins 90 | 91 | When calling `_super`, most object-oriented systems will attempt to call the 92 | method from the parent class. Azul's `_super` will call the method that it 93 | _overrides_. That method could actually be on the _same class_ rather than the 94 | parent class. This allows you to easily program with a mixin style: 95 | 96 | ```js 97 | var Mixin = azul.core.Mixin; 98 | 99 | var ValidationMixin = Mixin.create({ 100 | save: function() { 101 | if (!this.validateForSave()) { 102 | throw new Error('Validation failed!'); 103 | } 104 | return this._super(); 105 | } 106 | }); 107 | 108 | var Article = db.model('article', { 109 | updatedAt: db.attr(), 110 | 111 | save: function() { 112 | this.updatedAt = new Date(); 113 | return this._super(); 114 | } 115 | }); 116 | 117 | Article.reopen(ValidationMixin); 118 | ``` 119 | 120 | In this case, save methods will be called in this order: 121 | 122 | 1. `ValidationMixin#save` (because it was added last) 123 | 1. `Article#save` (overridden by `ValidationMixin`) 124 | 1. `Model#save` (overridden by `Article` class) 125 | 126 | #### Properties 127 | 128 | Properties can easily be added to objects as well. Imagine a person who has a 129 | given first and last names that cannot change throughout their lifetime, but 130 | also has a nickname that can change: 131 | 132 | ```js 133 | var property = azul.core.property; 134 | 135 | var Person = Class.extend({ 136 | init: function(firstName, lastName) { 137 | this._firstName = firstName; 138 | this._lastName = lastName; 139 | }, 140 | 141 | nickname: property(function() { // getter 142 | return this._nickname; 143 | }, function(nickname) { // setter 144 | this._nickname = nickname; 145 | }), 146 | 147 | // alternative, simpler nickname definition: 148 | // nickname: property({ writable: true }), 149 | 150 | firstName: property(), 151 | lastName: property(), 152 | 153 | fullName: property(function() { // getter only (readonly) 154 | var first = this.nickname || this.firstName; 155 | var last = this.lastName; 156 | return first + ' ' + last; 157 | }) 158 | }); 159 | ``` 160 | 161 | [ember-object]: http://emberjs.com/guides/object-model/classes-and-instances/ 162 | -------------------------------------------------------------------------------- /docs/source/guides/express/anti-patterns.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Express Addon Anti-Patterns 3 | toc: true 4 | template: guide-page.html 5 | --- 6 | 7 | # Express Addon Anti-Patterns 8 | 9 | ## Anti-pattern: Naming Models 10 | 11 | The idea of naming your model classes when you define them is appealing to 12 | allow easy re-use. With this addon, though, it is considered an anti-pattern 13 | because it leads to easily overlooked mistakes. 14 | 15 | The code below illustrates this idea. The definition of `Article` and `Author` 16 | in the first two lines, hides the following mistakes: 17 | 18 | - `Author` is not provided as part in the second route's decorations 19 | - `Article` is misspelled in the second route's decorations 20 | 21 | This leads to the following bugs: 22 | 23 | - Uses of `Author` in the second route will not be bound to the transaction 24 | - Uses of `Article` in the second route will not be bound to the transaction 25 | 26 | Essentially, even though it appears that the second route is executing all 27 | queries in a transaction, none of them are. 28 | 29 | ```js 30 | var Article = db.model('article', { title: db.attr(), author: db.belongsTo(), }); 31 | var Author = db.model('author', { name: db.attr(), articles: db.hasMany(), }); 32 | 33 | var transaction = azulExpress.transaction; 34 | var route = azulExpress.route; 35 | 36 | // no transaction 37 | app.get('/articles', function(req, res) { 38 | return Article.objects.fetch().then(function(articles) { 39 | res.send({ articles: _.map(articles, 'json') }); 40 | }); 41 | })); 42 | 43 | // transaction enabled 44 | app.post('/articles', transaction, route(function(req, res, Aritcle) { 45 | return Author.objects.findOrCreate({ name: req.body.author }) 46 | .then(function(author) { 47 | return Article.create({ author: author title: req.body.title }).save(); 48 | }) 49 | .then(function(article) { 50 | res.send({ article: article.json }); 51 | }); 52 | })); 53 | ``` 54 | 55 | The above code has one minor advantage: routes that do not use transactions do 56 | not require the use of the addon's [`route`][azul-express#decorated-route] 57 | wrapper for dependency injection since the models are accessible globally. This 58 | saves a few keystrokes, but the disadvantages outlined above outweigh the few 59 | keys saved. 60 | 61 | ### Solution: Dependency Injection 62 | 63 | The corrected version does not name the model classes and instead uses 64 | a [decorated route][azul-express#decorated-route] that provides dependency 65 | injection. The two issues outlined above would have become very clear since 66 | both would have produced runtime errors. 67 | 68 | ```js 69 | db.model('article', { title: db.attr(), author: db.belongsTo(), }); 70 | db.model('author', { name: db.attr(), articles: db.hasMany(), }); 71 | 72 | var transaction = azulExpress.transaction; 73 | var route = azulExpress.route; 74 | 75 | // no transaction 76 | app.get('/articles', route(function(req, res, Article) { 77 | return Article.objects.fetch().then(function(articles) { 78 | res.send({ articles: _.map(articles, 'json') }); 79 | }); 80 | })); 81 | 82 | // transaction enabled 83 | app.post('/articles', transaction, route(function(req, res, Author, Article) { 84 | return Author.objects.findOrCreate({ name: req.body.author }) 85 | .then(function(author) { 86 | return Article.create({ author: author title: req.body.title }).save(); 87 | }) 88 | .then(function(article) { 89 | res.send({ article: article.json }); 90 | }); 91 | })); 92 | ``` 93 | 94 | You could also define your models in a separate module and not import them into 95 | the module that defines your Express routes. 96 | 97 | [azul-express#decorated-route]: /guides/express/#methods-properties-route 98 | -------------------------------------------------------------------------------- /docs/source/guides/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Guides 3 | active: guides 4 | template: guide-page.html 5 | --- 6 | 7 | # Guides 8 | 9 | ## Available Guides 10 | 11 | [Core][azul-core] 12 | Basic core functionality. 13 | 14 | [Express Addon][azul-express] 15 | Addon for using Azul.js with Express. 16 | 17 | [Express Addon Anti-Patterns][azul-express-anti-patterns] 18 | Express addon anti-patterns. 19 | 20 | [Managers][azul-managers] 21 | Pre-configured queries for frequently accessed collection. 22 | 23 | [Migrations][azul-migrations] 24 | Migration & schema information. 25 | 26 | [Models][azul-models] 27 | Models are the basic unit of data within Azul.js. 28 | 29 | [Queries][azul-queries] 30 | Queries in Azul.js allow you to interact with data from the database. 31 | 32 | [Relations][azul-relations] 33 | Build relationships between models. 34 | 35 | [Transactions][azul-transactions] 36 | Database transaction support. 37 | 38 | [azul-models]: /guides/models/ 39 | [azul-core]: /guides/core/ 40 | [azul-express]: /guides/express/ 41 | [azul-express-anti-patterns]: /guides/express/anti-patterns/ 42 | [azul-managers]: /guides/managers/ 43 | [azul-migrations]: /guides/migrations/ 44 | [azul-queries]: /guides/queries/ 45 | [azul-relations]: /guides/relations/ 46 | [azul-transactions]: /guides/transactions/ 47 | -------------------------------------------------------------------------------- /docs/source/guides/managers.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Managers 3 | toc: true 4 | active: guides 5 | template: guide-page.html 6 | --- 7 | 8 | # Managers 9 | 10 | Managers allow you to pre-configure queries for collections of objects that 11 | are frequently accessed. You can define new collections on your model and 12 | also have the ability to override the default `objects` collection. 13 | 14 | ## Basic Example 15 | 16 | For example, setting up custom managers to allow quick access to 17 | `Person.men` and `Person.women` would be done like so: 18 | 19 | ```js 20 | var Manager = azul.manager; 21 | 22 | var FemaleManager = Manager.extend({ 23 | query: function() { 24 | return this._super().where({ sex: 'female' }); 25 | } 26 | }); 27 | 28 | var MaleManager = Manager.extend({ 29 | query: function() { 30 | return this._super().where({ sex: 'male' }); 31 | } 32 | }); 33 | 34 | var Person = db.model('person').reopenClass({ 35 | women: FemaleManager.create(), 36 | men: MaleManager.create() 37 | }); 38 | 39 | Person.men.where({ age$lt: 12 }).fetch().then(function(people) { 40 | // people is all men under the age of 12 41 | }); 42 | 43 | Person.women.where({ age$gt: 25 }).fetch().then(function(people) { 44 | // people is all women over the age of 25 45 | }); 46 | ``` 47 | 48 | ## Overriding the Default Manager 49 | 50 | It is also possible to override the default `objects`. For instance, having an 51 | `Article` model default to all `published` articles would be done like so: 52 | 53 | ```js 54 | var PublishedManager = Manager.extend({ 55 | query: function() { 56 | return this._super().where({ published: true }); 57 | } 58 | }); 59 | 60 | Article.reopenClass({ 61 | objects: PublishedManager.create(), 62 | allObjects: Manager.create() 63 | }); 64 | ``` 65 | 66 | Note that `allObjects` has been added as a simple manager, so that it is still 67 | possible to access articles that have not been published. 68 | -------------------------------------------------------------------------------- /docs/source/guides/transactions.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Transactions 3 | toc: true 4 | template: guide-page.html 5 | --- 6 | 7 | # Transactions 8 | 9 | ## Basics 10 | 11 | Transactions are straightforward to use in Azul.js. You simply create a new 12 | transaction object via [`db.transaction()`][azul-queries#transactions]. That 13 | object can then be used to create [`begin`](#methods-begin), 14 | [`commit`](#methods-commit), and [`rollback`](#methods-rollback) queries. 15 | Here's a quick example: 16 | 17 | ```js 18 | var transaction = db.transaction(); 19 | 20 | transaction.begin().execute() 21 | .then(function() { 22 | return User.objects 23 | .transaction(transaction) // associate with transaction 24 | .update({ username: 'azul' }).where({ pk: 25 }); 25 | }) 26 | .then(function() { 27 | return profile.save({ transaction: transaction }); 28 | }) 29 | .then(function() { return transaction.commit(); }) 30 | .catch(function() { return transaction.rollback(); }); 31 | ``` 32 | 33 | Read on to see how to associate transactions with [queries](#with-queries) and 34 | [models](#with-models). If you're using Express, we recommend reading the 35 | [Express guide][azul-express] as well which makes using transactions on a 36 | per-request basis extremely simple. 37 | 38 | ## Methods 39 | 40 | ### `#begin()` 41 | 42 | Create a new begin query. Make sure you execute this query before any queries 43 | within the transaction. 44 | 45 | ### `#commit()` 46 | 47 | Create a new commit query. 48 | 49 | ### `#rollback()` 50 | 51 | Create a new rollback query. 52 | 53 | ## With Queries 54 | 55 | As shown in the initial example, you can call 56 | [`transaction`][azul-queries#transaction-method] on any query to generate a 57 | query that will run in that transaction. When you plan to execute many queries 58 | in a transaction, it may be useful to reuse that query. 59 | 60 | ```js 61 | var transaction = db.transaction(); 62 | var articles = Article.objects.transaction(transaction); 63 | 64 | transaction.begin().execute() 65 | .then(function() { 66 | return articles.update({ title: 'Azul.js' }).where({ title: 'Azul' }); 67 | }) 68 | .then(function() { 69 | return articles.insert({ title: 'Azul.js Launch' }); 70 | }) 71 | .then(function() { return transaction.commit(); }) 72 | .catch(function() { return transaction.rollback(); }); 73 | ``` 74 | 75 | ## With Models 76 | 77 | As shown in the initial example, you can pass `transaction` as an option when 78 | you [`save`][azul-model#save] a model. 79 | 80 | ## Nested Transactions 81 | 82 | Azul.js supports nested transactions as well. Simply execute additional begins: 83 | 84 | ```js 85 | var transaction = db.transaction(); 86 | 87 | transaction.begin().execute() 88 | .then(function() { /* something else */ }) 89 | .then(function() { return transaction.begin(); }) 90 | .then(function() { /* something else */ }) 91 | .then(function() { /* something else */ }) 92 | .then(function() { return transaction.commit(); }) 93 | .then(function() { return transaction.commit(); }) 94 | .catch(function() { return transaction.rollback(); }); 95 | ``` 96 | 97 | [azul-express]: /guides/express/ 98 | [azul-model#save]: /guides/models/#methods-properties-save 99 | [azul-queries#transactions]: /guides/queries/#transactions 100 | [azul-queries#transaction-method]: /guides/queries/#transactions-transaction 101 | -------------------------------------------------------------------------------- /docs/source/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Azul.js 3 | active: home 4 | template: home.html 5 | --- 6 | 7 |

8 | 9 | # Start Quickly 10 | 11 | Azul is easy to [install and start using][azul-getting-started]. 12 | 13 | ```bash 14 | $ npm install azul -g 15 | $ npm install azul 16 | $ azul init 17 | ``` 18 | 19 | Give it a try today! 20 | 21 | # Work Efficiently 22 | 23 | Azul.js exposes a simple API that follows patterns found in popular tools 24 | available in other languages like [Django][django] and [Ruby on Rails][rails]. 25 | You'll feel right at home when working with Azul.js. Even if you haven't worked 26 | with other ORMs, our [documentation][azul-getting-started] is top notch, so 27 | you'll be building stellar apps in no time. 28 | 29 | ```js 30 | var Article = db.model('article', { 31 | title: db.attr(), 32 | author: db.belongsTo('user') 33 | }); 34 | 35 | var User = db.model('user', { 36 | name: db.attr(), 37 | username: db.attr(), 38 | articles: db.hasMany('article', { inverse: 'author' }) 39 | }); 40 | 41 | Article.objects.with('author').fetch().then(/* ... */); 42 | 43 | // -> select * from "articles"; 44 | // -> select * from "users" where "id" in (?, ?, ?) limit 3; 45 | // !> [3,5,8] 46 | ``` 47 | 48 | # Express Yourself 49 | 50 | Azul.js was designed to be the most expressive ORM for Node.js. You'll write 51 | good code that others can easily understand. Want to find all articles that 52 | include a comment that is spam? Simple. 53 | 54 | ```js 55 | Article.objects.where({ 'comments.spam': true }).fetch().then(/* ... */); 56 | 57 | // -> select "articles".* from "articles" 58 | // -> inner join "comments" on "comments"."article_id" = "articles"."id" 59 | // -> where "comments"."spam" = ? 60 | // -> group by "articles"."id" 61 | // !> [true] 62 | ``` 63 | 64 | # Join Us 65 | 66 | From the beginning, Azul.js was created with ease of use and quality in mind. 67 | Focus has been placed on expressiveness of the API, 68 | [documentation][azul-api-docs], security, [code quality][azul-codeclimate], and 69 | [test coverage][azul-coveralls]. 70 | 71 | We'd love your help in making it even better. :) 72 | 73 | 74 | [django]: https://djangoproject.com/ 75 | [rails]: http://rubyonrails.org/ 76 | [azul-getting-started]: /getting-started/ 77 | [azul-codeclimate]: https://codeclimate.com/github/wbyoung/azul 78 | [azul-coveralls]: https://coveralls.io/r/wbyoung/azul 79 | [azul-api-docs]: https://github.com/wbyoung/azul#documentation 80 | -------------------------------------------------------------------------------- /docs/source/releases.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Releases 3 | header: Releases 4 | template: releases.html 5 | --- 6 | 7 | # Releases 8 | 9 | Documentation is available for all past releases: 10 | -------------------------------------------------------------------------------- /docs/source/styles/application.scss: -------------------------------------------------------------------------------- 1 | $color-1: #ced7f2; 2 | $color-2: #22648c; 3 | $color-3: #224459; 4 | $color-4: #26a3bf; 5 | $color-5: #72dbf2; 6 | 7 | body { 8 | font-family: 'Open Sans', sans-serif; 9 | } 10 | 11 | body > .container { 12 | font-size: 11pt; 13 | line-height: 1.6em; 14 | 15 | h1, h2, h3, h4 { 16 | color: $color-4; 17 | &:before { 18 | display: block; 19 | content: ""; 20 | padding-top: 70px; 21 | margin-top: -70px; 22 | } 23 | } 24 | 25 | h1, h2, h3, h4, .btn { 26 | font-weight: 300; 27 | } 28 | 29 | h2 { margin-top: 1.6em; } 30 | h1 + h2 { margin-top: 0.6em; } 31 | 32 | h3 { margin-top: 2em; } 33 | h2 + h3 { margin-top: 1em; } 34 | 35 | h4 { margin-top: 2em; } 36 | h3 + h4 { margin-top: 1em; } 37 | 38 | } 39 | 40 | body > .container { 41 | padding-top: 70px; 42 | padding-bottom: 90px; 43 | } 44 | 45 | body > .jumbotron + .container { 46 | padding-top: 0px; 47 | } 48 | 49 | .navbar-default { 50 | background-color: $color-2; 51 | border: 0px; 52 | } 53 | 54 | .navbar-default .navbar-brand, 55 | .navbar-default .navbar-nav > li > a { 56 | &, &:hover, &:focus { 57 | color: #fff; 58 | } 59 | } 60 | 61 | .navbar-default .navbar-nav > .open > a, 62 | .navbar-default .navbar-nav > .active > a { 63 | &, &:hover, &:focus { 64 | background-color: $color-3; 65 | color: #fff; 66 | } 67 | } 68 | 69 | // when nav bar toggle is shown 70 | @media (max-width: 767px) { 71 | .navbar-default .navbar-toggle { 72 | border: 0px; 73 | } 74 | 75 | .navbar-default .navbar-toggle .icon-bar { 76 | background-color: lighten($color-2, 10%); 77 | } 78 | 79 | .navbar-default .navbar-toggle:hover, 80 | .navbar-default .navbar-toggle:focus { 81 | background-color: transparent; 82 | .icon-bar { 83 | background-color: lighten($color-2, 80%); 84 | } 85 | } 86 | 87 | .navbar-default .navbar-collapse, 88 | .navbar-default .navbar-form { 89 | border-width: 0px; 90 | background-color: lighten($color-2, 3%); 91 | -webkit-box-shadow: none; 92 | box-shadow: none; 93 | } 94 | 95 | .navbar-default .navbar-nav .open .dropdown-menu > li > a { 96 | &, &:hover, &:focus { 97 | color: #fff; 98 | } 99 | } 100 | 101 | .dropdown-menu .divider { 102 | background-color: $color-2; 103 | } 104 | } 105 | 106 | 107 | body.generic > .jumbotron { 108 | background-color: $color-5; 109 | border-bottom: 4px solid darken($color-5, 5%); 110 | padding: 3.2em 0px 0.6em 0px; 111 | color: #fff; 112 | 113 | h1 { 114 | font-size: 300%; 115 | font-weight: 300; 116 | 117 | a.release { 118 | color: #fff; 119 | font-size: 40%; 120 | &:hover { text-decoration: none; } 121 | } 122 | } 123 | } 124 | 125 | body.generic > .jumbotron + .container h1 { 126 | display: none; 127 | } 128 | 129 | body.home > .jumbotron { 130 | background-color: $color-2; 131 | border-bottom: 4px solid darken($color-2, 5%); 132 | color: #fff; 133 | 134 | h1 { 135 | font-family: Satisfy, cursive; 136 | text-transform: lowercase; 137 | margin-bottom: 0.5em; 138 | text-align: center; 139 | font-size: 800%; 140 | } 141 | } 142 | 143 | pre { 144 | max-height: 340px; 145 | overflow-y: scroll; 146 | } 147 | 148 | pre code.hljs { 149 | overflow-wrap: normal; 150 | white-space: pre; 151 | overflow-x: visible; 152 | background-color: transparent; 153 | } 154 | 155 | .guide-toc ul { 156 | padding-left: 0px; 157 | list-style-type: none; 158 | } 159 | 160 | .guide-toc ul ul { 161 | margin-bottom: 0.8em; 162 | padding-left: 1.2em; 163 | font-size: 95%; 164 | } 165 | 166 | .guide-toc ul ul ul { 167 | margin-bottom: 0em; 168 | padding-left: 1.8em; 169 | list-style-type: circle; 170 | } 171 | 172 | .guide-toc ul ul ul ul { 173 | margin-bottom: 0em; 174 | } 175 | -------------------------------------------------------------------------------- /docs/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {% block title %}Azul.js — {{ title }}{% endblock %} 10 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | 21 | 62 | {% block content %} 63 |
64 | {{ contents|safe }} 65 |
66 | {% endblock %} 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /docs/templates/guide-page.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 | {% if toc[0] %} 6 |
7 |
8 |

{{ toc[0].title }} {% if azulTag %}{{ azulTag }}{% else %}dev{% endif %}

9 |
10 |
11 | 12 |
13 |
14 |
15 | 16 | {% for h1Section in toc %} 17 |
    18 | {% for h2Section in h1Section.children %} 19 |
  • 20 | {{ h2Section.title }} 21 |
      22 | {% for h3Section in h2Section.children %} 23 |
    • 24 | {{ h3Section.title }} 25 |
        26 | {% for h4Section in h3Section.children %} 27 |
      • 28 | {{ h4Section.title }} 29 |
      • 30 | {% endfor %} 31 |
      32 |
    • 33 | {% endfor %} 34 |
    35 |
  • 36 | {% endfor %} 37 |
38 | {% endfor %} 39 | 40 |
41 |
42 | 43 | {{ contents|safe }} 44 | 45 |
46 |
47 |
48 | 49 | {% else %} 50 | 51 |
52 |
53 |

{{ title }} {% if azulTag %}{{ azulTag }}{% else %}dev{% endif %}

54 |
55 |
56 | 57 |
58 | {{ contents|safe }} 59 |
60 | {% endif %} 61 | 62 | {% endblock %} 63 | -------------------------------------------------------------------------------- /docs/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Azul.js{% endblock %} 4 | {% block body-class %}home{% endblock %} 5 | {% block content %} 6 |
7 |
8 |

Azul.js

9 |

10 | The most elegant object-relational mapping tool for Node.js. 11 | Designed with ease of use in mind, Azul.js allows you to write 12 | simple, expressive statements to access your application's data. 13 |

14 |
15 |
16 | 17 | {% parent %} 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /docs/templates/page.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 | {% if header %} 6 |
7 |
8 |

{{ header }}

9 |
10 |
11 | {% endif %} 12 | 13 |
14 | {% block contained-content %} 15 | {{ contents|safe }} 16 | {% endblock %} 17 |
18 | 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /docs/templates/releases.html: -------------------------------------------------------------------------------- 1 | {% extends "page.html" %} 2 | 3 | {% block contained-content %} 4 | {{ contents|safe }} 5 | 6 | 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./lib'); 4 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "include": ["lib"] 4 | }, 5 | "opts": { 6 | "template": "./node_modules/ink-docstrap/template", 7 | "recurse": true 8 | }, 9 | "plugins": [ "plugins/markdown" ], 10 | "tags" : { 11 | "allowUnknownTags" : true 12 | }, 13 | "templates" : { 14 | "cleverLinks" : false, 15 | "monospaceLinks" : false, 16 | "dateFormat" : "ddd MMM Do YYYY", 17 | "outputSourceFiles" : true, 18 | "outputSourcePath" : true, 19 | "systemName" : "Azul", 20 | "footer" : "", 21 | "copyright" : "Azul Copyright © 2014 Whitney Young.", 22 | "navType" : "vertical", 23 | "theme" : "simplex", 24 | "linenums" : false, 25 | "collapseSymbols" : false, 26 | "inverseNav" : true, 27 | "customCSS": "hello", 28 | "highlightTutorialCode" : true, 29 | "protocol": "fred://" 30 | }, 31 | "markdown" : { 32 | "parser" : "gfm", 33 | "hardwrap" : true 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/cli/actions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var path = require('path'); 5 | var fs = require('fs'); 6 | var chalk = require('chalk'); 7 | var util = require('util'); 8 | var env = process.env.NODE_ENV || 'development'; 9 | var Database = require('../database'); 10 | 11 | /** 12 | * Run the migrator. 13 | * 14 | * @private 15 | * @function runMigrator 16 | * @param {String} action Either `migrate` or `rollback`. 17 | * @param {Object} azulfile Configuration object. 18 | * @param {Object} options 19 | * @param {String} options.migrations 20 | * @param {Object} strings The output format strings to use. 21 | * @return {Promise} 22 | */ 23 | var runMigrator = function(action, azulfile, options, strings) { 24 | var db = Database.create(azulfile[env]); 25 | var migrator = db.migrator(path.resolve(options.migrations)); 26 | var message = ''; 27 | var batches = ''; 28 | 29 | return migrator[action]() 30 | .tap(function(migrations) { 31 | batches = _(migrations).pluck('batch').unique().join(', '); 32 | }) 33 | .then(function(migrations) { 34 | message += migrations.length ? 35 | chalk.magenta(strings.intro, 'migrations, batch', batches, '\n') : 36 | chalk.magenta(strings.none, '\n'); 37 | migrations.forEach(function(migration) { 38 | message += chalk.cyan(migration.name, '\n'); 39 | }); 40 | message += chalk.gray(_.capitalize(env), 'environment\n'); 41 | process.stdout.write(message); 42 | }) 43 | .catch(function(e) { 44 | message += chalk.red(strings.action, 'failed.', e); 45 | process.stderr.write(message); 46 | process.exit(1); 47 | }) 48 | .finally(function() { db.disconnect(); }); 49 | }; 50 | 51 | /** 52 | * Migrate CLI action. 53 | * 54 | * @public 55 | * @function 56 | * @see runMigrator 57 | */ 58 | module.exports.migrate = function(azulfile, options) { 59 | return runMigrator('migrate', azulfile, options, { 60 | intro: 'Applying', 61 | action: 'Migration', 62 | none: 'Migrations are up-to-date', 63 | }); 64 | }; 65 | 66 | /** 67 | * Rollback CLI action. 68 | * 69 | * @public 70 | * @function 71 | * @see runMigrator 72 | */ 73 | module.exports.rollback = function(azulfile, options) { 74 | return runMigrator('rollback', azulfile, options, { 75 | intro: 'Rolling back', 76 | action: 'Rollback', 77 | none: 'Nothing to rollback, you are at the beginning', 78 | }); 79 | }; 80 | 81 | /** 82 | * Initialization CLI action. 83 | * 84 | * @public 85 | * @function 86 | * @param {Object} options 87 | * @param {String} db The database adapter name to use. 88 | */ 89 | module.exports.init = function(options, db) { 90 | db = require('maguey').adapters.aliases[db || 'pg']; 91 | 92 | if (fs.existsSync('azulfile.js') || fs.existsSync('azulfile.json')) { 93 | process.stdout.write(chalk.gray('Already initialized\n')); 94 | } 95 | else if (!db) { 96 | process.stderr.write(chalk.red( 97 | util.format('Invalid database: %s\n', db))); 98 | process.exit(1); 99 | } 100 | else { 101 | var source = path.join(__dirname, '../templates/azulfile.js.template'); 102 | var content = fs.readFileSync(source); 103 | var templated = _.template(content)({ database: db }); 104 | fs.writeFileSync('azulfile.js', templated); 105 | process.stdout.write(chalk.magenta('Initialization complete\n')); 106 | } 107 | }; 108 | 109 | /** 110 | * Make migration CLI action. 111 | * 112 | * @public 113 | * @function 114 | * @param {Object} azulfile Configuration object. 115 | * @param {Object} options 116 | * @param {String} name The name of the migration. 117 | */ 118 | module.exports['make-migration'] = function(azulfile, options, name) { 119 | try { fs.mkdirSync('migrations'); } 120 | catch (e) { if (e.code !== 'EEXIST') { throw e; } } 121 | 122 | var prefix = new Date().toISOString().split('.')[0].replace(/[^\d]/g, ''); 123 | var fileName = [prefix, _.snakeCase(name) + '.js'].join('_'); 124 | 125 | var source = path.join(__dirname, '../templates/migration.js.template'); 126 | var dest = path.join('migrations', fileName); 127 | var content = fs.readFileSync(source); 128 | var templated = _.template(content)({ name: name }); 129 | fs.writeFileSync(dest, templated); 130 | process.stdout.write(chalk.magenta(fileName + '\n')); 131 | }; 132 | -------------------------------------------------------------------------------- /lib/compatibility.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Validate compatibility with other modules. 5 | * 6 | * @private 7 | * @function validateCompatibility 8 | */ 9 | module.exports = function() { 10 | var BaseQuery = require('maguey').BaseQuery; 11 | var Class = require('corazon').Class; 12 | if (!(BaseQuery instanceof Class.__metaclass__)) { 13 | throw new Error('Incompatible base class between `azul` and `maguey`.'); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /lib/database.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var Promise = require('bluebird'); 5 | var adapter = require('maguey').adapter; 6 | var Class = require('corazon/class'); 7 | var property = require('corazon/property'); 8 | var EntryQuery = require('maguey').EntryQuery; 9 | var Migration = require('./migration'); 10 | var Model = require('./model'); 11 | 12 | /** 13 | * @class Database 14 | */ 15 | var Database = Class.extend(/** @lends Database# */ { 16 | 17 | /** 18 | * Create a database. 19 | * 20 | * @public 21 | * @constructor Database 22 | * @param {Object} options The connection information. 23 | * @param {String} options.adapter The adapter to use. Possible choices are: 24 | * `pg`, `mysql`, or `sqlite3`. 25 | * @param {Object} options.connection The connection information to pass to 26 | * the adapter. This varies for each adapter. See each individual adapters 27 | * for more information. 28 | */ 29 | init: function(options) { 30 | this._super(); 31 | 32 | this._adapter = adapter(options); 33 | this._query = EntryQuery.create(this._adapter); 34 | this._schema = this._query.schema(); 35 | this._modelClasses = {}; 36 | }, 37 | 38 | /** 39 | * Disconnect a database. 40 | * 41 | * @return {Promise} A promise indicating that the database has been 42 | * disconnected. 43 | */ 44 | disconnect: Promise.method(function() { 45 | return this._adapter.disconnectAll(); 46 | }), 47 | 48 | /** 49 | * Get a new migrator to handle migrations at the given path. 50 | * 51 | * @param {String} migrationsPath The path to the directory containing 52 | * migrations. 53 | * @return {Migration} The migrator 54 | */ 55 | migrator: function(migrationsPath) { 56 | return Migration.create(this._query, migrationsPath); 57 | }, 58 | 59 | }); 60 | 61 | Database.reopen(/** @lends Database# */ { 62 | 63 | /** 64 | * The base model class for this database. 65 | * 66 | * While accessible, subclassing this class directly is strongly discouraged 67 | * and may no be supported in the future. Instead use {@link Database.model}. 68 | * 69 | * @public 70 | * @type {Class} 71 | * @readonly 72 | */ 73 | Model: property(function() { 74 | if (this._modelClass) { return this._modelClass; } 75 | 76 | this._modelClass = Model.extend({}, { 77 | db: this, 78 | adapter: this._adapter, 79 | query: this._query, 80 | }); 81 | 82 | return this._modelClass; 83 | }), 84 | 85 | /** 86 | * Create a new model class or retrieve an existing class. 87 | * 88 | * This is the preferred way of creating new model classes as it also stores 89 | * the model class by name, allowing you to use strings in certain places to 90 | * refer to classes (i.e. when defining relationships). 91 | * 92 | * @param {String} name The name for the class 93 | * @param {Object} [properties] Properties to add to the class 94 | * @return {Class} The model class 95 | */ 96 | model: function(name, properties) { 97 | var className = _.capitalize(_.camelCase(name)); 98 | var known = this._modelClasses; 99 | var model = known[className]; 100 | if (!model) { 101 | model = known[className] = 102 | this.Model.extend({}, { __name__: className }); 103 | } 104 | return model.reopen(properties); 105 | }, 106 | 107 | // convenience methods (documentation at original definitions) 108 | attr: Model.attr, 109 | hasMany: Model.hasMany, 110 | hasOne: Model.hasOne, 111 | belongsTo: Model.belongsTo, 112 | }); 113 | 114 | /** 115 | * A convenience method for tapping into a named query method. This is 116 | * basically a curry that allows quick definition on the database of various 117 | * query convenience methods, for instance: 118 | * 119 | * Database.reopen({ select: query('select') }) 120 | * db.select('users') // -> db.query.select('users') 121 | * 122 | * @private 123 | * @function Database~query 124 | */ 125 | var query = function(name) { 126 | return function() { 127 | return this._query[name].apply(this._query, arguments); 128 | }; 129 | }; 130 | 131 | /** 132 | * Access to a query object that is tied to this database. 133 | * 134 | * @name Database#query 135 | * @public 136 | * @type {EntryQuery} 137 | * @readonly 138 | */ 139 | Database.reopen({ query: property() }); 140 | 141 | /** 142 | * Access to a schema object that is tied to this database. 143 | * 144 | * @name Database#schema 145 | * @public 146 | * @type {Schema} 147 | * @readonly 148 | */ 149 | Database.reopen({ schema: property() }); 150 | 151 | Database.reopen(/** @lends Database# */ { 152 | 153 | /** 154 | * Shortcut for `db.query.select()`. 155 | * 156 | * @method 157 | * @public 158 | * @see {@link Database#query} 159 | * @see {@link EntryQuery#select} 160 | */ 161 | select: query('select'), 162 | 163 | /** 164 | * Shortcut for `db.query.insert()`. 165 | * 166 | * @method 167 | * @public 168 | * @see {@link Database#query} 169 | * @see {@link EntryQuery#insert} 170 | */ 171 | insert: query('insert'), 172 | 173 | /** 174 | * Shortcut for `db.query.update()`. 175 | * 176 | * @method 177 | * @public 178 | * @see {@link Database#query} 179 | * @see {@link EntryQuery#update} 180 | */ 181 | update: query('update'), 182 | 183 | /** 184 | * Shortcut for `db.query.delete()`. 185 | * 186 | * @method 187 | * @public 188 | * @see {@link Database#query} 189 | * @see {@link EntryQuery#delete} 190 | */ 191 | delete: query('delete'), 192 | 193 | /** 194 | * Shortcut for `db.query.transaction()`. 195 | * 196 | * @method 197 | * @public 198 | * @see {@link Database#query} 199 | * @see {@link EntryQuery#transaction} 200 | */ 201 | transaction: query('transaction'), 202 | 203 | }); 204 | 205 | module.exports = Database.reopenClass({ __name__: 'Database' }); 206 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | 5 | var Database = require('./database'); 6 | var Model = require('./model'); 7 | var Manager = require('./model/manager'); 8 | var Condition = require('maguey').Condition; 9 | var Migration = require('./migration'); 10 | var Adapter = require('maguey').Adapter; 11 | var core = { 12 | Class: require('corazon/class'), 13 | Mixin: require('corazon/mixin'), 14 | property: require('corazon/property'), 15 | }; 16 | 17 | /** 18 | * Main Azul.js export. Creates a database. 19 | * 20 | * @param {Object} config 21 | * @return {Database} 22 | * @see {@link Database} 23 | */ 24 | var azul = function(config) { 25 | return Database.create(config); 26 | }; 27 | 28 | require('./compatibility')(); 29 | 30 | module.exports = _.extend(azul, { 31 | Model: Model, 32 | Manager: Manager, 33 | Database: Database, 34 | Migration: Migration, 35 | Adapter: Adapter, 36 | Condition: Condition, 37 | w: Condition.w, 38 | f: Condition.f, 39 | l: Condition.l, 40 | attr: Model.attr, 41 | hasMany: Model.hasMany, 42 | belongsTo: Model.belongsTo, 43 | core: core, 44 | }); 45 | -------------------------------------------------------------------------------- /lib/model/attr.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var Property = require('corazon').Property; 5 | 6 | /** 7 | * Generate closure around calculation of attributes. 8 | * 9 | * @function Attr~attrs 10 | * @private 11 | * @return {Function} 12 | */ 13 | var attrs = function() { 14 | 15 | // we need to iterate the class hierarchy ourselves because we've defined 16 | // specific requirements around the preferring attributes that are defined 17 | // later. _.keysIn and for/in will not get us the desired order. 18 | var hierarchy = function(cls) { // jscs:ignore jsDoc 19 | var result = []; 20 | while (cls) { result.unshift(cls); cls = cls.__super__; } 21 | return result; 22 | }; 23 | 24 | // return the function with the value locked in 25 | return _.once(function() { 26 | var regex = /(.*)Attr$/; 27 | return _(hierarchy(this)) 28 | .map('__class__.prototype') // get all prototypes 29 | .map(_.keys).flatten() // get all keys 30 | .invoke('match', regex).map(0).filter() // find attrs 31 | .reverse() // reverse to prefer later attrs in unique 32 | .uniq(_.propertyOf(this.__class__.prototype)) // unique based on dbattr 33 | .invoke('match', regex).map(1) // get the real attr name 34 | .reverse().value(); // return to normal order 35 | }); 36 | }; 37 | 38 | /** 39 | * @class Attr 40 | * @extends Property 41 | * @classdesc 42 | */ 43 | var Attr = Property.extend(/** @lends Attr# */ { 44 | 45 | /** 46 | * Create an Attr 47 | * 48 | * @public 49 | * @constructor Attr 50 | * @param {String} column The column name for which this is an attribute. 51 | * @param {Object} [options] Options. 52 | * @param {Object} [options.writable] Whether this attribute is writable. 53 | * Defaults to true. 54 | */ 55 | init: function(column, options) { 56 | var opts = _.defaults({}, options, { writable: true }); 57 | this._column = column; 58 | this._super(_.pick(opts, 'writable')); 59 | }, 60 | 61 | /** 62 | * Convenience method to look up the attribute name. 63 | * 64 | * @method 65 | * @private 66 | * @param {String} string The name being while this property is being defined 67 | * on a class. 68 | */ 69 | _attr: function(name) { 70 | return this._column || _.snakeCase(name); 71 | }, 72 | 73 | /** 74 | * General attribute initialization method that overrides 75 | * {@link Model#_initAttributes} on the model this is mixed into. This calls 76 | * super & then calls the custom attribute initializer. 77 | * 78 | * @method 79 | * @protected 80 | * @see {@link Model#_initAttributes} 81 | * @see {@link Attr#_init} 82 | */ 83 | _initAttributes: function(opts) { 84 | var initName = opts.name + 'Init'; 85 | return function() { 86 | this._super.apply(this, arguments); 87 | this[initName].apply(this, arguments); 88 | }; 89 | }, 90 | 91 | /** 92 | * Custom attribute initializer that will be mixed in as `init`. This 93 | * simply initializes the attribute to `undefined`. It may be overridden by 94 | * model classes to change the default value of a property. Any changes made 95 | * during this method will not make the model dirty. 96 | * 97 | * @method 98 | * @protected 99 | * @see {@link Model#_initAttributes} 100 | * @see {@link Attr#_init} 101 | */ 102 | _init: function(opts) { 103 | var attr = this._attr(opts.name); 104 | return function() { 105 | this.setAttribute(attr, undefined); 106 | }; 107 | }, 108 | 109 | /** 110 | * Override of {@link Property#_getter}. 111 | * 112 | * @method 113 | * @protected 114 | * @see {@link Property#_getter} 115 | */ 116 | _getter: function(opts) { 117 | var attr = this._attr(opts.name); 118 | return function() { 119 | return this.getAttribute(attr); 120 | }; 121 | }, 122 | 123 | /** 124 | * Override of {@link Property#_setter}. 125 | * 126 | * @method 127 | * @protected 128 | * @see {@link Property#_setter} 129 | */ 130 | _setter: function(opts) { 131 | var attr = this._attr(opts.name); 132 | return function(val) { 133 | this.setAttribute(attr, val); 134 | }; 135 | }, 136 | 137 | /** 138 | * Override of {@link AttributeTrigger#invoke}. Adds an additional attributes 139 | * initialize the attribute and to look up the database attribute value. 140 | * 141 | * @method 142 | * @public 143 | * @see {@link AttributeTrigger#invoke} 144 | */ 145 | invoke: function(name, reopen, details) { 146 | var opts = { name: name }; 147 | var attr = this._attr(name); 148 | var attrName = name + 'Attr'; 149 | var initName = name + 'Init'; 150 | 151 | var properties = { _initAttributes: this._initAttributes(opts) }; 152 | properties[attrName] = Property.create(function() { return attr; }); 153 | properties[initName] = this._init(opts); 154 | reopen(properties); 155 | 156 | // add a new copy of the attrs function since the attributes have changed 157 | // on this model & they'll need to be recalculated. 158 | details.context.__identity__.reopenClass({ 159 | _attrs: attrs(), 160 | }); 161 | 162 | this._super.apply(this, arguments); 163 | }, 164 | 165 | }); 166 | 167 | module.exports = Attr.reopenClass({ __name__: 'Attr' }); 168 | -------------------------------------------------------------------------------- /lib/model/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./model'); 4 | -------------------------------------------------------------------------------- /lib/model/manager.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Property = require('corazon').Property; 4 | 5 | // we rely on the bound query class adding to functionality to the entry query 6 | // class, so make sure it's loaded. 7 | require('../query/bound'); 8 | 9 | /** 10 | * @class Manager 11 | * @extends Property 12 | * @classdesc 13 | * 14 | * The object manager for models. 15 | * 16 | * Managers allow you to pre-configure queries for collections of objects that 17 | * are frequently accessed. You can define new collections on your model and 18 | * also have the ability to override the default `objects` collection. 19 | * 20 | * For example, setting up custom managers to allow quick access to 21 | * `Person.men` and `Person.women` would be done like so: 22 | * 23 | * var FemaleManager = Manager.extend({ 24 | * query: function() { 25 | * return this._super().where({ sex: 'female' }); 26 | * } 27 | * }); 28 | * 29 | * var MaleManager = Manager.extend({ 30 | * query: function() { 31 | * return this._super().where({ sex: 'male' }); 32 | * } 33 | * }); 34 | * 35 | * var Person = Model.extend({}, { 36 | * women: FemaleManager.create(), 37 | * men: MaleManager.create(), 38 | * objects: Manager.create() // could customize... 39 | * }); 40 | */ 41 | var Manager = Property.extend(/** @lends Manager# */ { 42 | 43 | /** 44 | * Create a manager. 45 | * 46 | * @public 47 | * @constructor Manager 48 | */ 49 | init: function() { 50 | this._super(); 51 | }, 52 | 53 | /** 54 | * Return a getter function for this property. 55 | * 56 | * The getter function for a manager is responsible for setting up a bound 57 | * query that transforms the results into instances of the model class. The 58 | * getter method will always be attached as a static method of a 59 | * {@link Model}. 60 | * 61 | * @return {Function} The getter function. 62 | */ 63 | _getter: function() { 64 | var self = this; 65 | return function() { 66 | /* jscs:disable safeContextKeyword */ 67 | var modelClass = this; 68 | var query = modelClass.query 69 | .bind(modelClass) 70 | .autoload(); 71 | /* jscs:enable safeContextKeyword */ 72 | 73 | // store the query object on this manager so that we can call `query` 74 | // without having to pass any arguments. this allows a simpler override 75 | // of the `query` method. note, though, that for various reasons this is 76 | // not cache of the query. first, the query returned from this manager 77 | // should be a new query each time. second, and more importantly, the 78 | // manager will be reused across multiple model classes & potentially 79 | // with multiple adapters. caching the result on the manager would mean 80 | // that the same query would be re-used in these different situations. 81 | var result; 82 | self._query = query; 83 | result = self.query(); 84 | self._query = undefined; 85 | return result; 86 | }; 87 | }, 88 | 89 | /** 90 | * Override this method to configure how this manager will query for data. 91 | * 92 | * Make sure you always call super. 93 | * 94 | * @return {ChainedQuery} A new query. 95 | */ 96 | query: function() { 97 | return this._query; 98 | }, 99 | }); 100 | 101 | module.exports = Manager.reopenClass({ __name__: 'Manager' }); 102 | -------------------------------------------------------------------------------- /lib/query/mixins/bound_auto_join.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var util = require('util'); 5 | var Mixin = require('corazon/mixin'); 6 | var Condition = require('maguey').Condition; 7 | var w = Condition.w; 8 | 9 | 10 | /** 11 | * Simple override wrapper function to ensure that a method only will be called 12 | * when the current query is bound to a model. 13 | * 14 | * @function ModelAutoJoin~override 15 | * @private 16 | * @param {Function} fn The method implementation 17 | * @return {Function} A wrapped method 18 | */ 19 | var override = function(fn) { 20 | return function() { 21 | if (this._model) { return fn.apply(this, arguments); } 22 | else { return this._super.apply(this, arguments); } 23 | }; 24 | }; 25 | 26 | /** 27 | * Model query mixin for automatic joining based on field strings. 28 | * 29 | * This mixin relies on external functionality: 30 | * 31 | * - It relies on properties & functionality from {@link ModelCore}. 32 | * - It relies on properties of {@link ModelJoin}. Reference that mixin for 33 | * code & documentation. 34 | * 35 | * @mixin ModelAutoJoin 36 | */ 37 | module.exports = Mixin.create(/** @lends ModelAutoJoin# */ { 38 | 39 | /** 40 | * Override of {@link Where#where}. 41 | * 42 | * Add joins by looking at the conditions and finding any that have a field 43 | * prefix that matches a relation on the model. Joins any that have not yet 44 | * been joined. 45 | * 46 | * @method 47 | * @private 48 | * @return {BoundQuery} The newly transformed query. 49 | */ 50 | where: override(function() { 51 | var condition = w.apply(null, arguments); 52 | var result = condition.reduceFields(function(query, name) { 53 | return query._transformAutoJoinField(name); 54 | }, this); 55 | return this._super.unbound.apply(result, arguments); 56 | }), 57 | 58 | /** 59 | * Override of {@link Order#order}. 60 | * 61 | * Add joins by looking at the order by and finding any that have a field 62 | * prefix that matches a relation on the model. Joins any that have not yet 63 | * been joined. 64 | * 65 | * @method 66 | * @private 67 | * @return {BoundQuery} The newly transformed query. 68 | */ 69 | order: override(function() { 70 | var result = _.reduce(this._toOrder(arguments), function(query, order) { 71 | return query._transformAutoJoinField(order.field); 72 | }, this); 73 | return this._super.unbound.apply(result, arguments); 74 | }), 75 | 76 | /** 77 | * Override of {@link GroupBy#groupBy}. 78 | * 79 | * Add joins by looking at the group by and finding any that have a field 80 | * prefix that matches a relation on the model. Joins any that have not yet 81 | * been joined. 82 | * 83 | * @method 84 | * @private 85 | * @return {BoundQuery} The newly transformed query. 86 | */ 87 | groupBy: override(function(groupBy) { 88 | var result = this._transformAutoJoinField(groupBy); 89 | return this._super.unbound.apply(result, arguments); 90 | }), 91 | 92 | /** 93 | * Add joins by looking at the association prefix on the field. Joins any 94 | * that have not yet been joined. In a typical relationship setup with 95 | * authors, articles, and comments, this will handle cases like: 96 | * 97 | * author.email (joins author) 98 | * article.author.name (joins article.author) 99 | * author (joins author) 100 | * 101 | * @method 102 | * @private 103 | * @param {String} field The field name with association prefix. 104 | * @return {BoundQuery} The newly transformed query. 105 | */ 106 | _transformAutoJoinField: function(field) { 107 | var dup = this._dup(); 108 | var parts = field.split('.'); 109 | var association, completed; 110 | 111 | // we need to give this the full field string (i.e. article.blog.title) 112 | // rather than lopping of the last part & using what we think could be the 113 | // association string (i.e. remove title from above) because the field 114 | // given to us here could actually just refer to an association rather than 115 | // an attribute (i.e. article.blog). 116 | // we catch all errors here & consider that to be safe since we'll be 117 | // re-throwing in the event that we failed to iterate through the field as 118 | // far as we would like. 119 | try { 120 | this._relationForEach(field, function(assoc, relation, detials) { 121 | association = assoc; 122 | completed = detials.index + 1; 123 | }); 124 | } 125 | catch (e) { 126 | if (e.code !== 'RELATION_ITERATION_FAILURE') { throw e; } 127 | } 128 | 129 | // if we did not iterate through and complete at least the number of 130 | // parts that contribute to the table/association name (the minus one 131 | // is so we exclude the field), then this is an error. note that we could 132 | // end up iterating through all parts (when the field refers to a 133 | // relation object). 134 | if (completed < parts.length - 1) { 135 | var relationship = parts.slice(0, completed + 1).join('.'); 136 | var missing = parts.slice(completed)[0]; 137 | throw new Error(util.format( 138 | 'Invalid relationship %j in %s query. Could not find %j.', 139 | relationship, this._model.__identity__.__name__, missing)); 140 | } 141 | 142 | if (dup._joins[association] || dup._joinedRelations[association]) { 143 | association = null; 144 | } 145 | 146 | return association ? dup.join(association).unique() : dup; 147 | }, 148 | 149 | }); 150 | -------------------------------------------------------------------------------- /lib/query/mixins/bound_core.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Mixin = require('corazon/mixin'); 4 | 5 | /** 6 | * Model query mixin for core functionality (which must be present in all query 7 | * types that use any model query mixins). 8 | * 9 | * @mixin ModelCore 10 | */ 11 | module.exports = Mixin.create(/** @lends ModelCore# */ { 12 | 13 | /** 14 | * Duplication implementation. 15 | * 16 | * @method 17 | * @protected 18 | * @see {@link BaseQuery#_take} 19 | */ 20 | _take: function(orig) { 21 | this._super(orig); 22 | this._priorModel = orig._priorModel; 23 | this._model = orig._model; 24 | this._modelTable = orig._modelTable; 25 | this._arrayTransform = orig._arrayTransform; 26 | this._modelTransform = orig._modelTransform; 27 | }, 28 | 29 | /** 30 | * Enable automatic conversion of the query results to model instances. 31 | * 32 | * @method 33 | * @public 34 | * @return {ChainedQuery} The newly configured query. 35 | */ 36 | autoload: function() { 37 | var model = this._model; 38 | var arrayTransform = function(result) { // jscs:ignore jsDoc 39 | return result.rows; 40 | }; 41 | var modelTransform = function(rows) { // jscs:ignore jsDoc 42 | return rows.map(model.load.bind(model)); 43 | }; 44 | 45 | var dup = this._dup() 46 | .transform(arrayTransform) 47 | .transform(modelTransform); 48 | 49 | dup._arrayTransform = arrayTransform; 50 | dup._modelTransform = modelTransform; 51 | 52 | return dup; 53 | }, 54 | 55 | /** 56 | * Disable automatic conversion of the query results to model instances. 57 | * Note that while this disables conversion to model instances, results 58 | * will still be transformed to arrays if {@link ModelCore#autoload} was 59 | * previously called. 60 | * 61 | * There's currently no way to disable the array conversion once the 62 | * `autoload` has completed (though it would be trivial to add if required). 63 | * 64 | * @method 65 | * @public 66 | * @return {ChainedQuery} The newly configured query. 67 | */ 68 | noload: function() { 69 | var dup = this._dup(); 70 | dup = dup.untransform(dup._modelTransform); 71 | return dup; 72 | }, 73 | 74 | /** 75 | * Reverse the effects of an unbind. 76 | * 77 | * @method 78 | * @public 79 | * @return {ChainedQuery} The newly configured query. 80 | */ 81 | rebind: function() { 82 | var dup = this._dup(); 83 | dup = dup.transform(dup._modelTransform); 84 | dup._model = dup._priorModel; 85 | dup._priorModel = undefined; 86 | return dup; 87 | }, 88 | 89 | /** 90 | * No longer associate this query with a model class (disabling all model 91 | * related query features). This also disables the automatic conversion of 92 | * the query results to model instances, though the results will still be 93 | * returned as an array. 94 | * 95 | * @method 96 | * @public 97 | * @return {ChainedQuery} The newly configured query. 98 | * @see {@link ModelCore#noload} 99 | */ 100 | unbind: function() { 101 | var dup = this._dup(); 102 | dup = dup.noload(); 103 | dup._priorModel = dup._model; 104 | dup._model = undefined; 105 | return dup; 106 | }, 107 | 108 | }); 109 | -------------------------------------------------------------------------------- /lib/query/mixins/bound_helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var util = require('util'); 5 | var Mixin = require('corazon/mixin'); 6 | 7 | /** 8 | * Model query mixin for helper methods. 9 | * 10 | * This mixin relies on external functionality: 11 | * 12 | * - It relies on properties & functionality from {@link ModelCore}. 13 | * 14 | * @mixin BoundHelpers 15 | */ 16 | module.exports = Mixin.create(/** @lends BoundHelpers# */ { 17 | 18 | /** 19 | * @typedef {Object} BoundHelpers~RelationIterationDetails 20 | * @property {Integer} index The index in the association string or -1 if it 21 | * is a through relation. 22 | * @property {Relation} [through] The through relation from which this 23 | * association/relation originates. 24 | * @property {Boolean} [expanded] This option is present when the `through` 25 | * option is present, and is true when the association is for is an expansion 26 | * of a `through` relation & not actually part of the given association 27 | * string. 28 | * @property {Boolean} [source] This option is present when the `through` 29 | * option is present, and is true when the relation is the _source_ relation 30 | * of the through relation. 31 | */ 32 | 33 | /** 34 | * @function BoundHelpers~RelationIterator 35 | * @param {String} association The path to this association. 36 | * @param {Relation} relation The relation for this association. 37 | * @param {Object} BoundHelpers~RelationIterationDetails 38 | */ 39 | 40 | /** 41 | * Iterate through an association string (a key path for a relationship) 42 | * while taking into account expanded relationships. Expanded relationships 43 | * will referred to simply as _through_ relationships in the documentation of 44 | * this method, but could possibly be used in other ways. 45 | * 46 | * The _source_ of a _through_ relation will actually cause two invocations 47 | * of the callback function to occur. The first will be with the fully 48 | * expanded association name. The second will be with the _through_ 49 | * association name used in the given association string. The first one will 50 | * have the `expanded` detail flag set, and the second one will not. 51 | * 52 | * @method 53 | * @private 54 | * @param {String} association Relation key path. 55 | * @param {BoundHelpers~RelationIterator} fn The callback function. 56 | * @param {Object} thisArg The value of `this` for the callback function. 57 | */ 58 | _relationForEach: function(association, fn, thisArg) { 59 | var path = association.split('.'); 60 | var assoc = []; 61 | var modelClass = this._model; 62 | 63 | path.forEach(function(name, index) { 64 | var relation = modelClass.__class__.prototype[name + 'Relation']; 65 | if (!relation) { 66 | throw this._relationIterationError(association, name); 67 | } 68 | 69 | var expanded = relation.expand(); 70 | if (expanded) { 71 | assoc.push(name); 72 | this._expandedRelationForEach(assoc.join('.'), 73 | relation, expanded, fn, thisArg); 74 | } 75 | else { 76 | assoc.push(name); 77 | fn.call(thisArg, assoc.join('.'), relation, { 78 | index: index, 79 | }); 80 | } 81 | 82 | // get model class for next iteration 83 | modelClass = relation.relatedModelClass; 84 | }, this); 85 | }, 86 | 87 | /** 88 | * Create an error for a missing relation during relation iteration. 89 | * 90 | * @param {String} association Relation key path. 91 | * @param {String} name The name that caused the error. 92 | * @return {Error} The error object. 93 | */ 94 | _relationIterationError: function(association, name) { 95 | var context = this._relationCallContext; 96 | var through = association.indexOf('.') !== -1 ? 97 | util.format(' through %s', association) : ''; 98 | return _.extend(new Error(util.format( 99 | 'No relation %j found for `%s` on %s query%s.', 100 | name, context, this._model.__identity__.__name__, through)), { 101 | code: 'RELATION_ITERATION_FAILURE', 102 | }); 103 | 104 | }, 105 | 106 | /** 107 | * Enumerate an expanded (a.k.a. _through_) relation. This is a helper for 108 | * {@link BoundHelpers#_relationForEach} and should not be used directly. 109 | * 110 | * Calls the callback with arguments compatible with 111 | * {@link BoundHelpers#_relationForEach}. 112 | * 113 | * Each relation in the through relation will cause the callback to be 114 | * called. Each call will have the following details set: `through` will be 115 | * the association given to this method, `expanded` will be true, `index` 116 | * will be -1, and `source` will be true for the last through relation, the 117 | * _source_ relation. Each of these calls will use the expanded association 118 | * name, that is the name from each of the _non-through_ relations. 119 | * 120 | * Finally, the callback will be called again with the _source_ relation, but 121 | * will use the _through_ association name instead of the expanded 122 | * association name and the actual index in the association string. The 123 | * `expanded` option will be false and the `source` option true. The 124 | * `through` option will be set to the same value as the previous invocation. 125 | * 126 | * @method 127 | * @private 128 | * 129 | * @param {String} association Relation key path. 130 | * @param {Relation} rel The through relation that's been expanded. 131 | * @param {Array.} expanded The expanded relations to iterate. 132 | * @param {BoundHelpers~RelationIterator} fn The callback function. 133 | * @param {Object} thisArg The value of `this` for the callback function. 134 | */ 135 | _expandedRelationForEach: function(association, rel, expanded, fn, thisArg) { 136 | var assoc = association.split('.'); 137 | var index = assoc.length - 1; 138 | var sourceRelation = _.last(expanded); 139 | var expandedAssoc = assoc.slice(1); 140 | 141 | expanded.forEach(function(rel) { 142 | expandedAssoc.push(rel.expansionName); 143 | fn.call(thisArg, expandedAssoc.join('.'), rel, { 144 | through: rel, 145 | expanded: true, 146 | source: (rel === sourceRelation), 147 | index: -1, 148 | }); 149 | }); 150 | 151 | fn.call(thisArg, assoc.join('.'), sourceRelation, { 152 | through: rel, 153 | expanded: false, 154 | source: true, 155 | index: index, 156 | }); 157 | }, 158 | 159 | }); 160 | -------------------------------------------------------------------------------- /lib/query/mixins/bound_unique.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Mixin = require('corazon/mixin'); 4 | 5 | 6 | /** 7 | * Model query mixin for `unique` support. 8 | * 9 | * This mixin relies on external functionality: 10 | * 11 | * - It relies on properties & functionality from {@link ModelCore}. 12 | * 13 | * @mixin BoundUnique 14 | */ 15 | module.exports = Mixin.create(/** @lends BoundUnique# */ { 16 | 17 | /** 18 | * Return unique models by grouping by primary key. 19 | * 20 | * @return {ChainedQuery} The newly configured query. 21 | */ 22 | unique: function() { 23 | if (!this._model) { 24 | throw new Error('Cannot perform `unique` on unbound query.'); 25 | } 26 | return this.groupBy(this._model.__class__.prototype.pkAttr); 27 | }, 28 | 29 | }); 30 | -------------------------------------------------------------------------------- /lib/query/mixins/bound_util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * A function to decorate the context in which this method is operating for 5 | * relation lookups to enhance error messages. 6 | * 7 | * @function BoundQuery~decorateRelationContext 8 | * @param {String} name The context name. 9 | * @param {Function} fn The original function. 10 | * @return {Function} The decorated function. 11 | */ 12 | module.exports.decorateRelationContext = function(name, fn) { 13 | return function() { 14 | this._relationCallContext = name; 15 | var result = fn.apply(this, arguments); 16 | this._relationCallContext = undefined; 17 | return result; 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /lib/relations/belongs_to_associations.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var Mixin = require('corazon/mixin'); 5 | 6 | /** 7 | * HasMany mixin for associations. 8 | * 9 | * This mixin separates some of the logic of {@link BelongsTo} and is only 10 | * intended to be mixed into that one class. 11 | */ 12 | module.exports = Mixin.create(/** @lends BelongsTo# */ { 13 | 14 | /** 15 | * Override of {@link BaseRelation#associate}. 16 | * 17 | * @method 18 | * @protected 19 | * @see {@link BaseRelation#associate} 20 | */ 21 | associate: function(instance, relatedObject, options) { 22 | var opts = _.defaults({}, options, { follow: true, attrs: true }); 23 | var inverse = opts.follow && relatedObject && 24 | relatedObject[this.inverse + 'Relation']; 25 | 26 | if (opts.attrs) { 27 | // set the foreign key property as well 28 | instance.setAttribute(this.foreignKeyAttr, 29 | relatedObject[this.primaryKey]); 30 | } 31 | 32 | this._setRelated(instance, relatedObject); 33 | 34 | if (inverse) { 35 | inverse.associate(relatedObject, instance, _.extend({}, options, { 36 | follow: false, 37 | })); 38 | } 39 | }, 40 | 41 | /** 42 | * Override of {@link BaseRelation#disassociate}. 43 | * 44 | * @method 45 | * @protected 46 | * @see {@link BaseRelation#disassociate} 47 | */ 48 | disassociate: function(instance, relatedObject, options) { 49 | var opts = _.defaults({}, options, { follow: true, attrs: true }); 50 | var inverse = opts.follow && relatedObject[this.inverse + 'Relation']; 51 | 52 | if (opts.attrs) { 53 | // set the foreign key property as well 54 | instance.setAttribute(this.foreignKeyAttr, undefined); 55 | } 56 | 57 | this._setRelated(instance, undefined); 58 | 59 | if (inverse) { 60 | inverse.disassociate(relatedObject, instance, _.extend({}, options, { 61 | follow: false, 62 | })); 63 | } 64 | }, 65 | 66 | }); 67 | -------------------------------------------------------------------------------- /lib/relations/belongs_to_overrides.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var Mixin = require('corazon/mixin'); 5 | var Promise = require('bluebird'); 6 | 7 | /** 8 | * BelongsTo mixin for model overrides. 9 | * 10 | * This mixin separates some of the logic of {@link BelongsTo} and is only 11 | * intended to be mixed into that one class. 12 | */ 13 | module.exports = Mixin.create(/** @lends BelongsTo# */ { 14 | 15 | /** 16 | * Override of the model's init method. 17 | * 18 | * This simply calls super, but its inclusion ensures that the relation will 19 | * be configured during the initialization of any object. 20 | * 21 | * It is accessible on an individual model via `init`, and as such is an 22 | * override of the builtin init method. 23 | * 24 | * As with all other relation methods, the naming conventions are set forth 25 | * in {@link BelongsTo.methods}. 26 | * 27 | * @method 28 | * @protected 29 | * @param {Relation} relation 30 | * @return {Function} 31 | * @see {@link BaseRelation#methods} 32 | */ 33 | initialize: function(/*relation*/) { 34 | return function() { 35 | this._super.apply(this, arguments); 36 | }; 37 | }, 38 | 39 | /** 40 | * Override of the the model's `_dependents` method. 41 | * 42 | * This allows the relation to save the related object in response to the 43 | * instance on which it is operating being saved. 44 | * 45 | * @return {function():Array.} 46 | * @see {@link RelationSave#_dependents} 47 | */ 48 | dependents: function(relation) { 49 | return function() { 50 | var result = this._super().slice(); 51 | var related = relation._related(this); 52 | 53 | // we only depend on the related object being saved if it's a new record 54 | // or the primary key attribute is dirty. otherwise, the update can 55 | // proceed without saving the related object (the save is not dependent 56 | // on the update of the related object). 57 | var dependent = false; 58 | if (related) { 59 | var newRecord = related.newRecord; 60 | var dirtyAttributes = related.dirtyAttributes; 61 | var primaryKeyAttr = relation.primaryKeyAttr; 62 | var primaryKeyIsDirty = _.contains(dirtyAttributes, primaryKeyAttr); 63 | dependent = newRecord || primaryKeyIsDirty; 64 | } 65 | 66 | if (dependent) { result.push(related); } 67 | 68 | return result; 69 | }; 70 | }, 71 | 72 | /** 73 | * Override of the the model's `_relaints` method. 74 | * 75 | * @public 76 | * @method 77 | * @return {function():Array.} 78 | * @see {@link RelationSave#_reliants} 79 | */ 80 | reliants: function(/*relation*/) { 81 | return function() { 82 | return this._super(); 83 | }; 84 | }, 85 | 86 | /** 87 | * Override of the the model's `_presave` method. 88 | * 89 | * After the of saving the related object (which presumably occurred), this 90 | * checks to see if it has a different primary key (for instance, it was a 91 | * newly inserted object) & will update the foreign key (via the foreign 92 | * key property). 93 | * 94 | * It is worth noting here that {@link RelationModelSave} will try to save 95 | * all values provided in `_dependents` including the related instance we 96 | * provide in {@link BelongsTo#dependents}, but there can be no guarantee 97 | * since it's possible to have relationships that each require the save of 98 | * the other. {@link BelongsTo#postsave} handles this scenario. 99 | * 100 | * It is accessible on an individual model via `_presave`, and as such is an 101 | * override of the model's `_presave` method. 102 | * 103 | * As with all other relation methods, the naming conventions are set forth 104 | * in {@link BelongsTo.methods}. 105 | * 106 | * @method 107 | * @protected 108 | * @param {Relation} relation 109 | * @return {Function} 110 | * @see {@link BaseRelation#methods} 111 | * @see {@link RelationSave#_presave} 112 | */ 113 | presave: function(relation) { 114 | return Promise.method(function(/*options*/) { 115 | relation._updateForeignKey(this); 116 | return this._super.apply(this, arguments); 117 | }); 118 | }, 119 | 120 | /** 121 | * Override of the the model's `_postsave` method. 122 | * 123 | * Handle relationships that create a circular dependency. In these 124 | * relationships, you can't insert one model with the proper foreign key 125 | * until the other has been saved. 126 | * 127 | * So after the saves have occurred, we re-check if the foreign key needs to 128 | * be updated & update & clean that attribute that explicitly. 129 | * 130 | * @method 131 | * @protected 132 | * @param {Relation} relation 133 | * @return {Function} 134 | * @see {@link BaseRelation#methods} 135 | * @see {@link RelationSave#_postsave} 136 | */ 137 | postsave: function(relation) { 138 | return Promise.method(function(options) { 139 | if (options.partial) { return this._super(options); } 140 | 141 | var promise = Promise.resolve(); 142 | var changed = relation._updateForeignKey(this); 143 | if (changed && !options.partial) { 144 | var foreignKey = relation.foreignKey; 145 | var updates = _.object([[foreignKey, this[foreignKey]]]); 146 | var conditions = { pk: this.pk }; 147 | var query = relation._relatedModel.objects 148 | .where(conditions) 149 | .update(updates); 150 | promise = promise 151 | .then(query.execute.bind(query)) 152 | .then(this.cleanAttribute.bind(this, relation.foreignKeyAttr)); 153 | } 154 | return promise.then(this._super.bind(this, options)); 155 | }); 156 | }, 157 | 158 | /** 159 | * Updates the foreign key, but only does so if it's changed. 160 | * 161 | * @method 162 | * @private 163 | * @param {Model} instance The model instance on which to operate. 164 | * @return {Boolean} Whether a change was made. 165 | */ 166 | _updateForeignKey: function(instance) { 167 | var foreignKeyAttr = this.foreignKeyAttr; 168 | var primaryKey = this.primaryKey; 169 | var related = this._related(instance); 170 | var relatedIsLoaded = (related !== undefined); // ensure valid cache 171 | 172 | var current = instance.getAttribute(foreignKeyAttr); 173 | var updated = _.get(related, primaryKey); 174 | var changed = relatedIsLoaded && (current !== updated); 175 | if (changed) { 176 | instance.setAttribute(foreignKeyAttr, updated); 177 | } 178 | return changed; 179 | }, 180 | 181 | }); 182 | -------------------------------------------------------------------------------- /lib/relations/belongs_to_prefetch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var Mixin = require('corazon/mixin'); 5 | var Promise = require('bluebird'); 6 | 7 | /** 8 | * BelongsTo mixin for pre-fetching. 9 | * 10 | * This mixin separates some of the logic of {@link BelongsTo} and is only 11 | * intended to be mixed into that one class. 12 | */ 13 | module.exports = Mixin.create(/** @lends BelongsTo# */ { 14 | 15 | /** 16 | * Override of {@link BaseRelation#prefetch}. 17 | * 18 | * @method 19 | * @protected 20 | * @see {@link BaseRelation#prefetch} 21 | */ 22 | prefetch: Promise.method(function(instances) { 23 | if (instances.length === 0) { return {}; } 24 | 25 | var self = this; 26 | var queryKey = this.primaryKey; 27 | var foreignKeyAttr = this.foreignKeyAttr; 28 | var fks = _(instances) 29 | .map(function(instance) { return instance.getAttribute(foreignKeyAttr); }) 30 | .uniq() 31 | .reject(_.isUndefined) 32 | .reject(_.isNull) 33 | .value(); 34 | 35 | var limit = fks.length; 36 | 37 | if (fks.length === 1) { fks = fks[0]; } 38 | else { queryKey += '$in'; } 39 | 40 | var where = _.object([[queryKey, fks]]); 41 | var query = this._relatedModel.objects.where(where).limit(limit); 42 | 43 | return query.execute().then(function(related) { 44 | var grouped = _.groupBy(related, function(item) { 45 | return item.getAttribute(self.primaryKeyAttr); 46 | }); 47 | instances.forEach(function(instance) { 48 | var fk = instance.getAttribute(foreignKeyAttr); 49 | var results = grouped[fk] || []; 50 | self.validateFetchedObjects(instance, results); 51 | }); 52 | return grouped; 53 | }); 54 | }), 55 | 56 | /** 57 | * Override of {@link BaseRelation#associatePrefetchResults}. 58 | * 59 | * @method 60 | * @protected 61 | * @see {@link BaseRelation#associatePrefetchResults} 62 | */ 63 | associatePrefetchResults: function(instances, grouped) { 64 | instances.forEach(function(instance) { 65 | var fk = instance.getAttribute(this.foreignKeyAttr); 66 | var results = grouped[fk] || []; 67 | this.associateFetchedObjects(instance, results); 68 | }, this); 69 | }, 70 | 71 | }); 72 | -------------------------------------------------------------------------------- /lib/relations/has_many_associations.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var Mixin = require('corazon/mixin'); 5 | 6 | /** 7 | * HasMany mixin for associations. 8 | * 9 | * This mixin separates some of the logic of {@link HasMany} and is only 10 | * intended to be mixed into that one class. 11 | */ 12 | module.exports = Mixin.create(/** @lends HasMany# */ { 13 | 14 | /** 15 | * Specific method for associating objects that have just been fetched 16 | * from the database. 17 | * 18 | * Other mixins can override {@link HasMany#associateFetchedObjects} to 19 | * change the way fetched objects are associated. This is the default 20 | * implementation. 21 | * 22 | * @method 23 | * @protected 24 | * @param {Model} instance The model instance on which to operate. 25 | * @param {Array.} objects The objects to add to the association. 26 | * @see {@link HasMany~AssociationsMixin} 27 | */ 28 | _associateFetchedObjects: function(instance, objects) { 29 | this.associateObjects(instance, objects, { attrs: false }); 30 | }, 31 | 32 | /** 33 | * Associate multiple objects. For performance reasons, this is preferred 34 | * over simply associating each object individually. 35 | * 36 | * Other mixins can override {@link HasMany#associateFetchedObjects} to 37 | * change the way related objects are associated. This is the default 38 | * implementation. 39 | * 40 | * @method 41 | * @protected 42 | * @param {Model} instance The model instance on which to operate. 43 | * @param {Array.} objects The objects to add to the association. 44 | * @param {Object} [options] Options. See {@link BaseRelation#associate}. 45 | */ 46 | _associateObjects: function(instance, objects, options) { 47 | var opts = _.defaults({}, options, { follow: true, attrs: true }); 48 | 49 | objects.forEach(function(relatedObject) { 50 | if (opts.attrs) { 51 | // always set foreign key in case the inverse relation does not exist 52 | this.associateObjectAttributes(instance, relatedObject); 53 | } 54 | 55 | var inverse = opts.follow && relatedObject[this.inverse + 'Relation']; 56 | if (inverse) { 57 | inverse.associate(relatedObject, instance, _.extend({}, options, { 58 | follow: false, 59 | })); 60 | } 61 | }, this); 62 | }, 63 | 64 | /** 65 | * Associate attributes (such as the foreign key) during the association of 66 | * objects. 67 | * 68 | * Mixins can override {@link HasMany#associateObjectAttributes} to change 69 | * the way associating attributes is handled. This is the default 70 | * implementation. 71 | * 72 | * @method 73 | * @protected 74 | * @param {Model} instance The model instance on which to operate. 75 | * @param {Array.} objects The objects being added to the association. 76 | */ 77 | _associateObjectAttributes: function(instance, relatedObject) { 78 | relatedObject.setAttribute(this.foreignKeyAttr, instance.pk); 79 | }, 80 | 81 | /** 82 | * Override of {@link BaseRelation#associate}. 83 | * 84 | * @method 85 | * @protected 86 | * @see {@link BaseRelation#associate} 87 | */ 88 | associate: function(instance, relatedObject, options) { 89 | this.associateObjects(instance, [relatedObject], options); 90 | }, 91 | 92 | /** 93 | * Disassociate multiple objects. For performance reasons, this is preferred 94 | * over simply disassociating each object individually. 95 | * 96 | * Other mixins can override {@link HasMany#disassociateFetchedObjects} to 97 | * change the way related objects are disassociated. This is the default 98 | * implementation. 99 | * 100 | * @method 101 | * @protected 102 | * @param {Model} instance The model instance on which to operate. 103 | * @param {Array.} objects The objects to remove from the association. 104 | * @param {Object} [options] Options. See {@link BaseRelation#disassociate}. 105 | */ 106 | _disassociateObjects: function(instance, objects, options) { 107 | var opts = _.defaults({}, options, { follow: true, attrs: true }); 108 | 109 | objects.forEach(function(relatedObject) { 110 | if (opts.attrs) { 111 | // always set foreign key in case the inverse relation does not exist 112 | this.disassociateObjectAttributes(instance, relatedObject); 113 | } 114 | 115 | var inverse = opts.follow && relatedObject[this.inverse + 'Relation']; 116 | if (inverse) { 117 | inverse.disassociate(relatedObject, instance, _.extend({}, options, { 118 | follow: false, 119 | })); 120 | } 121 | }, this); 122 | }, 123 | 124 | /** 125 | * Disassociate attributes (such as the foreign key) during the 126 | * disassociation of objects. 127 | * 128 | * Mixins can override {@link HasMany#disassociateObjectAttributes} to change 129 | * the way disassociating attributes is handled. This is the default 130 | * implementation. 131 | * 132 | * @method 133 | * @protected 134 | * @param {Model} instance The model instance on which to operate. 135 | * @param {Array.} objects The objects being removed from the 136 | * association. 137 | */ 138 | _disassociateObjectAttributes: function(instance, relatedObject) { 139 | relatedObject.setAttribute(this.foreignKeyAttr, undefined); 140 | }, 141 | 142 | /** 143 | * Override of {@link BaseRelation#disassociate}. 144 | * 145 | * @method 146 | * @protected 147 | * @see {@link BaseRelation#disassociate} 148 | */ 149 | disassociate: function(instance, relatedObject, options) { 150 | this.disassociateObjects(instance, [relatedObject], options); 151 | }, 152 | 153 | }); 154 | -------------------------------------------------------------------------------- /lib/relations/has_many_collection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var util = require('util'); 5 | var Mixin = require('corazon/mixin'); 6 | 7 | /** 8 | * HasMany mixin for collection data cached on a model instance. 9 | * 10 | * This mixin separates some of the logic of {@link HasMany} and is only 11 | * intended to be mixed into that one class. 12 | */ 13 | module.exports = Mixin.create(/** @lends HasMany# */ { 14 | 15 | /** 16 | * Mixin initialization. 17 | */ 18 | init: function() { 19 | this._super.apply(this, arguments); 20 | this._collectionCacheKey = '_' + this._name; 21 | }, 22 | 23 | /** 24 | * The collection property for this relation. 25 | * 26 | * This property allows access to the cached objects that have been fetched 27 | * for a specific model in a given relation. Before the cache has been 28 | * filled, accessing this property will throw an exception. 29 | * 30 | * It is accessible on an individual model via ``. For 31 | * instance, a user that has many articles would cause this method to get 32 | * triggered via `user.articles`. 33 | * 34 | * The naming conventions are set forth in {@link HasMany.methods}. 35 | * 36 | * @method 37 | * @protected 38 | * @param {Model} instance The model instance on which to operate. 39 | * @see {@link BaseRelation#methods} 40 | */ 41 | collection: function(instance) { 42 | var result = this._getCollectionCache(instance); 43 | if (!result) { 44 | throw new Error(util.format('The relation "%s" has not yet been ' + 45 | 'loaded.', this._name)); 46 | } 47 | return result; 48 | }, 49 | 50 | /** 51 | * Get the {@link HasMany#collection} cache value. 52 | * 53 | * @method 54 | * @protected 55 | * @param {Model} instance The model instance on which to operate. 56 | * @return {Array} value The value of the collection cache. 57 | */ 58 | _getCollectionCache: function(instance) { 59 | return instance[this._collectionCacheKey]; 60 | }, 61 | 62 | /** 63 | * Update the {@link HasMany#collection} cache to contain a new value. 64 | * 65 | * @method 66 | * @protected 67 | * @param {Model} instance The model instance on which to operate. 68 | * @param {Array} value The new value for the collection cache. 69 | */ 70 | _setCollectionCache: function(instance, value) { 71 | instance[this._collectionCacheKey] = value; 72 | }, 73 | 74 | /** 75 | * Add to the {@link HasMany#collection} cache if it's been loaded. 76 | * 77 | * @method 78 | * @protected 79 | * @param {Model} instance The model instance on which to operate. 80 | * @param {Array} The objects to add. 81 | */ 82 | _unionCollectionCache: function(instance, objects) { 83 | var value = instance[this._collectionCacheKey]; 84 | if (value) { 85 | this._setCollectionCache(instance, _.union(value, objects)); 86 | } 87 | }, 88 | 89 | /** 90 | * Remove from the {@link HasMany#collection} cache if it's been loaded. 91 | * 92 | * @method 93 | * @protected 94 | * @param {Model} instance The model instance on which to operate. 95 | * @param {Array} The objects to remove. 96 | */ 97 | _differenceCollectionCache: function(instance, objects) { 98 | var value = instance[this._collectionCacheKey]; 99 | if (value) { 100 | this._setCollectionCache(instance, _.difference(value, objects)); 101 | } 102 | }, 103 | 104 | /** 105 | * Clear the {@link HasMany#collection} cache if it's been loaded. 106 | * 107 | * @method 108 | * @protected 109 | * @param {Model} instance The model instance on which to operate. 110 | */ 111 | _clearCollectionCache: function(instance) { 112 | var value = instance[this._collectionCacheKey]; 113 | if (value) { 114 | this._setCollectionCache(instance, []); 115 | } 116 | }, 117 | 118 | /** 119 | * Implementation of {@link HasMany#beforeClearingObjects} for updating 120 | * caches. 121 | * 122 | * @method 123 | * @protected 124 | * @see {@link HasMany#beforeClearingObjects} 125 | */ 126 | beforeClearingObjects: function(instance) { 127 | this._clearCollectionCache(instance); 128 | this._super.apply(this, arguments); 129 | }, 130 | 131 | /** 132 | * This simply creates an empty cache into which fetched associated objects 133 | * can be stored. 134 | * 135 | * @method 136 | * @protected 137 | * @see {@link HasMany#beforeAssociatingFetchedObjects} 138 | */ 139 | beforeAssociatingFetchedObjects: function(instance/*, objects, options*/) { 140 | this._setCollectionCache(instance, []); 141 | this._super.apply(this, arguments); 142 | }, 143 | 144 | /** 145 | * Adds to the collection cache when adding associated objects. 146 | * 147 | * @method 148 | * @protected 149 | * @see {@link HasMany#beforeAssociatingObjects} 150 | */ 151 | beforeAssociatingObjects: function(instance, objects/*, options*/) { 152 | this._unionCollectionCache(instance, objects); 153 | this._super.apply(this, arguments); 154 | }, 155 | 156 | /** 157 | * Removes from the collection cache when removing associated objects. 158 | * 159 | * @method 160 | * @protected 161 | * @see {@link HasMany#beforeDisassociatingObjects} 162 | */ 163 | beforeDisassociatingObjects: function(instance, objects/*, options*/) { 164 | this._differenceCollectionCache(instance, objects); 165 | this._super.apply(this, arguments); 166 | }, 167 | 168 | /** 169 | * Create a collection cache when new instances are created (those that are 170 | * not loaded). 171 | * 172 | * @method 173 | * @protected 174 | * @see {@link HasMany#afterInitializing} 175 | */ 176 | afterInitializing: function(instance) { 177 | if (instance.newRecord) { 178 | this._setCollectionCache(instance, []); 179 | } 180 | }, 181 | 182 | }); 183 | -------------------------------------------------------------------------------- /lib/relations/has_many_in_flight.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var Mixin = require('corazon/mixin'); 5 | 6 | /** 7 | * HasMany mixin for in flight data cached on a model instance. 8 | * 9 | * This mixin separates some of the logic of {@link HasMany} and is only 10 | * intended to be mixed into that one class. 11 | */ 12 | module.exports = Mixin.create(/** @lends HasMany# */ { 13 | 14 | /** 15 | * Mixin initialization. 16 | */ 17 | init: function() { 18 | this._super.apply(this, arguments); 19 | this._inFlightDataKey = 20 | '_' + _.camelCase(this._name + '_objectsInFlight'); 21 | }, 22 | 23 | /** 24 | * @typedef {Object} HasMany~InFlightData 25 | * @property {Array.} add An array of objects to add to the 26 | * relationship. 27 | * @property {Array.} remove An array of objects to remove from the 28 | * relationship. 29 | * @property {Array.} save An array of objects that require saving 30 | * in order to properly save the relationship (join models fall into this 31 | * category). 32 | * @property {Boolean} clear Whether or not the related objects should be 33 | * cleared entirely. 34 | */ 35 | 36 | /** 37 | * Get in flight data, the information pertinent to this relation that has 38 | * not yet been saved. 39 | * 40 | * @method 41 | * @protected 42 | * @param {Model} instance The model instance on which to operate. 43 | * @return {HasMany~InFlightData} In flight data. 44 | */ 45 | _getInFlightData: function(instance) { 46 | return _.defaults({}, instance[this._inFlightDataKey], { 47 | clear: false, 48 | add: [], 49 | remove: [], 50 | associated: [], // inverse associated these 51 | }); 52 | }, 53 | 54 | /** 55 | * Set the in flight. 56 | * 57 | * @method 58 | * @protected 59 | * @param {Model} instance The model instance on which to operate. 60 | * @param {HasMany~InFlightData} data In flight data. 61 | */ 62 | _setInFlightData: function(instance, data) { 63 | instance[this._inFlightDataKey] = data; 64 | }, 65 | 66 | /** 67 | * Implementation of {@link HasMany#beforeAssociatingObjects} for updating 68 | * caches. 69 | * 70 | * Note: this occurs after the adding of objects, but also when the inverse 71 | * association changes objects in the relationship. 72 | * 73 | * @method 74 | * @protected 75 | * @see {@link HasMany#beforeAssociatingObjects} 76 | */ 77 | beforeAssociatingObjects: function(instance, objects/*, options*/) { 78 | var data = this._getInFlightData(instance); 79 | data.associated = _.union(data.associated, objects); 80 | data.associated = _.difference(data.associated, data.add); 81 | this._setInFlightData(instance, data); 82 | this._super.apply(this, arguments); 83 | }, 84 | 85 | /** 86 | * Implementation of {@link HasMany#beforeDisassociatingObjects} for updating 87 | * caches. 88 | * 89 | * Note: this occurs after the removal of objects, but also when the inverse 90 | * association changes objects in the relationship. 91 | * 92 | * @method 93 | * @protected 94 | * @see {@link HasMany#beforeDisassociatingObjects} 95 | */ 96 | beforeDisassociatingObjects: function(instance, objects/*, options*/) { 97 | var data = this._getInFlightData(instance); 98 | data.associated = _.difference(data.associated, objects); 99 | this._setInFlightData(instance, data); 100 | this._super.apply(this, arguments); 101 | }, 102 | 103 | /** 104 | * Implementation of {@link HasMany#beforeCreatingObject} for updating 105 | * caches. 106 | * 107 | * @method 108 | * @protected 109 | * @see {@link HasMany#beforeCreatingObject} 110 | */ 111 | beforeCreatingObject: function(instance, object) { 112 | var data = this._getInFlightData(instance); 113 | data.add = _.union(data.add, [object]); 114 | this._setInFlightData(instance, data); 115 | this._super.apply(this, arguments); 116 | }, 117 | 118 | /** 119 | * Implementation of {@link HasMany#beforeAddingObjects} for updating caches. 120 | * 121 | * @method 122 | * @protected 123 | * @see {@link HasMany#beforeAddingObjects} 124 | */ 125 | beforeAddingObjects: function(instance, objects) { 126 | var data = this._getInFlightData(instance); 127 | var noLongerRemoved = _.intersection(objects, data.remove); 128 | var stillNeedAdding = _.difference(objects, noLongerRemoved); 129 | data.remove = _.difference(data.remove, noLongerRemoved); 130 | data.add = _.union(data.add, stillNeedAdding); 131 | this._setInFlightData(instance, data); 132 | this._super.apply(this, arguments); 133 | }, 134 | 135 | /** 136 | * Implementation of {@link HasMany#beforeRemovingObjects} for updating 137 | * caches. 138 | * 139 | * @method 140 | * @protected 141 | * @see {@link HasMany#beforeRemovingObjects} 142 | */ 143 | beforeRemovingObjects: function(instance, objects) { 144 | var data = this._getInFlightData(instance); 145 | var noLongerAdded = _.intersection(objects, data.add); 146 | var stillNeedRemoving = _.difference(objects, noLongerAdded); 147 | data.add = _.difference(data.add, noLongerAdded); 148 | data.remove = _.union(data.remove, stillNeedRemoving); 149 | this._setInFlightData(instance, data); 150 | this._super.apply(this, arguments); 151 | }, 152 | 153 | /** 154 | * Implementation of {@link HasMany#beforeClearingObjects} for updating 155 | * caches. 156 | * 157 | * @method 158 | * @protected 159 | * @see {@link HasMany#beforeClearingObjects} 160 | */ 161 | beforeClearingObjects: function(instance) { 162 | this._setInFlightData(instance, { clear: true, add: [], remove: [] }); 163 | this._super.apply(this, arguments); 164 | }, 165 | }); 166 | -------------------------------------------------------------------------------- /lib/relations/has_many_overrides.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var Mixin = require('corazon/mixin'); 5 | var Promise = require('bluebird'); 6 | 7 | /** 8 | * HasMany mixin for model overrides. 9 | * 10 | * This mixin separates some of the logic of {@link HasMany} and is only 11 | * intended to be mixed into that one class. 12 | */ 13 | module.exports = Mixin.create(/** @lends HasMany# */ { 14 | 15 | /** 16 | * Override of the model's init method. 17 | * 18 | * This allows the relation to create a collection cache when new instances 19 | * are created (those that are not loaded). 20 | * 21 | * During the process of creating the model, it checks to see if it was 22 | * loaded from the database & if it was not, it is assumed to be a brand new 23 | * object that could not have any associations. It therefore fills the 24 | * collection cache with an empty array. 25 | * 26 | * Its inclusion also ensures that the relation will be configured during the 27 | * initialization of any object. 28 | * 29 | * It is accessible on an individual model via `init`, and as such is an 30 | * override of the builtin init method. 31 | * 32 | * As with all other relation methods, the naming conventions are set forth 33 | * in {@link BelongsTo.methods}. 34 | * 35 | * @method 36 | * @protected 37 | * @param {Relation} relation 38 | * @return {Function} 39 | * @see {@link BaseRelation#methods} 40 | */ 41 | initialize: function(relation) { 42 | return function() { 43 | this._super.apply(this, arguments); 44 | relation.afterInitializing(this); 45 | }; 46 | }, 47 | 48 | /** 49 | * Override of the the model's `_dependents` method. 50 | * 51 | * @return {function():Array.} 52 | * @see {@link RelationSave#_dependents} 53 | */ 54 | dependents: function(/*relation*/) { 55 | return function() { 56 | return this._super(); 57 | }; 58 | }, 59 | 60 | /** 61 | * Override of the the model's `_relaints` that adds any related objects that 62 | * require updating. 63 | * 64 | * The models only need to be saved if they will not be processable by 65 | * {@link HasMany#postsave}. The `postsave` handles foreign key updates, 66 | * so reliant models are: 67 | * 68 | * - Newly created objects 69 | * - Dirty objects, unless only the foreign key is dirty 70 | * - Objects associated via the inverse relation w/ a dirty foreign key 71 | * 72 | * Searches the in-flight object information. 73 | * 74 | * The resulting objects will be saved before the `postsave`, potentially 75 | * requiring `postsave` to do less work. 76 | * 77 | * @return {function():Array.} 78 | * @see {@link RelationSave#_reliants} 79 | */ 80 | reliants: function(relation) { 81 | return function() { 82 | var result = this._super().slice(); 83 | var foreignKeyAttr = relation.foreignKeyAttr; 84 | var data = relation._getInFlightData(this); 85 | 86 | // search for objects that were added by the inverse relation that are 87 | // still dirtied by the foreign key of this relation. these were not 88 | // added explicitly to the relation. 89 | var associated = data.associated; 90 | _.forEach(associated, function(related) { 91 | if (_.contains(related.dirtyAttributes, foreignKeyAttr)) { 92 | result.push(related); 93 | } 94 | }); 95 | 96 | // search for added/removed objects that won't be fully saved by a bulk 97 | // foreign key update. 98 | var changed = data.add.concat(_.filter(data.remove, 'persisted')); 99 | _.forEach(changed, function(related) { 100 | var dirty = _.without(related.dirtyAttributes, foreignKeyAttr).length; 101 | var save = dirty || related.newRecord; 102 | if (save) { 103 | result.push(related); 104 | } 105 | }); 106 | 107 | return result; 108 | }; 109 | }, 110 | 111 | /** 112 | * Override of the the model's `_postsave` method. 113 | * 114 | * This allows the relation to save related objects & update the database to 115 | * reflect the associations in response to the instance on which it is 116 | * operating being saved. 117 | * 118 | * It uses the combined results from calls to {@link HasMany#addObjects}, 119 | * {@link HasMany#removeObjects}, and {@link HasMany#clearObjects} to 120 | * determine what actions need to be taken, then perform updates accordingly. 121 | * The actions, {@link HasMany#executeAdd}, {@link HasMany#executeRemove}, 122 | * and {@link HasMany#executeClear} will be invoked if necessary. 123 | * 124 | * It understands that some related objects have already been saved (if they 125 | * were included in {@link HasMany#reliants}). 126 | * 127 | * It is accessible on an individual model via `_postsave`, and as such is an 128 | * override of the model's `_postsave` method. 129 | * 130 | * As with all other relation methods, the naming conventions are set forth 131 | * in {@link HasMany.methods}. 132 | * 133 | * @method 134 | * @protected 135 | * @param {Relation} relation 136 | * @return {Function} 137 | * @see {@link BaseRelation#methods} 138 | * @see {@link RelationSave#_postsave} 139 | */ 140 | postsave: function(relation) { 141 | return Promise.method(function(options) { 142 | if (options.partial) { return this._super(options); } 143 | 144 | var promise = this._super(options); 145 | var data = relation._getInFlightData(this); 146 | var self = this; 147 | 148 | if (data.clear) { 149 | promise = promise.then(function() { 150 | return relation.executeClear(self); 151 | }); 152 | } 153 | 154 | if (data.add.length || data.remove.length) { 155 | promise = promise.then(function() { 156 | return [ 157 | relation.executeAdd(self, data.add), 158 | relation.executeRemove(self, data.remove), 159 | ]; 160 | }) 161 | .all(); 162 | } 163 | 164 | promise = promise.then(function() { 165 | relation._setInFlightData(self, undefined); 166 | }); 167 | 168 | return promise; 169 | }); 170 | }, 171 | 172 | }); 173 | -------------------------------------------------------------------------------- /lib/relations/has_many_prefetch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var Mixin = require('corazon/mixin'); 5 | var Promise = require('bluebird'); 6 | 7 | /** 8 | * HasMany mixin for pre-fetching. 9 | * 10 | * This mixin separates some of the logic of {@link HasMany} and is only 11 | * intended to be mixed into that one class. 12 | */ 13 | module.exports = Mixin.create(/** @lends HasMany# */ { 14 | 15 | /** 16 | * Override of {@link BaseRelation#prefetch}. 17 | * 18 | * Mixins can override {@link HasMany#prefetch} to change the way pre-fetch 19 | * is performed. This is the default implementation. 20 | * 21 | * @method 22 | * @protected 23 | * @see {@link BaseRelation#prefetch} 24 | */ 25 | _prefetch: Promise.method(function(instances) { 26 | if (instances.length === 0) { return {}; } 27 | 28 | var self = this; 29 | var query = this._prefetchQuery(instances); 30 | return query.execute().then(function(related) { 31 | var grouped = _.groupBy(related, function(item) { 32 | return item.getAttribute(self.foreignKeyAttr); 33 | }); 34 | return grouped; 35 | }); 36 | }), 37 | 38 | /** 39 | * Generate the prefetch query. 40 | * 41 | * Subclasses can override this. 42 | * 43 | * @method 44 | * @protected 45 | * @see {@link HasMany#_prefetch} 46 | */ 47 | _prefetchQuery: function(instances) { 48 | var queryKey = this.foreignKey; 49 | var pks = _.map(instances, this.primaryKey); 50 | 51 | if (instances.length === 1) { pks = pks[0]; } 52 | else { queryKey += '$in'; } 53 | 54 | var where = _.object([[queryKey, pks]]); 55 | return this._relatedModel.objects.where(where); 56 | }, 57 | 58 | /** 59 | * Override of {@link BaseRelation#associatePrefetchResults}. 60 | * 61 | * Mixins can override {@link HasMany#associatePrefetchResults} to change the 62 | * way pre-fetch association is performed. This is the default 63 | * implementation. 64 | * 65 | * @method 66 | * @protected 67 | * @see {@link BaseRelation#associatePrefetchResults} 68 | */ 69 | _associatePrefetchResults: function(instances, grouped) { 70 | instances.forEach(function(instance) { 71 | var pk = instance.getAttribute(this.primaryKeyAttr); 72 | var results = grouped[pk] || []; 73 | this.associateFetchedObjects(instance, results); 74 | }, this); 75 | }, 76 | 77 | }); 78 | -------------------------------------------------------------------------------- /lib/relations/has_many_query.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var Mixin = require('corazon/mixin'); 5 | 6 | /** 7 | * This helper method is used to immediately invalidate the `objectsQuery` 8 | * cache when performing any sort of update to related objects. 9 | * 10 | * The method expects to be called with the first argument being the model 11 | * instance on which it is operating. It will use this to invalidate the 12 | * `objectsQuery` cache. It also expects that it will be used as an instance 13 | * method on a `HasMany` relation. 14 | * 15 | * @method HasMany~invalidateQuery 16 | * @private 17 | * @see {@link HasMany#_cacheObjectsQuery} 18 | */ 19 | var invalidateQuery = function(instance) { 20 | this._cacheObjectsQuery(instance, undefined); 21 | this._super.apply(this, arguments); 22 | }; 23 | 24 | /** 25 | * HasMany mixin for they query object. 26 | * 27 | * This mixin separates some of the logic of {@link HasMany} and is only 28 | * intended to be mixed into that one class. 29 | */ 30 | module.exports = Mixin.create(/** @lends HasMany# */ { 31 | 32 | /** 33 | * Mixin initialization. 34 | */ 35 | init: function() { 36 | this._super.apply(this, arguments); 37 | this._objectsQueryCacheKey = 38 | '_' + _.camelCase(this._name + '_objectsQueryCache'); 39 | }, 40 | 41 | /** 42 | * Update the {@link HasMany#objectsQuery} cache to contain a new value. 43 | * 44 | * @method 45 | * @protected 46 | * @param {Model} instance The model instance on which to operate. 47 | * @param {Object} value The new value. 48 | */ 49 | _cacheObjectsQuery: function(instance, value) { 50 | instance[this._objectsQueryCacheKey] = value; 51 | }, 52 | 53 | /** 54 | * The objects query property for this relation. 55 | * 56 | * This property allows you access to a query which you can use to fetch all 57 | * objects for a relation on a given model and/or to configure the query to 58 | * fetch a subset of the related objects. 59 | * 60 | * When you fetch all objects, the resulting objects will be cached and 61 | * accessible via the {@link HasMany#collection} (if loaded). 62 | * 63 | * The value of this property is a query object that is cached so it will 64 | * always be the exact same query object (see exceptions below). This allows 65 | * multiple fetches to simply return the query's cached result. A simple 66 | * {@link BaseQuery#clone} of the query will ensure that it is always 67 | * performing a new query in the database. 68 | * 69 | * The cache of this property will be invalided (and the resulting object a 70 | * new query object) when changes are made to the relation. 71 | * 72 | * It is accessible on an individual model via `Objects`. For 73 | * instance, a user that has many articles would cause this method to get 74 | * triggered via `user.articleObjects`. 75 | * 76 | * The naming conventions are set forth in {@link HasMany.methods}. 77 | * 78 | * @method 79 | * @protected 80 | * @param {Model} instance The model instance on which to operate. 81 | * @see {@link BaseRelation#methods} 82 | */ 83 | objectsQuery: function(instance) { 84 | var cacheName = this._objectsQueryCacheKey; 85 | if (!instance[cacheName]) { 86 | var associateResult = 87 | _.bind(this.associateFetchedObjects, this, instance); 88 | instance[cacheName] = this 89 | .scopeObjectQuery(instance, this._relatedModel.objects) 90 | .on('result', associateResult); 91 | } 92 | return instance[cacheName]; 93 | }, 94 | 95 | /** 96 | * Scoping for object queries. 97 | * 98 | * Mixins can override {@link HasMany#scopeObjectQuery} to apply a more 99 | * specific scope for the search of objects. This is the default 100 | * implementation. 101 | * 102 | * @param {Model} instance The model instance on which to operate. 103 | * @param {ChainedQuery} query The query to scope. 104 | * @return {ChainedQuery} The newly configured query. 105 | */ 106 | _scopeObjectQuery: function(instance, query) { 107 | var where = _.object([ 108 | [this.foreignKey, instance.getAttribute(this.primaryKeyAttr)], 109 | ]); 110 | return query.where(where); 111 | }, 112 | 113 | /** 114 | * Implementation of {@link HasMany#beforeCreatingObject} for invalidating 115 | * the query cache. 116 | * 117 | * @method 118 | * @protected 119 | * @see {@link HasMany#beforeCreatingObject} 120 | */ 121 | beforeCreatingObject: invalidateQuery, 122 | 123 | /** 124 | * Implementation of {@link HasMany#beforeAddingObjects} for invalidating the 125 | * query cache. 126 | * 127 | * @method 128 | * @protected 129 | * @see {@link HasMany#beforeAddingObjects} 130 | */ 131 | beforeAddingObjects: invalidateQuery, 132 | 133 | /** 134 | * Implementation of {@link HasMany#afterAddingObjects} for invalidating the 135 | * query cache. 136 | * 137 | * @method 138 | * @protected 139 | * @see {@link HasMany#afterAddingObjects} 140 | */ 141 | afterAddingObjects: invalidateQuery, 142 | 143 | /** 144 | * Implementation of {@link HasMany#beforeRemovingObjects} for invalidating 145 | * the query cache. 146 | * 147 | * @method 148 | * @protected 149 | * @see {@link HasMany#beforeRemovingObjects} 150 | */ 151 | beforeRemovingObjects: invalidateQuery, 152 | 153 | /** 154 | * Implementation of {@link HasMany#afterRemovingObjects} for invalidating 155 | * the query cache. 156 | * 157 | * @method 158 | * @protected 159 | * @see {@link HasMany#afterRemovingObjects} 160 | */ 161 | afterRemovingObjects: invalidateQuery, 162 | 163 | /** 164 | * Implementation of {@link HasMany#beforeClearingObjects} for invalidating 165 | * the query cache. 166 | * 167 | * @method 168 | * @protected 169 | * @see {@link HasMany#beforeClearingObjects} 170 | */ 171 | beforeClearingObjects: invalidateQuery, 172 | 173 | /** 174 | * Implementation of {@link HasMany#afterClearingObjects} for invalidating 175 | * the query cache. 176 | * 177 | * @method 178 | * @protected 179 | * @see {@link HasMany#afterClearingObjects} 180 | */ 181 | afterClearingObjects: invalidateQuery, 182 | 183 | }); 184 | -------------------------------------------------------------------------------- /lib/relations/has_one.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var HasMany = require('./has_many'); 4 | var inflection = require('../util/inflection'); 5 | 6 | /** 7 | * The has many relation for models. 8 | * 9 | * For example: 10 | * 11 | * var User = Model.extend({ 12 | * blog: hasOne() 13 | * }); 14 | * 15 | * @public 16 | * @param {Class|String} [relatedModel] The model to which this relates. 17 | * @param {Object} [options] 18 | * @param {String} [options.inverse] The name of the inverse relationship. 19 | * @param {String} [options.primaryKey] The name of the primary key in the 20 | * relationship. 21 | * @param {String} [options.foreignKey] The name of the foreign key in the 22 | * relationship. 23 | * @param {String} [options.through] Specify the name of a relationship through 24 | * which this collection is accessed. 25 | * @param {String} [options.source] When using `through` this is the name of 26 | * the relationship on the destination model. The default value is the name of 27 | * the attribute for the relationship. 28 | * @param {Boolean} [options.implicit] Whether this relation is being added 29 | * implicitly by the system. 30 | * @function Database#hasOne 31 | */ 32 | 33 | /** 34 | * @class HasOne 35 | * @extends HasMany 36 | * @classdesc 37 | * 38 | * The has many relation for models. 39 | */ 40 | var HasOne = HasMany.extend(); 41 | 42 | HasOne.reopen(/** @lends HasOne# */ { 43 | 44 | /** 45 | * Create a HasOne relation. 46 | * 47 | * @protected 48 | * @constructor HasOne 49 | * @see {@link Database#hasOne} 50 | */ 51 | init: function() { 52 | this._super.apply(this, arguments); 53 | 54 | if (this._options.through) { 55 | this._options.through = inflection.singularize(this._options.through); 56 | } 57 | }, 58 | 59 | /** 60 | * The item property for this relation. 61 | * 62 | * This property allows access to the cached object that has been fetched 63 | * for a specific model in a given relation. Before the cache has been 64 | * filled, accessing this property will throw an exception. 65 | * 66 | * It is accessible on an individual model via `` and via 67 | * `get`. For instance, a user that has one blog would cause this 68 | * method to get triggered via `user.blog` or `user.getBlog`. 69 | * 70 | * The naming conventions are set forth in {@link HasOne#overrides}. 71 | * 72 | * @method 73 | * @protected 74 | * @param {Model} instance The model instance on which to operate. 75 | * @see {@link BaseRelation#methods} 76 | */ 77 | item: function(instance) { 78 | return this.collection(instance)[0]; 79 | }, 80 | 81 | /** 82 | * The item property setter for this relation. 83 | * 84 | * This property allows altering the associated object of a specific model in 85 | * a given relation. 86 | * 87 | * It is accessible on an individual model via assignment with `` 88 | * and via `set`. For instance, a user that has one blog would 89 | * cause this method to get triggered via 90 | * `user.blog = '...'` or `user.setBlog`. 91 | * 92 | * The naming conventions are set forth in {@link HasOne#overrides}. 93 | * 94 | * @method 95 | * @protected 96 | * @param {Model} instance The model instance on which to operate. 97 | * @see {@link BaseRelation#methods} 98 | */ 99 | set: function(instance, value) { 100 | var collection = this._getCollectionCache(instance); 101 | var current = collection && collection[0]; 102 | if (current) { this.removeObjects(instance, current); } 103 | if (value) { this.addObjects(instance, value); } 104 | }, 105 | 106 | /** 107 | * Fetch the related object. 108 | * 109 | * @method 110 | * @protected 111 | * @param {Model} instance The model instance on which to operate. 112 | * @see {@link BaseRelation#methods} 113 | */ 114 | fetch: function(instance) { 115 | return this.objectsQuery(instance).fetch().get('0'); 116 | }, 117 | 118 | /** 119 | * Override of {@link HasMany#scopeObjectQuery} that ensures that only one 120 | * object will be fetched. 121 | * 122 | * @method 123 | * @protected 124 | * @see {@link HasMany#scopeObjectQuery} 125 | */ 126 | scopeObjectQuery: function(/*instance, query*/) { 127 | return this._super.apply(this, arguments).limit(1); 128 | }, 129 | 130 | /** 131 | * Override of {@link HasMany#_prefetchQuery} that ensures that only the 132 | * necessary number of items are prefetched. 133 | * 134 | * @method 135 | * @protected 136 | * @see {@link HasMany#_prefetchQuery} 137 | */ 138 | _prefetchQuery: function(instances) { 139 | return this._super.apply(this, arguments).limit(instances.length); 140 | }, 141 | 142 | /** 143 | * Override of {@link HasMany#beforeAssociatingObjects} that ensures that the 144 | * collection cache always exists when associating objects (since with a 145 | * has-one it represents all possible objects in the relationship). 146 | * 147 | * @method 148 | * @protected 149 | * @see {@link HasMany#beforeAssociatingObjects} 150 | */ 151 | beforeAssociatingObjects: function(instance/*, ...*/) { 152 | this._setCollectionCache(instance, []); 153 | return this._super.apply(this, arguments); 154 | }, 155 | 156 | /** 157 | * Override of {@link BaseRelation#overrides}. 158 | * 159 | * @method 160 | * @protected 161 | * @see {@link BaseRelation#overrides} 162 | */ 163 | overrides: function() { 164 | this._super(); 165 | 166 | if (!this._options.implicit) { 167 | this.removeOverride(''); 168 | this.removeOverride('Objects'); 169 | this.removeOverride('add'); 170 | this.removeOverride('add'); 171 | this.removeOverride('remove'); 172 | this.removeOverride('remove'); 173 | this.removeOverride('clear'); 174 | 175 | this.overrideProperty('', 'item', 'set'); 176 | this.addHelper('get', 'item'); 177 | this.addHelper('set', 'set'); 178 | this.addHelper('fetch', 'fetch'); 179 | } 180 | }, 181 | 182 | }); 183 | 184 | module.exports = HasOne.reopenClass({ __name__: 'HasOne' }); 185 | -------------------------------------------------------------------------------- /lib/relations/relation_attr.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var AttributeTrigger = require('corazon').AttributeTrigger; 5 | 6 | /** 7 | * Generate closure around calculation of relations. 8 | * 9 | * @function RelationAttr~relations 10 | * @private 11 | * @return {Function} 12 | */ 13 | var relations = function() { 14 | 15 | var calculate = function() { // jscs:ignore jsDoc 16 | var source = this.__metaclass__.prototype; 17 | var relationSuffix = _.ary(_.partial(_.endsWith, _, 'Relation'), 1); 18 | return _(Object.keys(source)) 19 | .filter(relationSuffix).map(_.propertyOf(source)) 20 | .transform(function(obj, relation) { 21 | obj[relation._name] = relation; 22 | }, {}) 23 | .value(); 24 | }; 25 | 26 | // calculating the relations may trigger the addition of new implicit 27 | // relations causing the result to immediately be out of date. we therefore 28 | // re-calcuate the relations until we get the same number of relations as the 29 | // last time we calculated them, then cache the result. also, it's important 30 | // to note that the adding of implicit relations may actually trigger this 31 | // method being invoked again (mutual recursion), so we cannot cache the 32 | // result until we have the stable result. 33 | var cache; 34 | return function() { 35 | if (cache) { return cache; } 36 | 37 | var complete = false; 38 | var previous = []; 39 | var relations; 40 | while (!complete) { 41 | relations = calculate.call(this); 42 | complete = (relations.length === previous.length); 43 | previous = relations; 44 | } 45 | 46 | return (cache = relations); 47 | }; 48 | 49 | }; 50 | 51 | /** 52 | * @class RelationAttr 53 | * @extends AttributeTrigger 54 | * @classdesc 55 | * 56 | * An attribute trigger that uses subclasses of {@link BaseRelation} to define 57 | * a set of properties & methods dynamically. Everything that's returned from 58 | * the subclass's {@link BaseRelation#methods} will be added to the object on 59 | * which the trigger was defined. 60 | * 61 | * While you could use this class directly, the convenience method 62 | * {@link BaseRelation.attribute}, allows creation by subclasses with a simpler 63 | * method call. 64 | */ 65 | var RelationAttr = AttributeTrigger.extend(/** @lends RelationAttr# */ { 66 | 67 | /** 68 | * Create a RelationAttr. 69 | * 70 | * @protected 71 | * @constructor RelationAttr 72 | * @param {Class} type The type of relation 73 | * @param {Arguments} arguments Arguments to be pass through to the creation 74 | * of the relation type (these will be concatenated on the end of the 75 | * standard arguments passed to the relation constructor). 76 | */ 77 | init: function(type, args) { 78 | this._type = type; 79 | this._args = _.toArray(args); 80 | }, 81 | 82 | /** 83 | * Build a new relation object for determining methods to add. 84 | * 85 | * The {@link BaseRelation} subclass is created based on the type & arguments 86 | * that were given to the initializer. The object that's created will be 87 | * constructed with the following arguments: the name of the attribute being 88 | * defined, and the class object on which it's being defined. Any `args` 89 | * given to the trigger's constructor will be concatenated on to these 90 | * arguments. 91 | * 92 | * @method 93 | * @private 94 | * @param {String} name The name of the attribute being defined. 95 | * @param {Object} details The details of the attribute, given unaltered 96 | * from {@link AttributeTrigger#invoke}. 97 | * @see {@link AttributeTrigger#invoke} 98 | */ 99 | _relation: function(name, details) { 100 | var type = this._type; 101 | var args = [name, details].concat(this._args); 102 | return type.create.apply(type, args); 103 | }, 104 | 105 | /** 106 | * Get existing attribute and property names from the attribute setup details 107 | * provided to {@link RelationAttr#invoke}. This will pull from both those 108 | * that have already been defined on the class & those that are being defined 109 | * with this attribute. 110 | * 111 | * @param {Object} details The details of the attribute, given unaltered 112 | * from {@link AttributeTrigger#invoke}. 113 | * @return {Object} The collection of names with values set to a truthful 114 | * value. 115 | */ 116 | _existing: function(details) { 117 | var existing = {}; 118 | var assign = function(name) { existing[name] = true; }; // jscs:ignore jsDoc 119 | _.keys(details.prototype).forEach(assign); // already defined 120 | _.keys(details.properties).forEach(assign); // being defined with this attr 121 | return existing; 122 | }, 123 | 124 | /** 125 | * Override of {@link AttributeTrigger#invoke}. 126 | * 127 | * @method 128 | * @public 129 | * @see {@link AttributeTrigger#invoke} 130 | */ 131 | invoke: function(name, reopen, details) { 132 | var existing = this._existing(details); 133 | var relation = this._relation(name, details); 134 | var instanceMethods = relation.instanceMethods(existing); 135 | var classMethods = relation.classMethods(); 136 | reopen(instanceMethods); 137 | details.context.__identity__.reopenClass(classMethods); 138 | 139 | // add a new copy of the relations function since the relations have 140 | // changed on this model & they'll need to be recalculated. 141 | details.context.__identity__.reopenClass({ 142 | _relations: relations(), 143 | }); 144 | }, 145 | 146 | }); 147 | 148 | module.exports = RelationAttr.reopenClass({ __name__: 'RelationAttr' }); 149 | -------------------------------------------------------------------------------- /lib/templates/azulfile.js.template: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | <% if (database === 'pg') { %> 3 | module.exports = { 4 | production: { 5 | adapter: 'pg', 6 | connection: { 7 | database: 'database', 8 | user: 'root', 9 | password: '' 10 | } 11 | }, 12 | development: { 13 | adapter: 'pg', 14 | connection: { 15 | database: 'database_dev', 16 | user: 'root', 17 | password: '' 18 | } 19 | }, 20 | test: { 21 | adapter: 'pg', 22 | connection: { 23 | database: 'database_test', 24 | user: 'root', 25 | password: '' 26 | } 27 | } 28 | }; 29 | <% } else if (database === 'mysql') { %> 30 | module.exports = { 31 | production: { 32 | adapter: 'mysql', 33 | connection: { 34 | database: 'database', 35 | user: 'root', 36 | password: '' 37 | } 38 | }, 39 | development: { 40 | adapter: 'mysql', 41 | connection: { 42 | database: 'database_dev', 43 | user: 'root', 44 | password: '' 45 | } 46 | }, 47 | test: { 48 | adapter: 'mysql', 49 | connection: { 50 | database: 'database_test', 51 | user: 'root', 52 | password: '' 53 | } 54 | } 55 | }; 56 | <% } else if (database === 'sqlite3') { %> 57 | module.exports = { 58 | production: { 59 | adapter: 'sqlite3', 60 | connection: { 61 | filename: './.production.sqlite3' 62 | } 63 | }, 64 | development: { 65 | adapter: 'sqlite3', 66 | connection: { 67 | filename: './.development.sqlite3' 68 | } 69 | }, 70 | test: { 71 | adapter: 'sqlite3', 72 | connection: { 73 | filename: './.test.sqlite3' 74 | } 75 | } 76 | }; 77 | <% } %> -------------------------------------------------------------------------------- /lib/templates/migration.js.template: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.change = function(schema) { 4 | 5 | }; 6 | 7 | // 8 | // exports.up = function(schema) { 9 | // 10 | // }; 11 | // 12 | // exports.down = function(schema) { 13 | // 14 | // }; 15 | // 16 | -------------------------------------------------------------------------------- /lib/util/inflection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | 5 | /** 6 | * Inflection for morphing words. 7 | * 8 | * Note that all replacements are done in lowercase. 9 | * 10 | * @public 11 | * @constructor Inflection 12 | */ 13 | var Inflection = function Inflection() { 14 | this._plural = []; 15 | this._singular = []; 16 | this._uncountables = []; 17 | }; 18 | 19 | /** 20 | * Define a rule for making a string plural. Each added rule will take 21 | * precedence over prior added rules. 22 | * 23 | * @method 24 | * @public 25 | * @param {RegExp|String} rule The rule. 26 | * @param {String} replacement The string to use to replace the contents. 27 | */ 28 | Inflection.prototype.plural = function(rule, replacement) { 29 | this._plural.unshift({ 30 | regex: this._regex(rule), 31 | replacement: replacement, 32 | }); 33 | }; 34 | 35 | /** 36 | * Define a rule for making a string singular. Each added rule will take 37 | * precedence over prior added rules. 38 | * 39 | * @method 40 | * @public 41 | * @param {RegExp|String} rule The rule. 42 | * @param {String} replacement The string to use to replace the contents. 43 | */ 44 | Inflection.prototype.singular = function(rule, replacement) { 45 | this._singular.unshift({ 46 | regex: this._regex(rule), 47 | replacement: replacement, 48 | }); 49 | }; 50 | 51 | /** 52 | * Define a rule for an irregular word. Each added rule will take precedence 53 | * over prior added rules. 54 | * 55 | * @method 56 | * @public 57 | * @param {String} singular The singular form of the word. 58 | * @param {String} plural The singular form of the word. 59 | */ 60 | Inflection.prototype.irregular = function(singular, plural) { 61 | this.plural('\\b' + singular + '\\b', plural); 62 | this.plural('\\b' + plural + '\\b', plural); 63 | this.singular('\\b' + singular + '\\b', singular); 64 | this.singular('\\b' + plural + '\\b', singular); 65 | }; 66 | 67 | /** 68 | * Define a rule for an uncountable word. Each added rule will take precedence 69 | * over prior added rules. 70 | * 71 | * @method 72 | * @public 73 | * @param {...(String|Array)} word The word or words that are not countable. 74 | */ 75 | Inflection.prototype.uncountable = function(/*uncountable...*/) { 76 | var uncountables = _.flatten(arguments); 77 | this._uncountables = this._uncountables.concat(uncountables); 78 | }; 79 | 80 | /** 81 | * Make a word singular. 82 | * 83 | * @method 84 | * @public 85 | * @param {String} word The word to make singular. 86 | */ 87 | Inflection.prototype.singularize = function(word) { 88 | return this._applyRules(word, this._singular); 89 | }; 90 | 91 | /** 92 | * Make a word plural. 93 | * 94 | * @method 95 | * @public 96 | * @param {String} word The word to make plural. 97 | */ 98 | Inflection.prototype.pluralize = function(word) { 99 | return this._applyRules(word, this._plural); 100 | }; 101 | 102 | /** 103 | * Create a regular expression. 104 | * 105 | * @method 106 | * @private 107 | * @param {String|RegExp} value The value to make into a RegExp. 108 | */ 109 | Inflection.prototype._regex = function(value) { 110 | return value instanceof RegExp ? value : new RegExp(value); 111 | }; 112 | 113 | /** 114 | * Apply rules to a given word. 115 | * 116 | * @method 117 | * @private 118 | * @param {String} word The word to apply rules to. 119 | * @param {Array} rules The array of rules to search through to apply. 120 | */ 121 | Inflection.prototype._applyRules = function(word, rules) { 122 | var result = word; 123 | if (_.contains(this._uncountables, word)) { result = word; } 124 | else { 125 | var rule = _.find(rules, function(obj) { 126 | return obj.regex.test(word); 127 | }); 128 | if (rule) { 129 | result = word.replace(rule.regex, rule.replacement); 130 | } 131 | } 132 | return result; 133 | }; 134 | 135 | /** 136 | * The exported inflection object. 137 | * 138 | * The pre-defined rules are frozen to ensure that we don't break compatibility 139 | * with any applications that use them. 140 | * 141 | * @name Inflection~inflection 142 | * @type {Inflection} 143 | */ 144 | var inflection = new Inflection(); 145 | 146 | inflection.plural(/$/i, 's'); 147 | inflection.plural(/s$/i, 's'); 148 | inflection.plural(/^(ax|test)is$/i, '$1es'); 149 | inflection.plural(/(octop|vir)us$/i, '$1i'); 150 | inflection.plural(/(octop|vir)i$/i, '$1i'); 151 | inflection.plural(/(alias|status)$/i, '$1es'); 152 | inflection.plural(/(bu)s$/i, '$1ses'); 153 | inflection.plural(/(buffal|tomat)o$/i, '$1oes'); 154 | inflection.plural(/([ti])um$/i, '$1a'); 155 | inflection.plural(/([ti])a$/i, '$1a'); 156 | inflection.plural(/sis$/i, 'ses'); 157 | inflection.plural(/(?:([^f])fe|([lr])f)$/i, '$1$2ves'); 158 | inflection.plural(/(hive)$/i, '$1s'); 159 | inflection.plural(/([^aeiouy]|qu)y$/i, '$1ies'); 160 | inflection.plural(/(x|ch|ss|sh)$/i, '$1es'); 161 | inflection.plural(/(matr|vert|ind)(?:ix|ex)$/i, '$1ices'); 162 | inflection.plural(/^(m|l)ouse$/i, '$1ice'); 163 | inflection.plural(/^(m|l)ice$/i, '$1ice'); 164 | inflection.plural(/^(ox)$/i, '$1en'); 165 | inflection.plural(/^(oxen)$/i, '$1'); 166 | inflection.plural(/(quiz)$/i, '$1zes'); 167 | 168 | /* jscs:disable maximumLineLength */ 169 | inflection.singular(/s$/i, ''); 170 | inflection.singular(/(ss)$/i, '$1'); 171 | inflection.singular(/(n)ews$/i, '$1ews'); 172 | inflection.singular(/([ti])a$/i, '$1um'); 173 | inflection.singular(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)$/i, '$1sis'); 174 | inflection.singular(/(^analy)(sis|ses)$/i, '$1sis'); 175 | inflection.singular(/([^f])ves$/i, '$1fe'); 176 | inflection.singular(/(hive)s$/i, '$1'); 177 | inflection.singular(/(tive)s$/i, '$1'); 178 | inflection.singular(/([lr])ves$/i, '$1f'); 179 | inflection.singular(/([^aeiouy]|qu)ies$/i, '$1y'); 180 | inflection.singular(/(s)eries$/i, '$1eries'); 181 | inflection.singular(/(m)ovies$/i, '$1ovie'); 182 | inflection.singular(/(x|ch|ss|sh)es$/i, '$1'); 183 | inflection.singular(/^(m|l)ice$/i, '$1ouse'); 184 | inflection.singular(/(bus)(es)?$/i, '$1'); 185 | inflection.singular(/(o)es$/i, '$1'); 186 | inflection.singular(/(shoe)s$/i, '$1'); 187 | inflection.singular(/(cris|test)(is|es)$/i, '$1is'); 188 | inflection.singular(/^(a)x[ie]s$/i, '$1xis'); 189 | inflection.singular(/(octop|vir)(us|i)$/i, '$1us'); 190 | inflection.singular(/(alias|status)(es)?$/i, '$1'); 191 | inflection.singular(/^(ox)en/i, '$1'); 192 | inflection.singular(/(vert|ind)ices$/i, '$1ex'); 193 | inflection.singular(/(matr)ices$/i, '$1ix'); 194 | inflection.singular(/(quiz)zes$/i, '$1'); 195 | inflection.singular(/(database)s$/i, '$1'); 196 | /* jscs:enable maximumLineLength */ 197 | 198 | inflection.irregular('person', 'people'); 199 | inflection.irregular('man', 'men'); 200 | inflection.irregular('child', 'children'); 201 | inflection.irregular('sex', 'sexes'); 202 | inflection.irregular('move', 'moves'); 203 | inflection.irregular('zombie', 'zombies'); 204 | 205 | inflection.uncountable([ 206 | 'equipment', 'information', 'rice', 'money', 'species', 'series', 'fish', 207 | 'sheep', 'jeans', 'police', 208 | ]); 209 | 210 | module.exports = inflection; 211 | module.exports.Inflection = Inflection; 212 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "azul", 3 | "version": "0.0.1-alpha.15", 4 | "description": "Elegant ORM for Node.js", 5 | "homepage": "https://github.com/wbyoung/azul", 6 | "bugs": { 7 | "url": "https://github.com/wbyoung/azul/issues" 8 | }, 9 | "main": "index.js", 10 | "bin": { 11 | "azul": "./bin/azul" 12 | }, 13 | "scripts": { 14 | "test": "jshint . && jscs . && istanbul cover node_modules/.bin/_mocha --report html --", 15 | "test-travis": "jshint . && jscs . && istanbul cover node_modules/.bin/_mocha --report lcovonly --", 16 | "docs": "./docs/scripts/build && ./docs/scripts/deploy" 17 | }, 18 | "files": [ 19 | "index.js", 20 | "lib", 21 | "bin", 22 | "LICENSE" 23 | ], 24 | "repository": { 25 | "type": "git", 26 | "url": "git://github.com/wbyoung/azul.git" 27 | }, 28 | "keywords": [ 29 | "postgres", 30 | "mysql", 31 | "sqlite", 32 | "pg", 33 | "orm", 34 | "database", 35 | "sql" 36 | ], 37 | "author": "Whitney Young", 38 | "license": "MIT", 39 | "dependencies": { 40 | "bluebird": "^2.3.5", 41 | "chalk": "^1.0.0", 42 | "commander": "^2.6.0", 43 | "corazon": "^0.1.1", 44 | "generic-pool": "^2.1.1", 45 | "interpret": "^0.6.0", 46 | "liftoff": "^2.0.0", 47 | "lodash": "^3.0.0", 48 | "maguey": "^0.0.2", 49 | "minimist": "^1.1.0", 50 | "tildify": "^1.0.0", 51 | "underscore.string": "^3.0.0", 52 | "v8flags": "^2.0.2" 53 | }, 54 | "devDependencies": { 55 | "azul-chai": "^0.1.0", 56 | "chai": "^3.3.0", 57 | "chai-also": "^0.1.0", 58 | "chai-as-promised": "^5.0.0", 59 | "chai-properties": "^1.2.0", 60 | "cheerio": "^0.19.0", 61 | "ink-docstrap": "^0.5.2", 62 | "istanbul": "^0.3.0", 63 | "jscs": "^2.2.1", 64 | "jshint": "^2.8.0", 65 | "maguey-chai": "^0.3.4", 66 | "metalsmith": "^2.1.0", 67 | "metalsmith-markdown": "^0.2.1", 68 | "metalsmith-metallic": "^0.3.1", 69 | "metalsmith-sass": "^1.0.0", 70 | "metalsmith-serve": "^0.0.4", 71 | "metalsmith-templates": "^0.7.0", 72 | "metalsmith-watch": "^1.0.0", 73 | "mocha": "^2.0.0", 74 | "mysql": "^2.5.2", 75 | "pg": "^4.1.1", 76 | "semver": "^5.0.3", 77 | "sinon": "^1.10.3", 78 | "sinon-chai": "^2.5.0", 79 | "sqlite3": "^3.0.2", 80 | "swig": "^1.4.2" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.jshintrc", 3 | "globals": { 4 | "describe": true, 5 | "it": true, 6 | "before": true, 7 | "beforeEach": true, 8 | "after": true, 9 | "afterEach": true, 10 | "expect": true, 11 | "should": true, 12 | "sinon": true, 13 | "__adapter": true, 14 | "__db": true, 15 | "__query": true 16 | }, 17 | "expr": true 18 | } 19 | -------------------------------------------------------------------------------- /test/cli/cli_helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../helpers'); 4 | 5 | var _ = require('lodash'); 6 | var Promise = require('bluebird'); 7 | 8 | /** 9 | * Helper function to run the CLI and capture output/exit status. The resolved 10 | * object will contain the following properties: 11 | * 12 | * - `stdout` - The standard output for the cli 13 | * - `stderr` - The standard error for the cli 14 | * - `exitStatus` - The exit status 15 | * - `exitCalled` - Whether `process.exit` was called 16 | * 17 | * @param {Object} env Liftoff environment configuration. 18 | * @param {Function} fn The function to run, usually this is `cli`. 19 | * @return {Promise} A promise. 20 | */ 21 | module.exports.cmd = function(env, fn) { 22 | var details = { 23 | stdout: '', 24 | stderr: '', 25 | exitStatus: 0, 26 | exitCalled: false, 27 | }; 28 | 29 | sinon.stub(process, 'exit', function(status) { 30 | throw _.extend(new Error('Exit called.'), { 31 | code: 'PROCESS_EXIT_CALLED', 32 | status: status || 0, 33 | }); 34 | }); 35 | sinon.stub(process.stdout, 'write', function(data) { 36 | details.stdout += data.toString(); 37 | }); 38 | sinon.stub(process.stderr, 'write', function(data) { 39 | details.stderr += data.toString(); 40 | }); 41 | 42 | return Promise 43 | .resolve(env) 44 | .then(fn) 45 | .catch(function(e) { 46 | if (e.code === 'PROCESS_EXIT_CALLED') { 47 | details.exitStatus = e.status; 48 | details.exitCalled = true; 49 | } 50 | else { throw e; } 51 | }) 52 | .return(details) 53 | .finally(function() { 54 | process.exit.restore(); 55 | process.stdout.write.restore(); 56 | process.stderr.write.restore(); 57 | }); 58 | }; 59 | -------------------------------------------------------------------------------- /test/cli/cli_tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../helpers'); 4 | 5 | var cli = require('../../lib/cli'); 6 | var actions = require('../../lib/cli/actions'); 7 | var cmd = require('./cli_helpers').cmd; 8 | var path = require('path'); 9 | var cp = require('child_process'); 10 | 11 | describe('CLI', function() { 12 | var azulfile = path.join(__dirname, '../fixtures/cli/azulfile.json'); 13 | 14 | beforeEach(function() { 15 | sinon.stub(actions, 'init'); 16 | sinon.stub(actions, 'migrate'); 17 | }); 18 | 19 | afterEach(function() { 20 | actions.init.restore(); 21 | actions.migrate.restore(); 22 | }); 23 | 24 | it('provides help when no command is given', function() { 25 | process.argv = ['node', '/path/to/azul']; 26 | return cmd({ modulePath: '.', configPath: azulfile }, cli) 27 | .then(function(proc) { 28 | expect(proc.exitStatus).to.eql(0); 29 | expect(proc.exitCalled).to.eql(true); 30 | expect(proc.stdout).to.match(/usage: azul \[options\] command/i); 31 | expect(proc.stdout.match(/--azulfile/g).length).to.eql(1); 32 | }); 33 | }); 34 | 35 | it('provides help when local azul is missing', function() { 36 | process.argv = ['node', '/path/to/azul', '--help']; 37 | return cmd({ modulePath: '.', configPath: null }, cli) 38 | .then(function(proc) { 39 | expect(proc.exitStatus).to.eql(0); 40 | expect(proc.exitCalled).to.eql(true); 41 | expect(proc.stdout).to.match(/usage: azul \[options\] command/i); 42 | expect(proc.stdout.match(/--azulfile/g).length).to.eql(1); 43 | }); 44 | }); 45 | 46 | it('provides version when local azul is missing', function() { 47 | process.argv = ['node', '/path/to/azul', '--version']; 48 | return cmd({ modulePath: '.', configPath: null }, cli) 49 | .then(function(proc) { 50 | expect(proc.exitStatus).to.eql(0); 51 | expect(proc.exitCalled).to.eql(true); 52 | expect(proc.stdout).to.match(/\d+\.\d+\.\d+(-(alpha|beta)\.\d+)?\n/i); 53 | }); 54 | }); 55 | 56 | it('ensures a local azul is present', function() { 57 | process.argv = ['node', '/path/to/azul', 'migrate']; 58 | return cmd({ modulePath: null, cwd: '.', configPath: azulfile }, cli) 59 | .then(function(proc) { 60 | expect(proc.exitStatus).to.not.eql(0); 61 | expect(proc.exitCalled).to.eql(true); 62 | expect(proc.stdout).to.match(/local azul not found/i); 63 | expect(actions.migrate).to.not.have.been.called; 64 | }); 65 | }); 66 | 67 | it('ensures an azulfile is present', function() { 68 | process.argv = ['node', '/path/to/azul', 'migrate']; 69 | return cmd({ modulePath: '.', configPath: null }, cli) 70 | .then(function(proc) { 71 | expect(proc.exitStatus).to.not.eql(0); 72 | expect(proc.exitCalled).to.eql(true); 73 | expect(proc.stdout).to.match(/no azulfile found/i); 74 | expect(actions.migrate).to.not.have.been.called; 75 | }); 76 | }); 77 | 78 | it('does not need an azulfile for init', function() { 79 | process.argv = ['node', '/path/to/azul', 'init']; 80 | return cmd({ modulePath: '.', configPath: null }, cli) 81 | .then(function(proc) { 82 | expect(proc.exitStatus).to.eql(0); 83 | expect(proc.exitCalled).to.eql(false); 84 | expect(proc.stdout).to.eql(''); 85 | expect(actions.init).to.have.been.calledOnce; 86 | }); 87 | }); 88 | 89 | it('calls actions when a command is given', function() { 90 | process.argv = ['node', '/path/to/azul', 'migrate']; 91 | return cmd({ modulePath: '.', configPath: azulfile }, cli) 92 | .then(function(proc) { 93 | expect(proc.exitStatus).to.eql(0); 94 | expect(proc.exitCalled).to.eql(false); 95 | expect(proc.stdout).to.eql(''); 96 | expect(actions.migrate).to.have.been.calledOnce; 97 | }); 98 | }); 99 | 100 | it('passes config & options to actions', function() { 101 | process.argv = [ 102 | 'node', '/path/to/azul', 'migrate', 103 | '--migrations', './db-migrations', 104 | ]; 105 | return cmd({ modulePath: '.', configPath: azulfile }, cli) 106 | .then(function(proc) { 107 | expect(proc.exitStatus).to.eql(0); 108 | expect(proc.exitCalled).to.eql(false); 109 | expect(proc.stdout).to.eql(''); 110 | expect(actions.migrate).to.have.been.calledOnce; 111 | expect(actions.migrate.getCall(0).args[0]) 112 | .to.eql({ test: { adapter: 'mock' }}); 113 | expect(actions.migrate.getCall(0).args[1].migrations) 114 | .to.eql('./db-migrations'); 115 | }); 116 | }); 117 | 118 | describe('exported event handlers', function() { 119 | it('displays a message when external modules are loaded', function() { 120 | return cmd(null, function() { cli.require('xmod'); }) 121 | .then(function(proc) { 122 | expect(proc.stdout).to.match(/requiring.*module.*xmod/i); 123 | }); 124 | }); 125 | 126 | it('displays a message when external modules fail to load', function() { 127 | return cmd(null, function() { cli.requireFail('xmod'); }) 128 | .then(function(proc) { 129 | expect(proc.stdout).to.match(/failed.*module.*xmod/i); 130 | }); 131 | }); 132 | 133 | it('displays a message when respawned', function() { 134 | return cmd(null, function() { cli.respawn(['--harmony'], { pid: 1234 }); }) 135 | .then(function(proc) { 136 | expect(proc.stdout).to.match(/flags.*--harmony.*\n.*respawn.*1234/im); 137 | }); 138 | }); 139 | }); 140 | }); 141 | 142 | describe('CLI loading', function() { 143 | 144 | it('does not require local azul modules when loaded', function(done) { 145 | var loader = path.join(__dirname, '../fixtures/cli/load.js'); 146 | var cliPath = path.join(__dirname, '../../lib/cli/index.js'); 147 | 148 | var child = cp.fork(loader, [], { env: process.env }); 149 | var message; 150 | var verify = function() { 151 | try { 152 | var local = message.modules.filter(function(name) { 153 | return !name.match(/azul\/node_modules/); 154 | }); 155 | expect(local.sort()).to.eql([loader, cliPath].sort()); 156 | done(); 157 | } 158 | catch (e) { done(e); } 159 | }; 160 | 161 | child.on('close', verify); 162 | child.on('message', function(m) { message = m; }); 163 | }); 164 | 165 | it('does not require local azul modules when showing help', function(done) { 166 | var helpPath = path.join(__dirname, '../fixtures/cli/help.js'); 167 | var configPath = path.join(__dirname, '../../package.json'); 168 | var cliPath = path.join(__dirname, '../../lib/cli/index.js'); 169 | 170 | var child = cp.fork(helpPath, [], { env: process.env, silent: true }); 171 | var message; 172 | var verify = function() { 173 | try { 174 | var local = message.modules.filter(function(name) { 175 | return !name.match(/azul\/node_modules/); 176 | }); 177 | expect(local.sort()).to.eql([helpPath, configPath, cliPath].sort()); 178 | done(); 179 | } 180 | catch (e) { done(e); } 181 | }; 182 | 183 | child.on('close', verify); 184 | child.on('message', function(m) { message = m; }); 185 | }); 186 | 187 | }); 188 | -------------------------------------------------------------------------------- /test/common/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../helpers'); 4 | 5 | module.exports = { 6 | models: require('./models'), 7 | }; 8 | -------------------------------------------------------------------------------- /test/common/models.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../helpers'); 4 | 5 | module.exports = function() { 6 | /* global db, adapter */ 7 | 8 | var attr = db.attr; 9 | var hasMany = db.hasMany; 10 | var hasOne = db.hasOne; 11 | var belongsTo = db.belongsTo; 12 | 13 | // blog models: user, blog, article, comment 14 | // users each have their own blog, but can write articles for other blogs 15 | db.model('user', { 16 | username: attr(), 17 | email: attr('email_addr'), 18 | blog: hasOne({ inverse: 'owner' }), 19 | articles: hasMany({ inverse: 'author' }), 20 | comments: hasMany({ inverse: 'commenter' }), 21 | feedback: hasMany('comments', { through: 'articles', source: 'comments' }), 22 | ownArticles: hasMany('articles', { through: 'blog', source: 'articles' }), 23 | otherBlogs: hasMany('blogs', { through: 'articles', source: 'blog' }), 24 | }); 25 | db.model('blog', { 26 | title: attr(), 27 | owner: belongsTo('user'), 28 | articles: hasMany(), 29 | comments: hasMany({ through: 'articles' }), 30 | }); 31 | db.model('article', { 32 | title: attr(), 33 | blog: belongsTo(), 34 | comments: hasMany(), 35 | author: belongsTo('user'), 36 | authorId: attr('author_identifier'), 37 | }); 38 | db.model('comment', { 39 | body: attr(), 40 | article: belongsTo('article'), 41 | commenter: belongsTo('user'), 42 | }); 43 | 44 | adapter.respond(/select.*from "users"/i, [ 45 | { id: 231, username: 'jack', 'email_addr': 'jack@gmail.com' }, 46 | { id: 844, username: 'susan', 'email_addr': 'susan@gmail.com' }, 47 | ]); 48 | adapter.respond(/select.*from "users".*limit 1/i, [ 49 | { id: 231, username: 'jack', 'email_addr': 'jack@gmail.com' }, 50 | ]); 51 | adapter.sequence(/insert into "users".*returning "id"/i, 398); 52 | adapter.respond(/select.*from "blogs"/i, [ 53 | { id: 348, title: 'Azul.js', 'owner_id': 231 }, 54 | { id: 921, title: 'Maguey', 'owner_id': undefined }, 55 | ]); 56 | adapter.respond(/select.*from "blogs".*limit 1/i, [ 57 | { id: 348, title: 'Azul.js', 'owner_id': 231 }, 58 | ]); 59 | adapter.sequence(/insert into "blogs".*returning "id"/i, 823); 60 | adapter.sequence(/insert into "articles".*returning "id"/i, 199); 61 | adapter.sequence(/insert into "comments".*returning "id"/i, 919); 62 | 63 | // school models: student, course, enrollment 64 | db.model('student', { 65 | name: attr(), 66 | enrollments: hasMany(), 67 | courses: hasMany({ through: 'enrollments' }), 68 | }); 69 | db.model('course', { 70 | subject: attr(), 71 | enrollments: hasMany(), 72 | students: hasMany({ through: 'enrollments' }), 73 | }); 74 | db.model('enrollment', { 75 | date: attr(), 76 | student: belongsTo(), 77 | course: belongsTo(), 78 | }); 79 | 80 | // company models: employee 81 | db.model('employee', { 82 | subordinates: hasMany('employee', { inverse: 'manager' }), 83 | manager: belongsTo('employee', { inverse: 'subordinates' }), 84 | }); 85 | 86 | // manufacturing models: supplier, account, accountHistory 87 | db.model('supplier', { 88 | name: attr(), 89 | account: hasOne(), 90 | accountHistory: hasOne({ through: 'account' }), 91 | }); 92 | db.model('account', { 93 | name: attr(), 94 | supplier: belongsTo(), 95 | accountHistory: hasOne(), 96 | }); 97 | db.model('accountHistory', { 98 | details: attr(), 99 | account: belongsTo(), 100 | }); 101 | 102 | adapter.respond(/select.*from "suppliers"/i, [ 103 | { id: 229, name: 'Bay Foods' }, 104 | { id: 430, name: 'Natural Organics' }, 105 | ]); 106 | adapter.respond(/select.*from "accounts"/i, [ 107 | { id: 392, name: 'Bay Foods Account', 'supplier_id': 229 }, 108 | { id: 831, name: 'Natural Organics Account', 'supplier_id': 430 }, 109 | ]); 110 | adapter.respond(/select.*from "account_histories"/i, [ 111 | { id: 832, details: 'many details', 'account_id': 392 }, 112 | ]); 113 | 114 | // programming models: node 115 | db.model('node', { 116 | parent: belongsTo('node', { inverse: 'nodes' }), 117 | nodes: hasMany('node', { inverse: 'parent' }), 118 | }); 119 | 120 | // social model: person 121 | db.model('person', { 122 | name: attr(), 123 | bestFriendOf: hasMany('person', { inverse: 'bestFriend' }), 124 | bestFriend: belongsTo('person', { inverse: 'bestFriendOf' }), 125 | }); 126 | 127 | adapter.sequence(/insert into "people".*returning "id"/i, 102); 128 | 129 | // social model: individual, relationship 130 | db.model('individual', { 131 | name: attr(), 132 | followers: hasMany('individual', { through: 'passiveRelationship' }), 133 | following: hasMany('individual', { 134 | through: 'activeRelationships', 135 | source: 'followed', 136 | }), 137 | activeRelationships: hasMany('relationship', { inverse: 'follower' }), 138 | passiveRelationships: hasMany('relationship', { inverse: 'followed' }), 139 | }); 140 | db.model('relationship', { 141 | follower: belongsTo('individual'), 142 | followed: belongsTo('individual'), 143 | }); 144 | 145 | adapter.sequence(/insert into "individuals".*returning "id"/i, 102); 146 | }; 147 | -------------------------------------------------------------------------------- /test/compatibility_tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./helpers'); 4 | 5 | var validate = require('../lib/compatibility'); 6 | 7 | describe('compatibility', function() { 8 | 9 | it('validates', function() { 10 | expect(validate).not.to.throw(); 11 | }); 12 | 13 | describe('when `corazon` class has been swapped out', function() { 14 | beforeEach(function() { 15 | var corazon = require('corazon'); 16 | this.Class = corazon.Class; 17 | corazon.Class = corazon.Class.extend(); 18 | }); 19 | 20 | afterEach(function() { 21 | require('corazon').Class = this.Class; 22 | }); 23 | 24 | it('throws', function() { 25 | expect(validate).throw(/incompatible.*azul.*maguey/i); 26 | }); 27 | }); 28 | 29 | }); 30 | -------------------------------------------------------------------------------- /test/database_tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./helpers'); 4 | 5 | var lib = require('..'); 6 | var Database = require('..').Database; 7 | 8 | describe('Database', function() { 9 | it('fails with an invalid adapter', function() { 10 | var config = { 11 | adapter: 'invalid_adapter', 12 | connection: { 13 | username: 'root', 14 | password: '', 15 | database: 'azul_test', 16 | }, 17 | }; 18 | expect(function() { 19 | Database.create(config); 20 | }).to.throw(/no adapter.*invalid_adapter/i); 21 | }); 22 | 23 | it('can be created via main export', function() { 24 | lib({ adapter: 'pg' }).should.be.an.instanceof(Database.__class__); 25 | }); 26 | 27 | it('fails with an object that is not an adapter', function() { 28 | var config = { 29 | adapter: {}, 30 | }; 31 | expect(function() { 32 | Database.create(config); 33 | }).to.throw(/invalid adapter/i); 34 | }); 35 | 36 | it('loads adapters based on alias names', function() { 37 | expect(function() { 38 | Database.create({ adapter: 'postgres' }); 39 | }).to.not.throw(); 40 | }); 41 | 42 | it('fails with when no configuration is given', function() { 43 | expect(function() { 44 | Database.create(); 45 | }).to.throw(/missing connection/i); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/fixtures/cli/azulfile.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": { 3 | "adapter": "mock" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/cli/help.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (require.main !== module) { return; } 4 | 5 | process.argv.splice(2); 6 | process.argv.splice(2, 0, '--help'); 7 | 8 | process.on('exit', function() { 9 | process.send({ modules: Object.keys(require.cache) }); 10 | }); 11 | 12 | require('../../../lib/cli')({ modulePath: '.', configPath: './azulfile.json' }); 13 | -------------------------------------------------------------------------------- /test/fixtures/cli/load.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (require.main !== module) { return; } 4 | 5 | require('../../../lib/cli'); 6 | 7 | process.send({ modules: Object.keys(require.cache) }); 8 | -------------------------------------------------------------------------------- /test/fixtures/migrations/blog/20141022202234_create_articles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.up = function(schema) { 4 | schema.createTable('articles', function(table) { 5 | table.serial('id').primaryKey(); 6 | table.string('title'); 7 | table.string('author'); 8 | }); 9 | schema.alterTable('articles', function(table) { 10 | table.drop('author'); 11 | table.text('body'); 12 | }); 13 | }; 14 | 15 | exports.down = function(schema) { 16 | schema.dropTable('articles'); 17 | }; 18 | -------------------------------------------------------------------------------- /test/fixtures/migrations/blog/20141022202634_create_comments.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.up = function(schema) { 4 | schema.createTable('comments', function(table) { 5 | table.serial('identifier').primaryKey(); 6 | table.text('body'); 7 | table.integer('article_id').references('articles.id'); 8 | }); 9 | schema.alterTable('comments', function(table) { 10 | table.string('email'); 11 | }); 12 | }; 13 | 14 | exports.down = function(schema) { 15 | return schema.dropTable('comments'); 16 | }; 17 | -------------------------------------------------------------------------------- /test/helpers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./reset')(); 4 | 5 | var _ = require('lodash'); 6 | var sinon = require('sinon'); 7 | var chai = require('chai'); 8 | var chaiAsPromised = require('chai-as-promised'); 9 | 10 | var createAdapter = require('maguey-chai').adapter; 11 | var EntryQuery = require('maguey').EntryQuery; 12 | var Database = require('../..').Database; 13 | var Model = require('../..').Model; 14 | 15 | chaiAsPromised.transferPromiseness = function (assertion, promise) { 16 | assertion.then = promise.then.bind(promise); 17 | assertion.meanwhile = function(value) { 18 | var result = promise.return(value); 19 | return _.extend(result, { should: result.should.eventually }); 20 | }; 21 | }; 22 | 23 | chai.use(require('sinon-chai')); 24 | chai.use(require('maguey-chai')); 25 | chai.use(require('azul-chai')); 26 | chai.use(require('chai-also')); 27 | chai.use(require('chai-properties')); 28 | chai.use(require('chai-as-promised')); 29 | 30 | global.expect = chai.expect; 31 | global.should = chai.should(); 32 | global.sinon = sinon; 33 | 34 | global.__adapter = function(fn) { 35 | return function() { 36 | beforeEach(function() { 37 | global.adapter = createAdapter(); 38 | }); 39 | fn.call(this); 40 | }; 41 | }; 42 | 43 | global.__query = function(fn) { 44 | return __adapter(function() { 45 | beforeEach(function() { 46 | global.query = EntryQuery.create(global.adapter); 47 | }); 48 | fn.call(this); 49 | }); 50 | }; 51 | 52 | global.__db = function(fn) { 53 | return __adapter(function() { 54 | beforeEach(function() { 55 | global.db = Database.create({ adapter: global.adapter }); 56 | }); 57 | fn.call(this); 58 | }); 59 | }; 60 | 61 | Model.reopenClass({ 62 | $: function() { 63 | return _.extend(this.create.apply(this, arguments), { 64 | _dirtyAttributes: {}, 65 | }); 66 | }, 67 | }); 68 | 69 | createAdapter().__identity__.reopen({ 70 | 71 | /** 72 | * Respond specifically for select of migrations. 73 | * 74 | * @param {Array} names Names of migrations that will be used to build the 75 | * full result. 76 | */ 77 | respondToMigrations: function(names) { 78 | var migrations = names.map(function(name, index) { 79 | return { id: index + 1, name: name, batch: 1 }; 80 | }); 81 | this.respond(/select.*from "azul_migrations"/i, migrations); 82 | }, 83 | 84 | }); 85 | -------------------------------------------------------------------------------- /test/helpers/reset.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var maguey = require('maguey'); 5 | 6 | /** 7 | * Names of classes that require resetting. 8 | * 9 | * These query classes are mutated by the bound query & need to be reset each 10 | * time tests are re-run (and only Azul.js is reloaded). 11 | * 12 | * @type {Array} 13 | * @see reset 14 | */ 15 | var restore = [ 16 | 'SelectQuery', 17 | 'InsertQuery', 18 | 'UpdateQuery', 19 | 'DeleteQuery', 20 | ]; 21 | 22 | /** 23 | * Determine if an key is considered protected in a prototype. 24 | * 25 | * @function 26 | * @private 27 | * @param {String} key 28 | * @return {Boolean} 29 | */ 30 | var isProtected = function(key) { 31 | return key.match(/^__.*__$/); 32 | }; 33 | 34 | /** 35 | * Clone non-protected items from a prototype. 36 | * 37 | * @function 38 | * @private 39 | * @param {Object} prototype 40 | * @return {Object} 41 | */ 42 | var clone = function(prototype) { 43 | prototype = _.clone(prototype); 44 | prototype = _.omit(prototype, _.rearg(isProtected, [1, 0])); 45 | return prototype; 46 | }; 47 | 48 | /** 49 | * Remove non-protected items from a prototype. 50 | * 51 | * @function 52 | * @private 53 | * @param {Object} prototype 54 | */ 55 | var empty = function(obj) { 56 | _.keys(obj).filter(_.negate(isProtected)).forEach(function(key) { 57 | delete obj[key]; 58 | }); 59 | }; 60 | 61 | /** 62 | * Reset prototypes for a subset of classes within magay. 63 | * 64 | * This is required to support mocha re-running the test suite in watch mode. 65 | * Mocha re-loads all of Azul.js, and Azul.js makes changes to certain classes 66 | * in maguey. We need the maguey classes to be pristine when the tests are 67 | * re-run so Azul.js isn't adding changes on top of existing changes. 68 | * 69 | * This is currently the best solution to this issue. Another way would be to 70 | * simply force maguey to be re-required, but then all dependencies of maguey 71 | * would need to be re-required as well. 72 | * 73 | * @function reset 74 | * @private 75 | */ 76 | module.exports = function() { 77 | // define reset before updating the reset storage so the first reset does not 78 | // actually do any resetting. 79 | var reset = maguey._azulReset; 80 | 81 | // store clones of class & metaclass prototypes globally when first loaded so 82 | // they can be used later to perform restores. 83 | maguey._azulReset = maguey._azulReset || _(maguey) 84 | .pick(restore) 85 | .mapValues(function(cls) { 86 | return { 87 | cls: cls, 88 | __class__: clone(cls.__class__.prototype), 89 | __metaclass__: clone(cls.__metaclass__.prototype), 90 | }; 91 | }) 92 | .value(); 93 | 94 | _.forEach(reset, function(stored) { 95 | var cls = stored.cls; 96 | empty(cls.__class__.prototype); 97 | empty(cls.__metaclass__.prototype); 98 | _.extend(cls.__class__.prototype, stored.__class__); 99 | _.extend(cls.__metaclass__.prototype, stored.__metaclass__); 100 | }); 101 | }; 102 | -------------------------------------------------------------------------------- /test/integration/mysql_tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../helpers'); 4 | 5 | // $ mysql -u root 6 | // > CREATE DATABASE azul_test; 7 | // > exit 8 | 9 | if (!/^(1|true)$/i.test(process.env.TEST_MYSQL || '1')) { return; } 10 | 11 | var _ = require('lodash'); 12 | var Database = require('../../lib/database'); 13 | var shared = require('./shared_behaviors'); 14 | 15 | var db, connection = { 16 | adapter: 'mysql', 17 | connection: { 18 | user: process.env.MYSQL_USER || 'root', 19 | password: process.env.MYSQL_PASSWORD || '', 20 | database: process.env.MYSQL_DATABASE || 'azul_test', 21 | }, 22 | }; 23 | 24 | var resetSequence = function(table) { 25 | return db.query.raw('ALTER TABLE ' + table + ' AUTO_INCREMENT = 1;'); 26 | }; 27 | 28 | var castDatabaseValue = function(type, value) { 29 | switch(type) { 30 | case 'bool': value = Boolean(value); break; // bool is stored as number 31 | } 32 | return value; 33 | }; 34 | 35 | describe('MySQL', function() { 36 | before(function() { db = this.db = Database.create(connection); }); 37 | before(function() { this.resetSequence = resetSequence; }); 38 | before(function() { this.castDatabaseValue = castDatabaseValue; }); 39 | after(function() { return db.disconnect(); }); 40 | 41 | // run all shared examples 42 | _.each(shared(), function(fn, name) { 43 | if (fn.length !== 0) { 44 | throw new Error('Cannot execute shared example: ' + name); 45 | } 46 | fn(); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /test/integration/pg_tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../helpers'); 4 | 5 | // $ createuser -s root 6 | // $ psql -U root -d postgres 7 | // > CREATE DATABASE azul_test; 8 | // > \q 9 | 10 | if (!/^(1|true)$/i.test(process.env.TEST_POSTGRES || '1')) { return; } 11 | 12 | var _ = require('lodash'); 13 | var Database = require('../../lib/database'); 14 | var shared = require('./shared_behaviors'); 15 | 16 | var db, connection = { 17 | adapter: 'pg', 18 | connection: { 19 | user: process.env.PG_USER || 'root', 20 | password: process.env.PG_PASSWORD || '', 21 | database: process.env.PG_DATABASE || 'azul_test', 22 | }, 23 | }; 24 | 25 | var resetSequence = function(table) { 26 | return db.query.raw('ALTER SEQUENCE ' + table + '_id_seq restart'); 27 | }; 28 | 29 | var castDatabaseValue = function(type, value) { 30 | switch(type) { 31 | case 'integer64': // these numeric types are read from the db as strings 32 | case 'decimal': value = +value; break; 33 | } 34 | return value; 35 | }; 36 | 37 | describe('PostgreSQL', function() { 38 | before(function() { db = this.db = Database.create(connection); }); 39 | before(function() { this.resetSequence = resetSequence; }); 40 | before(function() { this.castDatabaseValue = castDatabaseValue; }); 41 | after(function() { return db.disconnect(); }); 42 | 43 | // run all shared examples 44 | _.each(shared(), function(fn, name) { 45 | if (fn.length !== 0) { 46 | throw new Error('Cannot execute shared example: ' + name); 47 | } 48 | fn(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/integration/shared_behaviors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../helpers'); 4 | 5 | var _ = require('lodash'); 6 | var path = require('path'); 7 | var Promise = require('bluebird'); 8 | 9 | var shared = {}; 10 | 11 | shared.shouldRunMigrationsAndQueries = function(it) { 12 | var db; before(function() { db = this.db; }); 13 | 14 | describe('with migrations applied', function() { 15 | before(function() { 16 | var migration = 17 | path.join(__dirname, '../fixtures/migrations/blog'); 18 | this.migrator = db.migrator(migration); 19 | return this.migrator.migrate(); 20 | }); 21 | 22 | after(function() { 23 | return this.migrator.rollback(); 24 | }); 25 | 26 | afterEach(function() { 27 | return this.resetSequence('articles'); 28 | }); 29 | 30 | it('can insert, update, and delete data', function() { 31 | return Promise.bind({}) 32 | .then(function() { 33 | return db 34 | .insert('articles', { title: 'Title 1', body: 'Contents 1'}); 35 | }).get('rows').get('0') 36 | .then(function(article) { expect(article).to.not.exist; }) 37 | .then(function() { 38 | return db 39 | .insert('articles', { title: 'Title 2', body: 'Contents 2'}) 40 | .returning('id'); 41 | }).get('rows').get('0') 42 | .then(function(details) { expect(details).to.eql({ id: 2 }); }) 43 | .then(function() { return db.select('articles'); }).get('rows') 44 | .then(function(articles) { 45 | expect(_.sortBy(articles, 'id')).to.eql([ 46 | { id: 1, title: 'Title 1', body: 'Contents 1'}, 47 | { id: 2, title: 'Title 2', body: 'Contents 2'}, 48 | ]); 49 | }) 50 | .then(function() { 51 | return db.update('articles', { title: 'Updated' }).where({ id: 1 }); 52 | }) 53 | .then(function() { return db.select('articles'); }).get('rows') 54 | .then(function(articles) { 55 | expect(_.sortBy(articles, 'id')).to.eql([ 56 | { id: 1, title: 'Updated', body: 'Contents 1'}, 57 | { id: 2, title: 'Title 2', body: 'Contents 2'}, 58 | ]); 59 | }) 60 | .then(function() { return db.delete('articles'); }) 61 | .then(function() { return db.select('articles'); }).get('rows') 62 | .then(function(articles) { 63 | expect(articles).to.eql([]); 64 | }); 65 | }); 66 | 67 | it('can create, update, read, and delete models', function() { 68 | 69 | var Article = db.model('article').reopen({ 70 | title: db.attr(), 71 | body: db.attr(), 72 | comments: db.hasMany(), 73 | }); 74 | 75 | var Comment = db.model('comment').reopen({ 76 | pk: db.attr('identifier'), 77 | identifier: db.attr('identifier'), 78 | email: db.attr(), 79 | body: db.attr(), 80 | article: db.belongsTo(), 81 | }); 82 | 83 | return Promise.bind({}) 84 | .then(function() { 85 | this.article1 = Article.create({ title: 'News', body: 'Azul 1.0' }); 86 | return this.article1.save(); 87 | }) 88 | .then(function() { 89 | this.article2 = Article.create({ title: 'Update', body: 'Azul 2.0' }); 90 | return this.article2.save(); 91 | }) 92 | .then(function() { 93 | this.comment1 = this.article1.createComment({ 94 | email: 'info@azuljs.com', body: 'Sweet initial release.', 95 | }); 96 | return this.comment1.save(); 97 | }) 98 | .then(function() { 99 | this.comment2 = this.article1.createComment({ 100 | email: 'person@azuljs.com', body: 'Great initial release!', 101 | }); 102 | return this.comment2.save(); 103 | }) 104 | .then(function() { 105 | this.comment3 = this.article2.createComment({ 106 | email: 'another@azuljs.com', body: 'Good update.', 107 | }); 108 | return this.comment3.save(); 109 | }) 110 | .then(function() { 111 | this.comment4 = this.article2.createComment({ 112 | email: 'spam@azuljs.com', body: 'Rolex watches.', 113 | }); 114 | return this.comment4.save(); 115 | }) 116 | .then(function() { return Article.objects.fetch(); }) 117 | .then(function(articles) { 118 | expect(_.map(articles, 'attrs')).to.eql([ 119 | { id: 1, title: 'News', body: 'Azul 1.0' }, 120 | { id: 2, title: 'Update', body: 'Azul 2.0' }, 121 | ]); 122 | }) 123 | .then(function() { 124 | return Article.objects.where({ 'comments.body$icontains': 'rolex' }); 125 | }) 126 | .then(function(articles) { 127 | expect(_.map(articles, 'attrs')).to.eql([ 128 | { id: 2, title: 'Update', body: 'Azul 2.0' }, 129 | ]); 130 | }) 131 | .then(function() { 132 | return Article.objects 133 | .where({ 'comments.body$icontains': 'initial' }); 134 | }) 135 | .then(function(articles) { 136 | expect(_.map(articles, 'attrs')).to.eql([ 137 | { id: 1, title: 'News', body: 'Azul 1.0' }, 138 | ]); 139 | }) 140 | .then(function() { 141 | // with join first, automatic joining will not occur, so duplicate 142 | // results will be returned 143 | return Article.objects.join('comments') 144 | .where({ 'comments.body$icontains': 'initial' }); 145 | }) 146 | .then(function(articles) { 147 | expect(_.map(articles, 'attrs')).to.eql([ 148 | { id: 1, title: 'News', body: 'Azul 1.0' }, 149 | { id: 1, title: 'News', body: 'Azul 1.0' }, 150 | ]); 151 | }) 152 | .then(function() { 153 | return Comment.objects.where({ 'article.title': 'News' }); 154 | }) 155 | .then(function(comments) { 156 | expect(_.map(comments, 'attrs')).to.eql([ 157 | { identifier: 1, 'article_id': 1, 158 | email: 'info@azuljs.com', body: 'Sweet initial release.', }, 159 | { identifier: 2, 'article_id': 1, 160 | email: 'person@azuljs.com', body: 'Great initial release!', }, 161 | ]); 162 | }); 163 | 164 | }); 165 | 166 | it('cannot violate foreign key constraint', function() { 167 | return db.insert('comments', { 'article_id': 923 }).execute() 168 | .throw(new Error('')) 169 | .catch(function(e) { 170 | expect(e.message).to.match(/constraint/i); 171 | }); 172 | }); 173 | 174 | }); 175 | }; 176 | 177 | module.exports = function(options) { 178 | var opts = options || {}; 179 | var skip = opts.skip; 180 | var replacementIt = function(description) { 181 | var args = _.toArray(arguments); 182 | if (skip && description && description.match(skip)) { 183 | args.splice(1); 184 | } 185 | it.apply(this, args); 186 | }; 187 | _.extend(replacementIt, it); 188 | 189 | return _.mapValues(shared, function(fn) { 190 | return _.partial(fn, replacementIt); 191 | }); 192 | }; 193 | -------------------------------------------------------------------------------- /test/integration/sqlite3_tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../helpers'); 4 | 5 | if (!/^(1|true)$/i.test(process.env.TEST_SQLITE || '1')) { return; } 6 | 7 | var _ = require('lodash'); 8 | var Database = require('../../lib/database'); 9 | var Promise = require('bluebird'); 10 | var shared = require('./shared_behaviors'); 11 | 12 | var db, connection = { 13 | adapter: 'sqlite3', 14 | connection: { 15 | filename: '', 16 | }, 17 | }; 18 | 19 | var resetSequence = Promise.method(function(/*table*/) { 20 | // no need to reset 21 | }); 22 | 23 | var castDatabaseValue = function(type, value, options) { 24 | switch (type) { 25 | case 'date': // dates are converted & stored as timestamps 26 | case 'dateTime': value = new Date(value); break; 27 | case 'bool': value = Boolean(value); break; // bool is stored as number 28 | case 'decimal': // precision & scale not supported, so fake it here 29 | value = _.size(options) ? +value.toFixed(options.scale) : value; 30 | break; 31 | } 32 | return value; 33 | }; 34 | 35 | describe('SQLite3', function() { 36 | before(function() { db = this.db = Database.create(connection); }); 37 | before(function() { this.resetSequence = resetSequence; }); 38 | before(function() { this.castDatabaseValue = castDatabaseValue; }); 39 | after(function() { return db.disconnect(); }); 40 | 41 | // run all shared examples 42 | var skip = /`i?regex|year|month|day|weekday|hour|minute|second`/i; 43 | _.each(shared({ skip: skip }), function(fn, name) { 44 | if (fn.length !== 0) { 45 | throw new Error('Cannot execute shared example: ' + name); 46 | } 47 | fn(); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --recursive 2 | -------------------------------------------------------------------------------- /test/relations/base_tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../helpers'); 4 | 5 | var BaseRelation = require('../../lib/relations/base'); 6 | var property = require('corazon/property'); 7 | 8 | var User; 9 | 10 | describe('BaseRelation', __db(function() { 11 | /* global db */ 12 | 13 | beforeEach(function() { 14 | User = db.model('user').reopen({ username: db.attr() }); 15 | }); 16 | 17 | it('requires subclass to implement associate', function() { 18 | var user1 = User.create(); 19 | var user2 = User.create(); 20 | var relation = BaseRelation.create('article', { context: User }); 21 | expect(function() { 22 | relation.associate(user1, user2); 23 | }).to.throw(/associate.*must.*implemented.*subclass/i); 24 | }); 25 | 26 | it('requires subclass to implement disassociate', function() { 27 | var user1 = User.create(); 28 | var user2 = User.create(); 29 | var relation = BaseRelation.create('article', { context: User }); 30 | expect(function() { 31 | relation.disassociate(user1, user2); 32 | }).to.throw(/disassociate.*must.*implemented.*subclass/i); 33 | }); 34 | 35 | it('requires subclass to implement prefetch', function() { 36 | var user1 = User.create(); 37 | var user2 = User.create(); 38 | var relation = BaseRelation.create('article', { context: User }); 39 | expect(function() { 40 | relation.prefetch(user1, user2); 41 | }).to.throw(/prefetch.*must.*implemented.*subclass/i); 42 | }); 43 | 44 | it('requires subclass to implement associatePrefetchResults', function() { 45 | var user1 = User.create(); 46 | var user2 = User.create(); 47 | var relation = BaseRelation.create('article', { context: User }); 48 | expect(function() { 49 | relation.associatePrefetchResults(user1, user2); 50 | }).to.throw(/associatePrefetchResults.*must.*implemented.*subclass/i); 51 | }); 52 | 53 | it('dispatches methods & properties to instance relation', function() { 54 | var method = sinon.spy(); 55 | var getter = sinon.spy(); 56 | var setter = sinon.spy(); 57 | 58 | var Relation = BaseRelation.extend({ 59 | method: method, 60 | get: getter, 61 | set: setter, 62 | }); 63 | Relation.reopen({ 64 | overrides: function() { 65 | this.addHelper('Method', 'method'); 66 | this.overrideProperty('Property', 'get', 'set'); 67 | }, 68 | }); 69 | 70 | // this is a strategy that azul-transaction uses, so we need to support it 71 | var Base = db.Model.extend({ custom: Relation.attr()() }); 72 | var relation = Object.create(Base.__class__.prototype.customRelation); 73 | var Override = Base.extend({ 74 | customRelation: property(function() { return relation; }), 75 | }); 76 | 77 | var base = Base.create(); 78 | var override = Override.create(); 79 | 80 | base.customMethod(); 81 | override.customMethod(); // should be called with custom object 82 | base.customProperty; 83 | override.customProperty; // should be called with custom object 84 | base.customProperty = undefined; 85 | override.customProperty = undefined; // should be called with custom object 86 | 87 | expect(method.getCall(0).thisValue) 88 | .to.equal(Base.__class__.prototype.customRelation); 89 | expect(method.getCall(1).thisValue) 90 | .to.equal(relation); 91 | expect(method).to.have.been.calledTwice; 92 | expect(getter.getCall(0).thisValue) 93 | .to.equal(Base.__class__.prototype.customRelation); 94 | expect(getter.getCall(1).thisValue) 95 | .to.equal(relation); 96 | expect(getter).to.have.been.calledTwice; 97 | expect(setter.getCall(0).thisValue) 98 | .to.equal(Base.__class__.prototype.customRelation); 99 | expect(setter.getCall(1).thisValue) 100 | .to.equal(relation); 101 | expect(setter).to.have.been.calledTwice; 102 | }); 103 | 104 | })); 105 | -------------------------------------------------------------------------------- /test/relations/has_one_join_tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../helpers'); 4 | 5 | var User; 6 | 7 | describe('Model.hasOne joins', __db(function() { 8 | /* global db, adapter */ 9 | 10 | beforeEach(require('../common').models); 11 | beforeEach(function() { 12 | User = db.model('user'); 13 | }); 14 | 15 | it('generates simple join queries', function() { 16 | return User.objects.join('blog').fetch().should.eventually.exist.meanwhile(adapter) 17 | .should.have.executed('SELECT "users".* FROM "users" ' + 18 | 'INNER JOIN "blogs" ON "blogs"."owner_id" = "users"."id"'); 19 | }); 20 | 21 | it('generates join queries that use where accessing fields in both types', function() { 22 | return User.objects.join('blog').where({ 23 | username: 'wbyoung', 24 | title$contains: 'Azul', 25 | }) 26 | .fetch().should.eventually.exist.meanwhile(adapter) 27 | .should.have.executed( 28 | 'SELECT "users".* FROM "users" ' + 29 | 'INNER JOIN "blogs" ON "blogs"."owner_id" = "users"."id" ' + 30 | 'WHERE "users"."username" = ? ' + 31 | 'AND "blogs"."title" LIKE ?', ['wbyoung', '%Azul%']); 32 | }); 33 | 34 | it('defaults to the main model on ambiguous property', function() { 35 | return User.objects.join('blog').where({ id: 5 }) 36 | .fetch().should.eventually.exist.meanwhile(adapter) 37 | .should.have.executed( 38 | 'SELECT "users".* FROM "users" ' + 39 | 'INNER JOIN "blogs" ON "blogs"."owner_id" = "users"."id" ' + 40 | 'WHERE "users"."id" = ?', [5]); 41 | }); 42 | 43 | it('gives an error when there is an ambiguous property in two joins', function() { 44 | db.model('profile', { title: db.attr() }); 45 | User.reopen({ profile: db.hasOne() }); 46 | 47 | expect(function() { 48 | User.objects 49 | .join('blog') 50 | .join('profile') 51 | .where({ title: 'Profile/Blog Title' }); 52 | }).to.throw(/ambiguous.*"title".*"(blog|profile)".*"(blog|profile)"/i); 53 | }); 54 | 55 | it('resolves fields specified by relation name', function() { 56 | return User.objects.join('blog').where({ 'blog.id': 5, }) 57 | .fetch().should.eventually.exist.meanwhile(adapter) 58 | .should.have.executed( 59 | 'SELECT "users".* FROM "users" ' + 60 | 'INNER JOIN "blogs" ON "blogs"."owner_id" = "users"."id" ' + 61 | 'WHERE "blogs"."id" = ?', [5]); 62 | }); 63 | 64 | it('resolves fields specified by relation name & attr name', function() { 65 | return User.objects.join('blog').where({ 'blog.pk': 5, }) 66 | .fetch().should.eventually.exist.meanwhile(adapter) 67 | .should.have.executed( 68 | 'SELECT "users".* FROM "users" ' + 69 | 'INNER JOIN "blogs" ON "blogs"."owner_id" = "users"."id" ' + 70 | 'WHERE "blogs"."id" = ?', [5]); 71 | }); 72 | 73 | it('automatically determines joins from conditions', function() { 74 | return User.objects.where({ 'blog.title': 'News', }) 75 | .fetch().should.eventually.exist.meanwhile(adapter) 76 | .should.have.executed( 77 | 'SELECT "users".* FROM "users" ' + 78 | 'INNER JOIN "blogs" ON "blogs"."owner_id" = "users"."id" ' + 79 | 'WHERE "blogs"."title" = ? ' + 80 | 'GROUP BY "users"."id"', ['News']); 81 | }); 82 | 83 | it('automatically determines joins from order by', function() { 84 | return User.objects.orderBy('-blog.pk') 85 | .fetch().should.eventually.exist.meanwhile(adapter) 86 | .should.have.executed( 87 | 'SELECT "users".* FROM "users" ' + 88 | 'INNER JOIN "blogs" ON "blogs"."owner_id" = "users"."id" ' + 89 | 'GROUP BY "users"."id" ' + 90 | 'ORDER BY "blogs"."id" DESC'); 91 | }); 92 | 93 | it('handles attrs during automatic joining', function() { 94 | return User.objects.where({ 'blog.pk': 5, }) 95 | .fetch().should.eventually.exist.meanwhile(adapter) 96 | .should.have.executed( 97 | 'SELECT "users".* FROM "users" ' + 98 | 'INNER JOIN "blogs" ON "blogs"."owner_id" = "users"."id" ' + 99 | 'WHERE "blogs"."id" = ? ' + 100 | 'GROUP BY "users"."id"', [5]); 101 | }); 102 | 103 | it('does not automatically join based on attributes', function() { 104 | return User.objects.where({ 'username': 'wbyoung', }) 105 | .fetch().should.eventually.exist.meanwhile(adapter) 106 | .should.have.executed( 107 | 'SELECT * FROM "users" ' + 108 | 'WHERE "username" = ?', ['wbyoung']); 109 | }); 110 | 111 | it('works with a complex query', function() { 112 | return User.objects.where({ 'blog.title$contains': 'news', }) 113 | .orderBy('username', '-blog.title') 114 | .limit(10) 115 | .offset(20) 116 | .fetch().should.eventually.exist.meanwhile(adapter) 117 | .should.have.executed( 118 | 'SELECT "users".* FROM "users" ' + 119 | 'INNER JOIN "blogs" ON "blogs"."owner_id" = "users"."id" ' + 120 | 'WHERE "blogs"."title" LIKE ? ' + 121 | 'GROUP BY "users"."id" ' + 122 | 'ORDER BY "users"."username" ASC, "blogs"."title" DESC ' + 123 | 'LIMIT 10 OFFSET 20', ['%news%']); 124 | }); 125 | 126 | it('joins & orders across multiple relationships', function() { 127 | return User.objects.where({ 'blog.articles.title$contains': 'rolex', }) 128 | .orderBy('username', 'blog.articles.title') 129 | .limit(10) 130 | .offset(20) 131 | .fetch().should.eventually.exist.meanwhile(adapter) 132 | .should.have.executed( 133 | 'SELECT "users".* FROM "users" ' + 134 | 'INNER JOIN "blogs" ON "blogs"."owner_id" = "users"."id" ' + 135 | 'INNER JOIN "articles" ON "articles"."blog_id" = "blogs"."id" ' + 136 | 'WHERE "articles"."title" LIKE ? ' + 137 | 'GROUP BY "users"."id" ' + 138 | 'ORDER BY "users"."username" ASC, "articles"."title" ASC ' + 139 | 'LIMIT 10 OFFSET 20', ['%rolex%']); 140 | }); 141 | 142 | it('gives a useful error when second bad relation is used for `join`', function() { 143 | expect(function() { 144 | User.objects.join('blog.streets'); 145 | }).to.throw(/no relation.*"streets".*join.*user query.*blog/i); 146 | }); 147 | 148 | })); 149 | -------------------------------------------------------------------------------- /test/relations/has_one_prefetch_tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../helpers'); 4 | 5 | var User; 6 | 7 | describe('Model.hasOne pre-fetch', __db(function() { 8 | /* global db, adapter */ 9 | 10 | beforeEach(require('../common').models); 11 | beforeEach(function() { 12 | User = db.model('user'); 13 | }); 14 | 15 | it('executes multiple queries', function() { 16 | return User.objects.with('blog').fetch().should.eventually.exist.meanwhile(adapter) 17 | .should.have.executed( 18 | 'SELECT * FROM "users"', 19 | 'SELECT * FROM "blogs" WHERE "owner_id" IN (?, ?) LIMIT 2', [231, 844]); 20 | }); 21 | 22 | it('works with all', function() { 23 | return User.objects.with('blog').all().fetch() 24 | .should.eventually.exist 25 | .meanwhile(adapter).should.have.executed( 26 | 'SELECT * FROM "users"', 27 | 'SELECT * FROM "blogs" WHERE "owner_id" IN (?, ?) LIMIT 2', [231, 844]); 28 | }); 29 | 30 | it('caches related objects', function() { 31 | return User.objects.with('blog').fetch().get('0') 32 | .should.eventually.have.properties({ id: 231, username: 'jack' }) 33 | .and.to.have.property('blog').that.is.a.model('blog') 34 | .with.json({ id: 348, title: 'Azul.js', ownerId: 231 }); 35 | }); 36 | 37 | it('works with multiple results', function() { 38 | return User.objects.with('blog').fetch() 39 | .should.eventually.have.lengthOf(2).and 40 | .have.deep.property('[0]').that.is.a.model('user') 41 | .with.json({ id: 231, username: 'jack', email: 'jack@gmail.com' }) 42 | .and.also.have.deep.property('[0].blog').that.is.a.model('blog') 43 | .with.json({ id: 348, ownerId: 231, title: 'Azul.js' }) 44 | .and.also.have.deep.property('[1]').that.is.a.model('user') 45 | .with.json({ id: 844, username: 'susan', email: 'susan@gmail.com' }) 46 | .and.also.have.deep.property('[1].blog').that.is.undefined; 47 | }); 48 | 49 | it('works when no objects are returned', function() { 50 | adapter.respond(/select.*from "users"/i, []); 51 | return User.objects.with('blog').fetch().should.eventually.eql([]); 52 | }); 53 | 54 | it('works via `fetchOne`', function() { 55 | return User.objects.where({ id: 231 }).limit(1).with('blog').fetchOne() 56 | .should.eventually.be.a.model('user') 57 | .with.json({ id: 231, username: 'jack', email: 'jack@gmail.com' }) 58 | .and.have.property('blog').that.is.a.model('blog') 59 | .with.json({ id: 348, ownerId: 231, title: 'Azul.js' }); 60 | }); 61 | 62 | it('works via `find`', function() { 63 | return User.objects.with('blog').find(231) 64 | .should.eventually.be.a.model('user') 65 | .with.json({ id: 231, username: 'jack', email: 'jack@gmail.com' }) 66 | .and.have.property('blog').that.is.a.model('blog') 67 | .with.json({ id: 348, ownerId: 231, title: 'Azul.js' }); 68 | }); 69 | 70 | })); 71 | -------------------------------------------------------------------------------- /test/relations/has_one_tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../helpers'); 4 | 5 | var user; 6 | var User; 7 | var Blog; 8 | 9 | describe('Model.hasOne', __db(function() { 10 | /* global db, adapter */ 11 | 12 | beforeEach(require('../common').models); 13 | beforeEach(function() { 14 | Blog = db.model('blog'); 15 | User = db.model('user'); 16 | user = User.$({ id: 231 }); 17 | }); 18 | 19 | describe('definition', function() { 20 | it('does not need to provide name', function() { 21 | user.blogRelation._relatedModel.should.equal(Blog); 22 | }); 23 | 24 | it('calculates foreign key from inverse', function() { 25 | User.reopen({ profile: db.hasOne({ inverse: 'stats' }) }); 26 | user.profileRelation.foreignKey.should.eql('statsId'); 27 | user.profileRelation.foreignKeyAttr.should.eql('stats_id'); 28 | }); 29 | 30 | it('uses the primary key as the join key', function() { 31 | user.blogRelation.joinKey.should.eql(user.blogRelation.primaryKey); 32 | }); 33 | 34 | it('uses the foreign key as the inverse key', function() { 35 | user.blogRelation.inverseKey.should.eql(user.blogRelation.foreignKey); 36 | }); 37 | 38 | it('can generate an inverse relation', function() { 39 | User.reopen({ profile: db.hasOne({ inverse: 'stats' }) }); 40 | var profileRelation = User.__class__.prototype.profileRelation; 41 | var inverse = profileRelation.inverseRelation(); 42 | inverse.joinKey.should.eql('statsId'); 43 | inverse.joinKeyAttr.should.eql('stats_id'); 44 | inverse.inverseKey.should.eql('pk'); 45 | inverse.inverseKeyAttr.should.eql('id'); 46 | }); 47 | }); 48 | 49 | it('has related methods', function() { 50 | User.__class__.prototype.should.have.ownProperty('blog'); 51 | user.should.respondTo('createBlog'); 52 | user.should.respondTo('fetchBlog'); 53 | user.should.respondTo('setBlog'); 54 | user.should.not.have.property('blogObjects'); 55 | }); 56 | 57 | describe('relation', function() { 58 | 59 | it('fetches related object', function() { 60 | return user.fetchBlog().should.eventually.be.a.model('blog') 61 | .with.json({ id: 348, title: 'Azul.js', ownerId: 231 }) 62 | .meanwhile(adapter).should.have 63 | .executed('SELECT * FROM "blogs" WHERE "owner_id" = ? LIMIT 1', [231]); 64 | }); 65 | 66 | it('fetches related objects when the result set is empty', function() { 67 | adapter.respond(/select.*from "blogs"/i, []); 68 | return user.fetchBlog().should.eventually.eql(undefined); 69 | }); 70 | 71 | it('throws when attempting to access un-loaded collection', function() { 72 | expect(function() { 73 | user.blog; 74 | }).to.throw(/blog.*not yet.*loaded/i); 75 | }); 76 | 77 | it('allows access loaded item', function() { 78 | return user.fetchBlog().should.eventually.exist 79 | .meanwhile(user).should.have.property('blog').that.is.a.model('blog') 80 | .with.json({ id: 348, title: 'Azul.js', ownerId: 231 }); 81 | }); 82 | 83 | it('does not load collection cache during model load', function() { 84 | return User.objects.limit(1).fetchOne() 85 | .get('blog').should.eventually 86 | .be.rejectedWith(/blog.*not yet.*loaded/i); 87 | }); 88 | 89 | it('allows access loaded collection when the result set is empty', function() { 90 | adapter.respond(/select.*from "blogs"/i, []); 91 | return user.fetchBlog().should.eventually.be.fulfilled 92 | .meanwhile(user).should.have.property('blog', undefined); 93 | }); 94 | }); 95 | 96 | describe('helpers', function() { 97 | it('allows create', function() { 98 | var blog = user.createBlog({ title: 'Hello' }); 99 | blog.ownerId.should.eql(user.id); 100 | blog.should.be.an.instanceOf(Blog.__class__); 101 | user.should.have.property('blog').that.equals(blog); 102 | }); 103 | 104 | it('allows created object to be saved', function() { 105 | return user.createBlog({ title: 'Hello' }).save() 106 | .should.eventually.exist 107 | .meanwhile(adapter).should.have.executed( 108 | 'INSERT INTO "blogs" ("title", "owner_id") VALUES (?, ?) ' + 109 | 'RETURNING "id"', ['Hello', 231]); 110 | }); 111 | 112 | it('allows store with existing object', function() { 113 | user.blog = Blog.$({ id: 3, title: 'Blog' }); 114 | return user.save().should.eventually.exist 115 | .meanwhile(adapter).should.have.executed('UPDATE "blogs" ' + 116 | 'SET "owner_id" = ? WHERE "id" = ?', [231, 3]); 117 | }); 118 | 119 | it('allows save to clear relationship', function() { 120 | return user.fetchBlog().then(function() { 121 | user.blog = null; 122 | return user.save(); 123 | }) 124 | .should.eventually.exist 125 | .meanwhile(adapter).should.have.executed(/select/i, [231], 126 | 'UPDATE "blogs" SET "owner_id" = ? ' + 127 | 'WHERE "id" = ?', [undefined, 348]); 128 | }); 129 | 130 | it('allows store with unsaved object', function() { 131 | user.blog = Blog.create({ title: 'Blog' }); 132 | return user.save().should.eventually.exist 133 | .meanwhile(adapter).should.have.executed( 134 | 'INSERT INTO "blogs" ("title", "owner_id") VALUES (?, ?) ' + 135 | 'RETURNING "id"', ['Blog', 231]); 136 | }); 137 | 138 | it('allows store via constructor', function() { 139 | var blog = Blog.create({ title: 'Blog' }); 140 | var user = User.create({ username: 'jack', blog: blog }); 141 | return user.save().should.eventually.exist 142 | .meanwhile(adapter).should.have.executed( 143 | 'INSERT INTO "users" ("username", "email_addr") VALUES (?, ?) ' + 144 | 'RETURNING "id"', ['jack', undefined], 145 | 'INSERT INTO "blogs" ("title", "owner_id") VALUES (?, ?) ' + 146 | 'RETURNING "id"', ['Blog', 398]); 147 | }); 148 | 149 | it('does not try to repeat setting relation', function() { 150 | var blog = Blog.$({ id: 5, title: 'Hello' }); 151 | user.blog = blog; 152 | return user.save().then(function() { 153 | return user.save(); 154 | }) 155 | .should.eventually.exist 156 | .meanwhile(adapter).should.have.executed( 157 | 'UPDATE "blogs" SET "owner_id" = ? WHERE "id" = ?', [231, 5]) 158 | .then(function() { 159 | expect(user).to.have.property('_blogObjectsInFlight') 160 | .that.is.undefined; 161 | blog.should.have.property('dirty', false); 162 | }); 163 | }); 164 | 165 | it('does not try to repeat clearing relation', function() { 166 | var removed; 167 | return user.fetchBlog().then(function() { 168 | removed = user.blog; 169 | user.blog = null; 170 | }) 171 | .then(function() { return user.save(); }) 172 | .should.eventually.exist 173 | .meanwhile(adapter).should.have.executed( 174 | 'SELECT * FROM \"blogs\" WHERE \"owner_id\" = ? LIMIT 1', [231], 175 | 'UPDATE "blogs" SET "owner_id" = ? WHERE "id" = ?', [undefined, 348]) 176 | .then(function() { 177 | expect(user).to.have.property('_articlesObjectsInFlight') 178 | .that.is.undefined; 179 | removed.should.have.property('dirty', false); 180 | }); 181 | }); 182 | }); 183 | })); 184 | -------------------------------------------------------------------------------- /test/relations/has_one_through_tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../helpers'); 4 | 5 | var Supplier; 6 | var supplier; 7 | 8 | describe('Model.hasOne :through', __db(function() { 9 | /* global db, adapter */ 10 | 11 | beforeEach(require('../common').models); 12 | beforeEach(function() { 13 | Supplier = db.model('supplier'); 14 | supplier = Supplier.$({ id: 489 }); 15 | }); 16 | 17 | describe('relation', function() { 18 | 19 | it('fetches related objects', function() { 20 | return supplier.fetchAccountHistory() 21 | .should.eventually.be.a.model('accountHistory') 22 | .with.json({ id: 832, details: 'many details', accountId: 392 }) 23 | .meanwhile(adapter).should.have.executed( 24 | 'SELECT "account_histories".* FROM "account_histories" ' + 25 | 'INNER JOIN "accounts" ' + 26 | 'ON "account_histories"."account_id" = "accounts"."id" ' + 27 | 'WHERE "accounts"."supplier_id" = ? LIMIT 1', [489]); 28 | }); 29 | 30 | }); 31 | 32 | describe('joins', function() { 33 | it('automatically determines joins from conditions', function() { 34 | return Supplier.objects.where({ 'accountHistory.details': 'details' }).fetch() 35 | .should.eventually.exist.meanwhile(adapter) 36 | .should.have.executed( 37 | 'SELECT "suppliers".* FROM "suppliers" ' + 38 | 'INNER JOIN "accounts" ' + 39 | 'ON "accounts"."supplier_id" = "suppliers"."id" ' + 40 | 'INNER JOIN "account_histories" ' + 41 | 'ON "account_histories"."account_id" = "accounts"."id" ' + 42 | 'WHERE "account_histories"."details" = ? ' + 43 | 'GROUP BY "suppliers"."id"', ['details']); 44 | }); 45 | }); 46 | 47 | describe('pre-fetch', function() { 48 | it('executes multiple queries', function() { 49 | return Supplier.objects.with('accountHistory').fetch() 50 | .should.eventually.exist.meanwhile(adapter) 51 | .should.have.executed( 52 | 'SELECT * FROM "suppliers"', 53 | 'SELECT * FROM "accounts" WHERE "supplier_id" ' + 54 | 'IN (?, ?) LIMIT 2', [229, 430], 55 | 'SELECT * FROM "account_histories" WHERE "account_id" ' + 56 | 'IN (?, ?) LIMIT 2', [392, 831]); 57 | }); 58 | }); 59 | 60 | })); 61 | -------------------------------------------------------------------------------- /test/relations/save_cycle_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../helpers'); 4 | 5 | describe('Model.save with many new models', __db(function() { 6 | /* global db, adapter */ 7 | 8 | var wbyoung, jack, azulBlog, jacksBlog, 9 | article1, article2, 10 | commentA, commentB, commentC, commentD; 11 | 12 | beforeEach(require('../common').models); 13 | beforeEach(function() { 14 | var User = db.model('user'); 15 | var Blog = db.model('blog'); 16 | var Article = db.model('article'); 17 | var Comment = db.model('comment'); 18 | 19 | wbyoung = User.create({ username: 'wbyoung' }); 20 | jack = User.create({ username: 'jack' }); 21 | azulBlog = Blog.create({ title: 'Azul.js', owner: wbyoung }); 22 | jacksBlog = Blog.create({ title: 'Jack\'s Adventures', owner: jack }); 23 | 24 | article1 = Article.create({ title: '1.0 Released' }); 25 | article1.author = wbyoung; 26 | commentA = Comment.create({ body: 'Correction', commenter: wbyoung }); 27 | commentB = Comment.create({ body: 'Awesome' }); 28 | article1.addComment(commentA); 29 | article1.addComment(commentB); 30 | azulBlog.addArticle(article1); 31 | 32 | article2 = azulBlog.createArticle({ title: '1.1 Released' }); 33 | article2.author = jack; 34 | commentC = Comment.create({ body: 'Question' }); 35 | commentD = Comment.create({ body: 'Great' }); 36 | article2.addComment(commentC); 37 | article2.addComment(commentD); 38 | }); 39 | 40 | it('saves everything when "top" relation is saved', function() { 41 | return azulBlog.save().should.eventually.exist 42 | .meanwhile(adapter).should.have.executed( 43 | 'INSERT INTO "users" ("username", "email_addr") VALUES (?, ?) ' + 44 | 'RETURNING "id"', ['wbyoung', undefined], 45 | 46 | 'INSERT INTO "users" ("username", "email_addr") VALUES (?, ?) ' + 47 | 'RETURNING "id"', ['jack', undefined], 48 | 49 | 'INSERT INTO "blogs" ("title", "owner_id") VALUES (?, ?) ' + 50 | 'RETURNING "id"', ['Azul.js', 398], 51 | 52 | 'INSERT INTO "articles" ("title", "blog_id", "author_identifier") ' + 53 | 'VALUES (?, ?, ?) RETURNING "id"', ['1.0 Released', 823, 398], 54 | 55 | 'INSERT INTO "articles" ("title", "blog_id", "author_identifier") ' + 56 | 'VALUES (?, ?, ?) RETURNING "id"', ['1.1 Released', 823, 399], 57 | 58 | 'INSERT INTO "comments" ("body", "article_id", "commenter_id") ' + 59 | 'VALUES (?, ?, ?) RETURNING "id"', ['Correction', 199, 398], 60 | 61 | 'INSERT INTO "comments" ("body", "article_id", "commenter_id") ' + 62 | 'VALUES (?, ?, ?) RETURNING "id"', ['Awesome', 199, undefined], 63 | 64 | 'INSERT INTO "comments" ("body", "article_id", "commenter_id") ' + 65 | 'VALUES (?, ?, ?) RETURNING "id"', ['Question', 200, undefined], 66 | 67 | 'INSERT INTO "comments" ("body", "article_id", "commenter_id") ' + 68 | 'VALUES (?, ?, ?) RETURNING "id"', ['Great', 200, undefined], 69 | 70 | 'INSERT INTO "blogs" ("title", "owner_id") VALUES (?, ?) ' + 71 | 'RETURNING "id"', ['Jack\'s Adventures', 399]); 72 | }); 73 | 74 | it('saves everything when "bottom" relation is saved'); 75 | 76 | it('saves everything when "mid" relation is saved'); 77 | 78 | })); 79 | 80 | describe('Model.save with codependent relations', __db(function() { 81 | /* global db, adapter */ 82 | 83 | var jack, jill; 84 | 85 | beforeEach(require('../common').models); 86 | beforeEach(function() { 87 | var Person = db.model('person'); 88 | 89 | jack = Person.create({ name: 'Jack' }); 90 | jill = Person.create({ name: 'Jill' }); 91 | jack.bestFriend = jill; 92 | jill.bestFriend = jack; 93 | }); 94 | 95 | it('saves everything', function() { 96 | return jill.save().should.eventually.exist 97 | .meanwhile(adapter).should.have.executed( 98 | 'INSERT INTO "people" ("name", "best_friend_id") ' + 99 | 'VALUES (?, ?) RETURNING "id"', ['Jack', undefined], 100 | 101 | 'INSERT INTO "people" ("name", "best_friend_id") ' + 102 | 'VALUES (?, ?) RETURNING "id"', ['Jill', 102], 103 | 104 | 'UPDATE "people" SET "best_friend_id" = ? ' + 105 | 'WHERE "id" = ?', [103, 102]) 106 | .meanwhile(jack).should.be.a.model('person') 107 | .with.json({ id: 102, name: 'Jack', bestFriendId: 103 }) 108 | .and.to.have.property('dirty', false) 109 | .meanwhile(jill).should.be.a.model('person') 110 | .with.json({ id: 103, name: 'Jill', bestFriendId: 102 }) 111 | .and.to.have.property('dirty', false); 112 | }); 113 | 114 | })); 115 | 116 | describe('Model.save with circular many-to-many', __db(function() { 117 | /* global db, adapter */ 118 | 119 | var jack, jill; 120 | 121 | beforeEach(require('../common').models); 122 | beforeEach(function() { 123 | var Person = db.model('individual'); 124 | 125 | jill = Person.create({ name: 'Jill' }); 126 | jack = Person.create({ name: 'Jack' }); 127 | jill = Person.create({ name: 'Jill' }); 128 | jack.addFollower(jill); 129 | jill.addFollower(jack); 130 | jack.createFollower({ name: 'Dob' }); 131 | }); 132 | 133 | it('saves everything', function() { 134 | return jill.save().should.eventually.exist 135 | .meanwhile(adapter).should.have.executed( 136 | 'INSERT INTO "individuals" ("name") ' + 137 | 'VALUES (?) RETURNING "id"', ['Jill'], 138 | 139 | 'INSERT INTO "individuals" ("name") ' + 140 | 'VALUES (?) RETURNING "id"', ['Jack'], 141 | 142 | 'INSERT INTO "individuals" ("name") ' + 143 | 'VALUES (?) RETURNING "id"', ['Dob'], 144 | 145 | 'INSERT INTO "relationships" ("followed_id", "follower_id") ' + 146 | 'VALUES (?, ?)', [102, 103], 147 | 148 | 'INSERT INTO "relationships" ("followed_id", "follower_id") ' + 149 | 'VALUES (?, ?), (?, ?)', [103, 102, 103, 104]) 150 | .meanwhile(jill).should.be.a.model('individual') 151 | .with.json({ id: 102, name: 'Jill' }) 152 | .meanwhile(jack).should.be.a.model('individual') 153 | .with.json({ id: 103, name: 'Jack' }); 154 | }); 155 | 156 | })); 157 | -------------------------------------------------------------------------------- /test/util/inflection_tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../helpers'); 4 | 5 | var inflection = require('../../lib/util/inflection'); 6 | 7 | describe('Inflection', function() { 8 | it('pluralizes', function() { 9 | expect(inflection.pluralize('dog')).to.eql('dogs'); 10 | }); 11 | 12 | it('singularizes', function() { 13 | expect(inflection.singularize('dogs')).to.eql('dog'); 14 | }); 15 | 16 | describe('uncountable', function() { 17 | it('pluralizes', function() { 18 | expect(inflection.pluralize('fish')).to.eql('fish'); 19 | }); 20 | 21 | it('singularizes', function() { 22 | expect(inflection.singularize('fish')).to.eql('fish'); 23 | }); 24 | }); 25 | 26 | describe('irregulars', function() { 27 | it('pluralizes', function() { 28 | expect(inflection.pluralize('person')).to.eql('people'); 29 | }); 30 | 31 | it('pluralizes when already plural', function() { 32 | expect(inflection.pluralize('people')).to.eql('people'); 33 | }); 34 | 35 | it('singularizes', function() { 36 | expect(inflection.singularize('people')).to.eql('person'); 37 | }); 38 | 39 | it('singularizes when already singular', function() { 40 | var i = new inflection.Inflection(); 41 | i.singular(/s$/, ''); 42 | i.irregular('octopus', 'octopi'); 43 | expect(i.singularize('octopus')).to.eql('octopus'); 44 | }); 45 | }); 46 | 47 | it('does not break with no rules & does nothing', function() { 48 | var i = new inflection.Inflection(); 49 | expect(i.singularize('word')).to.eql('word'); 50 | }); 51 | }); 52 | --------------------------------------------------------------------------------