Search code examples
angularrxjsobservablein-memory-database

Angular 4, angular-in-memory-web-api, and Observables: createDb() returning Observable or Promise does not work


I have been beating against this problem for a couple days now and cannot move forward.

I am trying to mock a database using some source JSON files that represent tables with foreign key relationships. Let us say that there is an entity A and an entity B, where A has a many-to-one relationship with B. In addition to requiring some additional cleanup, the JSON files only list primary keys for these relationships, so I have DTO wrappers RawA and RawB that I can map to A and B respectively once I get the data.

I am using the angular-in-memory-web-api, so I have set up my app.module.ts to use a custom service that allows me to retrieve the JSON files like so:

HttpClientInMemoryWebApiModule.forRoot(
  InMemoryDataService,
  { apiBase: '/api', dataEncapsulation: false, passThruUnknownUrl: true }
)

In my app.component.ts, I temporarily have an ngOnInit() method that test the construction of this database:

ngOnInit() {
  let result = this.http.get('/api/A')
    .subscribe(
      next => { console.log('next:'); console.log(next); },
      error => { console.log('error:'); console.log(error); },
      () => console.log('complete')
    );
}

As I understand this, the in-memory database should see this request, call createDb() on my service, and subscribe to the Observable that my method returns, and then use the list of As I create in subscribe().

I cannot get this to work. I have tried many things, perhaps the most intuitive of which is:

// most imports omitted, but I am using
import { Observable } from 'rxjs/Observable';
import 'rxjs/Rx';

@Injectable()
export class InMemoryDataService implements InMemoryDbService() {
  private static aUrl = './assets/data/a.json';
  private static bUrl = './assets/data/b.json';

  private http: HttpClient;

  constructor( private injector: Injector ) {}

  createDb( reqInfo?: RequestInfo ): {} | Observable<{}> | Promise<{}> {
    this.http = this.injector.get(HttpClient);

    return Observable.forkJoin(
        this.http.get<RawA[]>(InMemoryDataService.aUrl),
        this.http.get<RawB[]>(InMemoryDataService.bUrl)
      ).map(
        result => {
          let bs = (result[1] as RawB[]).map(raw => new B(raw));
          let as = (result[0] as RawA[]).map(raw => new A(raw, bs));
          return { A: as, B: bs };
        }
      );
  }
}

The subscribe() callbacks in ngOnInit() never run. I have tried various combinations of mapping, subscribing, converting to Promises, and swearing, and I cannot anything better than either ngOnInit() never doing anything or prompting the in-memory DB to return a 404.

I know that the forkJoin() documentation warns that all the Observables need to return before it will produce any values, but as best I can tell, the two http.get() calls should return if the wrapping plumbing subscribes to them. The following code successfully logs the database I expect (but it generates a 404, probably because the immediate call to api/A sees an empty array):

  createDb( reqInfo?: RequestInfo ): {} | Observable<{}> | Promise<{}> {
    this.http = this.injector.get(HttpClient);

    let database: { A: A[], B: B[] } = { A: [], B: [] };
    Observable.forkJoin(
        this.http.get<RawA[]>(InMemoryDataService.aUrl),
        this.http.get<RawB[]>(InMemoryDataService.bUrl)
      ).subscribe(
        result => {
          database.B = (result[1] as RawB[]).map(raw => new B(raw));
          database.A = (result[0] as RawA[]).map(raw => new A(raw, database.B));
          console.log(database);
          return database;
        }
      );
    return database;
  }
  // console logs {A: Array(100), B: Array(20)}, and inspection shows the field values are all correct

What am I missing to make this work? Also, is this legitimately tricky, or am I missing something obvious?


Solution

  • CreateDb

    Looking at your code:

    Observable.forkJoin(
        this.http.get<RawA[]>(InMemoryDataService.aUrl),
        this.http.get<RawB[]>(InMemoryDataService.bUrl)
      ).subscribe(
    

    I suspect that since you hooked up all http requests and haven't create Db yet these requests will be always in status waiting.

    In order to overcome it i would use something else to get json data.

    For instance Fetch API:

    createDb( reqInfo?: RequestInfo ): Observable<{}> | Promise<{}> {
      this.http = this.injector.get(HttpClient);
    
      return Promise.all([
          fetch(InMemoryDataService.aUrl),
          fetch(InMemoryDataService.bUrl)
        ])
        .then((result: any) => {
          return Promise.all(result.map(x => x.json()));
        })
        .then(
          result => {
            const bs = (result[1] as RawB[]).map(raw => new B(raw));
            const as = (result[0] as RawA[]).map(raw => new A(raw, bs));
            return { A: as, B: bs };
          }
        );
    }
    

    ApiBase

    Another thing you need to change is api url

    app.module.ts

    { apiBase: 'api/', dataEncapsulation: false, passThruUnknownUrl: true }
               ^^^^^^
    

    component.ts

    let result = this.http.get('api/A')
                               ^^^^^^^^^
    

    Or you can use the following pair:

    { apiBase: '/', dataEncapsulation: false, passThruUnknownUrl: true }
    
    let result = this.http.get('/A')
    

    The reason of this is how angular-in-web-memory-api parses your url