How to return Observable inside a subscription

AsyncTask picture AsyncTask · Mar 22, 2018 · Viewed 18.4k times · Source

I want to call http request inside a observable which makes select operation from database. I made two services, DbService and BackendService.

BackendService makes http post requests and returns response data. In my design BackendService should subscribe to DbService for getting url, after that make http post request then return response data.

BackendService can take url from DbService and try to make http request but couldn't. response data is (Json format)

{"_isScalar":false,"source":{"_isScalar":false},"operator":{}}

I don't understand what happening here. My services and AppComponent file is below.

There is BackendService

import { Injectable } from "@angular/core";
import { getString, setString } from "application-settings";
import { Headers, Http, Response, RequestOptions } from "@angular/http";
import { Observable } from "rxjs/Observable";
import 'rxjs/add/observable/of';
import "rxjs/add/operator/do";
import "rxjs/add/operator/map";
import "rxjs/add/observable/throw";
import "rxjs/add/operator/catch";
import { DbService } from "./db.service";

@Injectable()
export class BackendService {
    static BaseUrl= "http://blabla.com"

    constructor(public http: Http, private db: DbService) {
    }

        sendPost(key: string, requestObj: Object):Observable<any>{
        console.log("sendPost: ");
        return new Observable(obs=> {
            let obs1 = this.db.getActionUrl(key);
            obs1.subscribe(value => {
                let url = BackendService.BaseUrl + value;

                console.log("key: ", key);
                console.log("url: ", url);
                var h = BackendService.getHeaders();
                obs.next(this.http.post(
                    url,
                    JSON.stringify(requestObj),
                    { headers: h }
                ).map((res: Response) => res.json()));
                // .catch((error: any) => Observable.throw(error.json())));
                obs.complete();
            }
            , error => {
                console.error("send post error: "+ error);
                obs.error(error);
            }
        );
        });
    }

    static getHeaders() {
        let headers = new Headers();
        headers.append("Content-Type", "application/json");
        headers.append("SESSION-ID", this.sessionId);
        // headers.append("Authorization", BackendService.appUserHeader);
        return headers;
    }
}

There is DbService

import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Observable";
import 'rxjs/add/observable/of';
import "rxjs/add/operator/do";
import "rxjs/add/operator/map";
import "rxjs/add/observable/throw";
import "rxjs/add/operator/catch";
import 'rxjs/add/operator/toPromise';

var Sqlite = require("nativescript-sqlite");

@Injectable()
export class DbService {
    private tableActions = "actions";

    private columnActionName = "name";
    private columnActionUrl = "url";


    private database: any;
    constructor() {
        console.log("DbService Constructor");
        (new Sqlite("my_app.db")).then(db => {
            db.execSQL("CREATE TABLE IF NOT EXISTS " + this.tableActions + " (" + this.columnActionName + " TEXT PRIMARY KEY, " + this.columnActionUrl +" TEXT)").then(id => {
                this.database = db;
                console.log("DB SERVICE READY");
            }, error => {
                console.log("CREATE TABLE ERROR", error);
            });
        }, error => {
            console.log("OPEN DB ERROR", error);
        });
    }


    public getActionUrl(key: string):Observable<any>{
    return new Observable(observer => { 
        if (key === "getApiMap") {
            observer.next("/getApiMap");
            observer.complete();
            return;
        }
        console.log("getActionUrl :" + key);
        this.database.all("SELECT * FROM " + this.tableActions).then(
            rows => {
                console.log(rows);
                observer.next(rows[0][this.columnActionUrl]);
                observer.complete();
            }, error => {
                console.log("SELECT ERROR: getActionUrl: ", error);
                observer.error(error);
            })
    });
    }
}

And there is my AppComponent which makes http requests...

//some imports

export class AppComponent {
    public resp: Observable<ModelMainGetApiMapRes>;
    public constructor(private bs: BackendService, private db: DbService) {
let req = new ModelMainGetApiMapReq()
    bs.sendPost("getApiMap", req, false).subscribe(
        (res: ModelMainGetApiMapRes) => {
            console.log("getapimap response received!");
            console.log(JSON.stringify(res));
            console.log("apimap version:" + res.api_version);

        },
        err => {
             console.error("error!", err);
        }
    );
 }

//some functions
}

the console output of app.component is

CONSOLE LOG file:///app/shared/backend.service.js:61:20: sendPost:
CONSOLE LOG file:///app/shared/backend.service.js:66:28: key:  getApiMap
CONSOLE LOG file:///app/shared/backend.service.js:67:28: url:  http://blabla.com/getApiMap
CONSOLE LOG file:///app/app.component.js:55:36: getapimap response received!
CONSOLE LOG file:///app/app.component.js:56:36: {"_isScalar":false,"source":{"_isScalar":false},"operator":{}}
CONSOLE LOG file:///app/tns_modules/tns-core-modules/profiling/profiling.js:10:16: ANGULAR BOOTSTRAP DONE. 7805.849
CONSOLE ERROR file:///app/tns_modules/@angular/core/bundles/core.umd.js:1486:24: ERROR Error: Uncaught (in promise): TypeError: undefined is not an object (evaluating 'res.api_version')

Answer

Fetrarij picture Fetrarij · Mar 25, 2018

With your actual code in BackendService.ts:

return new Observable(obs=> {
    let obs1 = this.db.getActionUrl(key);
    obs1.subscribe(value => {
        let url = BackendService.BaseUrl + value;

        console.log("key: ", key);
        console.log("url: ", url);
        var h = BackendService.getHeaders();
        obs.next(this.http.post(
            url,
            JSON.stringify(requestObj),
            { headers: h }
        ).map((res: Response) => res.json()));
         obs.complete();
        ...
    });
 });

you emitted the http observable

this.http.post(
                    url,
                    JSON.stringify(requestObj),
                    { headers: h }
                ).map((res: Response) => res.json())

that's why you got: {"_isScalar":false,"source":{"_isScalar":false},"operator":{}} when subscribing on it, it's an observable.

The simplest solution with your code, you can emit the data after subscribing the second observable, something like:

return new Observable(obs=> {
    let obs1 = this.db.getActionUrl(key);
    obs1.subscribe(value => {
        let url = BackendService.BaseUrl + value;
        console.log("key: ", key);
        console.log("url: ", url);
        var h = BackendService.getHeaders();
        this.http.post(
            url,
            JSON.stringify(requestObj),
            { headers: h }
        ).map((res: Response) => res.json())
         .subscribe(data => obs.next(data));
    });
});

But the better solution is by using switchMap operator: (or any other xxxxMap operator)

import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/map';
...
sendPost(key: string, requestObj: Object):Observable<any>{
    return this.db.getActionUrl(key)
            .map( value => BackendService.BaseUrl + value)
            .switchMap(url => this.http.post(
                url,
                JSON.stringify(requestObj),
                { headers: h }
            )
            .map((res: Response) => res.json()))
}