├── .bowerrc ├── .codeclimate.yml ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .jscsrc ├── .yo-rc.json ├── LICENSE.md ├── README.md ├── bower.json ├── demo ├── app │ ├── components │ │ ├── attribute-field │ │ │ ├── attribute-field.directive.js │ │ │ ├── attribute-field.html │ │ │ └── attribute-field.scss │ │ ├── collection │ │ │ ├── collection.directive.js │ │ │ ├── collection.html │ │ │ └── collection.scss │ │ ├── error-message │ │ │ ├── error-list.directive.js │ │ │ ├── error-list.html │ │ │ ├── error-message.directive.js │ │ │ └── error-message.html │ │ ├── object │ │ │ ├── object-relationships.directive.js │ │ │ ├── object-relationships.html │ │ │ ├── object.directive.js │ │ │ └── object.html │ │ ├── promise-button │ │ │ ├── promise-button.directive.js │ │ │ └── promise-button.module.js │ │ ├── search │ │ │ ├── search-collection.filter.js │ │ │ ├── search-object.filter.js │ │ │ ├── search-polymorphic.html │ │ │ ├── search.directive.js │ │ │ ├── search.html │ │ │ └── search.scss │ │ └── utils │ │ │ ├── bytes.filter.js │ │ │ ├── semantic-ui.module.js │ │ │ └── to-title-case.js │ ├── footer │ │ ├── footer.html │ │ └── footer.scss │ ├── frame │ │ ├── frame.controller.js │ │ ├── frame.html │ │ └── hello.html │ ├── index.constants.js │ ├── index.js │ ├── index.run.js │ ├── index.scss │ ├── request │ │ ├── all.controller.js │ │ ├── all.html │ │ ├── get.controller.js │ │ ├── get.html │ │ └── request.controller.js │ ├── resources │ │ ├── jobs.factory.js │ │ ├── laser-guns.factory.js │ │ ├── locations.factory.js │ │ ├── planets.factory.js │ │ ├── power-armors.factory.js │ │ ├── robot-models.factory.js │ │ ├── robots.factory.js │ │ ├── spaceship-models.factory.js │ │ └── spaceships.factory.js │ ├── sidebar │ │ ├── sidebar.directive.js │ │ └── sidebar.html │ └── vendor.scss ├── assets │ └── images │ │ ├── .keep │ │ ├── mario-down.gif │ │ └── mario-up.gif ├── favicon.gif └── index.html ├── dist-demo ├── assets │ └── images │ │ ├── mario-down.gif │ │ └── mario-up.gif ├── favicon.gif ├── fonts │ ├── icons.eot │ ├── icons.svg │ ├── icons.ttf │ ├── icons.woff │ └── icons.woff2 ├── index.html ├── maps │ ├── scripts │ │ ├── app-4969e92e70.js.map │ │ ├── lib-f2c7ec66be.js.map │ │ └── vendor-5f48f0c060.js.map │ └── styles │ │ ├── app-3086bb8113.css.map │ │ └── vendor-5ff773b98f.css.map ├── scripts │ ├── app-4969e92e70.js │ ├── lib-f2c7ec66be.js │ └── vendor-5f48f0c060.js └── styles │ ├── app-3086bb8113.css │ └── vendor-5ff773b98f.css ├── dist ├── angular-jsonapi.js ├── angular-jsonapi.js.map ├── angular-jsonapi.min.js └── angular-jsonapi.min.js.map ├── gulp ├── .eslintrc ├── build.js ├── conf.js ├── e2e-tests.js ├── inject.js ├── scripts.js ├── server.js ├── styles.js ├── unit-tests.js └── watch.js ├── gulpfile.js ├── karma.conf.js ├── package.json ├── protractor.conf.js └── src ├── collection ├── collection.factory.js └── collection.factory.spec.js ├── errors ├── errors-manager │ ├── errors-manager.factory.js │ └── errors-manager.factory.spec.js ├── source-error │ ├── source-error.factory.js │ └── source-error.factory.spec.js └── validation-error │ ├── validation-error.factory.js │ └── validation-error.factory.spec.js ├── jsonapi.config.js ├── jsonapi.module.js ├── jsonapi.provider.js ├── jsonapi.provider.spec.js ├── model ├── abstract-model │ ├── abstract-model.js │ └── abstract-model.spec.js ├── model-factory.factory.js ├── model-factory.factory.spec.js ├── model-form │ ├── model-form.factory.js │ └── model-form.spec.js └── model-linker │ ├── model-linker.js │ └── model-linker.spec.js ├── resource ├── resource-cache │ ├── resource-cache.factory.js │ └── resource-cache.factory.spec.js ├── resource.factory.js └── resource.factory.spec.js ├── schema ├── schema-link.factory.js ├── schema-link.factory.spec.js ├── schema.factory.js └── schema.factory.spec.js ├── sources ├── local │ ├── source-local.decorator.js │ ├── source-local.factory.js │ └── source-local.module.js ├── parse │ ├── source-parse.decorator.js │ ├── source-parse.factory.js │ └── source-parse.module.js ├── rest │ ├── source-rest.decorator.js │ ├── source-rest.factory.js │ └── source-rest.module.js └── source-prototype.js ├── synchronizers ├── synchronizer-prototype.js ├── synchronizer-simple.factory.js └── synchronizer-simple.factory.spec.js └── utils ├── all-settled.js ├── kebab-case.js ├── lazy-property.js └── named-function.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | languages: 2 | JavaScript: true 3 | exclude_paths: 4 | - "dist/*" 5 | - "dist-demo/*" 6 | - "demo/*" 7 | - "gulp/*" 8 | - "karma.conf.js" 9 | - "gulpfile.js" 10 | - "protractor.conf.js" -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "plugins": ["angular"], 4 | "env": { 5 | "browser": true, 6 | "jasmine": true 7 | }, 8 | "globals": { 9 | "angular": true, 10 | "module": true, 11 | "inject": true 12 | }, 13 | "rules": { 14 | "angular/definedundefined": 0, 15 | "angular/controller-as": 0, 16 | "angular/controller-as-route": 0, 17 | "angular/controller-as-vm": [0, "vm"] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bower_components/ 3 | .sass-cache/ 4 | .tmp/ 5 | .sublime-gulp.cache -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "airbnb", 3 | "requireTrailingComma": null 4 | } -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-gulp-angular": { 3 | "version": "0.11.0", 4 | "props": { 5 | "angularVersion": "~1.3.4", 6 | "angularModules": [], 7 | "jQuery": { 8 | "key": "jquery2" 9 | }, 10 | "resource": { 11 | "key": "none", 12 | "module": null 13 | }, 14 | "router": { 15 | "key": "ui-router", 16 | "module": "ui.router" 17 | }, 18 | "ui": { 19 | "key": "bootstrap", 20 | "module": null 21 | }, 22 | "bootstrapComponents": { 23 | "key": "ui-bootstrap", 24 | "module": "ui.bootstrap" 25 | }, 26 | "cssPreprocessor": { 27 | "key": "node-sass", 28 | "extension": "scss" 29 | }, 30 | "jsPreprocessor": { 31 | "key": "none", 32 | "extension": "js", 33 | "srcExtension": "js" 34 | }, 35 | "htmlPreprocessor": { 36 | "key": "none", 37 | "extension": "html" 38 | }, 39 | "foundationComponents": { 40 | "name": null, 41 | "version": null, 42 | "key": null, 43 | "module": null 44 | }, 45 | "paths": { 46 | "src": "src", 47 | "dist": "dist", 48 | "e2e": "e2e", 49 | "tmp": ".tmp" 50 | } 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-jsonapi", 3 | "version": "1.0.0-alpha.7", 4 | "main": "dist/angular-jsonapi.js", 5 | "dependencies": { 6 | "angular": "1.4.x", 7 | "angular-uuid4": "~0.3.0", 8 | "moment": "~2.10.6", 9 | "pluralize": "~1.1.5", 10 | "validate": "~0.9.0", 11 | "parse": "~1.6.7" 12 | }, 13 | "ignore": [ 14 | "demo/", 15 | "demo-dist/" 16 | ], 17 | "devDependencies": { 18 | "angular-recursion": "~1.0.5", 19 | "angular-ui-router": "~0.2.15", 20 | "faker": "https://github.com/Marak/faker.js.git#~2.1.2", 21 | "jquery": "~2.1.4", 22 | "json-formatter": "~0.2.7", 23 | "lodash": "~3.10.1", 24 | "ng-clip": "~0.2.6", 25 | "semantic": "semantic-ui#~2.1.3", 26 | "favico.js": "~0.3.10" 27 | }, 28 | "resolutions": { 29 | "jquery": "~2.1.4", 30 | "angular": "1.4.x" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /demo/app/components/attribute-field/attribute-field.directive.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('promise-button') 5 | .directive('attributeField', attributeField); 6 | 7 | function attributeField() { 8 | return { 9 | restrict: 'A', 10 | templateUrl: 'app/components/attribute-field/attribute-field.html', 11 | scope: { 12 | object: '=', 13 | key: '=' 14 | } 15 | }; 16 | } 17 | })(); 18 | -------------------------------------------------------------------------------- /demo/app/components/attribute-field/attribute-field.html: -------------------------------------------------------------------------------- 1 |
5 | 6 | 12 | 13 |
14 | 17 |
18 |
-------------------------------------------------------------------------------- /demo/app/components/attribute-field/attribute-field.scss: -------------------------------------------------------------------------------- 1 | .ui.form .field.success .input, .ui.form .field.success label, 2 | .ui.form .fields.success .field .input, 3 | .ui.form .fields.success .field label { 4 | color: #1a531b; 5 | } 6 | 7 | .ui.form .field.success input:not([type]), .ui.form .field.success input[type=text], .ui.form .field.success input[type=email], .ui.form .field.success input[type=search], .ui.form .field.success input[type=password], .ui.form .field.success input[type=date], .ui.form .field.success input[type=datetime-local], .ui.form .field.success input[type=tel], .ui.form .field.success input[type=time], .ui.form .field.success input[type=url], .ui.form .field.success input[type=number], .ui.form .field.success select, .ui.form .field.success textarea, .ui.form .fields.success .field input:not([type]), .ui.form .fields.success .field input[type=text], .ui.form .fields.success .field input[type=email], .ui.form .fields.success .field input[type=search], .ui.form .fields.success .field input[type=password], .ui.form .fields.success .field input[type=date], .ui.form .fields.success .field input[type=datetime-local], .ui.form .fields.success .field input[type=tel], .ui.form .fields.success .field input[type=time], .ui.form .fields.success .field input[type=url], .ui.form .fields.success .field input[type=number], .ui.form .fields.success .field select, .ui.form .fields.success .field textarea { 8 | background: #fcfff5; 9 | border-color: #a3c293; 10 | color: #2c662d; 11 | border-radius: ''; 12 | box-shadow: none; 13 | } -------------------------------------------------------------------------------- /demo/app/components/collection/collection.directive.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angularJsonapiExample') 5 | .directive('angularJsonapiCollection', collection); 6 | 7 | function collection(RecursionHelper) { 8 | return { 9 | restrict: 'E', 10 | templateUrl: 'app/components/collection/collection.html', 11 | scope: { 12 | collection: '=data' 13 | }, 14 | compile: RecursionHelper.compile, 15 | controller: function($scope, $interval, $jsonapi, $location, $state) { 16 | $interval(function() { 17 | $scope.updateDiff = (Date.now() - $scope.collection.updatedAt) / 1000; 18 | }, 100); 19 | 20 | $scope.objectKeys = objectKeys; 21 | 22 | $scope.newObjects = []; 23 | 24 | var filters = $scope.collection.params.filter || {}; 25 | var params = $jsonapi.sourceRest.encodeParams({filter: filters}); 26 | $location.search(params); 27 | 28 | $scope.close = close; 29 | $scope.clear = clear; 30 | 31 | $scope.add = add; 32 | 33 | $scope.filter = filter; 34 | $scope.addFilter = addFilter; 35 | $scope.removeFilter = removeFilter; 36 | $scope.clearFilter = clearFilter; 37 | $scope.filtersArray = []; 38 | 39 | angular.forEach($scope.collection.params.filter, function(filter, filterKey) { 40 | angular.forEach(filter, function(filterValue) { 41 | addFilter(filterKey, filterValue); 42 | }); 43 | }); 44 | 45 | function close() { 46 | $scope.$broadcast('close'); 47 | } 48 | 49 | function clear() { 50 | $jsonapi.clearCache(); 51 | } 52 | 53 | function add() { 54 | $scope.newObjects.push($scope.collection.resource.initialize()); 55 | } 56 | 57 | function addFilter(filterKey, filterValue) { 58 | $scope.filtersArray.push({key: filterKey, value: filterValue}); 59 | } 60 | 61 | function removeFilter(index) { 62 | $scope.filtersArray.splice(index, 1); 63 | } 64 | 65 | function filter() { 66 | var filters = {}; 67 | 68 | angular.forEach($scope.filtersArray, function(filter) { 69 | filters[filter.key] = filters[filter.key] || []; 70 | filters[filter.key].push(filter.value); 71 | }); 72 | 73 | $location.search($jsonapi.sourceRest.encodeParams({filter: filters})); 74 | $state.reload(); 75 | } 76 | 77 | function clearFilter() { 78 | $location.search({}); 79 | $state.reload(); 80 | } 81 | 82 | function objectKeys(object) { 83 | if (angular.isObject(object)) { 84 | return Object.keys(object).length; 85 | } else { 86 | return 0; 87 | } 88 | } 89 | } 90 | }; 91 | } 92 | 93 | })(); 94 | -------------------------------------------------------------------------------- /demo/app/components/collection/collection.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

{{collection.type}} 4 | 5 |

6 | Loading {{collection.loadingCount}}:{{collection.loading}} 7 | 8 |
9 |
10 | 11 | 12 |
13 | 14 | 19 | 23 | 24 | 28 | 29 | 49 | 54 | 59 | 60 |
Synchronizing...
61 |
Synchronized {{updateDiff}} s. ago
70 |
71 | 72 | 73 | 74 |
75 |
76 |
77 | 78 |
79 | 80 |
81 |
82 |
83 | 84 |
85 | 86 |
87 |
88 |
89 | 90 | 94 |
95 |
96 | 100 |
101 | 102 |
103 | 104 | 105 | 106 | 107 | 108 |
109 |

New objects

110 | 111 | 116 | 117 |
118 | 119 | 120 |
121 |

All objects

122 |

Filtered objects

123 | 124 |
128 | Loading collection for the first time 129 |
130 | 131 |
135 | Collection not synchronized, refresh it to fetch data 136 |
137 | 138 | 143 | 144 |
145 |
Close all models
146 |
Clear cache
147 |
148 |
-------------------------------------------------------------------------------- /demo/app/components/collection/collection.scss: -------------------------------------------------------------------------------- 1 | .collection { 2 | margin-top: 2rem; 3 | position: relative; 4 | min-height: 20rem; 5 | } 6 | -------------------------------------------------------------------------------- /demo/app/components/error-message/error-list.directive.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angularJsonapiExample') 5 | .directive('errorList', errorList); 6 | 7 | function errorList() { 8 | return { 9 | restrict: 'E', 10 | templateUrl: 'app/components/error-message/error-list.html', 11 | scope: { 12 | errors: '=data' 13 | }, 14 | controller: controller 15 | }; 16 | 17 | function controller($scope) { 18 | $scope.isObject = angular.isObject; 19 | $scope.isString = angular.isString; 20 | } 21 | } 22 | 23 | })(); 24 | -------------------------------------------------------------------------------- /demo/app/components/error-message/error-list.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
{{errorManager.name}}
6 |
{{errorManager.description}}
7 |
8 |
9 |
10 |
{{key}}
11 |
12 |
13 | 14 |
15 |
{{error.message}}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
-------------------------------------------------------------------------------- /demo/app/components/error-message/error-message.directive.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angularJsonapiExample') 5 | .directive('errorMessage', errorMessage); 6 | 7 | function errorMessage() { 8 | return { 9 | restrict: 'E', 10 | templateUrl: 'app/components/error-message/error-message.html', 11 | scope: { 12 | errors: '=data' 13 | } 14 | }; 15 | } 16 | 17 | })(); 18 | -------------------------------------------------------------------------------- /demo/app/components/error-message/error-message.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | There were some errors: 4 |
5 |
6 | 7 |
-------------------------------------------------------------------------------- /demo/app/components/object/object-relationships.directive.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angularJsonapiExample') 5 | .directive('angularJsonapiObjectRelationships', objectRelationships); 6 | 7 | function objectRelationships(RecursionHelper) { 8 | return { 9 | restrict: 'E', 10 | templateUrl: 'app/components/object/object-relationships.html', 11 | scope: { 12 | object: '=data' 13 | }, 14 | compile: RecursionHelper.compile, 15 | controller: controller 16 | }; 17 | 18 | function controller($scope) { 19 | $scope.isArray = angular.isArray; 20 | $scope.emptyRelationship = emptyRelationship; 21 | $scope.form = $scope.object.parent !== undefined; 22 | 23 | function emptyRelationship(relationship) { 24 | return relationship === undefined || 25 | relationship === null || 26 | angular.isArray(relationship) && 27 | relationship.length === 0; 28 | } 29 | } 30 | } 31 | 32 | })(); 33 | -------------------------------------------------------------------------------- /demo/app/components/object/object-relationships.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Form: {{object.schema.relationships[relationshipName].model}}

5 | polymorphic 6 |
7 |
8 | 15 | 16 | 21 | 22 | 29 |
30 |
31 | 32 | 33 | 34 |
35 | Relationship data locked for new object 36 |
This object is new, you can only add relationships to its form.
37 |
38 | 39 |
40 | Relationship data undefined 41 |
The relationship data hasn't been fetched yet, refresh the model to fetch it.
42 |
43 | 44 |
45 | Has one relationship null 46 |
Set something as relationship object.
47 |
48 | 49 |
50 | Has many relationship empty 51 |
Add something to the relationship array.
52 |
53 | 54 | 61 | 62 | 68 | 69 |
-------------------------------------------------------------------------------- /demo/app/components/object/object.directive.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angularJsonapiExample') 5 | .directive('angularJsonapiObject', object); 6 | 7 | function object(RecursionHelper) { 8 | return { 9 | restrict: 'E', 10 | templateUrl: 'app/components/object/object.html', 11 | scope: { 12 | object: '=data', 13 | unlink: '&', 14 | nested: '=' 15 | }, 16 | require: '^angularJsonapiObject', 17 | compile: RecursionHelper.compile, 18 | controller: controller 19 | }; 20 | 21 | function controller($scope, $interval) { 22 | var interval; 23 | 24 | $scope.showMore = false; 25 | $scope.isArray = angular.isArray; 26 | 27 | $scope.$watch('showMore', toggleTimmer); 28 | 29 | $scope.$on('close', function() { 30 | $scope.showMore = false; 31 | }); 32 | 33 | $scope.equals = angular.equals; 34 | 35 | function toggleTimmer(value) { 36 | if (value === true) { 37 | $scope.updateDiff = (Date.now() - $scope.object.updatedAt) / 1000; 38 | interval = $interval(function() { 39 | $scope.updateDiff = (Date.now() - $scope.object.updatedAt) / 1000; 40 | }, 100); 41 | } else if (value === false) { 42 | $interval.cancel(interval); 43 | } 44 | } 45 | } 46 | } 47 | 48 | })(); 49 | -------------------------------------------------------------------------------- /demo/app/components/object/object.html: -------------------------------------------------------------------------------- 1 |
2 |
5 |
6 | Loading {{object.loadingCount}}:{{object.loading}} Saving {{object.savingCount}}:{{object.saving}} 7 |

{{object.toString()}}

8 | {{object.data.type}}:{{object.data.id}} 9 |
10 |
11 | 19 | 26 | 33 | 41 | 49 | 55 | 61 | 62 |
Not synchronized
63 |
68 | Synchronized 69 |
70 |
80 | Synchronized {{updateDiff}} s. ago 81 |
82 |
83 |
84 | 85 |
86 | 87 |
88 | 89 | 90 |
91 | 92 |
93 |
94 |
95 |

Data

96 |
102 | Show relationships 103 |
104 |
105 |
106 |
107 |
{{attributeValue}}
108 |
{{attributeName | toTitleCase}}
109 |
110 |
111 | 112 |
113 |
114 |
Relationships
115 |
116 | 117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |

Form

126 |
132 | Show relationships 133 |
134 |
135 |
141 | 142 | 143 | 144 | 151 | 152 | 159 |
160 |
161 |
162 |
163 | 164 | 165 | 166 | 167 | 168 | 169 |
170 |
171 | -------------------------------------------------------------------------------- /demo/app/components/promise-button/promise-button.directive.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('promise-button') 5 | .directive('promiseButton', promiseButton); 6 | 7 | function promiseButton($parse, $q) { 8 | return { 9 | restrict: 'A', 10 | priority: -1, 11 | compile: compile 12 | }; 13 | 14 | function compile($element, attr) { 15 | var loadingClass = attr.loadingClass || 'loading'; 16 | var errorClass = attr.errorClass || 'negative'; 17 | var successClass = attr.successClass || 'positive'; 18 | 19 | var fn = $parse(attr.ngClick, null, true); 20 | return function ngEventHandler(scope, element) { 21 | element.on('click', onClick); 22 | 23 | function onClick(event) { 24 | event.preventDefault(); 25 | event.stopImmediatePropagation(); 26 | element.off('click'); 27 | 28 | var callback = function() { 29 | element.addClass(loadingClass); 30 | element.removeClass(errorClass); 31 | element.removeClass(successClass); 32 | $q.resolve(fn(scope, {$event:event})).then(resolve, reject); 33 | }; 34 | 35 | function resolve(response) { 36 | element.removeClass(loadingClass); 37 | element.addClass(successClass); 38 | element.on('click', onClick); 39 | 40 | return response; 41 | } 42 | 43 | function reject(response) { 44 | element.removeClass(loadingClass); 45 | element.addClass(errorClass); 46 | element.on('click', onClick); 47 | 48 | return response; 49 | } 50 | 51 | scope.$apply(callback); 52 | } 53 | }; 54 | } 55 | } 56 | })(); 57 | -------------------------------------------------------------------------------- /demo/app/components/promise-button/promise-button.module.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('promise-button', []); 5 | })(); 6 | -------------------------------------------------------------------------------- /demo/app/components/search/search-collection.filter.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angularJsonapiExample') 5 | .filter('angularJsonapiSearchCollection', searchFilter); 6 | 7 | function searchFilter() { 8 | return function(items, search) { 9 | if (!search) { 10 | return items; 11 | } 12 | 13 | var results = {}; 14 | var words = search.split(' '); 15 | angular.forEach(items, function(value, key) { 16 | if (key.indexOf(words[0]) > -1) { 17 | results[key] = value; 18 | } 19 | }); 20 | 21 | return results; 22 | }; 23 | } 24 | 25 | })(); 26 | -------------------------------------------------------------------------------- /demo/app/components/search/search-object.filter.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angularJsonapiExample') 5 | .filter('angularJsonapiSearchObject', searchFilter); 6 | 7 | function searchFilter($jsonapi) { 8 | var names = $jsonapi.listResources(); 9 | 10 | return function(items, search, relationship, polymorphic) { 11 | if(items === undefined) { 12 | return; 13 | } 14 | 15 | if (!search) { 16 | if (polymorphic === true) { 17 | return []; 18 | } else if (angular.isArray(relationship)) { 19 | return items.filter(function(value) { 20 | return relationship.indexOf(value) === -1; 21 | }); 22 | } else { 23 | return items; 24 | } 25 | } 26 | 27 | var results = []; 28 | var words = search.split(' '); 29 | var searchWord; 30 | 31 | if (polymorphic === false) { 32 | searchWord = search; 33 | angular.forEach(items, function(value) { 34 | if (relationship.indexOf(value) === -1 && value.toString().toLowerCase().indexOf(searchWord.toLowerCase()) > -1) { 35 | results.push(value); 36 | } 37 | }); 38 | } else { 39 | if (words.length > 1) { 40 | searchWord = words.splice(1).join(' '); 41 | angular.forEach(items, function(value) { 42 | if (relationship.indexOf(value) === -1 && value.toString().toLowerCase().indexOf(searchWord.toLowerCase()) > -1) { 43 | results.push(value); 44 | } 45 | }); 46 | } else if (names.indexOf(words[0]) > -1) { 47 | if (angular.isArray(relationship)) { 48 | return items.filter(function(value) { 49 | return relationship.indexOf(value) === -1; 50 | }); 51 | } else { 52 | return items.filter(function(value) { 53 | return relationship !== value; 54 | }); 55 | } 56 | } 57 | } 58 | 59 | return results; 60 | }; 61 | } 62 | 63 | })(); 64 | -------------------------------------------------------------------------------- /demo/app/components/search/search-polymorphic.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/app/components/search/search.directive.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angularJsonapiExample') 5 | .directive('angularJsonapiSearch', search); 6 | 7 | function search() { 8 | return { 9 | restrict: 'E', 10 | template: '
', 11 | controller: controller, 12 | link: link, 13 | scope: { 14 | object: '=', 15 | key: '=' 16 | } 17 | }; 18 | 19 | function link(scope) { 20 | if (scope.schema.polymorphic === true) { 21 | scope.contentUrl = 'app/components/search/search-polymorphic.html'; 22 | } else { 23 | scope.contentUrl = 'app/components/search/search.html'; 24 | } 25 | } 26 | 27 | function controller($scope, $jsonapi, $timeout) { 28 | $scope.schema = $scope.object.schema.relationships[$scope.key]; 29 | if ($scope.schema.polymorphic) { 30 | $scope.collections = {}; 31 | angular.forEach($jsonapi.allResources(), function(resource, resourceName) { 32 | $scope.collections[resourceName] = resource.cache.index(); 33 | }); 34 | } else { 35 | $scope.model = $scope.schema.model; 36 | $scope.collection = $jsonapi.getResource($scope.model).cache.index(); 37 | } 38 | 39 | $scope.show = false; 40 | $scope.isEmpty = isEmpty; 41 | $scope.addLink = addLink; 42 | $scope.showResults = showResults; 43 | $scope.hideResults = hideResults; 44 | $scope.setInput = setInput; 45 | $scope.getIndex = getIndex; 46 | 47 | function isEmpty(obj) { 48 | return Object.keys(obj).length === 0; 49 | } 50 | 51 | function addLink(target) { 52 | $scope.loading = true; 53 | $scope.object.link($scope.key, target).then(resolve, reject); 54 | $scope.show = false; 55 | 56 | function resolve() { 57 | $scope.loading = false; 58 | $scope.input = ''; 59 | } 60 | 61 | function reject(error) { 62 | $scope.loading = false; 63 | $scope.error = true; 64 | $scope.errorText = error[0].statusText; 65 | } 66 | } 67 | 68 | function showResults() { 69 | $scope.error = false; 70 | $scope.show = true; 71 | } 72 | 73 | function hideResults() { 74 | $scope.show = false; 75 | } 76 | 77 | function setInput(value) { 78 | $scope.input = value; 79 | } 80 | 81 | function getIndex(modelName) { 82 | $timeout(function() { 83 | $scope.collection = $jsonapi.getResource(modelName).all().data; 84 | }); 85 | } 86 | } 87 | } 88 | 89 | })(); 90 | -------------------------------------------------------------------------------- /demo/app/components/search/search.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/app/components/search/search.scss: -------------------------------------------------------------------------------- 1 | .ui.search > .results { 2 | display: block; 3 | } 4 | 5 | .ui.category.search > .results .category > .name { 6 | @include word-wrap(); 7 | } 8 | 9 | .ui.search > .results .result { 10 | .description, .title { 11 | @include ellipsis(); 12 | } 13 | } -------------------------------------------------------------------------------- /demo/app/components/utils/bytes.filter.js: -------------------------------------------------------------------------------- 1 | // adopted from https://gist.github.com/thomseddon/3511330 2 | 3 | (function() { 4 | 'use strict'; 5 | 6 | angular.module('angularJsonapiExample') 7 | .filter('bytes', bytes); 8 | 9 | function bytes() { 10 | return function(bytes, precision) { 11 | if (isNaN(parseFloat(bytes)) || !isFinite(bytes)) { 12 | return '-'; 13 | } 14 | 15 | if (typeof precision === 'undefined') { 16 | precision = 1; 17 | } 18 | 19 | var units = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB']; 20 | var number = Math.floor(Math.log(bytes) / Math.log(1024)); 21 | 22 | return (bytes / Math.pow(1024, Math.floor(number))).toFixed(precision) + ' ' + units[number]; 23 | }; 24 | } 25 | })(); 26 | -------------------------------------------------------------------------------- /demo/app/components/utils/semantic-ui.module.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | /* global $:false*/ 4 | 5 | var app = angular.module('angularJsonapiExample'); 6 | 7 | $.each($.site.settings.modules, function(index, module) { 8 | var fn = $.fn[module]; 9 | var name = 'ui' + module.charAt(0).toUpperCase() + module.substring(1); 10 | 11 | /** @ngInject */ 12 | app.directive(name, ['$timeout', '_', '$rootScope', function($timeout, _, $rootScope) { 13 | return { 14 | restrict: 'A', 15 | seModule: { 16 | name: module, 17 | fn: fn 18 | }, 19 | scope: { 20 | options: '&', 21 | arguments: '=', 22 | ngModel: '=' 23 | }, 24 | link: function(scope, iElement) { 25 | if (!scope.options) { 26 | scope.options = {}; 27 | } 28 | 29 | scope.options.directive = scope; 30 | 31 | scope.options.onChange = function(value) { 32 | $timeout(function() { 33 | scope.ngModel = value; 34 | }); 35 | }; 36 | 37 | $timeout(function() { 38 | var element = iElement[module](_.clone(scope.options)); 39 | if (scope.arguments !== undefined) { 40 | iElement[module].apply(element, scope.arguments); 41 | } 42 | }, 300); 43 | 44 | var handler = $rootScope.$on('semantic-ui:reload', function() { 45 | $timeout(function() { 46 | var element = iElement[module](_.clone(scope.options)); 47 | if (scope.arguments !== undefined) { 48 | iElement[module].apply(element, scope.arguments); 49 | } 50 | }, 300); 51 | }); 52 | 53 | $rootScope.$on('$destroy', clear); 54 | 55 | function clear() { 56 | handler(); 57 | } 58 | } 59 | }; 60 | }]); 61 | }); 62 | })(); 63 | -------------------------------------------------------------------------------- /demo/app/components/utils/to-title-case.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angularJsonapiExample') 5 | .constant('toTitleCase', toTitleCase) 6 | .filter('toTitleCase', toTitleCaseFilter); 7 | 8 | function toTitleCase(string) { 9 | var out = string.replace(/^\s*/, ''); // strip leading spaces 10 | out = out.replace(/^[a-z]|[^\s][A-Z]/g, function(str, offset) { 11 | if (offset === 0) { 12 | return str.toUpperCase(); 13 | } else { 14 | return str.substr(0, 1) + ' ' + str.substr(1).toUpperCase(); 15 | } 16 | }); 17 | 18 | return out; 19 | } 20 | 21 | function toTitleCaseFilter() { 22 | return function(input) { 23 | return toTitleCase(input); 24 | }; 25 | } 26 | })(); 27 | -------------------------------------------------------------------------------- /demo/app/footer/footer.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakubrohleder/angular-jsonapi/4af1857fb78aeb5e41c0645152794437f1e46e40/demo/app/footer/footer.html -------------------------------------------------------------------------------- /demo/app/footer/footer.scss: -------------------------------------------------------------------------------- 1 | footer { 2 | min-height: 10rem; 3 | } -------------------------------------------------------------------------------- /demo/app/frame/frame.controller.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angularJsonapiExample') 5 | .controller('FrameController', frameController); 6 | 7 | function frameController( 8 | $jsonapi, 9 | $scope 10 | ) { 11 | $scope.names = $jsonapi.listResources(); 12 | 13 | $scope.localStoreSize = $jsonapi.sourceLocal.size; 14 | } 15 | })(); 16 | -------------------------------------------------------------------------------- /demo/app/frame/frame.html: -------------------------------------------------------------------------------- 1 |
2 | 21 | 22 |
23 | 24 |
25 | 33 | All 34 |
{{localStoreSize.all | bytes}} - {{localStoreSize.fraction | number:2}}%
35 |
36 | 45 | Max 46 |
{{localStoreSize.max}} - {{localStoreSize.list[localStoreSize.max] | bytes}}
47 |
48 | 49 | Localstore space occupied: 50 |
51 | 52 |
53 | 54 | -------------------------------------------------------------------------------- /demo/app/frame/hello.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 | Angular-jsonapi showcase 5 |

