├── src ├── demo │ ├── index.scss │ ├── index.spec.js │ ├── tests │ │ ├── noduplicatedhttpcalls.html │ │ └── noduplicatedhttpcalls.component.ts │ ├── assign.ts │ ├── photos │ │ ├── photos.service.ts │ │ ├── photos.html │ │ └── photos.component.ts │ ├── containers │ │ ├── app.html │ │ └── app.ts │ ├── index.html │ ├── authors │ │ ├── authors.service.ts │ │ ├── authors.html │ │ ├── authors.component.ts │ │ ├── author.html │ │ └── author.component.ts │ ├── books │ │ ├── book.html │ │ ├── books.service.ts │ │ ├── book.component.ts │ │ ├── books.html │ │ └── books.component.ts │ ├── routes.ts │ └── index.ts └── library │ ├── interfaces │ ├── storeobject.d.ts │ ├── params.d.ts │ ├── params-resource.d.ts │ ├── resources-by-id.d.ts │ ├── schema.d.ts │ ├── relationships.d.ts │ ├── resources-by-type.d.ts │ ├── links.d.ts │ ├── data-object.d.ts │ ├── cache.d.ts │ ├── page.d.ts │ ├── data-resource.d.ts │ ├── exec-params.d.ts │ ├── exec-params-processed.d.ts │ ├── data-collection.d.ts │ ├── attributes.d.ts │ ├── errors.d.ts │ ├── params-collection.d.ts │ ├── relationship.d.ts │ ├── document.d.ts │ ├── cachestore.d.ts │ ├── collection.d.ts │ ├── cachememory.d.ts │ ├── index.d.ts │ ├── core.d.ts │ ├── service.d.ts │ └── resource.d.ts │ ├── services │ ├── page.ts │ ├── core-services.service.ts │ ├── url-params-builder.ts │ ├── base.ts │ ├── resource-functions.ts │ ├── localfilter.ts │ ├── path-builder.ts │ ├── noduplicatedhttpcalls.service.ts │ ├── cachememory.ts │ ├── resource-relationships-converter.ts │ ├── converter.ts │ └── cachestore.ts │ ├── index.ts │ ├── parent-resource-service.ts │ ├── sources │ ├── http.service.ts │ └── store.service.ts │ ├── core.ts │ ├── resource.ts │ └── service.ts ├── .gitattributes ├── gulp_tasks ├── ghpages.js ├── browsersync.js ├── partials.js ├── karma.js ├── misc.js ├── library-dts.js └── webpack.js ├── index.js ├── conf ├── browsersync-dist.conf.js ├── browsersync.conf.js ├── tsconfig.build.json ├── webpack-test.conf.js ├── gulp.conf.js ├── karma.conf.js ├── karma-auto.conf.js ├── webpack-library.conf.js ├── webpack.conf.js └── webpack-demo.conf.js ├── .gitignore ├── .editorconfig ├── .htmlhintrc ├── tsconfig.json ├── .eslintrc ├── LICENSE ├── gulpfile.js ├── CHANGELOG.md ├── package.json ├── tslint.json └── README.md /src/demo/index.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /src/library/interfaces/storeobject.d.ts: -------------------------------------------------------------------------------- 1 | export interface IStoreObject { 2 | _lastupdate_time: number; 3 | } 4 | -------------------------------------------------------------------------------- /src/demo/index.spec.js: -------------------------------------------------------------------------------- 1 | const context = require.context('./app', true, /\.(js|ts|tsx)$/); 2 | context.keys().forEach(context); 3 | -------------------------------------------------------------------------------- /src/library/interfaces/params.d.ts: -------------------------------------------------------------------------------- 1 | export interface IParams { 2 | beforepath?: string; 3 | include?: Array; 4 | ttl?: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/library/interfaces/params-resource.d.ts: -------------------------------------------------------------------------------- 1 | import { IParams } from './params.d'; 2 | 3 | interface IParamsResource extends IParams { 4 | id?: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/library/interfaces/resources-by-id.d.ts: -------------------------------------------------------------------------------- 1 | import { IResource } from './resource'; 2 | 3 | export interface IResourcesById { 4 | [resource_id: string]: IResource; 5 | } 6 | -------------------------------------------------------------------------------- /src/library/interfaces/schema.d.ts: -------------------------------------------------------------------------------- 1 | export interface ISchema { 2 | attributes?: object; 3 | relationships?: object; 4 | ttl?: number; 5 | path?: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/library/interfaces/relationships.d.ts: -------------------------------------------------------------------------------- 1 | import { IRelationship } from '../interfaces/relationship.d'; 2 | 3 | interface IRelationships { 4 | [value: string]: IRelationship; 5 | } 6 | -------------------------------------------------------------------------------- /src/library/interfaces/resources-by-type.d.ts: -------------------------------------------------------------------------------- 1 | import { IResourcesById } from './resources-by-id'; 2 | 3 | export interface IResourcesByType { 4 | [type: string]: IResourcesById; 5 | } 6 | -------------------------------------------------------------------------------- /gulp_tasks/ghpages.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var ghPages = require('gulp-gh-pages'); 3 | 4 | gulp.task('ghpages', function() { 5 | return gulp.src('./dist-demo/**/*') 6 | .pipe(ghPages()); 7 | }); 8 | -------------------------------------------------------------------------------- /src/library/interfaces/links.d.ts: -------------------------------------------------------------------------------- 1 | // http://org/format/#document-links 2 | export interface ILinks { 3 | self?: string; 4 | related?: { 5 | href: string; 6 | meta: object; 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /src/library/interfaces/data-object.d.ts: -------------------------------------------------------------------------------- 1 | import { IDocument } from './document'; 2 | import { IDataResource } from './data-resource'; 3 | 4 | interface IDataObject extends IDocument { 5 | data: IDataResource; 6 | } 7 | -------------------------------------------------------------------------------- /src/library/interfaces/cache.d.ts: -------------------------------------------------------------------------------- 1 | import { IResource } from '../interfaces'; 2 | 3 | export interface ICache { 4 | setResource(resource: IResource): void; 5 | deprecateCollections(path_start_with: string): boolean; 6 | } 7 | -------------------------------------------------------------------------------- /src/library/services/page.ts: -------------------------------------------------------------------------------- 1 | import { IPage } from '../interfaces/page.d'; 2 | 3 | export class Page implements IPage { 4 | public number = 0; 5 | 6 | public total_resources = 0; 7 | public resources_per_page = 0; 8 | } 9 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Should already be required, here for clarity 2 | require('angular'); 3 | 4 | // Now load Ts Angular Jsonapi 5 | require('./dist/ts-angular-jsonapi.js'); 6 | 7 | // Export namespace 8 | module.exports = 'ts-angular-jsonapi'; 9 | -------------------------------------------------------------------------------- /src/demo/tests/noduplicatedhttpcalls.html: -------------------------------------------------------------------------------- 1 |

2 | Testing library with 3 equal HTTP request. Library only call to server only one time but resolve all promises individually. 3 |

4 |

5 | Check console and network activity. 6 |

7 | -------------------------------------------------------------------------------- /conf/browsersync-dist.conf.js: -------------------------------------------------------------------------------- 1 | const conf = require('./gulp.conf'); 2 | 3 | module.exports = function () { 4 | return { 5 | server: { 6 | baseDir: [ 7 | conf.paths.dist 8 | ] 9 | }, 10 | open: false 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /conf/browsersync.conf.js: -------------------------------------------------------------------------------- 1 | const conf = require('./gulp.conf'); 2 | 3 | module.exports = function () { 4 | return { 5 | server: { 6 | baseDir: [ 7 | conf.paths.tmp, 8 | conf.paths.src 9 | ] 10 | }, 11 | open: false 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /src/library/interfaces/page.d.ts: -------------------------------------------------------------------------------- 1 | export interface IPage { 2 | number: number; 3 | 4 | // http://jsonapi.org/format/#fetching-pagination 5 | limit?: number; 6 | 7 | // multinexo 8 | total_resources?: number; 9 | resources_per_page?: number; 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .tmp/ 2 | coverage/ 3 | node_modules/ 4 | jspm_packages/ 5 | bower_components/ 6 | 7 | .publish 8 | /build/ 9 | /dist/ 10 | /dist-demo/ 11 | **/bower_packages 12 | .DS_Store 13 | npm-debug.log 14 | 15 | # all generated JS 16 | src/js 17 | src/app/**/*.js 18 | src/app/**/*.js.map 19 | -------------------------------------------------------------------------------- /src/demo/assign.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable-next-line:interface-name 2 | interface ObjectCtor extends ObjectConstructor { 3 | assign(target: any, ...sources: any[]): any; 4 | } 5 | declare var Object: ObjectCtor; 6 | export let assign = Object.assign ? Object.assign : function(target: any, ...sources: any[]): any { 7 | return; 8 | }; 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [src/**/*] 14 | indent_size = 4 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /src/library/interfaces/data-resource.d.ts: -------------------------------------------------------------------------------- 1 | import { IAttributes } from '../interfaces'; 2 | import { ILinks } from '../interfaces/links.d'; 3 | 4 | interface IDataResource { 5 | type: string; 6 | id: string; 7 | attributes?: IAttributes; 8 | relationships?: object; 9 | links?: ILinks; 10 | meta?: object; 11 | } 12 | -------------------------------------------------------------------------------- /src/library/interfaces/exec-params.d.ts: -------------------------------------------------------------------------------- 1 | import { IParamsCollection, IParamsResource } from '../interfaces'; 2 | 3 | export interface IExecParams { 4 | id: string; 5 | params?: IParamsCollection | IParamsResource | Function; 6 | fc_success?: Function; 7 | fc_error?: Function; 8 | exec_type: 'all' | 'get' | 'delete' | 'save'; 9 | } 10 | -------------------------------------------------------------------------------- /src/library/interfaces/exec-params-processed.d.ts: -------------------------------------------------------------------------------- 1 | import { IParamsCollection, IParamsResource } from '../interfaces'; 2 | 3 | export interface IExecParamsProcessed { 4 | id: string; 5 | params: IParamsCollection | IParamsResource; 6 | fc_success: Function; 7 | fc_error: Function; 8 | exec_type: 'all' | 'get' | 'delete' | 'save'; 9 | } 10 | -------------------------------------------------------------------------------- /src/library/interfaces/data-collection.d.ts: -------------------------------------------------------------------------------- 1 | import { IDataResource } from './data-resource'; 2 | import { IDocument } from '../interfaces/document'; 3 | import { IPage } from './page'; 4 | 5 | interface IDataCollection extends IDocument { 6 | data: Array; 7 | page?: IPage; 8 | _lastupdate_time?: number; // used when come from Store 9 | } 10 | -------------------------------------------------------------------------------- /src/library/interfaces/attributes.d.ts: -------------------------------------------------------------------------------- 1 | export interface IAttributes { 2 | // problem with lines like 3 | /* 4 | this._receipt_number = order.attributes.receipt_number; 5 | this.transaction.attributes._due_date_with_format = new Date(this.transaction.attributes.due_date); 6 | */ 7 | // [value: string]: boolean | string | number; 8 | [value: string]: any; 9 | } 10 | -------------------------------------------------------------------------------- /.htmlhintrc: -------------------------------------------------------------------------------- 1 | { 2 | "tagname-lowercase": true, 3 | "space-tab-mixed-disabled": false, 4 | "attr-lowercase": true, 5 | "attr-value-double-quotes": true, 6 | "doctype-first": false, 7 | "tag-pair": true, 8 | "spec-char-escape": false, 9 | "id-unique": true, 10 | "src-not-empty": true, 11 | "attr-no-duplication": true, 12 | "title-require": true 13 | } 14 | -------------------------------------------------------------------------------- /src/library/interfaces/errors.d.ts: -------------------------------------------------------------------------------- 1 | import { IDocument } from './document'; 2 | 3 | interface IErrors extends IDocument { 4 | errors: [ 5 | { 6 | code?: string, 7 | source?: { 8 | attributes?: string, 9 | pointer: string 10 | }, 11 | title?: string, 12 | detail?: string 13 | } 14 | ]; 15 | } 16 | -------------------------------------------------------------------------------- /src/demo/photos/photos.service.ts: -------------------------------------------------------------------------------- 1 | import * as Jsonapi from '../../library/index'; 2 | 3 | export class PhotosService extends Jsonapi.Service { 4 | type = 'photos'; 5 | public schema: Jsonapi.ISchema = { 6 | attributes: { 7 | title: {}, 8 | uri: {}, 9 | imageable_id: {}, 10 | created_at: {}, 11 | updated_at: {} 12 | } 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/library/interfaces/params-collection.d.ts: -------------------------------------------------------------------------------- 1 | import { IParams } from './params.d'; 2 | import { IPage } from './page.d'; 3 | 4 | interface IParamsCollection extends IParams { 5 | localfilter?: object; 6 | remotefilter?: object; 7 | smartfilter?: object; 8 | page?: IPage; 9 | storage_ttl?: number; 10 | cachehash?: string; // solution for when we have different resources with a same id 11 | } 12 | -------------------------------------------------------------------------------- /src/library/interfaces/relationship.d.ts: -------------------------------------------------------------------------------- 1 | import { ICollection, IResource } from '../interfaces'; 2 | import { IDataResource } from './data-resource'; 3 | 4 | interface IRelationship { 5 | // IDataResource added for this reason: 6 | // redefined from IDataResource (return errors /home/rsk/desarrollo/ts-angular-jsonapi/src/library/services/resource-functions.ts) 7 | data: ICollection | IResource | IDataResource | {}; 8 | } 9 | -------------------------------------------------------------------------------- /src/library/interfaces/document.d.ts: -------------------------------------------------------------------------------- 1 | import { IDataResource } from '../interfaces/data-resource'; 2 | import { ILinks } from '../interfaces/links.d'; 3 | 4 | // http://org/format/#document-top-level 5 | interface IDocument { 6 | // data in child interface IJsonapiCollection 7 | // error in child interface IJsonapiErrors 8 | jsonapi?: string; 9 | links?: ILinks; 10 | included?: Array; 11 | meta?: { [key: string]: any }; 12 | } 13 | -------------------------------------------------------------------------------- /src/library/interfaces/cachestore.d.ts: -------------------------------------------------------------------------------- 1 | import { ICollection, IResource } from '../interfaces'; 2 | import { ICache } from '../interfaces/cache.d'; 3 | 4 | export interface ICacheStore extends ICache { 5 | getResource(resource: IResource): ng.IPromise; 6 | getCollectionFromStorePromise(url:string, includes: Array, collection: ICollection): ng.IPromise; 7 | setCollection(url: string, collection: ICollection, include: Array): void; 8 | } 9 | -------------------------------------------------------------------------------- /src/library/interfaces/collection.d.ts: -------------------------------------------------------------------------------- 1 | import { IResource } from './resource'; 2 | import { IPage } from './page'; 3 | import { IDataResource } from './data-resource'; 4 | 5 | export interface ICollection extends Array { 6 | $length: number; 7 | $toArray: Array; 8 | $is_loading: boolean; 9 | $source: 'new' | 'memory' | 'store' | 'server'; 10 | $cache_last_update: number; 11 | data: Array; // this need disapear is for datacollection 12 | page: IPage; 13 | } 14 | -------------------------------------------------------------------------------- /src/demo/photos/photos.html: -------------------------------------------------------------------------------- 1 |
2 |

photos

3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
IDTitleURI
{{ photo.id }}{{ photo.attributes.title }}{{ photo.attributes.uri }}
17 |
18 | -------------------------------------------------------------------------------- /src/library/services/core-services.service.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | import '../sources/http.service'; 3 | import '../sources/store.service'; 4 | 5 | export class CoreServices { 6 | 7 | /** @ngInject */ 8 | public constructor( 9 | protected JsonapiHttp, 10 | protected rsJsonapiConfig, 11 | protected $q: ng.IQService, 12 | protected JsonapiStoreService 13 | ) { 14 | 15 | } 16 | } 17 | 18 | angular.module('Jsonapi.services').service('JsonapiCoreServices', CoreServices); 19 | -------------------------------------------------------------------------------- /src/demo/containers/app.html: -------------------------------------------------------------------------------- 1 |

2 | TS Angular Jsonapi Example 3 | {{ $ctrl.loading }} 4 |

5 | 6 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/demo/index.html: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | TS Angular Jsonapi Example 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /gulp_tasks/browsersync.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const browserSync = require('browser-sync'); 3 | const spa = require('browser-sync-spa'); 4 | 5 | const browserSyncConf = require('../conf/browsersync.conf'); 6 | const browserSyncDistConf = require('../conf/browsersync-dist.conf'); 7 | 8 | browserSync.use(spa()); 9 | 10 | gulp.task('browsersync', browserSyncServe); 11 | gulp.task('browsersync:dist', browserSyncDist); 12 | 13 | function browserSyncServe(done) { 14 | browserSync.init(browserSyncConf()); 15 | done(); 16 | } 17 | 18 | function browserSyncDist(done) { 19 | browserSync.init(browserSyncDistConf()); 20 | done(); 21 | } 22 | -------------------------------------------------------------------------------- /gulp_tasks/partials.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const htmlmin = require('gulp-htmlmin'); 3 | const angularTemplatecache = require('gulp-angular-templatecache'); 4 | const insert = require('gulp-insert'); 5 | 6 | const conf = require('../conf/gulp.conf'); 7 | 8 | gulp.task('partials', partials); 9 | 10 | function partials() { 11 | return gulp.src(conf.path.src('**/*.html')) 12 | .pipe(htmlmin()) 13 | .pipe(angularTemplatecache('templateCacheHtml.ts', { 14 | module: conf.ngModule, 15 | // root: 'app' 16 | })) 17 | .pipe(insert.prepend('import * as angular from \'angular\';')) 18 | .pipe(gulp.dest(conf.path.tmp())); 19 | } 20 | -------------------------------------------------------------------------------- /src/library/interfaces/cachememory.d.ts: -------------------------------------------------------------------------------- 1 | import { ICollection, IResource } from '../interfaces'; 2 | import { ICache } from '../interfaces/cache.d'; 3 | 4 | export interface ICacheMemory extends ICache { 5 | resources: { [id: string]: IResource }; 6 | 7 | getOrCreateCollection(url: string): ICollection; 8 | isCollectionExist(url: string): boolean; 9 | isCollectionLive(url: string, ttl: number): boolean; 10 | 11 | isResourceLive(id: string, ttl: number): boolean; 12 | getOrCreateResource(type: string, id: string): IResource; 13 | setCollection(url: string, collection: ICollection): void; 14 | 15 | removeResource(id: string): void; 16 | } 17 | -------------------------------------------------------------------------------- /src/library/interfaces/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './attributes.d'; 2 | export * from './core.d'; 3 | export * from './collection.d'; 4 | export * from './schema.d'; 5 | export * from './service.d'; 6 | export * from './resource.d'; 7 | export * from './relationship.d'; 8 | export * from './relationships.d'; 9 | export * from './cachestore.d'; 10 | export * from './cachememory.d'; 11 | export * from './params-collection.d'; 12 | export * from './params-resource.d'; 13 | export * from './resources-by-id.d'; 14 | export * from './resources-by-type.d'; 15 | export * from './exec-params.d'; 16 | export * from './exec-params-processed.d'; 17 | export * from './storeobject.d'; 18 | -------------------------------------------------------------------------------- /src/demo/authors/authors.service.ts: -------------------------------------------------------------------------------- 1 | import * as Jsonapi from '../../library/index'; 2 | 3 | export class AuthorsService extends Jsonapi.Service { 4 | type = 'authors'; 5 | public schema: Jsonapi.ISchema = { 6 | attributes: { 7 | name: { }, 8 | date_of_birth: { default: '1993-12-10'}, 9 | date_of_death: {}, 10 | created_at: {}, 11 | updated_at: {} 12 | }, 13 | relationships: { 14 | books: { 15 | hasMany: true 16 | }, 17 | photos: { 18 | hasMany: true 19 | } 20 | }, 21 | ttl: 10 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "removeComments": false, 7 | "noImplicitAny": false, 8 | "typeRoots": ["node_modules/@types/"], 9 | "types": [ 10 | "angular-ui-router", 11 | "angular", 12 | "angular-mocks", 13 | "jquery", 14 | "node", 15 | "jasmine", 16 | "es6-shim" 17 | ] 18 | }, 19 | "compileOnSave": false, 20 | "filesGlob": [ 21 | "src/**/*.ts", 22 | "src/**/*.tsx", 23 | "!node_modules/**" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /conf/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "removeComments": false, 7 | "noImplicitAny": false, 8 | "typeRoots": ["node_modules/@types/"], 9 | "declaration": true, 10 | "noResolve": false, 11 | "moduleResolution":"node", 12 | "types": [ 13 | "angular-ui-router", 14 | "angular", 15 | "angular-mocks", 16 | "jquery", 17 | "node", 18 | "jasmine", 19 | "es6-shim" 20 | ] 21 | }, 22 | "compileOnSave": false, 23 | "filesGlob": [ 24 | "src/**/*.ts", 25 | "src/**/*.tsx", 26 | "!node_modules/**" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/library/services/url-params-builder.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | 3 | export class UrlParamsBuilder { 4 | 5 | private toparamsarray(params, add = ''): string { 6 | let ret = ''; 7 | if (angular.isArray(params) || angular.isObject(params)) { 8 | angular.forEach(params, (value, key) => { 9 | ret += this.toparamsarray(value, add + '[' + key + ']'); 10 | }); 11 | } else { 12 | ret += add + '=' + params; 13 | } 14 | 15 | return ret; 16 | } 17 | 18 | public toparams(params): string { 19 | let ret = ''; 20 | angular.forEach(params, (value, key) => { 21 | ret += this.toparamsarray(value, '&' + key); 22 | }); 23 | 24 | return ret.slice(1); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/library/interfaces/core.d.ts: -------------------------------------------------------------------------------- 1 | import { IService, IResource } from './index'; 2 | 3 | export interface ICore { 4 | // jsonapiServices: Object; 5 | 6 | loadingsCounter: number; 7 | loadingsStart: Function; 8 | loadingsDone: Function; 9 | loadingsError: Function; 10 | loadingsOffline: Function; 11 | 12 | _register(clase: IService): boolean; 13 | getResourceService(type: string): IService; 14 | refreshLoadings(factor: number): void; 15 | clearCache(): void; 16 | duplicateResource(resource: IResource, ...relations_types: Array): IResource; 17 | 18 | // static 19 | me?: IService; 20 | 21 | // defined on core code too 22 | injectedServices?: { 23 | $q: ng.IQService, 24 | JsonapiStoreService: any, 25 | JsonapiHttp: any, 26 | rsJsonapiConfig: any 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /gulp_tasks/karma.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const gulp = require('gulp'); 4 | const karma = require('karma'); 5 | 6 | gulp.task('karma:single-run', karmaSingleRun); 7 | gulp.task('karma:auto-run', karmaAutoRun); 8 | 9 | function karmaFinishHandler(done) { 10 | return failCount => { 11 | done(failCount ? new Error(`Failed ${failCount} tests.`) : null); 12 | }; 13 | } 14 | 15 | function karmaSingleRun(done) { 16 | const configFile = path.join(process.cwd(), 'conf', 'karma.conf.js'); 17 | const karmaServer = new karma.Server({configFile}, karmaFinishHandler(done)); 18 | karmaServer.start(); 19 | } 20 | 21 | function karmaAutoRun(done) { 22 | const configFile = path.join(process.cwd(), 'conf', 'karma-auto.conf.js'); 23 | const karmaServer = new karma.Server({configFile}, karmaFinishHandler(done)); 24 | karmaServer.start(); 25 | } 26 | -------------------------------------------------------------------------------- /src/demo/photos/photos.component.ts: -------------------------------------------------------------------------------- 1 | import * as Jsonapi from '../../library/index'; 2 | 3 | class PhotosController implements ng.IController { 4 | public photos: Jsonapi.ICollection; 5 | 6 | /** @ngInject */ 7 | constructor( 8 | protected PhotosService: Jsonapi.IService 9 | ) { 10 | // if you check your console, library make only one request 11 | this.makeRequest(1); 12 | this.makeRequest(2); 13 | this.makeRequest(3); 14 | this.makeRequest(4); 15 | this.makeRequest(5); 16 | } 17 | 18 | public $onInit() { 19 | 20 | } 21 | 22 | public makeRequest(id) { 23 | this.photos = this.PhotosService.all( 24 | succes => { 25 | console.log('photos success', id, this.photos); 26 | } 27 | ); 28 | } 29 | } 30 | 31 | export class Photos { 32 | public templateUrl = 'photos/photos.html'; 33 | public controller = PhotosController; 34 | } 35 | -------------------------------------------------------------------------------- /src/library/interfaces/service.d.ts: -------------------------------------------------------------------------------- 1 | import { ISchema, IResource, ICollection, ICacheMemory, IAttributes, ICacheStore, IParamsCollection, IParamsResource } from './index'; 2 | 3 | export interface IService { 4 | type: string; 5 | schema: ISchema; 6 | getPrePath(): string; 7 | getPath(): string; 8 | register(): boolean; 9 | get(id: string | number, params?: IParamsResource | Function, fc_success?: Function, fc_error?: Function): T; 10 | all(params?: IParamsCollection | Function, success?: Function, error?: Function): ICollection; 11 | delete (id: String, params?: IParamsResource | Function, success?: Function, error?: Function): void; 12 | getService ():T; 13 | clearCacheMemory? (): boolean; 14 | new?(): T; 15 | cachememory: ICacheMemory; 16 | cachestore: ICacheStore; 17 | parseFromServer(attributes: IAttributes): void; 18 | parseToServer?(attributes: IAttributes): void; 19 | } 20 | -------------------------------------------------------------------------------- /src/demo/books/book.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |

Book #{{ $ctrl.book.id }}

5 |
    6 |
  • Title: {{ $ctrl.book.attributes.title }}
  • 7 |
  • Date Published: {{ $ctrl.book.attributes.date_published | date }}
  • 8 |
9 | 10 |

Author (one)

11 |

This a relationship with hasMany:false

12 |
    13 |
  • Name: {{ $ctrl.book.relationships.author.data.attributes.name }}
  • 14 |
15 | 16 |

Photos (many)

17 |

This a relationship with hasMany:true

18 | 23 | 24 |

25 | Volver 26 |

27 |
28 | -------------------------------------------------------------------------------- /src/demo/tests/noduplicatedhttpcalls.component.ts: -------------------------------------------------------------------------------- 1 | import * as Jsonapi from '../../library/index'; 2 | 3 | class NoDuplicatedHttpCallsComponent implements ng.IController { 4 | public authors: Array = []; 5 | 6 | /** @ngInject */ 7 | constructor( 8 | protected JsonapiCore, 9 | protected AuthorsService: Jsonapi.IService 10 | ) { 11 | for (let i = 1; i <= 3; i++) { 12 | this.authors[i] = AuthorsService.all( 13 | success => { 14 | console.log('success authors request', i, this.authors); 15 | }, 16 | error => { 17 | console.log('error authors request', i, error); 18 | } 19 | ); 20 | } 21 | } 22 | 23 | public $onInit() { 24 | 25 | } 26 | } 27 | 28 | export class NoDuplicatedHttpCalls { 29 | public templateUrl = 'tests/noduplicatedhttpcalls.html'; 30 | public controller = NoDuplicatedHttpCallsComponent; 31 | } 32 | -------------------------------------------------------------------------------- /src/library/interfaces/resource.d.ts: -------------------------------------------------------------------------------- 1 | import { IRelationships, ICollection, IAttributes, IParamsResource, IService } from './index'; 2 | import { IDataObject } from './data-object'; 3 | 4 | export interface IResource { 5 | type: string; 6 | id: string; 7 | attributes?: IAttributes; 8 | relationships: IRelationships; // redefined from IDataResource 9 | 10 | is_new: boolean; 11 | is_loading: boolean; 12 | is_saving: boolean; 13 | lastupdate?: number; 14 | 15 | 16 | reset? (): void; 17 | addRelationship? (resource: IResource, type_alias?: string): void; 18 | addRelationships? (resources: ICollection, type_alias: string): void; 19 | removeRelationship? (type_alias: string, id: string): boolean; 20 | addRelationshipsArray (resources: Array, type_alias?: string): void; 21 | save(params?: IParamsResource, fc_success?: Function, fc_error?: Function): ng.IPromise; 22 | toObject? (params?: IParamsResource): IDataObject; 23 | getService(): IService; 24 | } 25 | -------------------------------------------------------------------------------- /src/demo/books/books.service.ts: -------------------------------------------------------------------------------- 1 | import * as Jsonapi from '../../library/index'; 2 | 3 | export class BooksService extends Jsonapi.Service { 4 | type = 'books'; 5 | public schema: Jsonapi.ISchema = { 6 | attributes: { 7 | date_published: { }, 8 | title: { presence: true, length: { maximum: 96 } }, 9 | created_at: { }, 10 | updated_at: { } 11 | }, 12 | relationships: { 13 | author: { 14 | hasMany: false 15 | }, 16 | photos: { 17 | hasMany: true 18 | } 19 | }, 20 | ttl: 10 21 | }; 22 | 23 | // executed before get data from server 24 | public parseFromServer(attributes): void { 25 | attributes.title = '📖 ' + attributes.title; 26 | } 27 | 28 | // executed before send to server 29 | public parseToServer(attributes): void { 30 | if ('title' in attributes) { 31 | attributes.title = attributes.title.replace('📖 ', ''); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /gulp_tasks/misc.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const gulp = require('gulp'); 4 | const del = require('del'); 5 | const filter = require('gulp-filter'); 6 | 7 | const conf = require('../conf/gulp.conf'); 8 | // const webpackconf = require('../conf/webpack.conf'); 9 | 10 | gulp.task('clean:tmp', cleanTmp); 11 | gulp.task('clean:demo', gulp.parallel('clean:tmp', cleanDemo)); 12 | gulp.task('clean:library', gulp.parallel('clean:tmp', cleanLibrary)); 13 | gulp.task('other', other); 14 | 15 | function cleanTmp() { 16 | return del([conf.paths.tmp]); 17 | } 18 | function cleanDemo() { 19 | return del([conf.paths.dist, conf.paths.tmp]); 20 | } 21 | function cleanLibrary() { 22 | return del([conf.paths.distdemo, conf.paths.tmp]); 23 | } 24 | 25 | function other() { 26 | const fileFilter = filter(file => file.stat.isFile()); 27 | 28 | return gulp.src([ 29 | path.join(conf.paths.srcdist, '/**/*'), 30 | path.join(`!${conf.paths.srcdist}`, '/**/*.{html,ts,css,js,scss}') 31 | ]) 32 | .pipe(fileFilter) 33 | .pipe(gulp.dest(conf.paths.dist)); 34 | } 35 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["angular"], 3 | "env": { 4 | "es6": true, 5 | "browser": true, 6 | "jasmine": true 7 | }, 8 | "rules": { 9 | "block-scoped-var": 2, 10 | "no-eval": 2, 11 | "no-eq-null": 2, 12 | "no-case-declarations": 2, 13 | "no-alert": 1, 14 | "no-console": 1, 15 | "no-multi-spaces": 1, 16 | "no-multi-str": 1, 17 | "no-undef": 0, 18 | "indent": ["error", 2], 19 | "no-unused-vars": 1, 20 | "quotes": [2, "single"], 21 | 22 | "angular/on-watch": 0, 23 | 24 | "strict": "off", 25 | 26 | "no-shadow-restricted-names": 2, 27 | 28 | "array-bracket-spacing": 1, 29 | "camelcase": [1, {properties: "never"}], 30 | "comma-style": 1, 31 | "comma-spacing": 1 32 | }, 33 | "parserOptions": { 34 | "sourceType": "module", 35 | "ecmaFeatures": { 36 | "jsx": true, 37 | "experimentalObjectRestSpread": true 38 | } 39 | }, 40 | "globals": { 41 | "require": true, 42 | "angular": true, 43 | "module": true, 44 | "inject": true 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /conf/webpack-test.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | module: { 3 | // https://github.com/localForage/localForage#browserify-and-webpack 4 | noParse: /node_modules\/localforage\/dist\/localforage.js/, 5 | 6 | preLoaders: [ 7 | { 8 | test: /\.ts$/, 9 | exclude: /node_modules/, 10 | loader: 'tslint' 11 | } 12 | ], 13 | 14 | loaders: [ 15 | { 16 | test: /.json$/, 17 | loaders: [ 18 | 'json' 19 | ] 20 | }, 21 | { 22 | test: /\.ts$/, 23 | exclude: /node_modules/, 24 | loaders: [ 25 | 'ng-annotate', 26 | 'ts' 27 | ] 28 | } 29 | ] 30 | }, 31 | plugins: [], 32 | debug: true, 33 | devtool: 'cheap-module-eval-source-map', 34 | resolve: { 35 | extensions: [ 36 | '', 37 | '.webpack.js', 38 | '.web.js', 39 | '.js', 40 | '.ts' 41 | ] 42 | }, 43 | ts: { 44 | configFile: 'tsconfig.json' 45 | }, 46 | tslint: { 47 | configuration: require('../tslint.json') 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 reyesoft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const HubRegistry = require('gulp-hub'); 3 | const browserSync = require('browser-sync'); 4 | 5 | const conf = require('./conf/gulp.conf'); 6 | 7 | // Load some files into the registry 8 | const hub = new HubRegistry([conf.path.tasks('*.js')]); 9 | 10 | // Tell gulp to use the tasks just loaded 11 | gulp.registry(hub); 12 | 13 | gulp.task('build', gulp.parallel('build:demo', 'build:library')); 14 | gulp.task('build:library', gulp.series('clean:library', gulp.parallel('other', 'webpack:library', 'library:dts'))); 15 | gulp.task('build:demo', gulp.series('clean:demo', 'partials', gulp.parallel('other', 'webpack:demo'))); 16 | gulp.task('deploy', gulp.series('build', 'ghpages')); 17 | gulp.task('test', gulp.series('karma:single-run')); 18 | gulp.task('serve', gulp.series('webpack:watch', 'watch', 'browsersync')); 19 | gulp.task('default', gulp.series('build:library')); 20 | gulp.task('watch', watch); 21 | 22 | function reloadBrowserSync(cb) { 23 | browserSync.reload(); 24 | cb(); 25 | } 26 | 27 | function watch(done) { 28 | gulp.watch(conf.path.src('demo/**/*.html'), reloadBrowserSync); 29 | done(); 30 | } 31 | -------------------------------------------------------------------------------- /src/demo/routes.ts: -------------------------------------------------------------------------------- 1 | export default routesConfig; 2 | 3 | /** @ngInject */ 4 | function routesConfig( 5 | $stateProvider: angular.ui.IStateProvider, 6 | $urlRouterProvider: angular.ui.IUrlRouterProvider, 7 | $locationProvider: angular.ILocationProvider 8 | ) { 9 | // $locationProvider.html5Mode(true).hashPrefix('!'); 10 | $urlRouterProvider.otherwise('/authors'); 11 | 12 | $stateProvider 13 | .state('authors', { 14 | url: '/authors', 15 | template: '' 16 | }) 17 | .state('author', { 18 | url: '/authors/author/:authorId', 19 | template: '' 20 | }) 21 | .state('photos', { 22 | url: '/photos', 23 | template: '' 24 | }) 25 | .state('books', { 26 | url: '/books/:filter', 27 | template: '' 28 | }) 29 | .state('book', { 30 | url: '/books/book/:bookId', 31 | template: '' 32 | }) 33 | 34 | .state('noduplicatedcalltests', { 35 | url: '/noduplicatedcalltests', 36 | template: '' 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/demo/authors/authors.html: -------------------------------------------------------------------------------- 1 |
2 |

Authors

3 |

$length={{ $ctrl.authors.$length }}. $is_loading={{ $ctrl.authors.$is_loading }}. $source={{ $ctrl.authors.$source }}.

4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 22 |
IDNameDate of birthDate of dead
{{ author.id }} 16 | {{ author.attributes.name }} 17 | {{ author.attributes.date_of_birth | date }}{{ author.attributes.date_of_death | date }}
23 | 24 | 28 | 29 |
30 | -------------------------------------------------------------------------------- /src/library/index.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | import 'angular-localforage'; 3 | 4 | angular.module('Jsonapi.config', []) 5 | .constant('rsJsonapiConfig', { 6 | url: 'http://yourdomain/api/v1/', 7 | params_separator: '?', 8 | delay: 0, 9 | unify_concurrency: true, 10 | cache_prerequests: true, 11 | parameters: { 12 | page: { 13 | number: 'page[number]', 14 | limit: 'page[limit]' 15 | } 16 | } 17 | }); 18 | 19 | angular.module('Jsonapi.services', []); 20 | 21 | angular.module('rsJsonapi', [ 22 | 'LocalForageModule', 23 | 'Jsonapi.config', 24 | 'Jsonapi.services' 25 | ]); 26 | 27 | import { Core } from './core'; 28 | import { Service } from './service'; 29 | import { Resource } from './resource'; 30 | 31 | // just for bootstrap this library on demo. 32 | // On dist version, all is exported inside a Jsonapi module 33 | export { Core }; 34 | export { Resource }; 35 | export { Service }; 36 | 37 | export * from './interfaces'; 38 | import { IResource } from './interfaces'; 39 | import { IService } from './interfaces'; 40 | export { IResource }; 41 | export { IService }; 42 | -------------------------------------------------------------------------------- /src/demo/authors/authors.component.ts: -------------------------------------------------------------------------------- 1 | import * as Jsonapi from '../../library/index'; 2 | 3 | class AuthorsController implements ng.IController { 4 | public authors: Jsonapi.ICollection; 5 | 6 | /** @ngInject */ 7 | constructor( 8 | protected JsonapiCore: Jsonapi.ICore, 9 | protected AuthorsService: Jsonapi.IService 10 | ) { 11 | this.authors = AuthorsService.all( 12 | // { include: ['books', 'photos'] }, 13 | success => { 14 | console.log('success authors controll', this.authors); 15 | }, 16 | error => { 17 | console.log('error authors controll', error); 18 | } 19 | ); 20 | } 21 | 22 | public $onInit() { 23 | 24 | } 25 | 26 | public delete(author: Jsonapi.IResource) { 27 | console.log('eliminaremos (no soportado en este ejemplo)', author.toObject()); 28 | this.AuthorsService.delete( 29 | author.id, 30 | success => { 31 | console.log('deleted', success); 32 | } 33 | ); 34 | } 35 | } 36 | 37 | export class Authors { 38 | public templateUrl = 'authors/authors.html'; 39 | public controller = AuthorsController; 40 | } 41 | -------------------------------------------------------------------------------- /src/demo/books/book.component.ts: -------------------------------------------------------------------------------- 1 | import * as Jsonapi from '../../library/index'; 2 | 3 | export class BookController implements ng.IController { 4 | public book: any = null; 5 | 6 | /** @ngInject */ 7 | constructor( 8 | protected BooksService: Jsonapi.IService, 9 | protected $stateParams 10 | ) { 11 | this.book = BooksService.get( 12 | $stateParams.bookId, 13 | { include: ['author', 'photos'] }, 14 | success => { 15 | console.log('success book ', this.book); 16 | // console.log('success book object', this.book.toObject({ include: ['authors', 'photos'] })); 17 | // console.log('success book relationships', this.book.toObject({ include: ['authors', 'photos'] }).data.relationships); 18 | // console.log('success book included', this.book.toObject({ include: ['authors', 'photos'] }).included); 19 | }, 20 | error => { 21 | console.log('error books controll', error); 22 | } 23 | ); 24 | } 25 | 26 | public $onInit() { 27 | 28 | } 29 | } 30 | 31 | export class Book { 32 | public templateUrl = 'books/book.html'; 33 | public controller = BookController; 34 | } 35 | -------------------------------------------------------------------------------- /gulp_tasks/library-dts.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | 3 | // modification by pablorsk for especial dist 4 | const concat = require('gulp-concat'); 5 | var deleteLines = require('gulp-delete-lines'); 6 | var ts = require('gulp-typescript'); 7 | var addsrc = require('gulp-add-src'); 8 | var replace = require('gulp-replace'); 9 | var inject = require('gulp-inject-string'); 10 | var ts = require('gulp-typescript'); 11 | 12 | gulp.task('library:dts', done => { 13 | makeLibraryDTs(); 14 | done(); 15 | }); 16 | 17 | function makeLibraryDTs() { 18 | var tsProjectDts = ts.createProject('conf/tsconfig.build.json'); 19 | var tsResult = gulp.src('src/library/**.ts') 20 | .pipe(tsProjectDts()); 21 | tsResult.dts 22 | .pipe(deleteLines({ 23 | 'filters': [/^\/\/\//i] 24 | })) 25 | .pipe(addsrc.prepend('src/library/interfaces/**.d.ts')) 26 | .pipe(concat('index.d.ts')) 27 | .pipe(deleteLines({ 28 | 'filters': [/^import/i] 29 | })) 30 | .pipe(deleteLines({ 31 | 'filters': [/^export [\*|\{]/i] 32 | })) 33 | 34 | .pipe(replace(/export declare class/g, 'export class')) // because all is on a "export module Jsonapi" 35 | .pipe(inject.wrap('export module Jsonapi { \n', '}')) 36 | 37 | .pipe(gulp.dest('dist')); 38 | } 39 | -------------------------------------------------------------------------------- /src/demo/books/books.html: -------------------------------------------------------------------------------- 1 |
2 | 10 |
11 |

Books

12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 27 | 28 | 29 | 30 | 31 | 32 |
IDTitleDate PublishedAuthorPhotos
{{ book.id }} 25 | {{ book.attributes.title }} 26 | {{ book.attributes.date_published | date }}{{ book.relationships.author.data.attributes.name }} #{{ book.relationships.author.data.id }}{{ photo.id }} DELETE
33 |
34 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.7.x (planned) 2 | 3 | - Interfaces are in `ng.jsonapi` now 4 | - Resources can be extended for personalized classes 5 | 6 | # 0.6.x 7 | 8 | ## Localstorage cache 9 | 10 | ### Features 11 | 12 | - Save on localstore all data. When you request a resource or collection, first check memory. If its empty, read from store. If is empty, get the data from back-end. 13 | - HttpStorage deprecated: jsons were saved as sent by the server, now we save json with logic (saving ids and resources separately). 14 | - Service with `toServer()` and `fromServer()` functions. They execute before and after http request. Ideal for type conversions. 15 | - `JsonapiCore.duplicateResource(resouce, ...relationtypes)` return a duplication of resource. You can duplicate resources and, optionally, their relationships. (v0.6.16) 16 | - resource save() method return a promise. 17 | 18 | ### Fixes 19 | 20 | - Fix problem with `Possibly unhandled rejection: undefined` with new AngularJs. 21 | - Fix problem with double angularjs library require. 22 | 23 | ## No more declaration file .d.ts 24 | 25 | - typings and index.d.ts removed. We only use `import` 26 | 27 | # 0.5.x 28 | 29 | All data is merged on one single resource. If you request a request a single related resource, and on this request not include any another resource, related resources come from memory cache (if exists) 30 | -------------------------------------------------------------------------------- /src/demo/containers/app.ts: -------------------------------------------------------------------------------- 1 | class AppController implements ng.IController { 2 | /** @ngInject */ 3 | constructor( 4 | protected JsonapiCore, 5 | protected AuthorsService, 6 | protected BooksService, 7 | protected PhotosService, 8 | protected $scope 9 | ) { 10 | $scope.loading = false; 11 | 12 | console.log('injected JsonapiCore?', JsonapiCore); 13 | 14 | // bootstrap all services 15 | AuthorsService.register(); 16 | BooksService.register(); 17 | PhotosService.register(); 18 | 19 | JsonapiCore.loadingsStart = (): void => { 20 | this.$scope.loading = 'LOADING...'; 21 | }; 22 | JsonapiCore.loadingsDone = (): void => { 23 | this.$scope.loading = ''; 24 | }; 25 | JsonapiCore.loadingsOffline = (error): void => { 26 | this.$scope.loading = 'No connection!!!'; 27 | }; 28 | JsonapiCore.loadingsError = (error): void => { 29 | this.$scope.loading = 'No connection 2!!!'; 30 | }; 31 | } 32 | 33 | public $onInit() { 34 | 35 | } 36 | } 37 | 38 | export class App implements ng.IComponentOptions { 39 | public templateUrl: string; 40 | public controller: ng.Injectable = AppController; 41 | public transclude: boolean; 42 | 43 | constructor() { 44 | this.templateUrl = 'containers/app.html'; 45 | this.transclude = true; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/library/parent-resource-service.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | import { Base } from './services/base'; 3 | import { IResource, ICollection, IExecParams, IExecParamsProcessed } from './interfaces'; 4 | 5 | export class ParentResourceService { 6 | /* 7 | This method sort params for all(), get(), delete() and save() 8 | */ 9 | protected proccess_exec_params(exec_params: IExecParams): IExecParamsProcessed { 10 | // makes `params` optional 11 | if (angular.isFunction(exec_params.params)) { 12 | exec_params.fc_error = exec_params.fc_success; 13 | exec_params.fc_success = exec_params.params; 14 | exec_params.params = angular.extend({}, Base.Params); 15 | } else { 16 | if (angular.isUndefined(exec_params.params)) { 17 | exec_params.params = angular.extend({}, Base.Params); 18 | } else { 19 | exec_params.params = angular.extend({}, Base.Params, exec_params.params); 20 | } 21 | } 22 | 23 | exec_params.fc_success = angular.isFunction(exec_params.fc_success) ? exec_params.fc_success : function() { /* */ }; 24 | exec_params.fc_error = angular.isFunction(exec_params.fc_error) ? exec_params.fc_error : undefined; 25 | 26 | return exec_params; // @todo 27 | } 28 | 29 | protected runFc(some_fc, param) { 30 | return angular.isFunction(some_fc) ? some_fc(param) : angular.noop(); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/library/services/base.ts: -------------------------------------------------------------------------------- 1 | import { ISchema, ICollection, IParamsCollection, IParamsResource } from '../interfaces'; 2 | import { Page } from './page'; 3 | 4 | export class Base { 5 | static Params: IParamsCollection | IParamsResource = { 6 | id: '', 7 | include: [] 8 | }; 9 | 10 | static Schema: ISchema = { 11 | attributes: {}, 12 | relationships: {}, 13 | ttl: 0 14 | }; 15 | 16 | static newCollection(): ICollection { 17 | return Object.defineProperties({}, { 18 | $length: { 19 | get: function() { 20 | return Object.keys(this).length * 1; 21 | }, 22 | enumerable: false 23 | }, 24 | $toArray: { 25 | get: function() { 26 | return Object.keys(this).map((key) => { 27 | return this[key]; 28 | }); 29 | }, 30 | enumerable: false 31 | }, 32 | $is_loading: { value: false, enumerable: false, writable: true }, 33 | $source: { value: '', enumerable: false, writable: true }, 34 | $cache_last_update: { value: 0, enumerable: false, writable: true }, 35 | page: { value: new Page(), enumerable: false, writable: true } 36 | }); 37 | } 38 | 39 | static isObjectLive(ttl: number, last_update: number) { 40 | return (ttl >= 0 && Date.now() <= (last_update + ttl * 1000)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /conf/gulp.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * This file contains the variables used in other gulp files 5 | * which defines tasks 6 | * By design, we only put there very generic config values 7 | * which are used in several places to keep good readability 8 | * of the tasks 9 | */ 10 | 11 | const path = require('path'); 12 | const gutil = require('gulp-util'); 13 | 14 | exports.ngModule = 'app'; 15 | 16 | /** 17 | * The main paths of your project handle these with care 18 | */ 19 | exports.paths = { 20 | src: 'src/demo', 21 | srcdist: 'src/library', 22 | dist: 'dist', 23 | distdemo: 'dist-demo', 24 | tmp: '.tmp', 25 | e2e: 'e2e', 26 | tasks: 'gulp_tasks' 27 | }; 28 | 29 | /** 30 | * used on gulp dist 31 | */ 32 | exports.htmlmin = { 33 | ignoreCustomFragments: [/{{.*?}}/] 34 | }; 35 | 36 | exports.path = {}; 37 | for (const pathName in exports.paths) { 38 | if (Object.prototype.hasOwnProperty.call(exports.paths, pathName)) { 39 | exports.path[pathName] = function () { 40 | const pathValue = exports.paths[pathName]; 41 | const funcArgs = Array.prototype.slice.call(arguments); 42 | const joinArgs = [pathValue].concat(funcArgs); 43 | return path.join.apply(this, joinArgs); 44 | }; 45 | } 46 | } 47 | 48 | /** 49 | * Common implementation for an error handler of a Gulp plugin 50 | */ 51 | exports.errorHandler = function (title) { 52 | return function (err) { 53 | gutil.log(gutil.colors.red(`[${title}]`), err.toString()); 54 | this.emit('end'); 55 | }; 56 | }; 57 | -------------------------------------------------------------------------------- /conf/karma.conf.js: -------------------------------------------------------------------------------- 1 | const conf = require('./gulp.conf'); 2 | 3 | module.exports = function (config) { 4 | const configuration = { 5 | basePath: '../', 6 | singleRun: true, 7 | autoWatch: false, 8 | logLevel: 'INFO', 9 | junitReporter: { 10 | outputDir: 'test-reports' 11 | }, 12 | browsers: [ 13 | 'PhantomJS' 14 | ], 15 | frameworks: [ 16 | 'jasmine', 17 | 'es6-shim' 18 | ], 19 | files: [ 20 | 'node_modules/es6-shim/es6-shim.js', 21 | conf.path.src('index.spec.js'), 22 | conf.path.src('**/*.html') 23 | ], 24 | preprocessors: { 25 | [conf.path.src('index.spec.js')]: [ 26 | 'webpack' 27 | ], 28 | [conf.path.src('**/*.html')]: [ 29 | 'ng-html2js' 30 | ] 31 | }, 32 | ngHtml2JsPreprocessor: { 33 | stripPrefix: `${conf.paths.src}/` 34 | }, 35 | reporters: ['progress', 'coverage'], 36 | coverageReporter: { 37 | type: 'html', 38 | dir: 'coverage/' 39 | }, 40 | webpack: require('./webpack-test.conf'), 41 | webpackMiddleware: { 42 | noInfo: true 43 | }, 44 | plugins: [ 45 | require('karma-jasmine'), 46 | require('karma-junit-reporter'), 47 | require('karma-coverage'), 48 | require('karma-phantomjs-launcher'), 49 | require('karma-phantomjs-shim'), 50 | require('karma-ng-html2js-preprocessor'), 51 | require('karma-webpack'), 52 | require('karma-es6-shim') 53 | ] 54 | }; 55 | 56 | config.set(configuration); 57 | }; 58 | -------------------------------------------------------------------------------- /conf/karma-auto.conf.js: -------------------------------------------------------------------------------- 1 | const conf = require('./gulp.conf'); 2 | 3 | module.exports = function (config) { 4 | const configuration = { 5 | basePath: '../', 6 | singleRun: false, 7 | autoWatch: true, 8 | logLevel: 'INFO', 9 | junitReporter: { 10 | outputDir: 'test-reports' 11 | }, 12 | browsers: [ 13 | 'PhantomJS' 14 | ], 15 | frameworks: [ 16 | 'jasmine', 17 | 'es6-shim' 18 | ], 19 | files: [ 20 | 'node_modules/es6-shim/es6-shim.js', 21 | conf.path.src('index.spec.js'), 22 | conf.path.src('**/*.html') 23 | ], 24 | preprocessors: { 25 | [conf.path.src('index.spec.js')]: [ 26 | 'webpack' 27 | ], 28 | [conf.path.src('**/*.html')]: [ 29 | 'ng-html2js' 30 | ] 31 | }, 32 | ngHtml2JsPreprocessor: { 33 | stripPrefix: `${conf.paths.src}/` 34 | }, 35 | reporters: ['progress', 'coverage'], 36 | coverageReporter: { 37 | type: 'html', 38 | dir: 'coverage/' 39 | }, 40 | webpack: require('./webpack-test.conf'), 41 | webpackMiddleware: { 42 | noInfo: true 43 | }, 44 | plugins: [ 45 | require('karma-jasmine'), 46 | require('karma-junit-reporter'), 47 | require('karma-coverage'), 48 | require('karma-phantomjs-launcher'), 49 | require('karma-phantomjs-shim'), 50 | require('karma-ng-html2js-preprocessor'), 51 | require('karma-webpack'), 52 | require('karma-es6-shim') 53 | ] 54 | }; 55 | 56 | config.set(configuration); 57 | }; 58 | -------------------------------------------------------------------------------- /src/library/services/resource-functions.ts: -------------------------------------------------------------------------------- 1 | import { IResource, ICollection } from '../interfaces'; 2 | 3 | export class ResourceFunctions { 4 | static resourceToResource(source: IResource, destination: IResource): void { 5 | destination.attributes = source.attributes; 6 | 7 | // remove relationships on destination resource 8 | for (let type_alias in destination.relationships) { 9 | if (!(type_alias in source.relationships)) { 10 | delete destination.relationships[type_alias]; 11 | } else { 12 | // this resource is a collection? 13 | if (!('id' in destination.relationships[type_alias].data)) { 14 | for (let id in destination.relationships[type_alias].data) { 15 | if (!(id in source.relationships[type_alias].data)) { 16 | delete destination.relationships[type_alias]; 17 | } 18 | } 19 | } 20 | } 21 | } 22 | 23 | // add source relationships to destination 24 | for (let type_alias in source.relationships) { 25 | if ('id' in source.relationships[type_alias].data) { 26 | destination.addRelationship( 27 | (source.relationships[type_alias].data), 28 | type_alias 29 | ); 30 | } else { 31 | destination.addRelationships(source.relationships[type_alias].data, type_alias); 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /gulp_tasks/webpack.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const gutil = require('gulp-util'); 3 | 4 | const webpack = require('webpack'); 5 | const webpackConf = require('../conf/webpack.conf'); 6 | const webpackDemoConf = require('../conf/webpack-demo.conf'); 7 | const webpackLibraryConf = require('../conf/webpack-library.conf'); 8 | const gulpConf = require('../conf/gulp.conf'); 9 | const browsersync = require('browser-sync'); 10 | 11 | // modification by pablorsk for especial dist 12 | var clean = require('gulp-clean'); 13 | 14 | gulp.task('webpack:watch', done => { 15 | webpackWrapper(true, webpackConf, done); 16 | }); 17 | 18 | gulp.task('webpack:demo', done => { 19 | webpackWrapper(false, webpackDemoConf, done); 20 | }); 21 | 22 | gulp.task('webpack:library', done => { 23 | webpackWrapper(false, webpackLibraryConf, done); 24 | }); 25 | 26 | function webpackWrapper(watch, conf, done) { 27 | const webpackBundler = webpack(conf); 28 | 29 | const webpackChangeHandler = (err, stats) => { 30 | if (err) { 31 | gulpConf.errorHandler('Webpack')(err); 32 | } 33 | gutil.log(stats.toString({ 34 | colors: true, 35 | chunks: false, 36 | hash: false, 37 | version: false 38 | })); 39 | if (done) { 40 | done(); 41 | done = null; 42 | } else { 43 | browsersync.reload(); 44 | } 45 | }; 46 | 47 | if (watch) { 48 | webpackBundler.watch(200, webpackChangeHandler); 49 | } else { 50 | 51 | // clear folder 52 | gulp.src('dist/*', {read: false}) 53 | .pipe(clean()); 54 | 55 | webpackBundler.run(webpackChangeHandler); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/library/services/localfilter.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | import * as Jsonapi from '../interfaces'; 3 | 4 | export class LocalFilter { 5 | private localfilterparams; 6 | 7 | /** @ngInject */ 8 | public constructor( 9 | localfilter: object 10 | ) { 11 | this.localfilterparams = localfilter || {}; 12 | } 13 | 14 | private passFilter(resource: Jsonapi.IResource, localfilter): boolean { 15 | for (let attribute in localfilter) { 16 | if (typeof resource !== 'object' || !('attributes' in resource)) { 17 | // is not a resource. Is an internal property, for example $source 18 | return true; 19 | } else if (typeof localfilter[attribute] === 'object') { 20 | // its a regular expression 21 | return localfilter[attribute].test(resource.attributes[attribute]); 22 | } else if (typeof resource.attributes[attribute] === 'string') { 23 | // just a string 24 | return (resource.attributes[attribute] === localfilter[attribute]); 25 | } 26 | } 27 | 28 | return false; 29 | } 30 | 31 | public filterCollection(source_collection: Jsonapi.ICollection, dest_collection: Jsonapi.ICollection) { 32 | if (Object.keys(this.localfilterparams).length) { 33 | angular.forEach(source_collection, (resource, key) => { 34 | if (this.passFilter(resource, this.localfilterparams)) { 35 | dest_collection[key] = resource; 36 | } 37 | }); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/library/services/path-builder.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | import { IService, IParamsCollection, IParamsResource } from '../interfaces'; 3 | import { Core } from '../core'; 4 | 5 | export class PathBuilder { 6 | public paths: Array = []; 7 | public includes: Array = []; 8 | private get_params: Array = []; 9 | 10 | public applyParams(service: IService, params: IParamsResource | IParamsCollection = {}) { 11 | this.appendPath(service.getPrePath()); 12 | if (params.beforepath) { 13 | this.appendPath(params.beforepath); 14 | } 15 | this.appendPath(service.getPath()); 16 | if (params.include) { 17 | this.setInclude(params.include); 18 | } 19 | } 20 | 21 | public appendPath(value: string) { 22 | if (value !== '') { 23 | this.paths.push(value); 24 | } 25 | } 26 | 27 | public addParam(param: string): void { 28 | this.get_params.push(param); 29 | } 30 | 31 | private setInclude(strings_array: Array) { 32 | this.includes = strings_array; 33 | } 34 | 35 | public getForCache(): string { 36 | return this.paths.join('/') + this.get_params.join('/'); 37 | } 38 | 39 | public get(): string { 40 | let params = []; 41 | angular.copy(this.get_params, params); 42 | 43 | if (this.includes.length > 0) { 44 | params.push('include=' + this.includes.join(',')); 45 | } 46 | 47 | return this.paths.join('/') + 48 | (params.length > 0 ? Core.injectedServices.rsJsonapiConfig.params_separator + params.join('&') : ''); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/demo/index.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | import 'angular-ui-router'; 3 | import 'bootstrap/dist/css/bootstrap.css'; 4 | import './index.scss'; 5 | import routesConfig from './routes'; 6 | 7 | // Jsonapi 8 | import '../library/index'; 9 | let rsJsonapiConfig = ['rsJsonapiConfig', (rsJsonapiConfigParam): void => { 10 | angular.extend(rsJsonapiConfigParam, { 11 | // url: 'http://laravel-jsonapi.dev/v2/', 12 | url: '//jsonapiplayground.reyesoft.com/v2/', 13 | delay: 800 14 | }); 15 | }]; 16 | 17 | import { App } from './containers/app'; 18 | import { Author } from './authors/author.component'; 19 | import { Authors } from './authors/authors.component'; 20 | import { AuthorsService } from './authors/authors.service'; 21 | import { Book } from './books/book.component'; 22 | import { Books } from './books/books.component'; 23 | import { BooksService } from './books/books.service'; 24 | import { Photos } from './photos/photos.component'; 25 | import { PhotosService } from './photos/photos.service'; 26 | import { NoDuplicatedHttpCalls } from './tests/noduplicatedhttpcalls.component'; 27 | 28 | angular 29 | .module('app', ['ui.router', 'rsJsonapi']) 30 | .config(routesConfig) 31 | .config(rsJsonapiConfig) 32 | .service('AuthorsService', AuthorsService) 33 | .service('BooksService', BooksService) 34 | .service('PhotosService', PhotosService) 35 | .component('app', new App()) 36 | .component('author', new Author()) 37 | .component('authors', new Authors()) 38 | .component('book', new Book()) 39 | .component('books', new Books()) 40 | .component('photos', new Photos()) 41 | .component('noduplicatedcalltests', new NoDuplicatedHttpCalls()) 42 | ; 43 | -------------------------------------------------------------------------------- /src/library/services/noduplicatedhttpcalls.service.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | 3 | export class NoDuplicatedHttpCallsService { 4 | public calls = {}; 5 | 6 | /** @ngInject */ 7 | public constructor( 8 | protected $q 9 | ) { 10 | 11 | } 12 | 13 | protected hasPromises(path: string) { 14 | return (path in this.calls); 15 | } 16 | 17 | protected getAPromise(path: string) { 18 | if (!(path in this.calls)) { 19 | this.calls[path] = []; 20 | } 21 | 22 | let deferred = this.$q.defer(); 23 | this.calls[path].push(deferred); 24 | 25 | return deferred.promise; 26 | } 27 | 28 | protected setPromiseRequest(path, promise) { 29 | promise.then( 30 | success => { 31 | if (path in this.calls) { 32 | for (let deferred of this.calls[path]) { 33 | deferred.resolve(success); 34 | } 35 | delete this.calls[path]; 36 | } 37 | } 38 | ).catch( 39 | error => { 40 | if (path in this.calls) { 41 | for (let deferred of this.calls[path]) { 42 | deferred.reject(error); 43 | } 44 | delete this.calls[path]; 45 | } 46 | } 47 | ); 48 | } 49 | // 50 | // protected resolve(path: string, success) { 51 | // if (path in this.calls) { 52 | // for (let deferred of this.calls[path]) { 53 | // deferred.resolve(success); 54 | // } 55 | // delete this.calls[path]; 56 | // } 57 | // } 58 | } 59 | angular.module('Jsonapi.services').service('noDuplicatedHttpCallsService', NoDuplicatedHttpCallsService); 60 | -------------------------------------------------------------------------------- /conf/webpack-library.conf.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const conf = require('./gulp.conf'); 3 | const path = require('path'); 4 | 5 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 6 | 7 | module.exports = { 8 | module: { 9 | // https://github.com/localForage/localForage#browserify-and-webpack 10 | noParse: /node_modules\/localforage\/dist\/localforage.js/, 11 | 12 | loaders: [ 13 | { 14 | test: /\.json$/, 15 | loaders: [ 16 | 'json-loader' 17 | ] 18 | }, 19 | { 20 | test: /\.ts$/, 21 | exclude: /node_modules/, 22 | loader: 'tslint-loader', 23 | enforce: 'pre' 24 | }, 25 | { 26 | test: /\.(css|scss)$/, 27 | loaders: ExtractTextPlugin.extract({ 28 | fallback: 'style-loader', 29 | use: 'css-loader?minimize!sass-loader!postcss-loader' 30 | }) 31 | }, 32 | { 33 | test: /\.ts$/, 34 | exclude: /node_modules/, 35 | loaders: [ 36 | 'ng-annotate-loader', 37 | 'ts-loader' 38 | ] 39 | }, 40 | { 41 | test: /\.html$/, 42 | loaders: [ 43 | 'html-loader' 44 | ] 45 | } 46 | ] 47 | }, 48 | plugins: [ 49 | new webpack.optimize.OccurrenceOrderPlugin(), 50 | new webpack.NoEmitOnErrorsPlugin() 51 | // new webpack.optimize.UglifyJsPlugin({ 52 | // compress: {unused: true, dead_code: true} // eslint-disable-line camelcase 53 | // }) 54 | ], 55 | bail: true, 56 | output: { 57 | // https://webpack.github.io/docs/library-and-externals.html 58 | path: path.join(process.cwd(), conf.paths.dist), 59 | library: 'Jsonapi', 60 | libraryTarget: 'commonjs', 61 | filename: 'ts-angular-jsonapi.js' 62 | }, 63 | externals: { 64 | 'angular': 'angular' 65 | }, 66 | resolve: { 67 | extensions: [ 68 | '.webpack.js', 69 | '.web.js', 70 | '.js', 71 | '.ts' 72 | ] 73 | }, 74 | entry: `./${conf.path.srcdist('index.ts')}` 75 | }; 76 | -------------------------------------------------------------------------------- /conf/webpack.conf.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const conf = require('./gulp.conf'); 3 | const path = require('path'); 4 | 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | const FailPlugin = require('webpack-fail-plugin'); 7 | const autoprefixer = require('autoprefixer'); 8 | 9 | module.exports = { 10 | module: { 11 | // https://github.com/localForage/localForage#browserify-and-webpack 12 | noParse: /node_modules\/localforage\/dist\/localforage.js/, 13 | 14 | loaders: [ 15 | { 16 | test: /\.json$/, 17 | loaders: [ 18 | 'json-loader' 19 | ] 20 | }, 21 | { 22 | test: /\.ts$/, 23 | exclude: /node_modules/, 24 | loader: 'tslint-loader', 25 | enforce: 'pre' 26 | }, 27 | { 28 | test: /\.(css|scss)$/, 29 | loaders: [ 30 | 'style-loader', 31 | 'css-loader', 32 | 'sass-loader', 33 | 'postcss-loader' 34 | ] 35 | }, 36 | { 37 | test: /\.(eot|woff|woff2|svg|ttf)([\?]?.*)$/, loader: 'file-loader' 38 | }, 39 | { 40 | test: /\.ts$/, 41 | exclude: /node_modules/, 42 | loaders: [ 43 | 'ng-annotate-loader', 44 | 'ts-loader' 45 | ] 46 | }, 47 | { 48 | test: /\.html$/, 49 | loaders: [ 50 | 'html-loader' 51 | ] 52 | } 53 | ] 54 | }, 55 | plugins: [ 56 | new webpack.optimize.OccurrenceOrderPlugin(), 57 | new webpack.NoEmitOnErrorsPlugin(), 58 | FailPlugin, 59 | new HtmlWebpackPlugin({ 60 | template: conf.path.src('index.html') 61 | }), 62 | new webpack.LoaderOptionsPlugin({ 63 | options: { 64 | postcss: () => [autoprefixer], 65 | resolve: {}, 66 | ts: { 67 | configFile: 'tsconfig.json' 68 | }, 69 | tslint: { 70 | configuration: require('../tslint.json') 71 | } 72 | }, 73 | debug: true 74 | }) 75 | ], 76 | devtool: 'source-map', 77 | output: { 78 | path: path.join(process.cwd(), conf.paths.tmp), 79 | filename: 'index.js' 80 | }, 81 | resolve: { 82 | extensions: [ 83 | '.webpack.js', 84 | '.web.js', 85 | '.js', 86 | '.ts' 87 | ] 88 | }, 89 | entry: `./${conf.path.src('index')}` 90 | }; 91 | -------------------------------------------------------------------------------- /src/demo/authors/author.html: -------------------------------------------------------------------------------- 1 |
2 |

Author, with

3 |
authors.get('{{$ctrl.author.id}}', { include: ['books', 'photos'] });
4 |
    5 |
  • Name: {{ $ctrl.author.attributes.name }}
  • 6 |
  • Date of birth: {{ $ctrl.author.attributes.date_of_birth | date }}
  • 7 |
  • Date of dead: {{ $ctrl.author.attributes.date_of_death | date }}
  • 8 |
9 |

10 | 11 | 12 | 13 |

14 | 15 |

Photos

16 | 20 | 21 |

Books

22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 35 | 36 | 37 |
IDTitleDate Published
{{ book.id }} 33 | {{ book.attributes.title }} 34 | {{ book.attributes.date_published | date }}
38 | 39 |

Related Books by URL

40 |
BooksService.all( { beforepath: 'authors/{{$ctrl.author.id}}' } );
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 54 | 55 | 56 |
IDTitleDate Published
{{ book.id }} 52 | {{ book.attributes.title }} 53 | {{ book.attributes.date_published | date }}
57 | 58 |

59 | Volver 60 |

61 |
62 | -------------------------------------------------------------------------------- /src/demo/authors/author.component.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | import 'angular-ui-router'; 3 | import * as Jsonapi from '../../library/index'; 4 | 5 | class AuthorController implements ng.IController { 6 | public author: Jsonapi.IResource; 7 | public relatedbooks: Array; 8 | 9 | /** @ngInject */ 10 | constructor( 11 | protected AuthorsService: Jsonapi.IService, 12 | protected BooksService: Jsonapi.IService, 13 | protected $stateParams 14 | ) { 15 | this.author = AuthorsService.get( 16 | $stateParams.authorId, 17 | { include: ['books', 'photos'] }, 18 | success => { 19 | console.info('success authors controller', success); 20 | }, 21 | error => { 22 | console.error('error authors controller', error); 23 | } 24 | ); 25 | 26 | this.relatedbooks = BooksService.all( 27 | { beforepath: 'authors/' + $stateParams.authorId }, 28 | () => { 29 | console.info('Books from authors relationship', this.relatedbooks); 30 | } 31 | ); 32 | } 33 | 34 | public $onInit() { 35 | 36 | } 37 | 38 | /* 39 | Add a new author 40 | */ 41 | public new() { 42 | let author = this.AuthorsService.new(); 43 | author.attributes.name = 'Pablo Reyes'; 44 | author.attributes.date_of_birth = '2030-12-10'; 45 | angular.forEach(this.relatedbooks, (book: Jsonapi.IResource) => { 46 | author.addRelationship(book /* , 'handbook' */); 47 | }); 48 | console.log('new save', author.toObject()); 49 | // author.save( /* { include: ['book'] } */ ); 50 | } 51 | 52 | /* 53 | Update name for actual author 54 | */ 55 | public update() { 56 | this.author.attributes.name += 'o'; 57 | this.author.save( 58 | // { include: ['books'] } 59 | ); 60 | console.log('update save with book include', this.author.toObject({ include: ['books'] })); 61 | console.log('update save without any include', this.author.toObject()); 62 | } 63 | 64 | public removeRelationship() { 65 | this.author.removeRelationship('photos', '1'); 66 | this.author.save(); 67 | console.log('removeRelationship save with photos include', this.author.toObject()); 68 | } 69 | } 70 | 71 | export class Author { 72 | templateUrl = 'authors/author.html'; 73 | controller = AuthorController; 74 | } 75 | -------------------------------------------------------------------------------- /src/demo/books/books.component.ts: -------------------------------------------------------------------------------- 1 | import * as Jsonapi from '../../library/index'; 2 | 3 | class BooksController implements ng.IController { 4 | public books: Jsonapi.ICollection; 5 | 6 | /** @ngInject */ 7 | constructor( 8 | protected BooksService: Jsonapi.IService, 9 | protected $stateParams 10 | ) { 11 | this.getAll({}); 12 | } 13 | 14 | public $onInit() { 15 | 16 | } 17 | 18 | public getAll(remotefilter) { 19 | 20 | // we add some remote filter 21 | remotefilter.date_published = { 22 | since: '1983-01-01', 23 | until: '2010-01-01' 24 | }; 25 | 26 | this.books = this.BooksService.all( 27 | { 28 | localfilter: { 29 | // name: 'Some name' 30 | }, 31 | remotefilter: remotefilter, 32 | page: { number: 1 }, 33 | include: ['author', 'photos'] 34 | }, 35 | success => { 36 | console.log('success books controller', success, this.books); 37 | 38 | /*** YOU CAN REMOVE THE NEXT TEST LINES **/ 39 | 40 | // TEST 1 41 | // this test merge data with cache (this not include author or photos) 42 | console.log('BooksRequest#1 received (author data from server)', 43 | (this.books[Object.keys(this.books)[2]].relationships.author.data).attributes 44 | ); 45 | 46 | console.log('BooksRequest#2 requested'); 47 | let books2 = this.BooksService.all( 48 | success2 => { 49 | console.log('BooksRequest#2 received (author data from cache)', 50 | (books2[Object.keys(this.books)[1]].relationships.author.data) 51 | ); 52 | } 53 | ); 54 | 55 | // TEST 2 56 | console.log('BookRequest#3 requested'); 57 | let book1 = this.BooksService.get(1, 58 | success1 => { 59 | console.log('BookRequest#3 received (author data from cache)', 60 | (book1.relationships.author.data).attributes 61 | ); 62 | }); 63 | }, 64 | error => { 65 | console.log('error books controller', error); 66 | } 67 | ); 68 | } 69 | 70 | public delete(book: Jsonapi.IResource) { 71 | this.BooksService.delete(book.id); 72 | } 73 | } 74 | 75 | export class Books { 76 | public templateUrl = 'books/books.html'; 77 | public controller = BooksController; 78 | } 79 | -------------------------------------------------------------------------------- /conf/webpack-demo.conf.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const conf = require('./gulp.conf'); 3 | const path = require('path'); 4 | 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 7 | const pkg = require('../package.json'); 8 | const autoprefixer = require('autoprefixer'); 9 | 10 | module.exports = { 11 | module: { 12 | // https://github.com/localForage/localForage#browserify-and-webpack 13 | noParse: /node_modules\/localforage\/dist\/localforage.js/, 14 | 15 | loaders: [ 16 | { 17 | test: /\.json$/, 18 | loaders: [ 19 | 'json-loader' 20 | ] 21 | }, 22 | { 23 | test: /\.ts$/, 24 | exclude: /node_modules/, 25 | loader: 'tslint-loader', 26 | enforce: 'pre' 27 | }, 28 | { 29 | test: /\.(css|scss)$/, 30 | loaders: ExtractTextPlugin.extract({ 31 | fallback: 'style-loader', 32 | use: 'css-loader?minimize!sass-loader!postcss-loader' 33 | }) 34 | }, 35 | { 36 | test: /\.(eot|woff|woff2|svg|ttf|png|jpg|jpeg)([\?]?.*)$/, loader: 'file-loader' 37 | }, 38 | { 39 | test: /\.ts$/, 40 | exclude: /node_modules/, 41 | loaders: [ 42 | 'ng-annotate-loader', 43 | 'ts-loader' 44 | ] 45 | }, 46 | { 47 | test: /\.html$/, 48 | loaders: [ 49 | 'html-loader' 50 | ] 51 | } 52 | ] 53 | }, 54 | plugins: [ 55 | new webpack.optimize.OccurrenceOrderPlugin(), 56 | new webpack.NoEmitOnErrorsPlugin(), 57 | // new webpack.ProvidePlugin({ 58 | // "window.jQuery": "jquery" 59 | // }), 60 | new HtmlWebpackPlugin({ 61 | template: conf.path.src('index.html') 62 | }), 63 | // new webpack.optimize.UglifyJsPlugin({ 64 | // output: {comments: false}, 65 | // compress: {unused: true, dead_code: true} // eslint-disable-line camelcase 66 | // }), 67 | new ExtractTextPlugin('index-[contenthash].css'), 68 | new webpack.optimize.CommonsChunkPlugin({name: 'vendor'}), 69 | new webpack.LoaderOptionsPlugin({ 70 | options: { 71 | postcss: () => [autoprefixer], 72 | resolve: {}, 73 | ts: { 74 | configFile: 'tsconfig.json' 75 | }, 76 | tslint: { 77 | configuration: require('../tslint.json') 78 | } 79 | } 80 | }) 81 | ], 82 | output: { 83 | path: path.join(process.cwd(), conf.paths.distdemo), 84 | filename: '[name]-[hash].js' 85 | }, 86 | resolve: { 87 | extensions: [ 88 | '.webpack.js', 89 | '.web.js', 90 | '.js', 91 | '.ts' 92 | ] 93 | }, 94 | entry: { 95 | app: [ 96 | `./${conf.path.src('index')}`, 97 | `./${conf.path.tmp('templateCacheHtml.ts')}` 98 | ], 99 | vendor: Object.keys(pkg.peerDependencies).concat(Object.keys(pkg.dependencies)) 100 | // vendor: Object.keys(pkg.dependencies) 101 | } 102 | }; 103 | -------------------------------------------------------------------------------- /src/library/sources/http.service.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | import { IDataObject } from '../interfaces/data-object'; 3 | import '../services/noduplicatedhttpcalls.service'; 4 | import { Core } from '../core'; 5 | 6 | export class Http { 7 | 8 | /** @ngInject */ 9 | public constructor( 10 | protected $http: ng.IHttpService, 11 | protected $timeout, 12 | protected rsJsonapiConfig, 13 | protected noDuplicatedHttpCallsService, 14 | protected $q 15 | ) { 16 | 17 | } 18 | 19 | public delete(path: string): ng.IPromise { 20 | return this.exec(path, 'DELETE'); 21 | } 22 | 23 | public get(path: string): ng.IPromise { 24 | return this.exec(path, 'get'); 25 | } 26 | 27 | protected exec(path: string, method: string, data?: IDataObject, call_loadings_error: boolean = true): ng.IPromise { 28 | 29 | let fakeHttpPromise = null; 30 | 31 | // http request (if we don't have any GET request yet) 32 | if (method !== 'get' || !this.noDuplicatedHttpCallsService.hasPromises(path)) { 33 | let req: ng.IRequestConfig = { 34 | method: method, 35 | url: this.rsJsonapiConfig.url + path, 36 | headers: { 37 | 'Content-Type': 'application/vnd.api+json' 38 | } 39 | }; 40 | if (data) { 41 | req.data = data; 42 | } 43 | let http_promise = this.$http(req); 44 | 45 | if (method === 'get') { 46 | this.noDuplicatedHttpCallsService.setPromiseRequest(path, http_promise); 47 | } else { 48 | fakeHttpPromise = http_promise; 49 | } 50 | } 51 | if (method === 'get') { 52 | fakeHttpPromise = this.noDuplicatedHttpCallsService.getAPromise(path); 53 | } 54 | 55 | let deferred = this.$q.defer(); 56 | Core.me.refreshLoadings(1); 57 | fakeHttpPromise.then( 58 | success => { 59 | // timeout just for develop environment 60 | this.$timeout( () => { 61 | Core.me.refreshLoadings(-1); 62 | deferred.resolve(success); 63 | }, this.rsJsonapiConfig.delay); 64 | } 65 | ).catch( 66 | error => { 67 | Core.me.refreshLoadings(-1); 68 | if (error.status <= 0) { 69 | // offline? 70 | if (!Core.me.loadingsOffline(error)) { 71 | console.warn('Jsonapi.Http.exec (use JsonapiCore.loadingsOffline for catch it) error =>', error); 72 | } 73 | } else { 74 | if (call_loadings_error && !Core.me.loadingsError(error)) { 75 | console.warn('Jsonapi.Http.exec (use JsonapiCore.loadingsError for catch it) error =>', error); 76 | } 77 | } 78 | deferred.reject(error); 79 | } 80 | ); 81 | 82 | return deferred.promise; 83 | } 84 | } 85 | angular.module('Jsonapi.services').service('JsonapiHttp', Http); 86 | -------------------------------------------------------------------------------- /src/library/core.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | import './services/core-services.service'; 3 | import { ICore, IResource, ICollection, IService } from './interfaces'; 4 | 5 | export class Core implements ICore { 6 | public static me: ICore; 7 | public static injectedServices: { 8 | $q: ng.IQService; 9 | JsonapiStoreService: any; 10 | JsonapiHttp: any; 11 | rsJsonapiConfig: any; 12 | }; 13 | 14 | private resourceServices: Object = {}; 15 | public loadingsCounter: number = 0; 16 | public loadingsStart: Function = (): void => {}; 17 | public loadingsDone: Function = (): void => {}; 18 | public loadingsError: Function = (): void => {}; 19 | public loadingsOffline = (): void => {}; 20 | 21 | /** @ngInject */ 22 | public constructor( 23 | protected rsJsonapiConfig, 24 | protected JsonapiCoreServices 25 | ) { 26 | Core.me = this; 27 | Core.injectedServices = JsonapiCoreServices; 28 | } 29 | 30 | public _register(clase: IService): boolean { 31 | if (clase.type in this.resourceServices) { 32 | return false; 33 | } 34 | this.resourceServices[clase.type] = clase; 35 | 36 | return true; 37 | } 38 | 39 | public getResourceService(type: string): IService { 40 | return this.resourceServices[type]; 41 | } 42 | 43 | public refreshLoadings(factor: number): void { 44 | this.loadingsCounter += factor; 45 | if (this.loadingsCounter === 0) { 46 | this.loadingsDone(); 47 | } else if (this.loadingsCounter === 1) { 48 | this.loadingsStart(); 49 | } 50 | } 51 | 52 | public clearCache(): boolean { 53 | Core.injectedServices.JsonapiStoreService.clearCache(); 54 | 55 | return true; 56 | } 57 | 58 | // just an helper 59 | public duplicateResource(resource: IResource, ...relations_alias_to_duplicate_too: Array): IResource { 60 | let newresource = this.getResourceService(resource.type).new(); 61 | angular.merge(newresource.attributes, resource.attributes); 62 | newresource.attributes.name = newresource.attributes.name + ' xXx'; 63 | angular.forEach(resource.relationships, (relationship, alias) => { 64 | if ('id' in relationship.data) { 65 | // relation hasOne 66 | if (relations_alias_to_duplicate_too.indexOf(alias) > -1) { 67 | newresource.addRelationship(this.duplicateResource(relationship.data), alias); 68 | } else { 69 | newresource.addRelationship(relationship.data, alias); 70 | } 71 | } else { 72 | // relation hasMany 73 | if (relations_alias_to_duplicate_too.indexOf(alias) > -1) { 74 | angular.forEach(relationship.data, relationresource => { 75 | newresource.addRelationship(this.duplicateResource(relationresource), alias); 76 | }); 77 | } else { 78 | newresource.addRelationships(relationship.data, alias); 79 | } 80 | } 81 | }); 82 | 83 | return newresource; 84 | } 85 | } 86 | 87 | angular.module('Jsonapi.services').service('JsonapiCore', Core); 88 | -------------------------------------------------------------------------------- /src/library/services/cachememory.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | import { ICollection, IResource } from '../interfaces'; 3 | import { ICacheMemory } from '../interfaces/cachememory.d'; 4 | import { Base } from './base'; 5 | import { Converter } from './converter'; 6 | import { ResourceFunctions } from './resource-functions'; 7 | 8 | export class CacheMemory implements ICacheMemory { 9 | private collections: { [url: string]: ICollection } = {}; 10 | private collections_lastupdate: { [url: string]: number } = {}; 11 | public resources: { [id: string]: IResource } = {}; 12 | 13 | public isCollectionExist(url: string): boolean { 14 | return (url in this.collections && this.collections[url].$source !== 'new' ? true : false); 15 | } 16 | 17 | public isCollectionLive(url: string, ttl: number): boolean { 18 | return (Date.now() <= (this.collections_lastupdate[url] + ttl * 1000)); 19 | } 20 | 21 | public isResourceLive(id: string, ttl: number): boolean { 22 | return this.resources[id] && (Date.now() <= (this.resources[id].lastupdate + ttl * 1000)); 23 | } 24 | 25 | public getOrCreateCollection(url: string): ICollection { 26 | if (!(url in this.collections)) { 27 | this.collections[url] = Base.newCollection(); 28 | this.collections[url].$source = 'new'; 29 | } 30 | 31 | return this.collections[url]; 32 | } 33 | 34 | public setCollection(url: string, collection: ICollection): void { 35 | // clone collection, because after maybe delete items for localfilter o pagination 36 | this.collections[url] = Base.newCollection(); 37 | angular.forEach(collection, (resource: IResource, resource_id: string) => { 38 | this.collections[url][resource_id] = resource; 39 | this.setResource(resource); 40 | }); 41 | this.collections[url].page = collection.page; 42 | this.collections_lastupdate[url] = Date.now(); 43 | } 44 | 45 | public getOrCreateResource(type: string, id: string): IResource { 46 | if (Converter.getService(type).cachememory && id in Converter.getService(type).cachememory.resources) { 47 | return Converter.getService(type).cachememory.resources[id]; 48 | } else { 49 | let resource = Converter.getService(type).new(); 50 | resource.id = id; 51 | // needed for a lot of request (all and get, tested on multinexo.com) 52 | this.setResource(resource, false); 53 | 54 | return resource; 55 | } 56 | } 57 | 58 | public setResource(resource: IResource, update_lastupdate = false): void { 59 | // we cannot redefine object, because view don't update. 60 | if (resource.id in this.resources) { 61 | ResourceFunctions.resourceToResource(resource, this.resources[resource.id]); 62 | } else { 63 | this.resources[resource.id] = resource; 64 | } 65 | this.resources[resource.id].lastupdate = (update_lastupdate ? Date.now() : 0); 66 | } 67 | 68 | public deprecateCollections(path_start_with: string): boolean { 69 | angular.forEach(this.collections_lastupdate, (lastupdate: number, key: string) => { 70 | // we don't need check, memorycache has one collection per resoure type 71 | // if (key.startsWith(path_start_with) === true) { 72 | this.collections_lastupdate[key] = 0; 73 | // } 74 | }); 75 | 76 | return true; 77 | } 78 | 79 | public removeResource(id: string): void { 80 | angular.forEach(this.collections, (value, url) => { 81 | delete value[id]; 82 | }); 83 | this.resources[id].attributes = {}; // just for confirm deletion on view 84 | this.resources[id].relationships = {}; // just for confirm deletion on view 85 | delete this.resources[id]; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/library/sources/store.service.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | import { IStoreObject } from '../interfaces'; 3 | 4 | // @types/angular-forage is outdated, we need add new parameter documented here 5 | // https://github.com/ocombe/angular-localForage#functions- 6 | interface ILocalForageServiceUpdated extends ng.localForage.ILocalForageService { 7 | getItem(key: string, rejectIfNull?: boolean): angular.IPromise; 8 | getItem(keys: Array, rejectIfNull?: boolean): angular.IPromise>; 9 | } 10 | 11 | export class StoreService { 12 | private globalstore: ILocalForageServiceUpdated; 13 | private allstore: ILocalForageServiceUpdated; 14 | 15 | /** @ngInject */ 16 | public constructor( 17 | protected $localForage, 18 | protected $q 19 | ) { 20 | this.globalstore = $localForage.createInstance({ name: 'jsonapiglobal' }); 21 | this.allstore = $localForage.createInstance({ name: 'allstore' }); 22 | this.checkIfIsTimeToClean(); 23 | } 24 | 25 | private checkIfIsTimeToClean() { 26 | // check if is time to check cachestore 27 | this.globalstore.getItem('_lastclean_time', true).then(success => { 28 | if (Date.now() >= (success.time + 12 * 3600 * 1000)) { 29 | // is time to check cachestore! 30 | this.globalstore.setItem('_lastclean_time', { time: Date.now() }); 31 | this.checkAndDeleteOldElements(); 32 | } 33 | }) 34 | .catch(() => { 35 | this.globalstore.setItem('_lastclean_time', { time: Date.now() }); 36 | }); 37 | } 38 | 39 | private checkAndDeleteOldElements() { 40 | this.allstore.keys().then(success => { 41 | angular.forEach(success, (key) => { 42 | // recorremos cada item y vemos si es tiempo de removerlo 43 | this.allstore.getItem(key).then(success2 => { 44 | // es tiempo de removerlo? 45 | if (Date.now() >= (success2._lastupdate_time + 24 * 3600 * 1000)) { 46 | // removemos!! 47 | this.allstore.removeItem(key); 48 | } 49 | }) 50 | .catch( () => { /* */ } ); 51 | }); 52 | }) 53 | .catch( () => { /* */ } ); 54 | } 55 | 56 | public getObjet(key: string): Promise { 57 | let deferred = this.$q.defer(); 58 | 59 | this.allstore.getItem('jsonapi.' + key, true) 60 | .then (success => { 61 | deferred.resolve(success); 62 | }) 63 | .catch(error => { 64 | deferred.reject(error); 65 | }); 66 | 67 | return deferred.promise; 68 | } 69 | 70 | public getObjets(keys: Array): Promise { 71 | return this.allstore.getItem('jsonapi.' + keys[0]); 72 | } 73 | 74 | public saveObject(key: string, value: IStoreObject): void { 75 | value._lastupdate_time = Date.now(); 76 | this.allstore.setItem('jsonapi.' + key, value); 77 | } 78 | 79 | public clearCache() { 80 | this.allstore.clear(); 81 | this.globalstore.clear(); 82 | } 83 | 84 | public deprecateObjectsWithKey(key_start_with: string) { 85 | this.allstore.keys().then(success => { 86 | angular.forEach(success, (key: string) => { 87 | if (key.startsWith(key_start_with)) { 88 | // key of stored object starts with key_start_with 89 | this.allstore.getItem(key).then(success2 => { 90 | success2._lastupdate_time = 0; 91 | this.allstore.setItem(key, success2); 92 | }) 93 | .catch( () => { /* */ } ) 94 | ; 95 | } 96 | }); 97 | }) 98 | .catch( () => { /* */ } ) 99 | ; 100 | } 101 | } 102 | 103 | angular.module('Jsonapi.services').service('JsonapiStoreService', StoreService); 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-angular-jsonapi", 3 | "version": "0.6.39", 4 | "description": "JSONAPI library developed for AngularJS in Typescript", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/reyesoft/ts-angular-jsonapi" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/reyesoft/ts-angular-jsonapi/issues" 11 | }, 12 | "dependencies": { 13 | "angular-localforage": "1.3.7" 14 | }, 15 | "peerDependencies": { 16 | "angular": ">=1.4.0" 17 | }, 18 | "devDependencies": { 19 | "@types/angular": "^1.6.32", 20 | "@types/angular-localforage": "^1.2.35", 21 | "@types/angular-mocks": "^1.5.11", 22 | "@types/angular-ui-router": "^1.1.36", 23 | "@types/es6-shim": "^0.31.35", 24 | "@types/jasmine": "^2.5.54", 25 | "@types/jquery": "^3.2.12", 26 | "@types/node": "^8.0.26", 27 | "angular": "^1.6.6", 28 | "angular-mocks": "^1.6.6", 29 | "angular-ui-router": "1.0.3", 30 | "autoprefixer": "^6.7.7", 31 | "babel-eslint": "^7.1.1", 32 | "babel-loader": "^6.3.2", 33 | "bootstrap": "^3.3.7", 34 | "browser-sync": "^2.18.13", 35 | "browser-sync-spa": "^1.0.3", 36 | "css-loader": "^0.26.1", 37 | "declaration-bundler-webpack-plugin": "^1.0.3", 38 | "del": "^2.2.2", 39 | "es6-shim": "^0.35.3", 40 | "eslint": "^4.6.0", 41 | "eslint-config-angular": "^0.5.0", 42 | "eslint-config-xo-space": "^0.15.0", 43 | "eslint-loader": "^1.6.1", 44 | "eslint-plugin-angular": "^1.6.1", 45 | "eslint-plugin-babel": "^4.1.2", 46 | "extract-text-webpack-plugin": "^2.0.0-rc.3", 47 | "file-loader": "^0.11.1", 48 | "gulp": "github:gulpjs/gulp#4ed9a4a3275559c73a396eff7e1fde3824951ebb", 49 | "gulp-add-src": "^0.2.0", 50 | "gulp-angular-filesort": "^1.1.1", 51 | "gulp-angular-templatecache": "^2.0.0", 52 | "gulp-clean": "^0.3.2", 53 | "gulp-delete-lines": "0.0.7", 54 | "gulp-filter": "^5.0.1", 55 | "gulp-gh-pages": "^0.5.4", 56 | "gulp-htmlmin": "^3.0.0", 57 | "gulp-hub": "github:frankwallis/gulp-hub#d461b9c700df9010d0a8694e4af1fb96d9f38bf4", 58 | "gulp-inject-string": "^1.1.0", 59 | "gulp-insert": "^0.5.0", 60 | "gulp-ng-annotate": "^2.0.0", 61 | "gulp-replace": "^0.5.4", 62 | "gulp-sass": "^3.1.0", 63 | "gulp-typescript": "^3.2.2", 64 | "gulp-util": "^3.0.8", 65 | "html-loader": "^0.4.4", 66 | "html-webpack-plugin": "^2.30.1", 67 | "jasmine": "^2.8.0", 68 | "json-loader": "^0.5.7", 69 | "karma": "^1.7.1", 70 | "karma-angular-filesort": "^1.0.2", 71 | "karma-coverage": "^1.1.1", 72 | "karma-es6-shim": "^1.0.0", 73 | "karma-jasmine": "^1.1.0", 74 | "karma-junit-reporter": "^1.2.0", 75 | "karma-ng-html2js-preprocessor": "^1.0.0", 76 | "karma-phantomjs-launcher": "^1.0.2", 77 | "karma-phantomjs-shim": "^1.4.0", 78 | "karma-webpack": "^2.0.4", 79 | "ng-annotate-loader": "^0.2.0", 80 | "node-sass": "^4.5.3", 81 | "phantomjs-prebuilt": "^2.1.15", 82 | "postcss-loader": "^1.3.1", 83 | "sass-loader": "^6.0.1", 84 | "style-loader": "^0.13.1", 85 | "ts-loader": "^2.3.4", 86 | "tslint": "5.5.0", 87 | "tslint-loader": "3.5.3", 88 | "typescript": "2.5.3", 89 | "webpack": "3.3.0", 90 | "webpack-fail-plugin": "^1.0.5" 91 | }, 92 | "files": [ 93 | "index.js", 94 | "dist/**", 95 | "LICENSE", 96 | "README.md", 97 | "package.json" 98 | ], 99 | "keywords": [ 100 | "angularjs", 101 | "jsonapi", 102 | "typescript" 103 | ], 104 | "license": "ISC", 105 | "scripts": { 106 | "build": "rm -rf dist/* && gulp build", 107 | "start": "gulp serve", 108 | "prepare": "gulp build" 109 | }, 110 | "eslintConfig": { 111 | "globals": { 112 | "expect": true 113 | }, 114 | "root": true, 115 | "env": { 116 | "browser": true, 117 | "jasmine": true 118 | }, 119 | "parser": "babel-eslint", 120 | "extends": [ 121 | "xo-space/esnext" 122 | ] 123 | }, 124 | "main": "/dist/ts-angular-jsonapi.js", 125 | "typings": "./dist/index.d.ts" 126 | } 127 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:all", 3 | "rules": { 4 | "no-empty": false, 5 | "max-classes-per-file": [true, 2], 6 | "linebreak-style": false, 7 | "no-require-imports": false, 8 | "no-inferrable-types": false, 9 | "prefer-function-over-method": false, 10 | "no-magic-numbers": false, 11 | "no-null-keyword": false, 12 | "no-default-export": false, 13 | "no-import-side-effect": false, 14 | "prefer-template": false, 15 | "no-any": false, 16 | "no-unsafe-any": false, 17 | "completed-docs": false, 18 | "forin": false, 19 | "align": [true, "elements", "members", "parameters", "statements"], 20 | "no-unnecessary-type-assertion": false, 21 | "no-unused-variable": false, 22 | "restrict-plus-operands": false, 23 | "arrow-return-shorthand": false, 24 | "no-unnecessary-callback-wrapper": false, 25 | "no-floating-promises": false, 26 | "cyclomatic-complexity": false, 27 | "await-promise": [true, "Thenable"], 28 | "array-type": [true, "generic"], 29 | "arrow-parens": false, 30 | "ban": [true, 31 | ["_", "extend"], 32 | ["_", "isNull"], 33 | ["_", "isDefined"] 34 | ], 35 | "ban-types": false, 36 | "comment-format": [ 37 | true, 38 | "check-space" 39 | ], 40 | "curly": [true, "ignore-same-line"], 41 | "indent": [ 42 | true, 43 | "spaces" 44 | ], 45 | "member-access": false, 46 | "max-line-length": [true, 140], 47 | "member-ordering": [true, 48 | "static-before-instance", 49 | "variables-before-functions" 50 | ], 51 | "no-angle-bracket-type-assertion": false, 52 | "no-conditional-assignment": false, 53 | "no-console": [true, 54 | "debug", 55 | "time", 56 | "timeEnd", 57 | "trace" 58 | ], 59 | "no-parameter-properties": false, 60 | "no-duplicate-variable": [true, "check-parameters"], 61 | "no-unbound-method": [true, "ignore-static"], 62 | "object-literal-shorthand": false, 63 | "object-literal-sort-keys": false, 64 | "only-arrow-functions": false, 65 | "ordered-imports": false, 66 | "prefer-conditional-expression": false, 67 | "prefer-const": false, 68 | "quotemark": [ 69 | true, 70 | "single" 71 | ], 72 | "semicolon": [ 73 | true, 74 | "always" 75 | ], 76 | "strict-boolean-expressions": false, 77 | "switch-default": false, 78 | "switch-final-break": [true, "always"], 79 | "trailing-comma": [ 80 | true, { 81 | "multiline": "never", 82 | "singleline": "never" 83 | } 84 | ], 85 | "triple-equals": [true, "allow-null-check"], 86 | "typedef": [ 87 | true, 88 | "arrow-call-signature", 89 | "property-declaration", 90 | "object-destructuring", 91 | "array-destructuring" 92 | ], 93 | "typedef-whitespace": [true, 94 | { 95 | "call-signature": "nospace", 96 | "index-signature": "nospace", 97 | "parameter": "nospace", 98 | "property-declaration": "nospace", 99 | "variable-declaration": "nospace" 100 | }, 101 | { 102 | "call-signature": "onespace", 103 | "index-signature": "onespace", 104 | "parameter": "onespace", 105 | "property-declaration": "onespace", 106 | "variable-declaration": "onespace" 107 | } 108 | ], 109 | "variable-name": [ 110 | true, 111 | "ban-keywords", 112 | "check-format", "allow-snake-case", "allow-pascal-case" 113 | ], 114 | "whitespace": [ 115 | true, 116 | "check-branch", 117 | "check-decl", 118 | "check-operator", 119 | "check-module", 120 | "check-separator", 121 | "check-type", 122 | "check-preblock" 123 | ] 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/library/services/resource-relationships-converter.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | import { IResource, IRelationships, ISchema, IResourcesByType } from '../interfaces'; 3 | import { IDataCollection } from '../interfaces/data-collection'; 4 | import { IDataObject } from '../interfaces/data-object'; 5 | import { IDataResource } from '../interfaces/data-resource'; 6 | import { Base } from '../services/base'; 7 | 8 | export class ResourceRelationshipsConverter { 9 | private getService: Function; 10 | private relationships_from: object; 11 | private relationships_dest: IRelationships; 12 | private included_resources: IResourcesByType; 13 | private schema: ISchema; 14 | 15 | /** @ngInject */ 16 | public constructor( 17 | getService: Function, 18 | relationships_from: object, 19 | relationships_dest: IRelationships, 20 | included_resources: IResourcesByType, 21 | schema: ISchema 22 | ) { 23 | this.getService = getService; 24 | this.relationships_from = relationships_from; 25 | this.relationships_dest = relationships_dest; 26 | this.included_resources = included_resources; 27 | this.schema = schema; 28 | } 29 | 30 | public buildRelationships(): void { 31 | // recorro los relationships levanto el service correspondiente 32 | angular.forEach(this.relationships_from, (relation_from_value: IDataCollection & IDataObject, relation_key) => { 33 | 34 | // relation is in schema? have data or just links? 35 | if (!(relation_key in this.relationships_dest) && ('data' in relation_from_value)) { 36 | this.relationships_dest[relation_key] = { data: Base.newCollection() }; 37 | } 38 | 39 | // sometime data=null or simple { } 40 | if (!relation_from_value.data) { 41 | return ; 42 | } 43 | 44 | if (this.schema.relationships[relation_key] && this.schema.relationships[relation_key].hasMany) { 45 | // hasMany 46 | if (relation_from_value.data.length === 0) { 47 | // from data is an empty array, remove all data on relationship 48 | this.relationships_dest[relation_key] = { data: Base.newCollection() }; 49 | 50 | return ; 51 | } 52 | this.__buildRelationshipHasMany( 53 | relation_from_value, 54 | relation_key 55 | ); 56 | } else { 57 | // hasOne 58 | this.__buildRelationshipHasOne( 59 | relation_from_value, 60 | relation_key 61 | ); 62 | } 63 | }); 64 | } 65 | 66 | private __buildRelationshipHasMany( 67 | relation_from_value: IDataCollection, 68 | relation_key: number 69 | ) { 70 | let resource_service = this.getService(relation_from_value.data[0].type); 71 | if (resource_service) { 72 | let tmp_relationship_data = Base.newCollection(); 73 | angular.forEach(relation_from_value.data, (relation_value: IDataResource) => { 74 | let tmp = this.__buildRelationship(relation_value, this.included_resources); 75 | 76 | // sometimes we have a cache like a services 77 | if (!('attributes' in tmp) 78 | && tmp.id in this.relationships_dest[relation_key].data 79 | && 'attributes' in this.relationships_dest[relation_key].data[tmp.id] 80 | ) { 81 | tmp_relationship_data[tmp.id] = this.relationships_dest[relation_key].data[tmp.id]; 82 | } else { 83 | tmp_relationship_data[tmp.id] = tmp; 84 | } 85 | }); 86 | 87 | // REMOVE resources from cached collection 88 | // build an array with the news ids 89 | let new_ids = {}; 90 | angular.forEach(relation_from_value.data, (data_resource: IDataResource) => { 91 | new_ids[data_resource.id] = true; 92 | }); 93 | // check if new ids are on destination. If not, delete resource 94 | angular.forEach(this.relationships_dest[relation_key].data, (relation_dest_value: IDataResource) => { 95 | if (!(relation_dest_value.id in new_ids)) { 96 | delete this.relationships_dest[relation_dest_value.id]; 97 | } 98 | }); 99 | 100 | this.relationships_dest[relation_key].data = tmp_relationship_data; 101 | } 102 | } 103 | 104 | private __buildRelationshipHasOne( 105 | relation_data_from: IDataObject, 106 | relation_data_key: number 107 | ): void { 108 | // new related resource <> cached related resource <> ? delete! 109 | if (!('type' in relation_data_from.data)) { 110 | this.relationships_dest[relation_data_key].data = {}; 111 | 112 | return; 113 | } 114 | 115 | if ( 116 | this.relationships_dest[relation_data_key].data == null || 117 | relation_data_from.data.id !== (this.relationships_dest[relation_data_key].data).id 118 | ) { 119 | this.relationships_dest[relation_data_key].data = {}; 120 | } 121 | 122 | // trae datos o cambió resource? actualizamos! 123 | if ( 124 | // 'attributes' in relation_data_from.data || // ??? 125 | !(this.relationships_dest[relation_data_key].data).attributes || // we have only a dataresource 126 | (this.relationships_dest[relation_data_key].data).id !== relation_data_from.data.id 127 | ) { 128 | let resource_data = this.__buildRelationship(relation_data_from.data, this.included_resources); 129 | this.relationships_dest[relation_data_key].data = resource_data; 130 | } 131 | } 132 | 133 | private __buildRelationship(resource_data_from: IDataResource, included_array: IResourcesByType): IResource | IDataResource { 134 | if (resource_data_from.type in included_array && 135 | resource_data_from.id in included_array[resource_data_from.type] 136 | ) { 137 | // it's in included 138 | return included_array[resource_data_from.type][resource_data_from.id]; 139 | } else { 140 | // OPTIONAL: return cached IResource 141 | let service = this.getService(resource_data_from.type); 142 | if (service && resource_data_from.id in service.cachememory.resources) { 143 | return service.cachememory.resources[resource_data_from.id]; 144 | } else { 145 | // we dont have information on included or memory. try pass to store 146 | if (service) { 147 | service.cachestore.getResource(resource_data_from) 148 | .catch(angular.noop); 149 | } 150 | 151 | return resource_data_from; 152 | } 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/library/services/converter.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | import { Core } from '../core'; 3 | import { Resource } from '../resource'; 4 | import { ICollection, IResource, IService, IResourcesById, IResourcesByType } from '../interfaces'; 5 | import { ResourceRelationshipsConverter } from './resource-relationships-converter'; 6 | import { IDataObject } from '../interfaces/data-object'; 7 | import { IDataCollection } from '../interfaces/data-collection'; 8 | import { IDataResource } from '../interfaces/data-resource'; 9 | 10 | export class Converter { 11 | 12 | /* 13 | Convert json arrays (like included) to an Resources arrays without [keys] 14 | */ 15 | private static json_array2resources_array( 16 | json_array: Array, 17 | destination_array: IResourcesById = {} 18 | ): void { 19 | for (let data of json_array) { 20 | let resource = Converter.json2resource(data, false); 21 | destination_array[resource.type + '_' + resource.id] = resource; 22 | } 23 | } 24 | 25 | /* 26 | Convert json arrays (like included) to an indexed Resources array by [type][id] 27 | */ 28 | static json_array2resources_array_by_type( 29 | json_array: Array 30 | ): IResourcesByType { 31 | let all_resources: IResourcesById = {}; 32 | let resources_by_type: IResourcesByType = {}; 33 | 34 | Converter.json_array2resources_array(json_array, all_resources); 35 | angular.forEach(all_resources, (resource: IResource) => { 36 | if (!(resource.type in resources_by_type)) { 37 | resources_by_type[resource.type] = {}; 38 | } 39 | resources_by_type[resource.type][resource.id] = resource; 40 | }); 41 | 42 | return resources_by_type; 43 | } 44 | 45 | static json2resource(json_resource: IDataResource, instance_relationships): IResource { 46 | let resource_service = Converter.getService(json_resource.type); 47 | if (resource_service) { 48 | return Converter.procreate(json_resource); 49 | } else { 50 | // service not registered 51 | console.warn('`' + json_resource.type + '`', 'service not found on json2resource()'); 52 | let temp = new Resource(); 53 | temp.id = json_resource.id; 54 | temp.type = json_resource.type; 55 | 56 | return temp; 57 | } 58 | } 59 | 60 | static getService(type: string): IService { 61 | let resource_service = Core.me.getResourceService(type); 62 | if (angular.isUndefined(resource_service)) { 63 | // console.warn('`' + type + '`', 'service not found on getService()'); 64 | } 65 | 66 | return resource_service; 67 | } 68 | 69 | /* return a resource type(resoruce_service) with data(data) */ 70 | private static procreate(data: IDataResource): IResource { 71 | if (!('type' in data && 'id' in data)) { 72 | console.error('Jsonapi Resource is not correct', data); 73 | } 74 | 75 | let resource: IResource; 76 | if (data.id in Converter.getService(data.type).cachememory.resources) { 77 | resource = Converter.getService(data.type).cachememory.resources[data.id]; 78 | } else { 79 | resource = Converter.getService(data.type).cachememory.getOrCreateResource(data.type, data.id); 80 | } 81 | 82 | resource.attributes = data.attributes || {}; 83 | resource.is_new = false; 84 | 85 | return resource; 86 | } 87 | 88 | public static build( 89 | document_from: IDataCollection & IDataObject, 90 | resource_dest: IResource | ICollection 91 | ) { 92 | // instancio los include y los guardo en included arrary 93 | let included_resources: IResourcesByType = {}; 94 | if ('included' in document_from) { 95 | included_resources = Converter.json_array2resources_array_by_type(document_from.included); 96 | } 97 | 98 | if (angular.isArray(document_from.data)) { 99 | Converter._buildCollection(document_from, resource_dest, included_resources); 100 | } else { 101 | Converter._buildResource(document_from.data, resource_dest, included_resources); 102 | } 103 | } 104 | 105 | private static _buildCollection( 106 | collection_data_from: IDataCollection, 107 | collection_dest: ICollection, 108 | included_resources: IResourcesByType 109 | ) { 110 | // sometime get Cannot set property 'number' of undefined (page) 111 | if (collection_dest.page && collection_data_from.meta) { 112 | collection_dest.page.number = collection_data_from.meta.page || 1; 113 | collection_dest.page.resources_per_page = collection_data_from.meta.resources_per_page || null; 114 | collection_dest.page.total_resources = collection_data_from.meta.total_resources || null; 115 | } 116 | 117 | // convert and add new dataresoures to final collection 118 | let new_ids = {}; 119 | for (let dataresource of collection_data_from.data) { 120 | if (!(dataresource.id in collection_dest)) { 121 | collection_dest[dataresource.id] = 122 | Converter.getService(dataresource.type).cachememory.getOrCreateResource(dataresource.type, dataresource.id); 123 | } 124 | Converter._buildResource(dataresource, collection_dest[dataresource.id], included_resources); 125 | new_ids[dataresource.id] = dataresource.id; 126 | } 127 | 128 | // remove old members of collection (bug, for example, when request something like orders/10/details and has new ids) 129 | angular.forEach(collection_dest, resource => { 130 | if (!(resource.id in new_ids)) { 131 | delete collection_dest[resource.id]; 132 | } 133 | }); 134 | } 135 | 136 | private static _buildResource( 137 | resource_data_from: IDataResource, 138 | resource_dest: IResource, 139 | included_resources: IResourcesByType 140 | ) { 141 | resource_dest.attributes = resource_data_from.attributes; 142 | 143 | resource_dest.id = resource_data_from.id; 144 | resource_dest.is_new = false; 145 | let service = Converter.getService(resource_data_from.type); 146 | 147 | // esto previene la creación indefinida de resources 148 | // el servicio debe estar sino no tenemos el schema 149 | if (!resource_dest.relationships || !service) { 150 | return; 151 | } 152 | 153 | Converter.getService(resource_data_from.type).parseFromServer(resource_dest.attributes); 154 | 155 | let relationships_converter = new ResourceRelationshipsConverter( 156 | Converter.getService, 157 | resource_data_from.relationships, 158 | resource_dest.relationships, 159 | included_resources, 160 | service.schema 161 | ); 162 | relationships_converter.buildRelationships(); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # For the latest Angular, use [ngx-jsonapi 🚀](//github.com/reyesoft/ngx-jsonapi) 2 | 3 | We are ready for Angular 2 or above versions on [ngx-jsonapi](//github.com/reyesoft/ngx-jsonapi). 4 | 5 | # ts-angular-jsonapi 6 | 7 | JsonApi client library for AngularJS + Typescript. 8 | 9 | ## Online demo 10 | 11 | You can test library on this online example 👌 12 | 13 | Data is obtained from [Json Api Playground](http://jsonapiplayground.reyesoft.com/). 14 | 15 | ## Supported features 16 | 17 | - Cache (on memory): Before a HTTP request objects are setted with cached data. 18 | - Cache (on memory): TTL for collections and resources 19 | - Cache on localstorage 20 | - Pagination 21 | - Filtering by attributes through a string or a regular expression 22 | - TS Definitions for strong typing and autocomplete ([See example image](https://github.com/reyesoft/ts-angular-jsonapi/wiki/Autocomplete)) 23 | - [Include param support](http://jsonapi.org/format/#fetching-includes) (also, when you save) 24 | - Two+ equal resource request, only one HTTP call. 25 | - Equal requests, return a same ResourceObject on memory 26 | - Default values for a new resource 27 | - [Properties on collections](https://github.com/reyesoft/ts-angular-jsonapi/blob/master/src/library/interfaces/collection.d.ts) like `$length`, `$is_loading` or `$source` (_`empty`_ |`cache`|`server`) 28 | 29 | ## Usage 30 | 31 | More information on [examples section](#examples). 32 | 33 | ### Installation 34 | 35 | First of all, you need read, read and read [Jsonapi specification](http://jsonapi.org/). 36 | 37 | ```bash 38 | npm install ts-angular-jsonapi --save 39 | ``` 40 | 41 | ### Dependecies and customization 42 | 43 | 1. Add Jsonapi dependency. 44 | 2. Configure your url and other paramemeters. 45 | 3. Inject JsonapiCore somewhere before you extend any class from `Jsonapi.Resource`. 46 | 47 | ```javascript 48 | import 'ts-angular-jsonapi'; 49 | 50 | var app = angular.module('yourAppName', ['rsJsonapi']); 51 | 52 | app.config(['rsJsonapiConfig', (rsJsonapiConfig) => { 53 | angular.extend(rsJsonapiConfig, { 54 | url: '//jsonapiplayground.reyesoft.com/v2/' 55 | }); 56 | }]); 57 | 58 | var MyController = function(JsonapiCore) { 59 | // ... 60 | } 61 | MyController.$inject = ['JsonapiCore']; 62 | ``` 63 | 64 | ## Examples 65 | 66 | Like you know, the better way is with examples. Based on [endpoints example library](https://github.com/endpoints/endpoints-example/). 67 | 68 | ### Defining a resource 69 | 70 | `authors.service.ts` 71 | 72 | ```typescript 73 | class AuthorsService extends Jsonapi.Resource { 74 | type = 'authors'; 75 | public schema: Jsonapi.ISchema = { 76 | attributes: { 77 | name: { presence: true, length: { maximum: 96 } }, 78 | date_of_birth: {}, 79 | date_of_death: {}, 80 | created_at: {}, 81 | updated_at: {} 82 | }, 83 | relationships: { 84 | books: {}, 85 | photos: {} 86 | } 87 | }; 88 | } 89 | angular.module('demoApp').service('AuthorsService', AuthorsService); 90 | ``` 91 | 92 | ### Get a collection of resources 93 | 94 | #### Controller 95 | 96 | ```javascript 97 | class AuthorsController { 98 | public authors: any = null; 99 | 100 | /** @ngInject */ 101 | constructor(AuthorsService) { 102 | this.authors = AuthorsService.all(); 103 | } 104 | } 105 | ``` 106 | 107 | #### View for this controller 108 | 109 | ```html 110 |

111 | id: {{ author.id }}
112 | name: {{ author.attributes.name }}
113 | birth date: {{ author.attributes.date_of_birth | date }} 114 |

115 | ``` 116 | 117 | #### More options? Collection filtering 118 | 119 | Filter resources with `attribute: value` values. Filters are used as 'exact match' (only resources with attribute value same as value are returned). `value` can also be an array, then only objects with same `attribute` value as one of `values` array elements are returned. 120 | 121 | ```javascript 122 | let authors = AuthorsService.all( 123 | { 124 | localfilter: { name: 'xx' }, // request all data and next filter locally 125 | remotefilter: { country: 'Argentina' } // request data with filter url parameter 126 | } 127 | ); 128 | ``` 129 | 130 | ### Get a single resource 131 | 132 | From this point, you only see important code for this library. For a full example, clone and see demo directory. 133 | 134 | ```javascript 135 | let author = AuthorsService.get('some_author_id'); 136 | ``` 137 | 138 | #### More options? Include resources when you fetch data (or save!) 139 | 140 | ```javascript 141 | let author = AuthorsService.get( 142 | 'some_author_id', 143 | { include: ['books', 'photos'] }, 144 | success => { 145 | console.log('Author loaded.', success); 146 | }, 147 | error => { 148 | console.log('Author don`t loaded. Error.', error); 149 | } 150 | ); 151 | ``` 152 | 153 | TIP: these parameters work with `all()` and `save()` methods too. 154 | 155 | ### Add a new resource 156 | 157 | ```javascript 158 | let author = this.AuthorsService.new(); 159 | author.attributes.name = 'Pablo Reyes'; 160 | author.attributes.date_of_birth = '2030-12-10'; 161 | author.save(); 162 | ``` 163 | 164 | #### Need you more control and options? 165 | 166 | ```javascript 167 | let author = this.AuthorsService.new(); 168 | author.attributes.name = 'Pablo Reyes'; 169 | author.attributes.date_of_birth = '2030-12-10'; 170 | 171 | // some_book is an another resource like author 172 | let some_book = this.BooksService.get(1); 173 | author.addRelationship(some_book); 174 | 175 | // some_publisher is a polymorphic resource named company on this case 176 | let some_publisher = this.PublishersService.get(1); 177 | author.addRelationship(some_publisher, 'company'); 178 | 179 | // wow, now we need detach a relationship 180 | author.removeRelationship('books', 'book_id'); 181 | 182 | // this library can send include information to server, for atomicity 183 | author.save( { include: ['book'] }); 184 | 185 | // mmmm, if I need get related resources? For example, books related with author 1 186 | let relatedbooks = BooksService.all( { beforepath: 'authors/1' } ); 187 | 188 | // you need get a cached object? you can force ttl on get 189 | let author = AuthorsService.get( 190 | 'some_author_id', 191 | { ttl: 60 } // ttl on seconds (default: 0) 192 | ); 193 | ``` 194 | 195 | ### Update a resource 196 | 197 | ```javascript 198 | let author = AuthorsService.get('some_author_id'); 199 | this.author.attributes.name += 'New Name'; 200 | this.author.save(success => { 201 | console.log('author saved!'); 202 | }); 203 | ``` 204 | 205 | ### Pagination 206 | 207 | ```javascript 208 | let authors = AuthorsService.all( 209 | { 210 | // get page 2 of authors collection, with a limit per page of 50 211 | page: { number: 2 ; limit: 50 } 212 | } 213 | ); 214 | ``` 215 | 216 | #### Collection page 217 | 218 | - number: number of the current page 219 | - limit: limit of resources per page ([it's sended to server by url](http://jsonapi.org/format/#fetching-pagination)) 220 | - information returned from server (check if is avaible) **total_resources: total of avaible resources on server** resources_per_page: total of resources returned per page requested 221 | 222 | ## Local Demo App 223 | 224 | You can run [JsonApi Demo App](http://reyesoft.github.io/ts-angular-jsonapi/) locally following the next steps: 225 | 226 | ```bash 227 | git clone git@github.com:reyesoft/ts-angular-jsonapi.git 228 | cd ts-angular-jsonapi 229 | npm install -g gulp # if you are on linux, you need do this with sudo 230 | npm install 231 | gulp serve 232 | ``` 233 | 234 | We use as backend [Json Api Playground](http://jsonapiplayground.reyesoft.com/). 235 | 236 | ## Colaborate 237 | 238 | First you need run the demo. Next, when you made new features on your fork, please run 239 | 240 | ```bash 241 | gulp dist 242 | ``` 243 | 244 | And commit! Don't forget your pull request :) 245 | -------------------------------------------------------------------------------- /src/library/services/cachestore.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | import { ICollection, IResource } from '../interfaces'; 3 | import { IDataResource } from '../interfaces/data-resource'; 4 | import { IDataCollection } from '../interfaces/data-collection'; 5 | import { ICacheStore } from '../interfaces'; 6 | import { Core } from '../core'; 7 | import { Converter } from './converter'; 8 | 9 | export class CacheStore implements ICacheStore { 10 | public getResource(resource: IResource/* | IDataResource*/, include: Array = []): ng.IPromise { 11 | let deferred = Core.injectedServices.$q.defer(); 12 | 13 | Core.injectedServices.JsonapiStoreService.getObjet(resource.type + '.' + resource.id) 14 | .then(success => { 15 | Converter.build({ data: success }, resource); 16 | 17 | let promises: Array> = []; 18 | 19 | // include some times is a collection :S 20 | // for (let keys in include) { 21 | angular.forEach(include, resource_type => { 22 | // && ('attributes' in resource.relationships[resource_type].data) 23 | if (resource_type in resource.relationships) { 24 | // hasOne 25 | let related_resource = (resource.relationships[resource_type].data); 26 | if (!('attributes' in related_resource)) { 27 | // no está cargado aún 28 | let builded_resource = this.getResourceFromMemory(related_resource); 29 | if (builded_resource.is_new) { 30 | // no está en memoria, la pedimos a store 31 | promises.push(this.getResource(builded_resource)); 32 | } else { 33 | console.warn('ts-angular-json: esto no debería pasar #isdjf2l1a'); 34 | } 35 | resource.relationships[resource_type].data = builded_resource; 36 | } 37 | 38 | // angular.forEach(resource.relationships[resource_type], (dataresource: IDataResource) => { 39 | // console.log('> debemos pedir', resource_type, 'id', dataresource, dataresource.id); 40 | // }); 41 | } 42 | }); 43 | 44 | resource.lastupdate = success._lastupdate_time; 45 | 46 | // no debo esperar a que se resuelvan los include 47 | if (promises.length === 0) { 48 | deferred.resolve(success); 49 | } else { 50 | // esperamos las promesas de los include antes de dar el resolve 51 | Core.injectedServices.$q.all(promises) 52 | .then(success3 => { 53 | deferred.resolve(success3); 54 | }) 55 | .catch((error3) => { 56 | deferred.reject(error3); 57 | }); 58 | } 59 | }) 60 | .catch(() => { 61 | deferred.reject(); 62 | }); 63 | 64 | // build collection and resources from store 65 | // Core.injectedServices.$q.all(promises) 66 | // .then(success2 => { 67 | // deferred.resolve(success2); 68 | // }) 69 | // .catch(() => { 70 | // deferred.reject(); 71 | // }); 72 | 73 | return deferred.promise; 74 | } 75 | 76 | public setResource(resource: IResource) { 77 | Core.injectedServices.JsonapiStoreService.saveObject( 78 | resource.type + '.' + resource.id, 79 | resource.toObject().data 80 | ); 81 | } 82 | 83 | public getCollectionFromStorePromise(url: string, include: Array, collection: ICollection): ng.IPromise { 84 | let deferred: ng.IDeferred = Core.injectedServices.$q.defer(); 85 | this.getCollectionFromStore(url, include, collection, deferred); 86 | 87 | return deferred.promise; 88 | } 89 | 90 | private getCollectionFromStore(url: string, include: Array, collection: ICollection, job_deferred: ng.IDeferred) { 91 | let promise = Core.injectedServices.JsonapiStoreService.getObjet('collection.' + url); 92 | promise.then((success: IDataCollection) => { 93 | // build collection from store and resources from memory 94 | if ( 95 | this.fillCollectionWithArrrayAndResourcesOnMemory(success.data, collection) 96 | ) { 97 | collection.$source = 'store'; // collection from storeservice, resources from memory 98 | collection.$cache_last_update = success._lastupdate_time; 99 | job_deferred.resolve(collection); 100 | 101 | return ; 102 | } 103 | 104 | let promise2 = this.fillCollectionWithArrrayAndResourcesOnStore(success, include, collection); 105 | promise2.then(() => { 106 | // just for precaution, we not rewrite server data 107 | if (collection.$source !== 'new') { 108 | console.warn('ts-angular-json: esto no debería pasar. buscar eEa2ASd2#'); 109 | throw new Error('ts-angular-json: esto no debería pasar. buscar eEa2ASd2#'); 110 | } 111 | collection.$source = 'store'; // collection and resources from storeservice 112 | collection.$cache_last_update = success._lastupdate_time; 113 | job_deferred.resolve(collection); 114 | }) 115 | .catch(() => { 116 | job_deferred.reject(); 117 | }); 118 | }) 119 | .catch(() => { 120 | job_deferred.reject(); 121 | }); 122 | } 123 | 124 | private fillCollectionWithArrrayAndResourcesOnMemory(dataresources: Array, collection: ICollection): boolean { 125 | let all_ok = true; 126 | for (let key in dataresources) { 127 | let dataresource = dataresources[key]; 128 | 129 | let resource = this.getResourceFromMemory(dataresource); 130 | if (resource.is_new) { 131 | all_ok = false; 132 | break; 133 | } 134 | collection[dataresource.id] = resource; 135 | } 136 | 137 | return all_ok; 138 | } 139 | 140 | private getResourceFromMemory(dataresource: IDataResource): IResource { 141 | let cachememory = Converter.getService(dataresource.type).cachememory; 142 | let resource = cachememory.getOrCreateResource(dataresource.type, dataresource.id); 143 | 144 | return resource; 145 | } 146 | 147 | private fillCollectionWithArrrayAndResourcesOnStore( 148 | datacollection: IDataCollection, include: Array, collection: ICollection 149 | ): ng.IPromise { 150 | let deferred: ng.IDeferred = Core.injectedServices.$q.defer(); 151 | 152 | // request resources from store 153 | let temporalcollection = {}; 154 | let promises = []; 155 | for (let key in datacollection.data) { 156 | let dataresource: IDataResource = datacollection.data[key]; 157 | let cachememory = Converter.getService(dataresource.type).cachememory; 158 | temporalcollection[dataresource.id] = cachememory.getOrCreateResource(dataresource.type, dataresource.id); 159 | promises.push( 160 | this.getResource(temporalcollection[dataresource.id], include) 161 | ); 162 | } 163 | 164 | // build collection and resources from store 165 | Core.injectedServices.$q.all(promises) 166 | .then(success2 => { 167 | if (datacollection.page) { 168 | collection.page = datacollection.page; 169 | } 170 | for (let key in temporalcollection) { 171 | let resource: IResource = temporalcollection[key]; 172 | collection[resource.id] = resource; // collection from storeservice, resources from memory 173 | } 174 | deferred.resolve(collection); 175 | }) 176 | .catch(error2 => { 177 | deferred.reject(error2); 178 | }); 179 | 180 | return deferred.promise; 181 | } 182 | 183 | public setCollection(url: string, collection: ICollection, include: Array) { 184 | let tmp = { data: {}, page: {} }; 185 | let resources_for_save: { [uniqkey: string]: IResource } = { }; 186 | angular.forEach(collection, (resource: IResource) => { 187 | this.setResource(resource); 188 | tmp.data[resource.id] = { id: resource.id, type: resource.type }; 189 | 190 | angular.forEach(include, resource_type_alias => { 191 | if ('id' in resource.relationships[resource_type_alias].data) { 192 | // hasOne 193 | let ress = resource.relationships[resource_type_alias].data; 194 | resources_for_save[resource_type_alias + ress.id] = ress; 195 | } else { 196 | // hasMany 197 | let collection2 = resource.relationships[resource_type_alias].data; 198 | angular.forEach(collection2, (inc_resource: IResource) => { 199 | resources_for_save[resource_type_alias + inc_resource.id] = inc_resource; 200 | }); 201 | } 202 | }); 203 | }); 204 | tmp.page = collection.page; 205 | Core.injectedServices.JsonapiStoreService.saveObject( 206 | 'collection.' + url, 207 | tmp 208 | ); 209 | 210 | angular.forEach(resources_for_save, resource_for_save => { 211 | if ('is_new' in resource_for_save) { 212 | this.setResource(resource_for_save); 213 | } else { 214 | console.warn('No se pudo guardar en la cache el', resource_for_save.type, 'por no se ser IResource.', resource_for_save); 215 | } 216 | }); 217 | } 218 | 219 | public deprecateCollections(path_start_with: string) { 220 | Core.injectedServices.JsonapiStoreService.deprecateObjectsWithKey( 221 | 'collection.' + path_start_with 222 | ); 223 | 224 | return true; 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/library/resource.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | import { Core } from './core'; 3 | import { Base } from './services/base'; 4 | import { ParentResourceService } from './parent-resource-service'; 5 | import { PathBuilder } from './services/path-builder'; 6 | // import { UrlParamsBuilder } from './services/url-params-builder'; 7 | import { Converter } from './services/converter'; 8 | import { IDataObject } from './interfaces/data-object'; 9 | 10 | import { IService, IAttributes, IResource, ICollection, IExecParams, IParamsResource } from './interfaces'; 11 | import { IRelationships, IRelationship } from './interfaces'; 12 | 13 | export class Resource extends ParentResourceService implements IResource { 14 | public is_new = true; 15 | public is_loading = false; 16 | public is_saving = false; 17 | public id: string = ''; 18 | public type: string = ''; 19 | public attributes: IAttributes = {}; 20 | public relationships: IRelationships = {}; 21 | 22 | public reset(): void { 23 | this.id = ''; 24 | this.attributes = {}; 25 | angular.forEach(this.getService().schema.attributes, (value, key) => { 26 | this.attributes[key] = ('default' in value) ? value.default : undefined; 27 | }); 28 | this.relationships = {}; 29 | angular.forEach(this.getService().schema.relationships, (value, key) => { 30 | let relation: IRelationship = { data: {} }; 31 | this.relationships[key] = relation; 32 | if (this.getService().schema.relationships[key].hasMany) { 33 | this.relationships[key].data = Base.newCollection(); 34 | } 35 | }); 36 | this.is_new = true; 37 | } 38 | 39 | public toObject(params?: IParamsResource): IDataObject { 40 | params = angular.extend({}, Base.Params, params); 41 | 42 | let relationships = {}; 43 | let included = []; 44 | let included_ids = []; // just for control don't repeat any resource 45 | 46 | // REALTIONSHIPS 47 | angular.forEach(this.relationships, (relationship: IRelationship, relation_alias) => { 48 | 49 | if (this.getService().schema.relationships[relation_alias] && this.getService().schema.relationships[relation_alias].hasMany) { 50 | // has many (hasMany:true) 51 | relationships[relation_alias] = { data: [] }; 52 | 53 | angular.forEach(relationship.data, (resource: IResource) => { 54 | let reational_object = { id: resource.id, type: resource.type }; 55 | relationships[relation_alias].data.push(reational_object); 56 | 57 | // no se agregó aún a included && se ha pedido incluir con el parms.include 58 | let temporal_id = resource.type + '_' + resource.id; 59 | if (included_ids.indexOf(temporal_id) === -1 && params.include.indexOf(relation_alias) !== -1) { 60 | included_ids.push(temporal_id); 61 | included.push(resource.toObject({ }).data); 62 | } 63 | }); 64 | } else { 65 | // has one (hasMany:false) 66 | 67 | let relationship_data = (relationship.data); 68 | if (!('id' in relationship.data) && !angular.equals({}, relationship.data)) { 69 | console.warn(relation_alias + ' defined with hasMany:false, but I have a collection'); 70 | } 71 | 72 | if (relationship_data.id && relationship_data.type) { 73 | relationships[relation_alias] = { data: { id: relationship_data.id, type: relationship_data.type } }; 74 | } else { 75 | relationships[relation_alias] = { data: { } }; 76 | } 77 | 78 | // no se agregó aún a included && se ha pedido incluir con el parms.include 79 | let temporal_id = relationship_data.type + '_' + relationship_data.id; 80 | if (included_ids.indexOf(temporal_id) === -1 && params.include.indexOf(relationship_data.type) !== -1) { 81 | included_ids.push(temporal_id); 82 | included.push(relationship_data.toObject({ }).data); 83 | } 84 | } 85 | }); 86 | 87 | // just for performance dont copy if not necessary 88 | let attributes; 89 | if (this.getService().parseToServer) { 90 | attributes = angular.copy(this.attributes); 91 | this.getService().parseToServer(attributes); 92 | } else { 93 | attributes = this.attributes; 94 | } 95 | 96 | let ret: IDataObject = { 97 | data: { 98 | type: this.type, 99 | id: this.id, 100 | attributes: attributes, 101 | relationships: relationships 102 | } 103 | }; 104 | 105 | if (included.length > 0) { 106 | ret.included = included; 107 | } 108 | 109 | return ret; 110 | } 111 | 112 | public save(params?: Object | Function, fc_success?: Function, fc_error?: Function): ng.IPromise { 113 | return this.__exec({ id: null, params: params, fc_success: fc_success, fc_error: fc_error, exec_type: 'save' }); 114 | } 115 | 116 | protected __exec(exec_params: IExecParams): ng.IPromise { 117 | let exec_pp = super.proccess_exec_params(exec_params); 118 | 119 | switch (exec_params.exec_type) { 120 | case 'save': 121 | return this._save(exec_pp.params, exec_params.fc_success, exec_params.fc_error); 122 | } 123 | } 124 | 125 | private _save(params: IParamsResource, fc_success: Function, fc_error: Function): ng.IPromise { 126 | let deferred = Core.injectedServices.$q.defer(); 127 | 128 | if (this.is_saving || this.is_loading) { 129 | return ; 130 | } 131 | this.is_saving = true; 132 | 133 | let object = this.toObject(params); 134 | 135 | // http request 136 | let path = new PathBuilder(); 137 | path.applyParams(this.getService(), params); 138 | if (this.id) { 139 | path.appendPath(this.id); 140 | } 141 | 142 | let promise = Core.injectedServices.JsonapiHttp.exec( 143 | path.get(), this.id ? 'PUT' : 'POST', 144 | object, !(angular.isFunction(fc_error)) 145 | ); 146 | 147 | promise.then( 148 | success => { 149 | this.is_saving = false; 150 | 151 | // foce reload cache (for example, we add a new element) 152 | if (!this.id) { 153 | this.getService().cachememory.deprecateCollections(path.get()); 154 | this.getService().cachestore.deprecateCollections(path.get()); 155 | } 156 | 157 | // is a resource? 158 | if ('id' in success.data.data) { 159 | this.id = success.data.data.id; 160 | Converter.build(success.data, this); 161 | /* 162 | Si lo guardo en la caché, luego no queda bindeado con la vista 163 | Usar {{ $ctrl.service.getCachedResources() | json }}, agregar uno nuevo, editar 164 | */ 165 | // this.getService().cachememory.setResource(this); 166 | } else if (angular.isArray(success.data.data)) { 167 | console.warn('Server return a collection when we save()', success.data.data); 168 | 169 | /* 170 | we request the service again, because server maybe are giving 171 | us another type of resource (getService(resource.type)) 172 | */ 173 | let tempororay_collection = this.getService().cachememory.getOrCreateCollection('justAnUpdate'); 174 | Converter.build(success.data, tempororay_collection); 175 | angular.forEach(tempororay_collection, (resource_value: IResource, key: string) => { 176 | let res = Converter.getService(resource_value.type).cachememory.resources[resource_value.id]; 177 | Converter.getService(resource_value.type).cachememory.setResource(resource_value); 178 | Converter.getService(resource_value.type).cachestore.setResource(resource_value); 179 | res.id = res.id + 'x'; 180 | }); 181 | 182 | console.warn('Temporal collection for a resource_value update', tempororay_collection); 183 | } 184 | 185 | this.runFc(fc_success, success); 186 | deferred.resolve(success); 187 | } 188 | ).catch( 189 | error => { 190 | this.is_saving = false; 191 | this.runFc(fc_error, 'data' in error ? error.data : error); 192 | deferred.reject('data' in error ? error.data : error); 193 | } 194 | ); 195 | 196 | return deferred.promise; 197 | } 198 | 199 | public addRelationship(resource: T, type_alias?: string) { 200 | let object_key = resource.id; 201 | if (!object_key) { 202 | object_key = 'new_' + (Math.floor(Math.random() * 100000)); 203 | } 204 | 205 | type_alias = (type_alias ? type_alias : resource.type); 206 | if (!(type_alias in this.relationships)) { 207 | this.relationships[type_alias] = { data: { } }; 208 | } 209 | 210 | if (type_alias in this.getService().schema.relationships && this.getService().schema.relationships[type_alias].hasMany) { 211 | this.relationships[type_alias].data[object_key] = resource; 212 | } else { 213 | this.relationships[type_alias].data = resource; 214 | } 215 | } 216 | 217 | public addRelationships(resources: ICollection, type_alias: string) { 218 | if (!(type_alias in this.relationships)) { 219 | this.relationships[type_alias] = { data: { } }; 220 | } else { 221 | // we receive a new collection of this relationship. We need remove old (if don't exist on new collection) 222 | angular.forEach(this.relationships[type_alias].data, (resource) => { 223 | if (!(resource.id in resources)) { 224 | delete this.relationships[type_alias].data[resource.id]; 225 | } 226 | }); 227 | } 228 | 229 | angular.forEach(resources, (resource) => { 230 | this.relationships[type_alias].data[resource.id] = resource; 231 | }); 232 | } 233 | 234 | public addRelationshipsArray(resources: Array, type_alias?: string): void { 235 | resources.forEach((item: IResource) => { 236 | this.addRelationship(item, type_alias || item.type); 237 | }); 238 | } 239 | 240 | public removeRelationship(type_alias: string, id: string): boolean { 241 | if (!(type_alias in this.relationships)) { 242 | return false; 243 | } 244 | if (!('data' in this.relationships[type_alias])) { 245 | return false; 246 | } 247 | 248 | if (type_alias in this.getService().schema.relationships && this.getService().schema.relationships[type_alias].hasMany) { 249 | if (!(id in this.relationships[type_alias].data)) { 250 | return false; 251 | } 252 | delete this.relationships[type_alias].data[id]; 253 | } else { 254 | this.relationships[type_alias].data = { }; 255 | } 256 | 257 | return true; 258 | } 259 | 260 | /* 261 | @return This resource like a service 262 | */ 263 | public getService(): IService { 264 | return Converter.getService(this.type); 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /src/library/service.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | import { Core } from './core'; 3 | import { Base } from './services/base'; 4 | import { Resource } from './resource'; 5 | import { ParentResourceService } from './parent-resource-service'; 6 | import { PathBuilder } from './services/path-builder'; 7 | import { UrlParamsBuilder } from './services/url-params-builder'; 8 | import { Converter } from './services/converter'; 9 | import { LocalFilter } from './services/localfilter'; 10 | import { CacheMemory } from './services/cachememory'; 11 | import { CacheStore } from './services/cachestore'; 12 | 13 | import { 14 | IService, ISchema, IResource, ICollection, IExecParams, ICacheStore, ICacheMemory, 15 | IParamsCollection, IParamsResource, IAttributes 16 | } from './interfaces'; 17 | 18 | export class Service extends ParentResourceService implements IService { 19 | public schema: ISchema; 20 | public cachememory: ICacheMemory; 21 | public cachestore: ICacheStore; 22 | public type: string; 23 | 24 | private path: string; // without slashes 25 | private smartfiltertype = 'undefined'; 26 | 27 | /* 28 | Register schema on Core 29 | @return true if the resource don't exist and registered ok 30 | */ 31 | public register(): boolean { 32 | if (Core.me === null) { 33 | throw new Error('Error: you are trying register `' + this.type + '` before inject JsonapiCore somewhere, almost one time.'); 34 | } 35 | // only when service is registered, not cloned object 36 | this.cachememory = new CacheMemory(); 37 | this.cachestore = new CacheStore(); 38 | this.schema = angular.extend({}, Base.Schema, this.schema); 39 | 40 | return Core.me._register(this); 41 | } 42 | 43 | public newResource(): IResource { 44 | let resource: IResource = new Resource(); 45 | 46 | return resource; 47 | } 48 | 49 | public new(): T { 50 | let resource = this.newResource(); 51 | resource.type = this.type; 52 | resource.reset(); 53 | 54 | return resource; 55 | } 56 | 57 | public getPrePath(): string { 58 | return ''; 59 | } 60 | public getPath(): string { 61 | return this.path ? this.path : this.type; 62 | } 63 | 64 | public get(id, params?: IParamsResource | Function, fc_success?: Function, fc_error?: Function): T { 65 | return this.__exec({ id: id, params: params, fc_success: fc_success, fc_error: fc_error, exec_type: 'get' }); 66 | } 67 | 68 | public delete(id: string, params?: Object | Function, fc_success?: Function, fc_error?: Function): void { 69 | return this.__exec({ id: id, params: params, fc_success: fc_success, fc_error: fc_error, exec_type: 'delete' }); 70 | } 71 | 72 | public all(params?: IParamsCollection | Function, fc_success?: Function, fc_error?: Function): ICollection { 73 | return this.__exec({ id: null, params: params, fc_success: fc_success, fc_error: fc_error, exec_type: 'all' }); 74 | } 75 | 76 | protected __exec(exec_params: IExecParams): IResource | ICollection | void { 77 | let exec_pp = super.proccess_exec_params(exec_params); 78 | 79 | switch (exec_pp.exec_type) { 80 | case 'get': 81 | return this._get(exec_pp.id, exec_pp.params, exec_pp.fc_success, exec_pp.fc_error); 82 | case 'delete': 83 | return this._delete(exec_pp.id, exec_pp.params, exec_pp.fc_success, exec_pp.fc_error); 84 | case 'all': 85 | return this._all(exec_pp.params, exec_pp.fc_success, exec_pp.fc_error); 86 | } 87 | } 88 | 89 | public _get(id: string, params: IParamsResource, fc_success, fc_error): IResource { 90 | // http request 91 | let path = new PathBuilder(); 92 | path.applyParams(this, params); 93 | path.appendPath(id); 94 | 95 | // CACHEMEMORY 96 | let resource = this.getService().cachememory.getOrCreateResource(this.type, id); 97 | resource.is_loading = true; 98 | // exit if ttl is not expired 99 | let temporal_ttl = params.ttl || 0; // this.schema.ttl 100 | if (this.getService().cachememory.isResourceLive(id, temporal_ttl)) { 101 | // we create a promise because we need return collection before 102 | // run success client function 103 | let deferred = Core.injectedServices.$q.defer(); 104 | deferred.resolve(fc_success); 105 | deferred.promise.then(fc_success2 => { 106 | this.runFc(fc_success2, 'cachememory'); 107 | }) 108 | .catch(() => {}) 109 | ; 110 | resource.is_loading = false; 111 | 112 | return resource; 113 | } else { 114 | // CACHESTORE 115 | this.getService().cachestore.getResource(resource) 116 | .then(success => { 117 | if (Base.isObjectLive(temporal_ttl, resource.lastupdate)) { 118 | this.runFc(fc_success, { data: success}); 119 | } else { 120 | this.getGetFromServer(path, fc_success, fc_error, resource); 121 | } 122 | }) 123 | .catch(error => { 124 | this.getGetFromServer(path, fc_success, fc_error, resource); 125 | }); 126 | } 127 | 128 | return resource; 129 | } 130 | 131 | private getGetFromServer(path, fc_success, fc_error, resource: IResource) { 132 | Core.injectedServices.JsonapiHttp 133 | .get(path.get()) 134 | .then( 135 | success => { 136 | Converter.build(success.data, resource); 137 | resource.is_loading = false; 138 | this.getService().cachememory.setResource(resource); 139 | this.getService().cachestore.setResource(resource); 140 | this.runFc(fc_success, success); 141 | } 142 | ) 143 | .catch( 144 | error => { 145 | this.runFc(fc_error, error); 146 | } 147 | ); 148 | } 149 | 150 | private _all(params: IParamsCollection, fc_success, fc_error): ICollection { 151 | 152 | // check smartfiltertype, and set on remotefilter 153 | if (params.smartfilter && this.smartfiltertype !== 'localfilter') { 154 | angular.extend(params.remotefilter, params.smartfilter); 155 | } 156 | 157 | params.cachehash = params.cachehash || ''; 158 | 159 | // http request 160 | let path = new PathBuilder(); 161 | let paramsurl = new UrlParamsBuilder(); 162 | path.applyParams(this, params); 163 | if (params.remotefilter && Object.keys(params.remotefilter).length > 0) { 164 | if (this.getService().parseToServer) { 165 | this.getService().parseToServer(params.remotefilter); 166 | } 167 | path.addParam(paramsurl.toparams( { filter: params.remotefilter } )); 168 | } 169 | if (params.page) { 170 | if (params.page.number > 1) { 171 | path.addParam(Core.injectedServices.rsJsonapiConfig.parameters.page.number + '=' + params.page.number); 172 | } 173 | if (params.page.limit) { 174 | path.addParam(Core.injectedServices.rsJsonapiConfig.parameters.page.limit + '=' + params.page.limit); 175 | } 176 | } 177 | 178 | // make request 179 | // if we remove this, dont work the same .all on same time (ej: ) 180 | let tempororay_collection = this.getService().cachememory.getOrCreateCollection(path.getForCache()); 181 | 182 | // creamos otra colleción si luego será filtrada 183 | let localfilter = new LocalFilter(params.localfilter); 184 | let cached_collection: ICollection; 185 | if (params.localfilter && Object.keys(params.localfilter).length > 0) { 186 | cached_collection = Base.newCollection(); 187 | } else { 188 | cached_collection = tempororay_collection; 189 | } 190 | 191 | // MEMORY_CACHE 192 | let temporal_ttl = params.ttl || this.schema.ttl; 193 | if (temporal_ttl >= 0 && this.getService().cachememory.isCollectionExist(path.getForCache())) { 194 | // get cached data and merge with temporal collection 195 | tempororay_collection.$source = 'memory'; 196 | 197 | // check smartfiltertype, and set on localfilter 198 | if (params.smartfilter && this.smartfiltertype === 'localfilter') { 199 | angular.extend(params.localfilter, params.smartfilter); 200 | } 201 | 202 | // fill collection and localfilter 203 | localfilter.filterCollection(tempororay_collection, cached_collection); 204 | 205 | // exit if ttl is not expired 206 | if (this.getService().cachememory.isCollectionLive(path.getForCache(), temporal_ttl)) { 207 | // we create a promise because we need return collection before 208 | // run success client function 209 | let deferred = Core.injectedServices.$q.defer(); 210 | deferred.resolve(fc_success); 211 | deferred.promise.then(fc_success2 => { 212 | this.runFc(fc_success2, 'cachememory'); 213 | }) 214 | .catch(() => {}) 215 | ; 216 | } else { 217 | this.getAllFromServer(path, params, fc_success, fc_error, tempororay_collection, cached_collection); 218 | } 219 | } else { 220 | // STORE 221 | tempororay_collection.$is_loading = true; 222 | 223 | this.getService().cachestore.getCollectionFromStorePromise(path.getForCache(), path.includes, tempororay_collection) 224 | .then( 225 | success => { 226 | tempororay_collection.$source = 'store'; 227 | tempororay_collection.$is_loading = false; 228 | 229 | // when load collection from store, we save collection on memory 230 | this.getService().cachememory.setCollection(path.getForCache(), tempororay_collection); 231 | 232 | // localfilter getted data 233 | localfilter.filterCollection(tempororay_collection, cached_collection); 234 | 235 | if (Base.isObjectLive(temporal_ttl, tempororay_collection.$cache_last_update)) { 236 | this.runFc(fc_success, { data: success}); 237 | } else { 238 | this.getAllFromServer(path, params, fc_success, fc_error, tempororay_collection, cached_collection); 239 | } 240 | } 241 | ).catch( 242 | error => { 243 | this.getAllFromServer(path, params, fc_success, fc_error, tempororay_collection, cached_collection); 244 | } 245 | ); 246 | } 247 | 248 | return cached_collection; 249 | } 250 | 251 | private getAllFromServer(path, params, fc_success, fc_error, tempororay_collection: ICollection, cached_collection: ICollection) { 252 | // SERVER REQUEST 253 | tempororay_collection.$is_loading = true; 254 | Core.injectedServices.JsonapiHttp 255 | .get(path.get()) 256 | .then( 257 | success => { 258 | tempororay_collection.$source = 'server'; 259 | tempororay_collection.$is_loading = false; 260 | 261 | // this create a new ID for every resource (for caching proposes) 262 | // for example, two URL return same objects but with different attributes 263 | if (params.cachehash) { 264 | angular.forEach(success.data.data, resource => { 265 | resource.id = resource.id + params.cachehash; 266 | }); 267 | } 268 | 269 | Converter.build(success.data, tempororay_collection); 270 | 271 | this.getService().cachememory.setCollection(path.getForCache(), tempororay_collection); 272 | this.getService().cachestore.setCollection(path.getForCache(), tempororay_collection, params.include); 273 | 274 | // localfilter getted data 275 | let localfilter = new LocalFilter(params.localfilter); 276 | localfilter.filterCollection(tempororay_collection, cached_collection); 277 | 278 | // trying to define smartfiltertype 279 | if (this.smartfiltertype === 'undefined') { 280 | let page = tempororay_collection.page; 281 | if (page.number === 1 && page.total_resources <= page.resources_per_page) { 282 | this.smartfiltertype = 'localfilter'; 283 | } else if (page.number === 1 && page.total_resources > page.resources_per_page) { 284 | this.smartfiltertype = 'remotefilter'; 285 | } 286 | } 287 | 288 | this.runFc(fc_success, success); 289 | } 290 | ) 291 | .catch( 292 | error => { 293 | // do not replace $source, because localstorage don't write if = server 294 | // tempororay_collection.$source = 'server'; 295 | tempororay_collection.$is_loading = false; 296 | this.runFc(fc_error, error); 297 | } 298 | ); 299 | } 300 | 301 | private _delete(id: string, params, fc_success, fc_error): void { 302 | // http request 303 | let path = new PathBuilder(); 304 | path.applyParams(this, params); 305 | path.appendPath(id); 306 | 307 | Core.injectedServices.JsonapiHttp 308 | .delete(path.get()) 309 | .then( 310 | success => { 311 | this.getService().cachememory.removeResource(id); 312 | this.runFc(fc_success, success); 313 | } 314 | ).catch( 315 | error => { 316 | this.runFc(fc_error, error); 317 | } 318 | ); 319 | } 320 | 321 | /* 322 | @return This resource like a service 323 | */ 324 | public getService(): T { 325 | return Converter.getService(this.type); 326 | } 327 | 328 | public clearCacheMemory(): boolean { 329 | let path = new PathBuilder(); 330 | path.applyParams(this); 331 | 332 | return this.getService().cachememory.deprecateCollections(path.getForCache()) && 333 | this.getService().cachestore.deprecateCollections(path.getForCache()); 334 | } 335 | 336 | public parseFromServer(attributes: IAttributes): void { 337 | 338 | } 339 | } 340 | --------------------------------------------------------------------------------