├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .mocharc.json ├── LICENSE.md ├── README.md ├── eslint.config.cjs ├── mocha.setup.ts ├── ng-package.json ├── package.json ├── src ├── abort │ ├── abort.interceptor.ts │ ├── abort.interface.ts │ ├── abort.module.ts │ ├── abort.service.ts │ ├── abort.type.ts │ └── index.ts ├── cache │ ├── cache.interceptor.ts │ ├── cache.interface.ts │ ├── cache.module.ts │ ├── cache.service.ts │ └── index.ts ├── common │ ├── common.decorator.ts │ ├── common.helper.ts │ ├── common.interface.ts │ ├── common.service.ts │ ├── common.type.ts │ └── index.ts ├── core │ ├── create.service.ts │ ├── crud.interface.ts │ ├── crud.module.ts │ ├── crud.service.ts │ ├── crud.type.ts │ ├── custom.service.ts │ ├── delete.service.ts │ ├── find.service.ts │ ├── index.ts │ ├── patch.service.ts │ ├── read.service.ts │ └── update.service.ts ├── index.ts └── observe │ ├── index.ts │ ├── observe.interceptor.ts │ ├── observe.interface.ts │ ├── observe.module.ts │ ├── observe.service.ts │ ├── observe.token.ts │ └── observe.type.ts ├── tests ├── abort.service.spec.ts ├── cache.service.spec.ts ├── common.service.spec.ts ├── crud.service.spec.ts ├── helper.spec.ts ├── observe.service.spec.ts ├── test.effect.ts ├── test.helper.ts ├── test.interface.ts └── test.service.ts ├── tsconfig.build.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_size = 4 7 | indent_style = tab 8 | trim_trailing_whitespace = true 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v4 11 | - name: Set up Node 22 12 | uses: actions/setup-node@v4 13 | with: 14 | node-version: 22 15 | - run: npm install 16 | - run: npm run lint 17 | build: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | - name: Set up Node 22 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: 22 26 | - run: npm install 27 | - run: npm run build 28 | test: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v4 33 | - name: Set up Node 22 34 | uses: actions/setup-node@v4 35 | with: 36 | node-version: 22 37 | - run: npm install 38 | - run: npm run test 39 | report: 40 | needs: test 41 | runs-on: ubuntu-latest 42 | steps: 43 | - name: Checkout 44 | uses: actions/checkout@v4 45 | - name: Set up Node 22 46 | uses: actions/setup-node@v4 47 | with: 48 | node-version: 22 49 | - run: npm install 50 | - run: npx c8 --reporter lcov mocha 51 | - name: Report to Coveralls 52 | uses: coverallsapp/github-action@v2 53 | with: 54 | github-token: ${{ secrets.GITHUB_TOKEN }} 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | build 3 | node_modules 4 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension": 3 | [ 4 | "ts" 5 | ], 6 | "node-option": 7 | [ 8 | "experimental-specifier-resolution=node", 9 | "loader=ts-node/esm" 10 | ], 11 | "require": "mocha.setup.ts", 12 | "spec": "tests", 13 | "timeout": 8000 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT license 2 | 3 | Copyright (c) 2025 Henry Ruhs 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | NGX CRUD 2 | ======== 3 | 4 | > CRUD services in Angular with effortless aborting, caching and observing. 5 | 6 | [![Build Status](https://img.shields.io/github/actions/workflow/status/henryruhs/ngx-crud/ci.yml.svg?branch=master)](https://github.com/henryruhs/ngx-crud/actions?query=workflow:ci) 7 | [![Coverage Status](https://img.shields.io/coveralls/henryruhs/ngx-crud.svg)](https://coveralls.io/r/henryruhs/ngx-crud) 8 | [![NPM Version](https://img.shields.io/npm/v/ngx-crud.svg)](https://npmjs.com/package/ngx-crud) 9 | [![License](https://img.shields.io/npm/l/ngx-crud.svg)](https://npmjs.com/package/ngx-crud) 10 | 11 | 12 | Installation 13 | ------------ 14 | 15 | ``` 16 | npm install ngx-crud 17 | ``` 18 | 19 | 20 | Setup 21 | ----- 22 | 23 | Import the `CrudModule` and `provideHttpClient` inside your `AppModule`: 24 | 25 | ```typescript 26 | import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; 27 | import { NgModule } from '@angular/core'; 28 | import { CrudModule } from 'ngx-crud'; 29 | 30 | @NgModule( 31 | { 32 | imports: 33 | [ 34 | CrudModule 35 | ], 36 | providers: 37 | [ 38 | provideHttpClient(withInterceptorsFromDi()) 39 | ] 40 | }) 41 | export class AppModule 42 | { 43 | } 44 | ``` 45 | 46 | 47 | Usage 48 | ----- 49 | 50 | Extend the `ExampleService` from the `CrudService`: 51 | 52 | ```typescript 53 | import { Injectable } from '@angular/core'; 54 | import { CrudService } from 'ngx-crud'; 55 | import { RequestBody, ResponseBody } from './example.interface'; 56 | 57 | import { environment } from '@environments'; 58 | 59 | @Injectable() 60 | @ApiUrl(environment.apiUrl) 61 | @ApiRoute(environment.apiRoutes.example) 62 | export class ExampleService extends CrudService 63 | { 64 | } 65 | ``` 66 | 67 | Use the HTTP operations as needed: 68 | 69 | ```typescript 70 | exampleService.create(body, options); 71 | exampleService.read(id, options); 72 | exampleService.find(options); 73 | exampleService.update(id, body, options); 74 | exampleService.patch(id, body, options); 75 | exampleService.delete(id, options); 76 | exampleService.custom(method, options); 77 | ``` 78 | 79 | 80 | Documentation 81 | ------------- 82 | 83 | Read the [documentation](https://henryruhs.gitbook.io/ngx-crud) for a deep dive. 84 | -------------------------------------------------------------------------------- /eslint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = 2 | [ 3 | ...require('@isnotdefined/eslint-config/common'), 4 | ...require('@isnotdefined/eslint-config/jest') 5 | ] 6 | -------------------------------------------------------------------------------- /mocha.setup.ts: -------------------------------------------------------------------------------- 1 | import 'core-js/proposals/reflect-metadata'; 2 | import 'core-js/proposals/relative-indexing-method'; 3 | import 'zone.js'; 4 | import 'jsdom-global/register'; 5 | import { TestBed } from '@angular/core/testing'; 6 | import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; 7 | 8 | TestBed.resetTestEnvironment(); 9 | TestBed.initTestEnvironment( 10 | BrowserDynamicTestingModule, 11 | platformBrowserDynamicTesting() 12 | ); 13 | -------------------------------------------------------------------------------- /ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dest": "build", 3 | "lib": 4 | { 5 | "entryFile": "src/index.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-crud", 3 | "description": "CRUD services in Angular with effortless aborting, caching and observing", 4 | "version": "19.0.1", 5 | "homepage": "https://ngx-crud.com", 6 | "license": "MIT", 7 | "type": "module", 8 | "keywords": 9 | [ 10 | "angular", 11 | "http", 12 | "crud", 13 | "aborting", 14 | "caching", 15 | "observing" 16 | ], 17 | "author": 18 | { 19 | "name": "Henry Ruhs", 20 | "url": "https://henryruhs.com" 21 | }, 22 | "bugs": 23 | { 24 | "url": "https://github.com/henryruhs/ngx-crud/issues" 25 | }, 26 | "repository": 27 | { 28 | "type": "git", 29 | "url": "https://github.com/henryruhs/ngx-crud.git" 30 | }, 31 | "peerDependencies": 32 | { 33 | "@angular/common": "^19", 34 | "@angular/core": "^19", 35 | "rxjs": "^7", 36 | "rxjs-collection": "^2" 37 | }, 38 | "devDependencies": 39 | { 40 | "@angular/common": "19.1.5", 41 | "@angular/compiler": "19.1.5", 42 | "@angular/compiler-cli": "19.1.5", 43 | "@angular/core": "19.1.5", 44 | "@angular/platform-browser": "19.1.5", 45 | "@angular/platform-browser-dynamic": "19.1.5", 46 | "@isnotdefined/eslint-config": "10.0.0", 47 | "@types/chai": "5.0.1", 48 | "@types/mocha": "10.0.10", 49 | "chai": "5.1.2", 50 | "core-js": "3.40.0", 51 | "eslint": "9.19.0", 52 | "jsdom": "26.0.0", 53 | "jsdom-global": "3.0.2", 54 | "mocha": "11.1.0", 55 | "ng-packagr": "19.1.2", 56 | "rxjs": "7.8.1", 57 | "rxjs-collection": "2.2.0", 58 | "ts-node": "10.9.2", 59 | "typescript": "5.7.3" 60 | }, 61 | "scripts": 62 | { 63 | "build": "ng-packagr --project ng-package.json --config tsconfig.build.json", 64 | "prepublishOnly": "exit 1", 65 | "lint": "eslint .", 66 | "fix": "npm run lint -- --fix", 67 | "test": "mocha" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/abort/abort.interceptor.ts: -------------------------------------------------------------------------------- 1 | import 2 | { 3 | HttpEvent, 4 | HttpHandler, 5 | HttpInterceptor, 6 | HttpRequest 7 | } from '@angular/common/http'; 8 | import { Injectable } from '@angular/core'; 9 | import { Observable } from 'rxjs'; 10 | import { filter, takeUntil } from 'rxjs/operators'; 11 | 12 | import { Context } from './abort.interface'; 13 | import { AbortService } from './abort.service'; 14 | 15 | @Injectable() 16 | export class AbortInterceptor implements HttpInterceptor 17 | { 18 | constructor(protected abortService : AbortService) 19 | { 20 | } 21 | 22 | intercept(request : HttpRequest, next : HttpHandler) : Observable> 23 | { 24 | const context : Context = request.context.get(this.abortService.getToken()); 25 | const enableAbort : boolean = (context.method === 'ANY' || context.method === request.method) && context.lifetime > 0; 26 | 27 | return enableAbort ? this.handle(request, next) : next.handle(request); 28 | } 29 | 30 | handle(request : HttpRequest, next : HttpHandler) : Observable> 31 | { 32 | return next 33 | .handle(request) 34 | .pipe( 35 | takeUntil(this.abortService.get(request).pipe(filter(signal => signal === 'ABORTED'))) 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/abort/abort.interface.ts: -------------------------------------------------------------------------------- 1 | import { BehaviorSubject, Subscription } from 'rxjs'; 2 | 3 | import { UniversalMethod } from '../common'; 4 | 5 | import { AbortSignal } from './abort.type'; 6 | 7 | export interface Store 8 | { 9 | signal : BehaviorSubject; 10 | timer : Subscription; 11 | } 12 | 13 | export interface Context 14 | { 15 | method : UniversalMethod; 16 | lifetime : number; 17 | } 18 | -------------------------------------------------------------------------------- /src/abort/abort.module.ts: -------------------------------------------------------------------------------- 1 | import { HTTP_INTERCEPTORS } from '@angular/common/http'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { AbortInterceptor } from './abort.interceptor'; 5 | import { AbortService } from './abort.service'; 6 | 7 | @NgModule( 8 | { 9 | providers: 10 | [ 11 | AbortService, 12 | { 13 | multi: true, 14 | provide: HTTP_INTERCEPTORS, 15 | useClass: AbortInterceptor 16 | } 17 | ] 18 | }) 19 | export class AbortModule 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /src/abort/abort.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpContextToken, HttpRequest } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable, BehaviorSubject, Subscription, filter, from, timer, mergeMap } from 'rxjs'; 4 | import { ReactiveMap } from 'rxjs-collection'; 5 | 6 | import { stripUrlParams } from '../common'; 7 | 8 | import { Context, Store } from './abort.interface'; 9 | import { AbortSignal } from './abort.type'; 10 | 11 | @Injectable() 12 | export class AbortService 13 | { 14 | protected defaultContext : Context = 15 | { 16 | method: null, 17 | lifetime: null 18 | }; 19 | 20 | protected token : HttpContextToken = new HttpContextToken(() => this.defaultContext); 21 | protected store : ReactiveMap = new ReactiveMap(); 22 | 23 | getToken() : HttpContextToken 24 | { 25 | return this.token; 26 | } 27 | 28 | get(request : HttpRequest) : Observable 29 | { 30 | if (!this.has(request)) 31 | { 32 | this.set(request); 33 | } 34 | return this.store.get(request.urlWithParams).signal; 35 | } 36 | 37 | set(request : HttpRequest) : this 38 | { 39 | const context : Context = request.context.get(this.getToken()); 40 | 41 | if (this.has(request)) 42 | { 43 | this.store.get(request.urlWithParams).timer.unsubscribe(); 44 | } 45 | this.store.set(request.urlWithParams, 46 | { 47 | signal: new BehaviorSubject('STARTED'), 48 | timer: context.lifetime > 0 ? timer(context.lifetime).subscribe(() => this.abort(request.urlWithParams)) : new Subscription() 49 | }); 50 | return this; 51 | } 52 | 53 | has(request : HttpRequest) : boolean 54 | { 55 | return this.store.has(request.urlWithParams); 56 | } 57 | 58 | abort(urlWithParams : string) : this 59 | { 60 | if (this.store.has(urlWithParams)) 61 | { 62 | this.store.get(urlWithParams).signal.next('ABORTED'); 63 | this.store.get(urlWithParams).timer.unsubscribe(); 64 | this.store.delete(urlWithParams); 65 | } 66 | return this; 67 | } 68 | 69 | abortMany(url : string) : this 70 | { 71 | this.store.forEach((store, urlWithParams) => stripUrlParams(urlWithParams) === url ? this.abort(urlWithParams) : null); 72 | return this; 73 | } 74 | 75 | abortAll() : this 76 | { 77 | this.store.forEach((store, urlWithParams) => this.abort(urlWithParams)); 78 | return this; 79 | } 80 | 81 | observe(urlWithParams : string) : Observable<[string, Store]> 82 | { 83 | return this.observeAll().pipe(filter(([ value ] : [ string, Store ]) => value === urlWithParams)); 84 | } 85 | 86 | observeMany(url : string) : Observable<[string, Store]> 87 | { 88 | return this.observeAll().pipe(filter(([ value ] : [ string, Store ]) => stripUrlParams(value) === url)); 89 | } 90 | 91 | observeAll() : Observable<[string, Store]> 92 | { 93 | return this.store.asObservable().pipe(mergeMap(value => from(value))); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/abort/abort.type.ts: -------------------------------------------------------------------------------- 1 | export type AbortSignal = 'STARTED' | 'ABORTED'; 2 | -------------------------------------------------------------------------------- /src/abort/index.ts: -------------------------------------------------------------------------------- 1 | export { AbortModule } from './abort.module'; 2 | export { AbortInterceptor } from './abort.interceptor'; 3 | export { AbortService } from './abort.service'; 4 | export { AbortSignal } from './abort.type'; 5 | -------------------------------------------------------------------------------- /src/cache/cache.interceptor.ts: -------------------------------------------------------------------------------- 1 | import 2 | { 3 | HttpEvent, 4 | HttpHandler, 5 | HttpInterceptor, 6 | HttpRequest, 7 | HttpResponse 8 | } from '@angular/common/http'; 9 | import { Injectable } from '@angular/core'; 10 | import { Observable, ReplaySubject, of } from 'rxjs'; 11 | import { filter, share, tap } from 'rxjs/operators'; 12 | 13 | import { Context } from './cache.interface'; 14 | import { CacheService } from './cache.service'; 15 | 16 | @Injectable() 17 | export class CacheInterceptor implements HttpInterceptor 18 | { 19 | constructor(protected cacheService : CacheService) 20 | { 21 | } 22 | 23 | intercept(request : HttpRequest, next : HttpHandler) : Observable> 24 | { 25 | const context : Context = request.context.get(this.cacheService.getToken()); 26 | const enableCache : boolean = (context.method === 'ANY' || context.method === request.method) && context.lifetime > 0; 27 | 28 | return enableCache ? this.handle(request, next) : next.handle(request); 29 | } 30 | 31 | protected handle(request : HttpRequest, next : HttpHandler) : Observable> 32 | { 33 | return this.cacheService.has(request) ? this.cacheService.get(request) : this.store(request, next); 34 | } 35 | 36 | protected store(request : HttpRequest, next : HttpHandler) : Observable> 37 | { 38 | const nextHandler : Observable> = next 39 | .handle(request) 40 | .pipe( 41 | filter(event => event instanceof HttpResponse), 42 | tap((response : HttpResponse) => this.cacheService.set(request, of(response))), 43 | share( 44 | { 45 | connector: () => new ReplaySubject() 46 | }) 47 | ); 48 | 49 | this.cacheService.set(request, nextHandler); 50 | return nextHandler; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/cache/cache.interface.ts: -------------------------------------------------------------------------------- 1 | import { HttpResponse } from '@angular/common/http'; 2 | import { Observable, Subscription } from 'rxjs'; 3 | 4 | import { UniversalMethod } from '../common'; 5 | 6 | export interface Store 7 | { 8 | response : Observable>; 9 | timer : Subscription; 10 | } 11 | 12 | export interface Context 13 | { 14 | method : UniversalMethod; 15 | lifetime : number; 16 | } 17 | -------------------------------------------------------------------------------- /src/cache/cache.module.ts: -------------------------------------------------------------------------------- 1 | import { HTTP_INTERCEPTORS } from '@angular/common/http'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { CacheInterceptor } from './cache.interceptor'; 5 | import { CacheService } from './cache.service'; 6 | 7 | @NgModule( 8 | { 9 | providers: 10 | [ 11 | CacheService, 12 | { 13 | multi: true, 14 | provide: HTTP_INTERCEPTORS, 15 | useClass: CacheInterceptor 16 | } 17 | ] 18 | }) 19 | export class CacheModule 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /src/cache/cache.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpContextToken, HttpRequest, HttpResponse } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable, Subscription, filter, from, timer, mergeMap } from 'rxjs'; 4 | import { ReactiveMap } from 'rxjs-collection'; 5 | 6 | import { stripUrlParams } from '../common'; 7 | 8 | import { Context, Store } from './cache.interface'; 9 | 10 | @Injectable() 11 | export class CacheService 12 | { 13 | protected defaultContext : Context = 14 | { 15 | method: null, 16 | lifetime: null 17 | }; 18 | 19 | protected token : HttpContextToken = new HttpContextToken(() => this.defaultContext); 20 | protected store : ReactiveMap = new ReactiveMap(); 21 | 22 | getToken() : HttpContextToken 23 | { 24 | return this.token; 25 | } 26 | 27 | get(request : HttpRequest) : Observable> 28 | { 29 | if (!this.has(request)) 30 | { 31 | return new Observable(observer => observer.error()); 32 | } 33 | return this.store.get(request.urlWithParams).response; 34 | } 35 | 36 | set(request : HttpRequest, response : Observable>) : this 37 | { 38 | const context : Context = request.context.get(this.getToken()); 39 | 40 | if (this.has(request)) 41 | { 42 | this.store.get(request.urlWithParams).timer.unsubscribe(); 43 | } 44 | this.store.set(request.urlWithParams, 45 | { 46 | response, 47 | timer: context.lifetime > 0 ? timer(context.lifetime).subscribe(() => this.flush(request.urlWithParams)) : new Subscription() 48 | }); 49 | return this; 50 | } 51 | 52 | has(request : HttpRequest) : boolean 53 | { 54 | return this.store.has(request.urlWithParams); 55 | } 56 | 57 | flush(urlWithParams : string) : this 58 | { 59 | if (this.store.has(urlWithParams)) 60 | { 61 | this.store.get(urlWithParams).timer.unsubscribe(); 62 | this.store.delete(urlWithParams); 63 | } 64 | return this; 65 | } 66 | 67 | flushMany(url : string) : this 68 | { 69 | this.store.forEach((store, urlWithParams) => stripUrlParams(urlWithParams) === url ? this.flush(urlWithParams) : null); 70 | return this; 71 | } 72 | 73 | flushAll() : this 74 | { 75 | this.store.forEach((store, urlWithParams) => this.flush(urlWithParams)); 76 | return this; 77 | } 78 | 79 | observe(urlWithParams : string) : Observable<[string, Store]> 80 | { 81 | return this.observeAll().pipe(filter(([ value ] : [ string, Store ]) => value === urlWithParams)); 82 | } 83 | 84 | observeMany(url : string) : Observable<[string, Store]> 85 | { 86 | return this.observeAll().pipe(filter(([ value ] : [ string, Store ]) => stripUrlParams(value) === url)); 87 | } 88 | 89 | observeAll() : Observable<[string, Store]> 90 | { 91 | return this.store.asObservable().pipe(mergeMap(value => from(value))); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/cache/index.ts: -------------------------------------------------------------------------------- 1 | export { CacheModule } from './cache.module'; 2 | export { CacheInterceptor } from './cache.interceptor'; 3 | export { CacheService } from './cache.service'; 4 | -------------------------------------------------------------------------------- /src/common/common.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Constructor } from './common.interface'; 2 | 3 | export function ApiUrl(apiUrl : string) : Function 4 | { 5 | return (constructor : Constructor) => 6 | { 7 | constructor.prototype.setApiUrl(apiUrl); 8 | }; 9 | } 10 | 11 | export function ApiRoute(apiRoute : string) : Function 12 | { 13 | return (constructor : Constructor) => 14 | { 15 | constructor.prototype.setApiRoute(apiRoute); 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/common/common.helper.ts: -------------------------------------------------------------------------------- 1 | import { Id } from './common.type'; 2 | 3 | export function createUrl(apiUrl : string, apiRoute : string) : string 4 | { 5 | const route : string = 6 | [ 7 | apiRoute 8 | ] 9 | .filter(value => value) 10 | .join('/'); 11 | 12 | return apiUrl + route; 13 | } 14 | 15 | export function createUrlWithId(apiUrl : string, apiRoute : string, id : Id) : string 16 | { 17 | const route : string = 18 | [ 19 | apiRoute, 20 | id 21 | ] 22 | .filter(value => value) 23 | .join('/'); 24 | 25 | return apiUrl + route; 26 | } 27 | 28 | export function stripUrlParams(urlWithParams : string) : string 29 | { 30 | return urlWithParams?.split('?').at(0); 31 | } 32 | -------------------------------------------------------------------------------- /src/common/common.interface.ts: -------------------------------------------------------------------------------- 1 | import { HttpContext, HttpHeaders, HttpParams } from '@angular/common/http'; 2 | 3 | export interface Options 4 | { 5 | context ?: HttpContext; 6 | headers ?: HttpHeaders; 7 | params ?: HttpParams; 8 | observe ?: any; 9 | reportProgress ?: boolean; 10 | responseType ?: any; 11 | withCredentials ?: boolean; 12 | } 13 | 14 | export interface OptionsWithBody extends Options 15 | { 16 | body ?: ResponseBody; 17 | } 18 | 19 | export interface Context 20 | { 21 | [index : string] : any; 22 | } 23 | 24 | export interface Constructor 25 | { 26 | prototype : 27 | { 28 | setApiUrl : (apiUrl : string) => void; 29 | setApiRoute : (apiRoute : string) => void; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/common/common.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient, HttpContext, HttpContextToken, HttpHeaders, HttpParams } from '@angular/common/http'; 2 | import { Injectable, Injector } from '@angular/core'; 3 | 4 | import { AbortService } from '../abort'; 5 | import { CacheService } from '../cache'; 6 | import { ObserveService } from '../observe'; 7 | 8 | import { Context, Options } from './common.interface'; 9 | import { UniversalMethod } from './common.type'; 10 | import { createUrl } from './common.helper'; 11 | 12 | @Injectable() 13 | export class CommonService 14 | { 15 | protected httpClient : HttpClient = this.injector.get(HttpClient); 16 | protected abortService : AbortService = this.injector.get(AbortService); 17 | protected cacheService : CacheService = this.injector.get(CacheService); 18 | protected observeService : ObserveService = this.injector.get(ObserveService); 19 | protected apiUrl : string; 20 | protected apiRoute : string; 21 | protected options : Options; 22 | 23 | constructor(protected injector : Injector) 24 | { 25 | this.init(); 26 | } 27 | 28 | bind(that : CommonService) : this 29 | { 30 | return this 31 | .setApiUrl(that.getApiUrl()) 32 | .setApiRoute(that.getApiRoute()) 33 | .setOptions(that.getOptions()); 34 | } 35 | 36 | clone() : this 37 | { 38 | return new (this.constructor as new (injector) => this)(this.injector); 39 | } 40 | 41 | clear() : this 42 | { 43 | return this 44 | .clearOptions() 45 | .clearContext() 46 | .clearHeaders() 47 | .clearParams(); 48 | } 49 | 50 | destroy() : this 51 | { 52 | return this 53 | .abort() 54 | .flush() 55 | .clear(); 56 | } 57 | 58 | getApiUrl() : string 59 | { 60 | return this.apiUrl; 61 | } 62 | 63 | setApiUrl(apiUrl : string) : this 64 | { 65 | this.apiUrl = apiUrl; 66 | return this; 67 | } 68 | 69 | getApiRoute() : string 70 | { 71 | return this.apiRoute; 72 | } 73 | 74 | setApiRoute(apiRoute : string) : this 75 | { 76 | this.apiRoute = apiRoute; 77 | return this; 78 | } 79 | 80 | getOption(name : keyof Options) : Options[keyof Options] 81 | { 82 | return this.options[name]; 83 | } 84 | 85 | getOptions() : Options 86 | { 87 | return this.options; 88 | } 89 | 90 | setOption(name : keyof Options, value : Options[keyof Options]) : this 91 | { 92 | this.options[name] = value; 93 | return this; 94 | } 95 | 96 | setOptions(options : Options) : this 97 | { 98 | this.options = options; 99 | return this; 100 | } 101 | 102 | clearOption(name : keyof Options) : this 103 | { 104 | return this.setOption(name, null); 105 | } 106 | 107 | clearOptions() : this 108 | { 109 | return this.setOptions( 110 | { 111 | reportProgress: true 112 | }); 113 | } 114 | 115 | getContextByToken(token : HttpContextToken) : HttpContext 116 | { 117 | return this.getContext().get(token) as HttpContext; 118 | } 119 | 120 | getContext() : HttpContext 121 | { 122 | return this.getOption('context'); 123 | } 124 | 125 | setContextByToken(token : HttpContextToken, context : Context) : this 126 | { 127 | return this.setContext(this.getContext().set(token, context)); 128 | } 129 | 130 | setContext(context : HttpContext) : this 131 | { 132 | this.setOption('context', context); 133 | return this; 134 | } 135 | 136 | clearContextByToken(token : HttpContextToken) : this 137 | { 138 | return this.setContext(this.getContext().delete(token)); 139 | } 140 | 141 | clearContext() : this 142 | { 143 | return this.setContext(new HttpContext()); 144 | } 145 | 146 | getHeader(name : string) : string 147 | { 148 | return this.getHeaders().get(name); 149 | } 150 | 151 | getHeaders() : HttpHeaders 152 | { 153 | return this.getOption('headers'); 154 | } 155 | 156 | getHeaderArray(name : string) : string[] 157 | { 158 | return this.getHeaders().getAll(name); 159 | } 160 | 161 | setHeader(name : string, value : string) : this 162 | { 163 | return this.setHeaders(this.getHeaders().set(name, value)); 164 | } 165 | 166 | setHeaders(headers : HttpHeaders) : this 167 | { 168 | return this.setOption('headers', headers); 169 | } 170 | 171 | setHeaderArray(name : string, valueArray : string[]) : this 172 | { 173 | return this.setHeaders(this.getHeaders().set(name, valueArray)); 174 | } 175 | 176 | appendHeader(name : string, value : string) : this 177 | { 178 | return this.setHeaders(this.getHeaders().append(name, value)); 179 | } 180 | 181 | appendHeaderArray(name : string, valueArray : string[]) : this 182 | { 183 | return this.setHeaders(this.getHeaders().append(name, valueArray)); 184 | } 185 | 186 | clearHeader(name : string) : this 187 | { 188 | return this.setHeaders(this.getHeaders().delete(name)); 189 | } 190 | 191 | clearHeaders() : this 192 | { 193 | return this.setHeaders(new HttpHeaders()); 194 | } 195 | 196 | getParam(name : string) : string 197 | { 198 | return this.getParams().get(name); 199 | } 200 | 201 | getParams() : HttpParams 202 | { 203 | return this.getOption('params'); 204 | } 205 | 206 | getParamArray(name : string) : string[] 207 | { 208 | return this.getParams().getAll(name); 209 | } 210 | 211 | setParam(name : string, value : string) : this 212 | { 213 | return this.setParams(this.getParams().set(name, value)); 214 | } 215 | 216 | setParams(params : HttpParams) : this 217 | { 218 | return this.setOption('params', params); 219 | } 220 | 221 | setParamArray(name : string, valueArray : string[]) : this 222 | { 223 | this.clearParam(name); 224 | valueArray.forEach(value => this.appendParam(name, value)); 225 | return this; 226 | } 227 | 228 | appendParam(name : string, value : string) : this 229 | { 230 | return this.setParams(this.getParams().append(name, value)); 231 | } 232 | 233 | appendParamArray(name : string, valueArray : string[]) : this 234 | { 235 | valueArray.forEach(value => this.appendParam(name, value)); 236 | return this; 237 | } 238 | 239 | clearParam(name : string) : this 240 | { 241 | return this.setParams(this.getParams().delete(name)); 242 | } 243 | 244 | clearParams() : this 245 | { 246 | return this.setParams(new HttpParams()); 247 | } 248 | 249 | enableAbort(method : UniversalMethod = 'GET', lifetime : number = 2000) : this 250 | { 251 | return this.setContextByToken(this.abortService.getToken(), 252 | { 253 | method, 254 | lifetime 255 | }); 256 | } 257 | 258 | disableAbort() : this 259 | { 260 | return this.clearContextByToken(this.abortService.getToken()); 261 | } 262 | 263 | abort() : this 264 | { 265 | const url : string = createUrl(this.getApiUrl(), this.getApiRoute()); 266 | 267 | this.abortService.abortMany(url); 268 | return this; 269 | } 270 | 271 | enableCache(method : UniversalMethod = 'GET', lifetime : number = 2000) : this 272 | { 273 | return this.setContextByToken(this.cacheService.getToken(), 274 | { 275 | method, 276 | lifetime 277 | }); 278 | } 279 | 280 | disableCache() : this 281 | { 282 | return this.clearContextByToken(this.cacheService.getToken()); 283 | } 284 | 285 | flush() : this 286 | { 287 | const url : string = createUrl(this.getApiUrl(), this.getApiRoute()); 288 | 289 | this.cacheService.flushMany(url); 290 | return this; 291 | } 292 | 293 | enableObserve(method : UniversalMethod = 'ANY', lifetime : number = 1000) : this 294 | { 295 | return this.setContextByToken(this.observeService.getToken(), 296 | { 297 | method, 298 | lifetime 299 | }); 300 | } 301 | 302 | disableObserve() : this 303 | { 304 | return this.clearContextByToken(this.observeService.getToken()); 305 | } 306 | 307 | getHttpClient() : HttpClient 308 | { 309 | return this.httpClient; 310 | } 311 | 312 | protected init() : this 313 | { 314 | return this.clear(); 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /src/common/common.type.ts: -------------------------------------------------------------------------------- 1 | export type Id = number | string; 2 | 3 | export type Method = 'DELETE' | 'GET' | 'HEAD' | 'OPTIONS' | 'PATCH' | 'POST' | 'PUT'; 4 | 5 | export type UniversalMethod = 'ANY' | 'DELETE' | 'GET' | 'HEAD' | 'OPTIONS' | 'PATCH' | 'POST' | 'PUT'; 6 | 7 | -------------------------------------------------------------------------------- /src/common/index.ts: -------------------------------------------------------------------------------- 1 | export { Context, Options, OptionsWithBody } from './common.interface'; 2 | export { CommonService } from './common.service'; 3 | export { Id, Method, UniversalMethod } from './common.type'; 4 | export { ApiUrl, ApiRoute } from './common.decorator'; 5 | export { createUrl, createUrlWithId, stripUrlParams } from './common.helper'; 6 | -------------------------------------------------------------------------------- /src/core/create.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | 4 | import { CommonService, Options, createUrl } from '../common'; 5 | 6 | import { NoInfer } from './crud.type'; 7 | 8 | @Injectable() 9 | export class CreateService extends CommonService 10 | { 11 | create< 12 | RequestBody = CreateRequestBody, 13 | ResponseBody = CreateResponseBody 14 | >(body : NoInfer, options ?: Options) : Observable 15 | { 16 | return this.httpClient.post(createUrl(this.getApiUrl(), this.getApiRoute()), body, 17 | { 18 | ...this.getOptions(), 19 | ...options 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/core/crud.interface.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | 3 | import { Options, OptionsWithBody, Id, Method } from '../common'; 4 | 5 | import { NoInfer } from './crud.type'; 6 | 7 | export interface Crud< 8 | CreateRequestBody, 9 | CreateResponseBody, 10 | ReadResponseBody, 11 | FindResponseBody, 12 | UpdateRequestBody, 13 | UpdateResponseBody, 14 | PatchRequestBody, 15 | PatchResponseBody, 16 | DeleteResponseBody, 17 | CustomRequestBody, 18 | CustomResponseBody 19 | > 20 | { 21 | create< 22 | RequestBody = CreateRequestBody, 23 | ResponseBody = CreateResponseBody 24 | >(body : NoInfer, options ?: Options) : Observable; 25 | read< 26 | ResponseBody = ReadResponseBody 27 | >(id : Id, options ?: Options) : Observable 28 | find< 29 | ResponseBody = FindResponseBody 30 | >(options ?: Options) : Observable; 31 | update< 32 | RequestBody = UpdateRequestBody, 33 | ResponseBody = UpdateResponseBody 34 | >(id : Id, body : NoInfer, options ?: Options) : Observable 35 | patch< 36 | RequestBody = PatchRequestBody, 37 | ResponseBody = PatchResponseBody 38 | >(id : Id, body : NoInfer, options ?: Options) : Observable 39 | delete< 40 | ResponseBody = DeleteResponseBody 41 | >(id : Id, options ?: Options) : Observable 42 | custom< 43 | RequestBody = CustomRequestBody, 44 | ResponseBody = CustomResponseBody 45 | >(method : Method, options ?: OptionsWithBody>) : Observable 46 | } 47 | -------------------------------------------------------------------------------- /src/core/crud.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { AbortModule } from '../abort'; 4 | import { CacheModule } from '../cache'; 5 | import { ObserveModule } from '../observe'; 6 | 7 | import { DeleteService } from './delete.service'; 8 | import { FindService } from './find.service'; 9 | import { ReadService } from './read.service'; 10 | import { PatchService } from './patch.service'; 11 | import { CreateService } from './create.service'; 12 | import { UpdateService } from './update.service'; 13 | import { CustomService } from './custom.service'; 14 | 15 | @NgModule( 16 | { 17 | imports: 18 | [ 19 | AbortModule, 20 | CacheModule, 21 | ObserveModule 22 | ], 23 | providers: 24 | [ 25 | CreateService, 26 | ReadService, 27 | FindService, 28 | UpdateService, 29 | PatchService, 30 | DeleteService, 31 | CustomService 32 | ] 33 | }) 34 | export class CrudModule 35 | { 36 | } 37 | -------------------------------------------------------------------------------- /src/core/crud.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | 4 | import { CommonService, Options, OptionsWithBody, Id, Method } from '../common'; 5 | 6 | import { CreateService } from './create.service'; 7 | import { ReadService } from './read.service'; 8 | import { FindService } from './find.service'; 9 | import { UpdateService } from './update.service'; 10 | import { PatchService } from './patch.service'; 11 | import { DeleteService } from './delete.service'; 12 | import { CustomService } from './custom.service'; 13 | import { Crud } from './crud.interface'; 14 | import { NoInfer } from './crud.type'; 15 | 16 | @Injectable() 17 | export class CrudService< 18 | RequestBody, 19 | ResponseBody, 20 | CreateRequestBody = RequestBody, 21 | CreateResponseBody = ResponseBody, 22 | ReadResponseBody = ResponseBody, 23 | FindResponseBody = ResponseBody[], 24 | UpdateRequestBody = RequestBody, 25 | UpdateResponseBody = ResponseBody, 26 | PatchRequestBody = Partial, 27 | PatchResponseBody = ResponseBody, 28 | DeleteResponseBody = ResponseBody, 29 | CustomRequestBody = RequestBody, 30 | CustomResponseBody = ResponseBody | ResponseBody[] 31 | > extends CommonService implements Crud< 32 | CreateRequestBody, 33 | CreateResponseBody, 34 | ReadResponseBody, 35 | FindResponseBody, 36 | UpdateRequestBody, 37 | UpdateResponseBody, 38 | PatchRequestBody, 39 | PatchResponseBody, 40 | DeleteResponseBody, 41 | CustomRequestBody, 42 | CustomResponseBody 43 | > 44 | { 45 | protected createService : CreateService = this.injector.get(CreateService); 46 | protected readService : ReadService = this.injector.get(ReadService); 47 | protected findService : FindService = this.injector.get(FindService); 48 | protected updateService : UpdateService = this.injector.get(UpdateService); 49 | protected patchService : PatchService = this.injector.get(PatchService); 50 | protected deleteService : DeleteService = this.injector.get(DeleteService); 51 | protected customService : CustomService = this.injector.get(CustomService); 52 | 53 | create< 54 | RequestBody = CreateRequestBody, 55 | ResponseBody = CreateResponseBody 56 | >(body : NoInfer, options ?: Options) : Observable 57 | { 58 | return this.createService.bind(this).create(body, options); 59 | } 60 | 61 | read< 62 | ResponseBody = ReadResponseBody 63 | >(id : Id, options ?: Options) : Observable 64 | { 65 | return this.readService.bind(this).read(id, options); 66 | } 67 | 68 | find< 69 | ResponseBody = FindResponseBody 70 | >(options ?: Options) : Observable 71 | { 72 | return this.findService.bind(this).find(options); 73 | } 74 | 75 | update< 76 | RequestBody = UpdateRequestBody, 77 | ResponseBody = UpdateResponseBody 78 | >(id : Id, body : NoInfer, options ?: Options) : Observable 79 | { 80 | return this.updateService.bind(this).update(id, body, options); 81 | } 82 | 83 | patch< 84 | RequestBody = PatchRequestBody, 85 | ResponseBody = PatchResponseBody 86 | >(id : Id, body : NoInfer, options ?: Options) : Observable 87 | { 88 | return this.patchService.bind(this).patch(id, body, options); 89 | } 90 | 91 | delete< 92 | ResponseBody = DeleteResponseBody 93 | >(id : Id, options ?: Options) : Observable 94 | { 95 | return this.deleteService.bind(this).delete(id, options); 96 | } 97 | 98 | custom< 99 | RequestBody = CustomRequestBody, 100 | ResponseBody = CustomResponseBody 101 | >(method : Method, options ?: OptionsWithBody>) : Observable 102 | { 103 | return this.customService.bind(this).custom(method, options); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/core/crud.type.ts: -------------------------------------------------------------------------------- 1 | export type NoInfer = [T][T extends unknown ? 0 : never]; 2 | -------------------------------------------------------------------------------- /src/core/custom.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | 4 | import { CommonService, OptionsWithBody, Method, createUrl } from '../common'; 5 | 6 | import { NoInfer } from './crud.type'; 7 | 8 | @Injectable() 9 | export class CustomService extends CommonService 10 | { 11 | custom< 12 | RequestBody = CustomRequestBody, 13 | ResponseBody = CustomResponseBody 14 | >(method : Method, options ?: OptionsWithBody>) : Observable 15 | { 16 | return this.httpClient.request(method, createUrl(this.getApiUrl(), this.getApiRoute()), 17 | { 18 | ...this.getOptions(), 19 | ...options 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/core/delete.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | 4 | import { CommonService, Options, Id, createUrlWithId } from '../common'; 5 | 6 | @Injectable() 7 | export class DeleteService extends CommonService 8 | { 9 | delete< 10 | ResponseBody = DeleteResponseBody 11 | >(id : Id, options ?: Options) : Observable 12 | { 13 | return this.httpClient.delete(createUrlWithId(this.getApiUrl(), this.getApiRoute(), id), 14 | { 15 | ...this.getOptions(), 16 | ...options 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/core/find.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | 4 | import { CommonService, Options, createUrl } from '../common'; 5 | 6 | @Injectable() 7 | export class FindService extends CommonService 8 | { 9 | find< 10 | ResponseBody = FindResponseBody 11 | >(options ?: Options) : Observable 12 | { 13 | return this.httpClient.get(createUrl(this.getApiUrl(), this.getApiRoute()), 14 | { 15 | ...this.getOptions(), 16 | ...options 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/core/index.ts: -------------------------------------------------------------------------------- 1 | export { CrudModule } from './crud.module'; 2 | export { Crud } from './crud.interface'; 3 | export { CrudService } from './crud.service'; 4 | export { CreateService } from './create.service'; 5 | export { ReadService } from './read.service'; 6 | export { FindService } from './find.service'; 7 | export { UpdateService } from './update.service'; 8 | export { PatchService } from './patch.service'; 9 | export { DeleteService } from './delete.service'; 10 | export { CustomService } from './custom.service'; 11 | -------------------------------------------------------------------------------- /src/core/patch.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | 4 | import { CommonService, Options, Id, createUrlWithId } from '../common'; 5 | 6 | import { NoInfer } from './crud.type'; 7 | 8 | @Injectable() 9 | export class PatchService extends CommonService 10 | { 11 | patch< 12 | RequestBody = PatchRequestBody, 13 | ResponseBody = PatchResponseBody 14 | >(id : Id, body : NoInfer, options ?: Options) : Observable 15 | { 16 | return this.httpClient.patch(createUrlWithId(this.getApiUrl(), this.getApiRoute(), id), body, 17 | { 18 | ...this.getOptions(), 19 | ...options 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/core/read.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | 4 | import { CommonService, Options, Id, createUrlWithId } from '../common'; 5 | 6 | @Injectable() 7 | export class ReadService extends CommonService 8 | { 9 | read< 10 | ResponseBody = ReadResponseBody 11 | >(id : Id, options ?: Options) : Observable 12 | { 13 | return this.httpClient.get(createUrlWithId(this.getApiUrl(), this.getApiRoute(), id), 14 | { 15 | ...this.getOptions(), 16 | ...options 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/core/update.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | 4 | import { CommonService, Options, Id, createUrlWithId } from '../common'; 5 | 6 | import { NoInfer } from './crud.type'; 7 | 8 | @Injectable() 9 | export class UpdateService extends CommonService 10 | { 11 | update< 12 | RequestBody = UpdateRequestBody, 13 | ResponseBody = UpdateResponseBody 14 | >(id : Id, body : NoInfer, options ?: Options) : Observable 15 | { 16 | return this.httpClient.put(createUrlWithId(this.getApiUrl(), this.getApiRoute(), id), body, 17 | { 18 | ...this.getOptions(), 19 | ...options 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './abort'; 2 | export * from './cache'; 3 | export * from './common'; 4 | export * from './core'; 5 | export * from './observe'; 6 | -------------------------------------------------------------------------------- /src/observe/index.ts: -------------------------------------------------------------------------------- 1 | export { ObserveModule } from './observe.module'; 2 | export { ObserveInterceptor } from './observe.interceptor'; 3 | export { ObserveAfterEffect, ObserveBeforeEffect } from './observe.interface'; 4 | export { ObserveService } from './observe.service'; 5 | export { OBSERVE_EFFECT } from './observe.token'; 6 | export { ObserveStatus } from './observe.type'; 7 | -------------------------------------------------------------------------------- /src/observe/observe.interceptor.ts: -------------------------------------------------------------------------------- 1 | import 2 | { 3 | HttpErrorResponse, 4 | HttpEvent, 5 | HttpHandler, 6 | HttpInterceptor, 7 | HttpRequest, 8 | HttpResponse 9 | } from '@angular/common/http'; 10 | import { Injectable } from '@angular/core'; 11 | import { Observable, throwError } from 'rxjs'; 12 | import { catchError, filter, tap } from 'rxjs/operators'; 13 | 14 | import { Context } from './observe.interface'; 15 | import { ObserveService } from './observe.service'; 16 | 17 | @Injectable() 18 | export class ObserveInterceptor implements HttpInterceptor 19 | { 20 | constructor(protected observeService : ObserveService) 21 | { 22 | } 23 | 24 | intercept(request : HttpRequest, next : HttpHandler) : Observable> 25 | { 26 | const context : Context = request.context.get(this.observeService.getToken()); 27 | const enableObserve : boolean = (context.method === 'ANY' || context.method === request.method) && context.lifetime > 0; 28 | 29 | return enableObserve ? this.handle(request, next) : next.handle(request); 30 | } 31 | 32 | handle(request : HttpRequest, next : HttpHandler) : Observable> 33 | { 34 | this.observeService.start(request); 35 | return next 36 | .handle(this.observeService.before(request)) 37 | .pipe( 38 | filter(event => event instanceof HttpResponse), 39 | tap((response : HttpResponse) => 40 | { 41 | this.observeService.after(request, response); 42 | this.observeService.complete(request.urlWithParams); 43 | }), 44 | catchError((response : HttpErrorResponse) => 45 | { 46 | this.observeService.after(request, response); 47 | this.observeService.error(request.urlWithParams); 48 | return throwError(() => response); 49 | }) 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/observe/observe.interface.ts: -------------------------------------------------------------------------------- 1 | import { HttpErrorResponse, HttpRequest, HttpResponse } from '@angular/common/http'; 2 | import { BehaviorSubject, Subscription } from 'rxjs'; 3 | 4 | import { UniversalMethod } from '../common'; 5 | 6 | import { ObserveStatus } from './observe.type'; 7 | 8 | export interface Store 9 | { 10 | status : BehaviorSubject; 11 | timer : Subscription; 12 | } 13 | 14 | export interface Context 15 | { 16 | method : UniversalMethod; 17 | lifetime : number; 18 | } 19 | 20 | export interface ObserveBeforeEffect 21 | { 22 | before(request : HttpRequest) : HttpRequest; 23 | } 24 | 25 | export interface ObserveAfterEffect 26 | { 27 | after(request : HttpRequest, response : HttpResponse | HttpErrorResponse) : void 28 | } 29 | -------------------------------------------------------------------------------- /src/observe/observe.module.ts: -------------------------------------------------------------------------------- 1 | import { HTTP_INTERCEPTORS } from '@angular/common/http'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { ObserveInterceptor } from './observe.interceptor'; 5 | import { ObserveService } from './observe.service'; 6 | 7 | @NgModule( 8 | { 9 | providers: 10 | [ 11 | ObserveService, 12 | { 13 | multi: true, 14 | provide: HTTP_INTERCEPTORS, 15 | useClass: ObserveInterceptor 16 | } 17 | ] 18 | }) 19 | export class ObserveModule 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /src/observe/observe.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpContextToken, HttpErrorResponse, HttpRequest, HttpResponse } from '@angular/common/http'; 2 | import { Optional, Inject, Injectable } from '@angular/core'; 3 | import { Observable, BehaviorSubject, Subscription, filter, from, timer, mergeMap } from 'rxjs'; 4 | import { ReactiveMap } from 'rxjs-collection'; 5 | 6 | import { stripUrlParams } from '../common'; 7 | 8 | import { ObserveAfterEffect, ObserveBeforeEffect, Context, Store } from './observe.interface'; 9 | import { OBSERVE_EFFECT } from './observe.token'; 10 | import { ObserveStatus } from './observe.type'; 11 | 12 | @Injectable() 13 | export class ObserveService 14 | { 15 | protected defaultContext : Context = 16 | { 17 | method: null, 18 | lifetime: null 19 | }; 20 | 21 | protected token : HttpContextToken = new HttpContextToken(() => this.defaultContext); 22 | protected store : ReactiveMap = new ReactiveMap(); 23 | 24 | constructor(@Optional() @Inject(OBSERVE_EFFECT) protected observeEffect : ObserveBeforeEffect | ObserveAfterEffect) 25 | { 26 | } 27 | 28 | getToken() : HttpContextToken 29 | { 30 | return this.token; 31 | } 32 | 33 | start(request : HttpRequest) : this 34 | { 35 | const context : Context = request.context.get(this.getToken()); 36 | 37 | if (this.has(request)) 38 | { 39 | this.store.get(request.urlWithParams).timer.unsubscribe(); 40 | } 41 | this.store.set(request.urlWithParams, 42 | { 43 | status: new BehaviorSubject('STARTED'), 44 | timer: context.lifetime > 0 ? timer(context.lifetime).subscribe(() => this.complete(request.urlWithParams)) : new Subscription() 45 | }); 46 | return this; 47 | } 48 | 49 | before(request : HttpRequest) : HttpRequest 50 | { 51 | if (this.observeEffect && 'before' in this.observeEffect) 52 | { 53 | return this.observeEffect.before(request); 54 | } 55 | return request; 56 | } 57 | 58 | after(request : HttpRequest, response : HttpResponse | HttpErrorResponse) : this 59 | { 60 | if (this.observeEffect && 'after' in this.observeEffect) 61 | { 62 | this.observeEffect.after(request, response); 63 | } 64 | return this; 65 | } 66 | 67 | has(request : HttpRequest) : boolean 68 | { 69 | return this.store.has(request.urlWithParams); 70 | } 71 | 72 | error(urlWithParams : string) : this 73 | { 74 | if (this.store.has(urlWithParams)) 75 | { 76 | this.store.get(urlWithParams).status.next('ERRORED'); 77 | this.store.get(urlWithParams).timer.unsubscribe(); 78 | } 79 | return this; 80 | } 81 | 82 | complete(urlWithParams : string) : this 83 | { 84 | if (this.store.has(urlWithParams)) 85 | { 86 | this.store.get(urlWithParams).status.next('COMPLETED'); 87 | this.store.get(urlWithParams).timer.unsubscribe(); 88 | } 89 | return this; 90 | } 91 | 92 | completeMany(url : string) : this 93 | { 94 | this.store.forEach((store, urlWithParams) => stripUrlParams(urlWithParams) === url ? this.complete(urlWithParams) : null); 95 | return this; 96 | } 97 | 98 | completeAll() : this 99 | { 100 | this.store.forEach((store, urlWithParams) => this.complete(urlWithParams)); 101 | return this; 102 | } 103 | 104 | observe(urlWithParams : string) : Observable<[string, Store]> 105 | { 106 | return this.observeAll().pipe(filter(([ value ] : [ string, Store ]) => value === urlWithParams)); 107 | } 108 | 109 | observeMany(url : string) : Observable<[string, Store]> 110 | { 111 | return this.observeAll().pipe(filter(([ value ] : [ string, Store ]) => stripUrlParams(value) === url)); 112 | } 113 | 114 | observeAll() : Observable<[string, Store]> 115 | { 116 | return this.store.asObservable().pipe(mergeMap(value => from(value))); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/observe/observe.token.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | 3 | import { ObserveAfterEffect, ObserveBeforeEffect } from './observe.interface'; 4 | 5 | export const OBSERVE_EFFECT : InjectionToken = new InjectionToken('NGX_CRUD__OBSERVE_EFFECT'); 6 | -------------------------------------------------------------------------------- /src/observe/observe.type.ts: -------------------------------------------------------------------------------- 1 | export type ObserveStatus = 'STARTED' | 'COMPLETED' | 'ERRORED'; 2 | -------------------------------------------------------------------------------- /tests/abort.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; 2 | import { filter, take } from 'rxjs/operators'; 3 | import { inject, TestBed } from '@angular/core/testing'; 4 | import { expect } from 'chai'; 5 | 6 | import { CrudModule, AbortService } from '../src'; 7 | 8 | import { TestService } from './test.service'; 9 | import { mockRequest } from './test.helper'; 10 | 11 | before(() => 12 | { 13 | TestBed 14 | .configureTestingModule( 15 | { 16 | imports: 17 | [ 18 | CrudModule 19 | ], 20 | providers: 21 | [ 22 | provideHttpClient(withInterceptorsFromDi()), 23 | AbortService, 24 | TestService 25 | ] 26 | }); 27 | }); 28 | 29 | describe(AbortService.name, () => 30 | { 31 | it('enable and disable', () => 32 | { 33 | inject( 34 | [ 35 | AbortService, 36 | TestService 37 | ], (abortService : AbortService, testService : TestService) => 38 | { 39 | testService.enableAbort(); 40 | expect(testService.getContext().get(abortService.getToken()).method).to.be.equal('GET'); 41 | expect(testService.getContext().get(abortService.getToken()).lifetime).to.be.equal(2000); 42 | testService.disableAbort(); 43 | expect(testService.getContext().get(abortService.getToken()).method).to.be.equal(null); 44 | expect(testService.getContext().get(abortService.getToken()).lifetime).to.be.equal(null); 45 | }); 46 | }); 47 | 48 | it('natural abort', done => 49 | { 50 | inject( 51 | [ 52 | AbortService, 53 | TestService 54 | ], (abortService : AbortService, testService : TestService) => 55 | { 56 | testService 57 | .enableAbort('GET', 1) 58 | .setParam('abort', '1') 59 | .find() 60 | .subscribe(() => 61 | { 62 | testService.clear(); 63 | done('error'); 64 | }); 65 | abortService 66 | .get(mockRequest(testService)) 67 | .pipe( 68 | filter(signal => signal === 'ABORTED') 69 | ) 70 | .subscribe(() => 71 | { 72 | testService.clear(); 73 | done(); 74 | }); 75 | })(); 76 | }); 77 | 78 | it('programmatic abort', done => 79 | { 80 | inject( 81 | [ 82 | AbortService, 83 | TestService 84 | ], (abortService : AbortService, testService : TestService) => 85 | { 86 | testService 87 | .enableAbort() 88 | .setParam('abort', '2') 89 | .find() 90 | .subscribe(() => 91 | { 92 | testService.clear(); 93 | done('error'); 94 | }); 95 | testService.abort(); 96 | abortService 97 | .get(mockRequest(testService)) 98 | .pipe( 99 | filter(signal => signal === 'ABORTED') 100 | ) 101 | .subscribe(() => 102 | { 103 | testService.clear(); 104 | done(); 105 | }); 106 | })(); 107 | }); 108 | 109 | it('programmatic abort many', done => 110 | { 111 | inject( 112 | [ 113 | AbortService, 114 | TestService 115 | ], (abortService : AbortService, testService : TestService) => 116 | { 117 | testService 118 | .enableAbort() 119 | .setParam('abort', '3') 120 | .find() 121 | .subscribe(() => 122 | { 123 | testService.clear(); 124 | done('error'); 125 | }); 126 | abortService 127 | .abortMany('https://jsonplaceholder.typicode.com/posts') 128 | .get(mockRequest(testService)) 129 | .pipe( 130 | filter(signal => signal === 'ABORTED') 131 | ) 132 | .subscribe(() => 133 | { 134 | testService.clear(); 135 | done(); 136 | }); 137 | })(); 138 | }); 139 | 140 | it('programmatic abort all', done => 141 | { 142 | inject( 143 | [ 144 | AbortService, 145 | TestService 146 | ], (abortService : AbortService, testService : TestService) => 147 | { 148 | testService 149 | .enableAbort() 150 | .setParam('abort', '4') 151 | .find() 152 | .subscribe(() => 153 | { 154 | testService.clear(); 155 | done('error'); 156 | }); 157 | abortService 158 | .abortAll() 159 | .get(mockRequest(testService)) 160 | .pipe( 161 | filter(signal => signal === 'ABORTED') 162 | ) 163 | .subscribe(() => 164 | { 165 | testService.clear(); 166 | done(); 167 | }); 168 | })(); 169 | }); 170 | 171 | it('observe', done => 172 | { 173 | inject( 174 | [ 175 | AbortService, 176 | TestService 177 | ], (abortService : AbortService, testService : TestService) => 178 | { 179 | abortService 180 | .observe('https://jsonplaceholder.typicode.com/posts?abort=5') 181 | .pipe(take(1)) 182 | .subscribe( 183 | { 184 | next: value => 185 | { 186 | expect(value.length).to.be.above(0); 187 | testService.clear(); 188 | done(); 189 | }, 190 | error: () => 191 | { 192 | testService.clear(); 193 | done('error'); 194 | } 195 | }); 196 | testService 197 | .enableAbort() 198 | .setParam('abort', '5') 199 | .find() 200 | .subscribe(); 201 | })(); 202 | }); 203 | 204 | it('observe many', done => 205 | { 206 | inject( 207 | [ 208 | AbortService, 209 | TestService 210 | ], (abortService : AbortService, testService : TestService) => 211 | { 212 | abortService 213 | .observeMany('https://jsonplaceholder.typicode.com/posts') 214 | .pipe(take(1)) 215 | .subscribe( 216 | { 217 | next: value => 218 | { 219 | expect(value.length).to.be.above(0); 220 | testService.clear(); 221 | done(); 222 | }, 223 | error: () => 224 | { 225 | testService.clear(); 226 | done('error'); 227 | } 228 | }); 229 | testService 230 | .enableAbort() 231 | .setParam('abort', '6') 232 | .find() 233 | .subscribe(); 234 | })(); 235 | }); 236 | 237 | it('observe all', done => 238 | { 239 | inject( 240 | [ 241 | AbortService, 242 | TestService 243 | ], (abortService : AbortService, testService : TestService) => 244 | { 245 | abortService 246 | .observeAll() 247 | .pipe(take(1)) 248 | .subscribe( 249 | { 250 | next: value => 251 | { 252 | expect(value.length).to.be.above(0); 253 | testService.clear(); 254 | done(); 255 | }, 256 | error: () => 257 | { 258 | testService.clear(); 259 | done('error'); 260 | } 261 | }); 262 | testService 263 | .enableAbort() 264 | .setParam('abort', '7') 265 | .find() 266 | .subscribe(); 267 | })(); 268 | }); 269 | }); 270 | -------------------------------------------------------------------------------- /tests/cache.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; 2 | import { concatMap, delay, take, tap } from 'rxjs/operators'; 3 | import { inject, TestBed } from '@angular/core/testing'; 4 | import { expect } from 'chai'; 5 | 6 | import { CrudModule, CacheService } from '../src'; 7 | 8 | import { TestService } from './test.service'; 9 | import { mockRequest } from './test.helper'; 10 | 11 | before(() => 12 | { 13 | TestBed 14 | .configureTestingModule( 15 | { 16 | imports: 17 | [ 18 | CrudModule 19 | ], 20 | providers: 21 | [ 22 | provideHttpClient(withInterceptorsFromDi()), 23 | CacheService, 24 | TestService 25 | ] 26 | }); 27 | }); 28 | 29 | describe(CacheService.name, () => 30 | { 31 | it('enable and disable', () => 32 | { 33 | inject( 34 | [ 35 | CacheService, 36 | TestService 37 | ], (cacheService : CacheService, testService : TestService) => 38 | { 39 | testService.enableCache(); 40 | expect(testService.getContext().get(cacheService.getToken()).method).to.be.equal('GET'); 41 | expect(testService.getContext().get(cacheService.getToken()).lifetime).to.be.equal(2000); 42 | testService.disableCache(); 43 | expect(testService.getContext().get(cacheService.getToken()).method).to.be.equal(null); 44 | expect(testService.getContext().get(cacheService.getToken()).lifetime).to.be.equal(null); 45 | }); 46 | }); 47 | 48 | it('natural cache', done => 49 | { 50 | inject( 51 | [ 52 | CacheService, 53 | TestService 54 | ], (cacheService : CacheService, testService : TestService) => 55 | { 56 | testService 57 | .enableCache('GET', 1000) 58 | .setParam('cache', '1') 59 | .find() 60 | .pipe( 61 | delay(500), 62 | concatMap(() => cacheService.get(mockRequest(testService))) 63 | ) 64 | .subscribe( 65 | { 66 | next: () => 67 | { 68 | testService.clear(); 69 | done(); 70 | }, 71 | error: () => 72 | { 73 | testService.clear(); 74 | done('error'); 75 | } 76 | }); 77 | })(); 78 | }); 79 | 80 | it('outdated cache', done => 81 | { 82 | inject( 83 | [ 84 | CacheService, 85 | TestService 86 | ], (cacheService : CacheService, testService : TestService) => 87 | { 88 | testService 89 | .enableCache('GET', 500) 90 | .setParam('cache', '2') 91 | .find() 92 | .pipe( 93 | delay(1000), 94 | concatMap(() => cacheService.get(mockRequest(testService))) 95 | ) 96 | .subscribe( 97 | { 98 | next: () => 99 | { 100 | testService.clear(); 101 | done('error'); 102 | }, 103 | error: () => 104 | { 105 | testService.clear(); 106 | done(); 107 | } 108 | }); 109 | })(); 110 | }); 111 | 112 | it('programmatic flush', done => 113 | { 114 | inject( 115 | [ 116 | CacheService, 117 | TestService 118 | ], (cacheService : CacheService, testService : TestService) => 119 | { 120 | testService 121 | .enableCache() 122 | .setParam('cache', '3') 123 | .find() 124 | .pipe( 125 | tap(() => testService.flush()), 126 | concatMap(() => cacheService.get(mockRequest(testService))) 127 | ) 128 | .subscribe( 129 | { 130 | next: () => 131 | { 132 | testService.clear(); 133 | done('error'); 134 | }, 135 | error: () => 136 | { 137 | testService.clear(); 138 | done(); 139 | } 140 | }); 141 | })(); 142 | }); 143 | 144 | it('programmatic flush many', done => 145 | { 146 | inject( 147 | [ 148 | CacheService, 149 | TestService 150 | ], (cacheService : CacheService, testService : TestService) => 151 | { 152 | testService 153 | .enableCache() 154 | .setParam('cache', '4') 155 | .find() 156 | .pipe( 157 | concatMap(() => cacheService.flushMany('https://jsonplaceholder.typicode.com/posts').get(mockRequest(testService))) 158 | ) 159 | .subscribe( 160 | { 161 | next: () => 162 | { 163 | testService.clear(); 164 | done('error'); 165 | }, 166 | error: () => 167 | { 168 | testService.clear(); 169 | done(); 170 | } 171 | }); 172 | })(); 173 | }); 174 | 175 | it('programmatic flush all', done => 176 | { 177 | inject( 178 | [ 179 | CacheService, 180 | TestService 181 | ], (cacheService : CacheService, testService : TestService) => 182 | { 183 | testService 184 | .enableCache() 185 | .setParam('cache', '5') 186 | .find() 187 | .pipe( 188 | concatMap(() => cacheService.flushAll().get(mockRequest(testService))) 189 | ) 190 | .subscribe( 191 | { 192 | next: () => 193 | { 194 | testService.clear(); 195 | done('error'); 196 | }, 197 | error: () => 198 | { 199 | testService.clear(); 200 | done(); 201 | } 202 | }); 203 | })(); 204 | }); 205 | 206 | it('observe', done => 207 | { 208 | inject( 209 | [ 210 | CacheService, 211 | TestService 212 | ], (cacheService : CacheService, testService : TestService) => 213 | { 214 | cacheService 215 | .observe('https://jsonplaceholder.typicode.com/posts?cache=6') 216 | .pipe(take(1)) 217 | .subscribe( 218 | { 219 | next: value => 220 | { 221 | expect(value.length).to.be.above(0); 222 | testService.clear(); 223 | done(); 224 | }, 225 | error: () => 226 | { 227 | testService.clear(); 228 | done('error'); 229 | } 230 | }); 231 | testService 232 | .enableCache() 233 | .setParam('cache', '6') 234 | .find() 235 | .subscribe(); 236 | })(); 237 | }); 238 | 239 | it('observe many', done => 240 | { 241 | inject( 242 | [ 243 | CacheService, 244 | TestService 245 | ], (cacheService : CacheService, testService : TestService) => 246 | { 247 | cacheService 248 | .observeMany('https://jsonplaceholder.typicode.com/posts') 249 | .pipe(take(1)) 250 | .subscribe( 251 | { 252 | next: value => 253 | { 254 | expect(value.length).to.be.above(0); 255 | testService.clear(); 256 | done(); 257 | }, 258 | error: () => 259 | { 260 | testService.clear(); 261 | done('error'); 262 | } 263 | }); 264 | testService 265 | .enableCache() 266 | .setParam('cache', '7') 267 | .find() 268 | .subscribe(); 269 | })(); 270 | }); 271 | 272 | it('observe all', done => 273 | { 274 | inject( 275 | [ 276 | CacheService, 277 | TestService 278 | ], (cacheService : CacheService, testService : TestService) => 279 | { 280 | cacheService 281 | .observeAll() 282 | .pipe(take(1)) 283 | .subscribe( 284 | { 285 | next: value => 286 | { 287 | expect(value.length).to.be.above(0); 288 | testService.clear(); 289 | done(); 290 | }, 291 | error: () => 292 | { 293 | testService.clear(); 294 | done('error'); 295 | } 296 | }); 297 | testService 298 | .enableCache() 299 | .setParam('cache', '8') 300 | .find() 301 | .subscribe(); 302 | })(); 303 | }); 304 | }); 305 | -------------------------------------------------------------------------------- /tests/common.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HttpClient, 3 | HttpContextToken, 4 | provideHttpClient, 5 | withInterceptorsFromDi 6 | } from '@angular/common/http'; 7 | import { inject, TestBed } from '@angular/core/testing'; 8 | import { expect } from 'chai'; 9 | 10 | import { CrudModule, CommonService, Context } from '../src'; 11 | 12 | import { TestService } from './test.service'; 13 | 14 | before(() => 15 | { 16 | TestBed 17 | .configureTestingModule( 18 | { 19 | imports: 20 | [ 21 | CrudModule 22 | ], 23 | providers: 24 | [ 25 | provideHttpClient(withInterceptorsFromDi()), 26 | CommonService, 27 | TestService 28 | ] 29 | }); 30 | }); 31 | 32 | describe(CommonService.name, () => 33 | { 34 | it('simple param', () => 35 | { 36 | inject( 37 | [ 38 | CommonService 39 | ], (commonService : CommonService) => 40 | { 41 | commonService.setParam('test', 'test'); 42 | expect(commonService.getParam('test')).to.be.equal('test'); 43 | expect(commonService.clearParam('test').getParam('test')).to.be.equal(null); 44 | })(); 45 | }); 46 | 47 | it('multidimensional param', () => 48 | { 49 | inject( 50 | [ 51 | CommonService 52 | ], (commonService : CommonService) => 53 | { 54 | commonService.appendParam('test', '1').setParamArray('test', 55 | [ 56 | '2', 57 | '3' 58 | ]).appendParam('test', '4'); 59 | expect(commonService.getParamArray('test')).to.deep.equal( 60 | [ 61 | '2', 62 | '3', 63 | '4' 64 | ]); 65 | commonService.appendParam('test', '5').appendParamArray('test', [ 66 | '6', 67 | '7' 68 | ]); 69 | expect(commonService.getParamArray('test')).to.deep.equal( 70 | [ 71 | '2', 72 | '3', 73 | '4', 74 | '5', 75 | '6', 76 | '7' 77 | ]); 78 | expect(commonService.clearParam('test').getParamArray('test')).to.be.equal(null); 79 | })(); 80 | }); 81 | 82 | it('simple header', () => 83 | { 84 | inject( 85 | [ 86 | CommonService 87 | ], (commonService : CommonService) => 88 | { 89 | commonService.setHeader('test', 'test'); 90 | expect(commonService.getHeader('test')).to.be.equal('test'); 91 | expect(commonService.clearHeader('test').getHeader('test')).to.be.equal(null); 92 | })(); 93 | }); 94 | 95 | it('multidimensional header', () => 96 | { 97 | inject( 98 | [ 99 | CommonService 100 | ], (commonService : CommonService) => 101 | { 102 | commonService.appendHeader('test', '1').setHeaderArray('test', 103 | [ 104 | '2', 105 | '3' 106 | ]).appendHeader('test', '4'); 107 | expect(commonService.getHeaderArray('test')).to.deep.equal( 108 | [ 109 | '2', 110 | '3', 111 | '4' 112 | ]); 113 | commonService.appendHeader('test', '5').appendHeaderArray('test', [ 114 | '6', 115 | '7' 116 | ]); 117 | expect(commonService.getHeaderArray('test')).to.deep.equal( 118 | [ 119 | '2', 120 | '3', 121 | '4', 122 | '5', 123 | '6', 124 | '7' 125 | ]); 126 | expect(commonService.clearHeader('test').getHeaderArray('test')).to.be.equal(null); 127 | })(); 128 | }); 129 | 130 | it('context by token', () => 131 | { 132 | inject( 133 | [ 134 | CommonService 135 | ], (commonService : CommonService) => 136 | { 137 | const defaultContext : Context = 138 | { 139 | method: 'ANY', 140 | lifetime: 1000 141 | }; 142 | const token : HttpContextToken = new HttpContextToken(() => defaultContext); 143 | 144 | expect((commonService.getContextByToken(token) as Context).method).to.be.equal('ANY'); 145 | expect((commonService.getContextByToken(token) as Context).lifetime).to.be.equal(1000); 146 | commonService.setContextByToken(token, 147 | { 148 | method: 'GET', 149 | lifetime: 2000 150 | }); 151 | expect((commonService.getContextByToken(token) as Context).method).to.be.equal('GET'); 152 | expect((commonService.getContextByToken(token) as Context).lifetime).to.be.equal(2000); 153 | commonService.clearContextByToken(token); 154 | expect((commonService.getContextByToken(token) as Context).method).to.be.equal('ANY'); 155 | expect((commonService.getContextByToken(token) as Context).lifetime).to.be.equal(1000); 156 | })(); 157 | }); 158 | 159 | it('get http client', () => 160 | { 161 | inject( 162 | [ 163 | CommonService 164 | ], (commonService : CommonService) => 165 | { 166 | expect(commonService.getHttpClient()).to.be.instanceof(HttpClient); 167 | })(); 168 | }); 169 | }); 170 | -------------------------------------------------------------------------------- /tests/crud.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; 2 | import { inject, TestBed } from '@angular/core/testing'; 3 | import { expect } from 'chai'; 4 | 5 | import { CrudModule, CrudService } from '../src'; 6 | 7 | import { TestService } from './test.service'; 8 | 9 | before(() => 10 | { 11 | TestBed 12 | .configureTestingModule( 13 | { 14 | imports: 15 | [ 16 | CrudModule 17 | ], 18 | providers: 19 | [ 20 | provideHttpClient(withInterceptorsFromDi()), 21 | TestService 22 | ] 23 | }); 24 | }); 25 | 26 | describe(CrudService.name, () => 27 | { 28 | it('create', done => 29 | { 30 | inject( 31 | [ 32 | TestService 33 | ], (testService : TestService) => 34 | { 35 | testService 36 | .create( 37 | { 38 | title: 'test', 39 | body: 'test', 40 | userId: '1' 41 | }) 42 | .subscribe(response => 43 | { 44 | expect(response.id).to.be.above(100); 45 | expect(response.title).to.equal('test'); 46 | done(); 47 | }); 48 | })(); 49 | }); 50 | 51 | it('read', done => 52 | { 53 | inject( 54 | [ 55 | TestService 56 | ], (testService : TestService) => 57 | { 58 | testService 59 | .read('1') 60 | .subscribe(response => 61 | { 62 | expect(response.id).to.equal(1); 63 | done(); 64 | }); 65 | })(); 66 | }); 67 | 68 | it('find by user', done => 69 | { 70 | inject( 71 | [ 72 | TestService 73 | ], (testService : TestService) => 74 | { 75 | testService 76 | .findByUser('10') 77 | .subscribe(response => 78 | { 79 | expect(response.at(0).userId).to.equal(10); 80 | done(); 81 | }); 82 | })(); 83 | }); 84 | 85 | it('find', done => 86 | { 87 | inject( 88 | [ 89 | TestService 90 | ], (testService : TestService) => 91 | { 92 | testService 93 | .find() 94 | .subscribe(response => 95 | { 96 | expect(response.at(0).userId).to.equal(1); 97 | done(); 98 | }); 99 | })(); 100 | }); 101 | 102 | it('update', done => 103 | { 104 | inject( 105 | [ 106 | TestService 107 | ], (testService : TestService) => 108 | { 109 | testService 110 | .update('1', 111 | { 112 | title: 'test', 113 | body: 'test', 114 | userId: '1' 115 | }) 116 | .subscribe(response => 117 | { 118 | expect(response).to.deep.equal( 119 | { 120 | id: 1, 121 | title: 'test', 122 | body: 'test', 123 | userId: '1' 124 | }); 125 | done(); 126 | }); 127 | })(); 128 | }); 129 | 130 | it('patch', done => 131 | { 132 | inject( 133 | [ 134 | TestService 135 | ], (testService : TestService) => 136 | { 137 | testService 138 | .patch('1', 139 | { 140 | title: 'test' 141 | }) 142 | .subscribe(response => 143 | { 144 | expect(response.id).to.equal(1); 145 | expect(response.title).to.equal('test'); 146 | expect(response).to.have.property('body'); 147 | done(); 148 | }); 149 | })(); 150 | }); 151 | 152 | it('delete', done => 153 | { 154 | inject( 155 | [ 156 | TestService 157 | ], (testService : TestService) => 158 | { 159 | testService 160 | .delete('1') 161 | .subscribe(() => done()); 162 | })(); 163 | }); 164 | 165 | it('custom', done => 166 | { 167 | inject( 168 | [ 169 | TestService 170 | ], (testService : TestService) => 171 | { 172 | testService 173 | .custom('GET') 174 | .subscribe(() => done()); 175 | })(); 176 | }); 177 | }); 178 | -------------------------------------------------------------------------------- /tests/helper.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import { createUrl, createUrlWithId, stripUrlParams, Id } from '../src'; 4 | 5 | describe(createUrl.name, () => 6 | { 7 | it('create url', () => 8 | { 9 | const testArray : { apiUrl : string; apiRoute : string; url : string; }[] = 10 | [ 11 | { 12 | apiUrl: 'http://localhost', 13 | apiRoute: '/posts', 14 | url: 'http://localhost/posts' 15 | }, 16 | { 17 | apiUrl: '..', 18 | apiRoute: '/posts', 19 | url: '../posts' 20 | } 21 | ]; 22 | 23 | testArray.map(testSet => expect(createUrl(testSet.apiUrl, testSet.apiRoute)).to.be.equal(testSet.url)); 24 | }); 25 | 26 | it('create url with id', () => 27 | { 28 | const testArray : { apiUrl : string; apiRoute : string; id : Id; url : string; }[] = 29 | [ 30 | { 31 | apiUrl: 'http://localhost/v1.0.0', 32 | apiRoute: '/posts', 33 | id: '1', 34 | url: 'http://localhost/v1.0.0/posts/1' 35 | }, 36 | { 37 | apiUrl: '../v1.0.0', 38 | apiRoute: '/posts', 39 | id: '1', 40 | url: '../v1.0.0/posts/1' 41 | } 42 | ]; 43 | 44 | testArray.map(testSet => expect(createUrlWithId(testSet.apiUrl, testSet.apiRoute, testSet.id)).to.be.equal(testSet.url)); 45 | }); 46 | }); 47 | 48 | describe(stripUrlParams.name, () => 49 | { 50 | it('strip url params', () => 51 | { 52 | expect(stripUrlParams('http://localhost/v1.0.0/posts/1?cache=1'), 'http://localhost/v1.0.0/posts/1'); 53 | expect(stripUrlParams('../v1.0.0/posts/1?cache=1'), '../v1.0.0/posts/1'); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /tests/observe.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; 2 | import { EMPTY } from 'rxjs'; 3 | import { catchError, take } from 'rxjs/operators'; 4 | import { inject, TestBed } from '@angular/core/testing'; 5 | import { expect } from 'chai'; 6 | 7 | import { CrudModule, ObserveService, OBSERVE_EFFECT } from '../src'; 8 | 9 | import { TestService } from './test.service'; 10 | import { TestEffect } from './test.effect'; 11 | import { getToken } from './test.helper'; 12 | 13 | before(() => 14 | { 15 | TestBed 16 | .configureTestingModule( 17 | { 18 | imports: 19 | [ 20 | CrudModule 21 | ], 22 | providers: 23 | [ 24 | provideHttpClient(withInterceptorsFromDi()), 25 | ObserveService, 26 | TestService, 27 | { 28 | provide: OBSERVE_EFFECT, 29 | useClass: TestEffect 30 | } 31 | ] 32 | }); 33 | }); 34 | 35 | describe(ObserveService.name, () => 36 | { 37 | it('enable and disable', () => 38 | { 39 | inject( 40 | [ 41 | ObserveService, 42 | TestService 43 | ], (observeService : ObserveService, testService : TestService) => 44 | { 45 | testService.enableObserve(); 46 | expect(testService.getContext().get(observeService.getToken()).method).to.be.equal('ANY'); 47 | expect(testService.getContext().get(observeService.getToken()).lifetime).to.be.equal(1000); 48 | testService.disableObserve(); 49 | expect(testService.getContext().get(observeService.getToken()).method).to.be.equal(null); 50 | expect(testService.getContext().get(observeService.getToken()).lifetime).to.be.equal(null); 51 | }); 52 | }); 53 | 54 | it('before and after', done => 55 | { 56 | inject( 57 | [ 58 | TestService 59 | ], (testService : TestService) => 60 | { 61 | testService 62 | .enableObserve('GET') 63 | .setParam('observe', '1') 64 | .find() 65 | .subscribe( 66 | { 67 | next: () => 68 | { 69 | expect(testService.getContext().get(getToken()).before).to.be.true; 70 | expect(testService.getContext().get(getToken()).after).to.be.true; 71 | testService.clear(); 72 | done(); 73 | }, 74 | error: () => 75 | { 76 | testService.clear(); 77 | done('error'); 78 | } 79 | }); 80 | })(); 81 | }); 82 | 83 | it('natural error', done => 84 | { 85 | inject( 86 | [ 87 | ObserveService, 88 | TestService 89 | ], (observeService : ObserveService, testService : TestService) => 90 | { 91 | observeService 92 | .observe('https://jsonplaceholder.typicode.com/error') 93 | .pipe(take(1)) 94 | .subscribe( 95 | { 96 | next: value => 97 | { 98 | expect(value.length).to.be.above(0); 99 | testService.clear(); 100 | done(); 101 | }, 102 | error: () => 103 | { 104 | testService.clear(); 105 | done('error'); 106 | } 107 | }); 108 | testService 109 | .clone() 110 | .setApiRoute('/error') 111 | .enableObserve() 112 | .find() 113 | .pipe(catchError(() => EMPTY)) 114 | .subscribe(); 115 | })(); 116 | }); 117 | 118 | it('observe', done => 119 | { 120 | inject( 121 | [ 122 | ObserveService, 123 | TestService 124 | ], (observeService : ObserveService, testService : TestService) => 125 | { 126 | observeService 127 | .observe('https://jsonplaceholder.typicode.com/posts?observe=2') 128 | .pipe(take(1)) 129 | .subscribe( 130 | { 131 | next: value => 132 | { 133 | expect(value.length).to.be.above(0); 134 | testService.clear(); 135 | done(); 136 | }, 137 | error: () => 138 | { 139 | testService.clear(); 140 | done('error'); 141 | } 142 | }); 143 | testService 144 | .enableObserve() 145 | .setParam('observe', '2') 146 | .find() 147 | .subscribe(); 148 | })(); 149 | }); 150 | 151 | it('observe many', done => 152 | { 153 | inject( 154 | [ 155 | ObserveService, 156 | TestService 157 | ], (observeService : ObserveService, testService : TestService) => 158 | { 159 | observeService 160 | .observeMany('https://jsonplaceholder.typicode.com/posts') 161 | .pipe(take(1)) 162 | .subscribe( 163 | { 164 | next: value => 165 | { 166 | expect(value.length).to.be.above(0); 167 | testService.clear(); 168 | done(); 169 | }, 170 | error: () => 171 | { 172 | testService.clear(); 173 | done('error'); 174 | } 175 | }); 176 | testService 177 | .enableObserve() 178 | .setParam('observe', '3') 179 | .find() 180 | .subscribe(); 181 | })(); 182 | }); 183 | 184 | it('observe all', done => 185 | { 186 | inject( 187 | [ 188 | ObserveService, 189 | TestService 190 | ], (observeService : ObserveService, testService : TestService) => 191 | { 192 | observeService 193 | .observeAll() 194 | .pipe(take(1)) 195 | .subscribe( 196 | { 197 | next: value => 198 | { 199 | expect(value.length).to.be.above(0); 200 | testService.clear(); 201 | done(); 202 | }, 203 | error: () => 204 | { 205 | testService.clear(); 206 | done('error'); 207 | } 208 | }); 209 | testService 210 | .enableObserve() 211 | .setParam('observe', '4') 212 | .find() 213 | .subscribe(); 214 | })(); 215 | }); 216 | }); 217 | -------------------------------------------------------------------------------- /tests/test.effect.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpErrorResponse, HttpRequest, HttpResponse } from '@angular/common/http'; 3 | 4 | import { ObserveAfterEffect, ObserveBeforeEffect } from '../src'; 5 | 6 | import { getToken } from './test.helper'; 7 | 8 | @Injectable() 9 | export class TestEffect implements ObserveBeforeEffect, ObserveAfterEffect 10 | { 11 | before(request : HttpRequest) : HttpRequest 12 | { 13 | request.context.set(getToken(), 14 | { 15 | before: true, 16 | after: false 17 | }); 18 | return request; 19 | } 20 | 21 | after(request : HttpRequest, response : HttpResponse | HttpErrorResponse) : void 22 | { 23 | if (response.ok) 24 | { 25 | request.context.set(getToken(), 26 | { 27 | before: request.context.get(getToken()).before, 28 | after: true 29 | }); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/test.helper.ts: -------------------------------------------------------------------------------- 1 | import { HttpRequest, HttpContextToken } from '@angular/common/http'; 2 | 3 | import { createUrl } from '../src'; 4 | 5 | import { TestService } from './test.service'; 6 | import { Context, ResponseBody } from './test.interface'; 7 | 8 | const defaultContext : Context = 9 | { 10 | before: false, 11 | after: false 12 | }; 13 | const token : HttpContextToken = new HttpContextToken(() => defaultContext); 14 | 15 | export function getToken() : HttpContextToken 16 | { 17 | return token; 18 | } 19 | 20 | export function mockRequest(testService : TestService) : HttpRequest 21 | { 22 | return new HttpRequest('GET', createUrl(testService.getApiUrl(), testService.getApiRoute()), 23 | { 24 | context: testService.getContext(), 25 | headers: testService.getHeaders(), 26 | params: testService.getParams() 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /tests/test.interface.ts: -------------------------------------------------------------------------------- 1 | export interface RequestBody 2 | { 3 | title : string; 4 | body : string; 5 | userId : string; 6 | } 7 | 8 | export interface ResponseBody 9 | { 10 | id : number; 11 | title : string; 12 | body : string; 13 | userId : string; 14 | } 15 | 16 | export interface Context 17 | { 18 | before : boolean; 19 | after : boolean 20 | } 21 | -------------------------------------------------------------------------------- /tests/test.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | 4 | import { ApiUrl, ApiRoute, CrudService } from '../src'; 5 | 6 | import { RequestBody, ResponseBody } from './test.interface'; 7 | 8 | @Injectable() 9 | @ApiUrl('https://jsonplaceholder.typicode.com') 10 | @ApiRoute('/posts') 11 | export class TestService extends CrudService 12 | { 13 | findByUser(userId : string) : Observable 14 | { 15 | return this.clone().setParam('userId', userId).find(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "ng-packagr/lib/ts/conf/tsconfig.ngc.json", 3 | "compilerOptions": 4 | { 5 | "useDefineForClassFields": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": 3 | { 4 | "lib": 5 | [ 6 | "esnext" 7 | ], 8 | "module": "esnext", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true 12 | } 13 | } 14 | --------------------------------------------------------------------------------