gooddata-js v13.5.0

File: src/metadata.ts

                        // (C) 2007-2020 GoodData Corporation
                        import isPlainObject from "lodash/isPlainObject";
                        import get from "lodash/get";
                        import chunk from "lodash/chunk";
                        import flatten from "lodash/flatten";
                        import pick from "lodash/pick";
                        import { AFM, VisualizationObject } from "@gooddata/typings";
                        import { getIn, handlePolling, queryString } from "./util";
                        import { ApiResponse, ApiResponseError, XhrModule } from "./xhr";
                        import { IGetObjectsByQueryOptions, IGetObjectUsingOptions, SortDirection } from "./interfaces";
                        import { convertUrisToReferences, convertReferencesToUris } from "./referenceHandling";
                        import { convertAfm } from "./execution/execute-afm.convert";
                        
                        export interface IValidElementsOptions {
                            limit?: number;
                            offset?: number;
                            order?: SortDirection;
                            filter?: string;
                            prompt?: string;
                            uris?: string[];
                            complement?: boolean;
                            includeTotalCountWithoutFilters?: boolean;
                            restrictiveDefinition?: string;
                            restrictiveDefinitionContent?: object;
                            afm?: AFM.IAfm;
                        }
                        
                        /**
                         * Functions for working with metadata objects
                         *
                         * @class metadata
                         * @module metadata
                         */
                        export class MetadataModule {
                            constructor(private xhr: XhrModule) {}
                        
                            /**
                             * Load all objects with given uris
                             * (use bulk loading instead of getting objects one by one)
                             *
                             * @method getObjects
                             * @param {String} projectId id of the project
                             * @param {Array} objectUris array of uris for objects to be loaded
                             * @return {Array} array of loaded elements
                             */
                            public getObjects(projectId: string, objectUris: string[]): any {
                                const LIMIT = 50;
                                const uri = `/gdc/md/${projectId}/objects/get`;
                        
                                const objectsUrisChunks = chunk(objectUris, LIMIT);
                        
                                const promises = objectsUrisChunks.map(objectUrisChunk => {
                                    const body = {
                                        get: {
                                            items: objectUrisChunk,
                                        },
                                    };
                        
                                    return this.xhr
                                        .post(uri, { body })
                                        .then((r: ApiResponse) => {
                                            if (!r.response.ok) {
                                                throw new ApiResponseError(r.response.statusText, r.response, r.responseBody);
                                            }
                        
                                            return r.getData();
                                        })
                                        .then((result: any) =>
                                            get(result, ["objects", "items"]).map((item: any) => {
                                                if (item.visualizationObject) {
                                                    return {
                                                        visualizationObject: convertReferencesToUris(item.visualizationObject),
                                                    };
                                                }
                                                if (item.visualizationWidget) {
                                                    return {
                                                        visualizationWidget: convertReferencesToUris(item.visualizationWidget),
                                                    };
                                                }
                                                return item;
                                            }),
                                        );
                                });
                        
                                return Promise.all(promises).then(flatten);
                            }
                        
                            /**
                             * Loads all objects by query (fetches all pages, one by one)
                             *
                             * @method getObjectsByQuery
                             * @param {String} projectId id of the project
                             * @param {Object} options (see https://developer.gooddata.com/api endpoint: /gdc/md/{project_id}/objects/query)
                             *        - category {String} for example 'dataSets' or 'projectDashboard'
                             *        - mode {String} 'enriched' or 'raw'
                             *        - author {String} the URI of the author of the metadata objects
                             *        - limit {number} default is 50 (also maximum)
                             *        - deprecated {boolean} show also deprecated objects
                             * @return {Promise<Array>} array of returned objects
                             */
                            public getObjectsByQuery(projectId: string, options: IGetObjectsByQueryOptions): Promise<any[]> {
                                const getOnePage = (uri: string, items: any[] = []): Promise<any> => {
                                    return this.xhr
                                        .get(uri)
                                        .then((r: ApiResponse) => r.getData())
                                        .then(({ objects }: any) => {
                                            items.push(...objects.items);
                                            const nextUri = objects.paging.next;
                                            return nextUri ? getOnePage(nextUri, items) : items;
                                        });
                                };
                        
                                const deprecated = options.deprecated ? { deprecated: 1 } : {};
                                const uri = `/gdc/md/${projectId}/objects/query`;
                                const query = pick({ limit: 50, ...options, ...deprecated }, [
                                    "category",
                                    "mode",
                                    "author",
                                    "limit",
                                    "deprecated",
                                ]);
                                return getOnePage(uri + queryString(query));
                            }
                        
                            /**
                             * Get MD objects from using2 resource. Include only objects of given types
                             * and take care about fetching only nearest objects if requested.
                             *
                             * @method getObjectUsing
                             * @param {String} projectId id of the project
                             * @param {String} uri uri of the object for which dependencies are to be found
                             * @param {Object} options objects with options:
                             *        - types {Array} array of strings with object types to be included
                             *        - nearest {Boolean} whether to include only nearest dependencies
                             * @return {jQuery promise} promise promise once resolved returns an array of
                             *         entries returned by using2 resource
                             */
                            public getObjectUsing(projectId: string, uri: string, options: IGetObjectUsingOptions = {}) {
                                const { types = [], nearest = false } = options;
                                const resourceUri = `/gdc/md/${projectId}/using2`;
                        
                                const body = {
                                    inUse: {
                                        uri,
                                        types,
                                        nearest: nearest ? 1 : 0,
                                    },
                                };
                        
                                return this.xhr
                                    .post(resourceUri, { body })
                                    .then((r: ApiResponse) => {
                                        if (!r.response.ok) {
                                            throw new ApiResponseError(r.response.statusText, r.response, r.getData());
                                        }
                        
                                        return r.getData();
                                    })
                                    .then((result: any) => result.entries);
                            }
                        
                            /**
                             * Get MD objects from using2 resource. Include only objects of given types
                             * and take care about fetching only nearest objects if requested.
                             *
                             * @method getObjectUsingMany
                             * @param {String} projectId id of the project
                             * @param {Array} uris uris of objects for which dependencies are to be found
                             * @param {Object} options objects with options:
                             *        - types {Array} array of strings with object types to be included
                             *        - nearest {Boolean} whether to include only nearest dependencies
                             * @return {jQuery promise} promise promise once resolved returns an array of
                             *         entries returned by using2 resource
                             */
                            public getObjectUsingMany(
                                projectId: string,
                                uris: string[],
                                options: IGetObjectUsingOptions = {},
                            ): Promise<any> {
                                const { types = [], nearest = false } = options;
                                const resourceUri = `/gdc/md/${projectId}/using2`;
                        
                                const body = {
                                    inUseMany: {
                                        uris,
                                        types,
                                        nearest: nearest ? 1 : 0,
                                    },
                                };
                        
                                return this.xhr
                                    .post(resourceUri, { body })
                                    .then((r: ApiResponse) => {
                                        if (!r.response.ok) {
                                            throw new ApiResponseError(r.response.statusText, r.response, r.getData());
                                        }
                        
                                        return r.getData();
                                    })
                                    .then((result: any) => result.useMany);
                            }
                        
                            /**
                             * Returns all visualizationObjects metadata in a project specified by projectId param
                             *
                             * @method getVisualizations
                             * @param {string} projectId Project identifier
                             * @return {Array} An array of visualization objects metadata
                             */
                            public getVisualizations(projectId: string): Promise<any> {
                                return this.xhr
                                    .get(`/gdc/md/${projectId}/query/visualizationobjects`)
                                    .then((apiResponse: ApiResponse) =>
                                        apiResponse.response.ok ? apiResponse.getData() : apiResponse.response,
                                    )
                                    .then(getIn("query.entries"));
                            }
                        
                            /**
                             * Returns all attributes in a project specified by projectId param
                             *
                             * @method getAttributes
                             * @param {string} projectId Project identifier
                             * @return {Array} An array of attribute objects
                             */
                            public getAttributes(projectId: string): Promise<any> {
                                return this.xhr
                                    .get(`/gdc/md/${projectId}/query/attributes`)
                                    .then((apiResponse: ApiResponse) =>
                                        apiResponse.response.ok ? apiResponse.getData() : apiResponse.response,
                                    )
                                    .then(getIn("query.entries"));
                            }
                        
                            /**
                             * Returns all dimensions in a project specified by projectId param
                             *
                             * @method getDimensions
                             * @param {string} projectId Project identifier
                             * @return {Array} An array of dimension objects
                             * @see getFolders
                             */
                            public getDimensions(projectId: string): Promise<any> {
                                return this.xhr
                                    .get(`/gdc/md/${projectId}/query/dimensions`)
                                    .then((apiResponse: ApiResponse) =>
                                        apiResponse.response.ok ? apiResponse.getData() : apiResponse.response,
                                    )
                                    .then(getIn("query.entries"));
                            }
                        
                            /**
                             * Returns project folders. Folders can be of specific types and you can specify
                             * the type you need by passing and optional `type` parameter
                             *
                             * @method getFolders
                             * @param {String} projectId - Project identifier
                             * @param {String} type - Optional, possible values are `metric`, `fact`, `attribute`
                             * @return {Array} An array of dimension objects
                             */
                            public getFolders(projectId: string, type: string) {
                                // TODO enum?
                                const getFolderEntries = (pId: string, t: string) => {
                                    const typeURL = t ? `?type=${t}` : "";
                        
                                    return this.xhr
                                        .get(`/gdc/md/${pId}/query/folders${typeURL}`)
                                        .then(r => r.getData())
                                        .then(getIn("query.entries"));
                                };
                        
                                switch (type) {
                                    case "fact":
                                    case "metric":
                                        return getFolderEntries(projectId, type);
                                    case "attribute":
                                        return this.getDimensions(projectId);
                                    default:
                                        return Promise.all([
                                            getFolderEntries(projectId, "fact"),
                                            getFolderEntries(projectId, "metric"),
                                            this.getDimensions(projectId),
                                        ]).then(([fact, metric, attribute]) => {
                                            return { fact, metric, attribute };
                                        });
                                }
                            }
                        
                            /**
                             * Returns all facts in a project specified by the given projectId
                             *
                             * @method getFacts
                             * @param {string} projectId Project identifier
                             * @return {Array} An array of fact objects
                             */
                            public getFacts(projectId: string): Promise<any> {
                                return this.xhr
                                    .get(`/gdc/md/${projectId}/query/facts`)
                                    .then((apiResponse: ApiResponse) =>
                                        apiResponse.response.ok ? apiResponse.getData() : apiResponse.response,
                                    )
                                    .then(getIn("query.entries"));
                            }
                        
                            /**
                             * Returns all metrics in a project specified by the given projectId
                             *
                             * @method getMetrics
                             * @param {string} projectId Project identifier
                             * @return {Array} An array of metric objects
                             */
                            public getMetrics(projectId: string): Promise<any> {
                                return this.xhr
                                    .get(`/gdc/md/${projectId}/query/metrics`)
                                    .then((apiResponse: ApiResponse) =>
                                        apiResponse.response.ok ? apiResponse.getData() : apiResponse.response,
                                    )
                                    .then(getIn("query.entries"));
                            }
                        
                            /**
                             * Returns all metrics that are reachable (with respect to ldm of the project
                             * specified by the given projectId) for given attributes
                             *
                             * @method getAvailableMetrics
                             * @param {String} projectId - Project identifier
                             * @param {Array} attrs - An array of attribute uris for which we want to get
                             * available metrics
                             * @return {Array} An array of reachable metrics for the given attrs
                             * @see getAvailableAttributes
                             * @see getAvailableFacts
                             */
                            public getAvailableMetrics(projectId: string, attrs: string[] = []): Promise<any> {
                                return this.xhr
                                    .post(`/gdc/md/${projectId}/availablemetrics`, { body: attrs })
                                    .then((apiResponse: ApiResponse) =>
                                        apiResponse.response.ok ? apiResponse.getData() : apiResponse.response,
                                    )
                                    .then((data: any) => data.entries);
                            }
                        
                            /**
                             * Returns all attributes that are reachable (with respect to ldm of the project
                             * specified by the given projectId) for given metrics (also called as drillCrossPath)
                             *
                             * @method getAvailableAttributes
                             * @param {String} projectId - Project identifier
                             * @param {Array} metrics - An array of metric uris for which we want to get
                             * available attributes
                             * @return {Array} An array of reachable attributes for the given metrics
                             * @see getAvailableMetrics
                             * @see getAvailableFacts
                             */
                            public getAvailableAttributes(projectId: string, metrics: string[] = []): Promise<any> {
                                return this.xhr
                                    .post(`/gdc/md/${projectId}/drillcrosspaths`, { body: metrics })
                                    .then(apiResponse => (apiResponse.response.ok ? apiResponse.getData() : apiResponse.response))
                                    .then((r: any) => r.drillcrosspath.links);
                            }
                        
                            /**
                             * Returns all attributes that are reachable (with respect to ldm of the project
                             * specified by the given projectId) for given metrics (also called as drillCrossPath)
                             *
                             * @method getAvailableFacts
                             * @param {String} projectId - Project identifier
                             * @param {Array} items - An array of metric or attribute uris for which we want to get
                             * available facts
                             * @return {Array} An array of reachable facts for the given items
                             * @see getAvailableAttributes
                             * @see getAvailableMetrics
                             */
                            public getAvailableFacts(projectId: string, items: string[] = []): Promise<any> {
                                return this.xhr
                                    .post(`/gdc/md/${projectId}/availablefacts`, { body: items })
                                    .then((r: ApiResponse) => (r.response.ok ? r.getData() : r.response))
                                    .then((r: any) => r.entries);
                            }
                        
                            /**
                             * Get details of a metadata object specified by its uri
                             *
                             * @method getObjectDetails
                             * @param uri uri of the metadata object for which details are to be retrieved
                             * @return {Object} object details
                             */
                            public getObjectDetails(uri: string): Promise<any> {
                                return this.xhr.get(uri).then((r: ApiResponse) => r.getData());
                            }
                        
                            /**
                             * Get folders with items.
                             * Returns array of folders, each having a title and items property which is an array of
                             * corresponding items. Each item is either a metric or attribute, keeping its original
                             * verbose structure.
                             *
                             * @method getFoldersWithItems
                             * @param {String} type type of folders to return
                             * @return {Array} Array of folder object, each containing title and
                             * corresponding items.
                             */
                            public getFoldersWithItems(projectId: string, type: string) {
                                // fetch all folders of given type and process them
                                return this.getFolders(projectId, type).then(folders => {
                                    // Helper public to get details for each metric in the given
                                    // array of links to the metadata objects representing the metrics.
                                    // @return the array of promises
                                    const getMetricItemsDetails = (array: any[]) => {
                                        return Promise.all(array.map(this.getObjectDetails)).then(metricArgs => {
                                            return metricArgs.map((item: any) => item.metric);
                                        });
                                    };
                        
                                    // helper mapBy function
                                    function mapBy(array: any[], key: string) {
                                        return array.map((item: any) => {
                                            return item[key];
                                        });
                                    }
                        
                                    // helper for sorting folder tree structure
                                    // sadly @returns void (sorting == mutating array in js)
                                    const sortFolderTree = (structure: any[]) => {
                                        structure.forEach(folder => {
                                            folder.items.sort((a: any, b: any) => {
                                                if (a.meta.title < b.meta.title) {
                                                    return -1;
                                                } else if (a.meta.title > b.meta.title) {
                                                    return 1;
                                                }
                        
                                                return 0;
                                            });
                                        });
                                        structure.sort((a, b) => {
                                            if (a.title < b.title) {
                                                return -1;
                                            } else if (a.title > b.title) {
                                                return 1;
                                            }
                        
                                            return 0;
                                        });
                                    };
                        
                                    const foldersLinks = mapBy(folders, "link");
                                    const foldersTitles = mapBy(folders, "title");
                        
                                    // fetch details for each folder
                                    return Promise.all(foldersLinks.map(this.getObjectDetails)).then(folderDetails => {
                                        // if attribute, just parse everything from what we've received
                                        // and resolve. For metrics, lookup again each metric to get its
                                        // identifier. If passing unsupported type, reject immediately.
                                        if (type === "attribute") {
                                            // get all attributes, subtract what we have and add rest in unsorted folder
                                            return this.getAttributes(projectId).then(attributes => {
                                                // get uris of attributes which are in some dimension folders
                                                const attributesInFolders: any[] = [];
                                                folderDetails.forEach((fd: any) => {
                                                    fd.dimension.content.attributes.forEach((attr: any) => {
                                                        attributesInFolders.push(attr.meta.uri);
                                                    });
                                                });
                                                // unsortedUris now contains uris of all attributes which aren't in a folder
                                                const unsortedUris = attributes
                                                    .filter((item: any) => attributesInFolders.indexOf(item.link) === -1)
                                                    .map((item: any) => item.link);
                                                // now get details of attributes in no folders
                                                return Promise.all(unsortedUris.map(this.getObjectDetails)).then(
                                                    unsortedAttributeArgs => {
                                                        // TODO add map to r.json
                                                        // get unsorted attribute objects
                                                        const unsortedAttributes = unsortedAttributeArgs.map(
                                                            (attr: any) => attr.attribute,
                                                        );
                                                        // create structure of folders with attributes
                                                        const structure = folderDetails.map((folderDetail: any) => {
                                                            return {
                                                                title: folderDetail.dimension.meta.title,
                                                                items: folderDetail.dimension.content.attributes,
                                                            };
                                                        });
                                                        // and append "Unsorted" folder with attributes to the structure
                                                        structure.push({
                                                            title: "Unsorted",
                                                            items: unsortedAttributes,
                                                        });
                                                        sortFolderTree(structure);
                        
                                                        return structure;
                                                    },
                                                );
                                            });
                                        } else if (type === "metric") {
                                            const entriesLinks = folderDetails.map((entry: any) =>
                                                mapBy(entry.folder.content.entries, "link"),
                                            );
                                            // get all metrics, subtract what we have and add rest in unsorted folder
                                            return this.getMetrics(projectId).then(metrics => {
                                                // get uris of metrics which are in some dimension folders
                                                const metricsInFolders: string[] = [];
                                                folderDetails.forEach((fd: any) => {
                                                    fd.folder.content.entries.forEach((metric: any) => {
                                                        metricsInFolders.push(metric.link);
                                                    });
                                                });
                                                // unsortedUris now contains uris of all metrics which aren't in a folder
                                                const unsortedUris = metrics
                                                    .filter((item: any) => metricsInFolders.indexOf(item.link) === -1)
                                                    .map((item: any) => item.link);
                        
                                                // sadly order of parameters of concat matters! (we want unsorted last)
                                                entriesLinks.push(unsortedUris);
                        
                                                // now get details of all metrics
                                                return Promise.all(
                                                    entriesLinks.map(linkArray => getMetricItemsDetails(linkArray)),
                                                ).then(tree => {
                                                    // TODO add map to r.json
                                                    // all promises resolved, i.e. details for each metric are available
                                                    const structure = tree.map((treeItems, idx) => {
                                                        // if idx is not in folders list than metric is in "Unsorted" folder
                                                        return {
                                                            title: foldersTitles[idx] || "Unsorted",
                                                            items: treeItems,
                                                        };
                                                    });
                                                    sortFolderTree(structure);
                                                    return structure;
                                                });
                                            });
                                        }
                        
                                        return Promise.reject(null);
                                    });
                                });
                            }
                        
                            /**
                             * Get identifier of a metadata object identified by its uri
                             *
                             * @method getObjectIdentifier
                             * @param uri uri of the metadata object for which the identifier is to be retrieved
                             * @return {String} object identifier
                             */
                            public getObjectIdentifier(uri: string) {
                                function idFinder(obj: any) {
                                    // TODO
                                    if (obj.attribute) {
                                        return obj.attribute.content.displayForms[0].meta.identifier;
                                    } else if (obj.dimension) {
                                        return obj.dimension.content.attributes.content.displayForms[0].meta.identifier;
                                    } else if (obj.metric) {
                                        return obj.metric.meta.identifier;
                                    }
                        
                                    throw Error("Unknown object!");
                                }
                        
                                if (!isPlainObject(uri)) {
                                    return this.getObjectDetails(uri).then(data => idFinder(data));
                                }
                                return Promise.resolve(idFinder(uri));
                            }
                        
                            /**
                             * Get uri of an metadata object, specified by its identifier and project id it belongs to
                             *
                             * @method getObjectUri
                             * @param {string} projectId id of the project
                             * @param identifier identifier of the metadata object
                             * @return {String} uri of the metadata object
                             */
                            public getObjectUri(projectId: string, identifier: string) {
                                return this.xhr
                                    .post(`/gdc/md/${projectId}/identifiers`, {
                                        body: {
                                            identifierToUri: [identifier],
                                        },
                                    })
                                    .then((r: ApiResponse) => {
                                        const data = r.getData();
                                        const found = data.identifiers.find((pair: any) => pair.identifier === identifier);
                        
                                        if (found) {
                                            return found.uri;
                                        }
                        
                                        throw new ApiResponseError(
                                            `Object with identifier ${identifier} not found in project ${projectId}`,
                                            r.response,
                                            r.responseBody,
                                        );
                                    });
                            }
                        
                            /**
                             * Get uris specified by identifiers
                             *
                             * @method getUrisFromIdentifiers
                             * @param {String} projectId id of the project
                             * @param {Array} identifiers identifiers of the metadata objects
                             * @return {Array} array of identifier + uri pairs
                             */
                            public getUrisFromIdentifiers(projectId: string, identifiers: string[]) {
                                return this.xhr
                                    .post(`/gdc/md/${projectId}/identifiers`, {
                                        body: {
                                            identifierToUri: identifiers,
                                        },
                                    })
                                    .then((r: ApiResponse) => r.getData())
                                    .then(data => {
                                        return data.identifiers;
                                    });
                            }
                        
                            /**
                             * Get identifiers specified by uris
                             *
                             * @method getIdentifiersFromUris
                             * @param {String} projectId id of the project
                             * @param {Array} uris of the metadata objects
                             * @return {Array} array of identifier + uri pairs
                             */
                            public getIdentifiersFromUris(projectId: string, uris: string[]) {
                                return this.xhr
                                    .post(`/gdc/md/${projectId}/identifiers`, {
                                        body: {
                                            uriToIdentifier: uris,
                                        },
                                    })
                                    .then((r: ApiResponse) => r.getData())
                                    .then(data => {
                                        return data.identifiers;
                                    });
                            }
                        
                            /**
                             * Get attribute elements with their labels and uris.
                             *
                             * @param {String} projectId id of the project
                             * @param {String} labelUri uri of the label (display form)
                             * @param {Array<String>} patterns elements labels/titles (for EXACT mode), or patterns (for WILD mode)
                             * @param {('EXACT'|'WILD')} mode match mode, currently only EXACT supported
                             * @return {Array} array of elementLabelUri objects
                             */
                            public translateElementLabelsToUris(
                                projectId: string,
                                labelUri: string,
                                patterns: string[],
                                mode = "EXACT",
                            ) {
                                return this.xhr
                                    .post(`/gdc/md/${projectId}/labels`, {
                                        body: {
                                            elementLabelToUri: [
                                                {
                                                    labelUri,
                                                    mode,
                                                    patterns,
                                                },
                                            ],
                                        },
                                    })
                                    .then((r: ApiResponse) => (r.response.ok ? get(r.getData(), "elementLabelUri") : r.response));
                            }
                        
                            /**
                             * Get valid elements of an attribute, specified by its identifier and project id it belongs to
                             *
                             * @method getValidElements
                             * @param {string} projectId id of the project
                             * @param id display form id of the metadata object
                             * @param {Object} options objects with options:
                             *      - limit {Number}
                             *      - offset {Number}
                             *      - order {String} 'asc' or 'desc'
                             *      - filter {String}
                             *      - prompt {String}
                             *      - uris {Array}
                             *      - complement {Boolean}
                             *      - includeTotalCountWithoutFilters {Boolean}
                             *      - restrictiveDefinition {String}
                             *      - afm {Object}
                             * @return {Object} ValidElements response with:
                             *      - items {Array} elements
                             *      - paging {Object}
                             *      - elementsMeta {Object}
                             */
                            public getValidElements(projectId: string, id: string, options: IValidElementsOptions = {}) {
                                const query = pick(options, ["limit", "offset", "order", "filter", "prompt"]);
                                const queryParams = queryString(query);
                                const pickedOptions = pick(options, [
                                    "uris",
                                    "complement",
                                    "includeTotalCountWithoutFilters",
                                    "restrictiveDefinition",
                                ]);
                                const { afm } = options;
                        
                                const getRequestBodyWithReportDefinition = () =>
                                    this.xhr
                                        .post(`/gdc/app/projects/${projectId}/executeAfm/debug`, {
                                            body: {
                                                execution: {
                                                    afm: convertAfm(afm),
                                                },
                                            },
                                        })
                                        .then(response => response.getData())
                                        .then(reportDefinitionResult => ({
                                            ...pickedOptions,
                                            restrictiveDefinitionContent:
                                                reportDefinitionResult.reportDefinitionWithInlinedMetrics.content,
                                        }));
                        
                                const getOptions = afm ? getRequestBodyWithReportDefinition : () => Promise.resolve(pickedOptions);
                        
                                return getOptions().then(requestBody =>
                                    this.xhr
                                        .post(`/gdc/md/${projectId}/obj/${id}/validElements${queryParams}`.replace(/\?$/, ""), {
                                            body: {
                                                validElementsRequest: requestBody,
                                            },
                                        })
                                        .then(response => response.getData()),
                                );
                            }
                        
                            /**
                             * Get visualization by Uri and process data
                             *
                             * @method getVisualization
                             * @param {String} visualizationUri
                             */
                            public getVisualization(uri: string): Promise<VisualizationObject.IVisualization> {
                                return this.getObjectDetails(uri).then(
                                    (visualizationObject: VisualizationObject.IVisualizationObjectResponse) => {
                                        const mdObject = visualizationObject.visualizationObject;
                                        return {
                                            visualizationObject: convertReferencesToUris(
                                                mdObject,
                                            ) as VisualizationObject.IVisualizationObject,
                                        };
                                    },
                                );
                            }
                        
                            /**
                             * Save visualization
                             *
                             * @method saveVisualization
                             * @param {String} visualizationUri
                             */
                            public saveVisualization(projectId: string, visualization: VisualizationObject.IVisualization) {
                                const converted = convertUrisToReferences(visualization.visualizationObject);
                                return this.createObject(projectId, { visualizationObject: converted });
                            }
                        
                            /**
                             * Update visualization
                             *
                             * @method updateVisualization
                             * @param {String} visualizationUri
                             */
                            public updateVisualization(
                                projectId: string,
                                visualizationUri: string,
                                visualization: VisualizationObject.IVisualization,
                            ) {
                                const converted = convertUrisToReferences(visualization.visualizationObject);
                                return this.updateObject(projectId, visualizationUri, { visualizationObject: converted });
                            }
                        
                            /**
                             * Delete visualization
                             *
                             * @method deleteVisualization
                             * @param {String} visualizationUri
                             */
                            public deleteVisualization(visualizationUri: string) {
                                return this.deleteObject(visualizationUri);
                            }
                        
                            /**
                             * Delete object
                             *
                             * @experimental
                             * @method deleteObject
                             * @param {String} uri of the object to be deleted
                             */
                            public deleteObject(uri: string) {
                                return this.xhr.del(uri);
                            }
                        
                            /**
                             * Create object
                             *
                             * @experimental
                             * @method createObject
                             * @param {String} projectId
                             * @param {String} obj object definition
                             */
                            public createObject(projectId: string, obj: any) {
                                return this.xhr
                                    .post(`/gdc/md/${projectId}/obj?createAndGet=true`, {
                                        body: obj,
                                    })
                                    .then((r: ApiResponse) => r.getData());
                            }
                        
                            /**
                             * Update object
                             *
                             * @experimental
                             * @method updateObject
                             * @param {String} projectId
                             * @param {String} visualizationUri
                             * @param {String} obj object definition
                             */
                            public updateObject(projectId: string, visualizationUri: string, obj: any) {
                                return this.xhr
                                    .put(`/gdc/md/${projectId}/obj/${visualizationUri}`, {
                                        body: obj,
                                    })
                                    .then((r: ApiResponse) => r.getData());
                            }
                        
                            /**
                             * LDM manage
                             *
                             * @experimental
                             * @method ldmManage
                             * @param {String} projectId
                             * @param {String} maql
                             * @param {Object} options for polling (maxAttempts, pollStep)
                             */
                            public ldmManage(projectId: string, maql: string, options = {}) {
                                return this.xhr
                                    .post(`/gdc/md/${projectId}/ldm/manage2`, { body: { manage: { maql } } })
                                    .then((r: ApiResponse) => r.getData())
                                    .then((response: any) => {
                                        const manageStatusUri = response.entries[0].link;
                                        return handlePolling(
                                            this.xhr.get.bind(this.xhr),
                                            manageStatusUri,
                                            this.isTaskFinished,
                                            options,
                                        );
                                    })
                                    .then(this.checkStatusForError);
                            }
                        
                            /**
                             * ETL pull
                             *
                             * @experimental
                             * @method etlPull
                             * @param {String} projectId
                             * @param {String} uploadsDir
                             * @param {Object} options for polling (maxAttempts, pollStep)
                             */
                            public etlPull(projectId: string, uploadsDir: string, options = {}) {
                                return this.xhr
                                    .post(`/gdc/md/${projectId}/etl/pull2`, { body: { pullIntegration: uploadsDir } })
                                    .then((r: ApiResponse) => r.getData())
                                    .then((response: any) => {
                                        const etlPullStatusUri = response.pull2Task.links.poll;
                                        return handlePolling(
                                            this.xhr.get.bind(this.xhr),
                                            etlPullStatusUri,
                                            this.isTaskFinished,
                                            options,
                                        );
                                    })
                                    .then(this.checkStatusForError);
                            }
                        
                            private isTaskFinished(task: any) {
                                const taskState = task.wTaskStatus.status;
                                return taskState === "OK" || taskState === "ERROR";
                            }
                        
                            private checkStatusForError(response: any) {
                                if (response.wTaskStatus.status === "ERROR") {
                                    return Promise.reject(response);
                                }
                                return response;
                            }
                        }