UNPKG

@gooddata/gooddata-js

Version:
892 lines (826 loc) • 34.1 kB
// (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; } }