0
votes

TLDR: How can I tell an Observed array to complete. See this jsBin for example.

I am new to Observables so I may be coming at this from the wrong angle. Given the code below, or this jsBin, what should I be doing differently to get the User's sites array to "complete".

let firstUser = {
  name: 'Susan',
  sites: [
    {company: 'ABC Co', role: 'admin'},
    {company: 'XYZ Co', role: 'reader'}
  ]
};

user = new Rx.BehaviorSubject(firstUser);

function authorized(authRoles) {
    // check if the current user has one of the authorizedRoles roles
    return this.user
      .do(user => console.log("Checking user: ",JSON.stringify(user)))
      .flatMap( user => user.sites )
      .do(res => console.log("Mapped user roles: ",res))
      .first( site => authRoles.indexOf(site.role) !== -1 ) // only return when a role matches
      .do( res => console.log('First: ',res))
      .map( res => true)
}

// This one finds a match and completes
authorized(['writer','admin','reader']).subscribe(res =>{
  console.log("1: isAuthorized?: ",res);
}, err => {
  console.log("1: ERROR: User is not authorized!");
}, () => {
  console.log("1: Authorized check completed!");
});

// This one never completes
authorized(['writer','foo']).subscribe(res =>{
  console.log("2: isAuthorized?: ",res);
}, err => {
  console.log("2: ERROR: User is not authorized!");
}, () => {
  console.log("2: Authorized check completed!");
});

Note that (first)[http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-first] should throw an error if it doesn't find one, but only if the Observable completes.

The above will return properly and complete if a match is found, but it will never complete or error without a match because the array of user.sites never completes.

Is there a way to make the array complete? I can get it to work if I fetch/subscribe to the user first:

//
// Pretend that fetchedUser is fetched in a safer/more sane way

// fetch the user and set it
let fetchedUser;
user.subscribe(user => fetchedUser = user);

function authorized2(authRoles) {
    // check if the current user has one of the authorizedRoles roles
    return Rx.Observable.of(this.fetchedUser.sites)
      .do(sites => console.log("auth2: Checking users sites: ",sites))
      .flatMap( sites => sites )
      .do(res => console.log("Mapped user roles: ",res))
      .first( site => authRoles.indexOf(site.role) !== -1 ) // only return when a role matches
      .do( res => console.log('First: ',res))
      .map( res => true)
}

I feel like I am missing a crucial but yet simple step to get this to work using pure Observables. Thanks in advance for your help!

1
@Dom I'm not sure what you are getting at. The second example shows using something like that, but I don't feel that is the correct way or can find any articles, etc that say otherwise. - jrose

1 Answers

1
votes

Rx.Observable.of(this.fetchedUser.sites) converts its arguments to an observable sequence and then completes. This triggers the .first operator to throw an exception. You are never completing the this.user subject. You could change it to this.user.take(1) or even Rx.Observable.of(fetchedUser) and skip making it a behavior subject if you want it to complete. Or you could change your .first to a .map so that it returns false if the user is unauthorized. Here is an example of using the take operator.

const user = Rx.Observable.create((o) => {
  window.setTimeout(() => {
    o.next({
      name: 'Susan',
      sites: [
        {company: 'ABC Co', role: 'admin'},
        {company: 'XYZ Co', role: 'reader'}
      ]
    });
  }, 1000);
});

function authorized(authRoles) {
    return user
      .take(1)
      .flatMap( user => user.sites )
      .first( site => authRoles.indexOf(site.role) !== -1 )
      .map( res => true);
}

authorized(['writer','admin','reader']).subscribe(res =>{
  console.log("1: isAuthorized?: ",res);
}, err => {
  console.log("1: ERROR: User is not authorized!");
}, () => {
  console.log("1: Authorized check completed!");
});

authorized(['writer','foo']).subscribe(res =>{
  console.log("2: isAuthorized?: ",res);
}, err => {
  console.log("2: ERROR: User is not authorized!");
}, () => {
  console.log("2: Authorized check completed!");
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.2/Rx.min.js"></script>

After looking at your example I am left wondering why you are trying to solve this problem with RxJs. RxJs is tailored for acting on streams of data but you are acting on a static value. Maybe your example is just simplifying what actually occurs in your app but you could just accomplish what you are going for without RxJs in a synchronous manner and it would be much simpler. Here is an example:

function authorized(authRoles) {
    return firstUser.sites.some(x => authRoles.includes(x));
}