├── _config.yml
├── index.js
├── source
├── global.js
├── store.js
├── state.js
├── util.js
├── modelConstructor.js
├── md.js
├── collection.js
└── baseModel.js
├── .npmignore
├── test
├── www
│ ├── bundles
│ │ └── chai
│ │ │ ├── index.js
│ │ │ └── chai-plugins.bundle.js
│ ├── test-md.store.js
│ ├── test-ModelLodash.js
│ ├── index.html
│ ├── test-State.js
│ ├── test-redraw.js
│ ├── test-CollectionLodash.js
│ ├── configure.js
│ ├── test-md.js
│ ├── test-ModelConstructor.js
│ ├── test-md.config.js
│ └── test-Model.js
└── profile
│ ├── index.html
│ └── app.js
├── .travis.yml
├── .gitignore
├── webpack-config.js
├── bower.json
├── .eslintrc.json
├── LICENSE
├── gulpfile.js
├── package.json
├── examples
├── model-and-collection.html
└── model-redraw.html
├── README.md
└── mithril-data.min.js
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-minimal
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./source/md.js');
--------------------------------------------------------------------------------
/source/global.js:
--------------------------------------------------------------------------------
1 |
2 | exports.config = {};
3 |
4 | exports.modelConstructors = {};
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # === IGNORE ALL ===
2 | /*
3 |
4 | # === EXCEPT ===
5 | !/*.js
6 | !/source
--------------------------------------------------------------------------------
/test/www/bundles/chai/index.js:
--------------------------------------------------------------------------------
1 | window.chaiPromise = require('chai-as-promised');
2 | window.chaiSubset = require('chai-subset');
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - stable
4 | before_script:
5 | - npm install -g mocha-phantomjs
6 | branches:
7 | only:
8 | - master
9 | sudo: false
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # === IGNORE ALL ===
3 | /*
4 |
5 | # === EXCEPT ===
6 | !/.gitignore
7 | !/.npmignore
8 | !/.travis.yml
9 | !/*.json
10 | !/*.txt
11 | !/*.md
12 | !/*.js
13 | !/LICENSE
14 |
15 | !/source
16 | !/test
17 | !/examples
18 |
19 | **/.DS_Store
--------------------------------------------------------------------------------
/webpack-config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack'),
2 | pkg = require('./package.json');
3 |
4 | var bannerText = `${pkg.name} v${pkg.version}
5 | ${pkg.description}
6 | ${pkg.homepage||''}
7 | (c) ${new Date().getFullYear()} ${pkg.authorName}
8 | License: ${pkg.license}`;
9 |
10 | module.exports = {
11 | entry: './source/md.js',
12 | output: {
13 | filename: 'mithril-data.js'
14 | },
15 | externals: {
16 | 'mithril': 'm',
17 | 'lodash': '_'
18 | },
19 | plugins: [
20 | new webpack.BannerPlugin(bannerText)
21 | ]
22 | };
--------------------------------------------------------------------------------
/test/profile/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | mithril-data
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mithril-data",
3 | "description": "A rich model library for Mithril javascript framework.",
4 | "main": "index.js",
5 | "authors": [
6 | "Kevin (https://github.com/rhaldkhein)"
7 | ],
8 | "license": "MIT",
9 | "keywords": [
10 | "model",
11 | "mithril",
12 | "database",
13 | "data"
14 | ],
15 | "moduleType": [
16 | "amd",
17 | "globals",
18 | "node"
19 | ],
20 | "homepage": "",
21 | "ignore": [
22 | "*",
23 | "!LICENSE",
24 | "!README.md",
25 | "!mithril-data.js",
26 | "!mithril-data.min.js",
27 | "!bower.json",
28 | "!package.json"
29 | ]
30 | }
31 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "commonjs": true,
5 | "es6": true
6 | },
7 | "extends": "eslint:recommended",
8 | "rules": {
9 | "indent": 0,
10 | "linebreak-style": [
11 | "error",
12 | "unix"
13 | ],
14 | "quotes": [
15 | "error",
16 | "single"
17 | ],
18 | "semi": [
19 | "error",
20 | "always"
21 | ]
22 | },
23 | "globals": {
24 | "m": true,
25 | "md": true,
26 | "Model": true,
27 | "it": true,
28 | "expect": true,
29 | "describe": true
30 | }
31 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Ronald Kevin Villanueva
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | 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, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/test/www/test-md.store.js:
--------------------------------------------------------------------------------
1 | describe('md.store', function() {
2 | 'use strict';
3 |
4 | describe('#request()', function() {
5 | 'use strict';
6 |
7 | it('exist and is a function', function() {
8 | expect(md.store.request).to.be.a('function');
9 | });
10 |
11 | it('returns a promise', function() {
12 | var req = md.store.request('/exist');
13 | expect(req).to.be.instanceof(Promise);
14 | expect(req.then).to.be.a('function');
15 | expect(req.catch).to.be.a('function');
16 | });
17 |
18 | it('reject on http error', function() {
19 | expect(md.store.request('/notexist')).to.be.rejected;
20 | });
21 |
22 | it('resolve on http success', function() {
23 | var req = md.store.request('/exist');
24 | expect(req).to.be.fulfilled;
25 | expect(req).to.be.become('ok');
26 | });
27 |
28 | });
29 |
30 | describe('#get(), #post(), #put(), #destroy()', function() {
31 | 'use strict';
32 |
33 | it('exist and is a function', function() {
34 | expect(md.store.get).to.be.a('function');
35 | expect(md.store.post).to.be.a('function');
36 | expect(md.store.put).to.be.a('function');
37 | expect(md.store.destroy).to.be.a('function');
38 | });
39 |
40 | });
41 |
42 | });
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp'),
2 | uglify = require('gulp-uglify'),
3 | rename = require('gulp-rename'),
4 | header = require('gulp-header'),
5 | replace = require('gulp-replace'),
6 | webpack = require('webpack-stream'),
7 | sequence = require('run-sequence');
8 |
9 | var package = require('./package.json');
10 |
11 | gulp.task('version', function() {
12 | return gulp.src('source/md.js')
13 | .pipe(replace(/v.+\/\/version/g, 'v' + package.version + '\';//version'))
14 | .pipe(gulp.dest('source'));
15 | });
16 |
17 | gulp.task('bundle', function() {
18 | return gulp.src('')
19 | .pipe(webpack(require('./webpack-config.js')))
20 | .pipe(gulp.dest(''));
21 | });
22 |
23 | gulp.task('minify', function() {
24 | return gulp.src('mithril-data.js')
25 | .pipe(uglify())
26 | .pipe(rename({
27 | suffix: '.min'
28 | }))
29 | .pipe(header(['/**',
30 | ' * <%= pkg.name %> v<%= pkg.version %>',
31 | ' * <%= pkg.description %>',
32 | ' * <%= pkg.homepage %>',
33 | ' * (c) ' + new Date().getFullYear() + ' <%= pkg.authorName %>',
34 | ' * License: <%= pkg.license %>',
35 | ' */',
36 | ''
37 | ].join('\n'), {
38 | pkg: package
39 | }))
40 | .pipe(gulp.dest(''));
41 | });
42 |
43 | gulp.task('release', function(callback) {
44 | sequence('version', 'bundle', 'minify', callback);
45 | });
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mithril-data",
3 | "version": "0.4.9",
4 | "description": "A rich data model library for Mithril javascript framework.",
5 | "homepage": "https://github.com/rhaldkhein/mithril-data",
6 | "main": "index.js",
7 | "scripts": {
8 | "test": "mocha-phantomjs test/www/index.html"
9 | },
10 | "keywords": [
11 | "model",
12 | "mithril",
13 | "database",
14 | "data"
15 | ],
16 | "author": "Kevin (https://github.com/rhaldkhein)",
17 | "authorName": "Kevin Villanueva",
18 | "license": "MIT",
19 | "repository": {
20 | "type": "git",
21 | "url": "https://github.com/rhaldkhein/mithril-data.git"
22 | },
23 | "devDependencies": {
24 | "body-parser": "^1.15.1",
25 | "chai": "^3.5.0",
26 | "chai-as-promised": "^6.0.0",
27 | "chai-subset": "^1.4.0",
28 | "express": "^4.13.4",
29 | "gulp": "^3.9.1",
30 | "gulp-header": "^1.8.2",
31 | "gulp-rename": "^1.2.2",
32 | "gulp-replace": "^0.5.4",
33 | "gulp-uglify": "^3.0.0",
34 | "mocha": "^2.4.5",
35 | "run-sequence": "^1.2.2",
36 | "shortid": "^2.2.6",
37 | "webpack-stream": "^3.2.0"
38 | },
39 | "dependencies": {
40 | "lodash": "^4.12.0",
41 | "mithril": "^1.0.1"
42 | },
43 | "files": [
44 | "source/*",
45 | "index.js",
46 | "mithril-data.js",
47 | "mithril-data.min.js"
48 | ]
49 | }
50 |
--------------------------------------------------------------------------------
/test/www/test-ModelLodash.js:
--------------------------------------------------------------------------------
1 | describe('Model.', function() {
2 | 'use strict';
3 |
4 | it('has()', function() {
5 | var user = new Model.User();
6 | expect(user.has).to.exist;
7 | expect(user.has('name')).to.equal(true);
8 | })
9 |
10 | it('keys()', function() {
11 | var user = new Model.User();
12 | expect(user.keys).to.exist;
13 | var props = Model.User.modelOptions.props;
14 | var keys = user.keys();
15 | for (var i = 0; i < props.length; i++) {
16 | expect(keys).to.contain(props[i]);
17 | }
18 | })
19 |
20 | it('values()', function() {
21 | var user = new Model.User();
22 | expect(user.values).to.exist;
23 | user.name('Test');
24 | var props = Model.User.modelOptions.props;
25 | var values = user.values();
26 | for (var i = 0; i < props.length; i++) {
27 | expect(values).to.contain(user[props[i]]());
28 | }
29 | })
30 |
31 | it('pick()', function() {
32 | var user = new Model.User();
33 | expect(user.pick).to.exist;
34 | user.name('Name');
35 | user.profile('Profile');
36 | var picked = user.pick(['profile']);
37 | expect(picked.profile).to.exist;
38 | expect(picked.profile).to.equal(user.profile());
39 | })
40 |
41 | it('omit()', function() {
42 | var user = new Model.User();
43 | expect(user.omit).to.exist;
44 | user.name('Name');
45 | user.profile('Profile');
46 | var omited = user.omit(['profile']);
47 | expect(omited).to.not.equal(user.getJson());
48 | expect(omited.name).to.exist.and.to.equal('Name');
49 | expect(omited.profile).to.not.exist;
50 | })
51 |
52 | });
--------------------------------------------------------------------------------
/test/profile/app.js:
--------------------------------------------------------------------------------
1 | window.onload = function() {
2 |
3 | // Model
4 | var Model = window.Model = {};
5 |
6 | Model.User = md.model({
7 | name: 'User',
8 | props: ['name', 'profile', 'data']
9 | });
10 |
11 | Model.Note = md.model({
12 | name: 'Note',
13 | props: ['foo', 'bar'],
14 | defaults: {
15 | title: 'Default Title',
16 | body: 'Default Note Body',
17 | author: new Model.User({
18 | name: 'User Default'
19 | })
20 | },
21 | refs: {
22 | author: 'User'
23 | }
24 | });
25 |
26 | // console.dir(Model.User);
27 | // console.dir(Model.Note);
28 |
29 | var Demo = {
30 | // Controller
31 | controller: function() {
32 | var self = this;
33 | this.collection = Model.User.createCollection();
34 | this.add = function() {
35 | console.log('Add');
36 | self.collection.add(new Model.User({
37 | name: 'Foo_' + self.collection.size()
38 | }));
39 | };
40 | this.remove = function() {
41 | console.log('Remove');
42 | self.collection.remove(self.collection.last());
43 | };
44 | // this.add();
45 | },
46 | // View
47 | view: function(ctrl) {
48 | return m('div', [
49 | m('button', {
50 | onclick: ctrl.add
51 | }, 'Add'),
52 | m('button', {
53 | onclick: ctrl.remove
54 | }, 'Remove'),
55 | m('div', ctrl.collection.map(function(model, i) {
56 | return m('ul', [
57 | m('li', 'Model ' + i),
58 | m('li', 'name: ' + model.name()),
59 | m('hr')
60 | ]);
61 | }))
62 | ]);
63 | }
64 | };
65 |
66 | //initialize
67 | m.mount(document.body, Demo);
68 |
69 | };
--------------------------------------------------------------------------------
/source/store.js:
--------------------------------------------------------------------------------
1 | var _ = require('lodash');
2 | var config = require('./global').config;
3 | var BaseModel = require('./baseModel');
4 |
5 | function __config(xhr, xhrOptions) {
6 | if (config.storeConfigXHR)
7 | config.storeConfigXHR(xhr, xhrOptions);
8 | xhr.setRequestHeader('Content-Type', 'application/json');
9 | }
10 |
11 | function __extract(xhr, xhrOptions) {
12 | if (config.storeExtract) {
13 | return config.storeExtract(xhr, xhrOptions);
14 | } else if (xhr.responseText.length) {
15 | return __deserializer(xhr.responseText);
16 | } else {
17 | return null;
18 | }
19 | }
20 |
21 | function __serializer(data) {
22 | // data = data instanceof BaseModel ? data.getCopy() : data;
23 | __dereference(data);
24 | if (config.storeSerializer)
25 | return config.storeSerializer(data);
26 | else
27 | return JSON.stringify(data);
28 | }
29 |
30 | function __deserializer(data) {
31 | if (config.storeDeserializer) {
32 | return config.storeDeserializer(data);
33 | } else {
34 | try {
35 | return JSON.parse(data);
36 | } catch (e) {
37 | return data;
38 | }
39 | }
40 | }
41 |
42 | function __dereference(data) {
43 | var value;
44 | for (var key in data) {
45 | value = data[key];
46 | if (_.isObject(value)) {
47 | data[key] = value[config.keyId] || value;
48 | }
49 | }
50 | }
51 |
52 | module.exports = _.create(null, {
53 | request: function(url, method, data, options) {
54 | var _options = {
55 | method: method || 'GET',
56 | url: url,
57 | data: (data instanceof BaseModel ? data.getCopy() : data) || {},
58 | background: !!config.storeBackground,
59 | serialize: __serializer,
60 | deserialize: __deserializer,
61 | config: __config,
62 | extract: __extract
63 | };
64 | if (options) _options = _.defaultsDeep(options, _options);
65 | if (config.storeConfigOptions) config.storeConfigOptions(_options);
66 | return config.store(_options);
67 | },
68 | get: function(url, data, opt) {
69 | return this.request(url, 'GET', data, opt);
70 | },
71 | post: function(url, data, opt) {
72 | return this.request(url, 'POST', data, opt);
73 | },
74 | put: function(url, data, opt) {
75 | return this.request(url, 'PUT', data, opt);
76 | },
77 | destroy: function(url, data, opt) {
78 | return this.request(url, 'DELETE', data, opt);
79 | }
80 | });
81 |
--------------------------------------------------------------------------------
/test/www/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | mithril-data - Test Suites
6 |
7 |
8 |
13 |
14 |
15 |
16 |
17 |
18 | Redraw
19 |
20 |
21 |
22 |
23 |
24 |
25 | Test Results
26 |
27 |
28 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/source/state.js:
--------------------------------------------------------------------------------
1 | /**
2 | * State
3 | */
4 | var _ = require('lodash');
5 | var config = require('./global').config;
6 | var defaultKey = '__key__';
7 | var privateKeys = ['factory', 'toJson', '_options'];
8 |
9 | function _toJson() {
10 | var json = {};
11 | for (var prop in this) {
12 | if (_.indexOf(privateKeys, prop) === -1) {
13 | json[prop] = this[prop]();
14 | }
15 | }
16 | return json;
17 | }
18 |
19 | function createState(signature, state, options, factoryKey) {
20 | var propVal;
21 | var store = options && options.store ? options.store : config.stream;
22 | for (var propKey in signature) {
23 | if (_.indexOf(privateKeys, propKey) > -1)
24 | throw new Error('State key `' + propKey + '` is not allowed.');
25 | propVal = signature[propKey];
26 | state[propKey] = _.isFunction(propVal) ? propVal : store(propVal, propKey, factoryKey, options);
27 | }
28 | return state;
29 | }
30 |
31 | // Class
32 | function State(signature, options) {
33 | if (_.isArray(signature)) {
34 | this.signature = _.invert(signature);
35 | for (var prop in this.signature) {
36 | this.signature[prop] = undefined;
37 | }
38 | } else {
39 | this.signature = signature;
40 | }
41 | this._options = options;
42 | this.map = {};
43 | }
44 |
45 | // Exports
46 | module.exports = State;
47 |
48 | // Single state
49 | State.create = function(signature, options) {
50 | return createState(signature, {
51 | toJson: _toJson
52 | }, options);
53 | };
54 |
55 | // Assign state
56 | State.assign = function(object, signature, options) {
57 | createState(signature, object, options);
58 | };
59 |
60 | // Prototype
61 | State.prototype = {
62 | set: function(key) {
63 | if (!key)
64 | key = defaultKey;
65 | if (!this.map[key]) {
66 | this.map[key] = createState(this.signature, {
67 | factory: config.stream(this),
68 | toJson: _toJson
69 | }, this._options, key);
70 | }
71 | return this.map[key];
72 | },
73 | get: function(key) {
74 | if (!key)
75 | key = defaultKey;
76 | if (!this.map[key]) {
77 | this.set(key);
78 | }
79 | return this.map[key];
80 | },
81 | remove: function(key) {
82 | if (!key)
83 | key = defaultKey;
84 | if (this.map[key]) {
85 | var b, keys = _.keys(this.map[key]);
86 | for (b = 0; b < keys.length; b++) {
87 | this.map[key][keys[b]] = null;
88 | }
89 | delete this.map[key];
90 | }
91 | },
92 | dispose: function() {
93 | var keysThis = _.keys(this);
94 | var keysMap = _.keys(this.map);
95 | var keySignature = _.keys(this.signature);
96 | var a, b;
97 | for (a = 0; a < keysMap.length; a++) {
98 | for (b = 0; b < keySignature.length; b++) {
99 | this.map[keysMap[a]][keySignature[b]] = null;
100 | }
101 | this.map[keysMap[a]] = null;
102 | }
103 | for (a = 0; a < keysThis.length; a++) {
104 | this[keysThis[a]] = null;
105 | }
106 | }
107 | };
108 |
--------------------------------------------------------------------------------
/examples/model-and-collection.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | model-and-collection
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/examples/model-redraw.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | model-and-collection
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
113 |
114 |
115 |
--------------------------------------------------------------------------------
/test/www/test-State.js:
--------------------------------------------------------------------------------
1 | describe('State', function() {
2 | 'use strict';
3 |
4 | var customStoreData = {};
5 | var customStore = function(initVal, key, factorykey, options) {
6 | var prefix = options.prefix || '';
7 | factorykey = factorykey ? (factorykey + '.') : '';
8 | key = prefix + factorykey + key;
9 | var prop = function(valNew) {
10 | if (arguments.length) {
11 | customStoreData[key] = valNew;
12 | } else {
13 | return customStoreData[key];
14 | }
15 | };
16 | if (!(key in customStoreData))
17 | prop(initVal);
18 | return prop;
19 | };
20 |
21 | it('Single instance', function() {
22 | var state = md.State.create({
23 | isEditing: false,
24 | test: 'Foo'
25 | });
26 | expect(state.isEditing()).to.be.false;
27 | expect(state.test()).to.be.equal('Foo');
28 | state.isEditing(true);
29 | state.test('Bar');
30 | expect(state.isEditing()).to.be.true;
31 | expect(state.test()).to.be.equal('Bar');
32 | });
33 |
34 | it('Assign to existing object', function() {
35 | var obj = {
36 | name: 'Foo'
37 | };
38 | md.State.assign(obj, {
39 | isEditing: false,
40 | test: 'Bar'
41 | });
42 | expect(obj.name).to.be.equal('Foo');
43 | expect(obj.isEditing()).to.be.false;
44 | expect(obj.test()).to.be.equal('Bar');
45 | obj.isEditing(true);
46 | obj.test('Baz');
47 | expect(obj.isEditing()).to.be.true;
48 | expect(obj.test()).to.be.equal('Baz');
49 | });
50 |
51 | it('Accepts fuctions initialized with md.stream()', function() {
52 | var _isDone = md.stream(false);
53 | var _test = md.stream('Foo');
54 | var state = md.State.create({
55 | isDone: _isDone,
56 | test: _test
57 | });
58 | expect(state.isDone()).to.be.false;
59 | expect(state.test()).to.be.equal('Foo');
60 | state.isDone(true);
61 | state.test('Bar');
62 | expect(state.isDone()).to.be.true;
63 | expect(state.test()).to.be.equal('Bar');
64 | expect(state.isDone.constructor).to.be.equal(md.stream);
65 |
66 | });
67 |
68 | it('`toJson()` method', function() {
69 | var _isDone = md.stream(false);
70 | var _test = md.stream('Foo');
71 | var state = md.State.create({
72 | isDone: _isDone,
73 | test: _test
74 | });
75 | var json = state.toJson();
76 | expect(json.isDone).to.be.false;
77 | expect(json.test).to.be.equal('Foo');
78 | });
79 |
80 | it('Custom store / prop (non factory)', function() {
81 | var state = md.State.create({
82 | name: 'Foo',
83 | age: 25,
84 | active: false
85 | }, {
86 | store: customStore
87 | });
88 | expect(state.name()).to.equal(customStoreData.name).and.to.equal('Foo');
89 | expect(state.age()).to.equal(customStoreData.age).and.to.equal(25);
90 | expect(state.active()).to.equal(customStoreData.active).and.to.equal(false);
91 | });
92 |
93 | it('Custom store / prop with prefix (non factory)', function() {
94 | var state = md.State.create({
95 | name: 'Foo',
96 | age: 25
97 | }, {
98 | prefix: 'pref.',
99 | store: customStore
100 | });
101 | expect(state.name()).to.equal(customStoreData['pref.name']).and.to.equal('Foo');
102 | expect(state.age()).to.equal(customStoreData['pref.age']).and.to.equal(25);
103 | });
104 |
105 | it('Custom store / prop with prefix (factory)', function() {
106 | var stateFactory = new md.State({
107 | name: 'Foo',
108 | age: 25
109 | }, {
110 | prefix: 'pref.',
111 | store: customStore
112 | });
113 | var stateA = stateFactory.set('a');
114 | expect(stateA.name()).to.equal(customStoreData['pref.a.name']).and.to.equal('Foo');
115 | expect(stateA.age()).to.equal(customStoreData['pref.a.age']).and.to.equal(25);
116 | });
117 |
118 | });
--------------------------------------------------------------------------------
/source/util.js:
--------------------------------------------------------------------------------
1 | var _ = require('lodash');
2 | var slice = Array.prototype.slice;
3 | var BaseModel = require('./baseModel');
4 | var hasWindow = typeof window !== 'undefined';
5 |
6 | function resolveWrapper(func, property) {
7 | return function(argA, argB, argC, argD) {
8 | return func(argA ? (argA[property] || argA) : argA, argB ? (argB[property] || argB) : argB, argC, argD);
9 | };
10 | }
11 |
12 | function resolveArguments(args, property) {
13 | var i = args.length - 1;
14 | var arg;
15 | for (; i >= 0; i--) {
16 | arg = args[i];
17 | if (_.isFunction(arg))
18 | args[i] = resolveWrapper(arg, property);
19 | else if (arg instanceof BaseModel)
20 | args[i] = arg.__json;
21 | }
22 | return args;
23 | }
24 |
25 | function resolveResult(result, collection, property) {
26 | if (result === collection) {
27 | return result;
28 | } else {
29 | if (_.isArray(result)) {
30 | var i = result.length - 1;
31 | var value;
32 | for (; i >= 0; i--) {
33 | value = result[i];
34 | if (value && value[property])
35 | result[i] = value[property];
36 | }
37 | return result;
38 | } else {
39 | return result ? (result[property] || result) : result;
40 | }
41 | }
42 | }
43 |
44 | function getNextTickMethod() {
45 | if (hasWindow && window.setImmediate) {
46 | return window.setImmediate;
47 | } else if (typeof process === 'object' && typeof process.nextTick === 'function') {
48 | return process.nextTick;
49 | }
50 | return function(fn) {
51 | setTimeout(fn, 0);
52 | };
53 | }
54 |
55 | module.exports = _.create(null, {
56 | isBrowser: hasWindow,
57 | nextTick: getNextTickMethod(),
58 | clearObject: function(obj) {
59 | for (var member in obj)
60 | delete obj[member];
61 | },
62 | hasValueOfType: function(obj, type) {
63 | var keys = _.keys(obj);
64 | for (var i = 0; i < keys.length; i++) {
65 | if (obj[keys[i]] instanceof type) {
66 | return true;
67 | }
68 | }
69 | return false;
70 | },
71 | isConflictExtend: function(objSource, objInject) {
72 | var keys = _.keys(objInject);
73 | var i = 0;
74 | for (; i < keys.length; i++) {
75 | if (_.hasIn(objSource, keys[i])) {
76 | return keys[i];
77 | }
78 | }
79 | return false;
80 | },
81 | strictExtend: function(objSource, objInject) {
82 | var isConflict = this.isConflictExtend(objSource, objInject);
83 | if (isConflict)
84 | throw new Error('`' + isConflict + '` method / property is not allowed.');
85 | else
86 | _.extend(objSource, objInject);
87 | },
88 | addMethods: function(dist, src, methods, distProp, retProp) {
89 | // Need to use _.each loop. To retain value of methods' arguments.
90 | _.each(methods, function(length, method) {
91 | if (src[method]) {
92 | switch (length) {
93 | case 0:
94 | dist[method] = function() {
95 | return resolveResult(src[method](this[distProp]), this[distProp], retProp);
96 | };
97 | break;
98 | case 1:
99 | dist[method] = function(valueA) {
100 | if (_.isFunction(valueA))
101 | valueA = resolveWrapper(valueA, retProp);
102 | else if (valueA instanceof BaseModel)
103 | valueA = valueA.__json;
104 | return resolveResult(src[method](this[distProp], valueA), this[distProp], retProp);
105 | };
106 | break;
107 | case 2:
108 | dist[method] = function(valueA, valueB) {
109 | if (_.isFunction(valueA))
110 | valueA = resolveWrapper(valueA, retProp);
111 | else if (valueA instanceof BaseModel)
112 | valueA = valueA.__json;
113 | if (_.isFunction(valueB))
114 | valueB = resolveWrapper(valueB, retProp);
115 | else if (valueB instanceof BaseModel)
116 | valueB = valueB.__json;
117 | return resolveResult(src[method](this[distProp], valueA, valueB), this[distProp], retProp);
118 | };
119 | break;
120 | default:
121 | dist[method] = function() {
122 | var args = resolveArguments(slice.call(arguments), retProp);
123 | args.unshift(this[distProp]);
124 | return resolveResult(src[method].apply(src, args), this[distProp], retProp);
125 | };
126 | }
127 | }
128 | });
129 | }
130 | });
131 |
--------------------------------------------------------------------------------
/source/modelConstructor.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Model Constructor
3 | */
4 |
5 | var _ = require('lodash');
6 | // var m = require('mithril');
7 | var store = require('./store');
8 | var config = require('./global').config;
9 | var Collection = require('./collection');
10 |
11 | function ModelConstructor() {}
12 |
13 | // Export class.
14 | module.exports = ModelConstructor;
15 |
16 | // Prototype methods.
17 | ModelConstructor.prototype = {
18 | __init: function(options) {
19 | if (this.__options)
20 | return;
21 | // Set defaults
22 | this.__options = {
23 | redraw: false,
24 | cache: config.cache === true
25 | };
26 | // Inject schema level options to '__options'
27 | if (options)
28 | this.opt(options);
29 | // Check cache enabled
30 | if (this.__options.cache) {
31 | this.__cacheCollection = new Collection({
32 | model: this,
33 | _cache: true
34 | });
35 | if (!this.__options.cacheLimit)
36 | this.__options.cacheLimit = config.cacheLimit;
37 | }
38 | },
39 | __flagSaved: function(models) {
40 | for (var i = 0; i < models.length; i++)
41 | models[i].__saved = true;
42 | },
43 | opt: function(key, value) {
44 | if (!this.__options)
45 | this.__init();
46 | if (_.isPlainObject(key))
47 | _.assign(this.__options, key);
48 | else
49 | this.__options[key] = _.isUndefined(value) ? true : value;
50 | },
51 | // Creates a model. Comply with parsing and caching.
52 | create: function(values, options) {
53 | if (values == null) values = {};
54 | if (!_.isPlainObject(values))
55 | throw new Error('Plain object required');
56 | var cachedModel;
57 | if (this.modelOptions.parser) {
58 | values = this.modelOptions.parser(values);
59 | }
60 | if (this.__options.cache && values[config.keyId]) {
61 | cachedModel = this.__cacheCollection.get(values);
62 | if (!cachedModel) {
63 | cachedModel = new this(values, options);
64 | this.__cacheCollection.add(cachedModel);
65 | if (this.__cacheCollection.size() > this.__options.cacheLimit) {
66 | this.__cacheCollection.shift();
67 | }
68 | }
69 | } else {
70 | cachedModel = new this(values, options);
71 | }
72 | return cachedModel;
73 | },
74 | createCollection: function(options) {
75 | return new Collection(_.assign({
76 | model: this
77 | }, options));
78 | },
79 | createModels: function(data, options) {
80 | if (!_.isArray(data))
81 | data = [data];
82 | var models = [];
83 | for (var i = 0; i < data.length; i++) {
84 | models[i] = this.create(data[i], options);
85 | }
86 | return models;
87 | },
88 | pull: function(url, data, options, callback) {
89 | if (_.isFunction(data)) {
90 | callback = data;
91 | data = undefined;
92 | } else if (_.isFunction(options)) {
93 | callback = options;
94 | options = data;
95 | data = undefined;
96 | }
97 | var self = this;
98 | return store.get(url, data, options)
99 | .then(function(data) {
100 | // `data` can be either array of model or object with
101 | // additional information (like total result and pagination)
102 | // and a property with value of array of models
103 | var models = self.createModels(options && options.path ? _.get(data, options.path) : data, options);
104 | self.__flagSaved(models);
105 | // Resolve the raw data from server as it might contain additional information
106 | if (_.isFunction(callback)) callback(null, data, models);
107 | return models;
108 | }, function(err) {
109 | if (_.isFunction(callback)) callback(err);
110 | throw err;
111 | });
112 | }
113 | };
--------------------------------------------------------------------------------
/test/www/test-redraw.js:
--------------------------------------------------------------------------------
1 | (function() {
2 |
3 | var redrawCount = 0;
4 |
5 | var PostA = md.model({
6 | name: 'PostA',
7 | props: ['name'],
8 | redraw: true
9 | });
10 |
11 | var PostB = md.model({
12 | name: 'PostB',
13 | props: ['name']
14 | });
15 |
16 | var redrawComponent = {
17 | count: 0,
18 | modelA1: new PostA(),
19 | modelA2: new PostA(),
20 | modelB1: new PostB(),
21 | modelB2: new PostB(),
22 | collection: PostB.createCollection(),
23 | view: function() {
24 | // console.info('Redraw ' + (++redrawCount));
25 | return m('div', [
26 | m('h2', 'count = ' + (++this.count)),
27 | m('h2', [
28 | 'modelA1.name (schema:PostA, instance:1) = ',
29 | m('span', {
30 | id: 'modela1-name'
31 | }, '' + this.modelA1.name())
32 | ]),
33 | m('h2', [
34 | 'modelA2.name (schema:PostA, instance:2) = ',
35 | m('span', {
36 | id: 'modela2-name'
37 | }, '' + this.modelA2.name())
38 | ]),
39 | m('h2', [
40 | 'modelB1.name (schema:PostB, instance:1) = ',
41 | m('span', {
42 | id: 'modelb1-name'
43 | }, '' + this.modelB1.name())
44 | ]),
45 | m('h2', [
46 | 'modelB2.name (schema:PostB, instance:2) = ',
47 | m('span', {
48 | id: 'modelb2-name'
49 | }, '' + this.modelB2.name())
50 | ]),
51 | m('div', this.collection.map(function(model, i) {
52 | i++;
53 | return m('h2', [
54 | 'collection.modelB' + i + '.name (schema:PostB, instance:' + i + ') = ',
55 | m('span', {
56 | id: 'col-modelb' + i + '-name'
57 | }, '' + model.name())
58 | ])
59 | }))
60 | ]);
61 | }
62 | };
63 |
64 | var elRedraw = document.getElementById('redraw');
65 | m.mount(elRedraw, redrawComponent);
66 |
67 | describe('Auto Redraw', function() {
68 | 'use strict';
69 |
70 |
71 | describe('Model', function() {
72 | it('should redraw ALL instances - if redraw=true is set in `schema` option', function(done) {
73 | // Open `test-redraw.js` to see schema configuration.
74 |
75 | // First instance.
76 | var elemA1 = document.getElementById('modela1-name');
77 | var modelA1 = redrawComponent.modelA1;
78 | expect(elemA1.innerHTML).to.equal('undefined');
79 |
80 | // Second instance.
81 | var elemA2 = document.getElementById('modela2-name');
82 | var modelA2 = redrawComponent.modelA2;
83 | expect(elemA2.innerHTML).to.equal('undefined');
84 |
85 | modelA1.name('Foo');
86 | modelA2.name('Bar');
87 |
88 | // Need to delay and wait for DOM update.
89 | setTimeout(function() {
90 | expect(elemA1.innerHTML).to.equal('Foo');
91 | expect(elemA2.innerHTML).to.equal('Bar');
92 | done();
93 | }, 200);
94 |
95 |
96 | });
97 |
98 | it('should redraw ONLY centain instances - if redraw=true is set in `instance` option', function(done) {
99 | // Open `test-redraw.js` to see schema configuration.
100 |
101 | // First instance.
102 | var modelB1 = redrawComponent.modelB1;
103 | var elemB1 = document.getElementById('modelb1-name');
104 | expect(elemB1.innerHTML).to.equal('undefined');
105 |
106 | modelB1.name('Foo');
107 |
108 | // Need to delay and wait for DOM update.
109 | setTimeout(function() {
110 | expect(elemB1.innerHTML).to.equal('undefined');
111 |
112 | // Second instance.
113 | var modelB2 = redrawComponent.modelB2;
114 | var elemB2 = document.getElementById('modelb2-name');
115 | expect(elemB2.innerHTML).to.equal('undefined');
116 |
117 | // Set option here.
118 | modelB2.opt('redraw', true);
119 | modelB2.name('Bar');
120 |
121 | setTimeout(function() {
122 | expect(elemB2.innerHTML).to.equal('Bar');
123 | done();
124 | }, 200);
125 | }, 200);
126 | });
127 | });
128 |
129 | describe('Collection', function() {
130 | it('should redraw - even if contained model has false redraw', function(done) {
131 | var col = redrawComponent.collection;
132 | col.opt('redraw', true);
133 |
134 | var postB1 = new PostB();
135 | postB1.opt('redraw', false);
136 | col.add(postB1);
137 |
138 | var postB2 = new PostB();
139 | postB2.opt('redraw', false);
140 | col.add(postB2);
141 |
142 | postB1.name('Foo');
143 | postB2.name('Bar');
144 |
145 | setTimeout(function() {
146 | var elemB1 = document.getElementById('col-modelb1-name');
147 | expect(elemB1.innerHTML).to.equal('Foo');
148 | var elemB2 = document.getElementById('col-modelb2-name');
149 | expect(elemB2.innerHTML).to.equal('Bar');
150 | // Change model value
151 | postB1.name('Test');
152 | setTimeout(function() {
153 | var elemB1x = document.getElementById('col-modelb1-name');
154 | expect(elemB1x.innerHTML).to.equal('Test');
155 | done();
156 | }, 200);
157 | }, 200);
158 |
159 | });
160 | });
161 |
162 | });
163 |
164 | })();
--------------------------------------------------------------------------------
/test/www/test-CollectionLodash.js:
--------------------------------------------------------------------------------
1 | describe('Collection.', function() {
2 | 'use strict';
3 |
4 | it('size', function() {
5 | var col = new md.Collection();
6 | expect(col.size()).to.equal(0);
7 | var user = new Model.User();
8 | col.add(user);
9 | col.add(user);
10 | expect(col.size()).to.equal(1);
11 | col.remove(user);
12 | expect(col.size()).to.equal(0);
13 | });
14 |
15 | it('forEach', function() {
16 | var col = new md.Collection();
17 | var user = new Model.User();
18 | col.add(user);
19 | col.add(new Model.User());
20 | col.add(new Model.User());
21 | var loopCount = 0;
22 | var result = col.forEach(function(model) {
23 | expect(model).to.be.instanceof(Model.User);
24 | loopCount++;
25 | });
26 | expect(loopCount).to.be.equal(3);
27 | expect(result).to.be.a('array');
28 | expect(result.length).to.be.equal(3);
29 | expect(result[0]).to.be.equal(user.getJson());
30 | });
31 |
32 | it('map', function() {
33 | var col = new md.Collection();
34 | var userA = new Model.User();
35 | var userB = new Model.User();
36 | var userC = new Model.User();
37 | col.add(userA);
38 | col.add(userB);
39 | col.add(userC);
40 | var loopCount = 0;
41 | var result = col.map(function(model) {
42 | expect(model).to.be.instanceof(Model.User);
43 | loopCount++;
44 | return model.lid();
45 | });
46 | expect(loopCount).to.be.equal(3);
47 | expect(result).to.be.a('array');
48 | expect(result.length).to.be.equal(3);
49 | expect(result[0]).to.be.equal(userA.lid());
50 | expect(result[1]).to.be.equal(userB.lid());
51 | expect(result[2]).to.be.equal(userC.lid());
52 | });
53 |
54 | it('find', function() {
55 | var col = new md.Collection();
56 | var userA = new Model.User();
57 | userA.name('Foo');
58 | userA.active(false);
59 | col.add(userA);
60 | var userB = new Model.User();
61 | userB.name('Bar');
62 | userB.active(false);
63 | col.add(userB);
64 | var userC = new Model.User();
65 | userC.name('Baz');
66 | userC.active(true);
67 | col.add(userC);
68 | var result = col.find(['name', 'Foo']);
69 | expect(result).to.be.equal(userA);
70 | result = col.find('active');
71 | expect(result).to.be.equal(userC);
72 | });
73 |
74 | it('filter', function() {
75 | var col = new md.Collection();
76 | var userA = new Model.User();
77 | userA.name('Foo');
78 | userA.active(true);
79 | col.add(userA);
80 | var userB = new Model.User();
81 | userB.name('Bar');
82 | userB.active(false);
83 | col.add(userB);
84 | var userC = new Model.User();
85 | userC.name('Baz');
86 | userC.active(true);
87 | col.add(userC);
88 | var result = col.filter('active');
89 | expect(result).to.be.a('array');
90 | expect(result.length).to.equal(2);
91 | expect(result[0]).to.be.equal(userA);
92 | expect(result[1]).to.be.equal(userC);
93 | });
94 |
95 | it('reduce', function() {
96 | var col = new md.Collection();
97 | var userA = new Model.User();
98 | userA.name('Foo');
99 | userA.active(true);
100 | col.add(userA);
101 | var userB = new Model.User();
102 | userB.name('Bar');
103 | userB.active(false);
104 | col.add(userB);
105 | var userC = new Model.User();
106 | userC.name('Baz');
107 | userC.active(true);
108 | col.add(userC);
109 | var result = col.reduce(function(arr, item) {
110 | if (item.active()) {
111 | arr.push(item);
112 | }
113 | return arr;
114 | }, []);
115 | expect(result).to.be.a('array');
116 | expect(result.length).to.equal(2);
117 | expect(result[0]).to.be.equal(userA);
118 | expect(result[1]).to.be.equal(userC);
119 | });
120 |
121 | it('partition', function() {
122 | var col = new md.Collection();
123 | var userA = new Model.User();
124 | userA.name('Foo');
125 | userA.active(true);
126 | col.add(userA);
127 | var userB = new Model.User();
128 | userB.name('Bar');
129 | userB.active(false);
130 | col.add(userB);
131 | var userC = new Model.User();
132 | userC.name('Baz');
133 | userC.active(true);
134 | col.add(userC);
135 | var result = col.partition('active');
136 | expect(result).to.be.a('array');
137 | expect(result.length).to.equal(2);
138 | expect(result[0][0].__model).to.be.equal(userA);
139 | expect(result[0][1].__model).to.be.equal(userC);
140 | expect(result[1][0].__model).to.be.equal(userB);
141 | });
142 |
143 | it('slice', function() {
144 | var col = new md.Collection();
145 | var userA = new Model.User();
146 | userA.name('Foo');
147 | userA.active(true);
148 | col.add(userA);
149 | var userB = new Model.User();
150 | userB.name('Bar');
151 | userB.active(false);
152 | col.add(userB);
153 | var userC = new Model.User();
154 | userC.name('Baz');
155 | userC.active(true);
156 | col.add(userC);
157 | var slicedA = col.slice(0, 2);
158 | expect(slicedA).to.be.a('array');
159 | expect(slicedA.length).to.equal(2);
160 | expect(slicedA[0]).to.be.equal(userA);
161 | expect(slicedA[1]).to.be.equal(userB);
162 | var slicedB = col.slice(1, 3);
163 | expect(slicedB).to.be.a('array');
164 | expect(slicedB.length).to.equal(2);
165 | expect(slicedB[0]).to.be.equal(userB);
166 | expect(slicedB[1]).to.be.equal(userC);
167 | });
168 |
169 | });
--------------------------------------------------------------------------------
/test/www/configure.js:
--------------------------------------------------------------------------------
1 | (function() {
2 |
3 | window.Model = {};
4 |
5 | var STORE;
6 |
7 | function reset() {
8 | window.STORE = STORE = {};
9 | STORE.user = {};
10 | STORE.note = {};
11 | STORE.folder = {};
12 | STORE.cacheuser = {};
13 | }
14 |
15 | reset();
16 |
17 |
18 | md.defaultConfig({
19 | store: function(rawData) {
20 | return new Promise(function(resolve, reject) {
21 | setTimeout(function() {
22 | // console.log(rawData.method + ':' + rawData.url, rawData.data);
23 | var model = rawData.url.replace('/', '');
24 | // console.log(model);
25 | switch (rawData.method + ':' + rawData.url) {
26 | case 'GET:/exist':
27 | resolve('ok');
28 | break;
29 | case 'GET:/clear':
30 | reset();
31 | resolve('ok');
32 | break;
33 | case 'POST:/user':
34 | case 'POST:/cacheuser':
35 | case 'POST:/folder':
36 | var data = _.assign({
37 | id: _.uniqueId('serverid')
38 | }, JSON.parse(rawData.serialize(rawData.data)));
39 | STORE[model][data.id] = data;
40 | resolve(rawData.deserialize(JSON.stringify(STORE[model][data.id])));
41 | break;
42 | case 'PUT:/user':
43 | case 'PUT:/cacheuser':
44 | case 'PUT:/folder':
45 | var data = JSON.parse(rawData.serialize(rawData.data));
46 | if (_.has(STORE[model], data.id)) {
47 | STORE[model][data.id] = data;
48 | resolve(rawData.deserialize(JSON.stringify(STORE[model][data.id])));
49 | } else if (data.id) {
50 | var data = _.assign({}, JSON.parse(rawData.serialize(rawData.data)));
51 | STORE[model][data.id] = data;
52 | resolve(rawData.deserialize(JSON.stringify(STORE[model][data.id])));
53 | } else {
54 | reject(new Error('Model does not exist!'));
55 | }
56 | break;
57 | case 'GET:/user':
58 | case 'GET:/cacheuser':
59 | case 'GET:/folder':
60 | var data = rawData.data;
61 | if (_.isPlainObject(data) && data.id) {
62 | // Single user
63 | if (_.has(STORE[model], data.id)) {
64 | resolve(rawData.deserialize(STORE[model][data.id]));
65 | } else {
66 | reject(new Error('Model ' + data.id + ' does not exist!'));
67 | }
68 | } else {
69 | if (!_.isEmpty(data)) {
70 | // Selected users
71 | resolve(rawData.deserialize(
72 | JSON.stringify(
73 | _.transform(data, function(result, id) {
74 | if (STORE[model][id])
75 | result.push(STORE[model][id]);
76 | }, []))));
77 | } else {
78 | // Return all users
79 | resolve(
80 | rawData.deserialize(
81 | JSON.stringify(
82 | _.values(STORE[model]))));
83 | }
84 | }
85 | break;
86 | case 'DELETE:/user':
87 | case 'DELETE:/cacheuser':
88 | case 'DELETE:/folder':
89 | var data = JSON.parse(rawData.serialize(rawData.data));
90 | if (data && data.id) {
91 | delete STORE[model][data.id];
92 | resolve(
93 | rawData.deserialize(
94 | JSON.stringify({
95 | err: false
96 | })));
97 | } else {
98 | reject(new Error('ID is required!'));
99 | }
100 | break;
101 | case 'GET:/users/wrap':
102 | // Return all users
103 | resolve({
104 | outer: {
105 | inner: {
106 | items: rawData.deserialize(JSON.stringify(_.values(STORE.user)))
107 | }
108 | }
109 | });
110 | break;
111 | case 'GET:/undefined':
112 | resolve(undefined);
113 | break;
114 | default:
115 | reject(new Error('Unknown request'));
116 | }
117 | }, 100);
118 | });
119 | }
120 | });
121 |
122 | })();
--------------------------------------------------------------------------------
/source/md.js:
--------------------------------------------------------------------------------
1 | var _ = require('lodash');
2 | var m = require('mithril');
3 | var config = require('./global').config;
4 | var modelConstructors = require('./global').modelConstructors;
5 | var BaseModel = require('./baseModel');
6 | var ModelConstructor = require('./modelConstructor');
7 | var util = require('./util');
8 | var Collection = require('./collection');
9 |
10 | Object.setPrototypeOf = Object.setPrototypeOf || function(obj, proto) {
11 | obj.__proto__ = proto;
12 | return obj;
13 | };
14 |
15 | /**
16 | * `this.__options` is instance options, registered in `new Model(, <__options>)`.
17 | * `this.options` is schema options, registered in `m.model(schema)`.
18 | */
19 |
20 | function createModelConstructor(schema) {
21 | // Resolve model options. Mutates the object.
22 | resolveSchemaOptions(schema);
23 | // The model constructor.
24 | function Model(vals, opts) {
25 | // Calling parent class.
26 | BaseModel.call(this, opts);
27 | // Local variables.
28 | var data = (this.__options.parse ? this.options.parser(vals) : vals) || {};
29 | var props = schema.props;
30 | // var initial;
31 | // Make user id is in prop;
32 | if (_.indexOf(props, config.keyId) === -1) {
33 | props.push(config.keyId);
34 | }
35 | // Adding props.
36 | for (var i = 0, value; i < props.length; i++) {
37 | value = props[i];
38 | // 1. Must not starts with '__'.
39 | // 2. Omit id in data if you configure different id field.
40 | if (!this.__isProp(value) || (value === 'id' && value !== config.keyId))
41 | return;
42 | // Make sure that it does not create conflict with
43 | // internal reserved keywords.
44 | if (!_.hasIn(this, value) || value === 'id') {
45 | // Use default if data is not available. Only `undefined` should change to default.
46 | // In order to accept other falsy value. Like, `false` and `0`.
47 | this[value] = this.__gettersetter(_.isUndefined(data[value]) ? schema.defaults[value] : data[value], value);
48 | } else {
49 | throw new Error('`' + value + '` prop is not allowed.');
50 | }
51 | }
52 | }
53 | // Make sure that it custom methods and statics does not create conflict with internal ones.
54 | var confMethods = util.isConflictExtend(BaseModel.prototype, schema.methods);
55 | if (confMethods) throw new Error('`' + confMethods + '` method is not allowed.');
56 | // Attach the options to model constructor.
57 | Model.modelOptions = schema;
58 | // Extend from base model prototype.
59 | Model.prototype = _.create(BaseModel.prototype, _.assign(schema.methods || {}, {
60 | constructor: Model,
61 | options: schema,
62 | }));
63 | // Link model controller prototype.
64 | Object.setPrototypeOf(Model, ModelConstructor.prototype);
65 | // Attach statics for model.
66 | if (!_.isEmpty(schema.statics)) {
67 | var confStatics = util.isConflictExtend(Model, schema.statics);
68 | if (confStatics) throw new Error('`' + confStatics + '` method is not allowed for statics.');
69 | _.assign(Model, schema.statics);
70 | }
71 | // Return the model.
72 | return Model;
73 | }
74 |
75 | // Default parser, if user did not specify
76 | function defaultParser(data) { return data; }
77 |
78 | // Make sure options got correct properties
79 | function resolveSchemaOptions(options) {
80 | options.defaults = options.defaults || {};
81 | options.props = _.union(options.props || [], _.keys(options.defaults));
82 | options.refs = options.refs || {};
83 | options.parser = options.parser || defaultParser;
84 | }
85 |
86 | /**
87 | * Exports
88 | */
89 |
90 | // Return the current version.
91 | exports.version = function() {
92 | return 'v0.4.9';//version
93 | };
94 |
95 | // Export class BaseModel
96 | exports.BaseModel = BaseModel;
97 |
98 | // Export class Collection.
99 | exports.Collection = require('./collection');
100 |
101 | // Export class State.
102 | exports.State = require('./state');
103 |
104 | // Export our own store controller.
105 | exports.store = require('./store');
106 |
107 | // Export Mithril's Stream
108 | exports.stream = null;
109 |
110 | // Helper to convert any value to stream
111 | exports.toStream = function(value) {
112 | return (value && value.constructor === exports.stream) ? value : exports.stream(value);
113 | };
114 |
115 | // Export model instantiator.
116 | exports.model = function(schemaOptions, ctrlOptions) {
117 | schemaOptions = schemaOptions || {};
118 | ctrlOptions = ctrlOptions || {};
119 | if (!schemaOptions.name)
120 | throw new Error('Model name must be set.');
121 | var modelConstructor = modelConstructors[schemaOptions.name] = createModelConstructor(schemaOptions);
122 | modelConstructor.__init(ctrlOptions);
123 | return modelConstructor;
124 | };
125 |
126 | // A way to get a constructor from this scope
127 | exports.model.get = function(name) {
128 | return modelConstructors[name];
129 | };
130 |
131 | // Export configurator
132 | var defaultConfig = {};
133 | exports.config = function(userConfig) {
134 | // Configure prototypes.
135 | if (userConfig.modelMethods)
136 | util.strictExtend(BaseModel.prototype, userConfig.modelMethods);
137 | if (userConfig.constructorMethods)
138 | util.strictExtend(ModelConstructor.prototype, userConfig.constructorMethods);
139 | if (userConfig.collectionMethods)
140 | util.strictExtend(Collection.prototype, userConfig.collectionMethods);
141 | if (userConfig.stream)
142 | exports.stream = userConfig.stream;
143 | // Clear
144 | userConfig.modelMethods = null;
145 | userConfig.constructorMethods = null;
146 | userConfig.collectionMethods = null;
147 | // Assign configuration.
148 | _.assign(config, userConfig);
149 | };
150 |
151 | // Option to reset to first initial config.
152 | exports.resetConfig = function() {
153 | util.clearObject(config);
154 | exports.config(defaultConfig);
155 | };
156 |
157 | // Add config to default config. Does not overwrite the old config.
158 | exports.defaultConfig = function(defaults, silent) {
159 | _.assign(defaultConfig, defaults);
160 | if (!silent)
161 | exports.resetConfig();
162 | };
163 |
164 | // Set config defaults.
165 | exports.defaultConfig({
166 | baseUrl: '',
167 | keyId: 'id',
168 | store: m.request,
169 | stream: require('mithril/stream'),
170 | redraw: false,
171 | storeBackground: false,
172 | cache: false,
173 | cacheLimit: 100,
174 | placeholder: null
175 | });
176 |
177 | // Export for AMD & browser's global.
178 | if (typeof define === 'function' && define.amd) {
179 | define(function() {
180 | return exports;
181 | });
182 | }
183 |
184 | // Export for browser's global.
185 | if (typeof window !== 'undefined') {
186 | var oldObject;
187 | // Return back old md.
188 | exports.noConflict = function() {
189 | if (oldObject) {
190 | window.md = oldObject;
191 | oldObject = null;
192 | }
193 | return window.md;
194 | };
195 | // Export private objects for unit testing.
196 | if (window.__TEST__ && window.mocha && window.chai) {
197 | exports.__TEST__ = {
198 | config: config,
199 | BaseModel: BaseModel,
200 | ModelConstructor: ModelConstructor
201 | };
202 | }
203 | if (window.md)
204 | oldObject = window.md;
205 | window.md = exports;
206 | }
--------------------------------------------------------------------------------
/test/www/test-md.js:
--------------------------------------------------------------------------------
1 | // Root before - Run before all tests.
2 | before(function(done) {
3 |
4 | it('create sample schemas for test', function(done) {
5 |
6 | window.Model.User = md.model({
7 | name: 'User',
8 | props: ['name', 'profile', 'age', 'active'],
9 | statics: {
10 | hello: function() {
11 | return 'Hello World';
12 | }
13 | }
14 | });
15 |
16 | window.Model.Folder = md.model({
17 | name: 'Folder',
18 | props: ['name', 'size', 'thumbnail'],
19 | defaults: {
20 | name: 'Untitled Folder',
21 | thumbnail: 'thumbnail.jpg'
22 | },
23 | placehold: ['name']
24 | });
25 |
26 | window.Model.Note = md.model({
27 | name: 'Note',
28 | defaults: {
29 | title: 'Default Title',
30 | body: 'Default Note Body',
31 | author: new Model.User({
32 | name: 'User Default'
33 | }),
34 | folder: new Model.Folder({
35 | name: 'Folder Default'
36 | })
37 | },
38 | refs: {
39 | author: 'User',
40 | folder: 'Folder'
41 | },
42 | parser: function(data) {
43 | if (data && data.wrap) {
44 | return {
45 | title: data.wrap.title,
46 | body: data.wrap.inner.body,
47 | author: data.wrap.inner.author
48 | };
49 | }
50 | return data;
51 | }
52 | });
53 |
54 | // This model simulate undefined result
55 | window.Model.Alarm = md.model({
56 | name: 'Alarm',
57 | url: '/undefined',
58 | props: ['title', 'time'],
59 | defaults: {
60 | title: 'Default Alarm Title',
61 | time: '8:00 AM'
62 | }
63 | });
64 |
65 | expect(window.Model.User).to.exist;
66 | expect(window.Model.Folder).to.exist;
67 | expect(window.Model.Note).to.exist;
68 |
69 | Promise.all([
70 | (new Model.Folder({
71 | id: 'fold001',
72 | name: 'System'
73 | })).save(),
74 | (new Model.Folder({
75 | id: 'fold002',
76 | name: 'Admin'
77 | })).save(),
78 | (new Model.User({
79 | id: 'user001',
80 | name: 'UserFoo',
81 | age: 1
82 | })).save(),
83 | (new Model.User({
84 | id: 'user002',
85 | name: 'UserBar',
86 | age: 2
87 | })).save(),
88 | ]).then(function() {
89 | done();
90 | });
91 |
92 | });
93 |
94 | // Clear all test data in server.
95 | md.store.get('/clear').then(function() {
96 | done();
97 | }, function(err) {
98 | done(err);
99 | });
100 |
101 | });
102 |
103 | describe('md', function() {
104 | 'use strict';
105 |
106 | it('exists', function() {
107 | expect(md).to.exists;
108 | });
109 |
110 | it('is an object', function() {
111 | expect(md).to.be.a('object');
112 | });
113 |
114 | describe('#version()', function() {
115 | 'use strict';
116 |
117 | it('exists', function() {
118 | expect(md.version).to.be.a('function');
119 | });
120 |
121 | it('is a string', function() {
122 | expect(md.version()).to.be.a('string');
123 | });
124 |
125 | });
126 |
127 | describe('#stream()', function() {
128 | 'use strict';
129 |
130 | it('exists', function() {
131 | expect(md.stream).to.be.a('function');
132 | });
133 |
134 | it('real stream', function() {
135 | var sInt = md.stream(123);
136 | expect(sInt.constructor).to.be.equal(md.stream);
137 | expect(sInt.name).to.be.equal('stream');
138 | });
139 |
140 | });
141 |
142 | describe('#toStream()', function() {
143 | 'use strict';
144 |
145 | it('exists', function() {
146 | expect(md.toStream).to.be.a('function');
147 | });
148 |
149 | it('any value', function() {
150 | var sInt = md.toStream(123);
151 | var sString = md.toStream('Foo');
152 | var sUndefined = md.toStream();
153 | expect(sInt()).to.be.a('number');
154 | expect(sInt()).to.be.equal(123);
155 | expect(sString()).to.be.a('string');
156 | expect(sString()).to.be.equal('Foo');
157 | expect(sUndefined()).to.be.undefined;
158 | });
159 |
160 | it('stream value', function() {
161 | var sInt = md.toStream(123);
162 | var newStream = md.toStream(sInt);
163 | expect(newStream()).to.be.a('number');
164 | expect(newStream()).to.be.equal(123);
165 | });
166 |
167 | it('real stream', function() {
168 | var sInt = md.toStream(123);
169 | expect(sInt.constructor).to.be.equal(md.stream);
170 | expect(sInt.name).to.be.equal('stream');
171 | });
172 |
173 | });
174 |
175 | describe('#noConflict()', function() {
176 | 'use strict';
177 |
178 | it('exists', function() {
179 | expect(md.noConflict).to.be.a('function');
180 | });
181 |
182 | });
183 |
184 | describe('#config()', function() {
185 | 'use strict';
186 |
187 | it('exists', function() {
188 | expect(md.config).to.be.a('function');
189 | });
190 |
191 | });
192 |
193 | describe('#resetConfig()', function() {
194 | 'use strict';
195 |
196 | after(function() {
197 | md.resetConfig();
198 | })
199 |
200 | it('exists', function() {
201 | expect(md.resetConfig).to.be.a('function');
202 | });
203 |
204 | it('return back to defaults', function() {
205 | var fn = function() {};
206 | md.config({
207 | keyId: 'customId',
208 | storeExtract: fn
209 | });
210 | expect(md.__TEST__.config.storeExtract).to.equal(fn);
211 | expect(md.__TEST__.config.keyId).to.equal('customId');
212 | md.resetConfig();
213 | expect(md.__TEST__.config.storeExtract).to.be.undefined;
214 | expect(md.__TEST__.config.keyId).to.equal('id');
215 | });
216 |
217 | });
218 |
219 | describe('#model()', function() {
220 | 'use strict';
221 |
222 | it('exists', function() {
223 | expect(md.model).to.exist;
224 | });
225 |
226 | it('is a function', function() {
227 | expect(md.model).to.be.a('function');
228 | });
229 |
230 | it('returns a Model Constructor', function() {
231 | var User = window.Model.User;
232 | expect(User.prototype.__proto__).to.equal(md.__TEST__.BaseModel.prototype);
233 | expect(User).to.be.instanceof(md.__TEST__.ModelConstructor);
234 | });
235 |
236 | it('throw error on missing name', function() {
237 | expect(function() {
238 | var ModelA = md.model({
239 | props: ['name']
240 | });
241 | }).to.throw(Error);
242 | });
243 |
244 | it('throw error on restricted props', function() {
245 | expect(function() {
246 | var ModelB = md.model({
247 | name: 'ModelB',
248 | props: ['url'] // `url` is reserved
249 | });
250 | var mdl = new ModelB();
251 | }).to.throw(Error);
252 | });
253 |
254 | it('correct url', function() {
255 | var ModelB = md.model({
256 | name: 'ModelB',
257 | url: '/modelb'
258 | });
259 | var mdl = new ModelB();
260 | expect(mdl.url()).to.equal('/modelb');
261 | });
262 |
263 | it('get model constructor from different scope', function() {
264 | // In scope 'A'
265 | md.model({
266 | name: 'ModelC'
267 | });
268 | // In scope 'B'
269 | var mdl = new(md.model.get('ModelC'));
270 | expect(mdl).to.be.instanceof(md.__TEST__.BaseModel);
271 | });
272 |
273 | });
274 |
275 | });
--------------------------------------------------------------------------------
/test/www/test-ModelConstructor.js:
--------------------------------------------------------------------------------
1 | describe('Model Constructor', function() {
2 | 'use strict';
3 |
4 | it('is a constructor function', function() {
5 | var User = Model.User;
6 | expect(User).to.be.a('function');
7 | });
8 |
9 | it('has `modelOptions` property', function() {
10 | var User = Model.User;
11 | expect(User).to.have.property('modelOptions');
12 | });
13 |
14 | it('`modelOptions` has `name` property with type of string', function() {
15 | var User = Model.User;
16 | expect(User.modelOptions).to.have.property('name');
17 | expect(User.modelOptions.name).to.be.a('string');
18 | });
19 |
20 | it('`modelOptions` has `props` property with type of array of strings', function() {
21 | var User = Model.User;
22 | expect(User.modelOptions).to.have.property('props');
23 | expect(User.modelOptions.props).to.be.a('array');
24 | expect(User.modelOptions.props[0]).to.be.a('string');
25 | });
26 |
27 | describe('#createCollection()', function() {
28 | 'use strict';
29 |
30 | it('exist', function() {
31 | var User = Model.User;
32 | expect(User).to.have.property('createCollection');
33 | });
34 |
35 | it('returns new instance of `md.Collection`', function() {
36 | var User = Model.User;
37 | var userCollection = User.createCollection();
38 | expect(userCollection).to.be.an.instanceof(md.Collection);
39 | expect(userCollection.__options.model).to.exist;
40 | expect(userCollection.__options.model).to.equal(User);
41 | });
42 |
43 | });
44 |
45 | describe('#createModels()', function() {
46 | 'use strict';
47 |
48 | it('exist', function() {
49 | expect(Model.User.createModels).to.be.a.function;
50 | });
51 |
52 | it('convert array of object-data to array of models', function() {
53 | var models = Model.User.createModels([{
54 | name: 'Foo'
55 | }, {
56 | name: 'Bar'
57 | }]);
58 | expect(models[0].name()).to.equal('Foo');
59 | expect(models[1].name()).to.equal('Bar');
60 | });
61 |
62 | it('create with parser', function() {
63 | var models = Model.Note.createModels([{
64 | wrap: {
65 | title: 'Foo',
66 | inner: {
67 | body: 'Bar',
68 | author: 'Baz'
69 | }
70 | }
71 | }]);
72 | expect(models[0].title()).to.equal('Foo');
73 | expect(models[0].body()).to.equal('Bar');
74 | expect(models[0].author()).to.equal('Baz');
75 | });
76 |
77 | it('cache and cache limit', function() {
78 | var CacheNoteModel = md.model({
79 | name: 'CacheNoteModel',
80 | props: ['title', 'body']
81 | }, {
82 | cache: true,
83 | cacheLimit: 2
84 | });
85 | var modelsA = CacheNoteModel.createModels([{
86 | id: '123',
87 | name: 'Foo',
88 | body: 'Bar'
89 | }, {
90 | id: '456',
91 | name: 'Foo',
92 | body: 'Bar'
93 | }]);
94 | var modelsB = CacheNoteModel.createModels([{
95 | id: '123',
96 | name: 'Foo',
97 | body: 'Bar'
98 | }, {
99 | id: '456',
100 | name: 'Baz',
101 | body: 'Tes'
102 | }, {
103 | id: '789',
104 | name: 'Zzz',
105 | body: 'Xxx'
106 | }]);
107 | expect(modelsA.length).to.equal(2);
108 | expect(modelsB.length).to.equal(3);
109 | expect(modelsB[0]).to.equal(modelsA[0]);
110 | expect(modelsB[1]).to.equal(modelsA[1]);
111 | expect(modelsB[0].lid()).to.equal(modelsA[0].lid());
112 | expect(modelsB[1].lid()).to.equal(modelsA[1].lid());
113 | expect(modelsB[2].id()).to.equal('789');
114 | expect(CacheNoteModel.__cacheCollection.size()).to.equal(2);
115 | expect(CacheNoteModel.__cacheCollection.contains('123')).to.be.false;
116 | expect(CacheNoteModel.__cacheCollection.contains('789')).to.be.true;
117 | });
118 |
119 |
120 | });
121 |
122 | describe('#create()', function() {
123 | 'use strict';
124 |
125 | var CacheUser;
126 | var tempIdA;
127 | var tempIdB;
128 |
129 | before(function() {
130 | CacheUser = md.model({
131 | name: 'CacheUser',
132 | props: ['name', 'age']
133 | }, {
134 | cache: true,
135 | });
136 | });
137 |
138 | it('exist', function() {
139 | expect(CacheUser.create).to.be.a.function;
140 | });
141 |
142 | it('instantiate model *1', function(done) {
143 | // Create a model and add to cache but without saving to store
144 | CacheUser.create({
145 | id: 'mcid001',
146 | name: 'Fooz'
147 | });
148 | // Create a model, saving to store and add to cache
149 | var userB = CacheUser.create();
150 | expect(userB).to.be.instanceof(CacheUser);
151 | userB.name('Barz');
152 | // By new keyword
153 | var userC = new CacheUser({
154 | name: 'Jazz'
155 | });
156 | // userC.save();
157 | Promise.all([
158 | userB.save().then(function() {
159 | // We now have id generated by server
160 | expect(userB.id()).to.exist;
161 | tempIdA = userB.id();
162 | }),
163 | userC.save().then(function() {
164 | expect(userC.id()).to.exist;
165 | tempIdB = userC.id();
166 | })
167 | ]).then(function() {
168 | done();
169 | });
170 | });
171 |
172 | it('get from cache (should not create new id)', function(done) {
173 | // Get get model that was create without saving
174 | var userA = CacheUser.create({
175 | // User id was created in *1
176 | id: 'mcid001'
177 | });
178 | expect(userA.name()).to.equal('Fooz');
179 | // Get get model that was create WITH saving
180 | var userB = CacheUser.create({
181 | // User id was created in *1
182 | id: tempIdA
183 | });
184 | expect(userB.name()).to.equal('Barz');
185 | var userC = CacheUser.create({
186 | // User id was created in *1
187 | id: tempIdB
188 | });
189 | expect(userC.name()).to.equal('Jazz');
190 | var userCA = new CacheUser({
191 | id: tempIdB
192 | });
193 | userCA.fetch().then(function() {
194 | expect(userCA.name()).to.equal('Jazz');
195 | done();
196 | });
197 | });
198 |
199 | });
200 |
201 | describe('#pull()', function() {
202 | 'use strict';
203 |
204 | var _ids = [];
205 | var _models = {};
206 |
207 | before(function(done) {
208 | var len = 5;
209 | var cnt = 0;
210 | var user;
211 | var fn = function(model) {
212 | _ids.push(model.id());
213 | _models[model.id()] = model;
214 | cnt++;
215 | if (cnt >= len) {
216 | done();
217 | return;
218 | }
219 | };
220 | for (var i = len; i > 0; i--) {
221 | user = new Model.User();
222 | user.name('Foo' + i);
223 | user.age(111);
224 | user.save().then(fn);
225 | }
226 | });
227 |
228 | it('exist', function() {
229 | expect(Model.User.pull).to.exist.and.be.a.function;
230 | });
231 |
232 | it('should pull (1) - pull by array (of ids)', function(done) {
233 | // url: /user?0=idabc&1=idxyz
234 | // if you prefer /user/idabc/idxyz
235 | // url is parsable through md.config { storeConfigOptions }
236 | Model.User.pull('/user', [_ids[1], _ids[2]]).then(function(models) {
237 | try {
238 | expect(models.length).to.equal(2);
239 | var m0 = models[0];
240 | expect(m0.name()).to.equal(_models[m0.id()].name());
241 | var m1 = models[1];
242 | expect(m1.name()).to.equal(_models[m1.id()].name());
243 | done();
244 | } catch (e) {
245 | done(e);
246 | }
247 | }, function(err) {
248 | done(err);
249 | })
250 | });
251 |
252 | it('should pull (2) - pull by object (query)', function(done) {
253 | // url: /user?name=Test&age=111
254 | // if you prefere /user/Test/111
255 | // url is parsable through md.config { storeConfigOptions }
256 | Model.User.pull('/user', {
257 | name: 'Test',
258 | age: 111
259 | }).then(function(models) {
260 | try {
261 | done();
262 | } catch (e) {
263 | done(e);
264 | }
265 | }, function(err) {
266 | done(err);
267 | })
268 | });
269 |
270 | it('should pull (3) - pull without data', function(done) {
271 | Model.User.pull('/user').then(function(models) {
272 | try {
273 | expect(models.length).to.above(1);
274 | done();
275 | } catch (e) {
276 | done(e);
277 | }
278 | }, function(err) {
279 | done(err);
280 | })
281 | });
282 |
283 | it('pull using callback function', function(done) {
284 | Model.User.pull('/user', function(err, response, models) {
285 | if (err) {
286 | done(err);
287 | return;
288 | }
289 | try {
290 | expect(response).to.exist;
291 | expect(models).to.exist;
292 | done();
293 | } catch (e) {
294 | done(e);
295 | }
296 | });
297 | });
298 |
299 | it('pull with path', function(done) {
300 | Model.User.pull('/users/wrap', {
301 | path: 'outer.inner.items'
302 | }, function(err, response, models) {
303 | if (err) {
304 | done(err)
305 | return;
306 | }
307 | try {
308 | expect(response.outer.inner.items).to.exist;
309 | expect(models.length).to.be.above(3);
310 | expect(models[0].id()).to.be.not.empty;
311 | expect(models[0].name()).to.be.not.empty;
312 | expect(models[0].age()).to.be.not.empty;
313 | done();
314 | } catch (e) {
315 | done(e);
316 | }
317 | });
318 | });
319 |
320 | });
321 |
322 | });
323 |
--------------------------------------------------------------------------------
/source/collection.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Collection
3 | */
4 |
5 | var _ = require('lodash');
6 | var m = require('mithril');
7 | var util = require('./util');
8 | var config = require('./global').config;
9 | var BaseModel = require('./baseModel');
10 | var State = require('./state');
11 |
12 | function Collection(options) {
13 | this.models = [];
14 | this.__options = {
15 | redraw: false,
16 | _cache: false
17 | };
18 | if (options)
19 | this.opt(options);
20 | var state = this.__options.state;
21 | if (state) {
22 | if (!(state instanceof State)) {
23 | state = new State(state);
24 | }
25 | this.__state = state;
26 | }
27 | _.bindAll(this, _.union(collectionBindMethods, config.collectionBindMethods));
28 | this.__working = false;
29 | }
30 |
31 | // Export class.
32 | module.exports = Collection;
33 |
34 | // Prototype methods.
35 | Collection.prototype = {
36 | opt: function(key, value) {
37 | if (_.isPlainObject(key))
38 | _.assign(this.__options, key);
39 | else
40 | this.__options[key] = _.isUndefined(value) ? true : value;
41 | },
42 | add: function(model, unshift, silent) {
43 | if (!(model instanceof BaseModel) || (this.__options.model && !(model instanceof this.__options.model)))
44 | throw new Error('Must be a model or an instance of set model');
45 | var existingModel = this.get(model);
46 | var added = false;
47 | if (existingModel) {
48 | existingModel.set(model);
49 | } else {
50 | if (unshift)
51 | this.models.unshift(model.getJson());
52 | else
53 | this.models.push(model.getJson());
54 | model.attachCollection(this);
55 | if (this.__state)
56 | this.__state.set(model.lid());
57 | added = true;
58 | }
59 | if (added && !silent)
60 | this.__update();
61 | return added;
62 | },
63 | addAll: function(models, unshift, silent) {
64 | if (!_.isArray(models))
65 | models = [models];
66 | var added = false;
67 | var i = 0;
68 | for (; i < models.length; i++) {
69 | if (this.add(models[i], unshift, true))
70 | added = true;
71 | }
72 | if (added && !silent)
73 | this.__update();
74 | return added;
75 | },
76 | create: function(data, opts) {
77 | if (!_.isArray(data))
78 | data = [data];
79 | var newModels = [];
80 | var existingModel;
81 | var modelData;
82 | var i = 0;
83 | for (; i < data.length; i++) {
84 | modelData = data[i];
85 | if (!_.isPlainObject(modelData))
86 | throw new Error('Plain object required');
87 | existingModel = this.get(modelData);
88 | if (existingModel) {
89 | existingModel.set(modelData, true);
90 | } else {
91 | // TODO: Check to use cache
92 | if (this.__options.model)
93 | newModels.push(this.__options.model.create(modelData, opts));
94 | // newModels.push(new this.__options.model(modelData));
95 | }
96 | }
97 | this.addAll(newModels);
98 | return newModels;
99 | },
100 | get: function(mixed) {
101 | // mixed can be id-number, id-string, plain-object or model.
102 | // NOTE: check if model/object contains id and use it instead.
103 | // returns a model.
104 | var jsonModel;
105 | if (mixed instanceof BaseModel) {
106 | // mixed is a model and is in this collection.
107 | return this.contains(mixed) ? mixed : undefined;
108 | } else if (_.isObject(mixed)) {
109 | // Use `isObject` to include functions.
110 | if (mixed[config.keyId])
111 | mixed = mixed[config.keyId];
112 | else
113 | return this.find(mixed) || undefined;
114 | }
115 | jsonModel = this.find([config.keyId, mixed]);
116 | return jsonModel || undefined;
117 | },
118 | getAll: function(mixed, falsy) {
119 | // Note that this will not get all matched.
120 | // Will only get the first match of each array item.
121 | if (!_.isArray(mixed))
122 | mixed = [mixed];
123 | var models = [];
124 | var i = 0;
125 | var exist;
126 | for (; i < mixed.length; i++) {
127 | exist = this.get(mixed[i]);
128 | if (exist || falsy) {
129 | models.push(exist);
130 | }
131 | }
132 | return models;
133 | },
134 | remove: function(mixed, silent) {
135 | // mixed can be array of id-number, id-string, plain-object or model.
136 | if (!_.isArray(mixed))
137 | mixed = [mixed];
138 | var lastLength = this.size();
139 | var removedModels = [];
140 | var matchMix;
141 | var mix;
142 | var i;
143 | if (!lastLength)
144 | return;
145 | for (i = 0; i < mixed.length; i++) {
146 | mix = mixed[i];
147 | if (!mix)
148 | throw new Error('Can\'t remove from collection. Argument must be set.');
149 | if (mix instanceof BaseModel) {
150 | removedModels.push.apply(removedModels, _.remove(this.models, function(value) {
151 | return _.eq(value, mix.getJson());
152 | }));
153 | } else if (_.isObjectLike(mix)) {
154 | removedModels.push.apply(removedModels, _.remove(this.models, function(value) {
155 | return _.isMatch(value, mix);
156 | }));
157 | } else {
158 | removedModels.push.apply(removedModels, _.remove(this.models, function(value) {
159 | matchMix = {};
160 | matchMix[config.keyId] = mix;
161 | return _.isMatch(value, matchMix);
162 | }));
163 | }
164 | }
165 | var model;
166 | for (i = 0; i < removedModels.length; i++) {
167 | model = removedModels[i].__model;
168 | model.detachCollection(this);
169 | if (this.__state)
170 | this.__state.remove(model.lid());
171 | }
172 | if (lastLength !== this.size()) {
173 | if (!silent)
174 | this.__update();
175 | return true;
176 | }
177 | return false;
178 | },
179 | push: function(models, silent) {
180 | return this.addAll(models, silent);
181 | },
182 | unshift: function(models, silent) {
183 | return this.addAll(models, true, silent);
184 | },
185 | shift: function(silent) {
186 | var model = this.first();
187 | this.remove(model, silent);
188 | return model;
189 | },
190 | pop: function(silent) {
191 | var model = this.last();
192 | this.remove(model, silent);
193 | return model;
194 | },
195 | clear: function(silent) {
196 | var i, item, items = this.toArray(),
197 | len = items.length;
198 | for (i = 0; i < len; i++) {
199 | item = items[i];
200 | item.detachCollection(this);
201 | if (this.__state) this.__state.remove(item.lid());
202 | }
203 | if (len !== this.size()) {
204 | if (!silent) this.__update();
205 | return true;
206 | }
207 | return false;
208 | },
209 | pluck: function(key, filter) {
210 | var plucked = [],
211 | isId = (key === 'id');
212 | for (var i = 0, models = this.models; i < models.length; i++) {
213 | if (filter && !filter(models[i].__model)) continue;
214 | plucked.push(isId ? models[i].__model[key]() : models[i][key]);
215 | }
216 | return plucked;
217 | },
218 | dispose: function() {
219 | var keys = _.keys(this);
220 | var i = 0;
221 | if (this.__options.model)
222 | this.__options.model = null;
223 | if (this.__state)
224 | this.__state.dispose();
225 | for (; i < keys.length; i++) {
226 | this[keys[i]] = null;
227 | }
228 | },
229 | destroy: function() {
230 | this.clear(true);
231 | this.dispose();
232 | },
233 | stateOf: function(mixed) {
234 | if (this.__state) {
235 | var model = this.get(mixed);
236 | if (model)
237 | return this.__state.get(model.lid());
238 | }
239 | },
240 | contains: function(mixed) {
241 | if (mixed instanceof BaseModel) {
242 | // mixed is a model and is in this collection.
243 | return this.indexOf(mixed.getJson()) > -1;
244 | } else if (_.isObject(mixed)) {
245 | // Use `isObject` to include functions.
246 | // If mixed contains `keyId` then search by id
247 | if (mixed[config.keyId])
248 | mixed = mixed[config.keyId];
249 | else
250 | return this.findIndex(mixed) > -1;
251 | }
252 | return this.findIndex([config.keyId, mixed]) > -1;
253 | },
254 | sort: function(fields, orders) {
255 | if (!_.isArray(fields))
256 | fields = [fields];
257 | var sorted;
258 | if (orders) {
259 | if (!_.isArray(orders))
260 | orders = [orders];
261 | sorted = this.orderBy(fields, orders);
262 | } else {
263 | sorted = this.orderBy(fields);
264 | }
265 | this.__replaceModels(sorted);
266 | },
267 | sortByOrder: function(order, path) {
268 | if (!path) path = config.keyId;
269 | this.__replaceModels(this.sortBy([function(item) {
270 | return order.indexOf(_.get(item, path));
271 | }]));
272 | },
273 | randomize: function() {
274 | this.__replaceModels(this.shuffle());
275 | },
276 | hasModel: function() {
277 | return !!this.__options.model;
278 | },
279 | model: function() {
280 | return this.__options.model;
281 | },
282 | url: function(noBase) {
283 | var url = noBase ? '' : config.baseUrl;
284 | if (this.__options.url)
285 | url += this.__options.url;
286 | else if (this.hasModel())
287 | url += this.model().modelOptions.url || '/' + this.model().modelOptions.name.toLowerCase();
288 | return url;
289 | },
290 | fetch: function(query, options, callback) {
291 | if (_.isFunction(options)) {
292 | callback = options;
293 | options = undefined;
294 | }
295 | var self = this;
296 | self.__working = true;
297 | if (self.hasModel()) {
298 | options = options || {};
299 | return self.model().pull(self.url(), query, options, function(err, response, models) {
300 | self.__working = false;
301 | if (err) {
302 | if (_.isFunction(callback)) callback(err);
303 | } else {
304 | if (options.clear)
305 | self.clear(true);
306 | self.addAll(models);
307 | if (_.isFunction(callback)) callback(null, response, models);
308 | }
309 | });
310 | } else {
311 | self.__working = false;
312 | var err = new Error('Collection must have a model to perform fetch');
313 | if (_.isFunction(callback)) callback(err);
314 | return Promise.reject(err);
315 | }
316 | },
317 | isWorking: function() {
318 | return this.__working;
319 | },
320 | isFetching: function() {
321 | return this.isWorking();
322 | },
323 | __replaceModels: function(models) {
324 | for (var i = models.length - 1; i >= 0; i--) {
325 | this.models[i] = models[i].__json;
326 | }
327 | },
328 | __update: function(fromModel) {
329 | // Levels: instance || global
330 | if (!this.__options._cache && (this.__options.redraw || config.redraw)) {
331 | // If `fromModel` is specified, means triggered by contained model.
332 | // Otherwise, triggered by collection itself.
333 | if (!fromModel) {
334 | // console.log('Redraw', 'Collection');
335 | m.redraw();
336 | }
337 | return true;
338 | }
339 | }
340 | };
341 |
342 |
343 | // Method to bind to Collection object. Use by _.bindAll().
344 | var collectionBindMethods = [];
345 |
346 | // Lodash methods to add.
347 | var collectionMethods = {
348 | difference: 1,
349 | every: 1,
350 | find: 1,
351 | findIndex: 1,
352 | findLastIndex: 1,
353 | filter: 1,
354 | first: 0,
355 | forEach: 1,
356 | indexOf: 2,
357 | initial: 0,
358 | invoke: 3,
359 | groupBy: 1,
360 | last: 0,
361 | lastIndexOf: 2,
362 | map: 1,
363 | maxBy: 1,
364 | minBy: 1,
365 | nth: 1,
366 | orderBy: 2,
367 | partition: 1,
368 | reduce: -1,
369 | reject: 1,
370 | reverse: 0,
371 | sample: 0,
372 | shuffle: 0,
373 | size: 0,
374 | slice: 2,
375 | sortBy: 1,
376 | some: 1,
377 | transform: 2,
378 | toArray: 0,
379 | without: 1
380 | };
381 |
382 |
383 | // Inject lodash method.
384 | util.addMethods(Collection.prototype, _, collectionMethods, 'models', '__model');
--------------------------------------------------------------------------------
/test/www/test-md.config.js:
--------------------------------------------------------------------------------
1 | describe('md.config()', function() {
2 | 'use strict';
3 |
4 | afterEach(function() {
5 | md.resetConfig();
6 | });
7 |
8 | describe('baseUrl', function() {
9 | 'use strict';
10 |
11 | it('for model', function() {
12 | md.config({
13 | baseUrl: '/base'
14 | });
15 | var user = new Model.User();
16 | expect(user.url()).to.equal('/base/user');
17 | });
18 |
19 | it('for model with custom url', function() {
20 | md.config({
21 | baseUrl: '/base'
22 | });
23 | var ModelConfigBaseUrlA = md.model({
24 | name: 'ModelConfigBaseUrlA',
25 | url: '/modelurl'
26 | });
27 | var mdl = new ModelConfigBaseUrlA();
28 | expect(mdl.url()).to.equal('/base/modelurl');
29 | });
30 |
31 | it('for collection without model', function() {
32 | md.config({
33 | baseUrl: '/base'
34 | });
35 | var colA = new md.Collection();
36 | expect(colA.url()).to.equal('/base');
37 | });
38 |
39 | it('for collection without model but with custom url', function() {
40 | md.config({
41 | baseUrl: '/base'
42 | });
43 | var colA = new md.Collection({
44 | url: '/collectionurl'
45 | });
46 | expect(colA.url()).to.equal('/base/collectionurl');
47 | });
48 |
49 | it('for collection with model', function() {
50 | md.config({
51 | baseUrl: '/base'
52 | });
53 | var colB = new md.Collection({
54 | model: Model.User
55 | });
56 | expect(colB.url()).to.equal('/base/user');
57 | });
58 |
59 | it('for collection with custom url', function() {
60 | md.config({
61 | baseUrl: '/base'
62 | });
63 | var colC = new md.Collection({
64 | model: Model.User,
65 | url: '/collectionurl'
66 | });
67 | expect(colC.url()).to.equal('/base/collectionurl');
68 | });
69 |
70 |
71 | it('for collection with model that is with custom url', function() {
72 | md.config({
73 | baseUrl: '/base'
74 | });
75 | var ModelConfigBaseUrlB = md.model({
76 | name: 'ModelConfigBaseUrlB',
77 | url: '/modelurl'
78 | });
79 | var colD = new md.Collection({
80 | model: ModelConfigBaseUrlB
81 | });
82 | expect(colD.url()).to.equal('/base/modelurl');
83 | });
84 |
85 | });
86 |
87 | describe('keyId', function() {
88 | 'use strict';
89 |
90 | it('exist in model and is a function', function() {
91 | md.config({
92 | keyId: 'customKey'
93 | });
94 | var ModelCustomKeyIdA = md.model({
95 | name: 'ModelCustomKeyIdA',
96 | props: ['name']
97 | });
98 | var mdl = new ModelCustomKeyIdA();
99 | expect(mdl.customKey).to.be.a('function');
100 | });
101 |
102 | it('set and get correct value through method `id()`', function() {
103 | md.config({
104 | keyId: 'customKey'
105 | });
106 | var ModelCustomKeyIdB = md.model({
107 | name: 'ModelCustomKeyIdB',
108 | props: ['name']
109 | });
110 | var mdl = new ModelCustomKeyIdB();
111 | mdl.id('test123');
112 | expect(mdl.id()).to.equal('test123');
113 | expect(mdl.__json.customKey).to.equal('test123');
114 | expect(mdl.__json.id).to.not.exist;
115 | });
116 |
117 | });
118 |
119 | describe('constructorMethods', function() {
120 | 'use strict';
121 |
122 | it('exist in model and is a function', function() {
123 | md.config({
124 | constructorMethods: {
125 | customConsMethod: function() {
126 | expect(this).to.be.a('function');
127 | }
128 | }
129 | });
130 | var ConsCustomMethodsA = md.model({
131 | name: 'ConsCustomMethodsA'
132 | });
133 | var ConsCustomMethodsB = md.model({
134 | name: 'ConsCustomMethodsA'
135 | });
136 | expect(ConsCustomMethodsA.customConsMethod).to.be.a('function');
137 | ConsCustomMethodsA.customConsMethod();
138 | expect(ConsCustomMethodsB.customConsMethod).to.be.a('function');
139 | ConsCustomMethodsB.customConsMethod();
140 | });
141 |
142 | });
143 |
144 | describe('modelMethods', function() {
145 | 'use strict';
146 |
147 | it('exist in model and is a function', function() {
148 | md.config({
149 | modelMethods: {
150 | customModelMethod: function() {
151 | expect(this).to.be.instanceof(md.__TEST__.BaseModel);
152 | }
153 | }
154 | });
155 | var ModelCustomMethodsA = md.model({
156 | name: 'ModelCustomMethodsA'
157 | });
158 | // var ModelCustomMethodsB = md.model({
159 | // name: 'ModelCustomMethodsB'
160 | // });
161 | var mdlA = new ModelCustomMethodsA();
162 | expect(mdlA.customModelMethod).to.be.a('function');
163 | mdlA.customModelMethod();
164 | var mdlB = new ModelCustomMethodsA();
165 | expect(mdlB.customModelMethod).to.be.a('function');
166 | mdlB.customModelMethod();
167 | });
168 |
169 | });
170 |
171 | describe('collectionMethods', function() {
172 | 'use strict';
173 |
174 | it('exist in model and is a function', function() {
175 | md.config({
176 | collectionMethods: {
177 | customColMethod: function() {
178 | expect(this).to.be.instanceof(md.Collection);
179 | }
180 | }
181 | });
182 | var ColA = new md.Collection();
183 | var ColB = new md.Collection();
184 | expect(ColA.customColMethod).to.be.a('function');
185 | ColA.customColMethod();
186 | expect(ColB.customColMethod).to.be.a('function');
187 | ColB.customColMethod();
188 | });
189 |
190 | });
191 |
192 | describe('modelBindMethods', function() {
193 | 'use strict';
194 |
195 | it('direct own property', function() {
196 | md.config({
197 | modelBindMethods: ['save', 'destroy']
198 | });
199 | var ModelBindMethodsA = md.model({
200 | name: 'ModelBindMethodsA'
201 | });
202 | var mdl = new ModelBindMethodsA();
203 | expect(mdl.hasOwnProperty('save')).to.be.true;
204 | expect(mdl.hasOwnProperty('destroy')).to.be.true;
205 | });
206 |
207 | });
208 |
209 | describe('collectionBindMethods', function() {
210 | 'use strict';
211 |
212 | it('direct own property', function() {
213 | md.config({
214 | collectionBindMethods: ['fetch']
215 | });
216 | var col = new md.Collection();
217 | expect(col.hasOwnProperty('fetch')).to.be.true;
218 | });
219 |
220 | });
221 |
222 | describe('storeConfigOptions', function() {
223 | 'use strict';
224 |
225 | it('added to md config', function() {
226 | var fn = function() {};
227 | md.config({
228 | storeConfigOptions: fn
229 | });
230 | expect(md.__TEST__.config.storeConfigOptions).to.equal(fn);
231 | });
232 |
233 | it('called with correct arguments', function(done) {
234 | var fn = function(options) {
235 | expect(options.method).to.equal('GET');
236 | expect(options.url).to.equal('/exist');
237 | done();
238 | };
239 | md.config({
240 | storeConfigOptions: fn
241 | });
242 | md.store.request('/exist');
243 | });
244 |
245 | });
246 |
247 | describe('storeConfigXHR', function() {
248 | 'use strict';
249 |
250 | it('added to md config', function() {
251 | var fn = function() {};
252 | md.config({
253 | storeConfigXHR: fn
254 | });
255 | expect(md.__TEST__.config.storeConfigXHR).to.equal(fn);
256 | });
257 |
258 | });
259 |
260 | describe('storeExtract', function() {
261 | 'use strict';
262 | // This test case suite will create error
263 | // storeExtract must be configured before everthing else
264 | it('added to md config', function() {
265 | var fn = function() {};
266 | md.config({
267 | storeExtract: fn
268 | });
269 | expect(md.__TEST__.config.storeExtract).to.equal(fn);
270 | });
271 |
272 | });
273 |
274 | describe('store', function() {
275 | 'use strict';
276 |
277 | it('added to md config', function() {
278 | var fn = function() {};
279 | md.config({
280 | store: fn
281 | });
282 | expect(md.__TEST__.config.store).to.equal(fn);
283 | });
284 |
285 | it('called with correct arguments', function(done) {
286 | var fn = function(data) {
287 | expect(data.method).to.equal('GET');
288 | expect(data.url).to.equal('/exist');
289 | expect(data.data).to.exist;
290 | done();
291 | };
292 | md.config({
293 | store: fn
294 | });
295 | md.store.request('/exist');
296 | });
297 |
298 | });
299 |
300 | describe('stream', function() {
301 | 'use strict';
302 |
303 | it('added to config', function() {
304 | var fn = function() {};
305 | md.config({
306 | stream: fn
307 | });
308 | expect(md.__TEST__.config.stream).to.equal(fn);
309 | });
310 |
311 | it('added to md', function() {
312 | var fn = function() {};
313 | md.config({
314 | stream: fn
315 | });
316 | expect(md.stream).to.equal(fn);
317 | });
318 |
319 | });
320 |
321 | describe('placeholder', function() {
322 | 'use strict';
323 |
324 | it('added to config', function() {
325 | md.config({
326 | placeholder: 'Fooing...'
327 | });
328 | expect(md.__TEST__.config.placeholder).to.equal('Fooing...');
329 | });
330 |
331 | it('defaults to null', function() {
332 | expect(md.__TEST__.config.placeholder).to.be.null;
333 | var folder = new Model.Folder({
334 | id: 'fold001'
335 | });
336 | folder.fetch();
337 | expect(folder.name()).to.equal('Untitled Folder');
338 | });
339 |
340 | it('should be working', function(done) {
341 | md.config({
342 | placeholder: 'Baring...'
343 | });
344 |
345 | var folder = new Model.Folder({
346 | id: 'fold001'
347 | });
348 | folder.fetch().then(function() {
349 | expect(folder.name()).to.equal('System');
350 | expect(folder.thumbnail()).to.equal('thumbnail.jpg');
351 | done();
352 | }, done);
353 | expect(folder.name()).to.equal('Baring...');
354 | expect(folder.thumbnail()).to.equal('thumbnail.jpg');
355 | expect(folder.size()).to.be.undefined;
356 | });
357 |
358 | });
359 |
360 | });
--------------------------------------------------------------------------------
/source/baseModel.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Base Model
3 | */
4 |
5 | var _ = require('lodash');
6 | var m = require('mithril');
7 | var config = require('./global').config;
8 | var modelConstructors = require('./global').modelConstructors;
9 |
10 | function BaseModel(opts) {
11 | this.__options = {
12 | redraw: false,
13 | parse: true
14 | };
15 | this.__collections = [];
16 | this.__lid = _.uniqueId('model');
17 | this.__saved = false;
18 | this.__modified = false;
19 | this.__working = 0; // 0:idle, 1: fetch, 2: save, 3: destroy
20 | this.__json = {
21 | __model: this
22 | };
23 | _.bindAll(this, _.union(modelBindMethods, config.modelBindMethods));
24 | if (opts) this.opt(opts);
25 | }
26 |
27 | // Export class.
28 | module.exports = BaseModel;
29 |
30 | // Need to require after export. To fix the circular dependencies issue.
31 | var store = require('./store');
32 | var util = require('./util');
33 | var Collection = require('./collection');
34 |
35 | // Method to bind to Model object. Use by _.bindAll().
36 | var modelBindMethods = [];
37 |
38 | // Lodash methods to add.
39 | var objectMethods = {
40 | has: 1,
41 | keys: 0,
42 | values: 0,
43 | pick: 1,
44 | omit: 1
45 | };
46 |
47 | // Prototype methods.
48 | BaseModel.prototype = {
49 | opt: function(key, value) {
50 | if (_.isPlainObject(key)) _.assign(this.__options, key);
51 | else this.__options[key] = _.isUndefined(value) ? true : value;
52 | },
53 | // Get or set id of model.
54 | id: function(id) {
55 | return id ? this[config.keyId](id, true) : this[config.keyId]();
56 | },
57 | lid: function() {
58 | return this.__lid;
59 | },
60 | // Get the full url for store.
61 | url: function() {
62 | return config.baseUrl + (this.options.url || '/' + this.options.name.toLowerCase());
63 | },
64 | // Add this model to collection.
65 | attachCollection: function(collection) {
66 | if (!(collection instanceof Collection))
67 | throw new Error('Argument `collection` must be instance of Collection.');
68 | var model = collection.get(this);
69 | if (model && _.indexOf(this.__collections, collection) === -1) {
70 | // This model exist in collection.
71 | // Add collection to model's local reference.
72 | this.__collections.push(collection);
73 | } else {
74 | collection.add(this);
75 | }
76 | },
77 | // Remove this model from collection.
78 | detachCollection: function(collection) {
79 | if (!(collection instanceof Collection))
80 | throw new Error('Argument `collection` must be instance of Collection.');
81 | // Remove this model from collection first.
82 | if (collection.get(this)) collection.remove(this);
83 | // Remove that collection from model's collection.
84 | if (_.indexOf(this.__collections, collection) > -1)
85 | _.pull(this.__collections, collection);
86 | },
87 | // Sets all or a prop values from passed data.
88 | set: function(key, value, silent, saved) {
89 | if (_.isString(key)) this[key](value, silent);
90 | else this.setObject(key, silent, saved);
91 | },
92 | // Sets props by object.
93 | setObject: function(obj, silent, saved) {
94 | var isModel = obj instanceof BaseModel;
95 | if (!isModel && !_.isPlainObject(obj))
96 | throw new Error('Argument `obj` must be a model or plain object.');
97 | var _obj = (!isModel && this.__options.parse) ? this.options.parser(obj) : obj;
98 | var keys = _.keys(_obj);
99 | for (var i = keys.length - 1, key, val; i >= 0; i--) {
100 | key = keys[i];
101 | val = _obj[key];
102 | if (!this.__isProp(key) || !_.isFunction(this[key]))
103 | continue;
104 | if (isModel && _.isFunction(val)) {
105 | this[key](val(), true, saved);
106 | } else {
107 | this[key](val, true, saved);
108 | }
109 | }
110 | if (!silent) // silent
111 | this.__update();
112 | },
113 | // Get all or a prop values in object format. Creates a copy.
114 | get: function(key) {
115 | return key ? this[key]() : this.getCopy();
116 | },
117 | // Retrieve json representation. Including private properties.
118 | getJson: function() {
119 | return this.__json;
120 | },
121 | // Get a copy of json representation. Removing private properties.
122 | getCopy: function(deep, depopulate) {
123 | var copy = {};
124 | var keys = _.keys(this.__json);
125 | for (var i = 0, key, value; i < keys.length; i++) {
126 | key = keys[i];
127 | value = this.__json[key];
128 | if (this.__isProp(key)) {
129 | if (value && value.__model instanceof BaseModel)
130 | copy[key] = depopulate ? value.__model.id() : value.__model.getCopy(deep, true);
131 | else
132 | copy[key] = value;
133 | }
134 | }
135 | return deep ? _.cloneDeep(copy) : copy;
136 | },
137 | save: function(options, callback) {
138 | if (_.isFunction(options)) {
139 | callback = options;
140 | options = undefined;
141 | }
142 | var self = this;
143 | var req = this.id() ? store.put : store.post;
144 | this.__working = 2;
145 | return req.call(store, this.url(), this, options).then(function(data) {
146 | self.set(options && options.path ? _.get(data, options.path) : data, null, null, true);
147 | self.__saved = !!self.id();
148 | // Add to cache, if enabled
149 | self.__addToCache();
150 | self.__working = 0;
151 | if (_.isFunction(callback)) callback(null, data, self);
152 | return self;
153 | }, function(err) {
154 | self.__working = 0;
155 | if (_.isFunction(callback)) callback(err);
156 | throw err;
157 | });
158 | },
159 | fetch: function(options, callback) {
160 | if (_.isFunction(options)) {
161 | callback = options;
162 | options = undefined;
163 | }
164 | this.__working = 1;
165 | var self = this,
166 | data = this.__getDataId(),
167 | id = _.trim(data[config.keyId]);
168 | if (id && id != 'undefined' && id != 'null') {
169 | return store.get(this.url(), data, options).then(function(data) {
170 | if (data) {
171 | self.set(options && options.path ? _.get(data, options.path) : data, null, null, true);
172 | self.__saved = !!self.id();
173 | } else {
174 | self.__saved = false;
175 | }
176 | self.__working = 0;
177 | self.__addToCache();
178 | if (_.isFunction(callback)) callback(null, data, self);
179 | return self;
180 | }, function(err) {
181 | self.__working = 0;
182 | if (_.isFunction(callback)) callback(err);
183 | throw err;
184 | });
185 | } else {
186 | this.__working = 0;
187 | var err = new Error('Model must have an id to fetch')
188 | if (_.isFunction(callback)) callback(err);
189 | return Promise.reject(err);
190 | }
191 | },
192 | populate: function(options, callback) {
193 | if (_.isFunction(options)) {
194 | callback = options;
195 | options = undefined;
196 | }
197 | var self = this;
198 | return new Promise(function(resolve, reject) {
199 | var refs = self.options.refs;
200 | var error;
201 | var countFetch = 0;
202 | _.forEach(refs, function(refName, refKey) {
203 | var value = self.__json[refKey];
204 | if (_.isString(value) || _.isNumber(value)) {
205 | var data = {};
206 | data[config.keyId] = value;
207 | var model = modelConstructors[refName].create(data);
208 | if (model.isSaved()) {
209 | // Ok to link reference
210 | self[refKey](model);
211 | } else {
212 | // Fetch and link
213 | countFetch++;
214 | model.fetch(
215 | (options && options.fetchOptions && options.fetchOptions[refKey] ? options.fetchOptions[refKey] : null),
216 | function(err, data, mdl) {
217 | countFetch--;
218 | if (err) {
219 | error = err;
220 | } else {
221 | self[refKey](mdl);
222 | }
223 | if (!countFetch) {
224 | if (error) {
225 | reject(error);
226 | if (_.isFunction(callback)) callback(null, error);
227 | } else {
228 | // All fetched
229 | resolve(self);
230 | if (_.isFunction(callback)) callback(null, self);
231 | }
232 | }
233 | }
234 | ).catch(reject);
235 | }
236 | }
237 | });
238 | if (!countFetch) {
239 | resolve(self);
240 | if (_.isFunction(callback)) callback(null, self);
241 | }
242 | });
243 | },
244 | destroy: function(options, callback) {
245 | if (_.isFunction(options)) {
246 | callback = options;
247 | options = undefined;
248 | }
249 | // Destroy the model. Will sync to store.
250 | var self = this;
251 | var data = this.__getDataId(),
252 | id = _.trim(data[config.keyId]);
253 | this.__working = 3;
254 | if (id && id != 'undefined' && id != 'null') {
255 | return store.destroy(this.url(), data, options).then(function() {
256 | self.detach();
257 | self.__working = 0;
258 | if (_.isFunction(callback)) callback(null);
259 | self.dispose();
260 | }, function(err) {
261 | self.__working = 0;
262 | if (_.isFunction(callback)) callback(err);
263 | throw err;
264 | });
265 | } else {
266 | var err = new Error('Model must have an id to destroy');
267 | this.__working = 0;
268 | if (_.isFunction(callback)) callback(err);
269 | return Promise.reject(err);
270 | }
271 | },
272 | remove: function() {
273 | this.detach();
274 | this.dispose();
275 | },
276 | detach: function() {
277 | // Detach this model to all collection.
278 | var clonedCollections = _.clone(this.__collections);
279 | for (var i = 0; i < clonedCollections.length; i++) {
280 | clonedCollections[i].remove(this);
281 | clonedCollections[i] = null;
282 | }
283 | },
284 | dispose: function() {
285 | var keys = _.keys(this);
286 | var props = this.options.props;
287 | var i;
288 | this.__json.__model = null;
289 | for (i = 0; i < props.length; i++) {
290 | this[props[i]](null);
291 | }
292 | for (i = 0; i < keys.length; i++) {
293 | this[keys[i]] = null;
294 | }
295 | },
296 | isSaved: function() {
297 | // Fresh from store
298 | return this.__saved;
299 | },
300 | isNew: function() {
301 | return !this.__saved;
302 | },
303 | isModified: function() {
304 | // When a prop is modified
305 | return this.__modified;
306 | },
307 | isDirty: function() {
308 | return !this.isSaved() || this.isModified();
309 | },
310 | isWorking: function() {
311 | return this.__working > 0;
312 | },
313 | isFetching: function() {
314 | return this.__working === 1;
315 | },
316 | isSaving: function() {
317 | return this.__working === 2;
318 | },
319 | isDestroying: function() {
320 | return this.__working === 3;
321 | },
322 | __update: function() {
323 | var redraw;
324 | // Propagate change to model's collections.
325 | for (var i = 0; i < this.__collections.length; i++) {
326 | if (this.__collections[i].__update(true)) {
327 | redraw = true;
328 | }
329 | }
330 | // Levels: instance || schema || global
331 | if (redraw || this.__options.redraw || this.options.redraw || config.redraw) {
332 | // console.log('Redraw', 'Model');
333 | m.redraw();
334 | }
335 | },
336 | __isProp: function(key) {
337 | return _.indexOf(this.options.props, key) > -1;
338 | },
339 | __getDataId: function() {
340 | var dataId = {};
341 | dataId[config.keyId] = this.id();
342 | return dataId;
343 | },
344 | __addToCache: function() {
345 | if (this.constructor.__options.cache && !this.constructor.__cacheCollection.contains(this) && this.__saved) {
346 | this.constructor.__cacheCollection.add(this);
347 | }
348 | },
349 | __gettersetter: function(initial, key) {
350 | var _stream = config.stream();
351 | var self = this;
352 | // Wrapper
353 | function prop() {
354 | var value, defaultVal;
355 | // arguments[0] is value
356 | // arguments[1] is silent
357 | // arguments[2] is saved (from store)
358 | // arguments[3] is isinitial
359 | if (arguments.length) {
360 | // Write
361 | value = arguments[0];
362 | var ref = self.options.refs[key];
363 | if (ref) {
364 | var refConstructor = modelConstructors[ref];
365 | if (_.isPlainObject(value)) {
366 | value = refConstructor.create(value);
367 | } else if ((_.isString(value) || _.isNumber(value)) && refConstructor.__cacheCollection) {
368 | // Try to find the model in the cache
369 | value = refConstructor.__cacheCollection.get(value) || value;
370 | }
371 | }
372 | if (value instanceof BaseModel) {
373 | value.__saved = value.__saved || !!(arguments[2] && value.id());
374 | value = value.getJson();
375 | }
376 | _stream(value);
377 | self.__modified = arguments[2] ? false : !arguments[3] && self.__json[key] !== _stream._state.value;
378 | self.__json[key] = _stream._state.value;
379 | if (!arguments[1])
380 | self.__update(key);
381 | return value;
382 | }
383 |
384 | value = _stream();
385 | defaultVal = self.options && self.options.defaults[key];
386 | if (_.isNil(value)) {
387 | if (!_.isNil(defaultVal)) value = defaultVal;
388 | } else {
389 | if (value.__model instanceof BaseModel) {
390 | value = value.__model;
391 | } else if (_.isPlainObject(value) && defaultVal instanceof BaseModel) {
392 | // Fix invalid value of stream, might be due deleted reference instance model
393 | _stream(null);
394 | value = defaultVal;
395 | }
396 | }
397 | return (config.placeholder &&
398 | self.isFetching() &&
399 | key !== config.keyId &&
400 | _.indexOf(self.options.placehold, key) > -1 ?
401 | config.placeholder : value);
402 | }
403 | prop.stream = _stream;
404 | prop.call(this, initial, true, undefined, true);
405 | return prop;
406 | }
407 | };
408 |
409 | // Inject lodash methods.
410 | util.addMethods(BaseModel.prototype, _, objectMethods, '__json');
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # mithril-data
2 |
3 | A rich data model library for Mithril javascript framework.
4 |
5 | [](https://travis-ci.org/rhaldkhein/mithril-data)
6 |
7 | #### Features
8 |
9 | * Create brilliant application with **Schema**-based **Model** and **Collection**
10 | * Enriched with **Lodash** methods, integrated into Model & Collection
11 | * **Auto Redraw** on Model & Collection changes
12 | * **State** (View-Model) using Mithril's stream
13 | * Extensible, Configurable and Customizable
14 | * And many more ...
15 |
16 | #### Dependencies
17 |
18 | - [Mithril](http://mithril.js.org/) (>= 1.0.0)
19 | - [Lodash](http://lodash.com/) (>= 4.12.0) - Many Lodash methods are attached to Model and Collection
20 |
21 | - - - -
22 |
23 | ## Schema
24 |
25 | ```javascript
26 | var userSchema = {
27 | name : 'User',
28 | props : ['name', 'age']
29 | }
30 |
31 | var noteSchema = {
32 | name : 'Note',
33 | props : ['title', 'body', 'author'],
34 | defaults : {
35 | title: 'Default Title',
36 | body: 'Default Body'
37 | },
38 | refs : {
39 | author: 'User'
40 | },
41 | url : '/customurl',
42 | redraw : true
43 | }
44 | ```
45 |
46 | All available schema options:
47 | * **name** - (string, **required**) name of the model
48 | * **props** - (string array, **required**) list of model props
49 | * **defaults** - (object {prop:value}) default value of props
50 | * **refs** - (object {prop:model}) list of references to other models
51 | * **url** - (string) the specific url of the model. defaults to model's `name`
52 | * **redraw** - (boolean) trigger a redraw when a model with this schema is updated. defaults to `false`
53 | * **methods** - (object {name:function}) add custom methods to model instances (by schema)
54 | * **parser** - (function) add data parser to automatically parse data storing to model
55 |
56 | Additional option information:
57 |
58 | **parser** - An option to parse different data objects.
59 |
60 | ```javascript
61 | // Parsers for Notes schema
62 | parser : function(obj) {
63 | if(obj.kind === '3rd#party') {
64 | return {
65 | title : obj.wrap.title,
66 | body : obj.wrap.inner.body,
67 | author : obj.wrap.inner.author
68 | }
69 | } else {
70 | // Another source
71 | }
72 | }
73 | // This auto parse wrapped data. Also parsed with `setObject()`.
74 | var note = new Note({wrapped: 'data'})
75 | note.setObject({wrapped: 'data'})
76 | // To disable parsing. Set `parse: false` in the options.
77 | var note = new Note({unwrapped: 'data'}, {parse: false})
78 | ```
79 |
80 | - - - -
81 |
82 | ## Model
83 | ```javascript
84 | var User = md.model(userSchema)
85 | var Note = md.model(noteSchema)
86 |
87 | var userA = new User()
88 | userA.name('Foo')
89 | userA.age(123)
90 | userA.save(function (err) {
91 | if (!err)
92 | console.log('Saved')
93 | })
94 |
95 | var noteA = new Note({
96 | title: 'My Notes'
97 | }, {
98 | redraw : true
99 | })
100 | noteA.body('A note content')
101 | noteA.author(userA)
102 | noteA.save().then(fnResolve, fnReject)
103 | ```
104 |
105 | #### new \([initials, options])
106 | Creates an instance of model.
107 | * **initials** - (object {prop:value}) initial values of props
108 | * **options** - (object) specific options to model instance
109 | * **redraw** - (boolean) redraw
110 | * **parse** - (boolean) set `false` to disable parsing. defaults to `true`
111 |
112 | #### \#\([value, silent])
113 | Get or set value of prop. If auto-redraw is enabled, pass `true` at the end to set without auto redrawing. This uses the basic usage of stream, and to get the stream itself, use `.stream`.
114 | ```javascript
115 | user.name('Foo') // Sets the name to `Foo`
116 | var n = user.name() // Get the name... returns `Foo`
117 | user.name('Bar', true) // Silently sets the name to `Bar` (without redrawing)
118 | var s = user.name.stream.map(callback) // Get the stream object with `.stream`
119 | ```
120 |
121 | #### \#opt(key[, value])
122 | Sets an option(s) specific to the model. See `ModelConstructor` for list of options.
123 |
124 | #### \#id([strId])
125 | Get or set the ID of model regardless of real ID prop. This is useful if you have different id prop like `_id` (mongodb).
126 | ```javascript
127 | // Assumes that you configure `keyId` to `_id`
128 | user._id('Bar') // Sets the id to `Bar`
129 | var id = user.id() // Returns `Bar`
130 | // user.id('Bar') // You can also use this
131 | ```
132 |
133 | #### \#lid()
134 | Get the local ID of model. This ID is generated automatically when model is created.
135 |
136 | #### \#url()
137 | Get the url. It return the combination of `baseUrl` and the models' `url`.
138 |
139 | #### \#set(key[, value, silent])
140 | Set a value to a prop of the model.
141 | ```javascript
142 | user.set('name', 'Foo') // Sets single prop
143 | // Silent set, will NOT trigger the auto redraw
144 | user.set('age', 34, true) // Pass true at the end
145 | ```
146 |
147 | #### \#setObject(obj[, silent])
148 | Set multiple values at once using object.
149 | ```javascript
150 | user.set({name: 'Bar', age: 12}) // Sets multiple props using object
151 | user.set(existingModelInstance) // Sets multiple props user existing model instance
152 | // Silent set, will NOT trigger the auto redraw
153 | user.set({age: 32}, true) // Pass true at the end
154 | ```
155 |
156 | #### \#get(key)
157 | Get a value or a copy of all values in object literal format.
158 | ```javascript
159 | user.get('name') // Returns the value of name
160 | user.get() // Returns an object (copy) with all props and values. e.g. {name: "Foo", age: 12}
161 | ```
162 |
163 | #### \#getCopy([deep])
164 | Get a copy model in object literal format. Additionally, you can set deep to `true` to copy all props recursively.
165 |
166 | #### \#attachCollection(collection)
167 | Attach the model to a collection.
168 |
169 | #### \#detachCollection(collection)
170 | Detach the model from a collection.
171 |
172 | #### \#detach()
173 | Detach the model from ALL associated collections.
174 |
175 | #### \#remove()
176 | Triggers `detach` and also `dispose` the object. Make sure you're not using the model anymore.
177 |
178 | #### \#isSaved()
179 | True if it contains id and fresh from store (server or local storage).
180 |
181 | #### \#isModified()
182 | True when a prop is modified.
183 |
184 | #### \#isDirty()
185 | True if the model is modified or not saved.
186 |
187 | #### \#save([options, callback])
188 | Saves the model to data store. To check for result, you can use either `callback` or `then`. Callback arguments are `(err, response, model)`. Properties for `options` is the same with `m.request`'s options but with additional `path` string property. `path` is the path to actual value for the model in the response object. Like in `response:{outer:{model:{}}}` will be `"outer.model"`.
189 | ```javascript
190 | user.save()
191 | .then(
192 | function (model) { console.log('Saved') },
193 | function (err) { console.log(err) }
194 | )
195 | .catch(
196 | function(catchErr) { console.log(catchErr) }
197 | )
198 | ```
199 |
200 | #### \#fetch([options, callback])
201 | Fetches the model from data store. Model ID required to fetch. This method also accept `callback` or `then`. Properties for `options` is the same with `#save()`'s options. If `placeholder` is given in the configuration, the returned value will be that placeholder string.
202 | ```javascript
203 | user.id('abc123')
204 | user.fetch().then( function (model) { /* Success! model now have other prop values */ } );
205 | // If placeholder is set as 'Loading...'
206 | user.name(); // Returns `Loading...`, until the fetch completed
207 | ```
208 |
209 | #### \#destroy([options, callback])
210 | Destroys the model from data store and triggers `remove` method. Also accept `callback` or `then`. Parameter `options` is the same with `#save()`'s options.
211 |
212 | #### \#populate()
213 | Populates all references. This will trigger fetch if necessary.
214 |
215 | #### \#\()
216 | Model includes few methods of Lodash. `has`, `keys`, `values`, `pick`, and `omit`. See **Lodash** for info.
217 | ```javascript
218 | userA.pick(['name', 'age'])
219 | // Returns an object with only two properties `name` and `age`, excluding others.
220 | ```
221 |
222 | - - - -
223 |
224 | ## Collection
225 | ```javascript
226 | var userCollection = new md.Collection({
227 | model : User,
228 | url : '/usercollectionurl',
229 | redraw : true
230 | })
231 | userCollection.add(new User())
232 | ```
233 |
234 | #### new Collection([options])
235 |
236 | All available collection options:
237 | * **model** - (model constructor) the associated model to the collection
238 | * **url** - (string) the specific url of the collection. defaults to associated model's `name`
239 | * **redraw** - (boolean) trigger a redraw when the collection is updated. Defaults to `false`
240 | * **state** - (State | object | array) set a state factory (View-Model) for the collection. See `#stateOf()` method and `md.State()` for more info.
241 |
242 | > A collection with redraw = `true` will always trigger a `redraw` even though the contained model has redraw = `false`.
243 |
244 | > Omitted `model` in option is allowed and will make the collection `generic`. Therefore, some methods will NOT be available, like `create` and `fetch`.
245 |
246 | #### \#opt(key[, value])
247 | Sets an option(s) to the collection. See `Collection` for list of options.
248 |
249 | #### \#add(model[, unshift, silent])
250 | Adds a model to the collection. Optionally, you can add at the beginning with `unshift` = `true` and silently with `silent` = `true`.
251 |
252 | #### \#addAll(models[, unshift, silent])
253 | Adds an array of models to the collection. Optionally, you can set `unshift` and `silent` as well.
254 |
255 | #### \#create(objects)
256 | Create and add multiple models to the collection from passed array of objects.
257 | ```javascript
258 | userCollection.create([ {name:'Foo'}, {name:'Bar'} ])
259 | ```
260 |
261 | #### \#get(mixed)
262 | Get a model from collection. Argument `mixed` can be a `number`, `string`, `object` or `model` instance. Returns the first matched only otherwise `undefined`.
263 | ```javascript
264 | userCollection.get('abc')
265 | userCollection.get(123) // If string or number, it will find by Id
266 | userCollection.get({name:'Foo'}) // Will match the first model with name equal to `Foo`
267 | userCollection.get(model) // Will find by model instance, compared with Lodash's `indexOf`
268 | ```
269 |
270 | #### \#getAll(mixedArr[, falsy])
271 | Get multiple models from collection. Array can contain `mixed` type, same with `get()`. Returns an array of first matched only of each array element. Argument `falsy` will include falsy value like `undifened` in the list, instead of omitting.
272 |
273 | #### \#remove(mixed[, silent])
274 | Removes a model from collection. `mixed` can be same with `get()`'s mixed argument.
275 |
276 | #### \#push(model[, silent])
277 | Adds a model or array of models at the end.
278 |
279 | #### \#unshift(model[, silent])
280 | Adds a model or array of models at the beginning.
281 |
282 | #### \#shift([silent])
283 | Removes the model at the beginning.
284 |
285 | #### \#pop([silent])
286 | Removes the model at the end.
287 |
288 | #### \#clear([silent])
289 | Removes ALL models.
290 |
291 | #### \#sort(props[, orders])
292 | Sort the collection. Argument `props` is an array of props to sort and `orders` is an array of `asc` or `desc`. Optioanally, if you're sorting only single prop, you can pass a string instread of array.
293 | ```javascript
294 | // Sort the collection by `name` in default order `asc`
295 | userCollection.sort('name')
296 | // Sort the collection by `name` in `desc` order
297 | userCollection.sort('name', 'desc')
298 | // Sort the collection by `age` first starting from old (desc) and then `name`
299 | userCollection.sort(['age', 'name'], ['desc', 'asc'])
300 | ```
301 |
302 | #### \#sortByOrder(order , path )
303 | Sort the collection by giver order.
304 |
305 | #### \#pluck()
306 | Pluck a prop from each model.
307 | ```javascript
308 | userCollection.pluck('id')
309 | // Returns [123, 456, 789]
310 | ```
311 |
312 | #### \#contains(mixed)
313 | Returns `true` if the model contains in the collection, otherwise `false`. Argument `mixed` is the same with `get()` method.
314 |
315 | #### \#reserve()
316 | Reverse the order of the collection.
317 |
318 | #### \#randomize()
319 | Randomize the order of the collection.
320 |
321 | #### \#model()
322 | Get the associated model constructor.
323 |
324 | #### \#stateOf(mixed)
325 | Get the state of a model in the collection. Argument `mixed` is the same with `get()` method.
326 | ```javascript
327 | // Set state signature on creating collection
328 | var col = new md.Collection({
329 | state : {
330 | isEditing: true,
331 | isLoading: false
332 | }
333 | })
334 | // Create user
335 | var user = new User()
336 | // Add user to collection
337 | col.add(user);
338 | // Retrieving state value
339 | col.stateOf(user).isEditing() // Returns `true`
340 | // Setting state
341 | col.stateOf(user).isEditing(false) // Sets and returns `false`
342 | ```
343 |
344 | #### \#url()
345 | Get the url.
346 |
347 | #### \#fetch(query[, options, callback])
348 | Query to data store and populate the collection. Callback arguments are `(err, response, models)`. Properties for `options` is the same with `m.request`'s options but with additional `path` and `clear` property. `path` is the path to actual array of items for the collection in the response object. Like in `response:{outer:{items:[]}}` will be `"outer.items"`. And `clear` will clear the collection before placing the fetched data.
349 | ```javascript
350 | userCollection.fetch({ age : 30 }).then(function (){
351 | // Success! `userCollection` now have models with age 30
352 | })
353 | ```
354 |
355 | #### \#hasModel()
356 | Returns `true` if the collection has associated model, otherwise `false`.
357 |
358 | #### \#destroy()
359 | Destroys the collection. Trigger `clear` and `dispose`.
360 |
361 | #### \#isFetching()
362 | Checks if the collection is fetching.
363 |
364 | #### \#\()
365 | Collection includes several methods of Lodash. `forEach`, `map`, `find`, `findIndex`, `findLastIndex`, `filter`, `reject`, `every`, `some`, `invoke`, `maxBy`, `minBy`, `sortBy`, `groupBy`, `shuffle`, `size`, `initial`, `without`, `indexOf`, `lastIndexOf`, `difference`, `sample`, `reverse`, `nth`, `first`, `last`, `toArray`, `slice`, `orderBy`, `transform`. See **Lodash** for info.
366 | ```javascript
367 | var filtered = userCollection.filter({age: 30})
368 | // Returns an array of models with age of 30.
369 | ```
370 |
371 | - - - -
372 |
373 | ## State
374 | Also known as View-Model. See Mithril's view-model description for more info.
375 | ```javascript
376 | var _isEditing = md.stream(false)
377 | // Create state factory
378 | var stateFactory = new md.State({
379 | isLoading: false,
380 | isEditing: _isEditing // Add exisiting stream created somewhere
381 | })
382 | // Creating states
383 | stateFactory.set('A');
384 | stateFactory.set('B');
385 | // Using states
386 | var component = {
387 | controller: function() {
388 | this.stateA = stateFactory.get('A')
389 | this.stateA.isEditing(true)
390 | var s = this.stateA.isEditing.map(callback) // Using with stream
391 | },
392 | view: function(ctrl) {
393 | return m('div', 'Is editing ' + ctrl.stateA.isEditing()) // Displays `Is editing true`
394 | }
395 | }
396 | ```
397 |
398 | #### new State(signature[, options])
399 | Creates a new State factory. `signature` can be object or array.
400 |
401 | All available state options:
402 | * **store** - (function) the custom store function (it must return a function). defaults to `stream` that was set in `md.config`
403 | * **prefix** - (string) the string prefix for custom store.
404 |
405 | #### \#set(key)
406 | Internally creates a new state by `key`.
407 |
408 | #### \#get(key)
409 | Get the state by `key`.
410 |
411 | #### \#remove(key)
412 | Removes the state by `key`.
413 |
414 | #### md.State.create(signature[, options])
415 | Creates a state without instantiating new State factory. `options` is same with state factory constructor.
416 | ```javascript
417 | // Create state factory
418 | var state = md.State.create({
419 | isWorking: false
420 | })
421 | state.isWorking() // Get state. => false
422 | state.isWorking(true) // Set state. => true
423 | ```
424 |
425 | #### md.State.assign(object, signature[, options])
426 | Same with `create` but assigns to given `object`, instead of creating new object.
427 | ```javascript
428 | // Existing object
429 | var obj = {};
430 | // Create state factory
431 | md.State.assign(obj, {
432 | isWorking: false
433 | })
434 | obj.isWorking()
435 | ```
436 |
437 | - - - -
438 |
439 | ## Configure & Customize
440 | Configuration must be set before using any of `mithril-data`'s functionality.
441 | ```javascript
442 | md.config({
443 | baseUrl : '/baseurl',
444 | keyId : '_id', // mongodb
445 | store : customStoreFunction
446 | })
447 | ```
448 | All available config options:
449 | * **baseUrl** - (string) the base url
450 | * **keyId** - (string) the custom ID of the model. defaults to `id`
451 | * **redraw** - (boolean) the global redraw flag. default to `false`
452 | * **cache** - (boolean) enable caching the models created by a collection. defaults to `false`
453 | * **modelMethods** - (object { methodName : `function()` }) additional methods to bind to `model`'s prototype
454 | * **collectionMethods** - (object { methodName : `function()` }) additional methods to bind to `collection`'s prototype
455 | * **modelBindMethods** - (string array) model's methods to bind to itself. see Lodash `bindAll()`
456 | * **collectionBindMethods** - (string array) collection's methods to bind to itself
457 | * **storeConfigOptions** - (function) a function to `manipulate the options` before sending data to data-store
458 | * **storeConfigXHR** - (function) a function to `manipulate XHR` before sending data to data-store. see Mithril's `m.request` for more info
459 | * **storeExtract** - (function) a function to trigger after receiving data from data-store. see Mithril's `m.request` for more info
460 | * **storeSerializer** - (function) a function that overrides data-store serializer. see Mithril's `m.request` for more info
461 | * **storeDeserializer** - (function) a function that overrides data-store deserializer. see Mithril's `m.request` for more info
462 | * **store** - (function) a function that handles the storing of data. defaults to `m.request`
463 | * **stream** - (function) a function that handles the model props as well as md's State class. defaults to Mithril's `Stream`
464 | * **cache** - (boolean) should use cache or not in all collections. defaults to `false`
465 | * **cacheLimit** - (number) limit of cache. defaults to `100`
466 | * **placeholder** - (string) the string to return by prop when the model fetching or collection through `isFetching` method
467 |
468 | #### storeConfigOptions
469 | This is useful when you want to modify the `options` object before sending to data-store. One scenario is to create custom url instead of default generated url.
470 | ```javascript
471 | user.id('abc123')
472 | user.fetch()
473 | // The default url is `/user?id=abc123` but we want `/user/abc123
474 | md.config({ storeConfigOptions : function (options) {
475 | options.url = options.url + '/' + options.data.id
476 | options.data = null // clear the data as we've used it already
477 | }})
478 | ```
479 |
480 | #### store
481 | A function responsible for storing the data (defaults to `m.request`). An example is, if you want to store data using local storage.
482 | ```javascript
483 | var fnLocalStorage = function (data) {
484 | if (data.method === 'POST') { /* Writing data... */ }
485 | else if (data.method === 'GET') { /* Reading data... */ }
486 | else if (data.method === 'DELETE') { /* Deleting data... */ }
487 | else { /* Do something with other methods */ }
488 | }
489 | md.config({ store : fnLocalStorage})
490 | ```
491 | > Just make sure that your custom store should return a Promise.
492 |
493 | - - - -
494 |
495 | ## More
496 |
497 | #### md.store
498 | A handy tool that handles request to data-store. The result is through `then` / `catch`.
499 | * **request(url[, method, data, opt])** - creates a request to data-store. the `opt` will override the options when storing to data-store
500 | * **get(url[, data, opt])** - calls`request` with `GET` method, passing the `data` and `opt`
501 | * **post(url[, data, opt])** - calls`request` with `POST` method, passing the `data` and `opt`
502 | * **destroy(url[, data, opt])** - calls`request` with `DELETE` method, passing the `data` and `opt`
503 |
504 | #### md.stream
505 | Expose Mithril's Stream (unmodified and only bundled).
506 |
507 | #### md.toStream
508 | A helper function to convert any value to stream.
509 |
510 | #### md.model.get(name)
511 | A way to get a model constructor from other scope. Argument `name` is the model name.
512 |
513 | #### md.defaultConfig(config)
514 | Overrides the default config.
515 |
516 | #### md.resetConfig()
517 | Resets the config to default. If `defaultConfig()` is used, it will reset to that config.
518 |
519 | #### md.noConflict()
520 | Return the old reference to `md`.
521 |
522 | #### md.version()
523 | Return the current version.
524 |
525 | - - - -
526 |
527 | ## Installation
528 | ```sh
529 | # NPM
530 | npm install mithril-data
531 | # Bower
532 | bower install mithril-data
533 | ```
534 | Node / CommonJS:
535 | ```javascript
536 | var md = require('mithril-data');
537 | ```
538 | HTML: (`md` is automatically exposed to browser's global scope)
539 | ```html
540 |
541 |
542 |
543 |
546 | ```
547 |
548 | - - - -
549 |
550 | ### License
551 | MIT
552 |
--------------------------------------------------------------------------------
/mithril-data.min.js:
--------------------------------------------------------------------------------
1 | /**
2 | * mithril-data v0.4.9
3 | * A rich data model library for Mithril javascript framework.
4 | * https://github.com/rhaldkhein/mithril-data
5 | * (c) 2018 Kevin Villanueva
6 | * License: MIT
7 | */
8 | !function(t){function e(i){if(n[i])return n[i].exports;var o=n[i]={exports:{},id:i,loaded:!1};return t[i].call(o.exports,o,o.exports,e),o.loaded=!0,o.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}([function(t,e,n){function i(t){function e(e,n){l.call(this,n);var i=(this.__options.parse?this.options.parser(e):e)||{},o=t.props;a.indexOf(o,c.keyId)===-1&&o.push(c.keyId);for(var r,s=0;s-1&&o.pull(this.__collections,t)},set:function(t,e,n,i){o.isString(t)?this[t](e,n):this.setObject(t,n,i)},setObject:function(t,e,n){var r=t instanceof i;if(!r&&!o.isPlainObject(t))throw new Error("Argument `obj` must be a model or plain object.");for(var s,a,u=!r&&this.__options.parse?this.options.parser(t):t,c=o.keys(u),h=c.length-1;h>=0;h--)s=c[h],a=u[s],this.__isProp(s)&&o.isFunction(this[s])&&(r&&o.isFunction(a)?this[s](a(),!0,n):this[s](a,!0,n));e||this.__update()},get:function(t){return t?this[t]():this.getCopy()},getJson:function(){return this.__json},getCopy:function(t,e){for(var n,r,s={},a=o.keys(this.__json),u=0;u0},isFetching:function(){return 1===this.__working},isSaving:function(){return 2===this.__working},isDestroying:function(){return 3===this.__working},__update:function(){for(var t,e=0;e-1},__getDataId:function(){var t={};return t[s.keyId]=this.id(),t},__addToCache:function(){this.constructor.__options.cache&&!this.constructor.__cacheCollection.contains(this)&&this.__saved&&this.constructor.__cacheCollection.add(this)},__gettersetter:function(t,e){function n(){var t,n;if(arguments.length){t=arguments[0];var c=u.options.refs[e];if(c){var h=a[c];o.isPlainObject(t)?t=h.create(t):(o.isString(t)||o.isNumber(t))&&h.__cacheCollection&&(t=h.__cacheCollection.get(t)||t)}return t instanceof i&&(t.__saved=t.__saved||!(!arguments[2]||!t.id()),t=t.getJson()),r(t),u.__modified=!arguments[2]&&(!arguments[3]&&u.__json[e]!==r._state.value),u.__json[e]=r._state.value,arguments[1]||u.__update(e),t}return t=r(),n=u.options&&u.options.defaults[e],o.isNil(t)?o.isNil(n)||(t=n):t.__model instanceof i?t=t.__model:o.isPlainObject(t)&&n instanceof i&&(r(null),t=n),s.placeholder&&u.isFetching()&&e!==s.keyId&&o.indexOf(u.options.placehold,e)>-1?s.placeholder:t}var r=s.stream(),u=this;return n.stream=r,n.call(this,t,!0,void 0,!0),n}},c.addMethods(i.prototype,o,f,"__json")},function(t,e,n){function i(t,e){c.storeConfigXHR&&c.storeConfigXHR(t,e),t.setRequestHeader("Content-Type","application/json")}function o(t,e){return c.storeExtract?c.storeExtract(t,e):t.responseText.length?s(t.responseText):null}function r(t){return a(t),c.storeSerializer?c.storeSerializer(t):JSON.stringify(t)}function s(t){if(c.storeDeserializer)return c.storeDeserializer(t);try{return JSON.parse(t)}catch(e){return t}}function a(t){var e;for(var n in t)e=t[n],u.isObject(e)&&(t[n]=e[c.keyId]||e)}var u=n(1),c=n(3).config,h=n(4);t.exports=u.create(null,{request:function(t,e,n,a){var l={method:e||"GET",url:t,data:(n instanceof h?n.getCopy():n)||{},background:!!c.storeBackground,serialize:r,deserialize:s,config:i,extract:o};return a&&(l=u.defaultsDeep(a,l)),c.storeConfigOptions&&c.storeConfigOptions(l),c.store(l)},get:function(t,e,n){return this.request(t,"GET",e,n)},post:function(t,e,n){return this.request(t,"POST",e,n)},put:function(t,e,n){return this.request(t,"PUT",e,n)},destroy:function(t,e,n){return this.request(t,"DELETE",e,n)}})},function(t,e,n){(function(e){function i(t,e){return function(n,i,o,r){return t(n?n[e]||n:n,i?i[e]||i:i,o,r)}}function o(t,e){for(var n,o=t.length-1;o>=0;o--)n=t[o],a.isFunction(n)?t[o]=i(n,e):n instanceof c&&(t[o]=n.__json);return t}function r(t,e,n){if(t===e)return t;if(a.isArray(t)){for(var i,o=t.length-1;o>=0;o--)i=t[o],i&&i[n]&&(t[o]=i[n]);return t}return t?t[n]||t:t}function s(){return h&&window.setImmediate?window.setImmediate:"object"==typeof e&&"function"==typeof e.nextTick?e.nextTick:function(t){setTimeout(t,0)}}var a=n(1),u=Array.prototype.slice,c=n(4),h="undefined"!=typeof window;t.exports=a.create(null,{isBrowser:h,nextTick:s(),clearObject:function(t){for(var e in t)delete t[e]},hasValueOfType:function(t,e){for(var n=a.keys(t),i=0;i1)for(var n=1;n-1;if(o.isObject(t)){if(!t[a.keyId])return this.findIndex(t)>-1;t=t[a.keyId]}return this.findIndex([a.keyId,t])>-1},sort:function(t,e){o.isArray(t)||(t=[t]);var n;e?(o.isArray(e)||(e=[e]),n=this.orderBy(t,e)):n=this.orderBy(t),this.__replaceModels(n)},sortByOrder:function(t,e){e||(e=a.keyId),this.__replaceModels(this.sortBy([function(n){return t.indexOf(o.get(n,e))}]))},randomize:function(){this.__replaceModels(this.shuffle())},hasModel:function(){return!!this.__options.model},model:function(){return this.__options.model},url:function(t){var e=t?"":a.baseUrl;return this.__options.url?e+=this.__options.url:this.hasModel()&&(e+=this.model().modelOptions.url||"/"+this.model().modelOptions.name.toLowerCase()),e},fetch:function(t,e,n){o.isFunction(e)&&(n=e,e=void 0);var i=this;if(i.__working=!0,i.hasModel())return e=e||{},i.model().pull(i.url(),t,e,function(t,r,s){i.__working=!1,t?o.isFunction(n)&&n(t):(e.clear&&i.clear(!0),i.addAll(s),o.isFunction(n)&&n(null,r,s))});i.__working=!1;var r=new Error("Collection must have a model to perform fetch");return o.isFunction(n)&&n(r),Promise.reject(r)},isWorking:function(){return this.__working},isFetching:function(){return this.isWorking()},__replaceModels:function(t){for(var e=t.length-1;e>=0;e--)this.models[e]=t[e].__json},__update:function(t){if(!this.__options._cache&&(this.__options.redraw||a.redraw))return t||r.redraw(),!0}};var h=[],l={difference:1,every:1,find:1,findIndex:1,findLastIndex:1,filter:1,first:0,forEach:1,indexOf:2,initial:0,invoke:3,groupBy:1,last:0,lastIndexOf:2,map:1,maxBy:1,minBy:1,nth:1,orderBy:2,partition:1,reduce:-1,reject:1,reverse:0,sample:0,shuffle:0,size:0,slice:2,sortBy:1,some:1,transform:2,toArray:0,without:1};s.addMethods(i.prototype,o,l,"models","__model")},function(t,e,n){function i(){var t={};for(var e in this)s.indexOf(c,e)===-1&&(t[e]=this[e]());return t}function o(t,e,n,i){var o,r=n&&n.store?n.store:a.stream;for(var u in t){if(s.indexOf(c,u)>-1)throw new Error("State key `"+u+"` is not allowed.");o=t[u],e[u]=s.isFunction(o)?o:r(o,u,i,n)}return e}function r(t,e){if(s.isArray(t)){this.signature=s.invert(t);for(var n in this.signature)this.signature[n]=void 0}else this.signature=t;this._options=e,this.map={}}var s=n(1),a=n(3).config,u="__key__",c=["factory","toJson","_options"];t.exports=r,r.create=function(t,e){return o(t,{toJson:i},e)},r.assign=function(t,e,n){o(e,t,n)},r.prototype={set:function(t){return t||(t=u),this.map[t]||(this.map[t]=o(this.signature,{factory:a.stream(this),toJson:i},this._options,t)),this.map[t]},get:function(t){return t||(t=u),this.map[t]||this.set(t),this.map[t]},remove:function(t){if(t||(t=u),this.map[t]){var e,n=s.keys(this.map[t]);for(e=0;ethis.__options.cacheLimit&&this.__cacheCollection.shift())):n=new this(t,e),n},createCollection:function(t){return new a(o.assign({model:this},t))},createModels:function(t,e){o.isArray(t)||(t=[t]);for(var n=[],i=0;i0&&arguments[0]!==x&&i(t,arguments[0]),t._state.value}return n(t),arguments.length>0&&arguments[0]!==x&&i(t,arguments[0]),t}function n(t){t.constructor=e,t._state={id:O++,value:void 0,state:0,derive:void 0,recover:void 0,deps:{},parents:[],endStream:void 0,unregister:void 0},t.map=t["fantasy-land/map"]=l,t["fantasy-land/ap"]=f,t["fantasy-land/of"]=e,t.valueOf=d,t.toJSON=_,t.toString=d,Object.defineProperties(t,{end:{get:function(){if(!t._state.endStream){var n=e();n.map(function(e){return e===!0&&(h(t),n._state.unregister=function(){h(n)}),e}),t._state.endStream=n}return t._state.endStream}}})}function i(t,e){o(t,e);for(var n in t._state.deps)r(t._state.deps[n],!1);null!=t._state.unregister&&t._state.unregister(),s(t)}function o(t,e){t._state.value=e,t._state.changed=!0,2!==t._state.state&&(t._state.state=1)}function r(t,e){var n=t._state,i=n.parents;if(i.length>0&&i.every(m)&&(e||i.some(v))){var r=t._state.derive();if(r===x)return!1;o(t,r)}}function s(t){t._state.changed=!1;for(var e in t._state.deps)t._state.deps[e]._state.changed=!1}function a(t,n){if(!n.every(p))throw new Error("Ensure that each item passed to stream.combine/stream.merge is a stream");return u(e(),n,function(){return t.apply(this,n.concat([n.filter(v)]))})}function u(t,e,n){var i=t._state;return i.derive=n,i.parents=e.filter(g),c(t,i.parents),r(t,!0),t}function c(t,e){for(var n=0;n-1&&o._state.parents.splice(r,1)}t._state.state=2,t._state.deps={}}function l(t){return a(function(e){return t(e())},[this])}function f(t){return a(function(t,e){return t()(e())},[t,this])}function d(){return this._state.value}function _(){return null!=this._state.value&&"function"==typeof this._state.value.toJSON?this._state.value.toJSON():this._state.value}function p(t){return t._state}function m(t){return 1===t._state.state}function v(t){return t._state.changed}function g(t){return 2!==t._state.state}function y(t){return a(function(){return t.map(function(t){return t()})},t)}function w(t,e,n){var i=a(function(n){return e=t(e,n._state.value)},[n]);return 0===i._state.state&&i(e),i}function k(t,e){var n=t.map(function(t){var e=t[0];return 0===e._state.state&&e(void 0),e}),i=a(function(){var i=arguments[arguments.length-1];return n.forEach(function(n,o){i.indexOf(n)>-1&&(e=t[o][1](e,n._state.value))}),e},n);return i}var O=0,x={};e["fantasy-land/of"]=e,e.merge=y,e.combine=a,e.scan=w,e.scanMerge=k,e.HALT=x,t.exports=e}()}).call(e,n(13)(t))},function(t,e){t.exports=function(t){return t.webpackPolyfill||(t.deprecate=function(){},t.paths=[],t.children=[],t.webpackPolyfill=1),t}}]);
--------------------------------------------------------------------------------
/test/www/bundles/chai/chai-plugins.bundle.js:
--------------------------------------------------------------------------------
1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o
462 | * MIT Licensed
463 | */
464 |
465 | /**
466 | * ### .checkError
467 | *
468 | * Checks that an error conforms to a given set of criteria and/or retrieves information about it.
469 | *
470 | * @api public
471 | */
472 |
473 | /**
474 | * ### .compatibleInstance(thrown, errorLike)
475 | *
476 | * Checks if two instances are compatible (strict equal).
477 | * Returns false if errorLike is not an instance of Error, because instances
478 | * can only be compatible if they're both error instances.
479 | *
480 | * @name compatibleInstance
481 | * @param {Error} thrown error
482 | * @param {Error|ErrorConstructor} errorLike object to compare against
483 | * @namespace Utils
484 | * @api public
485 | */
486 |
487 | function compatibleInstance(thrown, errorLike) {
488 | return errorLike instanceof Error && thrown === errorLike;
489 | }
490 |
491 | /**
492 | * ### .compatibleConstructor(thrown, errorLike)
493 | *
494 | * Checks if two constructors are compatible.
495 | * This function can receive either an error constructor or
496 | * an error instance as the `errorLike` argument.
497 | * Constructors are compatible if they're the same or if one is
498 | * an instance of another.
499 | *
500 | * @name compatibleConstructor
501 | * @param {Error} thrown error
502 | * @param {Error|ErrorConstructor} errorLike object to compare against
503 | * @namespace Utils
504 | * @api public
505 | */
506 |
507 | function compatibleConstructor(thrown, errorLike) {
508 | if (errorLike instanceof Error) {
509 | // If `errorLike` is an instance of any error we compare their constructors
510 | return thrown.constructor === errorLike.constructor || thrown instanceof errorLike.constructor;
511 | } else if (errorLike.prototype instanceof Error || errorLike === Error) {
512 | // If `errorLike` is a constructor that inherits from Error, we compare `thrown` to `errorLike` directly
513 | return thrown.constructor === errorLike || thrown instanceof errorLike;
514 | }
515 |
516 | return false;
517 | }
518 |
519 | /**
520 | * ### .compatibleMessage(thrown, errMatcher)
521 | *
522 | * Checks if an error's message is compatible with a matcher (String or RegExp).
523 | * If the message contains the String or passes the RegExp test,
524 | * it is considered compatible.
525 | *
526 | * @name compatibleMessage
527 | * @param {Error} thrown error
528 | * @param {String|RegExp} errMatcher to look for into the message
529 | * @namespace Utils
530 | * @api public
531 | */
532 |
533 | function compatibleMessage(thrown, errMatcher) {
534 | var comparisonString = typeof thrown === 'string' ? thrown : thrown.message;
535 | if (errMatcher instanceof RegExp) {
536 | return errMatcher.test(comparisonString);
537 | } else if (typeof errMatcher === 'string') {
538 | return comparisonString.indexOf(errMatcher) !== -1; // eslint-disable-line no-magic-numbers
539 | }
540 |
541 | return false;
542 | }
543 |
544 | /**
545 | * ### .getFunctionName(constructorFn)
546 | *
547 | * Returns the name of a function.
548 | * This also includes a polyfill function if `constructorFn.name` is not defined.
549 | *
550 | * @name getFunctionName
551 | * @param {Function} constructorFn
552 | * @namespace Utils
553 | * @api private
554 | */
555 |
556 | var functionNameMatch = /\s*function(?:\s|\s*\/\*[^(?:*\/)]+\*\/\s*)*([^\(\/]+)/;
557 | function getFunctionName(constructorFn) {
558 | var name = '';
559 | if (typeof constructorFn.name === 'undefined') {
560 | // Here we run a polyfill if constructorFn.name is not defined
561 | var match = String(constructorFn).match(functionNameMatch);
562 | if (match) {
563 | name = match[1];
564 | }
565 | } else {
566 | name = constructorFn.name;
567 | }
568 |
569 | return name;
570 | }
571 |
572 | /**
573 | * ### .getConstructorName(errorLike)
574 | *
575 | * Gets the constructor name for an Error instance or constructor itself.
576 | *
577 | * @name getConstructorName
578 | * @param {Error|ErrorConstructor} errorLike
579 | * @namespace Utils
580 | * @api public
581 | */
582 |
583 | function getConstructorName(errorLike) {
584 | var constructorName = errorLike;
585 | if (errorLike instanceof Error) {
586 | constructorName = getFunctionName(errorLike.constructor);
587 | } else if (typeof errorLike === 'function') {
588 | // If `err` is not an instance of Error it is an error constructor itself or another function.
589 | // If we've got a common function we get its name, otherwise we may need to create a new instance
590 | // of the error just in case it's a poorly-constructed error. Please see chaijs/chai/issues/45 to know more.
591 | constructorName = getFunctionName(errorLike).trim() ||
592 | getFunctionName(new errorLike()); // eslint-disable-line new-cap
593 | }
594 |
595 | return constructorName;
596 | }
597 |
598 | /**
599 | * ### .getMessage(errorLike)
600 | *
601 | * Gets the error message from an error.
602 | * If `err` is a String itself, we return it.
603 | * If the error has no message, we return an empty string.
604 | *
605 | * @name getMessage
606 | * @param {Error|String} errorLike
607 | * @namespace Utils
608 | * @api public
609 | */
610 |
611 | function getMessage(errorLike) {
612 | var msg = '';
613 | if (errorLike && errorLike.message) {
614 | msg = errorLike.message;
615 | } else if (typeof errorLike === 'string') {
616 | msg = errorLike;
617 | }
618 |
619 | return msg;
620 | }
621 |
622 | module.exports = {
623 | compatibleInstance: compatibleInstance,
624 | compatibleConstructor: compatibleConstructor,
625 | compatibleMessage: compatibleMessage,
626 | getMessage: getMessage,
627 | getConstructorName: getConstructorName,
628 | };
629 |
630 | },{}],4:[function(require,module,exports){
631 | window.chaiPromise = require('chai-as-promised');
632 | window.chaiSubset = require('chai-subset');
633 | },{"chai-as-promised":1,"chai-subset":2}]},{},[4]);
634 |
--------------------------------------------------------------------------------
/test/www/test-Model.js:
--------------------------------------------------------------------------------
1 | describe('Model Instance', function() {
2 | 'use strict';
3 |
4 | it('is instance of `BaseModel`', function() {
5 | var user = new Model.User();
6 | expect(user).to.be.instanceof(md.__TEST__.BaseModel);
7 | });
8 |
9 | it('create instance with DEFAULT prop values', function() {
10 | var note = new Model.Note({
11 | title: 'Foo'
12 | });
13 | expect(note.title()).to.be.equal('Foo');
14 | expect(note.body()).to.be.equal(Model.Note.modelOptions.defaults.body);
15 | expect(note.author()).to.be.equal(Model.Note.modelOptions.defaults.author);
16 | });
17 |
18 | it('create instance with defined prop values', function() {
19 | var user = new Model.User({
20 | name: 'Foo',
21 | age: 0,
22 | active: false
23 | });
24 | expect(user.name()).to.be.equal('Foo');
25 | expect(user.profile()).to.be.undefined;
26 | expect(user.age()).to.be.equal(0);
27 | expect(user.active()).to.be.equal(false);
28 | });
29 |
30 | it('create instance with defined prop values and reference model', function() {
31 | var noteA = new Model.Note({
32 | title: 'Foo',
33 | author: {
34 | name: 'Bar'
35 | }
36 | });
37 | expect(noteA.title()).to.be.equal('Foo');
38 | expect(noteA.author()).to.be.instanceof(md.__TEST__.BaseModel);
39 | expect(noteA.author().name()).to.be.equal('Bar');
40 |
41 | var userB = new Model.User({
42 | name: 'TestUser'
43 | });
44 | var noteB = new Model.Note({
45 | title: 'TestNote',
46 | author: userB
47 | });
48 | expect(noteB.title()).to.be.equal('TestNote');
49 | expect(noteB.author()).to.be.instanceof(md.__TEST__.BaseModel);
50 | expect(noteB.author().name()).to.be.equal('TestUser');
51 | });
52 |
53 | it('create instance with parser', function() {
54 | var noteA = new Model.Note({
55 | wrap: {
56 | title: 'Title',
57 | inner: {
58 | body: 'Body',
59 | author: 'Author'
60 | }
61 | }
62 | }, {
63 | parse: true
64 | });
65 | expect(noteA.title()).to.equal('Title');
66 | expect(noteA.body()).to.equal('Body');
67 | expect(noteA.author()).to.equal('Author');
68 | });
69 |
70 | it('prop defaults to `undefined`', function() {
71 | var user = new Model.User();
72 | expect(user.profile()).to.be.undefined;
73 | });
74 |
75 | it('prop sets `null`', function() {
76 | var user = new Model.User();
77 | user.name(null);
78 | expect(user.name()).to.be.null;
79 | });
80 |
81 | it('prop sets `undefined`', function() {
82 | var user = new Model.User();
83 | user.name(undefined);
84 | expect(user.name()).to.be.undefined;
85 | });
86 |
87 | it('prop set and read the correct value', function() {
88 | var user = new Model.User();
89 | user.name('test');
90 | expect(user.name()).to.equal('test');
91 | });
92 |
93 | it('with stream property', function() {
94 | var user = new Model.User();
95 | user.name('Foo');
96 | expect(user.name).to.have.property('stream');
97 | expect(user.name.stream.constructor).to.be.equal(md.stream);
98 | });
99 |
100 | it('have all properties that was set in `props` with values of type function', function() {
101 | var props = Model.User.modelOptions.props;
102 | var user = new Model.User();
103 | for (var i = 0; i < props.length; i++) {
104 | expect(user).to.have.property(props[i]).and.to.be.a('function');
105 | }
106 | });
107 |
108 | it('create instance with options', function() {
109 | var user = new Model.User({
110 | name: 'Foo'
111 | }, {
112 | test: 'test'
113 | });
114 | expect(user).to.be.instanceof(Model.User);
115 | expect(user.__options.test).to.be.equal('test');
116 | });
117 |
118 | it('return the default value set in schema if value of prop is `undefined` or `null`', function() {
119 | var ModelUserTest = md.model({
120 | name: 'ModelUserTest',
121 | props: ['test'],
122 | defaults: {
123 | test: 'Foo'
124 | }
125 | });
126 | var user = new ModelUserTest();
127 | expect(user.test()).to.be.equal('Foo');
128 | user.test(undefined);
129 | expect(user.test()).to.be.equal('Foo');
130 | user.test(null);
131 | expect(user.test()).to.be.equal('Foo');
132 | });
133 |
134 | it('finds the reference if value is string or number (only for enabled cache)', function() {
135 |
136 | var CacheSenderModel = md.model({
137 | name: 'CacheSenderModel',
138 | props: ['name', 'age']
139 | }, {
140 | cache: true
141 | });
142 |
143 | var CacheEmailModel = md.model({
144 | name: 'CacheEmailModel',
145 | props: ['subject', 'body', 'sender'],
146 | refs: {
147 | sender: 'CacheSenderModel'
148 | }
149 | }, {
150 | cache: true
151 | });
152 |
153 | var modelsSender = CacheSenderModel.createModels([{
154 | id: 123,
155 | name: 'Foo'
156 | }, {
157 | id: '456',
158 | name: 'Bar'
159 | }]);
160 |
161 | var modelsEmail = CacheEmailModel.createModels([{
162 | subject: 'Baz',
163 | sender: '456'
164 | }, {
165 | subject: 'Test',
166 | sender: 123
167 | }]);
168 |
169 | // Note that the `first email` is referencing the `second sender`. Check the ID!
170 | expect(modelsEmail[0].sender()).to.be.equal(modelsSender[1]);
171 | expect(modelsEmail[1].sender()).to.be.equal(modelsSender[0]);
172 | expect(modelsEmail[0].sender().name()).to.be.equal('Bar');
173 | expect(modelsEmail[1].sender().name()).to.be.equal('Foo');
174 |
175 | });
176 |
177 | });
178 |
179 | describe('Model.', function() {
180 | 'use strict';
181 |
182 | it('`__lid` exist with value type of string', function() {
183 | var user = new Model.User();
184 | expect(user).to.have.property('__lid').and.to.be.a('string');
185 | });
186 |
187 | it('`__collections` exist with value type of array', function() {
188 | var user = new Model.User();
189 | expect(user).to.have.property('__collections').and.to.be.a('array');
190 | });
191 |
192 | it('`__options` exist with value type of object', function() {
193 | var user = new Model.User();
194 | expect(user).to.have.property('__options').and.to.be.a('object');
195 | });
196 |
197 | it('`__json` exist - *** the object representation of the model ***', function() {
198 | var user = new Model.User();
199 | expect(user).to.have.property('__json').and.to.be.a('object');
200 | });
201 |
202 | it('`__json` has `__model` property with value referencing to its model instance', function() {
203 | var user = new Model.User();
204 | expect(user.__json.__model).to.be.equal(user);
205 | });
206 |
207 |
208 | it('`__json` have all model properties (props)', function() {
209 | var props = Model.User.modelOptions.props;
210 | var user = new Model.User();
211 | for (var i = 0; i < props.length; i++) {
212 | expect(user.__json).to.have.property(props[i]);
213 | }
214 | });
215 |
216 | it('`__json` properties are equal to its model properties', function() {
217 | var props = Model.User.modelOptions.props;
218 | var user = new Model.User();
219 | user.profile(false);
220 | for (var i = 0; i < props.length; i++) {
221 | expect(user[props[i]]()).to.equal(user.__json[props[i]]);
222 | }
223 | });
224 |
225 | it('`__json` will update on change of prop', function() {
226 | var user = new Model.User();
227 | user.name('New Value');
228 | expect(user.__json.name).to.equal('New Value');
229 | });
230 |
231 | it('`__json` will update through `set()` or `setAll()`', function() {
232 | var user = new Model.User();
233 | user.set('name', 'foo');
234 | expect(user.__json.name).to.equal('foo');
235 | user.set({
236 | name: 'bar',
237 | profile: 'baz'
238 | });
239 | expect(user.__json.name).to.equal('bar');
240 | expect(user.__json.profile).to.equal('baz');
241 | });
242 |
243 | });
244 |
245 | describe('Model.', function() {
246 | 'use strict';
247 |
248 | describe('#opt()', function() {
249 | 'use strict';
250 |
251 | it('set options by plain object', function() {
252 | var user = new Model.User();
253 | user.opt({
254 | test: 'test'
255 | });
256 | expect(user.__options.test).to.be.equal('test');
257 | });
258 |
259 | it('set options by key/value`', function() {
260 | var user = new Model.User();
261 | user.opt('key', 'value');
262 | expect(user.__options.key).to.be.equal('value');
263 | });
264 |
265 | it('defaults to boolean true', function() {
266 | var user = new Model.User();
267 | user.opt('key');
268 | expect(user.__options.key).to.be.equal(true);
269 | });
270 |
271 | it('can set falsy except `undefined`', function() {
272 | var user = new Model.User();
273 | user.opt('key', false);
274 | expect(user.__options.key).to.be.false;
275 | });
276 |
277 | });
278 |
279 | describe('#lid()', function() {
280 | 'use strict';
281 |
282 | it('returns a string with value of its `__lid`', function() {
283 | var user = new Model.User();
284 | expect(user.lid()).to.be.a('string').and.equal(user.__lid);
285 | });
286 |
287 | });
288 |
289 | describe('#url()', function() {
290 | 'use strict';
291 |
292 | it('returns a string', function() {
293 | var user = new Model.User();
294 | expect(user.url()).to.be.a('string');
295 | });
296 |
297 | });
298 |
299 | describe('#attachCollection()', function() {
300 | 'use strict';
301 |
302 | it('property `__collections` contains the collection', function() {
303 | var collection = new md.Collection();
304 | var user = new Model.User();
305 |
306 | user.attachCollection(collection);
307 | expect(user.__collections).to.contain(collection);
308 | });
309 |
310 | it('property `__collections` has correct size', function() {
311 | var collection = new md.Collection();
312 | var user = new Model.User();
313 | var size = user.__collections.length;
314 |
315 | user.attachCollection(collection);
316 | expect(user.__collections.length).to.be.equal(++size);
317 |
318 | user.attachCollection(collection);
319 | expect(user.__collections.length).to.be.equal(size);
320 | });
321 |
322 | });
323 |
324 | describe('#detachCollection()', function() {
325 | 'use strict';
326 |
327 | it('property `__collections` does NOT contain the collection', function() {
328 | var collection = new md.Collection();
329 | var user = new Model.User();
330 |
331 | user.attachCollection(collection);
332 | expect(user.__collections).to.contain(collection);
333 |
334 | user.detachCollection(collection);
335 | expect(user.__collections).to.not.contain(collection);
336 |
337 | });
338 |
339 | it('property `__collections` has correct size', function() {
340 | var collection = new md.Collection();
341 | var user = new Model.User();
342 |
343 | user.attachCollection(collection);
344 | expect(user.__collections.length).to.be.equal(1);
345 |
346 | user.attachCollection(collection);
347 | expect(user.__collections.length).to.be.equal(1);
348 |
349 | user.detachCollection(collection);
350 | expect(user.__collections.length).to.be.equal(0);
351 | });
352 |
353 | });
354 |
355 | describe('#set()', function() {
356 | 'use strict';
357 |
358 | it('set by object', function() {
359 | var note = new Model.Note();
360 | var user = new Model.User();
361 | note.set({
362 | title: 'Foo',
363 | body: 'Bar',
364 | author: user
365 | });
366 | expect(note.title()).to.equal('Foo');
367 | expect(note.body()).to.equal('Bar');
368 | expect(note.author()).to.equal(user);
369 | });
370 |
371 | it('set by object with child object (reference model)', function() {
372 | var note = new Model.Note();
373 | note.set({
374 | title: 'Foo',
375 | body: 'Bar',
376 | author: {
377 | name: 'Test'
378 | }
379 | });
380 | expect(note.title()).to.equal('Foo');
381 | expect(note.body()).to.equal('Bar');
382 | expect(note.author()).to.instanceof(md.__TEST__.BaseModel);
383 | expect(note.author().name()).to.equal('Test');
384 | });
385 |
386 | it('set by object with parser', function() {
387 | var note = new Model.Note(null, {
388 | parse: true
389 | });
390 | note.set({
391 | wrap: {
392 | title: 'Foo',
393 | inner: {
394 | body: 'Bar',
395 | author: 'Authx'
396 | }
397 | }
398 | });
399 | expect(note.title()).to.equal('Foo');
400 | expect(note.body()).to.equal('Bar');
401 | expect(note.author()).to.equal('Authx');
402 | });
403 |
404 | it('set by key & value', function() {
405 | var note = new Model.Note();
406 | var user = new Model.User();
407 | note.set('title', 'Foo');
408 | note.set('body', 'Bar');
409 | note.set('author', user);
410 | expect(note.title()).to.equal('Foo');
411 | expect(note.body()).to.equal('Bar');
412 | expect(note.author()).to.equal(user);
413 | });
414 |
415 | it('set to undefined if no value is set', function() {
416 | var user = new Model.User();
417 | user.set({
418 | name: 'Foo',
419 | });
420 | user.set('profile');
421 | expect(user.name()).to.equal('Foo');
422 | expect(user.profile()).to.be.undefined;
423 | });
424 |
425 | it('set falsy value like `false` and `0` ', function() {
426 | var user = new Model.User();
427 | user.set({
428 | name: 'Foo',
429 | age: 0
430 | });
431 | user.set('active', false);
432 | expect(user.name()).to.equal('Foo');
433 | expect(user.age()).to.equal(0);
434 | expect(user.active()).to.equal(false);
435 | });
436 |
437 | it('throw error if key is invalid', function() {
438 | var user = new Model.User();
439 | expect(function() {
440 | user.set('noprop', 'Foo');
441 | }).to.throw(Error);
442 | });
443 |
444 | });
445 |
446 | describe('#setObject()', function() {
447 | 'use strict';
448 |
449 | it('successful set ', function() {
450 | var user = new Model.User();
451 | user.setObject({
452 | name: 'Foo',
453 | age: 32
454 | });
455 | expect(user.name()).to.equal('Foo');
456 | expect(user.age()).to.equal(32);
457 | });
458 |
459 | it('successful set with parser', function() {
460 | var note = new Model.Note(null, {
461 | parse: true
462 | });
463 | note.setObject({
464 | wrap: {
465 | title: 'Foo',
466 | inner: {
467 | body: 'Bar',
468 | author: 'Auth'
469 | }
470 | }
471 | });
472 | expect(note.title()).to.equal('Foo');
473 | expect(note.body()).to.equal('Bar');
474 | expect(note.author()).to.equal('Auth');
475 | });
476 |
477 | });
478 |
479 | describe('#get()', function() {
480 | 'use strict';
481 |
482 | it('return correct value', function() {
483 | var note = new Model.Note();
484 | var user = new Model.User();
485 | note.set({
486 | title: 'Foo',
487 | body: 'Bar',
488 | author: user
489 | });
490 | expect(note.get('title')).to.equal('Foo');
491 | expect(note.get('body')).to.equal('Bar');
492 | expect(note.get('author')).to.equal(user);
493 | });
494 |
495 | it('return a copy if NO key is specified (alias of .getCopy())', function() {
496 | var note = new Model.Note();
497 | var user = new Model.User();
498 | note.set({
499 | title: 'Foo',
500 | body: 'Bar',
501 | author: user
502 | });
503 | var copy = note.get();
504 | expect(copy).to.not.equal(note.getJson());
505 | expect(copy.title).to.equal('Foo');
506 | expect(copy.body).to.equal('Bar');
507 | expect(copy.author).to.eql(user.get());
508 | });
509 |
510 | });
511 |
512 | describe('#getJson()', function() {
513 | 'use strict';
514 |
515 | it('return correct value', function() {
516 | var user = new Model.User();
517 | user.set({
518 | name: 'Foo',
519 | profile: 'Bar'
520 | });
521 | expect(user.getJson()).to.equal(user.__json);
522 | expect(user.getJson().name).to.equal('Foo');
523 | expect(user.getJson().profile).to.equal('Bar');
524 | });
525 |
526 | });
527 |
528 | describe('#getCopy()', function() {
529 | 'use strict';
530 |
531 | it('really return a copy', function() {
532 | var user = new Model.User();
533 | user.set({
534 | name: 'Foo',
535 | profile: 'Bar'
536 | });
537 | var copy = user.getCopy();
538 | expect(copy).to.not.equal(user.getJson());
539 | });
540 |
541 | it('values are equal to the original', function() {
542 | var note = new Model.Note();
543 | var user = new Model.User();
544 | note.set({
545 | title: 'Foo',
546 | body: 'Bar',
547 | author: user
548 | });
549 | var copy = note.getCopy();
550 | expect(copy.title).to.equal(note.title());
551 | expect(copy.body).to.equal(note.body());
552 | expect(copy.author).to.eql(note.author().getCopy());
553 | });
554 |
555 | it('copy should not set the original or vice versa', function() {
556 | var note = new Model.Note();
557 | var user = new Model.User();
558 | note.set({
559 | title: 'Foo',
560 | body: 'Bar',
561 | author: user
562 | });
563 | var copy = note.getCopy();
564 | copy.title = 'Baz';
565 | expect(copy.title).to.not.equal(note.title());
566 | note.title('Test');
567 | expect(note.title()).to.not.equal(copy.title);
568 | });
569 |
570 | it('depopulate', function() {
571 | var note = new Model.Note();
572 | var user = new Model.User({
573 | id: 'id001'
574 | });
575 | note.set({
576 | title: 'Foo',
577 | body: 'Bar',
578 | author: user
579 | });
580 | // Default
581 | var copyA = note.getCopy();
582 | expect(copyA.author).to.be.object;
583 | // Depopulate
584 | var copyB = note.getCopy(false, true);
585 | expect(copyB.author).to.equal('id001');
586 | });
587 |
588 | });
589 |
590 |
591 | describe('#detach()', function() {
592 | 'use strict';
593 |
594 | it('detached from all collections', function() {
595 | var user = new Model.User();
596 | var collA = new md.Collection();
597 | var collB = new md.Collection();
598 | collA.add(user);
599 | user.attachCollection(collB);
600 | expect(user.__collections).to.contain(collA);
601 | expect(user.__collections).to.contain(collB);
602 | var arr = user.__collections;
603 | user.detach();
604 | expect(arr.length).to.be.equal(0);
605 | });
606 |
607 | });
608 |
609 | describe('#dispose()', function() {
610 | 'use strict';
611 |
612 | it('disposed', function() {
613 | var user = new Model.User();
614 | var keys = _.keys(user);
615 | user.dispose();
616 | for (var i = 0; i < keys.length; i++) {
617 | expect(user[keys[i]]).to.be.null;
618 | }
619 | });
620 |
621 | });
622 |
623 | describe('#remove()', function() {
624 | 'use strict';
625 |
626 | it('detached from all collections', function() {
627 | var user = new Model.User();
628 | var collA = new md.Collection();
629 | var collB = new md.Collection();
630 | collA.add(user);
631 | user.attachCollection(collB);
632 | expect(user.__collections).to.contain(collA);
633 | expect(user.__collections).to.contain(collB);
634 | var arr = user.__collections;
635 | user.remove();
636 | expect(arr.length).to.be.equal(0);
637 | });
638 |
639 | it('disposed', function() {
640 | var user = new Model.User();
641 | var keys = _.keys(user);
642 | user.remove();
643 | for (var i = 0; i < keys.length; i++) {
644 | expect(user[keys[i]]).to.be.null;
645 | }
646 | });
647 |
648 | });
649 |
650 | describe('#save()', function() {
651 | 'use strict';
652 |
653 | it('successful save (create)', function(done) {
654 | var user = new Model.User();
655 | user.name('Create');
656 | user.age(123);
657 | user.active(false);
658 | user.save().then(function(model) {
659 | try {
660 | expect(model).to.be.equal(user);
661 | expect(user.id().length).to.be.above(0);
662 | expect(user.name()).to.equal('Create');
663 | expect(user.profile()).to.be.undefined;
664 | expect(user.age()).to.equal(123);
665 | expect(user.active()).to.equal(false);
666 | expect(user.isSaved()).to.equal(true);
667 | done();
668 | } catch (e) {
669 | done(e);
670 | }
671 | }, function(err) {
672 | done(err)
673 | });
674 | });
675 |
676 | it('successful save (update)', function(done) {
677 | var user = new Model.User();
678 | user.name('Update');
679 | user.age(123);
680 | user.active(false);
681 | user.save().then(function(model) {
682 | try {
683 | expect(model).to.be.equal(user);
684 | expect(user.id().length).to.be.above(0);
685 | expect(user.name()).to.equal('Update');
686 | expect(user.age()).to.equal(123);
687 | user.name('Updated!');
688 | user.age(456);
689 | return user.save();
690 | } catch (e) {
691 | done(e);
692 | }
693 | }, function(err) {
694 | done(err)
695 | }).then(function(model) {
696 | try {
697 | expect(model).to.be.equal(user);
698 | expect(user.id().length).to.be.above(0);
699 | expect(user.name()).to.equal('Updated!');
700 | expect(user.age()).to.equal(456);
701 | expect(user.active()).to.equal(false);
702 | expect(user.profile()).to.be.undefined;
703 | done();
704 | } catch (e) {
705 | done(e);
706 | }
707 | }, function(err) {
708 | done(err)
709 | });
710 | });
711 |
712 | it('save result through callback', function(done) {
713 | var user = new Model.User();
714 | user.name('Callback');
715 | user.save(function(err, response, model) {
716 | if (err) {
717 | done(err);
718 | return;
719 | }
720 | try {
721 | expect(response.id.length).to.be.above(0);
722 | expect(response.name).to.equal('Callback');
723 | expect(model).to.be.equal(user);
724 | expect(user.id().length).to.be.above(0);
725 | expect(user.name()).to.equal('Callback');
726 | done();
727 | } catch (e) {
728 | done(e);
729 | }
730 | });
731 | });
732 |
733 | });
734 |
735 | describe('#fetch()', function() {
736 | 'use strict';
737 |
738 | var user;
739 |
740 | before(function(done) {
741 | user = new Model.User();
742 | user.name('Hello');
743 | user.profile('World');
744 | user.save().then(function(model) {
745 | done()
746 | }, function(err) {
747 | done(err);
748 | })
749 | });
750 |
751 | it('successful fetch', function(done) {
752 | var existingUser = new Model.User();
753 | existingUser.id(user.id());
754 | existingUser.fetch().then(function(model) {
755 | // Fetch result : resolve.
756 | try {
757 | expect(model.id()).to.be.equal(user.id());
758 | expect(model.name()).to.equal('Hello');
759 | expect(model.profile()).to.equal('World');
760 | expect(model.isSaved()).to.equal(true);
761 | expect(model.age()).to.be.undefined;
762 | expect(model.active()).to.be.undefined;
763 | done();
764 | } catch (e) {
765 | done(e);
766 | }
767 | }, function(err) {
768 | // Fetch result : reject.
769 | done(err);
770 | });
771 | });
772 |
773 | it('fetch result through callback', function(done) {
774 | var existingUser = new Model.User();
775 | existingUser.id(user.id());
776 | existingUser.fetch(function(err, response, model) {
777 | if (err) {
778 | done(err);
779 | return;
780 | }
781 | try {
782 | expect(response.id.length).to.be.above(0);
783 | expect(response.name).to.equal('Hello');
784 | expect(response.profile).to.equal('World');
785 | expect(model.id()).to.be.equal(user.id());
786 | expect(model.name()).to.equal('Hello');
787 | expect(model.profile()).to.equal('World');
788 | expect(model.isSaved()).to.equal(true);
789 | expect(model.age()).to.be.undefined;
790 | expect(model.active()).to.be.undefined;
791 | done();
792 | } catch (e) {
793 | done(e);
794 | }
795 | });
796 | });
797 |
798 | it('react to undefined result', function(done) {
799 | // Alarm model is configured to return `undefined` in `test-md.js`
800 | var undefAlarm = new Model.Alarm();
801 | undefAlarm.id('123');
802 | undefAlarm.fetch().then(function(mdl) {
803 | // Values are default values from model
804 | expect(mdl.title()).to.equal('Default Alarm Title');
805 | expect(mdl.time()).to.equal('8:00 AM');
806 | expect(mdl.isSaved()).to.be.false;
807 | done();
808 | });
809 | });
810 |
811 | it('falsy id (missing, string `undefined` and `null`', function(done) {
812 | var falsyIdAlarm = new Model.Alarm();
813 | // Fetching wihtout id should reject
814 | falsyIdAlarm.fetch()
815 | .then(function() { done('Must not fullfill'); })
816 | .catch(function(err) {
817 | falsyIdAlarm.id('undefined');
818 | // Fetching with string id `undefined` should reject
819 | return falsyIdAlarm.fetch();
820 | })
821 | .then(function() { done('Must not fullfill'); })
822 | .catch(function(err) {
823 | falsyIdAlarm.id(' null ');
824 | // Fetching with string id `null` should reject. Including space padd
825 | return falsyIdAlarm.fetch();
826 | })
827 | .then(function() { done('Must not fullfill'); })
828 | .catch(function(err) {
829 | done();
830 | });
831 | });
832 |
833 | });
834 |
835 | describe('#destroy()', function() {
836 | 'use strict';
837 |
838 | it('successful destroy', function(done) {
839 | var user = new Model.User();
840 | user.name('Destroy');
841 | user.save().then(function(model) {
842 | // Save successful. Destroying now...
843 | return model.destroy();
844 | }, function(err) {
845 | done(err);
846 | }).then(function() {
847 | // Model `user` should be desposed after then block.
848 | setTimeout(function() {
849 | try {
850 | expect(user.__json).to.be.null;
851 | expect(user.id).to.be.null;
852 | done();
853 | } catch (e) {
854 | done(e);
855 | }
856 | }, 0);
857 | }, function(err) {
858 | done(err)
859 | });
860 | });
861 |
862 | it('destroy result through callback', function(done) {
863 | var user = new Model.User();
864 | user.name('Destroy');
865 | user.save().then(function(model) {
866 | // Save successful. Destroying now...
867 | model.destroy(function(err) {
868 | if (err) {
869 | done(err);
870 | return;
871 | }
872 | setTimeout(function() {
873 | try {
874 | expect(user.__json).to.be.null;
875 | expect(user.id).to.be.null;
876 | done();
877 | } catch (e) {
878 | done(e);
879 | }
880 | }, 0);
881 | });
882 | }, function(err) {
883 | done(err);
884 | });
885 | });
886 |
887 | });
888 |
889 | describe('#isSaved()', function() {
890 | 'use strict';
891 |
892 | it('default to false', function() {
893 | var user = new Model.User();
894 | expect(user.isSaved()).to.be.false;
895 | });
896 |
897 | it('default to false even modified', function() {
898 | var user = new Model.User();
899 | user.name('Modified Foo');
900 | expect(user.isSaved()).to.be.false;
901 | });
902 |
903 | it('false even id is set', function() {
904 | var user = new Model.User({
905 | id: '001'
906 | });
907 | expect(user.isSaved()).to.be.false;
908 | });
909 |
910 | it('mark saved through save', function(done) {
911 | var user = new Model.User();
912 | user.name('IsSaved');
913 | if (user.isSaved())
914 | done('Model must not be saved yet.')
915 | user.save().then(function(model) {
916 | try {
917 | expect(user.isSaved()).to.be.true;
918 | done();
919 | } catch (e) {
920 | done(e);
921 | }
922 | }, function(err) {
923 | done(err);
924 | });
925 | });
926 |
927 | it('mark saved through fetch', function(done) {
928 | var user = new Model.User();
929 | user.name('IsSaved');
930 | if (user.isSaved())
931 | done('Model must not be saved yet.')
932 | user.save().then(function(model) {
933 | try {
934 | expect(user.isSaved()).to.be.true;
935 | var existingUser = new Model.User();
936 | existingUser.id(model.id());
937 | expect(existingUser.isSaved()).to.be.false;
938 | return existingUser.fetch();
939 | } catch (e) {
940 | done(e);
941 | }
942 | }, function(err) {
943 | done(err);
944 | }).then(function(existingUser) {
945 | try {
946 | expect(existingUser.isSaved()).to.be.true;
947 | done();
948 | } catch (e) {
949 | done(e);
950 | }
951 | }, function(err) {
952 | done(err);
953 | });
954 | });
955 |
956 | });
957 |
958 | describe('#isNew()', function() {
959 | 'use strict';
960 |
961 | it('new if fresh instance', function(done) {
962 | try {
963 | var user = new Model.User();
964 | user.name('IsNew');
965 | expect(user.isNew()).to.be.true;
966 | user.id('notexist');
967 | expect(user.isNew()).to.be.true;
968 | done();
969 | } catch (e) {
970 | done(e);
971 | }
972 | });
973 |
974 | it('not new if saved', function(done) {
975 | var user = new Model.User();
976 | try {
977 | user.name('IsNew');
978 | expect(user.isNew()).to.be.true;
979 | } catch (e) {
980 | done(e);
981 | }
982 | user.save(function() {
983 | try {
984 | expect(user.isNew()).to.be.false;
985 | done();
986 | } catch (e) {
987 | done(e);
988 | }
989 | }, function(err) {
990 | done(err);
991 | });
992 | });
993 |
994 | });
995 |
996 | describe('#isModified()', function() {
997 | 'use strict';
998 |
999 | it('default to false', function() {
1000 | var user = new Model.User();
1001 | expect(user.isModified()).to.be.false;
1002 | });
1003 |
1004 | it('true if a prop is changed', function() {
1005 | var user = new Model.User();
1006 | user.name('New Foo');
1007 | expect(user.isModified()).to.be.true;
1008 | });
1009 |
1010 | });
1011 |
1012 | describe('#isDirty()', function() {
1013 | 'use strict';
1014 |
1015 | it('true if modified', function() {
1016 | var user = new Model.User();
1017 | user.name('New Foo');
1018 | expect(user.isDirty()).to.be.true;
1019 | });
1020 |
1021 | it('true if not saved', function() {
1022 | var user = new Model.User();
1023 | expect(user.isDirty()).to.be.true;
1024 | });
1025 |
1026 | it('false if saved and not modified', function(done) {
1027 | var user = new Model.User();
1028 | user.save(function() {
1029 | try {
1030 | expect(user.isDirty()).to.be.false;
1031 | user.name('New Foo');
1032 | expect(user.isDirty()).to.be.true;
1033 | done();
1034 | } catch (e) {
1035 | done(e);
1036 | }
1037 | }, function(err) {
1038 | done(err);
1039 | });
1040 | });
1041 |
1042 | });
1043 |
1044 | describe('#populate()', function() {
1045 | 'use strict';
1046 |
1047 | it('successful populate', function(done) {
1048 | var note = new Model.Note({
1049 | folder: 'fold001',
1050 | author: 'user001'
1051 | });
1052 | var populate = note.populate();
1053 | populate.then(function(model) {
1054 | expect(model).to.containSubset({
1055 | __json: {
1056 | folder: {
1057 | name: 'System'
1058 | }
1059 | }
1060 | });
1061 | expect(model).to.containSubset({
1062 | __json: {
1063 | author: {
1064 | name: 'UserFoo'
1065 | }
1066 | }
1067 | });
1068 | done();
1069 | }).catch(function(err) {
1070 | done(err);
1071 | });
1072 | expect(populate).to.be.fulfilled;
1073 | expect(populate).to.eventually.deep.equal(note);
1074 | });
1075 |
1076 | });
1077 |
1078 | });
--------------------------------------------------------------------------------