├── .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 |
22 |
61 |
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 |
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 |
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 |
--------------------------------------------------------------------------------