npm package: @quantumart/qa-engine-page-structure
Description: **@quantumart/qa-engine-page-structure**
Author: Quantum Art
Latest version: 0.1.1

**@quantumart/qa-engine-page-structure**

Данная библиотека предоставляет набор инструментов для построения фронтенда для сайта на базе QP

**Demo site**
* [Исходники](https://tfs.dev.qsupport.ru/tfs/QuantumartCollection/QA.Core/_git/QA.Engine.Demosite)
* [Площадка](http://stage-pg.wpdemo.dev.qsupport.ru/)
* [QP](https://qp.dev.qsupport.ru/) (customer code: demo_site_pg_react)

**Описание**

Необходимый бэкенд для построения сайта:
* База данных на основе qp
* API на .net core с методом получения структуры сайта (дерево UniversalAbstractItem) 

**Типы данных**


```typescript
interface UniversalAbstractItem {
  readonly type: string;
  readonly id: number;
  readonly alias?: string;
  readonly title?: string;
  isPage: boolean;
  readonly sortOrder: number;
  readonly regionIds?: number[];
  readonly cultureId?: number;
  childItems?: UniversalAbstractItem[];
  readonly untypedFields: { [key: string]: any };
}
```

Интерфейс UniversalAbstractItem соответствующий UniversalAbstractItem из бэкенд апи.
Описывает элемент структуры сайта (виджет или страницу).

```
class AbstractItem
```
Для дальнейшей работы со структурой сайта надо сконвертировать элементы структуры сайта в экземпляры классов AbstractItem (или их наследников).
Класс AbstractItem предоставляет возможность подниматься по дереву (есть свойство parent), предоставляет свойства по получению полей из untypedFields, а также свойство для получения относительного пути к странице.

```
interface BaseItemModel {
  readonly type: string;
  readonly id: number;
  readonly title: string;
  readonly alias: string;
  readonly sortOrder: number;
  readonly regionIds?: number[];
  readonly cultureId?: number;
  readonly isPage: boolean;
}
```

```
interface BaseWidgetModel extends BaseItemModel {
  zoneName: string;
  children?: BaseWidgetModel[];
}
```

```
interface BasePageModel extends BaseItemModel {
  widgets?: BaseWidgetModel[];
}
```

Интерфейсы BaseItemModel, BaseWidgetModel и BasePageModel используются уже непосрественно для компонентов вашего сайта.


**Функции**


**Использование**
Для построения страницы с использованием библиотеки нужно сначала получить структуру сайта (дерево элементов, соответствующих интерфейсу `UniversalAbstractItem`), сконвертировать его в дерево экземпляров класса `AbstractItem` (или его наследников).

Далее нужно определить стартовую страницу по хосту.

Далее можно найти Path (содержит AbstractItem и остаточный путь) соответствующий текущему пути, при этом можно использовать TargetingFilter для структуры сайта.

Из полученного Path можно уже построить модель данных для страницы.
```
import {
  AbstractItem,
  DefaultWidgetFilter,
  findPath,
  getPageStructureContextScript,
  getStartPage,
  ServerPageStructureContextProvider,
} from '@quantumart/qa-engine-page-structure';


// ....

const rawSiteStructure = await apiService.getSiteStructure();
const siteStructure = new AbstractItem(rawSiteStructure, type => type === PageType.StartPage);

const startPage = getStartPage(siteStructure, req.headers.host || '');
if (!startPage) {
  res.status(NOT_FOUND).send('Page not found');
  return;
}
const siteStructureFilter = new SiteStructureFilter();
const path = findPath(startPage, req.path, siteStructureFilter);
const widgetFilter = new DefaultWidgetFilter(req.path);

const modelFactory = new ModelFactory(widgetFilter);
const pageModel = await modelFactory.buildPageModel(path);

```
Модель строится исходя из типа найденного AbstractItem и остаточного пути.
Пример реализации:
  
```
interface PageModelResult {
  notFound?: boolean;
  redirectTo?: string;
  permanentRedirect?: boolean;
  pageModel?: BasePageModel;
}

export class ModelFactory {
  private readonly _widgetFilter?: TargetingFilter;
  constructor(widgetFilter?: TargetingFilter) {
    this._widgetFilter = widgetFilter;
  }

  async buildPageModel(pathData?: PathData): Promise<PageModelResult> {
    if (!pathData) {
      return { notFound: true };
    }
    const page = pathData.abstractItem as AbstractItem;
    const pageType = page.type as PageType;
    switch (pageType) {
      case PageType.StartPage: {
        // ...
      }
      case PageType.TextPage: {
        if (pathData.remainingPath && pathData.remainingPath !== '/') {
          return { notFound: true };
        }
        const result: TextPageModel = {
          ...(await this.mapBasePage(page)),
          text: page.getField('TEXT', ''),
          hideTitle: page.getField('HIDETITLE', false),
        };
        return { pageModel: result };
      }
      case PageType.RedirectPage: {
        return { redirectTo: page.getField('REDIRECTTO', ''), permanentRedirect: true };
      }
      default:
        return { notFound: true };
    }
  }


  private isMatchingWidget(item: AbstractItem): boolean {
    return !item.isPage && (!this._widgetFilter || this._widgetFilter.match(item));
  }

  private async mapBaseWidget(widget: AbstractItem): Promise<BaseWidgetModel> {
    const childrenPromises = widget.childItems?.filter(x => this.isMatchingWidget(x)).map(x => this.mapWidget(x));
    const children = childrenPromises ? await Promise.all(childrenPromises) : [];
    return {
      id: widget.id,
      title: widget.title || '',
      type: widget.type,
      alias: widget.alias || '',
      zoneName: widget.zoneName,
      cultureId: widget.cultureId,
      regionIds: widget.regionIds,
      sortOrder: widget.sortOrder,
      isPage: false,
      children,
    };
  }

  private async mapBasePage(page: AbstractItem): Promise<BasePageModel> {
    const widgetPromises = getPageWidgets(page, this._widgetFilter)?.map(x => this.mapWidget(x));
    const widgets = widgetPromises ? await Promise.all(widgetPromises) : [];
    return {
      id: page.id,
      title: page.title || '',
      type: page.type,
      alias: page.alias || '',
      cultureId: page.cultureId,
      regionIds: page.regionIds,
      sortOrder: page.sortOrder,
      isPage: true,
      widgets,
    };
  }

  private async mapWidget(widget: AbstractItem): Promise<BaseWidgetModel> {
    const type = widget.type as WidgetType;
    switch (type) {
      case WidgetType.FaqWidget: {
        const result: FaqWidgetModel = {
          ...(await this.mapBaseWidget(widget)),
          header: widget.getField('HEADER', ''),
          questions: await apiService.getFaqQuestions(widget.getField<number[]>('QUESTIONS', [])),
        };
        return result;
      }
      case WidgetType.HtmlWidget: {
        const result: HtmlWidgetModel = {
          ...(await this.mapBaseWidget(widget)),
          html: widget.getField('HTML', ''),
        };
        return result;
      }
      default:
        console.error(`unknown widget type: ${type}`);
        return this.mapBaseWidget(widget);
    }
  }
}
```