gooddata-js v13.5.0

File: src/metadataExt.ts

                        // (C) 2020 GoodData Corporation
                        import { MetadataModule } from "./metadata";
                        import { XhrModule } from "./xhr";
                        import { UserModule } from "./user";
                        import cloneDeepWith from "lodash/cloneDeepWith";
                        import isEmpty from "lodash/isEmpty";
                        import compact from "lodash/compact";
                        import omit from "lodash/omit";
                        import {
                            IKPI,
                            IAnalyticalDashboardContent,
                            DashboardExport,
                            IVisualizationWidget,
                            IAnalyticalDashboard,
                            IObjectMeta,
                        } from "@gooddata/typings";
                        
                        /**
                         * Modify how and what should be copied to the cloned dashboard
                         */
                        
                        export interface ICopyDashboardOptions {
                            /** copy new kpi and reference it in the cloned dashboard */
                            copyKpi?: boolean;
                            /** copy new visualization object and reference it in the cloned widget */
                            copyVisObj?: boolean;
                            /** optional, default value of name is "Copy of (current dashboard title)" */
                            name?: string;
                            /** optional, default value of summary is (current dashboard summary) */
                            summary?: string;
                            /** optional, if true, the isLocked flag will be cleared for the newly created dashboard, defaults to false */
                            clearLockedFlag?: boolean;
                        }
                        
                        type UriTranslator = (oldUri: string) => string;
                        
                        export function createTranslator(
                            kpiMap: Map<string, string>,
                            visWidgetMap: Map<string, string>,
                        ): UriTranslator {
                            return (oldUri: string): string => {
                                const kpiMatch = kpiMap.get(oldUri);
                                const visWidgetMatch = visWidgetMap.get(oldUri);
                                if (kpiMatch) {
                                    return kpiMatch;
                                } else if (visWidgetMatch) {
                                    return visWidgetMatch;
                                } else {
                                    return oldUri;
                                }
                            };
                        }
                        
                        /**
                         * Updates content of the dashboard
                         *
                         * @param {string} dashboardUri uri of dashboard
                         * @param {UriTranslator} uriTranslator gets updated widgets and kpis uri
                         * @param {string} filterContext updated filter context uri
                         * @experimental
                         */
                        export function updateContent(
                            analyticalDashboard: any,
                            uriTranslator: UriTranslator,
                            filterContext: string,
                        ): IAnalyticalDashboardContent {
                            return cloneDeepWith(
                                {
                                    ...analyticalDashboard.content,
                                    filterContext,
                                    widgets: analyticalDashboard.content.widgets.map((uri: string) => {
                                        return uriTranslator(uri);
                                    }),
                                },
                                value => {
                                    const uri = value.uri;
                                    if (!uri) {
                                        return;
                                    }
                                    return {
                                        ...value,
                                        uri: uriTranslator(uri),
                                    };
                                },
                            );
                        }
                        
                        export class MetadataModuleExt {
                            private metadataModule: MetadataModule;
                            private userModule: UserModule;
                            private xhr: XhrModule;
                        
                            constructor(xhr: XhrModule) {
                                this.xhr = xhr;
                                this.metadataModule = new MetadataModule(xhr);
                                this.userModule = new UserModule(xhr);
                            }
                        
                            /**
                             * @param {string} projectId id of the project
                             * @param {string} dashboardUri uri of the dashboard
                             * @param {ICopyDashboardOptions} options object with options:
                             *          - default {} dashboard is cloned with new kpi reference and visualization widget is cloned with new
                             *              visualization object reference
                             *          - copyKpi {boolean} choose whether dashboard is cloned with new Kpi reference
                             *          - copyVisObj {boolean} choose whether visualization widget is cloned with new visualization object reference
                             *          - name {string} optional - choose name, default value is "Copy of (old title of the dashboard)"
                             * @returns {string} uri of cloned dashboard
                             * @experimental
                             */
                        
                            public async saveDashboardAs(
                                projectId: string,
                                dashboardUri: string,
                                options: ICopyDashboardOptions,
                            ): Promise<string> {
                                const objectsFromDashboard = await this.getObjectsFromDashboard(projectId, dashboardUri);
                                const dashboardDetails = await this.metadataModule.getObjectDetails(dashboardUri);
                                const { analyticalDashboard }: { analyticalDashboard: IAnalyticalDashboard } = dashboardDetails;
                                const allCreatedObjUris: string[] = [];
                                const visWidgetUris: string[] = [];
                                try {
                                    const filterContext = await this.duplicateFilterContext(projectId, objectsFromDashboard, options);
                                    allCreatedObjUris.push(filterContext);
                                    const kpiMap = await this.duplicateOrKeepKpis(projectId, objectsFromDashboard, options);
                                    if (this.shouldCopyKpi(options)) {
                                        allCreatedObjUris.push(...Array.from(kpiMap.values()));
                                    }
                                    const visWidgetMap = await this.duplicateWidgets(projectId, objectsFromDashboard, options);
                                    visWidgetUris.push(...Array.from(visWidgetMap.values()));
                                    const translator = createTranslator(kpiMap, visWidgetMap);
                                    const updatedContent = updateContent(analyticalDashboard, translator, filterContext);
                                    const dashboardTitle = this.getDashboardName(analyticalDashboard.meta.title, options.name);
                                    const dashboardSummary = this.getDashboardSummary(
                                        analyticalDashboard.meta.summary,
                                        options.summary,
                                    );
                                    const duplicateDashboard = {
                                        ...dashboardDetails,
                                        analyticalDashboard: {
                                            ...dashboardDetails.analyticalDashboard,
                                            content: this.getDashboardDetailObject(updatedContent, filterContext),
                                            meta: {
                                                ...this.getSanitizedMeta(dashboardDetails.analyticalDashboard.meta, options),
                                                title: dashboardTitle,
                                                summary: dashboardSummary,
                                            },
                                        },
                                    };
                        
                                    const duplicateDashboardUri: string = (
                                        await this.metadataModule.createObject(projectId, duplicateDashboard)
                                    ).analyticalDashboard.meta.uri;
                        
                                    return duplicateDashboardUri;
                                } catch (err) {
                                    if (this.shouldCopyVisObj(options)) {
                                        await Promise.all(visWidgetUris.map(uri => this.cascadingDelete(projectId, uri)));
                                    } else {
                                        await Promise.all(visWidgetUris.map(uri => this.metadataModule.deleteObject(uri)));
                                    }
                                    await Promise.all(allCreatedObjUris.map(uri => this.cascadingDelete(projectId, uri)));
                                    return dashboardUri;
                                }
                            }
                        
                            /**
                             * Deletes dashboard and its objects
                             * (only the author of the dashboard can delete the dashboard and its objects)
                             *
                             * @method deleteAllObjects
                             * @param {string} projectId Project identifier
                             * @param {string} dashboardUri Uri of a dashboard to be deleted
                             * @experimental
                             */
                        
                            public async cascadingDelete(projectID: string, dashboardUri: string): Promise<any> {
                                const objects: any[] = await this.metadataModule.getObjectUsing(projectID, dashboardUri);
                                const currentUser: string = (await this.userModule.getAccountInfo()).profileUri;
                        
                                const objectsToBeDeleted = objects
                                    .filter((object: any) => object.author === currentUser)
                                    .map((object: any) => {
                                        return object.link;
                                    });
                        
                                return this.xhr.post(`/gdc/md/${projectID}/objects/delete`, {
                                    body: {
                                        delete: {
                                            items: [dashboardUri].concat(objectsToBeDeleted),
                                            mode: "cascade",
                                        },
                                    },
                                });
                            }
                        
                            private getDashboardDetailObject(
                                updatedContent: IAnalyticalDashboardContent,
                                filterContext: string,
                            ): IAnalyticalDashboardContent {
                                const { layout } = updatedContent;
                                return {
                                    ...updatedContent,
                                    filterContext,
                                    widgets: [...updatedContent.widgets],
                                    ...(isEmpty(layout) ? {} : { layout }),
                                };
                            }
                        
                            private getDashboardName(originalName: string, newName?: string): string {
                                if (newName !== undefined) {
                                    return newName;
                                }
                                return `Copy of ${originalName}`;
                            }
                        
                            private getDashboardSummary(originalSummary?: string, newSummary?: string): string {
                                if (newSummary !== undefined) {
                                    return newSummary;
                                } else if (originalSummary !== undefined) {
                                    return originalSummary;
                                }
                                return "";
                            }
                        
                            private async duplicateOrKeepKpis(
                                projectId: string,
                                objsFromDashboard: any[],
                                options: ICopyDashboardOptions,
                            ): Promise<Map<string, string>> {
                                const uriMap: Map<string, string> = new Map();
                                if (this.shouldCopyKpi(options)) {
                                    await Promise.all(
                                        objsFromDashboard
                                            .filter((obj: any) => this.unwrapObj(obj).meta.category === "kpi")
                                            .map(async (kpiWidget: any) => {
                                                const { kpi }: { kpi: IKPI } = kpiWidget;
                                                const toSave = {
                                                    kpi: {
                                                        meta: this.getSanitizedMeta(kpi.meta as IObjectMeta, options),
                                                        content: { ...kpi.content },
                                                    },
                                                };
                                                const newUriKpiObj: string = (
                                                    await this.metadataModule.createObject(projectId, toSave)
                                                ).kpi.meta.uri;
                                                uriMap.set(kpi.meta.uri as string, newUriKpiObj);
                                            }),
                                    );
                                }
                        
                                return uriMap;
                            }
                        
                            private async duplicateWidgets(
                                projectId: string,
                                objsFromDashboard: any[],
                                options: ICopyDashboardOptions,
                            ): Promise<Map<string, string>> {
                                const uriMap: Map<string, string> = new Map();
                        
                                await Promise.all(
                                    objsFromDashboard
                                        .filter((obj: any) => this.unwrapObj(obj).meta.category === "visualizationWidget")
                                        .map(async (visWidget: any) => {
                                            return this.createAndUpdateWidgets(projectId, visWidget, options, uriMap);
                                        }),
                                );
                        
                                return uriMap;
                            }
                        
                            private async createAndUpdateWidgets(
                                projectId: string,
                                visWidget: any,
                                options: ICopyDashboardOptions,
                                uriMap: Map<string, string>,
                            ): Promise<void> {
                                const { visualizationWidget } = visWidget;
                                if (this.shouldCopyVisObj(options)) {
                                    const visObj = await this.metadataModule.getObjectDetails(
                                        visualizationWidget.content.visualization,
                                    );
                                    const toSave = {
                                        visualizationObject: {
                                            meta: this.getSanitizedMeta(visObj.visualizationObject.meta, options),
                                            content: { ...visObj.visualizationObject.content },
                                        },
                                    };
                                    const newUriVisObj = (await this.metadataModule.createObject(projectId, toSave))
                                        .visualizationObject.meta.uri;
                        
                                    const updatedVisWidget = {
                                        ...visWidget,
                                        visualizationWidget: {
                                            meta: this.getSanitizedMeta(visWidget.visualizationWidget.meta, options),
                                            content: {
                                                ...visWidget.visualizationWidget.content,
                                                visualization: newUriVisObj,
                                            },
                                        },
                                    };
                                    const visUri = (await this.metadataModule.createObject(projectId, updatedVisWidget))
                                        .visualizationWidget.meta.uri;
                                    uriMap.set(visualizationWidget.meta.uri, visUri);
                                } else {
                                    const updatedVisWidget = {
                                        ...visWidget,
                                        visualizationWidget: {
                                            meta: this.getSanitizedMeta(visWidget.visualizationWidget.meta, options),
                                            content: { ...visWidget.visualizationWidget.content },
                                        },
                                    };
                                    const { visualizationWidget } = await this.metadataModule.createObject(
                                        projectId,
                                        updatedVisWidget,
                                    );
                                    uriMap.set(visWidget.visualizationWidget.meta.uri, visualizationWidget.meta.uri);
                                }
                            }
                        
                            private async duplicateFilterContext(
                                projectId: string,
                                objsFromDashboard: any,
                                options: ICopyDashboardOptions,
                            ): Promise<string> {
                                const originalFilterContext = objsFromDashboard.filter(
                                    (obj: any) => this.unwrapObj(obj).meta.category === "filterContext",
                                )[0];
                        
                                const toSave = {
                                    filterContext: {
                                        meta: this.getSanitizedMeta(originalFilterContext.filterContext.meta, options),
                                        content: { ...originalFilterContext.filterContext.content },
                                    },
                                };
                        
                                const { filterContext } = await this.metadataModule.createObject(projectId, toSave);
                                return filterContext.meta.uri;
                            }
                        
                            private getSanitizedMeta(originalMeta: IObjectMeta, options: ICopyDashboardOptions): IObjectMeta {
                                return omit(
                                    originalMeta,
                                    compact([
                                        "identifier",
                                        "uri",
                                        "author",
                                        "created",
                                        "updated",
                                        "contributor",
                                        options && options.clearLockedFlag && "locked",
                                    ]),
                                ) as IObjectMeta;
                            }
                        
                            private async getObjectsFromDashboard(
                                projectId: string,
                                dashboardUri: string,
                            ): Promise<Array<IKPI | DashboardExport.IFilterContext | IVisualizationWidget>> {
                                const uris = await this.getObjectsUrisInDashboard(projectId, dashboardUri);
                                return this.metadataModule.getObjects(projectId, uris);
                            }
                        
                            private async getObjectsUrisInDashboard(projectId: string, dashboardUri: string): Promise<string[]> {
                                return (
                                    await this.metadataModule.getObjectUsing(projectId, dashboardUri, {
                                        types: ["kpi", "visualizationWidget", "filterContext"],
                                    })
                                ).map((obj: any) => {
                                    return obj.link;
                                });
                            }
                        
                            private unwrapObj(obj: any): any {
                                return obj[Object.keys(obj)[0]];
                            }
                        
                            private shouldCopyVisObj(options: ICopyDashboardOptions): boolean {
                                return !!(options.copyVisObj || typeof options.copyVisObj === "undefined");
                            }
                        
                            private shouldCopyKpi(options: ICopyDashboardOptions): boolean {
                                return !!(options.copyKpi || typeof options.copyKpi === "undefined");
                            }
                        }