6 |

Go to one of the models in the menu and check out how this package works.

7 |

If you want to use it yourself check github readme and source code of this demo.

8 |
-------------------------------------------------------------------------------- /demo/app/index.constants.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angularJsonapiExample') 5 | /* global _: false, Favico: false */ 6 | .constant('_', _) 7 | .constant('apiURL', 'http://localhost:3000') 8 | .constant('Favico', Favico); 9 | })(); 10 | -------------------------------------------------------------------------------- /demo/app/index.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angularJsonapiExample', [ 5 | 'ui.router', 6 | 'angular-jsonapi', 7 | 'angular-jsonapi-local', 8 | 'angular-jsonapi-rest', 9 | 'jsonFormatter', 10 | 'ngClipboard', 11 | 'promise-button', 12 | 'RecursionHelper' 13 | ]) 14 | .config(function(ngClipProvider) { 15 | ngClipProvider.setPath('bower_components/zeroclipboard/dist/ZeroClipboard.swf'); 16 | }) 17 | .config(function($stateProvider, $urlRouterProvider) { 18 | $stateProvider 19 | .state('frame', { 20 | url: '', 21 | templateUrl: 'app/frame/frame.html', 22 | controller: 'FrameController', 23 | abstract: true 24 | }) 25 | .state('frame.hello', { 26 | url: '', 27 | templateUrl: 'app/frame/hello.html' 28 | }) 29 | .state('frame.request', { 30 | url: '/{type}', 31 | template: '', 32 | controller: 'RequestController', 33 | abstract: true, 34 | resolve: { 35 | factory: function($jsonapi, $stateParams) { 36 | return $jsonapi.getResource($stateParams.type); 37 | } 38 | } 39 | }) 40 | .state('frame.request.all', { 41 | url: '', 42 | templateUrl: 'app/request/all.html', 43 | controller: 'RequestAllController', 44 | resolve: { 45 | collection: function(factory, $location, $jsonapi) { 46 | var params = $jsonapi.sourceRest.decodeParams($location.search()); 47 | 48 | return factory.all(params); 49 | } 50 | } 51 | }) 52 | .state('frame.request.get', { 53 | url: '/{id}', 54 | templateUrl: 'app/request/get.html', 55 | controller: 'RequestGetController', 56 | resolve: { 57 | object: function(factory, $stateParams) { 58 | return factory.get($stateParams.id); 59 | } 60 | } 61 | }); 62 | 63 | $urlRouterProvider.otherwise('/robots'); 64 | }); 65 | })(); 66 | -------------------------------------------------------------------------------- /demo/app/index.run.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angularJsonapiExample') 5 | .run(logEvents) 6 | .run(marioFavicon); 7 | 8 | function logEvents( 9 | $rootScope, 10 | $jsonapi, 11 | $log 12 | ) { 13 | var events = [ 14 | 'resource:init', 15 | 'resource:clearCache', 16 | 'resource:initialize', 17 | 'object:add', 18 | 'object:update', 19 | 'object:refresh', 20 | 'object:remove', 21 | 'object:link', 22 | 'object:linkReflection', 23 | 'object:unlink', 24 | 'object:include', 25 | 'object:unlinkReflection', 26 | 'collection:fetch' 27 | ]; 28 | 29 | var resources = $jsonapi.listResources(); 30 | var watchers = []; 31 | 32 | angular.forEach(events, function(eventName) { 33 | angular.forEach(resources, function(resourceName) { 34 | logOnEvent(eventName, resourceName); 35 | }); 36 | }); 37 | 38 | function logOnEvent(eventName, resource) { 39 | var watcher = $rootScope.$on('angularJsonAPI:' + resource + ':' + eventName, function(event, status, object, response) { 40 | $log.info(resource, eventName, status, object, response); 41 | }); 42 | 43 | watchers.push(watcher); 44 | } 45 | 46 | $rootScope.$on('$destroy', clearWatchers); 47 | 48 | function clearWatchers() { 49 | angular.forEach(watchers, function(watcher) { 50 | watcher(); 51 | }); 52 | } 53 | } 54 | 55 | function marioFavicon(Favico, $interval) { 56 | var favicon = new Favico({ 57 | animation:'slide', 58 | position: 'up' 59 | }); 60 | var up = new Image(); 61 | up.src = 'assets/images/mario-up.gif'; 62 | 63 | var down = new Image(); 64 | down.src = 'assets/images/mario-down.gif'; 65 | 66 | var isDown = true; 67 | 68 | $interval(changeIcon, 10); 69 | 70 | function changeIcon() { 71 | isDown = !isDown; 72 | if (isDown) { 73 | favicon.image(down); 74 | } else { 75 | favicon.image(up); 76 | } 77 | } 78 | } 79 | })(); 80 | -------------------------------------------------------------------------------- /demo/app/index.scss: -------------------------------------------------------------------------------- 1 | @mixin word-wrap() { 2 | word-break: break-word; 3 | -webkit-hyphens: auto; 4 | -moz-hyphens: auto; 5 | hyphens: auto; 6 | } 7 | 8 | @mixin ellipsis() { 9 | overflow: hidden; 10 | white-space: nowrap; 11 | text-overflow: ellipsis; 12 | } 13 | 14 | .site { 15 | display: flex; 16 | min-height: 100vh; 17 | flex-direction: column; 18 | } 19 | 20 | main { 21 | flex: 1; 22 | } 23 | 24 | .ui.list .list > .item .description, .ui.list > .item .description { 25 | @include word-wrap(); 26 | } 27 | .ui.grid { 28 | margin-left: 0; 29 | margin-right: 0; 30 | } 31 | 32 | .ui.container { 33 | position: relative; 34 | } 35 | 36 | .ui.sidebar.menu { 37 | z-index: 20000; 38 | } 39 | 40 | .pushable > .pusher.dimmed .ui.loading.segment:before { 41 | z-index: 1; 42 | } 43 | 44 | .pushable > .pusher { 45 | position: absolute; 46 | left: 0; 47 | right: 0; 48 | } 49 | 50 | .ui.left.sidebar { 51 | transform: translate3d(-260px, 0, 0); 52 | } 53 | 54 | .ui.left.floated { 55 | float: left; 56 | } 57 | 58 | .ui.right.floated { 59 | float: right; 60 | } 61 | 62 | .icon.spin { 63 | -webkit-animation: spin 1000ms infinite linear; 64 | animation: spin 1000ms infinite linear; 65 | } 66 | @-webkit-keyframes spin { 67 | 0% { 68 | -webkit-transform: rotate(0deg); 69 | transform: rotate(0deg); 70 | } 71 | 100% { 72 | -webkit-transform: rotate(359deg); 73 | transform: rotate(359deg); 74 | } 75 | } 76 | @keyframes spin { 77 | 0% { 78 | -webkit-transform: rotate(0deg); 79 | transform: rotate(0deg); 80 | } 81 | 100% { 82 | -webkit-transform: rotate(359deg); 83 | transform: rotate(359deg); 84 | } 85 | } 86 | 87 | .show-loading, .show-error, .show-ok{ 88 | display: none; 89 | } 90 | .loading .show-loading { 91 | display: inline-block; 92 | } 93 | .error .show-error { 94 | display: inline-block; 95 | } 96 | .ok .show-ok { 97 | display: inline-block; 98 | } 99 | 100 | .stats { 101 | position: absolute; 102 | width: 100vw; 103 | top: 0; 104 | left: 50%; 105 | z-index: 10; 106 | transform: translateX(-200%); 107 | background: white; 108 | transition: all 1s ease-in-out; 109 | border-right: 1px #444 solid; 110 | } 111 | .dimmer { 112 | display: none; 113 | background: black; 114 | opacity: 0; 115 | position: fixed; 116 | top: 0; 117 | bottom: 0; 118 | left: 0; 119 | right: 0; 120 | z-index: 5; 121 | transition: all 1s ease-in-out; 122 | } 123 | .site { 124 | min-height: 100vh; 125 | transition: all 1s ease-in-out; 126 | } 127 | .show-stats .stats { 128 | transform: translateX(-50%); 129 | } 130 | .show-stats .dimmer { 131 | display: block; 132 | opacity: .5; 133 | } 134 | .show-stats .site { 135 | transform: translateX(50%); 136 | } 137 | 138 | .ui.segment.fixed { 139 | position: fixed; 140 | bottom: 1em; 141 | right: 1em; 142 | z-index: 200; 143 | } 144 | 145 | a[ui-sref] { 146 | cursor: pointer; 147 | } 148 | 149 | .narrow.container { 150 | max-width: 80rem; 151 | margin: auto; 152 | padding: 3rem; 153 | } 154 | /* Do not remove this comments bellow. It's the markers used by gulp-inject to inject 155 | all your sass files automatically */ 156 | // injector 157 | // endinjector 158 | 159 | 160 | @media only screen and (max-width: 1200px){ 161 | .secondary.pointing.menu .item, .secondary.pointing.menu .menu { 162 | display: none; 163 | } 164 | .secondary.pointing.menu .toc.item { 165 | display: block; 166 | } 167 | } 168 | 169 | @media only screen and (min-width: 1201px){ 170 | .secondary.pointing.menu .toc.item { 171 | display: none; 172 | } 173 | } 174 | 175 | .ui.mini.dropdown { 176 | font-size: 0.71428571em; 177 | 178 | .menu > .item { 179 | font-size: 1em; 180 | } 181 | } -------------------------------------------------------------------------------- /demo/app/request/all.controller.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angularJsonapiExample') 5 | .controller('RequestAllController', RequestAllController); 6 | 7 | function RequestAllController( 8 | $scope, collection 9 | ) { 10 | $scope.collection = collection; 11 | } 12 | })(); 13 | -------------------------------------------------------------------------------- /demo/app/request/all.html: -------------------------------------------------------------------------------- 1 | 2 |

All

3 | -------------------------------------------------------------------------------- /demo/app/request/get.controller.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angularJsonapiExample') 5 | .controller('RequestGetController', RequestGetController); 6 | 7 | function RequestGetController( 8 | $scope, object 9 | ) { 10 | $scope.object = object; 11 | } 12 | })(); 13 | -------------------------------------------------------------------------------- /demo/app/request/get.html: -------------------------------------------------------------------------------- 1 |

Get

2 | 3 |

{{object.toString()}}

