// (C) 2007-2019 GoodData Corporation import { Application } from "express"; import { flow, flatMap, get, has, isObject, isString, partial } from "lodash"; import * as HttpStatusCodes from "http-status-codes"; import { IMockProject, IProjectGroup } from "../../model/MockProject"; import { IWrappedMetric } from "../../model/Metric"; import { IWrappedAttribute } from "../../model/Attribute"; import { IWrappedFact } from "../../model/Fact"; import { IDataSetsSelector } from "./requests/DataSetsSelector"; import { filterByTitle, filterAvailable } from "../helpers/filtering"; import { orderByTitle } from "../helpers/ordering"; import { getPage } from "../helpers/paging"; import { filterByTags, getTitle, getWrappedMetadataObject } from "../helpers/metadataObjects"; import * as errors from "../errors/errors"; import { ValidationError } from "../ValidationError"; import { getGroups } from "../helpers/groups"; import { IMetadataObject } from "../../model/MetadataObject"; type CatalogObject = IWrappedMetric | IWrappedAttribute | IWrappedFact; export type CatalogType = "metric" | "fact" | "attribute"; export interface ILoadCatalogRequest { catalogRequest: { types: CatalogType[]; filter?: string; requiredDataSets?: IDataSetsSelector; bucketItems?: string[]; includeObjectsWithTags?: string[]; excludeObjectsWithTags?: string[]; paging: { offset: number; limit: number; }; }; } interface ILoadCatalogResponse { catalogResponse: { catalog: CatalogObject[]; totals: { unavailable: number; available: number; }; paging: { offset: number; count: number; }; }; } function getItems(groups: IProjectGroup[]): IMetadataObject[] { return flatMap(groups, group => [...group.metrics, ...group.facts, ...group.attributes]); } function filterByTypes( types: CatalogType[] = ["metric", "fact", "attribute"], items: IMetadataObject[], ): IMetadataObject[] { return items.filter(item => types.some(type => item.meta.category === type)); } function filterByTitleAndTypes(request: ILoadCatalogRequest, items: IMetadataObject[]) { return flow( partial(filterByTypes, request.catalogRequest.types), partial(filterByTitle, request.catalogRequest.filter, getTitle), )(items); } function getPipeLine(project: IMockProject, request: ILoadCatalogRequest) { return flow( partial(filterByTags, request.catalogRequest), partial(filterByTitleAndTypes, request), partial(filterAvailable, project, request.catalogRequest.bucketItems), partial(orderByTitle, "asc", getTitle), ); } function getBucketItems(request: ILoadCatalogRequest): string[] { return get(request, "catalogRequest.bucketItems", []); } function getUnavailableCount( project: IMockProject, request: ILoadCatalogRequest, items: IMetadataObject[], ): number { const bucketItems = getBucketItems(request); if (bucketItems.length === 0) { return 0; } const filteredItems = filterByTitleAndTypes(request, items); const availableItems = getPipeLine(project, request)(items); return filteredItems.length - availableItems.length; } function validate(request: ILoadCatalogRequest): void { if (Object.keys(request).length !== 1) { throw new ValidationError(errors.moreThanOneRootElement()); } if (!has(request, "catalogRequest")) { throw new ValidationError(errors.invalidElementKey("catalogRequest", Object.keys(request)[0])); } if (Array.isArray(request.catalogRequest)) { throw new ValidationError(errors.invalidCatalogRequestStructureType("ARRAY")); } if (isString(request.catalogRequest)) { throw new ValidationError(errors.invalidCatalogRequestStructureType("")); } if (has(request, "catalogRequest.types")) { if ( !Array.from(request.catalogRequest.types).every( type => type === "metric" || type === "fact" || type === "attribute", ) ) { throw new ValidationError(errors.invalidCatalogRequestTypes()); } if (isString(request.catalogRequest.types)) { throw new ValidationError(errors.invalidCatalogRequestTypesStructure("")); } if (!Array.isArray(request.catalogRequest.types)) { throw new ValidationError(errors.invalidCatalogRequestTypesStructure("HASH")); } } if (has(request, "catalogRequest.paging")) { if (isString(request.catalogRequest.paging)) { throw new ValidationError(errors.invalidCatalogRequestPagingStructure("")); } if (Array.isArray(request.catalogRequest.paging)) { throw new ValidationError(errors.invalidCatalogRequestPagingStructure("ARRAY")); } if ( Object.keys(request.catalogRequest.paging).length !== 2 || (Object.keys(request.catalogRequest.paging).length === 2 && !has(request.catalogRequest.paging, "limit") && !has(request.catalogRequest.paging, "offset")) ) { throw new ValidationError(errors.invalidCatalogRequestPaging()); } } if ( has(request, "catalogRequest.paging.offset") && (isString(request.catalogRequest.paging.offset) || isObject(request.catalogRequest.paging.offset) || Array.isArray(request.catalogRequest.paging.offset)) ) { throw new ValidationError(errors.invalidCatalogRequestScalarType("paging/offset")); } if ( has(request, "catalogRequest.paging.limit") && (isString(request.catalogRequest.paging.limit) || isObject(request.catalogRequest.paging.limit) || Array.isArray(request.catalogRequest.paging.limit)) ) { throw new ValidationError(errors.invalidCatalogRequestScalarType("paging/limit")); } if ( has(request, "catalogRequest.filter") && (isObject(request.catalogRequest.filter) || Array.isArray(request.catalogRequest.filter)) ) { throw new ValidationError(errors.invalidCatalogRequestScalarType("filter")); } if ( has(request, "catalogRequest.requiredDataSets") && (isString(request.catalogRequest.requiredDataSets) || Array.isArray(request.catalogRequest.requiredDataSets)) ) { throw new ValidationError( errors.invalidCatalogRequestScalarType("requiredDataSets/DataSetsSelector"), ); } if (has(request, "catalogRequest.bucketItems")) { if (isString(request.catalogRequest.bucketItems)) { throw new ValidationError(errors.invalidCatalogRequestBucketItemsStructure("")); } if (!Array.isArray(request.catalogRequest.bucketItems)) { throw new ValidationError(errors.invalidCatalogRequestBucketItemsStructure("HASH")); } } } export const catalog = { register(app: Application, project: IMockProject) { app.post(`/gdc/internal/projects/${project.project.meta.identifier}/loadCatalog`, (req, res) => { const request: ILoadCatalogRequest = req.body; try { validate(request); const groups: IProjectGroup[] = getGroups(request.catalogRequest.requiredDataSets, project); const catalogItems = getPipeLine(project, request)(getItems(groups)); const { offset = 0, limit } = get(request, "catalogRequest.paging", { offset: 0, limit: NaN, }); const sliced = getPage(catalogItems, offset, limit); const body: ILoadCatalogResponse = { catalogResponse: { catalog: sliced.map(getWrappedMetadataObject) as CatalogObject[], totals: { unavailable: getUnavailableCount(project, request, getItems(groups)), available: catalogItems.length, }, paging: { offset, count: sliced.length, }, }, }; res.status(HttpStatusCodes.OK).json(body); } catch (error) { if (error instanceof ValidationError) { res.status(HttpStatusCodes.BAD_REQUEST).json(error.getValidationError()); } else { res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).json(error); } } }); return app; }, };