Recursive calls to Firestore

拈花ヽ惹草 提交于 2019-12-20 03:02:28

问题


I have a Firebase Firestore with "Components" as a root collection. Each document (a "Component") in the collection may have an array called "children", with each item in the array being an "id" of another component, essentially, a one-to-many relationship. Since every child is also a component it may also have its own children and so forth.

Components Collection

Parent

1_Parent (document)
│ name: 'Parent'
│ id: '1_Parent'
└─children (array)
   ├─1.1_Child_one
   └─1.2_Child_two

First Child

1.1_Child_one (document)
     name: 'Child One'
     id: '1.1_Child_one'

Second Child

1.2_Child_two (document)
│ name: 'Child Two'
│ id: '1.2_Child_two'
└─children (array)
   ├─1.2.1_Grandchild_one
   └─1.2.2_Grandchild_two

First Grandchild

1.2.1_Grandchild_one (document)
     name: 'Grandchild One'
     id: '1.2.1_Grandchild_one'

Second Grandchild

1.2.2_Grandchild_two (document)
 name: 'Grandchild Two'
 id: '1.2.2_Grandchild_two'

In my code, I want to create an object for each component and if it has a children array then each of the id in the array is replaced by a fully fledged object retrieved from Firestore.

The output object tree should look like this

1_Parent
│ name: 'Parent'
│ id: '1_Parent'
└─children
   ├─1.1_Child_one
   │    name: 'Child One'
   │    id: '1.1_Child_one'
   └─1.2_Child_two
     │  name: 'Child Two'
     │  id: '1.2_Child_two'
     └─children
        ├─1.2.1_grandhild_one
        │   name: 'Grandchild One'
        │   id: '1.2.1_grandhild_one'
        └─1.2.2_grandhild_two
            name: 'Grandchild Two'
            id: '1.2.2_grandhild_two'

The output object as JSON should look like this

{
  "name": "Parent",
  "id": "1_Parent",
  "children": [
    {
      "name": "Child One",
      "id": "1.1_Child_one"
    },
    {
      "name": "Child Two",
      "id": "1.2_Child_two",
      "children": [
        {
          "name": "Grandchild One",
          "id": "1.2.1_Grandchild_one"
        },
        {
          "name": "Grandchild Two",
          "id": "1.2.2_Grandchild_two"
        }
      ]
    }
  ]
}

It is obvious, we need recursion here, but I am at complete loss about how to create a recursive function using RxJS. I would appreciate some tips or example code for allowing to do so.

Note, I am using this in an Angular project and I am using AngularFire to access Firebase-Firestore.


回答1:


Recursion in RxJS is best tackled with the use of the expand operator. You provide it with a projection function that returns an Observable, that, on notification, calls the projection function again with the emitted value. It does this for as long as your inner Observable is not emitting EMPTY or complete.

While it does that, every notification is also forwarded to the subscribers of expand, unlike with a traditional recursion where you'll only get the result at the very end.

From the official docs on expand:

Recursively projects each source value to an Observable which is merged in the output Observable.

Let's look at your example. Without RxJS, if we had a synchronous datasource that gave us each child of a node (let's call it getChildById), the function could look like this:

function createFamilyTree(node) {
    node.children = node.childrenIds.map(childId => 
        createFamilyTree(
            getChildById(childId)
        )
    );
    return node;
}

Now, we'll translate it to RxJS with the use of the expand operator:

parentDataSource$.pipe(
    map(parent => ({node: parent})),

    expand(({node}) => // expand is called for every node recursively 
                          (i.e. the parent and each child, then their children and so on)

        !node ? EMPTY : // if there is no node, end the recursion

        from(node.childrenIds) // we'll convert the array of children 
                                  ids to an observable stream

            .pipe(
                mergeMap(childId => getChildById(childId) // and get the child for 
                                                              the given id

                    .pipe(
                        map(child => ({node: child, parent: node}))) // map it, so we have 
                                                                       the reference to the 
                                                                       parent later on
                )
            )
    ),

    // we'll get a bunch of notifications, but only want the "root", 
       that's why we use reduce:

    reduce((acc, {node, parent}) =>
        !parent ?
            node : // if we have no parent, that's the root node and we return it
            parent.children.push(node) && acc // otherwise, we'll add children
    )
);

The used operators are explained in detail on the official RxJS documentation page: from, expand, reduce

EDIT: A clean and tested version of the above code can be found here: https://stackblitz.com/edit/rxjs-vzxhqf?devtoolsheight=60




回答2:


You may look at a solution along these lines

// simulates an async data fetch from a remote db
function getComponent(id) {
  return of(components.find(comp => comp.id === id)).pipe(delay(10));
}

function expandComp(component: Observable<any>) {
  return component.pipe(
    tap(d => console.log('s', d.name)),
    mergeMap(component => {
      if (component.childrenIds) {
        return concat(
          of(component).pipe(tap(comp => comp['children'] = [])),
          from(component.childrenIds).pipe(
            mergeMap(childId => expandComp(getComponent(childId)))
          )
        )
        .pipe(
          reduce((parent: any, child) => {
            parent.children.push(child);
            return parent;
          })
        )
      } 
      else {
        return of(component);
      }
    })
  )
}
.subscribe(d => // do stuff, e.g. console.log(JSON.stringify(d, null, 3)))

I have tested the above code with the following test data

const componentIds: Array<string> = [
  '1',
  '1.1.2'
]
const components: Array<any> = [
  {id: '1', name: 'one', childrenIds: ['1.1', '1.2']},
  {id: '1.1', name: 'one.one', childrenIds: ['1.1.1', '1.1.2']},
  {id: '1.2', name: 'one.two'},
  {id: '1.1.1', name: 'one.one.one'},
  {id: '1.1.2', name: 'one.one.two', childrenIds: ['1.1.2.1', '1.1.2.2', '1.1.2.3']},
  {id: '1.1.2.1', name: 'one.one.two.one'},
  {id: '1.1.2.2', name: 'one.one.two.two'},
  {id: '1.1.2.3', name: 'one.one.two.three'},
]

The basic idea is to call recursively the expandComp function, while the looping along the children of each component is obtained using the from function provided by RxJS.

The grouping of the children within the parent component is provided using the reduce operator of RxJS, used within expandComp function.

I have tried initially to look at the expand operator of RxJS, but I was not able to find a solution using it. The solution proposed by @ggradnig leverages expand.



来源:https://stackoverflow.com/questions/53129852/recursive-calls-to-firestore

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!