-------------------------------------------------------------------------------- /demo/app/request/request.controller.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angularJsonapiExample') 5 | .controller('RequestController', RequestController); 6 | 7 | function RequestController( 8 | ) { 9 | } 10 | })(); 11 | -------------------------------------------------------------------------------- /demo/app/resources/jobs.factory.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angularJsonapiExample') 5 | .run(function( 6 | $jsonapi, 7 | apiURL 8 | ) { 9 | var schema = { 10 | type: 'jobs', 11 | id: 'uuid4', 12 | attributes: { 13 | name: {presence: true, length: {maximum: 20, minimum: 3}}, 14 | salary: {presence: true, numericality: {onlyInteger: true}} 15 | }, 16 | relationships: { 17 | robots: { 18 | included: true, 19 | type: 'hasMany' 20 | } 21 | }, 22 | functions: { 23 | toString: function() { 24 | if (!this.data.attributes.name) { 25 | return this.data.id; 26 | } 27 | 28 | return this.data.attributes.name; 29 | } 30 | } 31 | }; 32 | 33 | var localeSynchro = $jsonapi.sourceLocal.create('LocalStore synchronization', 'AngularJsonAPI'); 34 | var restSynchro = $jsonapi.sourceRest.create('Rest synchronization', apiURL + '/jobs'); 35 | var synchronizer = $jsonapi.synchronizerSimple.create([localeSynchro, restSynchro]); 36 | 37 | $jsonapi.addResource(schema, synchronizer); 38 | }) 39 | .factory('Jobs', Jobs); 40 | 41 | function Jobs( 42 | $jsonapi 43 | ) { 44 | return $jsonapi.getResource('jobs'); 45 | } 46 | })(); 47 | -------------------------------------------------------------------------------- /demo/app/resources/laser-guns.factory.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angularJsonapiExample') 5 | .run(function( 6 | $jsonapi, 7 | apiURL 8 | ) { 9 | var schema = { 10 | type: 'laserGuns', 11 | id: 'uuid4', 12 | attributes: { 13 | name: {presence: true, length: {maximum: 20, minimum: 3}}, 14 | durability: {presence: true, numericality: {onlyInteger: true}}, 15 | quality: {presence: true, numericality: {onlyInteger: true}}, 16 | power: {presence: true, numericality: {onlyInteger: true}}, 17 | type: {presence: true, length: {maximum: 20, minimum: 3}}, 18 | rarity: {presence: true, length: {maximum: 20, minimum: 3}} 19 | }, 20 | relationships: { 21 | owner: { 22 | included: true, 23 | type: 'hasOne', 24 | polymorphic: true 25 | } 26 | }, 27 | functions: { 28 | toString: function() { 29 | if (!this.data.attributes.name) { 30 | return this.data.id; 31 | } 32 | 33 | return this.data.attributes.name; 34 | } 35 | } 36 | }; 37 | 38 | var localeSynchro = $jsonapi.sourceLocal.create('LocalStore synchronization', 'AngularJsonAPI'); 39 | var restSynchro = $jsonapi.sourceRest.create('Rest synchronization', apiURL + '/laserGuns'); 40 | var synchronizer = $jsonapi.synchronizerSimple.create([localeSynchro, restSynchro]); 41 | 42 | $jsonapi.addResource(schema, synchronizer); 43 | }) 44 | .factory('Jobs', Jobs); 45 | 46 | function Jobs( 47 | $jsonapi 48 | ) { 49 | return $jsonapi.getResource('laserGuns'); 50 | } 51 | })(); 52 | -------------------------------------------------------------------------------- /demo/app/resources/locations.factory.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angularJsonapiExample') 5 | .run(function( 6 | $jsonapi, 7 | apiURL 8 | ) { 9 | var schema = { 10 | type: 'locations', 11 | id: 'uuid4', 12 | attributes: { 13 | cordsX: {presence: true, numericality: {onlyInteger: true}}, 14 | cordsY: {presence: true, numericality: {onlyInteger: true}} 15 | }, 16 | relationships: { 17 | planet: { 18 | included: true, 19 | type: 'hasOne' 20 | }, 21 | entity: { 22 | included: true, 23 | type: 'hasOne', 24 | polymorphic: true, 25 | reflection: 'location' 26 | } 27 | }, 28 | functions: { 29 | toString: function() { 30 | if (!this.relationships.planet || !this.relationships.planet.data.attributes.name) { 31 | return this.data.id; 32 | } 33 | 34 | return this.relationships.planet.data.attributes.name; 35 | } 36 | } 37 | }; 38 | 39 | var localeSynchro = $jsonapi.sourceLocal.create('LocalStore synchronization', 'AngularJsonAPI'); 40 | var restSynchro = $jsonapi.sourceRest.create('Rest synchronization', apiURL + '/locations'); 41 | var synchronizer = $jsonapi.synchronizerSimple.create([localeSynchro, restSynchro]); 42 | 43 | $jsonapi.addResource(schema, synchronizer); 44 | }) 45 | .factory('Locations', Locations); 46 | 47 | function Locations( 48 | $jsonapi 49 | ) { 50 | return $jsonapi.getResource('locations'); 51 | } 52 | })(); 53 | -------------------------------------------------------------------------------- /demo/app/resources/planets.factory.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angularJsonapiExample') 5 | .run(function( 6 | $jsonapi, 7 | apiURL 8 | ) { 9 | var schema = { 10 | type: 'planets', 11 | id: 'uuid4', 12 | attributes: { 13 | name: {presence: true, length: {maximum: 20, minimum: 3}}, 14 | cordsX: {presence: true, numericality: {onlyInteger: true}}, 15 | cordsY: {presence: true, numericality: {onlyInteger: true}}, 16 | cordsZ: {presence: true, numericality: {onlyInteger: true}}, 17 | size: {presence: true, numericality: {onlyInteger: true}} 18 | }, 19 | relationships: { 20 | locations: { 21 | included: true, 22 | type: 'hasMany' 23 | } 24 | }, 25 | functions: { 26 | toString: function() { 27 | if (!this.data.attributes.name) { 28 | return this.data.id; 29 | } 30 | 31 | return this.data.attributes.name; 32 | } 33 | } 34 | }; 35 | 36 | var localeSynchro = $jsonapi.sourceLocal.create('LocalStore synchronization', 'AngularJsonAPI'); 37 | var restSynchro = $jsonapi.sourceRest.create('Rest synchronization', apiURL + '/planets'); 38 | var synchronizer = $jsonapi.synchronizerSimple.create([localeSynchro, restSynchro]); 39 | 40 | $jsonapi.addResource(schema, synchronizer); 41 | }) 42 | .factory('Planets', Planets); 43 | 44 | function Planets( 45 | $jsonapi 46 | ) { 47 | return $jsonapi.getResource('planets'); 48 | } 49 | })(); 50 | -------------------------------------------------------------------------------- /demo/app/resources/power-armors.factory.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angularJsonapiExample') 5 | .run(function( 6 | $jsonapi, 7 | apiURL 8 | ) { 9 | var schema = { 10 | type: 'powerArmors', 11 | id: 'uuid4', 12 | attributes: { 13 | name: {presence: true, length: {maximum: 20, minimum: 3}}, 14 | durability: {presence: true, numericality: {onlyInteger: true}}, 15 | quality: {presence: true, numericality: {onlyInteger: true}}, 16 | armor: {presence: true, numericality: {onlyInteger: true}}, 17 | type: {presence: true, length: {maximum: 20, minimum: 3}}, 18 | rarity: {presence: true, length: {maximum: 20, minimum: 3}} 19 | }, 20 | relationships: { 21 | owner: { 22 | included: true, 23 | type: 'hasOne', 24 | polymorphic: true 25 | } 26 | }, 27 | functions: { 28 | toString: function() { 29 | if (!this.data.attributes.name) { 30 | return this.data.id; 31 | } 32 | 33 | return this.data.attributes.name; 34 | } 35 | } 36 | }; 37 | 38 | var localeSynchro = $jsonapi.sourceLocal.create('LocalStore synchronization', 'AngularJsonAPI'); 39 | var restSynchro = $jsonapi.sourceRest.create('Rest synchronization', apiURL + '/powerArmors'); 40 | var synchronizer = $jsonapi.synchronizerSimple.create([localeSynchro, restSynchro]); 41 | 42 | $jsonapi.addResource(schema, synchronizer); 43 | }) 44 | .factory('PowerArmors', PowerArmors); 45 | 46 | function PowerArmors( 47 | $jsonapi 48 | ) { 49 | return $jsonapi.getResource('powerArmors'); 50 | } 51 | })(); 52 | -------------------------------------------------------------------------------- /demo/app/resources/robot-models.factory.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angularJsonapiExample') 5 | .run(function( 6 | $jsonapi, 7 | apiURL 8 | ) { 9 | var schema = { 10 | type: 'robotModels', 11 | id: 'uuid4', 12 | attributes: { 13 | name: {presence: true, length: {maximum: 20, minimum: 3}}, 14 | code: {presence: true, length: {maximum: 20, minimum: 3}} 15 | }, 16 | relationships: { 17 | robots: { 18 | included: true, 19 | type: 'hasMany' 20 | } 21 | }, 22 | functions: { 23 | toString: function() { 24 | if (!this.data.attributes.name) { 25 | return this.data.id; 26 | } 27 | 28 | return this.data.attributes.name; 29 | } 30 | } 31 | }; 32 | 33 | var localeSynchro = $jsonapi.sourceLocal.create('LocalStore synchronization', 'AngularJsonAPI'); 34 | var restSynchro = $jsonapi.sourceRest.create('Rest synchronization', apiURL + '/robotModels'); 35 | var synchronizer = $jsonapi.synchronizerSimple.create([localeSynchro, restSynchro]); 36 | 37 | $jsonapi.addResource(schema, synchronizer); 38 | }) 39 | .factory('RobotModels', RobotModels); 40 | 41 | function RobotModels( 42 | $jsonapi 43 | ) { 44 | return $jsonapi.getResource('robotModels'); 45 | } 46 | })(); 47 | -------------------------------------------------------------------------------- /demo/app/resources/robots.factory.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angularJsonapiExample') 5 | .run(function( 6 | $jsonapi, 7 | apiURL 8 | ) { 9 | var schema = { 10 | type: 'robots', 11 | id: 'uuid4', 12 | attributes: { 13 | nameFirst: {presence: true, length: {maximum: 20, minimum: 3}}, 14 | nameLast: {presence: true, length: {maximum: 20, minimum: 3}}, 15 | creationDate: {datetime: true}, 16 | pictureUrl: {presence: true} 17 | }, 18 | relationships: { 19 | location: { 20 | included: true, 21 | type: 'hasOne', 22 | reflection: 'entity' 23 | }, 24 | robotModel: { 25 | included: true, 26 | type: 'hasOne' 27 | }, 28 | job: { 29 | included: true, 30 | type: 'hasOne' 31 | }, 32 | laserGuns: { 33 | included: true, 34 | type: 'hasMany', 35 | reflection: 'owner' 36 | }, 37 | powerArmors: { 38 | included: true, 39 | type: 'hasMany', 40 | reflection: 'owner' 41 | }, 42 | spaceships: { 43 | included: true, 44 | type: 'hasMany', 45 | reflection: 'pilot' 46 | } 47 | }, 48 | include: { 49 | get: ['location.planet'] 50 | }, 51 | functions: { 52 | toString: function() { 53 | if (!this.data.attributes.nameFirst && !this.data.attributes.nameLast) { 54 | return this.data.id; 55 | } 56 | 57 | return this.data.attributes.nameFirst + this.data.attributes.nameLast; 58 | } 59 | } 60 | }; 61 | 62 | var localeSynchro = $jsonapi.sourceLocal.create('LocalStore synchronization', 'AngularJsonAPI'); 63 | var restSynchro = $jsonapi.sourceRest.create('Rest synchronization', apiURL + '/robots'); 64 | var synchronizer = $jsonapi.synchronizerSimple.create([localeSynchro, restSynchro]); 65 | 66 | $jsonapi.addResource(schema, synchronizer); 67 | }) 68 | .factory('Robots', Robots); 69 | 70 | function Robots( 71 | $jsonapi 72 | ) { 73 | return $jsonapi.getResource('robots'); 74 | } 75 | })(); 76 | -------------------------------------------------------------------------------- /demo/app/resources/spaceship-models.factory.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angularJsonapiExample') 5 | .run(function( 6 | $jsonapi, 7 | apiURL 8 | ) { 9 | var schema = { 10 | type: 'spaceshipModels', 11 | id: 'uuid4', 12 | attributes: { 13 | name: {presence: true, length: {maximum: 20, minimum: 3}}, 14 | code: {presence: true, length: {maximum: 20, minimum: 3}}, 15 | speed: {presence: true, numericality: {onlyInteger: true}}, 16 | cargo: {presence: true, numericality: {onlyInteger: true}}, 17 | type: {presence: true, length: {maximum: 20, minimum: 3}} 18 | }, 19 | relationships: { 20 | spaceships: { 21 | included: true, 22 | type: 'hasMany' 23 | } 24 | }, 25 | functions: { 26 | toString: function() { 27 | if (!this.data.attributes.name) { 28 | return this.data.id; 29 | } 30 | 31 | return this.data.attributes.name; 32 | } 33 | } 34 | }; 35 | 36 | var localeSynchro = $jsonapi.sourceLocal.create('LocalStore synchronization', 'AngularJsonAPI'); 37 | var restSynchro = $jsonapi.sourceRest.create('Rest synchronization', apiURL + '/spaceshipModels'); 38 | var synchronizer = $jsonapi.synchronizerSimple.create([localeSynchro, restSynchro]); 39 | 40 | $jsonapi.addResource(schema, synchronizer); 41 | }) 42 | .factory('SpaceshipModels', SpaceshipModels); 43 | 44 | function SpaceshipModels( 45 | $jsonapi 46 | ) { 47 | return $jsonapi.getResource('spaceshipModels'); 48 | } 49 | })(); 50 | -------------------------------------------------------------------------------- /demo/app/resources/spaceships.factory.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angularJsonapiExample') 5 | .run(function( 6 | $jsonapi, 7 | apiURL 8 | ) { 9 | var schema = { 10 | type: 'spaceships', 11 | id: 'uuid4', 12 | attributes: { 13 | name: {presence: true, length: {maximum: 20, minimum: 3}}, 14 | durability: {presence: true, numericality: {onlyInteger: true}}, 15 | quality: {presence: true, numericality: {onlyInteger: true}} 16 | }, 17 | relationships: { 18 | pilot: { 19 | included: true, 20 | type: 'hasOne', 21 | reflection: 'spaceships', 22 | polymorphic: true 23 | }, 24 | spaceshipModel: { 25 | included: true, 26 | type: 'hasOne' 27 | }, 28 | location: { 29 | included: true, 30 | type: 'hasOne', 31 | reflection: 'entity' 32 | } 33 | }, 34 | functions: { 35 | toString: function() { 36 | if (!this.data.attributes.name) { 37 | return this.data.id; 38 | } 39 | 40 | return this.data.attributes.name; 41 | } 42 | } 43 | }; 44 | 45 | var localeSynchro = $jsonapi.sourceLocal.create('LocalStore synchronization', 'AngularJsonAPI'); 46 | var restSynchro = $jsonapi.sourceRest.create('Rest synchronization', apiURL + '/spaceships'); 47 | var synchronizer = $jsonapi.synchronizerSimple.create([localeSynchro, restSynchro]); 48 | 49 | $jsonapi.addResource(schema, synchronizer); 50 | }) 51 | .factory('Spaceships', Spaceships); 52 | 53 | function Spaceships( 54 | $jsonapi 55 | ) { 56 | return $jsonapi.getResource('spaceships'); 57 | } 58 | })(); 59 | -------------------------------------------------------------------------------- /demo/app/sidebar/sidebar.directive.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angularJsonapiExample') 5 | .directive('sidebar', sidebar); 6 | 7 | function sidebar() { 8 | return { 9 | restrict: 'E', 10 | templateUrl: 'app/sidebar/sidebar.html', 11 | controller: sidebarController, 12 | replace: true 13 | }; 14 | 15 | function sidebarController($scope, $jsonapi) { 16 | $scope.names = $jsonapi.listResources(); 17 | } 18 | } 19 | 20 | })(); 21 | -------------------------------------------------------------------------------- /demo/app/sidebar/sidebar.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/app/vendor.scss: -------------------------------------------------------------------------------- 1 | /* Do not remove this comments bellow. It's the markers used by wiredep to inject 2 | sass dependencies when defined in the bower.json of your dependencies */ 3 | // bower:scss 4 | // endbower 5 | -------------------------------------------------------------------------------- /demo/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakubrohleder/angular-jsonapi/4af1857fb78aeb5e41c0645152794437f1e46e40/demo/assets/images/.keep -------------------------------------------------------------------------------- /demo/assets/images/mario-down.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakubrohleder/angular-jsonapi/4af1857fb78aeb5e41c0645152794437f1e46e40/demo/assets/images/mario-down.gif -------------------------------------------------------------------------------- /demo/assets/images/mario-up.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakubrohleder/angular-jsonapi/4af1857fb78aeb5e41c0645152794437f1e46e40/demo/assets/images/mario-up.gif -------------------------------------------------------------------------------- /demo/favicon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakubrohleder/angular-jsonapi/4af1857fb78aeb5e41c0645152794437f1e46e40/demo/favicon.gif -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | angularJsonapi 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 28 | 30 | 31 | 32 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /dist-demo/assets/images/mario-down.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakubrohleder/angular-jsonapi/4af1857fb78aeb5e41c0645152794437f1e46e40/dist-demo/assets/images/mario-down.gif -------------------------------------------------------------------------------- /dist-demo/assets/images/mario-up.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakubrohleder/angular-jsonapi/4af1857fb78aeb5e41c0645152794437f1e46e40/dist-demo/assets/images/mario-up.gif -------------------------------------------------------------------------------- /dist-demo/favicon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakubrohleder/angular-jsonapi/4af1857fb78aeb5e41c0645152794437f1e46e40/dist-demo/favicon.gif -------------------------------------------------------------------------------- /dist-demo/fonts/icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakubrohleder/angular-jsonapi/4af1857fb78aeb5e41c0645152794437f1e46e40/dist-demo/fonts/icons.eot -------------------------------------------------------------------------------- /dist-demo/fonts/icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakubrohleder/angular-jsonapi/4af1857fb78aeb5e41c0645152794437f1e46e40/dist-demo/fonts/icons.ttf -------------------------------------------------------------------------------- /dist-demo/fonts/icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakubrohleder/angular-jsonapi/4af1857fb78aeb5e41c0645152794437f1e46e40/dist-demo/fonts/icons.woff -------------------------------------------------------------------------------- /dist-demo/fonts/icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakubrohleder/angular-jsonapi/4af1857fb78aeb5e41c0645152794437f1e46e40/dist-demo/fonts/icons.woff2 -------------------------------------------------------------------------------- /dist-demo/index.html: -------------------------------------------------------------------------------- 1 | angularJsonapi -------------------------------------------------------------------------------- /dist-demo/maps/styles/app-3086bb8113.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["styles/app-3086bb8113.css"],"names":[],"mappings":"AAmGA,QAdA,OAmBE,IAAK,EATL,mBAAoB,IAAI,GAAG,YA/F7B,MACE,QAAS,YACT,QAAS,aACT,QAAS,YACT,QAAS,KAET,mBAAoB,SACpB,sBAAuB,OACvB,uBAAwB,OACpB,mBAAoB,OAChB,eAAgB,OAE1B,KACE,iBAAkB,EAClB,aAAc,EACV,SAAU,EACN,KAAM,EAEhB,kCAAqC,4BACnC,WAAY,WACZ,gBAAiB,KACjB,aAAc,KACd,YAAa,KACT,QAAS,KAEf,SACE,YAAa,EACb,aAAc,EAEhB,cACE,SAAU,SAEZ,iBACE,QAAS,MAEX,oDACE,QAAS,EAEX,kBACE,SAAU,SACV,KAAM,EACN,MAAO,EAET,iBACE,kBAAmB,wBACX,UAAW,wBAErB,iBACE,MAAO,KAET,kBACE,MAAO,MAET,WACE,kBAAmB,KAAK,GAAO,SAAS,OACxC,UAAW,KAAK,GAAO,SAAS,OAElC,wBACE,GACE,kBAAmB,UACnB,UAAW,UACb,KACE,kBAAmB,eACnB,UAAW,gBAEf,gBACE,GACE,kBAAmB,UACnB,UAAW,UACb,KACE,kBAAmB,eACnB,UAAW,gBAEA,YAAf,cAA4B,SAC1B,QAAS,KAKX,mBAHA,uBAMA,aALE,QAAS,aAQX,OACE,SAAU,SACV,MAAO,MAEP,KAAM,IACN,QAAS,GACT,kBAAmB,kBACf,cAAe,kBACX,UAAW,kBACnB,WAAY,KAEJ,WAAY,IAAI,GAAG,YAC3B,aAAc,IAAI,KAAK,MAEzB,QACE,QAAS,KACT,WAAY,KACZ,QAAS,EACT,SAAU,MAEV,OAAQ,EACR,KAAM,EACN,MAAO,EACP,QAAS,EAED,WAAY,IAAI,GAAG,YAE7B,MACE,WAAY,MACZ,mBAAoB,IAAI,GAAG,YACnB,WAAY,IAAI,GAAG,YAE7B,mBACE,kBAAmB,iBACf,cAAe,iBACX,UAAW,iBAErB,oBACE,QAAS,MACT,QAAS,GAEX,kBACE,kBAAmB,gBACf,cAAe,gBACX,UAAW,gBAErB,kBACE,SAAU,MACV,OAAQ,IACR,MAAO,IACP,QAAS,IAEX,WACE,OAAQ,QAEV,kBACE,UAAW,MACX,OAAQ,KACR,QAAS,KAMX,OACE,WAAY,MAEd,+BAAgC,8BAChC,uCACA,sCACE,MAAO,QAET,0CAA0N,yCAA0C,mDAA/K,0CAA+V,2CAAxQ,6CAA5C,2CAAwL,wCAA7Q,yCAAsT,yCAA0C,wCAAqF,+BAAgC,iCAAkC,kDAAkQ,iDAAkD,2DAA/M,kDAA+Z,mDAAxT,qDAApD,mDAAwN,gDAA7T,iDAA8W,iDAAkD,gDAAqG,uCAAwC,yCAChoC,WAAY,QACZ,aAAc,QACd,MAAO,QACP,cAAe,GACf,WAAY,KAEd,YACE,WAAY,KACZ,SAAU,SACV,WAAY,MAEd,oBACE,QAAS,MAEX,6CACE,WAAY,WACZ,gBAAiB,KACjB,aAAc,KACd,YAAa,KACT,QAAS,KAEf,yCAA4C,mCAC1C,SAAU,OACV,YAAa,OACb,cAAe,SAEjB,0CACE,+BAAgC,+BAC9B,QAAS,KACX,mCACE,QAAS,OAEb,0CACE,mCACE,QAAS,MAEb,kBACE,UAAW,YACX,8BACE,UAAW","file":"styles/app-3086bb8113.css","sourcesContent":[".site {\n display: -webkit-box;\n display: -webkit-flex;\n display: -ms-flexbox;\n display: flex;\n min-height: 100vh;\n -webkit-box-orient: vertical;\n -webkit-box-direction: normal;\n -webkit-flex-direction: column;\n -ms-flex-direction: column;\n flex-direction: column; }\n\nmain {\n -webkit-box-flex: 1;\n -webkit-flex: 1;\n -ms-flex: 1;\n flex: 1; }\n\n.ui.list .list > .item .description, .ui.list > .item .description {\n word-break: break-word;\n -webkit-hyphens: auto;\n -moz-hyphens: auto;\n -ms-hyphens: auto;\n hyphens: auto; }\n\n.ui.grid {\n margin-left: 0;\n margin-right: 0; }\n\n.ui.container {\n position: relative; }\n\n.ui.sidebar.menu {\n z-index: 20000; }\n\n.pushable > .pusher.dimmed .ui.loading.segment:before {\n z-index: 1; }\n\n.pushable > .pusher {\n position: absolute;\n left: 0;\n right: 0; }\n\n.ui.left.sidebar {\n -webkit-transform: translate3d(-260px, 0, 0);\n transform: translate3d(-260px, 0, 0); }\n\n.ui.left.floated {\n float: left; }\n\n.ui.right.floated {\n float: right; }\n\n.icon.spin {\n -webkit-animation: spin 1000ms infinite linear;\n animation: spin 1000ms infinite linear; }\n\n@-webkit-keyframes spin {\n 0% {\n -webkit-transform: rotate(0deg);\n transform: rotate(0deg); }\n 100% {\n -webkit-transform: rotate(359deg);\n transform: rotate(359deg); } }\n\n@keyframes spin {\n 0% {\n -webkit-transform: rotate(0deg);\n transform: rotate(0deg); }\n 100% {\n -webkit-transform: rotate(359deg);\n transform: rotate(359deg); } }\n\n.show-loading, .show-error, .show-ok {\n display: none; }\n\n.loading .show-loading {\n display: inline-block; }\n\n.error .show-error {\n display: inline-block; }\n\n.ok .show-ok {\n display: inline-block; }\n\n.stats {\n position: absolute;\n width: 100vw;\n top: 0;\n left: 50%;\n z-index: 10;\n -webkit-transform: translateX(-200%);\n -ms-transform: translateX(-200%);\n transform: translateX(-200%);\n background: white;\n -webkit-transition: all 1s ease-in-out;\n transition: all 1s ease-in-out;\n border-right: 1px #444 solid; }\n\n.dimmer {\n display: none;\n background: black;\n opacity: 0;\n position: fixed;\n top: 0;\n bottom: 0;\n left: 0;\n right: 0;\n z-index: 5;\n -webkit-transition: all 1s ease-in-out;\n transition: all 1s ease-in-out; }\n\n.site {\n min-height: 100vh;\n -webkit-transition: all 1s ease-in-out;\n transition: all 1s ease-in-out; }\n\n.show-stats .stats {\n -webkit-transform: translateX(-50%);\n -ms-transform: translateX(-50%);\n transform: translateX(-50%); }\n\n.show-stats .dimmer {\n display: block;\n opacity: .5; }\n\n.show-stats .site {\n -webkit-transform: translateX(50%);\n -ms-transform: translateX(50%);\n transform: translateX(50%); }\n\n.ui.segment.fixed {\n position: fixed;\n bottom: 1em;\n right: 1em;\n z-index: 200; }\n\na[ui-sref] {\n cursor: pointer; }\n\n.narrow.container {\n max-width: 80rem;\n margin: auto;\n padding: 3rem; }\n\n/* Do not remove this comments bellow. It's the markers used by gulp-inject to inject\n all your sass files automatically */\n/* Do not remove this comments bellow. It's the markers used by wiredep to inject\n sass dependencies when defined in the bower.json of your dependencies */\nfooter {\n min-height: 10rem; }\n\n.ui.form .field.success .input, .ui.form .field.success label,\n.ui.form .fields.success .field .input,\n.ui.form .fields.success .field label {\n color: #1a531b; }\n\n.ui.form .field.success input:not([type]), .ui.form .field.success input[type=text], .ui.form .field.success input[type=email], .ui.form .field.success input[type=search], .ui.form .field.success input[type=password], .ui.form .field.success input[type=date], .ui.form .field.success input[type=datetime-local], .ui.form .field.success input[type=tel], .ui.form .field.success input[type=time], .ui.form .field.success input[type=url], .ui.form .field.success input[type=number], .ui.form .field.success select, .ui.form .field.success textarea, .ui.form .fields.success .field input:not([type]), .ui.form .fields.success .field input[type=text], .ui.form .fields.success .field input[type=email], .ui.form .fields.success .field input[type=search], .ui.form .fields.success .field input[type=password], .ui.form .fields.success .field input[type=date], .ui.form .fields.success .field input[type=datetime-local], .ui.form .fields.success .field input[type=tel], .ui.form .fields.success .field input[type=time], .ui.form .fields.success .field input[type=url], .ui.form .fields.success .field input[type=number], .ui.form .fields.success .field select, .ui.form .fields.success .field textarea {\n background: #fcfff5;\n border-color: #a3c293;\n color: #2c662d;\n border-radius: '';\n box-shadow: none; }\n\n.collection {\n margin-top: 2rem;\n position: relative;\n min-height: 20rem; }\n\n.ui.search > .results {\n display: block; }\n\n.ui.category.search > .results .category > .name {\n word-break: break-word;\n -webkit-hyphens: auto;\n -moz-hyphens: auto;\n -ms-hyphens: auto;\n hyphens: auto; }\n\n.ui.search > .results .result .description, .ui.search > .results .result .title {\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis; }\n\n@media only screen and (max-width: 1200px) {\n .secondary.pointing.menu .item, .secondary.pointing.menu .menu {\n display: none; }\n .secondary.pointing.menu .toc.item {\n display: block; } }\n\n@media only screen and (min-width: 1201px) {\n .secondary.pointing.menu .toc.item {\n display: none; } }\n\n.ui.mini.dropdown {\n font-size: 0.71428571em; }\n .ui.mini.dropdown .menu > .item {\n font-size: 1em; }\n\n/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImluZGV4LnNjc3MiLCJ2ZW5kb3Iuc2NzcyIsImZvb3Rlci9mb290ZXIuc2NzcyIsImNvbXBvbmVudHMvYXR0cmlidXRlLWZpZWxkL2F0dHJpYnV0ZS1maWVsZC5zY3NzIiwiY29tcG9uZW50cy9jb2xsZWN0aW9uL2NvbGxlY3Rpb24uc2NzcyIsImNvbXBvbmVudHMvc2VhcmNoL3NlYXJjaC5zY3NzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQWFBO0VBQ0UscUJBQWM7RUFBZCxzQkFBYztFQUFkLHFCQUFjO0VBQWQsY0FBYztFQUNkLGtCQUFrQjtFQUNsQiw2QkFBdUI7RUFBdkIsOEJBQXVCO0VBQXZCLCtCQUF1QjtNQUF2QiwyQkFBdUI7VUFBdkIsdUJBQXVCLEVBSGxCOztBQU1QO0VBQ0Usb0JBQVE7RUFBUixnQkFBUTtNQUFSLFlBQVE7VUFBUixRQUFRLEVBREo7O0FBSWdEO0VBdEJwRCx1QkFBMkI7RUFDM0Isc0JBQXNCO0VBQ3RCLG1CQUFzQjtFQUN0QixrQkFBc0I7TUFBdEIsY0FBc0IsRUFtQjRDOztBQUdqRTtFQUNELGVBQWU7RUFDZixnQkFBZ0IsRUFGUjs7QUFLUDtFQUNELG1CQUFtQixFQUROOztBQUlKO0VBQ1QsZUFBZSxFQURDOztBQUk0QjtFQUM1QyxXQUFXLEVBRDBDOztBQUkzQztFQUNWLG1CQUFtQjtFQUNuQixRQUFRO0VBQ1IsU0FBUyxFQUhVOztBQU1iO0VBQ04sNkNBQXNCO1VBQXRCLHFDQUFzQixFQUROOztBQUlWO0VBQ04sWUFBWSxFQURJOztBQUlUO0VBQ1AsYUFBYSxFQURJOztBQUlkO0VBQ0QsK0NBQThDO0VBQzlDLHVDQUFzQyxFQUY5Qjs7QUFJWjtFQUNJO0lBQ0ksZ0NBQXlCO0lBQ3pCLHdCQUFpQixFQUFBO0VBRXJCO0lBQ0ksa0NBQXlCO0lBQ3pCLDBCQUFpQixFQUFBLEVBQUE7O0FBR3pCO0VBQ0k7SUFDSSxnQ0FBeUI7SUFDekIsd0JBQWlCLEVBQUE7RUFFckI7SUFDSSxrQ0FBeUI7SUFDekIsMEJBQWlCLEVBQUEsRUFBQTs7QUFJRztFQUMxQixjQUFjLEVBRHFCOztBQUc1QjtFQUNQLHNCQUFzQixFQURBOztBQUdqQjtFQUNMLHNCQUFzQixFQURKOztBQUdoQjtFQUNGLHNCQUFzQixFQURWOztBQUlkO0VBQ0UsbUJBQW1CO0VBQ25CLGFBQWE7RUFDYixPQUFPO0VBQ1AsVUFBVTtFQUNWLFlBQVk7RUFDWixxQ0FBcUI7TUFBckIsaUNBQXFCO1VBQXJCLDZCQUFxQjtFQUNyQixrQkFBa0I7RUFDbEIsdUNBQThCO1VBQTlCLCtCQUE4QjtFQUM5Qiw2QkFBNkIsRUFUdkI7O0FBV1I7RUFDRSxjQUFjO0VBQ2Qsa0JBQWtCO0VBQ2xCLFdBQVc7RUFDWCxnQkFBZ0I7RUFDaEIsT0FBTztFQUNQLFVBQVU7RUFDVixRQUFRO0VBQ1IsU0FBUztFQUNULFdBQVc7RUFDWCx1Q0FBOEI7VUFBOUIsK0JBQThCLEVBVnZCOztBQVlUO0VBQ0Usa0JBQWtCO0VBQ2xCLHVDQUE4QjtVQUE5QiwrQkFBOEIsRUFGekI7O0FBSUs7RUFDVixvQ0FBcUI7TUFBckIsZ0NBQXFCO1VBQXJCLDRCQUFxQixFQURIOztBQUdSO0VBQ1YsZUFBZTtFQUNmLFlBQVksRUFGTzs7QUFJVDtFQUNWLG1DQUFxQjtNQUFyQiwrQkFBcUI7VUFBckIsMkJBQXFCLEVBREo7O0FBSVI7RUFDVCxnQkFBZ0I7RUFDaEIsWUFBWTtFQUNaLFdBQVc7RUFDWCxhQUFhLEVBSkk7O0FBT1Y7RUFDUCxnQkFBZ0IsRUFETjs7QUFJTDtFQUNMLGlCQUFpQjtFQUNqQixhQUFhO0VBQ2IsY0FBYyxFQUhHOztBQUtuQjt1Q0FDdUM7QUMxSnZDOzJFQUMyRTtBQ0QzRTtFQUNFLGtCQUFrQixFQURaOztBQ0V3Qjs7O0VBQy9CLGVBQWUsRUFEdUI7O0FBSTJuQztFQUM5cEMsb0JBQW9CO0VBQ3BCLHNCQUFzQjtFQUN0QixlQUFlO0VBQ2Ysa0JBQWtCO0VBQ2xCLGlCQUFpQixFQUx1cEM7O0FDTjVxQztFQUNFLGlCQUFpQjtFQUNqQixtQkFBbUI7RUFDbkIsa0JBQWtCLEVBSFA7O0FDQUE7RUFDWCxlQUFlLEVBRE07O0FBSW9CO0VMSHpDLHVCQUEyQjtFQUMzQixzQkFBc0I7RUFDdEIsbUJBQXNCO0VBQ3RCLGtCQUFzQjtNQUF0QixjQUFzQixFS0EwQjs7QUFLbEM7RUxEZCxpQkFBaUI7RUFDakIsb0JBQW9CO0VBQ3BCLHdCQUF3QixFS0RGOztBTDJKeEI7RUFDMkQ7SUFDdkQsY0FBYyxFQURnRDtFQUduQztJQUMzQixlQUFlLEVBRG1CLEVBQUE7O0FBS3RDO0VBQytCO0lBQzNCLGNBQWMsRUFEb0IsRUFBQTs7QUFLOUI7RUFDTix3QkFBd0IsRUFEUDtFQUdUO0lBQ04sZUFBZSxFQURGIiwiZmlsZSI6ImluZGV4LmNzcyIsInNvdXJjZXNDb250ZW50IjpbIkBtaXhpbiB3b3JkLXdyYXAoKSB7XG4gIHdvcmQtYnJlYWs6ICAgICBicmVhay13b3JkO1xuICAtd2Via2l0LWh5cGhlbnM6IGF1dG87XG4gIC1tb3otaHlwaGVuczogICAgYXV0bztcbiAgaHlwaGVuczogICAgICAgICBhdXRvO1xufVxuXG5AbWl4aW4gZWxsaXBzaXMoKSB7XG4gIG92ZXJmbG93OiBoaWRkZW47XG4gIHdoaXRlLXNwYWNlOiBub3dyYXA7XG4gIHRleHQtb3ZlcmZsb3c6IGVsbGlwc2lzO1xufVxuXG4uc2l0ZSB7XG4gIGRpc3BsYXk6IGZsZXg7XG4gIG1pbi1oZWlnaHQ6IDEwMHZoO1xuICBmbGV4LWRpcmVjdGlvbjogY29sdW1uO1xufVxuXG5tYWluIHtcbiAgZmxleDogMTtcbn1cblxuLnVpLmxpc3QgLmxpc3QgPiAuaXRlbSAuZGVzY3JpcHRpb24sIC51aS5saXN0ID4gLml0ZW0gLmRlc2NyaXB0aW9uIHtcbiAgQGluY2x1ZGUgd29yZC13cmFwKCk7XG59XG4udWkuZ3JpZCB7XG4gIG1hcmdpbi1sZWZ0OiAwO1xuICBtYXJnaW4tcmlnaHQ6IDA7XG59XG5cbi51aS5jb250YWluZXIge1xuICBwb3NpdGlvbjogcmVsYXRpdmU7XG59XG5cbi51aS5zaWRlYmFyLm1lbnUge1xuICB6LWluZGV4OiAyMDAwMDtcbn1cblxuLnB1c2hhYmxlID4gLnB1c2hlci5kaW1tZWQgLnVpLmxvYWRpbmcuc2VnbWVudDpiZWZvcmUge1xuICB6LWluZGV4OiAxO1xufVxuXG4ucHVzaGFibGUgPiAucHVzaGVyIHtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICBsZWZ0OiAwO1xuICByaWdodDogMDtcbn1cblxuLnVpLmxlZnQuc2lkZWJhciB7XG4gIHRyYW5zZm9ybTogdHJhbnNsYXRlM2QoLTI2MHB4LCAwLCAwKTtcbn1cblxuLnVpLmxlZnQuZmxvYXRlZCB7XG4gIGZsb2F0OiBsZWZ0O1xufVxuXG4udWkucmlnaHQuZmxvYXRlZCB7XG4gIGZsb2F0OiByaWdodDtcbn1cblxuLmljb24uc3BpbiB7XG4gICAgLXdlYmtpdC1hbmltYXRpb246IHNwaW4gMTAwMG1zIGluZmluaXRlIGxpbmVhcjtcbiAgICBhbmltYXRpb246IHNwaW4gMTAwMG1zIGluZmluaXRlIGxpbmVhcjtcbn1cbkAtd2Via2l0LWtleWZyYW1lcyBzcGluIHtcbiAgICAwJSB7XG4gICAgICAgIC13ZWJraXQtdHJhbnNmb3JtOiByb3RhdGUoMGRlZyk7XG4gICAgICAgIHRyYW5zZm9ybTogcm90YXRlKDBkZWcpO1xuICAgIH1cbiAgICAxMDAlIHtcbiAgICAgICAgLXdlYmtpdC10cmFuc2Zvcm06IHJvdGF0ZSgzNTlkZWcpO1xuICAgICAgICB0cmFuc2Zvcm06IHJvdGF0ZSgzNTlkZWcpO1xuICAgIH1cbn1cbkBrZXlmcmFtZXMgc3BpbiB7XG4gICAgMCUge1xuICAgICAgICAtd2Via2l0LXRyYW5zZm9ybTogcm90YXRlKDBkZWcpO1xuICAgICAgICB0cmFuc2Zvcm06IHJvdGF0ZSgwZGVnKTtcbiAgICB9XG4gICAgMTAwJSB7XG4gICAgICAgIC13ZWJraXQtdHJhbnNmb3JtOiByb3RhdGUoMzU5ZGVnKTtcbiAgICAgICAgdHJhbnNmb3JtOiByb3RhdGUoMzU5ZGVnKTtcbiAgICB9XG59XG5cbi5zaG93LWxvYWRpbmcsIC5zaG93LWVycm9yLCAuc2hvdy1va3tcbiAgZGlzcGxheTogbm9uZTtcbn1cbi5sb2FkaW5nIC5zaG93LWxvYWRpbmcge1xuICBkaXNwbGF5OiBpbmxpbmUtYmxvY2s7XG59XG4uZXJyb3IgLnNob3ctZXJyb3Ige1xuICBkaXNwbGF5OiBpbmxpbmUtYmxvY2s7XG59XG4ub2sgLnNob3ctb2sge1xuICBkaXNwbGF5OiBpbmxpbmUtYmxvY2s7XG59XG5cbi5zdGF0cyB7XG4gIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgd2lkdGg6IDEwMHZ3O1xuICB0b3A6IDA7XG4gIGxlZnQ6IDUwJTtcbiAgei1pbmRleDogMTA7XG4gIHRyYW5zZm9ybTogdHJhbnNsYXRlWCgtMjAwJSk7XG4gIGJhY2tncm91bmQ6IHdoaXRlO1xuICB0cmFuc2l0aW9uOiBhbGwgMXMgZWFzZS1pbi1vdXQ7XG4gIGJvcmRlci1yaWdodDogMXB4ICM0NDQgc29saWQ7XG59XG4uZGltbWVyIHtcbiAgZGlzcGxheTogbm9uZTtcbiAgYmFja2dyb3VuZDogYmxhY2s7XG4gIG9wYWNpdHk6IDA7XG4gIHBvc2l0aW9uOiBmaXhlZDtcbiAgdG9wOiAwO1xuICBib3R0b206IDA7XG4gIGxlZnQ6IDA7XG4gIHJpZ2h0OiAwO1xuICB6LWluZGV4OiA1O1xuICB0cmFuc2l0aW9uOiBhbGwgMXMgZWFzZS1pbi1vdXQ7XG59XG4uc2l0ZSB7XG4gIG1pbi1oZWlnaHQ6IDEwMHZoO1xuICB0cmFuc2l0aW9uOiBhbGwgMXMgZWFzZS1pbi1vdXQ7XG59XG4uc2hvdy1zdGF0cyAuc3RhdHMge1xuICB0cmFuc2Zvcm06IHRyYW5zbGF0ZVgoLTUwJSk7XG59XG4uc2hvdy1zdGF0cyAuZGltbWVyIHtcbiAgZGlzcGxheTogYmxvY2s7XG4gIG9wYWNpdHk6IC41O1xufVxuLnNob3ctc3RhdHMgLnNpdGUge1xuICB0cmFuc2Zvcm06IHRyYW5zbGF0ZVgoNTAlKTtcbn1cblxuLnVpLnNlZ21lbnQuZml4ZWQge1xuICBwb3NpdGlvbjogZml4ZWQ7XG4gIGJvdHRvbTogMWVtO1xuICByaWdodDogMWVtO1xuICB6LWluZGV4OiAyMDA7XG59XG5cbmFbdWktc3JlZl0ge1xuICBjdXJzb3I6IHBvaW50ZXI7XG59XG5cbi5uYXJyb3cuY29udGFpbmVyIHtcbiAgbWF4LXdpZHRoOiA4MHJlbTtcbiAgbWFyZ2luOiBhdXRvO1xuICBwYWRkaW5nOiAzcmVtO1xufVxuLyogRG8gbm90IHJlbW92ZSB0aGlzIGNvbW1lbnRzIGJlbGxvdy4gSXQncyB0aGUgbWFya2VycyB1c2VkIGJ5IGd1bHAtaW5qZWN0IHRvIGluamVjdFxuICAgYWxsIHlvdXIgc2FzcyBmaWxlcyBhdXRvbWF0aWNhbGx5ICovXG4vLyBpbmplY3RvclxuQGltcG9ydCBcInZlbmRvci5zY3NzXCI7XG5AaW1wb3J0IFwiZm9vdGVyL2Zvb3Rlci5zY3NzXCI7XG5AaW1wb3J0IFwiY29tcG9uZW50cy9hdHRyaWJ1dGUtZmllbGQvYXR0cmlidXRlLWZpZWxkLnNjc3NcIjtcbkBpbXBvcnQgXCJjb21wb25lbnRzL2NvbGxlY3Rpb24vY29sbGVjdGlvbi5zY3NzXCI7XG5AaW1wb3J0IFwiY29tcG9uZW50cy9zZWFyY2gvc2VhcmNoLnNjc3NcIjtcbi8vIGVuZGluamVjdG9yXG5cblxuQG1lZGlhIG9ubHkgc2NyZWVuIGFuZCAobWF4LXdpZHRoOiAxMjAwcHgpe1xuICAuc2Vjb25kYXJ5LnBvaW50aW5nLm1lbnUgLml0ZW0sIC5zZWNvbmRhcnkucG9pbnRpbmcubWVudSAubWVudSB7XG4gICAgZGlzcGxheTogbm9uZTtcbiAgfVxuICAuc2Vjb25kYXJ5LnBvaW50aW5nLm1lbnUgLnRvYy5pdGVtIHtcbiAgICBkaXNwbGF5OiBibG9jaztcbiAgfVxufVxuXG5AbWVkaWEgb25seSBzY3JlZW4gYW5kIChtaW4td2lkdGg6IDEyMDFweCl7XG4gIC5zZWNvbmRhcnkucG9pbnRpbmcubWVudSAudG9jLml0ZW0ge1xuICAgIGRpc3BsYXk6IG5vbmU7XG4gIH1cbn1cblxuLnVpLm1pbmkuZHJvcGRvd24ge1xuICBmb250LXNpemU6IDAuNzE0Mjg1NzFlbTtcblxuICAubWVudSA+IC5pdGVtIHtcbiAgICBmb250LXNpemU6IDFlbTtcbiAgfVxufSIsIi8qIERvIG5vdCByZW1vdmUgdGhpcyBjb21tZW50cyBiZWxsb3cuIEl0J3MgdGhlIG1hcmtlcnMgdXNlZCBieSB3aXJlZGVwIHRvIGluamVjdFxuICAgc2FzcyBkZXBlbmRlbmNpZXMgd2hlbiBkZWZpbmVkIGluIHRoZSBib3dlci5qc29uIG9mIHlvdXIgZGVwZW5kZW5jaWVzICovXG4vLyBib3dlcjpzY3NzXG4vLyBlbmRib3dlclxuIiwiZm9vdGVyIHtcbiAgbWluLWhlaWdodDogMTByZW07XG59IiwiLnVpLmZvcm0gLmZpZWxkLnN1Y2Nlc3MgLmlucHV0LCAudWkuZm9ybSAuZmllbGQuc3VjY2VzcyBsYWJlbCxcbi51aS5mb3JtIC5maWVsZHMuc3VjY2VzcyAuZmllbGQgLmlucHV0LFxuLnVpLmZvcm0gLmZpZWxkcy5zdWNjZXNzIC5maWVsZCBsYWJlbCB7XG4gY29sb3I6ICMxYTUzMWI7XG59XG5cbi51aS5mb3JtIC5maWVsZC5zdWNjZXNzIGlucHV0Om5vdChbdHlwZV0pLCAudWkuZm9ybSAuZmllbGQuc3VjY2VzcyBpbnB1dFt0eXBlPXRleHRdLCAudWkuZm9ybSAuZmllbGQuc3VjY2VzcyBpbnB1dFt0eXBlPWVtYWlsXSwgLnVpLmZvcm0gLmZpZWxkLnN1Y2Nlc3MgaW5wdXRbdHlwZT1zZWFyY2hdLCAudWkuZm9ybSAuZmllbGQuc3VjY2VzcyBpbnB1dFt0eXBlPXBhc3N3b3JkXSwgLnVpLmZvcm0gLmZpZWxkLnN1Y2Nlc3MgaW5wdXRbdHlwZT1kYXRlXSwgLnVpLmZvcm0gLmZpZWxkLnN1Y2Nlc3MgaW5wdXRbdHlwZT1kYXRldGltZS1sb2NhbF0sIC51aS5mb3JtIC5maWVsZC5zdWNjZXNzIGlucHV0W3R5cGU9dGVsXSwgLnVpLmZvcm0gLmZpZWxkLnN1Y2Nlc3MgaW5wdXRbdHlwZT10aW1lXSwgLnVpLmZvcm0gLmZpZWxkLnN1Y2Nlc3MgaW5wdXRbdHlwZT11cmxdLCAudWkuZm9ybSAuZmllbGQuc3VjY2VzcyBpbnB1dFt0eXBlPW51bWJlcl0sIC51aS5mb3JtIC5maWVsZC5zdWNjZXNzIHNlbGVjdCwgLnVpLmZvcm0gLmZpZWxkLnN1Y2Nlc3MgdGV4dGFyZWEsIC51aS5mb3JtIC5maWVsZHMuc3VjY2VzcyAuZmllbGQgaW5wdXQ6bm90KFt0eXBlXSksIC51aS5mb3JtIC5maWVsZHMuc3VjY2VzcyAuZmllbGQgaW5wdXRbdHlwZT10ZXh0XSwgLnVpLmZvcm0gLmZpZWxkcy5zdWNjZXNzIC5maWVsZCBpbnB1dFt0eXBlPWVtYWlsXSwgLnVpLmZvcm0gLmZpZWxkcy5zdWNjZXNzIC5maWVsZCBpbnB1dFt0eXBlPXNlYXJjaF0sIC51aS5mb3JtIC5maWVsZHMuc3VjY2VzcyAuZmllbGQgaW5wdXRbdHlwZT1wYXNzd29yZF0sIC51aS5mb3JtIC5maWVsZHMuc3VjY2VzcyAuZmllbGQgaW5wdXRbdHlwZT1kYXRlXSwgLnVpLmZvcm0gLmZpZWxkcy5zdWNjZXNzIC5maWVsZCBpbnB1dFt0eXBlPWRhdGV0aW1lLWxvY2FsXSwgLnVpLmZvcm0gLmZpZWxkcy5zdWNjZXNzIC5maWVsZCBpbnB1dFt0eXBlPXRlbF0sIC51aS5mb3JtIC5maWVsZHMuc3VjY2VzcyAuZmllbGQgaW5wdXRbdHlwZT10aW1lXSwgLnVpLmZvcm0gLmZpZWxkcy5zdWNjZXNzIC5maWVsZCBpbnB1dFt0eXBlPXVybF0sIC51aS5mb3JtIC5maWVsZHMuc3VjY2VzcyAuZmllbGQgaW5wdXRbdHlwZT1udW1iZXJdLCAudWkuZm9ybSAuZmllbGRzLnN1Y2Nlc3MgLmZpZWxkIHNlbGVjdCwgLnVpLmZvcm0gLmZpZWxkcy5zdWNjZXNzIC5maWVsZCB0ZXh0YXJlYSB7XG4gICAgYmFja2dyb3VuZDogI2ZjZmZmNTtcbiAgICBib3JkZXItY29sb3I6ICNhM2MyOTM7XG4gICAgY29sb3I6ICMyYzY2MmQ7XG4gICAgYm9yZGVyLXJhZGl1czogJyc7XG4gICAgYm94LXNoYWRvdzogbm9uZTtcbn0iLCIuY29sbGVjdGlvbiB7XG4gIG1hcmdpbi10b3A6IDJyZW07XG4gIHBvc2l0aW9uOiByZWxhdGl2ZTtcbiAgbWluLWhlaWdodDogMjByZW07XG59XG4iLCIudWkuc2VhcmNoID4gLnJlc3VsdHMge1xuICBkaXNwbGF5OiBibG9jaztcbn1cblxuLnVpLmNhdGVnb3J5LnNlYXJjaCA+IC5yZXN1bHRzIC5jYXRlZ29yeSA+IC5uYW1lIHtcbiAgQGluY2x1ZGUgd29yZC13cmFwKCk7XG59XG5cbi51aS5zZWFyY2ggPiAucmVzdWx0cyAucmVzdWx0IHtcbiAgLmRlc2NyaXB0aW9uLCAudGl0bGUge1xuICAgIEBpbmNsdWRlIGVsbGlwc2lzKCk7XG4gIH1cbn0iXSwic291cmNlUm9vdCI6Ii9zb3VyY2UvIn0= */"],"sourceRoot":"/source/"} -------------------------------------------------------------------------------- /dist-demo/styles/app-3086bb8113.css: -------------------------------------------------------------------------------- 1 | .dimmer,.stats{top:0;-webkit-transition:all 1s ease-in-out}.site{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column}main{-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1}.ui.list .list>.item .description,.ui.list>.item .description{word-break:break-word;-webkit-hyphens:auto;-moz-hyphens:auto;-ms-hyphens:auto;hyphens:auto}.ui.grid{margin-left:0;margin-right:0}.ui.container{position:relative}.ui.sidebar.menu{z-index:20000}.pushable>.pusher.dimmed .ui.loading.segment:before{z-index:1}.pushable>.pusher{position:absolute;left:0;right:0}.ui.left.sidebar{-webkit-transform:translate3d(-260px,0,0);transform:translate3d(-260px,0,0)}.ui.left.floated{float:left}.ui.right.floated{float:right}.icon.spin{-webkit-animation:spin 1s infinite linear;animation:spin 1s infinite linear}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.show-error,.show-loading,.show-ok{display:none}.error .show-error,.loading .show-loading,.ok .show-ok{display:inline-block}.stats{position:absolute;width:100vw;left:50%;z-index:10;-webkit-transform:translateX(-200%);-ms-transform:translateX(-200%);transform:translateX(-200%);background:#fff;transition:all 1s ease-in-out;border-right:1px #444 solid}.dimmer{display:none;background:#000;opacity:0;position:fixed;bottom:0;left:0;right:0;z-index:5;transition:all 1s ease-in-out}.site{min-height:100vh;-webkit-transition:all 1s ease-in-out;transition:all 1s ease-in-out}.show-stats .stats{-webkit-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%)}.show-stats .dimmer{display:block;opacity:.5}.show-stats .site{-webkit-transform:translateX(50%);-ms-transform:translateX(50%);transform:translateX(50%)}.ui.segment.fixed{position:fixed;bottom:1em;right:1em;z-index:200}a[ui-sref]{cursor:pointer}.narrow.container{max-width:80rem;margin:auto;padding:3rem}footer{min-height:10rem}.ui.form .field.success .input,.ui.form .field.success label,.ui.form .fields.success .field .input,.ui.form .fields.success .field label{color:#1a531b}.ui.form .field.success input:not([type]),.ui.form .field.success input[type=date],.ui.form .field.success input[type=datetime-local],.ui.form .field.success input[type=email],.ui.form .field.success input[type=number],.ui.form .field.success input[type=password],.ui.form .field.success input[type=search],.ui.form .field.success input[type=tel],.ui.form .field.success input[type=text],.ui.form .field.success input[type=time],.ui.form .field.success input[type=url],.ui.form .field.success select,.ui.form .field.success textarea,.ui.form .fields.success .field input:not([type]),.ui.form .fields.success .field input[type=date],.ui.form .fields.success .field input[type=datetime-local],.ui.form .fields.success .field input[type=email],.ui.form .fields.success .field input[type=number],.ui.form .fields.success .field input[type=password],.ui.form .fields.success .field input[type=search],.ui.form .fields.success .field input[type=tel],.ui.form .fields.success .field input[type=text],.ui.form .fields.success .field input[type=time],.ui.form .fields.success .field input[type=url],.ui.form .fields.success .field select,.ui.form .fields.success .field textarea{background:#fcfff5;border-color:#a3c293;color:#2c662d;border-radius:'';box-shadow:none}.collection{margin-top:2rem;position:relative;min-height:20rem}.ui.search>.results{display:block}.ui.category.search>.results .category>.name{word-break:break-word;-webkit-hyphens:auto;-moz-hyphens:auto;-ms-hyphens:auto;hyphens:auto}.ui.search>.results .result .description,.ui.search>.results .result .title{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}@media only screen and (max-width:1200px){.secondary.pointing.menu .item,.secondary.pointing.menu .menu{display:none}.secondary.pointing.menu .toc.item{display:block}}@media only screen and (min-width:1201px){.secondary.pointing.menu .toc.item{display:none}}.ui.mini.dropdown{font-size:.71428571em}.ui.mini.dropdown .menu>.item{font-size:1em} 2 | /*# sourceMappingURL=../maps/styles/app-3086bb8113.css.map */ -------------------------------------------------------------------------------- /gulp/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /gulp/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var gulp = require('gulp'); 5 | var conf = require('./conf'); 6 | 7 | var $ = require('gulp-load-plugins')({ 8 | pattern: ['gulp-*', 'main-bower-files', 'uglify-save-license', 'del'] 9 | }); 10 | 11 | gulp.task('build:lib', function() { 12 | return gulp.src([ 13 | path.join(conf.paths.lib, '/**/*.js'), 14 | path.join('!' + conf.paths.lib, '/**/*.spec.js'), 15 | path.join('!' + conf.paths.lib, '/**/*.mock.js') 16 | ]) 17 | .pipe($.sourcemaps.init()) 18 | .pipe($.ngAnnotate()) 19 | .pipe($.angularFilesort()) 20 | .pipe($.concat('angular-jsonapi.js')) 21 | .pipe($.sourcemaps.write('./')) 22 | .pipe(gulp.dest(conf.paths.dist.lib + '/')) 23 | .pipe($.filter('angular-jsonapi.js')) 24 | .pipe($.rename('angular-jsonapi.min.js')) 25 | 26 | .pipe($.uglify({ preserveComments: $.uglifySaveLicense })).on('error', conf.errorHandler('Uglify')) 27 | .pipe($.sourcemaps.write('./')) 28 | .pipe(gulp.dest(conf.paths.dist.lib + '/')) 29 | .pipe($.size({ title: conf.paths.dist.lib + '/', showFiles: true })); 30 | }); 31 | 32 | gulp.task('partials', function() { 33 | return gulp.src([ 34 | path.join(conf.paths.src, '/app/**/*.html'), 35 | path.join(conf.paths.tmp, '/serve/app/**/*.html') 36 | ]) 37 | .pipe($.minifyHtml({ 38 | empty: true, 39 | spare: true, 40 | quotes: true 41 | })) 42 | .pipe($.angularTemplatecache('templateCacheHtml.js', { 43 | module: 'angularJsonapiExample', 44 | root: 'app' 45 | })) 46 | .pipe(gulp.dest(conf.paths.tmp + '/partials/')); 47 | }); 48 | 49 | gulp.task('html', ['inject', 'partials'], function() { 50 | var partialsInjectFile = gulp.src(path.join(conf.paths.tmp, '/partials/templateCacheHtml.js'), { read: false }); 51 | var partialsInjectOptions = { 52 | starttag: '', 53 | ignorePath: path.join(conf.paths.tmp, '/partials'), 54 | addRootSlash: false 55 | }; 56 | 57 | var htmlFilter = $.filter('*.html', { restore: true }); 58 | var jsFilter = $.filter('**/*.js', { restore: true }); 59 | var cssFilter = $.filter('**/*.css', { restore: true }); 60 | var assets; 61 | 62 | return gulp.src(path.join(conf.paths.tmp, '/serve/*.html')) 63 | .pipe($.inject(partialsInjectFile, partialsInjectOptions)) 64 | .pipe(assets = $.useref.assets()) 65 | .pipe($.rev()) 66 | .pipe(jsFilter) 67 | .pipe($.replace('localhost:3000', 'jsonapi-robot-wars.herokuapp.com')) 68 | .pipe($.sourcemaps.init()) 69 | .pipe($.ngAnnotate()) 70 | .pipe($.uglify({ preserveComments: $.uglifySaveLicense })).on('error', conf.errorHandler('Uglify')) 71 | .pipe($.sourcemaps.write('maps')) 72 | .pipe(jsFilter.restore) 73 | .pipe(cssFilter) 74 | .pipe($.replace('themes/default/assets/', '../')) 75 | .pipe($.sourcemaps.init()) 76 | .pipe($.minifyCss({ processImport: false })) 77 | .pipe($.sourcemaps.write('maps')) 78 | .pipe(cssFilter.restore) 79 | .pipe(assets.restore()) 80 | .pipe($.useref()) 81 | .pipe($.revReplace()) 82 | .pipe(htmlFilter) 83 | .pipe($.minifyHtml({ 84 | empty: true, 85 | spare: true, 86 | quotes: true, 87 | conditionals: true 88 | })) 89 | .pipe(htmlFilter.restore) 90 | .pipe(gulp.dest(path.join(conf.paths.dist.demo, '/'))) 91 | .pipe($.size({ title: path.join(conf.paths.dist.demo, '/'), showFiles: true })); 92 | }); 93 | 94 | gulp.task('fonts', function() { 95 | return gulp.src($.mainBowerFiles({includeDev: 'inclusive', overrides: conf.overrides})) 96 | .pipe($.filter('**/*.{eot,svg,ttf,woff,woff2}')) 97 | 98 | .pipe($.flatten()) 99 | .pipe(gulp.dest(path.join(conf.paths.dist.demo, '/fonts/'))); 100 | }); 101 | 102 | gulp.task('other', function() { 103 | var fileFilter = $.filter(function(file) { 104 | return file.stat.isFile(); 105 | }); 106 | 107 | return gulp.src([ 108 | path.join(conf.paths.src, '/**/*'), 109 | path.join('!' + conf.paths.src, '/**/*.{html,css,js,scss}') 110 | ]) 111 | .pipe(fileFilter) 112 | .pipe(gulp.dest(path.join(conf.paths.dist.demo, '/'))); 113 | }); 114 | 115 | gulp.task('clean', function() { 116 | return $.del([ 117 | path.join(conf.paths.dist.demo, '/'), 118 | path.join(conf.paths.tmp, '/'), 119 | path.join(conf.paths.dist.lib, '/') 120 | ]); 121 | }); 122 | 123 | gulp.task('build:demo', ['html', 'fonts', 'other']); 124 | -------------------------------------------------------------------------------- /gulp/conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains the variables used in other gulp files 3 | * which defines tasks 4 | * By design, we only put there very generic config values 5 | * which are used in several places to keep good readability 6 | * of the tasks 7 | */ 8 | 9 | var gutil = require('gulp-util'); 10 | 11 | /** 12 | * The main paths of your project handle these with care 13 | */ 14 | exports.paths = { 15 | src: 'demo', 16 | lib: 'src', 17 | dist: { 18 | lib: 'dist', 19 | demo: 'dist-demo' 20 | }, 21 | tmp: '.tmp', 22 | e2e: 'e2e' 23 | }; 24 | 25 | exports.overrides = { 26 | semantic: { 27 | main: [ 28 | 'dist/semantic.css', 29 | 'dist/semantic.js', 30 | 'dist/themes/default/assets/fonts/icons.eot', 31 | 'dist/themes/default/assets/fonts/icons.otf', 32 | 'dist/themes/default/assets/fonts/icons.svg', 33 | 'dist/themes/default/assets/fonts/icons.ttf', 34 | 'dist/themes/default/assets/fonts/icons.woff', 35 | 'dist/themes/default/assets/fonts/icons.woff2' 36 | ] 37 | } 38 | }; 39 | 40 | /** 41 | * Wiredep is the lib which inject bower dependencies in your project 42 | * Mainly used to inject script tags in the index.html but also used 43 | * to inject css preprocessor deps and js files in karma 44 | */ 45 | exports.wiredep = { 46 | exclude: [/bootstrap.js$/, /bootstrap-sass-official\/.*\.js/, /bootstrap\.css/, /semantic\.less/], 47 | devDependencies: true, 48 | directory: 'bower_components', 49 | overrides: exports.overrides 50 | }; 51 | 52 | /** 53 | * Common implementation for an error handler of a Gulp plugin 54 | */ 55 | exports.errorHandler = function(title) { 56 | 'use strict'; 57 | 58 | return function(err) { 59 | gutil.log(gutil.colors.red('[' + title + ']'), err.toString()); 60 | this.emit('end'); 61 | }; 62 | }; 63 | -------------------------------------------------------------------------------- /gulp/e2e-tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var gulp = require('gulp'); 5 | var conf = require('./conf'); 6 | 7 | var browserSync = require('browser-sync'); 8 | 9 | var $ = require('gulp-load-plugins')(); 10 | 11 | // Downloads the selenium webdriver 12 | gulp.task('webdriver-update', $.protractor.webdriver_update); 13 | 14 | gulp.task('webdriver-standalone', $.protractor.webdriver_standalone); 15 | 16 | function runProtractor (done) { 17 | var params = process.argv; 18 | var args = params.length > 3 ? [params[3], params[4]] : []; 19 | 20 | gulp.src(path.join(conf.paths.e2e, '/**/*.js')) 21 | .pipe($.protractor.protractor({ 22 | configFile: 'protractor.conf.js', 23 | args: args 24 | })) 25 | .on('error', function (err) { 26 | // Make sure failed tests cause gulp to exit non-zero 27 | throw err; 28 | }) 29 | .on('end', function () { 30 | // Close browser sync server 31 | browserSync.exit(); 32 | done(); 33 | }); 34 | } 35 | 36 | gulp.task('protractor', ['protractor:src']); 37 | gulp.task('protractor:src', ['serve:e2e', 'webdriver-update'], runProtractor); 38 | gulp.task('protractor:dist', ['serve:e2e-dist', 'webdriver-update'], runProtractor); 39 | -------------------------------------------------------------------------------- /gulp/inject.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var gulp = require('gulp'); 5 | var conf = require('./conf'); 6 | 7 | var $ = require('gulp-load-plugins')(); 8 | 9 | var wiredep = require('wiredep').stream; 10 | var _ = require('lodash'); 11 | 12 | gulp.task('inject', ['scripts', 'styles'], function() { 13 | var injectStyles = gulp.src([ 14 | path.join(conf.paths.tmp, '/serve/app/**/*.css'), 15 | path.join('!' + conf.paths.tmp, '/serve/app/vendor.css') 16 | ], { read: false }); 17 | 18 | var injectScripts = gulp.src([ 19 | path.join(conf.paths.src, '/app/**/*.module.js'), 20 | path.join(conf.paths.src, '/app/**/*.js'), 21 | path.join('!' + conf.paths.src, '/app/**/*.spec.js'), 22 | path.join('!' + conf.paths.src, '/app/**/*.mock.js') 23 | ]) 24 | .pipe($.angularFilesort()).on('error', conf.errorHandler('AngularFilesort')); 25 | 26 | var injectLib = gulp.src([ 27 | path.join(conf.paths.lib, '/app/**/*.module.js'), 28 | path.join(conf.paths.lib, '/**/*.js'), 29 | path.join('!' + conf.paths.lib, '/**/*.spec.js'), 30 | path.join('!' + conf.paths.lib, '/**/*.mock.js') 31 | ]) 32 | .pipe($.angularFilesort()).on('error', conf.errorHandler('AngularFilesort')); 33 | 34 | var injectOptions = { 35 | ignorePath: [conf.paths.src, path.join(conf.paths.tmp, '/serve')], 36 | addRootSlash: false 37 | }; 38 | 39 | var injectLibOptions = { 40 | ignorePath: [conf.paths.src, path.join(conf.paths.tmp, '/serve')], 41 | addRootSlash: false, 42 | name: 'lib' 43 | }; 44 | 45 | return gulp.src(path.join(conf.paths.src, '/*.html')) 46 | .pipe($.inject(injectStyles, injectOptions)) 47 | .pipe($.inject(injectScripts, injectOptions)) 48 | .pipe($.inject(injectLib, injectLibOptions)) 49 | .pipe(wiredep(_.extend({}, conf.wiredep))) 50 | .pipe(gulp.dest(path.join(conf.paths.tmp, '/serve'))); 51 | }); 52 | -------------------------------------------------------------------------------- /gulp/scripts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var gulp = require('gulp'); 5 | var conf = require('./conf'); 6 | 7 | var browserSync = require('browser-sync'); 8 | 9 | var $ = require('gulp-load-plugins')(); 10 | 11 | gulp.task('scripts', function() { 12 | return gulp.src([ 13 | path.join(conf.paths.src, '/app/**/*.js'), 14 | path.join(conf.paths.lib, '/**/*.js') 15 | ]) 16 | .pipe($.eslint()) 17 | .pipe($.eslint.format()) 18 | .pipe(browserSync.reload({ stream: true })) 19 | .pipe($.size()); 20 | }); 21 | -------------------------------------------------------------------------------- /gulp/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var gulp = require('gulp'); 5 | var conf = require('./conf'); 6 | 7 | var browserSync = require('browser-sync'); 8 | var browserSyncSpa = require('browser-sync-spa'); 9 | 10 | var util = require('util'); 11 | 12 | var proxyMiddleware = require('http-proxy-middleware'); 13 | 14 | function browserSyncInit(baseDir, browser) { 15 | browser = browser === undefined ? 'default' : browser; 16 | 17 | var routes = null; 18 | if (baseDir === conf.paths.src || (util.isArray(baseDir) && baseDir.indexOf(conf.paths.src) !== -1)) { 19 | routes = { 20 | '/bower_components': 'bower_components' 21 | }; 22 | 23 | routes['/' + conf.paths.lib] = conf.paths.lib; 24 | } 25 | 26 | var server = { 27 | baseDir: baseDir, 28 | routes: routes 29 | }; 30 | 31 | /* 32 | * You can add a proxy to your backend by uncommenting the line bellow. 33 | * You just have to configure a context which will we redirected and the target url. 34 | * Example: $http.get('/users') requests will be automatically proxified. 35 | * 36 | * For more details and option, https://github.com/chimurai/http-proxy-middleware/blob/v0.0.5/README.md 37 | */ 38 | // server.middleware = proxyMiddleware('/users', {target: 'http://jsonplaceholder.typicode.com', proxyHost: 'jsonplaceholder.typicode.com'}); 39 | 40 | browserSync.instance = browserSync.init({ 41 | startPath: '/', 42 | port: 5000, 43 | server: server, 44 | browser: browser 45 | }); 46 | } 47 | 48 | browserSync.use(browserSyncSpa({ 49 | selector: '[ng-app]'// Only needed for angular apps 50 | })); 51 | 52 | gulp.task('serve', ['watch'], function() { 53 | browserSyncInit([path.join(conf.paths.tmp, '/serve'), conf.paths.src]); 54 | }); 55 | 56 | gulp.task('serve:dist', ['build:demo'], function() { 57 | browserSyncInit(conf.paths.dist.demo); 58 | }); 59 | 60 | gulp.task('serve:e2e', ['inject'], function() { 61 | browserSyncInit([conf.paths.tmp + '/serve', conf.paths.src], []); 62 | }); 63 | 64 | gulp.task('serve:e2e-dist', ['build:lib'], function() { 65 | browserSyncInit(conf.paths.dist.lib, []); 66 | }); 67 | -------------------------------------------------------------------------------- /gulp/styles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var gulp = require('gulp'); 5 | var conf = require('./conf'); 6 | 7 | var browserSync = require('browser-sync'); 8 | 9 | var $ = require('gulp-load-plugins')(); 10 | 11 | var wiredep = require('wiredep').stream; 12 | var _ = require('lodash'); 13 | 14 | gulp.task('styles', function() { 15 | var sassOptions = { 16 | style: 'expanded' 17 | }; 18 | 19 | var injectFiles = gulp.src([ 20 | path.join(conf.paths.src, '/app/**/*.scss'), 21 | path.join('!' + conf.paths.src, '/app/index.scss') 22 | ], { read: false }); 23 | 24 | var injectOptions = { 25 | transform: function(filePath) { 26 | filePath = filePath.replace(conf.paths.src + '/app/', ''); 27 | return '@import "' + filePath + '";'; 28 | }, 29 | 30 | starttag: '// injector', 31 | endtag: '// endinjector', 32 | addRootSlash: false 33 | }; 34 | 35 | return gulp.src([ 36 | path.join(conf.paths.src, '/app/index.scss') 37 | ]) 38 | .pipe($.inject(injectFiles, injectOptions)) 39 | .pipe(wiredep(_.extend({}, conf.wiredep))) 40 | .pipe($.sourcemaps.init()) 41 | .pipe($.sass(sassOptions)).on('error', conf.errorHandler('Sass')) 42 | .pipe($.autoprefixer()).on('error', conf.errorHandler('Autoprefixer')) 43 | .pipe($.sourcemaps.write()) 44 | .pipe(gulp.dest(path.join(conf.paths.tmp, '/serve/app/'))) 45 | .pipe(browserSync.reload({ stream: true })); 46 | }); 47 | -------------------------------------------------------------------------------- /gulp/unit-tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var gulp = require('gulp'); 5 | var conf = require('./conf'); 6 | 7 | var karma = require('karma'); 8 | 9 | function runTests (singleRun, done) { 10 | karma.server.start({ 11 | configFile: path.join(__dirname, '/../karma.conf.js'), 12 | singleRun: singleRun, 13 | autoWatch: !singleRun 14 | }, function() { 15 | done(); 16 | }); 17 | } 18 | 19 | gulp.task('test', ['scripts'], function(done) { 20 | runTests(true, done); 21 | }); 22 | 23 | gulp.task('test:auto', ['watch'], function(done) { 24 | runTests(false, done); 25 | }); 26 | -------------------------------------------------------------------------------- /gulp/watch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var gulp = require('gulp'); 5 | var conf = require('./conf'); 6 | 7 | var browserSync = require('browser-sync'); 8 | 9 | function isOnlyChange(event) { 10 | return event.type === 'changed'; 11 | } 12 | 13 | gulp.task('watch', ['inject'], function() { 14 | 15 | gulp.watch([path.join(conf.paths.src, '/*.html'), 'bower.json'], ['inject']); 16 | 17 | gulp.watch([ 18 | path.join(conf.paths.src, '/app/**/*.css'), 19 | path.join(conf.paths.src, '/app/**/*.scss') 20 | ], function(event) { 21 | if (isOnlyChange(event)) { 22 | gulp.start('styles'); 23 | } else { 24 | gulp.start('inject'); 25 | } 26 | }); 27 | 28 | gulp.watch([ 29 | path.join(conf.paths.src, '/app/**/*.js'), 30 | path.join(conf.paths.lib, '/**/*.js') 31 | ], function(event) { 32 | if (isOnlyChange(event)) { 33 | gulp.start('scripts'); 34 | } else { 35 | gulp.start('inject'); 36 | } 37 | }); 38 | 39 | gulp.watch(path.join(conf.paths.lib, '/**/*.js'), function() { 40 | gulp.start('build:lib'); 41 | }); 42 | 43 | gulp.watch(path.join(conf.paths.src, '/app/**/*.html'), function(event) { 44 | browserSync.reload(event.path); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Welcome to your gulpfile! 3 | * The gulp tasks are splitted in several files in the gulp directory 4 | * because putting all here was really too long 5 | */ 6 | 7 | 'use strict'; 8 | 9 | var gulp = require('gulp'); 10 | var wrench = require('wrench'); 11 | 12 | /** 13 | * This will load all js or coffee files in the gulp directory 14 | * in order to load all gulp tasks 15 | */ 16 | wrench.readdirSyncRecursive('./gulp').filter(function(file) { 17 | return (/\.(js|coffee)$/i).test(file); 18 | }).map(function(file) { 19 | require('./gulp/' + file); 20 | }); 21 | 22 | 23 | /** 24 | * Default task clean temporaries directories and launch the 25 | * main optimization build task 26 | */ 27 | gulp.task('default', ['clean'], function() { 28 | gulp.start('build:lib'); 29 | gulp.start('build:demo'); 30 | }); 31 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var conf = require('./gulp/conf'); 5 | 6 | var _ = require('lodash'); 7 | var wiredep = require('wiredep'); 8 | 9 | function listFiles() { 10 | var wiredepOptions = _.extend({}, conf.wiredep, { 11 | dependencies: true, 12 | devDependencies: true 13 | }); 14 | 15 | return wiredep(wiredepOptions).js 16 | .concat([ 17 | path.join(conf.paths.lib, '/**/*.module.js'), 18 | path.join(conf.paths.lib, '/**/*.js'), 19 | path.join(conf.paths.lib, '/**/*.spec.js'), 20 | path.join(conf.paths.lib, '/**/*.mock.js'), 21 | path.join(conf.paths.lib, '/**/*.html') 22 | ]); 23 | } 24 | 25 | module.exports = function(config) { 26 | 27 | var configuration = { 28 | files: listFiles(), 29 | 30 | singleRun: true, 31 | 32 | autoWatch: false, 33 | 34 | frameworks: ['jasmine', 'angular-filesort'], 35 | 36 | angularFilesort: { 37 | whitelist: [path.join(conf.paths.lib, '/**/!(*.html|*.spec|*.mock).js')] 38 | }, 39 | 40 | ngHtml2JsPreprocessor: { 41 | stripPrefix: 'src/', 42 | moduleName: 'angularJsonapi' 43 | }, 44 | 45 | browsers : ['PhantomJS'], 46 | 47 | plugins : [ 48 | 'karma-phantomjs-launcher', 49 | 'karma-angular-filesort', 50 | 'karma-jasmine', 51 | 'karma-ng-html2js-preprocessor' 52 | ], 53 | 54 | preprocessors: { 55 | 'src/**/*.html': ['ng-html2js'] 56 | } 57 | }; 58 | 59 | // This block is needed to execute Chrome on Travis 60 | // If you ever plan to use Chrome and Travis, you can keep it 61 | // If not, you can safely remove it 62 | // https://github.com/karma-runner/karma/issues/1144#issuecomment-53633076 63 | if(configuration.browsers[0] === 'Chrome' && process.env.TRAVIS) { 64 | configuration.customLaunchers = { 65 | 'chrome-travis-ci': { 66 | base: 'Chrome', 67 | flags: ['--no-sandbox'] 68 | } 69 | }; 70 | configuration.browsers = ['chrome-travis-ci']; 71 | } 72 | 73 | config.set(configuration); 74 | }; 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-jsonapi", 3 | "version": "1.0.0-alpha.7", 4 | "dependencies": {}, 5 | "scripts": { 6 | "test": "gulp test" 7 | }, 8 | "devDependencies": { 9 | "browser-sync": "~2.9.2", 10 | "browser-sync-spa": "~1.0.2", 11 | "chalk": "~1.1.1", 12 | "del": "~2.0.1", 13 | "eslint-plugin-angular": "~0.11.0", 14 | "gulp": "~3.9.0", 15 | "gulp-angular-filesort": "~1.1.1", 16 | "gulp-angular-templatecache": "~1.7.0", 17 | "gulp-autoprefixer": "~3.0.1", 18 | "gulp-concat": "^2.6.0", 19 | "gulp-debug": "^2.1.0", 20 | "gulp-eslint": "~1.0.0", 21 | "gulp-filter": "~3.0.1", 22 | "gulp-flatten": "~0.2.0", 23 | "gulp-inject": "~1.5.0", 24 | "gulp-load-plugins": "~0.10.0", 25 | "gulp-minify-css": "~1.2.1", 26 | "gulp-minify-html": "~1.0.4", 27 | "gulp-ng-annotate": "~1.1.0", 28 | "gulp-protractor": "~1.0.0", 29 | "gulp-rename": "~1.2.2", 30 | "gulp-replace": "~0.5.4", 31 | "gulp-rev": "~6.0.1", 32 | "gulp-rev-replace": "~0.4.2", 33 | "gulp-sass": "~2.0.4", 34 | "gulp-size": "~2.0.0", 35 | "gulp-sourcemaps": "~1.5.2", 36 | "gulp-uglify": "~1.4.0", 37 | "gulp-useref": "~1.3.0", 38 | "gulp-util": "~3.0.6", 39 | "http-proxy-middleware": "~0.8.0", 40 | "karma": "~0.13.9", 41 | "karma-angular-filesort": "~1.0.0", 42 | "karma-coverage": "~0.5.2", 43 | "karma-jasmine": "~0.3.6", 44 | "karma-ng-html2js-preprocessor": "~0.1.2", 45 | "karma-phantomjs-launcher": "~0.2.1", 46 | "lodash": "~3.10.1", 47 | "main-bower-files": "~2.9.0", 48 | "uglify-save-license": "~0.4.1", 49 | "wiredep": "~2.2.2", 50 | "wrench": "~1.5.8" 51 | }, 52 | "engines": { 53 | "node": ">=0.10.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var paths = require('./.yo-rc.json')['generator-gulp-angular'].props.paths; 4 | 5 | // An example configuration file. 6 | exports.config = { 7 | // The address of a running selenium server. 8 | //seleniumAddress: 'http://localhost:4444/wd/hub', 9 | //seleniumServerJar: deprecated, this should be set on node_modules/protractor/config.json 10 | 11 | // Capabilities to be passed to the webdriver instance. 12 | capabilities: { 13 | 'browserName': 'chrome' 14 | }, 15 | 16 | baseUrl: 'http://localhost:3000', 17 | 18 | // Spec patterns are relative to the current working directly when 19 | // protractor is called. 20 | specs: [paths.e2e + '/**/*.js'], 21 | 22 | // Options to be passed to Jasmine-node. 23 | jasmineNodeOpts: { 24 | showColors: true, 25 | defaultTimeoutInterval: 30000 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/collection/collection.factory.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angular-jsonapi') 5 | .factory('AngularJsonAPICollection', AngularJsonAPICollectionWrapper); 6 | 7 | function AngularJsonAPICollectionWrapper( 8 | AngularJsonAPIModelSourceError, 9 | AngularJsonAPIModelErrorsManager, 10 | $rootScope, 11 | $injector, 12 | $q 13 | ) { 14 | 15 | AngularJsonAPICollection.prototype.fetch = fetch; 16 | AngularJsonAPICollection.prototype.refresh = fetch; 17 | AngularJsonAPICollection.prototype.get = get; 18 | AngularJsonAPICollection.prototype.hasErrors = hasErrors; 19 | 20 | return { 21 | create: AngularJsonAPICollectionFactory 22 | }; 23 | 24 | function AngularJsonAPICollectionFactory(resource, params) { 25 | return new AngularJsonAPICollection(resource, params); 26 | } 27 | 28 | /** 29 | * Constructor 30 | * @param {AngularJsonAPIResource} resource Factory associated with the collection 31 | * @param {object} params Params associated with this resource (such as filters) 32 | */ 33 | function AngularJsonAPICollection(resource, params) { 34 | var _this = this; 35 | 36 | _this.resource = resource; 37 | _this.type = resource.schema.type; 38 | _this.params = params || {}; 39 | 40 | _this.errors = { 41 | synchronization: AngularJsonAPIModelErrorsManager.create( 42 | 'Source', 43 | 'Errors of synchronizations', 44 | AngularJsonAPIModelSourceError 45 | ) 46 | }; 47 | 48 | _this.data = _this.resource.cache.index(_this.params); 49 | 50 | _this.loading = false; 51 | _this.loadingCount = 0; 52 | _this.synchronized = false; 53 | _this.pristine = _this.data === undefined; 54 | 55 | _this.promise = $q.resolve(_this); 56 | 57 | var onObjectRemove = $rootScope.$on('angularJsonAPI:' + _this.type + ':object:remove', remove); 58 | var onFactoryClear = $rootScope.$on('angularJsonAPI:' + _this.type + ':resource:clearCache', clear); 59 | var onObjectAdd = $rootScope.$on('angularJsonAPI:' + _this.type + ':object:add', add); 60 | 61 | $rootScope.$on('$destroy', clearWatchers); 62 | 63 | function remove(event, status, object) { 64 | var index; 65 | 66 | if (status === 'resolved' && _this.data !== undefined) { 67 | index = _this.data.indexOf(object); 68 | if (index > -1) { 69 | _this.data.splice(index, 1); 70 | _this.resource.cache.setIndexIds(_this.data); 71 | } 72 | } 73 | } 74 | 75 | function clear() { 76 | _this.data = undefined; 77 | _this.pristine = true; 78 | } 79 | 80 | function add(event, status, object) { 81 | if (status === 'resolved') { 82 | _this.data = _this.data || []; 83 | _this.data.push(object); 84 | } 85 | } 86 | 87 | function clearWatchers() { 88 | onObjectRemove(); 89 | onFactoryClear(); 90 | onObjectAdd(); 91 | } 92 | } 93 | 94 | /** 95 | * Check if the object has errors 96 | * @return {Boolean} 97 | */ 98 | function hasErrors() { 99 | var _this = this; 100 | var answer = false; 101 | 102 | angular.forEach(_this.errors, function(error) { 103 | answer = error.hasErrors() || answer; 104 | }); 105 | 106 | return answer; 107 | } 108 | 109 | /** 110 | * Shortcut to this.resource.get 111 | * @param {string} id Id of object] 112 | * @return {AngularJsonAPIModel} Model with id 113 | */ 114 | function get(id, params) { 115 | var _this = this; 116 | 117 | return _this.resource.get(id, params); 118 | } 119 | 120 | /** 121 | * Synchronizes collection with the server 122 | * @return {promise} Promise associated with synchronization that resolves to this 123 | */ 124 | function fetch() { 125 | var _this = this; 126 | var deferred = $q.defer(); 127 | var $jsonapi = $injector.get('$jsonapi'); 128 | var config = { 129 | action: 'all', 130 | params: _this.params 131 | }; 132 | 133 | __incrementLoadingCounter(_this); 134 | 135 | angular.forEach(_this.data, __incrementLoadingCounter); 136 | 137 | _this.resource.synchronizer.synchronize(config) 138 | .then(resolve, reject, notify) 139 | .finally(__decrementLoadingCounter.bind(_this, undefined)); 140 | 141 | return deferred.promise; 142 | 143 | function resolve(response) { 144 | var results = $jsonapi.__proccesResults(response.data); 145 | $rootScope.$emit('angularJsonAPI:' + _this.type + ':collection:fetch', 'resolved', _this, response); 146 | $q.allSettled(results.included.map(synchronizeIncluded)).then(resolveIncluded, deferred.reject); 147 | 148 | angular.forEach(_this.data, __decrementLoadingCounter); 149 | 150 | _this.data = results.data; 151 | _this.links = response.data.links; 152 | 153 | _this.updatedAt = Date.now(); 154 | _this.synchronized = true; 155 | _this.pristine = false; 156 | 157 | _this.resource.cache.setIndexIds(_this.data); 158 | response.finish(); 159 | _this.errors.synchronization.concat(response.errors); 160 | 161 | function synchronizeIncluded(object) { 162 | __incrementLoadingCounter(object); 163 | 164 | return object.synchronize({ 165 | action: 'include', 166 | object: object 167 | }).finally(__decrementLoadingCounter.bind(object, undefined)); 168 | } 169 | 170 | function resolveIncluded(includedResponse) { 171 | angular.forEach(includedResponse, function(operation, key) { 172 | if (operation.success === true) { 173 | $rootScope.$emit('angularJsonAPI:' + results.included[key].data.type + ':object:include', 'resolved', results.included[key], operation); 174 | 175 | operation.value.finish(); 176 | } 177 | }); 178 | 179 | deferred.resolve(response.data.meta); 180 | } 181 | } 182 | 183 | function reject(response) { 184 | $rootScope.$emit('angularJsonAPI:' + _this.type + ':collection:fetch', 'rejected', _this, response); 185 | 186 | angular.forEach(_this.data, __decrementLoadingCounter); 187 | response.finish(); 188 | _this.errors.synchronization.concat(response.errors); 189 | deferred.reject(_this); 190 | } 191 | 192 | function notify(response) { 193 | $rootScope.$emit('angularJsonAPI:' + _this.type + ':collection:fetch', 'notify', _this, response); 194 | 195 | deferred.notify(response); 196 | } 197 | } 198 | } 199 | 200 | function __incrementLoadingCounter(object) { 201 | object = object === undefined ? this : object; 202 | object.loadingCount += 1; 203 | object.loading = true; 204 | } 205 | 206 | function __decrementLoadingCounter(object) { 207 | object = object === undefined ? this : object; 208 | object.loadingCount -= 1; 209 | object.loading = object.loadingCount > 0; 210 | } 211 | })(); 212 | -------------------------------------------------------------------------------- /src/collection/collection.factory.spec.js: -------------------------------------------------------------------------------- 1 | /*jshint expr: true*/ 2 | (function() { 3 | 'use strict'; 4 | 5 | describe('AngularJsonAPICollection', function() { 6 | 7 | beforeEach(module('angular-jsonapi')); 8 | 9 | it('returns valid model', inject(function(AngularJsonAPICollection) { 10 | expect(AngularJsonAPICollection).toBeDefined(); 11 | })); 12 | }); 13 | })(); 14 | -------------------------------------------------------------------------------- /src/errors/errors-manager/errors-manager.factory.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angular-jsonapi') 5 | .factory('AngularJsonAPIModelErrorsManager', AngularJsonAPIModelErrorsManagerWrapper); 6 | 7 | function AngularJsonAPIModelErrorsManagerWrapper() { 8 | ErrorsManager.prototype.constructor = ErrorsManager; 9 | ErrorsManager.prototype.concat = concat; 10 | ErrorsManager.prototype.clear = clear; 11 | ErrorsManager.prototype.add = add; 12 | ErrorsManager.prototype.hasErrors = hasErrors; 13 | 14 | return { 15 | create: ErrorsManagerFactory 16 | }; 17 | 18 | function ErrorsManagerFactory(name, description, ErrorConstructor, defaultFilter) { 19 | return new ErrorsManager(name, description, ErrorConstructor, defaultFilter); 20 | } 21 | 22 | function ErrorsManager(name, description, ErrorConstructor, defaultFilter) { 23 | var _this = this; 24 | _this.name = name; 25 | _this.description = description; 26 | 27 | _this.ErrorConstructor = ErrorConstructor; 28 | _this.errors = {}; 29 | _this.defaultFilter = defaultFilter || function() { return true; }; 30 | } 31 | 32 | function clear(key) { 33 | var _this = this; 34 | 35 | if (key === undefined) { 36 | angular.forEach(_this.errors, function(obj, key) { 37 | _this.errors[key] = []; 38 | }); 39 | } else { 40 | _this.errors[key] = []; 41 | } 42 | } 43 | 44 | function add(key, error) { 45 | var _this = this; 46 | 47 | _this.errors[key] = _this.errors[key] || []; 48 | _this.errors[key].push(error); 49 | } 50 | 51 | function concat(errors) { 52 | var _this = this; 53 | 54 | angular.forEach(errors, function(error) { 55 | _this.errors[error.key] = []; 56 | }); 57 | 58 | angular.forEach(errors, function(error) { 59 | _this.errors[error.key].push(error.object); 60 | }); 61 | 62 | } 63 | 64 | function hasErrors(key) { 65 | var _this = this; 66 | 67 | if (key === undefined) { 68 | var answer = false; 69 | 70 | angular.forEach(_this.errors, function(error) { 71 | answer = answer || error.length > 0; 72 | }); 73 | 74 | return answer; 75 | } else { 76 | return _this.errors[key] !== undefined && _this.errors[key].length > 0; 77 | } 78 | } 79 | } 80 | })(); 81 | -------------------------------------------------------------------------------- /src/errors/errors-manager/errors-manager.factory.spec.js: -------------------------------------------------------------------------------- 1 | /*jshint expr: true*/ 2 | (function() { 3 | 'use strict'; 4 | 5 | describe('AngularJsonAPIModelErrorsManager', function() { 6 | 7 | beforeEach(module('angular-jsonapi')); 8 | 9 | it('returns valid model', inject(function(AngularJsonAPIModelErrorsManager) { 10 | expect(AngularJsonAPIModelErrorsManager).toBeDefined(); 11 | })); 12 | }); 13 | })(); 14 | -------------------------------------------------------------------------------- /src/errors/source-error/source-error.factory.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angular-jsonapi') 5 | .factory('AngularJsonAPIModelSourceError', AngularJsonAPIModelSourceErrorWrapper); 6 | 7 | function AngularJsonAPIModelSourceErrorWrapper() { 8 | SourceError.prototype = Object.create(Error.prototype); 9 | SourceError.prototype.constructor = SourceError; 10 | SourceError.prototype.name = 'SourceError'; 11 | 12 | return { 13 | create: SourceErrorFactory 14 | }; 15 | 16 | function SourceErrorFactory(message, source, code, action) { 17 | return new SourceError(message, source, code, action); 18 | } 19 | 20 | function SourceError(message, source, code, action) { 21 | var _this = this; 22 | 23 | _this.message = message; 24 | _this.context = { 25 | source: source, 26 | code: code, 27 | action: action 28 | }; 29 | } 30 | } 31 | })(); 32 | -------------------------------------------------------------------------------- /src/errors/source-error/source-error.factory.spec.js: -------------------------------------------------------------------------------- 1 | /*jshint expr: true*/ 2 | (function() { 3 | 'use strict'; 4 | 5 | describe('AngularJsonAPIModelSourceError', function() { 6 | 7 | beforeEach(module('angular-jsonapi')); 8 | 9 | it('returns valid model', inject(function(AngularJsonAPIModelSourceError) { 10 | expect(AngularJsonAPIModelSourceError).toBeDefined(); 11 | })); 12 | }); 13 | })(); 14 | -------------------------------------------------------------------------------- /src/errors/validation-error/validation-error.factory.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angular-jsonapi') 5 | .factory('AngularJsonAPIModelValidationError', AngularJsonAPIModelValidationErrorWrapper); 6 | 7 | function AngularJsonAPIModelValidationErrorWrapper() { 8 | ValidationError.prototype = Object.create(Error.prototype); 9 | ValidationError.prototype.constructor = ValidationError; 10 | ValidationError.prototype.name = 'ValidationError'; 11 | 12 | return { 13 | create: ValidationErrorFactory 14 | }; 15 | 16 | function ValidationErrorFactory(message, attribute) { 17 | return new ValidationError(message, attribute); 18 | } 19 | 20 | function ValidationError(message, attribute) { 21 | var _this = this; 22 | 23 | _this.message = message; 24 | _this.context = { 25 | attribute: attribute 26 | }; 27 | } 28 | } 29 | })(); 30 | -------------------------------------------------------------------------------- /src/errors/validation-error/validation-error.factory.spec.js: -------------------------------------------------------------------------------- 1 | /*jshint expr: true*/ 2 | (function() { 3 | 'use strict'; 4 | 5 | describe('AngularJsonAPIModelValidationError', function() { 6 | 7 | beforeEach(module('angular-jsonapi')); 8 | 9 | it('returns valid model', inject(function(AngularJsonAPIModelValidationError) { 10 | expect(AngularJsonAPIModelValidationError).toBeDefined(); 11 | })); 12 | }); 13 | })(); 14 | -------------------------------------------------------------------------------- /src/jsonapi.config.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angular-jsonapi') 5 | .config(function($logProvider) { 6 | $logProvider.debugEnabled(false); 7 | }) 8 | .run(function(validateJS, $q) { 9 | validateJS.Promise = $q; 10 | }); 11 | })(); 12 | -------------------------------------------------------------------------------- /src/jsonapi.module.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angular-jsonapi', ['uuid4']) 5 | /* global pluralize: false, validate: false */ 6 | .constant('pluralize', pluralize) 7 | .constant('validateJS', validate); 8 | })(); 9 | -------------------------------------------------------------------------------- /src/jsonapi.provider.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angular-jsonapi') 5 | .provider('$jsonapi', jsonapiProvider); 6 | 7 | function jsonapiProvider(validateJS) { 8 | var memory = {}; 9 | var names = []; 10 | this.$get = jsonapiFactory; 11 | 12 | function jsonapiFactory( 13 | $log, 14 | AngularJsonAPIResource, 15 | AngularJsonAPISynchronizerSimple 16 | ) { 17 | return { 18 | addResource: addResource, 19 | getResource: getResource, 20 | clearCache: clearCache, 21 | allResources: allResources, 22 | listResources: listResources, 23 | addValidator: addValidator, 24 | synchronizerSimple: AngularJsonAPISynchronizerSimple, 25 | 26 | __proccesResults: __proccesResults 27 | }; 28 | 29 | function addResource(schema, synchronizer) { 30 | var resource = AngularJsonAPIResource.create(schema, synchronizer); 31 | 32 | memory[schema.type] = resource; 33 | names.push(schema.type); 34 | } 35 | 36 | function getResource(type) { 37 | return memory[type]; 38 | } 39 | 40 | function allResources() { 41 | return memory; 42 | } 43 | 44 | function listResources() { 45 | return names; 46 | } 47 | 48 | function clearCache() { 49 | angular.forEach(memory, function(resource) { 50 | resource.clearCache(); 51 | }); 52 | } 53 | 54 | function addValidator(name, validator) { 55 | if (!angular.isString(name)) { 56 | $log.error('Validator name is not a string', name); 57 | return; 58 | } else if (validateJS.validators[name] === undefined) { 59 | $log.warn('Redeclaring validator', name); 60 | } 61 | 62 | validateJS.validators[name] = validator; 63 | } 64 | 65 | function __proccesResults(results) { 66 | var objects = { 67 | data: [], 68 | included: [] 69 | }; 70 | 71 | if (results === undefined) { 72 | $log.error('Can\'t proccess results:', results); 73 | return; 74 | } 75 | 76 | var config = { 77 | new: false, 78 | synchronized: true, 79 | stable: true, 80 | pristine: false, 81 | initialization: false 82 | }; 83 | 84 | angular.forEach(results.included, function(data) { 85 | objects.included.push(getResource(data.type).cache.addOrUpdate(data, config)); 86 | }); 87 | 88 | if (angular.isArray(results.data)) { 89 | angular.forEach(results.data, function(data) { 90 | objects.data.push(getResource(data.type).cache.addOrUpdate(data, config)); 91 | }); 92 | } else if (results.data !== undefined) { 93 | objects.data.push(getResource(results.data.type).cache.addOrUpdate(results.data, config)); 94 | } 95 | 96 | return objects; 97 | } 98 | } 99 | } 100 | 101 | })(); 102 | 103 | -------------------------------------------------------------------------------- /src/jsonapi.provider.spec.js: -------------------------------------------------------------------------------- 1 | /*jshint expr: true*/ 2 | (function() { 3 | 'use strict'; 4 | 5 | describe('$jsonapi', function() { 6 | 7 | beforeEach(module('angular-jsonapi')); 8 | 9 | it('returns valid model', inject(function($jsonapi) { 10 | expect($jsonapi).toBeDefined(); 11 | })); 12 | }); 13 | })(); 14 | -------------------------------------------------------------------------------- /src/model/abstract-model/abstract-model.spec.js: -------------------------------------------------------------------------------- 1 | /*jshint expr: true*/ 2 | (function() { 3 | 'use strict'; 4 | 5 | describe('AngularJsonAPIAbstractModel', function() { 6 | 7 | beforeEach(module('angular-jsonapi')); 8 | 9 | it('returns valid model', inject(function(AngularJsonAPIAbstractModel) { 10 | expect(AngularJsonAPIAbstractModel).toBeDefined(); 11 | })); 12 | }); 13 | })(); 14 | -------------------------------------------------------------------------------- /src/model/model-factory.factory.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angular-jsonapi') 5 | .factory('AngularJsonAPIModel', AngularJsonAPIModel); 6 | 7 | function AngularJsonAPIModel( 8 | AngularJsonAPIAbstractModel, 9 | AngularJsonAPISchema, 10 | namedFunction, 11 | pluralize, 12 | $log 13 | ) { 14 | 15 | return { 16 | modelFactory: createModelFactory 17 | }; 18 | 19 | function createModelFactory(schemaObj, resource) { 20 | var constructorName = pluralize.plural(schemaObj.type, 1); 21 | 22 | var Model = namedFunction(constructorName, function(data, config, updatedAt) { 23 | var _this = this; 24 | 25 | if (data.type !== _this.schema.type) { 26 | $log.error('Data type other then declared in schema: ', data.type, ' instead of ', _this.schema.type); 27 | } 28 | 29 | AngularJsonAPIAbstractModel.call(_this, data, config, updatedAt); 30 | 31 | _this.form.parent = _this; 32 | }); 33 | 34 | Model.prototype = Object.create(AngularJsonAPIAbstractModel.prototype); 35 | Model.prototype.constructor = Model; 36 | 37 | Model.prototype.schema = schemaObj; 38 | Model.prototype.resource = resource; 39 | Model.prototype.synchronize = resource.synchronizer.synchronize.bind(resource.synchronizer); 40 | 41 | angular.forEach(schemaObj.functions, function(metaFunction, metaFunctionName) { 42 | Model.prototype[metaFunctionName] = function() { 43 | return metaFunction.apply(this, arguments); 44 | }; 45 | }); 46 | 47 | return modelFactory; 48 | 49 | function modelFactory(data, updatedAt, isNew) { 50 | return new Model(data, updatedAt, isNew); 51 | } 52 | } 53 | } 54 | })(); 55 | -------------------------------------------------------------------------------- /src/model/model-factory.factory.spec.js: -------------------------------------------------------------------------------- 1 | /*jshint expr: true*/ 2 | (function() { 3 | 'use strict'; 4 | 5 | describe('AngularJsonAPIModel', function() { 6 | 7 | beforeEach(module('angular-jsonapi')); 8 | 9 | it('returns valid model', inject(function(AngularJsonAPIModel) { 10 | expect(AngularJsonAPIModel).toBeDefined(); 11 | })); 12 | }); 13 | })(); 14 | -------------------------------------------------------------------------------- /src/model/model-form/model-form.factory.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angular-jsonapi') 5 | .factory('AngularJsonAPIModelForm', AngularJsonAPIModelFormWrapper); 6 | 7 | function AngularJsonAPIModelFormWrapper( 8 | AngularJsonAPIModelValidationError, 9 | AngularJsonAPIModelLinkerService, 10 | validateJS, 11 | $q 12 | ) { 13 | 14 | AngularJsonAPIModelForm.prototype.save = save; 15 | AngularJsonAPIModelForm.prototype.reset = reset; 16 | AngularJsonAPIModelForm.prototype.validate = validate; 17 | 18 | AngularJsonAPIModelForm.prototype.link = link; 19 | AngularJsonAPIModelForm.prototype.unlink = unlink; 20 | 21 | AngularJsonAPIModelForm.prototype.toJson = toJson; 22 | 23 | return { 24 | create: AngularJsonAPIModelFormFactory 25 | }; 26 | 27 | function AngularJsonAPIModelFormFactory(parent) { 28 | return new AngularJsonAPIModelForm(parent); 29 | } 30 | 31 | function AngularJsonAPIModelForm(parent) { 32 | var _this = this; 33 | 34 | _this.data = { 35 | id: parent.data.id, 36 | type: parent.data.type, 37 | attributes: {}, 38 | relationships: {} 39 | }; 40 | 41 | _this.relationships = {}; 42 | _this.parent = parent; 43 | _this.schema = parent.schema; 44 | _this.reset(); 45 | } 46 | 47 | /** 48 | * Encodes object into json 49 | * @return {json} Json object 50 | */ 51 | function toJson() { 52 | var _this = this; 53 | var data = angular.copy(_this.data); 54 | var relationships = {}; 55 | 56 | angular.forEach(data.relationships, function(value, key) { 57 | if (value.data !== undefined) { 58 | relationships[key] = value; 59 | } 60 | }); 61 | 62 | data.relationships = relationships; 63 | 64 | return { 65 | data: data 66 | }; 67 | } 68 | 69 | /** 70 | * Saves form, shortcut to parent.save() 71 | * @return {promise} Promise associated with synchronization 72 | */ 73 | function save() { 74 | var _this = this; 75 | 76 | return _this.parent.save(); 77 | } 78 | 79 | /** 80 | * Resets form to state of a parent 81 | * @return {undefined} 82 | */ 83 | function reset(auto) { 84 | var _this = this; 85 | 86 | angular.forEach(_this.schema.relationships, function(data, key) { 87 | _this.data.relationships[key] = angular.copy(_this.parent.data.relationships[key]) || {}; 88 | if (angular.isArray(_this.relationships[key])) { 89 | _this.relationships[key] = _this.parent.relationships[key].slice(); 90 | } else { 91 | _this.relationships[key] = _this.parent.relationships[key]; 92 | } 93 | }); 94 | 95 | if (auto === true && _this.parent.synchronized === true) { 96 | return; 97 | } 98 | 99 | angular.forEach(_this.schema.attributes, function(validator, key) { 100 | _this.data.attributes[key] = angular.copy(_this.parent.data.attributes[key]); 101 | }); 102 | 103 | _this.parent.errors.validation.clear(); 104 | } 105 | 106 | /** 107 | * Validates form 108 | * @return {promise} Promise rejected to errors object indexed by keys. If the 109 | * key param i stated it only validates an attribute, else all attributes. 110 | */ 111 | function validate(key) { 112 | var _this = this; 113 | var attributesWrapper; 114 | var constraintsWrapper; 115 | var deferred = $q.defer(); 116 | 117 | if (key === undefined) { 118 | attributesWrapper = _this.data.attributes; 119 | constraintsWrapper = _this.schema.attributes; 120 | } else { 121 | attributesWrapper = {}; 122 | constraintsWrapper = {}; 123 | 124 | attributesWrapper[key] = _this.data.attributes[key]; 125 | constraintsWrapper[key] = _this.schema.attributes[key]; 126 | } 127 | 128 | validateJS.async( 129 | attributesWrapper, 130 | constraintsWrapper 131 | ).then(resolve, reject); 132 | 133 | function resolve() { 134 | if (key === undefined) { 135 | _this.parent.errors.validation.clear(); 136 | } else { 137 | _this.parent.errors.validation.clear(key); 138 | } 139 | 140 | deferred.resolve(); 141 | } 142 | 143 | function reject(errorsMap) { 144 | _this.parent.error = true; 145 | if (key === undefined) { 146 | _this.parent.errors.validation.clear(); 147 | } else { 148 | _this.parent.errors.validation.clear(key); 149 | } 150 | 151 | angular.forEach(errorsMap, function(errors, attribute) { 152 | angular.forEach(errors, function(error) { 153 | _this.parent.errors.validation.add(attribute, AngularJsonAPIModelValidationError.create(error, attribute)); 154 | }); 155 | }); 156 | 157 | deferred.reject(_this.parent.errors.validation); 158 | } 159 | 160 | return deferred.promise; 161 | } 162 | 163 | /** 164 | * Adds link to a form without synchronization 165 | * @param {string} key Relationship name 166 | * @param {AngularJsonAPIModel} target Object to be linked 167 | * @return {Boolean} Status 168 | */ 169 | function link(key, target, oneWay) { 170 | var _this = this; 171 | oneWay = oneWay === undefined ? false : true; 172 | 173 | return $q.resolve(AngularJsonAPIModelLinkerService.link(_this.parent, key, target, oneWay, true)); 174 | } 175 | 176 | /** 177 | * Removes link from form without synchronization 178 | * @param {[type]} key Relationship name 179 | * @param {AngularJsonAPIModel} target Object to be linked 180 | * @return {Boolean} Status 181 | */ 182 | function unlink(key, target, oneWay) { 183 | var _this = this; 184 | oneWay = oneWay === undefined ? false : true; 185 | 186 | return $q.resolve(AngularJsonAPIModelLinkerService.unlink(_this.parent, key, target, oneWay, true)); 187 | } 188 | } 189 | })(); 190 | -------------------------------------------------------------------------------- /src/model/model-form/model-form.spec.js: -------------------------------------------------------------------------------- 1 | /*jshint expr: true*/ 2 | (function() { 3 | 'use strict'; 4 | 5 | describe('AngularJsonAPIModelForm', function() { 6 | 7 | beforeEach(module('angular-jsonapi')); 8 | 9 | it('returns valid model', inject(function(AngularJsonAPIModelForm) { 10 | expect(AngularJsonAPIModelForm).toBeDefined(); 11 | })); 12 | }); 13 | })(); 14 | -------------------------------------------------------------------------------- /src/model/model-linker/model-linker.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angular-jsonapi') 5 | .service('AngularJsonAPIModelLinkerService', AngularJsonAPIModelLinkerService); 6 | 7 | function AngularJsonAPIModelLinkerService($log) { 8 | var _this = this; 9 | 10 | _this.toLinkData = toLinkData; 11 | 12 | _this.link = link; 13 | _this.unlink = unlink; 14 | 15 | return this; 16 | 17 | /** 18 | * Extracts data needed for relationship linking from object 19 | * @param {AngularJsonAPIModel} object Object 20 | * @return {json} Link data 21 | */ 22 | function toLinkData(object) { 23 | if (object === null) { 24 | return null; 25 | } 26 | 27 | return {type: object.data.type, id: object.data.id}; 28 | } 29 | 30 | /** 31 | * Add target to object relationships and data.relationships 32 | * @param {AngularJsonAPIModel} object Object to be modified 33 | * @param {string} key Relationship name 34 | * @param {AngularJsonAPIModel} target Object to be linked 35 | * @param {AngularJsonAPISchema} schema Relationship schema 36 | */ 37 | function link(object, key, target, oneWay, form) { 38 | var schema; 39 | form = form === undefined ? false : form; 40 | 41 | if (object === undefined) { 42 | $log.error('Can\'t add link to non existing object', object, key, target); 43 | $log.error('Object:', object.data.type, object); 44 | $log.error('Target:', target.data.type, target); 45 | $log.error('Key:', key); 46 | return []; 47 | } 48 | 49 | schema = object.schema.relationships[key]; 50 | 51 | if (target === undefined) { 52 | $log.error('Can\'t link non existing object', object, key, target, schema); 53 | $log.error('Object:', object.data.type, object); 54 | $log.error('Target:', target.data.type, target); 55 | $log.error('Key:', key); 56 | $log.error('Schema:', schema); 57 | return []; 58 | } 59 | 60 | if (schema === undefined) { 61 | $log.error('Can\'t add link not present in schema:', object, key, target, schema); 62 | $log.error('Object:', object.data.type, object); 63 | $log.error('Target:', target.data.type, target); 64 | $log.error('Key:', key); 65 | $log.error('Schema:', schema); 66 | return []; 67 | } 68 | 69 | if (target !== null && schema.polymorphic === false && schema.model !== target.data.type) { 70 | $log.error('This relation is not polymorphic, expected: ' + schema.model + ' instead of ' + target.data.type); 71 | $log.error('Object:', object.data.type, object); 72 | $log.error('Target:', target.data.type, target); 73 | $log.error('Key:', key); 74 | $log.error('Schema:', schema); 75 | return []; 76 | } 77 | 78 | if (schema.type === 'hasMany') { 79 | if (oneWay === true) { 80 | __addHasMany(object, key, target, form); 81 | return []; 82 | } else { 83 | return __processAddHasMany(object, key, target, form); 84 | } 85 | } else if (schema.type === 'hasOne') { 86 | if (oneWay === true) { 87 | __addHasOne(object, key, target, form); 88 | return []; 89 | } else { 90 | return __processAddHasOne(object, key, target, form); 91 | } 92 | } 93 | } 94 | 95 | /** 96 | * Remove target from object relationships and data.relationships 97 | * @param {AngularJsonAPIModel} object Object to be modified 98 | * @param {string} key Relationship name 99 | * @param {AngularJsonAPIModel} target Object to be unlinked 100 | * @param {AngularJsonAPISchema} schema Relationship schema 101 | */ 102 | function unlink(object, key, target, oneWay, form) { 103 | var schema; 104 | form = form === undefined ? false : form; 105 | 106 | if (object === undefined) { 107 | $log.error('Can\'t remove link from non existing object', object, key, target); 108 | $log.error('Object:', object.data.type, object); 109 | $log.error('Target:', target.data.type, target); 110 | $log.error('Key:', key); 111 | return []; 112 | } 113 | 114 | schema = object.schema.relationships[key]; 115 | 116 | if (schema === undefined) { 117 | $log.error('Can\'t remove link not present in schema:', object, key, target, schema); 118 | $log.error('Object:', object.data.type, object); 119 | $log.error('Target:', target.data.type, target); 120 | $log.error('Key:', key); 121 | $log.error('Schema:', schema); 122 | return []; 123 | } 124 | 125 | if (oneWay === true) { 126 | __removeHasMany(object, key, target, form); 127 | return []; 128 | } else { 129 | return __processRemove(object, key, target, form); 130 | } 131 | } 132 | 133 | ///////////// 134 | // Private // 135 | ///////////// 136 | 137 | function __processAddHasMany(object, key, target, form) { 138 | var reflectionKey = object.schema.relationships[key].reflection; 139 | var reflectionSchema; 140 | 141 | if (reflectionKey === false) { 142 | __addHasMany(object, key, target, form); 143 | return []; 144 | } 145 | 146 | reflectionSchema = target.schema.relationships[reflectionKey]; 147 | 148 | if (reflectionSchema === undefined) { 149 | $log.error('Cannot find reflection of', key, 'relationship for', object.data.type, 'in', target.data.type); 150 | $log.error('For one side relationships set schema.reflection to false'); 151 | return []; 152 | } else if (reflectionSchema.type === 'hasOne') { 153 | return __swapResults( 154 | __wrapResults(object, key, target), 155 | __wrapResults(target, reflectionKey, object), 156 | __processAddHasOne(target, reflectionKey, object, form) 157 | ); 158 | } else if (reflectionSchema.type === 'hasMany') { 159 | __addHasMany(object, key, target, form); 160 | __addHasMany(target, reflectionKey, object, form); 161 | return [__wrapResults(target, reflectionKey, object)]; 162 | } 163 | } 164 | 165 | function __processAddHasOne(object, key, target, form) { 166 | var reflectionKey = object.schema.relationships[key].reflection; 167 | var oldReflection = object.relationships[key]; 168 | var reflectionSchema; 169 | var oldReflectionSchema; 170 | var result = []; 171 | 172 | __addHasOne(object, key, target, form); 173 | 174 | if (reflectionKey === false) { 175 | return []; 176 | } 177 | 178 | if (oldReflection !== undefined && oldReflection !== null) { 179 | oldReflectionSchema = oldReflection.schema.relationships[reflectionKey]; 180 | 181 | if (oldReflectionSchema !== undefined) { 182 | if (oldReflectionSchema.type === 'hasOne') { 183 | __removeHasOne(oldReflection, reflectionKey, object, form); 184 | } else if (oldReflectionSchema.type === 'hasMany') { 185 | __removeHasMany(oldReflection, reflectionKey, object, form); 186 | } 187 | 188 | result.push(__wrapResults(oldReflection, reflectionKey, object)); 189 | } else { 190 | $log.error('Cannot find reflection of', key, 'relationship for', object.data.type, 'in', target.data.type); 191 | $log.error('For one side relationships set schema.reflection to false'); 192 | } 193 | } 194 | 195 | if (target !== undefined && target !== null && reflectionKey !== false) { 196 | reflectionSchema = target.schema.relationships[reflectionKey]; 197 | if (reflectionSchema !== undefined) { 198 | if (reflectionSchema.type === 'hasOne') { 199 | __addHasOne(target, reflectionKey, object, form); 200 | } else if (reflectionSchema.type === 'hasMany') { 201 | __addHasMany(target, reflectionKey, object, form); 202 | } 203 | 204 | result.push(__wrapResults(target, reflectionKey, object)); 205 | } else { 206 | $log.error('Cannot find reflection of', key, 'relationship for', object.data.type, 'in', target.data.type); 207 | $log.error('For one side relationships set schema.reflection to false'); 208 | } 209 | } 210 | 211 | return result; 212 | } 213 | 214 | function __processRemove(object, key, target, form) { 215 | var schema = object.schema.relationships[key]; 216 | var reflectionKey = schema.reflection; 217 | var reflectionSchema; 218 | 219 | if (schema.type === 'hasMany') { 220 | __removeHasMany(object, key, target, form); 221 | } else if (schema.type === 'hasOne') { 222 | __removeHasOne(object, key, target, form); 223 | } 224 | 225 | if (reflectionKey === false) { 226 | return []; 227 | } 228 | 229 | reflectionSchema = target.schema.relationships[reflectionKey]; 230 | 231 | if (reflectionSchema !== undefined) { 232 | if (reflectionSchema.type === 'hasOne') { 233 | __removeHasOne(target, reflectionKey, object, form); 234 | } else if (reflectionSchema.type === 'hasMany') { 235 | __removeHasMany(target, reflectionKey, object, form); 236 | } 237 | } else { 238 | $log.error('Cannot find reflection of', key, 'relationship for', object.data.type, 'in', target.data.type); 239 | $log.error('For one side relationships set schema.reflection to false'); 240 | return []; 241 | } 242 | 243 | return [__wrapResults(target, reflectionKey, object)]; 244 | } 245 | 246 | function __addHasOne(object, key, target, form) { 247 | $log.debug('addHasOne', object, key, target); 248 | 249 | if (form === true) { 250 | object = object.form; 251 | } 252 | 253 | object.relationships[key] = target; 254 | object.data.relationships[key].data = toLinkData(target); 255 | 256 | if (form === false) { 257 | object.reset(true); 258 | } 259 | 260 | return true; 261 | } 262 | 263 | function __addHasMany(object, key, target, form) { 264 | $log.debug('addHasMany', object, key, target); 265 | 266 | var linkData = toLinkData(target); 267 | if (form === true) { 268 | object = object.form; 269 | } 270 | 271 | if (angular.isArray(object.relationships[key]) && object.relationships[key].indexOf(target) > -1) { 272 | return false; 273 | } 274 | 275 | object.relationships[key] = object.relationships[key] || []; 276 | object.data.relationships[key].data = object.data.relationships[key].data || []; 277 | 278 | object.relationships[key].push(target); 279 | object.data.relationships[key].data.push(linkData); 280 | 281 | if (form === false) { 282 | object.reset(true); 283 | } 284 | 285 | return true; 286 | } 287 | 288 | function __removeHasOne(object, key, target, form) { 289 | $log.debug('removeHasOne', object, key, target); 290 | 291 | if (form === true) { 292 | object = object.form; 293 | } 294 | 295 | if (target !== undefined && object.relationships[key] !== target) { 296 | return false; 297 | } 298 | 299 | object.relationships[key] = null; 300 | object.data.relationships[key].data = undefined; 301 | 302 | if (form === false) { 303 | object.reset(true); 304 | } 305 | 306 | return true; 307 | } 308 | 309 | function __removeHasMany(object, key, target, form) { 310 | $log.debug('removeHasMany', object, key, target); 311 | 312 | if (form === true) { 313 | object = object.form; 314 | } 315 | 316 | if (object.relationships[key] === undefined) { 317 | return; 318 | } 319 | 320 | if (target === undefined) { 321 | object.relationships[key] = []; 322 | object.data.relationships[key].data = []; 323 | if (form === false) { 324 | object.reset(true); 325 | } 326 | 327 | return true; 328 | } 329 | 330 | var index = object.relationships[key].indexOf(target); 331 | 332 | if (index === -1) { 333 | return false; 334 | } 335 | 336 | object.relationships[key].splice(index, 1); 337 | object.data.relationships[key].data.splice(index, 1); 338 | 339 | if (form === false) { 340 | object.reset(true); 341 | } 342 | 343 | return true; 344 | } 345 | 346 | function __wrapResults(object, key, target) { 347 | return { 348 | object: object, 349 | key: key, 350 | target: target 351 | }; 352 | } 353 | 354 | function __swapResults(value, newValue, array) { 355 | var index = -1; 356 | angular.forEach(array, function(item, i) { 357 | if (item.object === value.object && item.key === value.key && item.target === value.target) { 358 | index = i; 359 | } 360 | }); 361 | 362 | if (index > -1) { 363 | array[index] = newValue; 364 | } else { 365 | array.push(newValue); 366 | } 367 | 368 | return array; 369 | } 370 | } 371 | })(); 372 | -------------------------------------------------------------------------------- /src/model/model-linker/model-linker.spec.js: -------------------------------------------------------------------------------- 1 | /*jshint expr: true*/ 2 | (function() { 3 | 'use strict'; 4 | 5 | describe('AngularJsonAPIModelLinkerService', function() { 6 | 7 | beforeEach(module('angular-jsonapi')); 8 | 9 | it('returns valid model', inject(function(AngularJsonAPIModelLinkerService) { 10 | expect(AngularJsonAPIModelLinkerService).toBeDefined(); 11 | })); 12 | }); 13 | })(); 14 | -------------------------------------------------------------------------------- /src/resource/resource-cache/resource-cache.factory.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angular-jsonapi') 5 | .factory('AngularJsonAPIResourceCache', AngularJsonAPIResourceCacheWrapper); 6 | 7 | function AngularJsonAPIResourceCacheWrapper( 8 | uuid4, 9 | $log 10 | ) { 11 | 12 | AngularJsonAPIResourceCache.prototype.get = get; 13 | AngularJsonAPIResourceCache.prototype.index = index; 14 | AngularJsonAPIResourceCache.prototype.setIndexIds = setIndexIds; 15 | AngularJsonAPIResourceCache.prototype.addOrUpdate = addOrUpdate; 16 | 17 | AngularJsonAPIResourceCache.prototype.fromJson = fromJson; 18 | AngularJsonAPIResourceCache.prototype.toJson = toJson; 19 | AngularJsonAPIResourceCache.prototype.clear = clear; 20 | 21 | AngularJsonAPIResourceCache.prototype.remove = remove; 22 | AngularJsonAPIResourceCache.prototype.revertRemove = revertRemove; 23 | AngularJsonAPIResourceCache.prototype.clearRemoved = clearRemoved; 24 | 25 | return { 26 | create: AngularJsonAPIResourceCacheFactory 27 | }; 28 | 29 | function AngularJsonAPIResourceCacheFactory(resource) { 30 | return new AngularJsonAPIResourceCache(resource); 31 | } 32 | 33 | /** 34 | * Constructor 35 | */ 36 | function AngularJsonAPIResourceCache(resource) { 37 | var _this = this; 38 | 39 | _this.resource = resource; 40 | _this.data = {}; 41 | _this.removed = {}; 42 | _this.size = 0; 43 | 44 | _this.indexIds = undefined; 45 | } 46 | 47 | /** 48 | * Add new model or update existing with data 49 | * @param {object} validatedData Data that are used to update or create an object, has to be valid 50 | * @return {AngularJsonAPIModel} Created model 51 | */ 52 | function addOrUpdate(validatedData, config, updatedAt) { 53 | var _this = this; 54 | var id = validatedData.id; 55 | 56 | if (id === undefined) { 57 | $log.error('Can\'t add data without id!', validatedData); 58 | return; 59 | } 60 | 61 | if (_this.data[id] === undefined) { 62 | _this.data[id] = _this.resource.modelFactory(validatedData, config, updatedAt); 63 | _this.size += 1; 64 | } else { 65 | _this.data[id].update(validatedData, !config.new, config.initialization); 66 | } 67 | 68 | return _this.data[id]; 69 | } 70 | 71 | 72 | /** 73 | * Recreate object structure from json data 74 | * @param {json} json Json data 75 | * @return {undefined} 76 | */ 77 | function fromJson(json) { 78 | var _this = this; 79 | var collection = angular.fromJson(json); 80 | 81 | var config = { 82 | new: false, 83 | synchronized: false, 84 | stable: false, 85 | pristine: false, 86 | initialization: true 87 | }; 88 | 89 | if (angular.isObject(collection) && collection.data !== undefined) { 90 | _this.updatedAt = collection.updatedAt; 91 | _this.indexIds = collection.indexIds; 92 | 93 | angular.forEach(collection.data, function(objectData) { 94 | var data = objectData.data; 95 | _this.addOrUpdate(data, config, objectData.updatedAt); 96 | }); 97 | } 98 | } 99 | 100 | /** 101 | * Encodes memory into json format 102 | * @return {json} Json encoded memory 103 | */ 104 | function toJson() { 105 | var _this = this; 106 | var json = { 107 | data: [], 108 | updatedAt: _this.updatedAt, 109 | indexIds: _this.indexIds 110 | }; 111 | 112 | angular.forEach(_this.data, function(object) { 113 | if (object.hasErrors() === false) { 114 | json.data.push(object.toJson()); 115 | } 116 | }); 117 | 118 | return angular.toJson(json); 119 | } 120 | 121 | /** 122 | * Clear memory 123 | * @return {undefined} 124 | */ 125 | function clear() { 126 | var _this = this; 127 | 128 | _this.indexIds = undefined; 129 | _this.data = {}; 130 | _this.removed = {}; 131 | } 132 | 133 | /** 134 | * Low level get used internally, does not run any synchronization 135 | * @param {uuid} id 136 | * @return {AngularJsonAPIModel} Model associated with id 137 | */ 138 | function get(id) { 139 | var _this = this; 140 | 141 | var data = { 142 | id: id, 143 | type: _this.resource.schema.type 144 | }; 145 | 146 | var config = { 147 | new: false, 148 | synchronized: false, 149 | stable: false, 150 | pristine: true 151 | }; 152 | 153 | if (_this.data[id] === undefined) { 154 | _this.data[id] = _this.resource.modelFactory(data, config); 155 | } 156 | 157 | return _this.data[id]; 158 | } 159 | 160 | /** 161 | * Low level get used internally, does not run any synchronization, used for index requests 162 | * @param {objec} params 163 | * @return {AngularJsonAPIModel} Model associated with id 164 | */ 165 | function index(params) { 166 | var _this = this; 167 | params = params || {}; 168 | 169 | if (_this.indexIds === undefined) { 170 | return _this.indexIds; 171 | } 172 | 173 | return _this.indexIds.map(_this.get.bind(_this)).filter(filter); 174 | 175 | function filter(argument) { 176 | var filterParams = params.filter; 177 | var valid = true; 178 | 179 | angular.forEach(filterParams, function(constraint) { 180 | valid = valid && argument.data.attributes[constraint.key] === constraint.value; 181 | }); 182 | 183 | return valid; 184 | } 185 | } 186 | 187 | /** 188 | * Cache ids of objects returned by index request 189 | * @param {ids array or AngularJsonAPIModel array} array Objects or ids to be cached 190 | */ 191 | function setIndexIds(array) { 192 | var _this = this; 193 | 194 | _this.indexIds = []; 195 | 196 | angular.forEach(array, function(element) { 197 | if (angular.isString(element) && _this.resource.schema.id.validate(element)) { 198 | _this.indexIds.push(element); 199 | } else if (angular.isObject(element) && _this.resource.schema.id.validate(element.data.id)) { 200 | _this.indexIds.push(element.data.id); 201 | } 202 | }); 203 | } 204 | 205 | /** 206 | * Remove object with given id from cache 207 | * @param {uuid} id 208 | * @return {AngularJsonAPIModel / undefined} Removed object, undefined if 209 | * object does not exist 210 | */ 211 | function remove(id) { 212 | var _this = this; 213 | 214 | if (_this.data[id] !== undefined) { 215 | _this.removed[id] = _this.data[id]; 216 | delete _this.data[id]; 217 | _this.size -= 1; 218 | } 219 | 220 | return _this.removed[id]; 221 | } 222 | 223 | /** 224 | * Revert removal of an object with given id from cache 225 | * @param {uuid} id 226 | * @return {AngularJsonAPIModel / undefined} Removed object, undefined if 227 | * object does not exist 228 | */ 229 | function revertRemove(id) { 230 | var _this = this; 231 | 232 | if (_this.removed[id] !== undefined) { 233 | _this.data[id] = _this.removed[id]; 234 | delete _this.removed[id]; 235 | _this.size += 1; 236 | } 237 | 238 | return _this.data[id]; 239 | } 240 | 241 | /** 242 | * Clear removed object from memory 243 | * @param {uuid} id 244 | * @return {undefined} 245 | */ 246 | function clearRemoved(id) { 247 | var _this = this; 248 | 249 | delete _this.removed[id]; 250 | } 251 | } 252 | })(); 253 | -------------------------------------------------------------------------------- /src/resource/resource-cache/resource-cache.factory.spec.js: -------------------------------------------------------------------------------- 1 | /*jshint expr: true*/ 2 | (function() { 3 | 'use strict'; 4 | 5 | describe('AngularJsonAPIResourceCache', function() { 6 | 7 | beforeEach(module('angular-jsonapi')); 8 | 9 | it('returns valid model', inject(function(AngularJsonAPIResourceCache) { 10 | expect(AngularJsonAPIResourceCache).toBeDefined(); 11 | })); 12 | 13 | describe('#addOrUpdate', function() { 14 | 15 | }); 16 | 17 | describe('#fromJson', function() { 18 | 19 | }); 20 | 21 | describe('#toJson', function() { 22 | 23 | }); 24 | 25 | describe('#clear', function() { 26 | 27 | }); 28 | 29 | describe('#get', function() { 30 | 31 | }); 32 | 33 | describe('#remove', function() { 34 | 35 | }); 36 | 37 | describe('#revertRemove', function() { 38 | 39 | }); 40 | 41 | describe('#clearRemoved', function() { 42 | 43 | }); 44 | 45 | }); 46 | })(); 47 | -------------------------------------------------------------------------------- /src/resource/resource.factory.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angular-jsonapi') 5 | .factory('AngularJsonAPIResource', AngularJsonAPIResourceWrapper); 6 | 7 | function AngularJsonAPIResourceWrapper( 8 | AngularJsonAPIModel, 9 | AngularJsonAPISchema, 10 | AngularJsonAPIResourceCache, 11 | AngularJsonAPICollection, 12 | $rootScope, 13 | $log, 14 | $q 15 | ) { 16 | AngularJsonAPIResource.prototype.get = get; 17 | AngularJsonAPIResource.prototype.all = all; 18 | AngularJsonAPIResource.prototype.remove = remove; 19 | AngularJsonAPIResource.prototype.initialize = initialize; 20 | 21 | AngularJsonAPIResource.prototype.clearCache = clearCache; 22 | 23 | return { 24 | create: AngularJsonAPIResourceFactory 25 | }; 26 | 27 | function AngularJsonAPIResourceFactory(schema, synchronizer) { 28 | return new AngularJsonAPIResource(schema, synchronizer); 29 | } 30 | 31 | /** 32 | * AngularJsonAPIResource constructor 33 | * @param {json} schema Schema object 34 | * @param {AngularJsonAPISynchronizer} synchronizer Synchronizer for the resource 35 | */ 36 | function AngularJsonAPIResource(schema, synchronizer) { 37 | var _this = this; 38 | var config = { 39 | action: 'init' 40 | }; 41 | 42 | _this.schema = AngularJsonAPISchema.create(schema); 43 | _this.cache = AngularJsonAPIResourceCache.create(_this); 44 | 45 | _this.synchronizer = synchronizer; 46 | _this.synchronizer.resource = _this; 47 | 48 | _this.modelFactory = AngularJsonAPIModel.modelFactory( 49 | _this.schema, 50 | _this 51 | ); 52 | 53 | _this.initialized = false; 54 | _this.type = _this.schema.type; 55 | 56 | synchronizer.resource = _this; 57 | 58 | _this.synchronizer.synchronize(config).then(resolve, reject, notify); 59 | 60 | function resolve(response) { 61 | $rootScope.$emit('angularJsonAPI:' + _this.type + ':resource:init', 'resolved', response); 62 | _this.cache.fromJson(response.data); 63 | _this.initialized = true; 64 | 65 | response.finish(); 66 | } 67 | 68 | function reject(response) { 69 | $rootScope.$emit('angularJsonAPI:' + _this.type + ':resource:init', 'rejected', response); 70 | response.finish(); 71 | _this.initialized = true; 72 | } 73 | 74 | function notify(response) { 75 | $rootScope.$emit('angularJsonAPI:' + _this.type + ':resource:init', 'notify', response); 76 | } 77 | } 78 | 79 | /** 80 | * Get request 81 | * @param {uuid} id 82 | * @return {AngularJsonAPIModel} Model associated with id, synchronized 83 | */ 84 | function get(id, params) { 85 | var _this = this; 86 | 87 | if (!_this.schema.id.validate(id)) { 88 | return $q.reject({errors: [{status: 0, statusText: 'Invalid id'}]}); 89 | } 90 | 91 | var object = _this.cache.get(id); 92 | 93 | object.promise = object.refresh(params); 94 | 95 | return object; 96 | } 97 | 98 | /** 99 | * All request 100 | * @param {object} params Object associated with params (for filters/pagination etc.) 101 | * @return {AngularJsonAPICollection} Collection of AngularJsonAPIModel, synchronized 102 | */ 103 | function all(params) { 104 | var _this = this; 105 | params = angular.extend({}, _this.schema.params.all, params); 106 | 107 | var collection = AngularJsonAPICollection.create( 108 | _this, 109 | params 110 | ); 111 | 112 | collection.promise = collection.fetch(); 113 | 114 | return collection; 115 | } 116 | 117 | /** 118 | * Remove request 119 | * @param {uuid} id 120 | * @return {promise} Promise associated with the synchronization, in case of 121 | * fail object is reverted to previous state 122 | */ 123 | function remove(id) { 124 | var _this = this; 125 | var object = _this.cache.remove(id); 126 | 127 | return object.remove(); 128 | } 129 | 130 | /** 131 | * Initialize new AngularJsonAPIModel 132 | * @return {AngularJsonAPIModel} New model 133 | */ 134 | function initialize() { 135 | var _this = this; 136 | var relationships = {}; 137 | 138 | angular.forEach(_this.schema.relationships, function(relationshipSchema, relationshipName) { 139 | if (relationshipSchema.type === 'hasOne') { 140 | relationships[relationshipName] = { 141 | data: null 142 | }; 143 | } else if (relationshipSchema.type === 'hasMany') { 144 | relationships[relationshipName] = { 145 | data: [] 146 | }; 147 | } 148 | }); 149 | 150 | var data = { 151 | type: _this.type, 152 | id: _this.schema.id.generate(), 153 | attributes: {}, 154 | relationships: relationships 155 | }; 156 | 157 | var config = { 158 | new: true, 159 | synchronized: false, 160 | stable: false, 161 | pristine: false, 162 | initialization: false 163 | }; 164 | 165 | var object = _this.modelFactory(data, config); 166 | 167 | $rootScope.$emit('angularJsonAPI:' + _this.type + ':resource:initialize', 'resolved', object); 168 | 169 | return object; 170 | } 171 | 172 | /** 173 | * Clears localy saved data 174 | * @return {promise} Promise associated with the synchronization resolves to nothing 175 | */ 176 | function clearCache() { 177 | var _this = this; 178 | var deferred = $q.defer(); 179 | var config = { 180 | action: 'clearCache' 181 | }; 182 | 183 | _this.cache.clear(); 184 | 185 | _this.synchronizer.synchronize(config).then(resolve, reject, notify); 186 | 187 | return deferred; 188 | 189 | function resolve(response) { 190 | $rootScope.$emit('angularJsonAPI:' + _this.type + ':resource:clearCache', 'resolved', response); 191 | response.finish(); 192 | 193 | deferred.resolve(response); 194 | } 195 | 196 | function reject(response) { 197 | $rootScope.$emit('angularJsonAPI:' + _this.type + ':resource:clearCache', 'resolved', response); 198 | response.finish(); 199 | 200 | deferred.reject(response); 201 | } 202 | 203 | function notify(response) { 204 | $rootScope.$emit('angularJsonAPI:' + _this.type + ':resource:clearCache', 'notify', response); 205 | 206 | deferred.notify(response); 207 | } 208 | } 209 | } 210 | })(); 211 | -------------------------------------------------------------------------------- /src/resource/resource.factory.spec.js: -------------------------------------------------------------------------------- 1 | /*jshint expr: true*/ 2 | (function() { 3 | 'use strict'; 4 | 5 | describe('AngularJsonAPIResource', function() { 6 | 7 | beforeEach(module('angular-jsonapi')); 8 | 9 | it('returns valid model', inject(function(AngularJsonAPIResource) { 10 | expect(AngularJsonAPIResource).toBeDefined(); 11 | })); 12 | }); 13 | })(); 14 | -------------------------------------------------------------------------------- /src/schema/schema-link.factory.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angular-jsonapi') 5 | .factory('AngularJsonAPISchemaLink', AngularJsonAPILinkSchrapperLink); 6 | 7 | function AngularJsonAPILinkSchrapperLink($log, pluralize) { 8 | 9 | return { 10 | create: AngularJsonAPISchemaLinkFactory 11 | }; 12 | 13 | function AngularJsonAPISchemaLinkFactory(linkSchema, linkName, type) { 14 | return new AngularJsonAPISchemaLink(linkSchema, linkName, type); 15 | } 16 | 17 | function AngularJsonAPISchemaLink(linkSchema, linkName, type) { 18 | var _this = this; 19 | 20 | if (angular.isString(linkSchema)) { 21 | _this.model = pluralize.plural(linkName); 22 | _this.type = linkSchema; 23 | _this.polymorphic = false; 24 | _this.reflection = type; 25 | } else { 26 | if (linkSchema.type === undefined) { 27 | $log.error('Schema of link without a type: ', linkSchema, linkName); 28 | } 29 | 30 | if (linkSchema.type !== 'hasMany' && linkSchema.type !== 'hasOne') { 31 | $log.error('Schema of link with wrong type: ', linkSchema.type, 'available: hasOne, hasMany'); 32 | } 33 | 34 | _this.model = linkSchema.model || pluralize.plural(linkName); 35 | _this.type = linkSchema.type; 36 | _this.polymorphic = linkSchema.polymorphic || false; 37 | 38 | if (linkSchema.reflection === undefined) { 39 | _this.reflection = _this.type === 'hasMany' ? pluralize.singular(type) : type; 40 | } else { 41 | _this.reflection = linkSchema.reflection; 42 | } 43 | 44 | _this.included = linkSchema.included || false; 45 | } 46 | } 47 | 48 | } 49 | })(); 50 | -------------------------------------------------------------------------------- /src/schema/schema-link.factory.spec.js: -------------------------------------------------------------------------------- 1 | /*jshint expr: true*/ 2 | (function() { 3 | 'use strict'; 4 | 5 | describe('AngularJsonAPISchemaLink', function() { 6 | 7 | beforeEach(module('angular-jsonapi')); 8 | 9 | it('returns valid model', inject(function(AngularJsonAPISchemaLink) { 10 | expect(AngularJsonAPISchemaLink).toBeDefined(); 11 | })); 12 | }); 13 | })(); 14 | -------------------------------------------------------------------------------- /src/schema/schema.factory.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angular-jsonapi') 5 | .factory('AngularJsonAPISchema', AngularJsonAPISchemaWrapper); 6 | 7 | function AngularJsonAPISchemaWrapper( 8 | $log, 9 | pluralize, 10 | uuid4, 11 | AngularJsonAPISchemaLink 12 | ) { 13 | 14 | return { 15 | create: AngularJsonAPISchemaFactory 16 | }; 17 | 18 | function AngularJsonAPISchemaFactory(schema) { 19 | return new AngularJsonAPISchema(schema); 20 | } 21 | 22 | function AngularJsonAPISchema(schema) { 23 | var _this = this; 24 | var include = schema.include || {}; 25 | schema.include = include; 26 | include.get = schema.include.get || []; 27 | include.all = schema.include.all || []; 28 | 29 | _this.params = { 30 | get: {}, 31 | all: {} 32 | }; 33 | 34 | if (schema.id === 'uuid4') { 35 | schema.id = uuid4; 36 | } else if (schema.id === 'int') { 37 | schema.id = { 38 | generate: angular.noop, 39 | validate: angular.isNumber 40 | }; 41 | } else if (angular.isObject(schema.id)) { 42 | if (!angular.isFunction(schema.id.generate)) { 43 | schema.id.generate = angular.noop; 44 | } 45 | } else { 46 | schema.id = { 47 | generate: angular.noop, 48 | validate: angular.identity.bind(null, true) 49 | }; 50 | } 51 | 52 | angular.forEach(schema.relationships, function(linkSchema, linkName) { 53 | var linkSchemaObj = AngularJsonAPISchemaLink.create(linkSchema, linkName, schema.type); 54 | schema.relationships[linkName] = linkSchemaObj; 55 | if (linkSchemaObj.included === true) { 56 | include.get.push(linkName); 57 | if (linkSchemaObj.type === 'hasOne') { 58 | include.all.push(linkName); 59 | } 60 | } 61 | }); 62 | 63 | angular.extend(_this, schema); 64 | 65 | if (include.get.length > 0) { 66 | _this.params.get.include = include.get; 67 | } 68 | 69 | if (include.all.length > 0) { 70 | _this.params.all.include = include.all; 71 | } 72 | } 73 | 74 | } 75 | })(); 76 | -------------------------------------------------------------------------------- /src/schema/schema.factory.spec.js: -------------------------------------------------------------------------------- 1 | /*jshint expr: true*/ 2 | (function() { 3 | 'use strict'; 4 | 5 | describe('AngularJsonAPISchema', function() { 6 | 7 | beforeEach(module('angular-jsonapi')); 8 | 9 | it('returns valid model', inject(function(AngularJsonAPISchema) { 10 | expect(AngularJsonAPISchema).toBeDefined(); 11 | })); 12 | }); 13 | })(); 14 | -------------------------------------------------------------------------------- /src/sources/local/source-local.decorator.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angular-jsonapi-local') 5 | .config(provide); 6 | 7 | function provide($provide) { 8 | $provide.decorator('$jsonapi', decorator); 9 | } 10 | 11 | function decorator($delegate, AngularJsonAPISourceLocal) { 12 | var $jsonapi = $delegate; 13 | 14 | $jsonapi.sourceLocal = AngularJsonAPISourceLocal; 15 | 16 | return $jsonapi; 17 | } 18 | })(); 19 | -------------------------------------------------------------------------------- /src/sources/local/source-local.factory.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angular-jsonapi-local') 5 | .factory('AngularJsonAPISourceLocal', AngularJsonAPISourceLocalWrapper); 6 | 7 | function AngularJsonAPISourceLocalWrapper( 8 | AngularJsonAPISourcePrototype, 9 | $window, 10 | $q 11 | ) { 12 | var size = { 13 | max: 0, 14 | all: 0, 15 | limit: 5200000, 16 | list: {} 17 | }; 18 | 19 | AngularJsonAPISourceLocal.prototype = Object.create(AngularJsonAPISourcePrototype.prototype); 20 | AngularJsonAPISourceLocal.prototype.constructor = AngularJsonAPISourceLocal; 21 | 22 | return { 23 | create: AngularJsonAPISourceLocalFactory, 24 | size: size 25 | }; 26 | 27 | function AngularJsonAPISourceLocalFactory(name, prefix) { 28 | return new AngularJsonAPISourceLocal(name, prefix); 29 | } 30 | 31 | function AngularJsonAPISourceLocal(name, prefix) { 32 | var _this = this; 33 | 34 | prefix = prefix || 'AngularJsonAPI'; 35 | 36 | _this.__updateStorage = updateStorage; 37 | 38 | AngularJsonAPISourcePrototype.apply(_this, arguments); 39 | 40 | _this.synchronization('init', init); 41 | 42 | _this.begin('clearCache', clear); 43 | 44 | _this.finish('init', updateStorage); 45 | _this.finish('clearCache', updateStorage); 46 | _this.finish('remove', updateStorage); 47 | _this.finish('refresh', updateStorage); 48 | _this.finish('unlink', updateStorage); 49 | _this.finish('unlinkReflection', updateStorage); 50 | _this.finish('link', updateStorage); 51 | _this.finish('linkReflection', updateStorage); 52 | _this.finish('update', updateStorage); 53 | _this.finish('add', updateStorage); 54 | _this.finish('get', updateStorage); 55 | _this.finish('all', updateStorage); 56 | _this.finish('include', updateStorage); 57 | 58 | function init() { 59 | var type = _this.synchronizer.resource.schema.type; 60 | return $q.resolve($window.localStorage.getItem(prefix + '.' + type)); 61 | } 62 | 63 | function clear() { 64 | var type = _this.synchronizer.resource.schema.type; 65 | var key = prefix + '.' + type; 66 | 67 | size.all -= size.list[key]; 68 | delete size.list[key]; 69 | size.max = objectMaxKey(size.list); 70 | size.fraction = size.list[size.max] / size.limit * 100; 71 | 72 | $window.localStorage.removeItem(key); 73 | } 74 | 75 | function updateStorage() { 76 | var type = _this.synchronizer.resource.schema.type; 77 | var cache = _this.synchronizer.resource.cache; 78 | var json = cache.toJson(); 79 | var key = prefix + '.' + type; 80 | 81 | size.list[key] = size.list[key] === undefined ? 0 : size.list[key]; 82 | size.all += json.length - size.list[key]; 83 | size.list[key] = json.length; 84 | size.max = objectMaxKey(size.list); 85 | size.fraction = size.list[size.max] / size.limit * 100; 86 | 87 | $window.localStorage.setItem(key, json); 88 | } 89 | 90 | function objectMaxKey(object) { 91 | return Object.keys(object).reduce(function(m, k) { 92 | return object[k] > object[m] ? k : m; 93 | }, Object.keys(object)[0]); 94 | } 95 | } 96 | } 97 | })(); 98 | -------------------------------------------------------------------------------- /src/sources/local/source-local.module.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angular-jsonapi-local', ['angular-jsonapi']); 5 | 6 | })(); 7 | -------------------------------------------------------------------------------- /src/sources/parse/source-parse.decorator.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angular-jsonapi-parse') 5 | .config(provide); 6 | 7 | function provide($provide) { 8 | $provide.decorator('$jsonapi', decorator); 9 | } 10 | 11 | function decorator($delegate, AngularJsonAPISourceParse) { 12 | var $jsonapi = $delegate; 13 | 14 | $jsonapi.sourceLocal = AngularJsonAPISourceParse; 15 | 16 | return $jsonapi; 17 | } 18 | })(); 19 | -------------------------------------------------------------------------------- /src/sources/parse/source-parse.factory.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angular-jsonapi-parse') 5 | .factory('AngularJsonAPISourceParse', AngularJsonAPISourceParseWrapper); 6 | 7 | function AngularJsonAPISourceParseWrapper( 8 | AngularJsonAPIModelSourceError, 9 | AngularJsonAPISourcePrototype, 10 | AngularJsonAPIModelLinkerService, 11 | pluralize, 12 | Parse, 13 | $log, 14 | $q 15 | ) { 16 | 17 | AngularJsonAPISourceParse.prototype = Object.create(AngularJsonAPISourcePrototype.prototype); 18 | AngularJsonAPISourceParse.prototype.constructor = AngularJsonAPISourceParse; 19 | AngularJsonAPISourceParse.prototype.initialize = initialize; 20 | 21 | return { 22 | create: AngularJsonAPISourceParseFactory 23 | }; 24 | 25 | function AngularJsonAPISourceParseFactory(name, table) { 26 | return new AngularJsonAPISourceParse(name, table); 27 | } 28 | 29 | function AngularJsonAPISourceParse(name, table) { 30 | var _this = this; 31 | 32 | _this.ParseObject = Parse.Object.extend(table); 33 | _this.type = pluralize(table).charAt(0).toLowerCase() + pluralize(table).slice(1); 34 | 35 | AngularJsonAPISourcePrototype.apply(_this, arguments); 36 | 37 | _this.synchronization('remove', remove); 38 | _this.synchronization('update', update); 39 | _this.synchronization('add', update); 40 | _this.synchronization('all', all); 41 | _this.synchronization('get', get); 42 | _this.synchronization('refresh', get); 43 | 44 | function all(config) { 45 | var query = new Parse.Query(_this.ParseObject); 46 | 47 | if (config.params.limit !== undefined) { 48 | query.limit(config.params.limit); 49 | } 50 | 51 | angular.forEach(config.params.filter, function(filter) { 52 | query.equalTo(filter.key, filter.value); 53 | }); 54 | 55 | return query.find().then(resolveParse, rejectParse.bind(null, 'all')); 56 | } 57 | 58 | function get(config) { 59 | var query = new Parse.Query(_this.ParseObject); 60 | return query.get(config.object.data.id).then(resolveParse, rejectParse.bind(null, 'get')); 61 | } 62 | 63 | function remove(config) { 64 | var object = new _this.ParseObject(); 65 | object.set('id', config.object.data.id); 66 | return object.destroy().then(resolveParse, rejectParse.bind(null, 'remove')); 67 | } 68 | 69 | function update(config) { 70 | var object = toParseObject(config.object); 71 | return object.save(null).then(resolveParse, rejectParse.bind(null, 'update')); 72 | } 73 | 74 | function toParseObject(object) { 75 | var parseObject = new _this.ParseObject(); 76 | angular.forEach(object.form.data.attributes, function(attribute, key) { 77 | parseObject.set(key, attribute); 78 | }); 79 | 80 | angular.forEach(object.schema.relationships, function(relationship, key) { 81 | if (relationship.type === 'hasOne' 82 | && object.form.data.relationships[key].data !== null 83 | && object.form.data.relationships[key].data !== undefined 84 | ) { 85 | var table = pluralize(key, 1).charAt(0).toUpperCase() + pluralize(key, 1).slice(1); 86 | var parsePointer = new (Parse.Object.extend(table))(); 87 | parsePointer.set('id', object.form.data.relationships[key].data.id); 88 | parseObject.set(key, parsePointer); 89 | } 90 | }); 91 | 92 | return parseObject; 93 | } 94 | 95 | function fromParseObject(object) { 96 | var relationships = _this.synchronizer.resource.schema.relationships; 97 | object.type = _this.type; 98 | object.relationships = object.relationships || []; 99 | angular.forEach(relationships, function(relationship, key) { 100 | if (object.attributes[key] && relationship.type === 'hasOne') { 101 | object.relationships[key] = { 102 | data: { 103 | type: relationship.model, 104 | id: object.attributes[key].id 105 | } 106 | }; 107 | } 108 | }); 109 | 110 | return object; 111 | } 112 | 113 | function resolveParse(response) { 114 | if (angular.isArray(response)) { 115 | angular.forEach(response, function(elem, key) { 116 | response[key] = fromParseObject(elem); 117 | }); 118 | } else if (angular.isObject(response)) { 119 | response = fromParseObject(response); 120 | } 121 | 122 | return $q.resolve({ 123 | data: response 124 | }); 125 | } 126 | 127 | function rejectParse(action, response) { 128 | $log.error('Parse error for', action, response); 129 | return $q.reject(response); 130 | } 131 | } 132 | 133 | function initialize(appId, jsKey) { 134 | Parse.initialize(appId, jsKey); 135 | } 136 | } 137 | })(); 138 | -------------------------------------------------------------------------------- /src/sources/parse/source-parse.module.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | /* global Parse: false */ 5 | angular.module('angular-jsonapi-parse', ['angular-jsonapi']) 6 | .constant('Parse', Parse); 7 | })(); 8 | -------------------------------------------------------------------------------- /src/sources/rest/source-rest.decorator.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angular-jsonapi-rest') 5 | .config(provide); 6 | 7 | function provide($provide) { 8 | $provide.decorator('$jsonapi', decorator); 9 | } 10 | 11 | function decorator($delegate, AngularJsonAPISourceRest) { 12 | var $jsonapi = $delegate; 13 | 14 | $jsonapi.sourceRest = AngularJsonAPISourceRest; 15 | 16 | return $jsonapi; 17 | } 18 | })(); 19 | -------------------------------------------------------------------------------- /src/sources/rest/source-rest.factory.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angular-jsonapi-rest') 5 | .factory('AngularJsonAPISourceRest', AngularJsonAPISourceRestWrapper); 6 | 7 | function AngularJsonAPISourceRestWrapper( 8 | AngularJsonAPIModelSourceError, 9 | AngularJsonAPISourcePrototype, 10 | AngularJsonAPIModelLinkerService, 11 | toKebabCase, 12 | $q, 13 | $http 14 | ) { 15 | 16 | AngularJsonAPISourceRest.prototype = Object.create(AngularJsonAPISourcePrototype.prototype); 17 | AngularJsonAPISourceRest.prototype.constructor = AngularJsonAPISourceRest; 18 | 19 | return { 20 | create: AngularJsonAPISourceRestFactory, 21 | encodeParams: encodeParams, 22 | decodeParams: decodeParams 23 | }; 24 | 25 | function AngularJsonAPISourceRestFactory(name, url) { 26 | return new AngularJsonAPISourceRest(name, url); 27 | } 28 | 29 | function AngularJsonAPISourceRest(name, url) { 30 | var _this = this; 31 | var headers = { // jscs:disable disallowQuotedKeysInObjects 32 | 'Accept': 'application/vnd.api+json', 33 | 'Content-Type': 'application/vnd.api+json' 34 | }; // jscs:enable disallowQuotedKeysInObjects 35 | 36 | AngularJsonAPISourcePrototype.apply(_this, arguments); 37 | 38 | _this.synchronization('remove', remove); 39 | _this.synchronization('unlink', unlink); 40 | _this.synchronization('link', link); 41 | _this.synchronization('update', update); 42 | _this.synchronization('add', add); 43 | _this.synchronization('all', all); 44 | _this.synchronization('get', get); 45 | _this.synchronization('refresh', get); 46 | 47 | function all(config) { 48 | return $http({ 49 | method: 'GET', 50 | headers: headers, 51 | url: url, 52 | params: encodeParams(config.params) 53 | }).then(resolveHttp, rejectHttp.bind(null, 'all')); 54 | } 55 | 56 | function get(config) { 57 | return $http({ 58 | method: 'GET', 59 | headers: headers, 60 | url: url + '/' + config.object.data.id, 61 | params: encodeParams(config.params) 62 | }).then(resolveHttp, rejectHttp.bind(null, 'get')); 63 | } 64 | 65 | function remove(config) { 66 | return $http({ 67 | method: 'DELETE', 68 | headers: headers, 69 | url: url + '/' + config.object.data.id 70 | }).then(resolveHttp, rejectHttp.bind(null, 'remove')); 71 | } 72 | 73 | function unlink(config) { 74 | var deferred = $q.defer(); 75 | var schema = config.object.schema.relationships[config.key]; 76 | 77 | if (config.object.removed === true) { 78 | deferred.reject(AngularJsonAPIModelSourceError.create('Object has been removed', _this, 0, 'unlink')); 79 | } else if (config.target !== undefined && config.target.data.id === undefined) { 80 | deferred.reject(AngularJsonAPIModelSourceError.create('Can\'t unlink object without id through rest call', _this, 0, 'unlink')); 81 | } else if (schema.type === 'hasOne') { 82 | $http({ 83 | method: 'DELETE', 84 | headers: headers, 85 | url: url + '/' + config.object.data.id + '/relationships/' + config.key 86 | }).then(resolveHttp, rejectHttp.bind(null, 'get')).then(deferred.resolve, deferred.reject); 87 | } else if (schema.type === 'hasMany') { 88 | if (config.target === undefined) { 89 | $http({ 90 | method: 'PUT', 91 | headers: headers, 92 | data: {data: []}, 93 | url: url + '/' + config.object.data.id + '/relationships/' + config.key 94 | }).then(resolveHttp, rejectHttp.bind(null, 'unlink')).then(deferred.resolve, deferred.reject); 95 | } else { 96 | $http({ 97 | method: 'DELETE', 98 | headers: headers, 99 | url: url + '/' + config.object.data.id + '/relationships/' + config.key + '/' + config.target.data.id 100 | }).then(resolveHttp, rejectHttp.bind(null, 'unlink')).then(deferred.resolve, deferred.reject); 101 | } 102 | } 103 | 104 | return deferred.promise; 105 | } 106 | 107 | function link(config) { 108 | var deferred = $q.defer(); 109 | var schema = config.object.schema.relationships[config.key]; 110 | 111 | if (config.object.removed === true) { 112 | deferred.reject({errors: [{status: 0, statusText: 'Object has been removed'}]}); 113 | } else if (config.target === undefined || config.target.data.id === undefined) { 114 | deferred.reject({errors: [{status: 0, statusText: 'Can\'t link object without id through rest call'}]}); 115 | } else if (schema.type === 'hasOne') { 116 | $http({ 117 | method: 'PUT', 118 | headers: headers, 119 | data: {data: AngularJsonAPIModelLinkerService.toLinkData(config.target)}, 120 | url: url + '/' + config.object.data.id + '/relationships/' + config.key 121 | }).then(resolveHttp, rejectHttp.bind(null, 'link')).then(deferred.resolve, deferred.reject); 122 | } else if (schema.type === 'hasMany') { 123 | $http({ 124 | method: 'POST', 125 | headers: headers, 126 | data: {data: [AngularJsonAPIModelLinkerService.toLinkData(config.target)]}, 127 | url: url + '/' + config.object.data.id + '/relationships/' + config.key 128 | }).then(resolveHttp, rejectHttp.bind(null, 'link')).then(deferred.resolve, deferred.reject); 129 | } 130 | 131 | return deferred.promise; 132 | } 133 | 134 | function update(config) { 135 | return $http({ 136 | method: 'PUT', 137 | headers: headers, 138 | url: url + '/' + config.object.data.id, 139 | data: config.object.form.toJson() 140 | }).then(resolveHttp, rejectHttp.bind(null, 'update')); 141 | } 142 | 143 | function add(config) { 144 | return $http({ 145 | method: 'POST', 146 | headers: headers, 147 | url: url, 148 | data: config.object.form.toJson() 149 | }).then(resolveHttp, rejectHttp.bind(null, 'add')); 150 | } 151 | 152 | function resolveHttp(response) { 153 | return $q.resolve(response.data); 154 | } 155 | 156 | function rejectHttp(action, response) { 157 | var deferred = $q.defer(); 158 | 159 | if (response.status === 0) { 160 | $http({ 161 | method: 'GET', 162 | url: 'https://status.cloud.google.com/incidents.schema.json' 163 | }).then(rejectServerOffline, rejectNoConnection); 164 | } else { 165 | deferred.reject(AngularJsonAPIModelSourceError.create(response.statusText, _this, response.status, action)); 166 | } 167 | 168 | return deferred.promise; 169 | 170 | function rejectServerOffline(response) { 171 | deferred.reject(AngularJsonAPIModelSourceError.create('Server is offline', _this, response.status, action)); 172 | } 173 | 174 | function rejectNoConnection() { 175 | deferred.reject(AngularJsonAPIModelSourceError.create('No internet connection', _this, response.status, action)); 176 | } 177 | } 178 | } 179 | 180 | function encodeParams(params) { 181 | var encodedParams = {}; 182 | 183 | if (params === undefined) { 184 | return {}; 185 | } 186 | 187 | angular.forEach(params, function(paramValue, paramKey) { 188 | if (angular.isArray(paramValue)) { 189 | encodedParams[paramKey] = encodeValue(paramValue); 190 | } else if (angular.isObject(paramValue)) { 191 | angular.forEach(paramValue, function(paramInnerValue, paramInnerKey) { 192 | encodedParams[paramKey + '[' + paramInnerKey + ']'] = encodeValue(paramInnerValue); 193 | }); 194 | } else { 195 | encodedParams[paramKey] = paramValue; 196 | } 197 | }); 198 | 199 | return encodedParams; 200 | 201 | function encodeValue(argument) { 202 | if (angular.isArray(argument)) { 203 | return argument.join(','); 204 | } else { 205 | return argument; 206 | } 207 | } 208 | } 209 | 210 | function decodeParams(params) { 211 | var decodedParams = {}; 212 | 213 | angular.forEach(params, function(value, key) { 214 | var objectKeyStart = key.indexOf('['); 215 | value = value.split(','); 216 | 217 | if (objectKeyStart > -1) { 218 | var objectKey = key.substr(0, objectKeyStart); 219 | var objectElementKey = key.substr(objectKeyStart + 1, key.indexOf(']') - objectKeyStart - 1); 220 | 221 | decodedParams[objectKey] = decodedParams[objectKey] || {}; 222 | decodedParams[objectKey][objectElementKey] = value; 223 | } else { 224 | decodedParams[key] = value; 225 | } 226 | }); 227 | 228 | return decodedParams; 229 | } 230 | } 231 | })(); 232 | -------------------------------------------------------------------------------- /src/sources/rest/source-rest.module.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angular-jsonapi-rest', ['angular-jsonapi']); 5 | 6 | })(); 7 | -------------------------------------------------------------------------------- /src/sources/source-prototype.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angular-jsonapi') 5 | .factory('AngularJsonAPISourcePrototype', AngularJsonAPISourcePrototypeWrapper); 6 | 7 | function AngularJsonAPISourcePrototypeWrapper() { 8 | AngularJsonAPISourcePrototype.prototype.before = beforeSynchro; 9 | AngularJsonAPISourcePrototype.prototype.after = afterSynchro; 10 | AngularJsonAPISourcePrototype.prototype.begin = begin; 11 | AngularJsonAPISourcePrototype.prototype.finish = finish; 12 | AngularJsonAPISourcePrototype.prototype.synchronization = synchronization; 13 | 14 | return AngularJsonAPISourcePrototype; 15 | 16 | function AngularJsonAPISourcePrototype(name) { 17 | var _this = this; 18 | var allHooks = [ 19 | 'add', 20 | 'init', 21 | 'get', 22 | 'all', 23 | 'clearCache', 24 | 'remove', 25 | 'unlink', 26 | 'unlinkReflection', 27 | 'link', 28 | 'linkReflection', 29 | 'update', 30 | 'refresh', 31 | 'include' 32 | ]; 33 | 34 | _this.name = name; 35 | _this.state = {}; 36 | 37 | _this.beginHooks = {}; 38 | _this.beforeHooks = {}; 39 | _this.synchronizationHooks = {}; 40 | _this.afterHooks = {}; 41 | _this.finishHooks = {}; 42 | 43 | _this.options = {}; 44 | 45 | angular.forEach(allHooks, function(hookName) { 46 | _this.beginHooks[hookName] = []; 47 | _this.beforeHooks[hookName] = []; 48 | _this.synchronizationHooks[hookName] = []; 49 | _this.afterHooks[hookName] = []; 50 | _this.finishHooks[hookName] = []; 51 | _this.state[hookName] = { 52 | loading: false, 53 | success: true 54 | }; 55 | }); 56 | } 57 | 58 | function begin(action, callback) { 59 | var _this = this; 60 | 61 | _this.beginHooks[action].push(callback); 62 | } 63 | 64 | function finish(action, callback) { 65 | var _this = this; 66 | 67 | _this.finishHooks[action].push(callback); 68 | } 69 | 70 | function beforeSynchro(action, callback) { 71 | var _this = this; 72 | 73 | _this.beforeHooks[action].push(callback); 74 | } 75 | 76 | function afterSynchro(action, callback) { 77 | var _this = this; 78 | 79 | _this.afterHooks[action].push(callback); 80 | } 81 | 82 | function synchronization(action, callback) { 83 | var _this = this; 84 | 85 | _this.synchronizationHooks[action].push(callback); 86 | } 87 | 88 | } 89 | })(); 90 | -------------------------------------------------------------------------------- /src/synchronizers/synchronizer-prototype.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angular-jsonapi') 5 | .factory('AngularJsonAPISynchronizerPrototype', AngularJsonAPISynchronizerPrototypeWrapper); 6 | 7 | function AngularJsonAPISynchronizerPrototypeWrapper($log) { 8 | 9 | AngularJsonAPISynchronizerPrototype.prototype.synchronize = synchronize; 10 | 11 | return AngularJsonAPISynchronizerPrototype; 12 | 13 | function AngularJsonAPISynchronizerPrototype(sources) { 14 | var _this = this; 15 | 16 | _this.sources = sources; 17 | } 18 | 19 | function synchronize(config) { 20 | var _this = this; 21 | 22 | $log.debug('Synchro Collection', _this.resource.schema.type, config); 23 | 24 | if (config.action === undefined) { 25 | $log.error('Can\'t synchronize undefined action', config); 26 | } 27 | } 28 | } 29 | })(); 30 | -------------------------------------------------------------------------------- /src/synchronizers/synchronizer-simple.factory.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angular-jsonapi') 5 | .factory('AngularJsonAPISynchronizerSimple', AngularJsonAPISynchronizerSimpleWrapper); 6 | 7 | function AngularJsonAPISynchronizerSimpleWrapper(AngularJsonAPISynchronizerPrototype, $q, $log) { 8 | 9 | AngularJsonAPISynchronizerSimple.prototype = Object.create(AngularJsonAPISynchronizerPrototype.prototype); 10 | AngularJsonAPISynchronizerSimple.prototype.constructor = AngularJsonAPISynchronizerSimple; 11 | 12 | AngularJsonAPISynchronizerSimple.prototype.synchronize = synchronize; 13 | 14 | return { 15 | create: AngularJsonAPISynchronizerSimpleFactory 16 | }; 17 | 18 | function AngularJsonAPISynchronizerSimpleFactory(sources) { 19 | return new AngularJsonAPISynchronizerSimple(sources); 20 | } 21 | 22 | function AngularJsonAPISynchronizerSimple(sources) { 23 | var _this = this; 24 | 25 | _this.state = {}; 26 | 27 | AngularJsonAPISynchronizerPrototype.call(_this, sources); 28 | 29 | angular.forEach(sources, function(source) { 30 | source.synchronizer = _this; 31 | }); 32 | } 33 | 34 | function synchronize(config) { 35 | var _this = this; 36 | var promises = []; 37 | var deferred = $q.defer(); 38 | var action = config.action; 39 | 40 | AngularJsonAPISynchronizerPrototype.prototype.synchronize.call(_this, config); 41 | 42 | angular.forEach(_this.sources, function(source) { 43 | angular.forEach(source.beginHooks[action], function(hook) { 44 | deferred.notify({step: 'begin', data: hook.call(_this, config)}); 45 | }); 46 | }); 47 | 48 | angular.forEach(_this.sources, function(source) { 49 | angular.forEach(source.beforeHooks[action], function(hook) { 50 | deferred.notify({step: 'before', data: hook.call(_this, config)}); 51 | }); 52 | }); 53 | 54 | angular.forEach(_this.sources, function(source) { 55 | angular.forEach(source.synchronizationHooks[action], function(hook) { 56 | promises.push(hook.call(_this, config)); 57 | }); 58 | }); 59 | 60 | $q.allSettled(promises, resolvedCallback, rejectedCallback).then(resolved, rejected); 61 | 62 | function resolvedCallback(value) { 63 | deferred.notify({step: 'synchronization', data: value}); 64 | } 65 | 66 | function rejectedCallback(reason) { 67 | deferred.notify({step: 'synchronization', errors: reason}); 68 | } 69 | 70 | function resolved(results) { 71 | _this.state[action] = _this.state[action] || {}; 72 | _this.state[action].success = true; 73 | 74 | angular.forEach(results, function(result) { 75 | if (result.success === false) { 76 | _this.state[action].success = false; 77 | } 78 | }); 79 | 80 | angular.forEach(_this.sources, function(source) { 81 | angular.forEach(source.afterHooks[action], function(hook) { 82 | deferred.notify({step: 'after', errors: hook.call(_this, config, results)}); 83 | }); 84 | }); 85 | 86 | var data; 87 | var errors = []; 88 | 89 | angular.forEach(results, function(result) { 90 | if (result.success === true) { 91 | data = result.value; 92 | } else { 93 | errors.push({ 94 | key: action, 95 | object: result.reason 96 | }); 97 | } 98 | }); 99 | 100 | if (errors.length > 0) { 101 | deferred.reject({data: data || {}, finish: finish, errors: errors}); 102 | } else { 103 | deferred.resolve({data: data || {}, finish: finish, errors: errors}); 104 | } 105 | } 106 | 107 | function finish() { 108 | angular.forEach(_this.sources, function(source) { 109 | angular.forEach(source.finishHooks[action], function(hook) { 110 | deferred.notify({step: 'finish', errors: hook.call(_this, config)}); 111 | }); 112 | }); 113 | } 114 | 115 | function rejected(errors) { 116 | $log.error('All settled rejected! Something went wrong'); 117 | 118 | deferred.reject({finish: angular.noop, errors: errors}); 119 | } 120 | 121 | return deferred.promise; 122 | } 123 | } 124 | })(); 125 | -------------------------------------------------------------------------------- /src/synchronizers/synchronizer-simple.factory.spec.js: -------------------------------------------------------------------------------- 1 | /*jshint expr: true*/ 2 | (function() { 3 | 'use strict'; 4 | 5 | describe('AngularJsonAPISynchronizerSimple', function() { 6 | 7 | beforeEach(module('angular-jsonapi')); 8 | 9 | it('returns valid model', inject(function(AngularJsonAPISynchronizerSimple) { 10 | expect(AngularJsonAPISynchronizerSimple).toBeDefined(); 11 | })); 12 | }); 13 | })(); 14 | -------------------------------------------------------------------------------- /src/utils/all-settled.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angular-jsonapi') 5 | .config(provide); 6 | 7 | function provide($provide) { 8 | $provide.decorator('$q', decorator); 9 | } 10 | 11 | function decorator($delegate) { 12 | var $q = $delegate; 13 | 14 | $q.allSettled = $q.allSettled || allSettled; 15 | 16 | function allSettled(promises, resolvedCallback, rejectedCallback) { 17 | // Implementation of allSettled function from Kris Kowal's Q: 18 | // https://github.com/kriskowal/q/wiki/API-Reference#promiseallsettled 19 | // by Michael Kropat from http://stackoverflow.com/a/27114615/1400432 slightly modified 20 | 21 | var wrapped = angular.isArray(promises) ? [] : {}; 22 | 23 | angular.forEach(promises, function(promise, key) { 24 | if (!wrapped.hasOwnProperty(key)) { 25 | wrapped[key] = wrap(promise); 26 | } 27 | }); 28 | 29 | return $q.all(wrapped); 30 | 31 | function wrap(promise) { 32 | return $q.resolve(promise) 33 | .then(function(value) { 34 | if (angular.isFunction(resolvedCallback)) { 35 | resolvedCallback(value); 36 | } 37 | 38 | return { success: true, value: value }; 39 | }, 40 | 41 | function(reason) { 42 | if (angular.isFunction(rejectedCallback)) { 43 | rejectedCallback(reason); 44 | } 45 | 46 | return { success: false, reason: reason }; 47 | }); 48 | } 49 | } 50 | 51 | return $q; 52 | } 53 | })(); 54 | -------------------------------------------------------------------------------- /src/utils/kebab-case.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angular-jsonapi') 5 | .constant('toKebabCase', function(str) { 6 | return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); 7 | }); 8 | 9 | })(); 10 | -------------------------------------------------------------------------------- /src/utils/lazy-property.js: -------------------------------------------------------------------------------- 1 | // from https://www.sitepen.com/blog/2012/10/19/lazy-property-access/ 2 | (function() { 3 | 'use strict'; 4 | 5 | angular.module('angular-jsonapi') 6 | .constant('lazyProperty', function(target, propertyName, callback) { 7 | var result; 8 | var done; 9 | Object.defineProperty(target, propertyName, { 10 | get: function() { // Define the getter 11 | if (!done) { 12 | // We cache the result and only compute once. 13 | done = true; 14 | result = callback.call(target); 15 | } 16 | 17 | return result; 18 | }, 19 | 20 | // Keep it enumerable and configurable, certainly not necessary. 21 | enumerable: true, 22 | configurable: true 23 | }); 24 | }); 25 | 26 | })(); 27 | -------------------------------------------------------------------------------- /src/utils/named-function.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('angular-jsonapi') 5 | .constant('namedFunction', namedFunction); 6 | 7 | function namedFunction(name, fn) { 8 | return new Function('fn', 9 | 'return function ' + name + '(){ return fn.apply(this,arguments)}' 10 | )(fn); 11 | } 12 | })(); 13 | --------------------------------------------------------------------------------