Рекурсивные звонки в Firestore - PullRequest
0 голосов
/ 03 ноября 2018

У меня есть Firebase Firestore с «Компонентами» в качестве корневой коллекции. Каждый документ («Компонент») в коллекции может иметь массив, называемый «потомками», причем каждый элемент в массиве является «идентификатором» другого компонента, по сути, отношения «один ко многим». Поскольку каждый дочерний элемент также является компонентом, он также может иметь своих собственных дочерних элементов и т. Д.

Коллекция компонентов

Родитель

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

Первый ребенок

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

Второй ребенок

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

Первый внук

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

Второй внук

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

В моем коде я хочу создать объект для каждого компонента, и если у него есть дочерний массив, то каждый идентификатор в массиве заменяется полноценным объектом, полученным из Firestore.

Дерево выходных объектов должно выглядеть так

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'

Выходной объект в формате JSON должен выглядеть следующим образом

{
  "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"
        }
      ]
    }
  ]
}

Очевидно, нам здесь нужна рекурсия, но я совершенно не уверен, как создать рекурсивную функцию с использованием RxJS. Буду признателен за некоторые советы или пример кода для разрешения сделать это

Обратите внимание, я использую это в проекте Angular и использую AngularFire для доступа к Firebase-Firestore.

Ответы [ 2 ]

0 голосов
/ 05 ноября 2018

Вы можете посмотреть на решение в этом направлении

// 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)))

Я протестировал приведенный выше код со следующими данными теста

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'},
]

Основная идея заключается в рекурсивном вызове функции expandComp, в то время как цикл по потомкам каждого компонента получается с использованием функции from, предоставленной RxJS.

Группирование children в компоненте parent обеспечивается с помощью оператора reduce RxJS, используемого в функции expandComp.

Сначала я попытался посмотреть на оператор expand в RxJS, но мне не удалось найти решение, используя его. Решение, предложенное @ggradnig, использует expand.

0 голосов
/ 05 ноября 2018

Рекурсию в RxJS лучше всего решать с использованием оператора expand. Вы предоставляете ему функцию проекции, которая возвращает Observable, которая при уведомлении снова вызывает функцию проекции с выданным значением. Он делает это до тех пор, пока ваша внутренняя наблюдаемая не излучает EMPTY или complete.

При этом каждое уведомление также направляется подписчикам expand, в отличие от традиционной рекурсии, в которой вы получите результат только в самом конце.

Из официальных документов по expand:

Рекурсивно проецирует каждое исходное значение в Observable, которое объединяется в выходной Observable.

Давайте посмотрим на ваш пример. Без RxJS, если бы у нас был синхронный источник данных, который давал нам каждого дочернего элемента узла (назовем его getChildById), функция могла бы выглядеть так:

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

Теперь мы переведем его в RxJS с использованием оператора expand:

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
    )
);

Используемые операторы подробно объясняются на официальной странице документации RxJS: из , расширение , уменьшение

РЕДАКТИРОВАТЬ : Чистую и проверенную версию вышеуказанного кода можно найти здесь: https://stackblitz.com/edit/rxjs-vzxhqf?devtoolsheight=60

...