diff --git a/src/generators.ts b/src/generators.ts
index ee4ecc2..400368b 100644
--- a/src/generators.ts
+++ b/src/generators.ts
@@ -1,6 +1,9 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
+import { ConnectionOnlyQueryInfo, QueryInfo } from "./types";
+import { missingQueryNameErr } from "./utils/constants";
+
export const generateMashupXMLTemplate = (base64: string): string =>
`${base64}`;
@@ -10,4 +13,25 @@ export const generateSingleQueryMashup = (queryName: string, query: string): str
shared #"${queryName}" =
${query};`;
+export const generateMultipleQueryMashup = (loadedQuery: QueryInfo, queries: ConnectionOnlyQueryInfo[]): string => {
+ if (!loadedQuery.queryName) {
+ throw new Error(missingQueryNameErr);
+ }
+
+ let mashup: string = generateSingleQueryMashup(loadedQuery.queryName, loadedQuery.queryMashup);
+ queries.forEach((query: ConnectionOnlyQueryInfo) => {
+ const queryName = query.queryName;
+ if (!queryName) {
+ throw new Error(missingQueryNameErr);
+ }
+
+ mashup += `
+
+ shared #"${queryName}" =
+ ${query.queryMashup};`;
+ });
+
+ return mashup;
+}
+
export const generateCustomXmlFilePath = (i: number): string => `customXml/item${i}.xml`;
diff --git a/src/types.ts b/src/types.ts
index 9c325b9..8582433 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -7,6 +7,13 @@ export interface QueryInfo {
queryName?: string;
}
+export interface QueriesInfo {
+ loadedQuery: QueryInfo;
+ connectionOnlyQueries: ConnectionOnlyQueryInfo[];
+}
+
+export type ConnectionOnlyQueryInfo = Omit;
+
export interface DocProps {
title?: string | null;
subject?: string | null;
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index 4694d6f..958f3dc 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
+import { v4 } from "uuid";
+
export const connectionsXmlPath = "xl/connections.xml";
export const sharedStringsXmlPath = "xl/sharedStrings.xml";
export const sheetsXmlPath = "xl/worksheets/sheet1.xml";
@@ -40,6 +42,8 @@ export const unexpectedErr = "Unexpected error";
export const arrayIsntMxNErr = "Array isn't MxN";
export const templateFileNotSupportedErr = "Template file is not supported for this API call";
export const relsNotFoundErr = ".rels were not found in template";
+export const queryNameAlreadyExistsErr = "Queries must have unique names";
+export const missingQueryNameErr = "Query name is missing";
export const blobFileType = "blob";
export const uint8ArrayType = "uint8array";
@@ -85,6 +89,9 @@ export const element = {
dimension: "dimension",
selection: "selection",
kindCell: "c",
+ connection: "connection",
+ connections: "connections",
+ databaseProps: "dbPr",
};
export const elementAttributes = {
@@ -99,6 +106,7 @@ export const elementAttributes = {
name: "name",
description: "description",
id: "id",
+ typeLowerCase: "type",
type: "Type",
value: "Value",
relationshipInfo: "RelationshipInfoContainer",
@@ -117,6 +125,19 @@ export const elementAttributes = {
spans: "spans",
x14acDyDescent: "x14ac:dyDescent",
xr3uid: "xr3:uid",
+ xr16uid: "xr16:uid",
+ keepAlive: "keepAlive",
+ refreshedVersion: "refreshedVersion",
+ background: "background",
+ isPrivate: "IsPrivate",
+ fillEnabled: "FillEnabled",
+ fillObjectType: "FillObjectType",
+ fillToDataModelEnabled: "FillToDataModelEnabled",
+ filLastUpdated: "FillLastUpdated",
+ filledCompleteResultToWorksheet: "FilledCompleteResultToWorksheet",
+ addedToDataModel: "AddedToDataModel",
+ fillErrorCode: "FillErrorCode",
+ fillStatus: "FillStatus",
};
export const dataTypeKind = {
@@ -125,16 +146,27 @@ export const dataTypeKind = {
boolean: "b",
};
+export const powerQueryResultType = {
+ table: "sTable",
+ connectionOnly: "sConnectionOnly",
+};
+
+export const itemPathTextContext = (queryName: string, isSource: boolean) => isSource ? `Section1/${encodeURIComponent(queryName)}/Source` : `Section1/${queryName}`;
+
export const elementAttributesValues = {
connectionName: (queryName: string) => `Query - ${queryName}`,
connectionDescription: (queryName: string) => `Connection to the '${queryName}' query in the workbook.`,
connection: (queryName: string) => `Provider=Microsoft.Mashup.OleDb.1;Data Source=$Workbook$;Location="${queryName}";`,
connectionCommand: (queryName: string) => `SELECT * FROM [${queryName}]`,
- tableResultType: () => "sTable",
+ fillStatusComplete: () => "sComplete",
+ fillErrorCodeUnknown: () => "sUnknown",
+ randomizedUid: () => "{" + v4().toUpperCase() + "}",
+ defaultConnectionType: () => "5",
};
export const defaults = {
queryName: "Query1",
+ connectionOnlyQueryNamePrefix: "Connection only query-",
sheetName: "Sheet1",
columnName: "Column",
};
diff --git a/src/utils/mashupDocumentParser.ts b/src/utils/mashupDocumentParser.ts
index b7c58ca..7d13ca1 100644
--- a/src/utils/mashupDocumentParser.ts
+++ b/src/utils/mashupDocumentParser.ts
@@ -16,6 +16,8 @@ import {
divider,
elementAttributes,
elementAttributesValues,
+ itemPathTextContext,
+ powerQueryResultType,
} from "./constants";
import { arrayUtils } from ".";
import { Metadata } from "../types";
@@ -42,6 +44,14 @@ export const replaceSingleQuery = async (base64Str: string, queryName: string, q
return base64.fromByteArray(newMashup);
};
+export const addConnectionOnlyQueries = async (base64Str: string, connectionOnlyQueryNames: string[]): Promise => {
+ var { version, packageOPC, permissionsSize, permissions, metadata, endBuffer } = getPackageComponents(base64Str);
+ const newMetadataBuffer: Uint8Array = addConnectionOnlyQuerieMetadata(metadata, connectionOnlyQueryNames);
+ const newMashup: Uint8Array = arrayUtils.concatArrays(version, arrayUtils.getInt32Buffer(packageOPC.byteLength), packageOPC, arrayUtils.getInt32Buffer(permissionsSize), permissions, arrayUtils.getInt32Buffer(newMetadataBuffer.byteLength), newMetadataBuffer, endBuffer);
+
+ return base64.fromByteArray(newMashup);
+};
+
type PackageComponents = {
version: Uint8Array;
packageOPC: Uint8Array;
@@ -131,7 +141,7 @@ export const editSingleQueryMetadata = (metadataArray: Uint8Array, metadata: Met
return prop?.name === elementAttributes.type;
});
if (entryProp?.nodeValue == elementAttributes.resultType) {
- entry.setAttribute(elementAttributes.value, elementAttributesValues.tableResultType());
+ entry.setAttribute(elementAttributes.value, powerQueryResultType.table);
}
if (entryProp?.nodeValue == elementAttributes.fillLastUpdated) {
@@ -150,3 +160,93 @@ export const editSingleQueryMetadata = (metadataArray: Uint8Array, metadata: Met
return newMetadataArray;
};
+
+const addConnectionOnlyQuerieMetadata = (metadataArray: Uint8Array, connectionOnlyQueryNames: string[]) => {
+ // extract metadataXml
+ const mashupArray: ArrayReader = new arrayUtils.ArrayReader(metadataArray.buffer);
+ const metadataVersion: Uint8Array = mashupArray.getBytes(4);
+ const metadataXmlSize: number = mashupArray.getInt32();
+ const metadataXml: Uint8Array = mashupArray.getBytes(metadataXmlSize);
+ const endBuffer: Uint8Array = mashupArray.getBytes();
+
+ // parse metadataXml
+ const metadataString: string = new TextDecoder("utf-8").decode(metadataXml);
+ const newMetadataString: string = addConnectionOnlyQueriesToMetadataStr(metadataString, connectionOnlyQueryNames);
+ const encoder: TextEncoder = new TextEncoder();
+ const newMetadataXml: Uint8Array = encoder.encode(newMetadataString);
+ const newMetadataXmlSize: Uint8Array = arrayUtils.getInt32Buffer(newMetadataXml.byteLength);
+ const newMetadataArray: Uint8Array = arrayUtils.concatArrays(
+ metadataVersion,
+ newMetadataXmlSize,
+ newMetadataXml,
+ endBuffer
+ );
+
+ return newMetadataArray;
+ };
+
+export const addConnectionOnlyQueriesToMetadataStr = (metadataString: string, connectionOnlyQueryNames: string[]) => {
+ const parser: DOMParser = new DOMParser();
+ let metadataDoc: Document = parser.parseFromString(metadataString, xmlTextResultType);
+ connectionOnlyQueryNames.forEach((queryName: string) => {
+ const items: Element = metadataDoc.getElementsByTagName(element.items)[0];
+ const stableEntriesItem: Element = createStableEntriesItem(metadataDoc, queryName);
+ items.appendChild(stableEntriesItem);
+ const sourceItem: Element = metadataDoc.createElementNS(metadataDoc.documentElement.namespaceURI, element.item);
+ sourceItem.appendChild(createItemLocation(metadataDoc, queryName, true));
+ const stableEntries: Element = metadataDoc.createElementNS(metadataDoc.documentElement.namespaceURI, element.stableEntries);
+ sourceItem.appendChild(stableEntries);
+ items.appendChild(sourceItem);
+ });
+
+ const updatedMetdataString: string = new XMLSerializer().serializeToString(metadataDoc);
+
+ return updatedMetdataString;
+ };
+
+ const createItemLocation = (metadataDoc: Document, queryName: string, isSource: boolean) => {
+ const newItemLocation: Element = metadataDoc.createElementNS(metadataDoc.documentElement.namespaceURI, element.itemLocation);
+ const newItemType: Element = metadataDoc.createElementNS(metadataDoc.documentElement.namespaceURI, element.itemType);
+ newItemType.textContent = "Formula";
+ const newItemPath: Element = metadataDoc.createElementNS(metadataDoc.documentElement.namespaceURI, element.itemPath);
+ newItemPath.textContent = itemPathTextContext(queryName, isSource);
+ newItemLocation.appendChild(newItemType);
+ newItemLocation.appendChild(newItemPath);
+
+ return newItemLocation;
+ };
+
+ const createStableEntriesItem = (metadataDoc: Document, queryName: string) => {
+ const newItem: Element = metadataDoc.createElementNS(metadataDoc.documentElement.namespaceURI, element.item);
+ newItem.appendChild(createItemLocation(metadataDoc, queryName, false));
+ const stableEntries: Element = createEntries(metadataDoc, powerQueryResultType.connectionOnly);
+ newItem.appendChild(stableEntries);
+
+ return newItem;
+ };
+
+ const createElementObject = (metadataDoc: Document, type: string, value: string) => {
+ const elementObject: Element = metadataDoc.createElementNS(metadataDoc.documentElement.namespaceURI, element.entry);
+ elementObject.setAttribute(elementAttributes.type, type);
+ elementObject.setAttribute(elementAttributes.value, value);
+
+ return elementObject;
+ };
+
+ const createEntries = (metadataDoc: Document, fillObjectType: string) => {
+ const stableEntries: Element = metadataDoc.createElementNS(metadataDoc.documentElement.namespaceURI, element.stableEntries);
+ const nowTime: string = new Date().toISOString();
+
+ stableEntries.appendChild(createElementObject(metadataDoc, elementAttributes.isPrivate, "l0"));
+ stableEntries.appendChild(createElementObject(metadataDoc, elementAttributes.fillEnabled, "l0"));
+ stableEntries.appendChild(createElementObject(metadataDoc, elementAttributes.fillObjectType, fillObjectType));
+ stableEntries.appendChild(createElementObject(metadataDoc, elementAttributes.fillToDataModelEnabled, "l0"));
+ stableEntries.appendChild(createElementObject(metadataDoc, elementAttributes.fillLastUpdated, (elementAttributes.day + nowTime).replace(/Z/, "0000Z")));
+ stableEntries.appendChild(createElementObject(metadataDoc, elementAttributes.resultType, powerQueryResultType.table));
+ stableEntries.appendChild(createElementObject(metadataDoc, elementAttributes.filledCompleteResultToWorksheet, "l0"));
+ stableEntries.appendChild(createElementObject(metadataDoc, elementAttributes.addedToDataModel, "l0"));
+ stableEntries.appendChild(createElementObject(metadataDoc, elementAttributes.fillErrorCode, elementAttributesValues.fillErrorCodeUnknown()));
+ stableEntries.appendChild(createElementObject(metadataDoc, elementAttributes.fillStatus, elementAttributesValues.fillStatusComplete()));
+
+ return stableEntries;
+ };
\ No newline at end of file
diff --git a/src/utils/pqUtils.ts b/src/utils/pqUtils.ts
index 4812320..d2634d8 100644
--- a/src/utils/pqUtils.ts
+++ b/src/utils/pqUtils.ts
@@ -2,9 +2,10 @@
// Licensed under the MIT license.
import JSZip from "jszip";
-import { EmptyQueryNameErr, QueryNameMaxLengthErr, maxQueryLength, URLS, BOM, QueryNameInvalidCharsErr } from "./constants";
+import { EmptyQueryNameErr, QueryNameMaxLengthErr, maxQueryLength, URLS, BOM, QueryNameInvalidCharsErr, queryNameAlreadyExistsErr, defaults } from "./constants";
import { generateMashupXMLTemplate, generateCustomXmlFilePath } from "../generators";
import { Buffer } from "buffer";
+import { ConnectionOnlyQueryInfo } from "../types";
type CustomXmlFile = {
found: boolean;
@@ -110,10 +111,56 @@ const validateQueryName = (queryName: string): void => {
throw new Error(EmptyQueryNameErr);
}
};
+
+const validateMultipleQueryNames = (queries: ConnectionOnlyQueryInfo[], loadedQueryName: string): string[] => {
+ const queryNames: string[] = [];
+ const cleanedLoadedQueryName: string = loadedQueryName.trim().toLowerCase();
+ queries.forEach((query: ConnectionOnlyQueryInfo) => {
+ if (query.queryName) {
+ validateQueryName(query.queryName);
+ const cleanedQueryName: string | undefined = query.queryName.trim().toLowerCase();
+ if (queryNames.includes(cleanedQueryName) || cleanedQueryName === cleanedLoadedQueryName) {
+ throw new Error(queryNameAlreadyExistsErr);
+ }
+
+ queryNames.push(cleanedQueryName);
+ }
+ });
+
+ return queryNames;
+};
+
+const assignQueryNames = (queries: ConnectionOnlyQueryInfo[], loadedQueryName: string, queryNames: string[]): ConnectionOnlyQueryInfo[] => {
+ // Generate unique name for queries without a name
+ queries.forEach((query: ConnectionOnlyQueryInfo) => {
+ if (!query.queryName) {
+ query.queryName = generateUniqueQueryName(queryNames, loadedQueryName);
+ queryNames.push(query.queryName.toLowerCase());
+ }
+ });
+
+ return queries;
+};
+
+const generateUniqueQueryName = (queryNames: string[], loadedQueryName: string,): string => {
+ let index: number = 1;
+ let queryName: string = defaults.connectionOnlyQueryNamePrefix + index++;
+ const cleanedLoadedQueryName: string = loadedQueryName.trim().toLowerCase();
+ // Assumes that query names are lower case
+ while (queryNames.includes(queryName.toLowerCase()) || queryName.toLowerCase() === cleanedLoadedQueryName) {
+ queryName = defaults.connectionOnlyQueryNamePrefix + index++;
+ }
+
+ return queryName;
+};
+
export default {
getBase64,
setBase64,
getCustomXmlFile,
getDataMashupFile,
validateQueryName,
+ assignQueryNames,
+ validateMultipleQueryNames,
+ generateUniqueQueryName
};
diff --git a/src/utils/xmlInnerPartsUtils.ts b/src/utils/xmlInnerPartsUtils.ts
index 4621221..ebde0b1 100644
--- a/src/utils/xmlInnerPartsUtils.ts
+++ b/src/utils/xmlInnerPartsUtils.ts
@@ -268,6 +268,27 @@ const updatePivotTable = (tableXmlString: string, connectionId: string, refreshO
return { isPivotTableUpdated, newPivotTable };
};
+const addNewConnection = async (connectionsDoc: Document, queryName: string): Promise => {
+ const connections = connectionsDoc.getElementsByTagName(element.connections)[0];
+ const newConnection = connectionsDoc.createElementNS(connectionsDoc.documentElement.namespaceURI, element.connection);
+ newConnection.setAttribute(elementAttributes.id, [...connectionsDoc.getElementsByTagName(element.connection)].length.toString());
+ newConnection.setAttribute(elementAttributes.xr16uid, elementAttributesValues.randomizedUid());
+ newConnection.setAttribute(elementAttributes.keepAlive, trueValue);
+ newConnection.setAttribute(elementAttributes.name, elementAttributesValues.connectionName(queryName));
+ newConnection.setAttribute(elementAttributes.description, elementAttributesValues.connectionDescription(queryName));
+ newConnection.setAttribute(elementAttributes.typeLowerCase, elementAttributesValues.defaultConnectionType());
+ newConnection.setAttribute(elementAttributes.refreshedVersion, falseValue);
+ newConnection.setAttribute(elementAttributes.background, trueValue);
+ connections.append(newConnection);
+
+ const databaseProperties = connectionsDoc.createElementNS(connectionsDoc.documentElement.namespaceURI, element.databaseProps);
+ databaseProperties.setAttribute(elementAttributes.connection, elementAttributesValues.connection(queryName));
+ databaseProperties.setAttribute(elementAttributes.command, elementAttributesValues.connectionCommand(queryName));
+ newConnection.appendChild(databaseProperties);
+
+ return connectionsDoc;
+ };
+
export default {
updateDocProps,
clearLabelInfo,
@@ -277,4 +298,5 @@ export default {
updatePivotTablesandQueryTables,
updateQueryTable,
updatePivotTable,
+ addNewConnection,
};
diff --git a/src/utils/xmlPartsUtils.ts b/src/utils/xmlPartsUtils.ts
index 36fd306..fdf3041 100644
--- a/src/utils/xmlPartsUtils.ts
+++ b/src/utils/xmlPartsUtils.ts
@@ -11,8 +11,9 @@ import {
sharedStringsNotFoundErr,
sheetsXmlPath,
sheetsNotFoundErr,
+ xmlTextResultType,
} from "./constants";
-import { replaceSingleQuery } from "./mashupDocumentParser";
+import { addConnectionOnlyQueries, replaceSingleQuery } from "./mashupDocumentParser";
import { FileConfigs, TableData } from "../types";
import pqUtils from "./pqUtils";
import xmlInnerPartsUtils from "./xmlInnerPartsUtils";
@@ -27,17 +28,31 @@ const updateWorkbookDataAndConfigurations = async (zip: JSZip, fileConfigs?: Fil
await tableUtils.updateTableInitialDataIfNeeded(zip, tableData, updateQueryTable);
};
-const updateWorkbookPowerQueryDocument = async (zip: JSZip, queryName: string, queryMashupDoc: string): Promise => {
+const updateWorkbookPowerQueryDocument = async (zip: JSZip, loadedQueryName: string, queryMashupDoc: string, connectionOnlyQueryNames?: string[]): Promise => {
const old_base64: string | undefined = await pqUtils.getBase64(zip);
-
if (!old_base64) {
throw new Error(base64NotFoundErr);
}
+ // The mashupDoc contains a default query, we replace that query with the loaded query
+ let updated_base64: string = await replaceSingleQuery(old_base64, loadedQueryName, queryMashupDoc);
+
+ // If connection-only queries were given, add them to the mashupDoc
+ updated_base64 = await addConnectionOnlyQueriesIfNeeded(updated_base64, connectionOnlyQueryNames);
+
+ await pqUtils.setBase64(zip, updated_base64);
+};
+
+const addConnectionOnlyQueriesIfNeeded = async(base64: string, connectionOnlyQueryNames?:string[]):Promise => {
+ if (!connectionOnlyQueryNames || (connectionOnlyQueryNames.length == 0))
+ {
+ return base64;
+ }
- const new_base64: string = await replaceSingleQuery(old_base64, queryName, queryMashupDoc);
- await pqUtils.setBase64(zip, new_base64);
+ return await addConnectionOnlyQueries(base64, connectionOnlyQueryNames);
};
+
+
const updateWorkbookSingleQueryAttributes = async (zip: JSZip, queryName: string, refreshOnOpen: boolean): Promise => {
// Update connections
const connectionsXmlString: string | undefined = await zip.file(connectionsXmlPath)?.async(textResultType);
@@ -70,8 +85,28 @@ const updateWorkbookSingleQueryAttributes = async (zip: JSZip, queryName: string
await xmlInnerPartsUtils.updatePivotTablesandQueryTables(zip, queryName, refreshOnOpen, connectionId!);
};
+const addConnectionOnlyQueriesToWorkbook = async (zip: JSZip, connectionOnlyQueryNames: string[]): Promise => {
+ // Update connections
+ let connectionsXmlString: string | undefined = await zip.file(connectionsXmlPath)?.async(textResultType);
+ if (connectionsXmlString === undefined) {
+ throw new Error(connectionsNotFoundErr);
+ }
+
+ const parser: DOMParser = new DOMParser();
+ const serializer: XMLSerializer = new XMLSerializer();
+ let connectionsDoc: Document = parser.parseFromString(connectionsXmlString, xmlTextResultType);
+ connectionOnlyQueryNames.forEach(async (queryName: string) => {
+ connectionsDoc = await xmlInnerPartsUtils.addNewConnection(connectionsDoc, queryName);
+ });
+
+ connectionsXmlString = serializer.serializeToString(connectionsDoc);
+ zip.file(connectionsXmlPath, connectionsXmlString);
+
+};
+
export default {
updateWorkbookDataAndConfigurations,
updateWorkbookPowerQueryDocument,
updateWorkbookSingleQueryAttributes,
+ addConnectionOnlyQueriesToWorkbook,
};
diff --git a/src/workbookManager.ts b/src/workbookManager.ts
index e56a902..7a2a302 100644
--- a/src/workbookManager.ts
+++ b/src/workbookManager.ts
@@ -13,8 +13,8 @@ import {
tableNotFoundErr,
templateFileNotSupportedErr,
} from "./utils/constants";
-import { QueryInfo, TableData, Grid, FileConfigs } from "./types";
-import { generateSingleQueryMashup } from "./generators";
+import { QueryInfo, TableData, Grid, FileConfigs, QueriesInfo } from "./types";
+import { generateMultipleQueryMashup, generateSingleQueryMashup } from "./generators";
export const generateSingleQueryWorkbook = async (query: QueryInfo, initialDataGrid?: Grid, fileConfigs?: FileConfigs): Promise => {
if (!query.queryMashup) {
@@ -40,6 +40,26 @@ export const generateSingleQueryWorkbook = async (query: QueryInfo, initialDataG
return await generateSingleQueryWorkbookFromZip(zip, query, fileConfigs, tableData);
};
+export const generateMultipleQueryWorkbook = async (queries: QueriesInfo, initialDataGrid?: Grid, fileConfigs?: FileConfigs): Promise => {
+ const templateFile: File | undefined = fileConfigs?.templateFile;
+ if (templateFile !== undefined && initialDataGrid !== undefined) {
+ throw new Error(templateWithInitialDataErr);
+ }
+
+ if (!queries.loadedQuery.queryName) {
+ queries.loadedQuery.queryName = defaults.queryName;
+ }
+
+ pqUtils.validateQueryName(queries.loadedQuery.queryName!);
+ const connectionOnlyQueryNames: string[] = pqUtils.validateMultipleQueryNames(queries.connectionOnlyQueries, queries.loadedQuery.queryName!);
+ pqUtils.assignQueryNames(queries.connectionOnlyQueries, queries.loadedQuery.queryName!, connectionOnlyQueryNames);
+ const zip: JSZip =
+ templateFile === undefined ? await JSZip.loadAsync(SIMPLE_QUERY_WORKBOOK_TEMPLATE, { base64: true }) : await JSZip.loadAsync(templateFile);
+ const tableData: TableData | undefined = initialDataGrid ? gridUtils.parseToTableData(initialDataGrid) : undefined;
+
+ return await generateMultipleQueryWorkbookFromZip(zip, queries, fileConfigs, tableData);
+};
+
export const generateTableWorkbookFromHtml = async (htmlTable: HTMLTableElement, fileConfigs?: FileConfigs): Promise => {
if (fileConfigs?.templateFile !== undefined) {
throw new Error(templateFileNotSupportedErr);
@@ -81,6 +101,20 @@ const generateSingleQueryWorkbookFromZip = async (zip: JSZip, query: QueryInfo,
});
};
+const generateMultipleQueryWorkbookFromZip = async (zip: JSZip, queries: QueriesInfo, fileConfigs?: FileConfigs, tableData?: TableData): Promise => {
+ const connectionOnlyQueryNames: string[] = queries.connectionOnlyQueries.map((query) => query.queryName!);
+ const mashupDocument: string = generateMultipleQueryMashup(queries.loadedQuery, queries.connectionOnlyQueries);
+ await xmlPartsUtils.updateWorkbookPowerQueryDocument(zip, queries.loadedQuery.queryName!, mashupDocument, connectionOnlyQueryNames);
+ await xmlPartsUtils.updateWorkbookSingleQueryAttributes(zip, queries.loadedQuery.queryName!, queries.loadedQuery.refreshOnOpen);
+ await xmlPartsUtils.updateWorkbookDataAndConfigurations(zip, fileConfigs, tableData, true /*updateQueryTable*/);
+ await xmlPartsUtils.addConnectionOnlyQueriesToWorkbook(zip, connectionOnlyQueryNames);
+
+ return await zip.generateAsync({
+ type: blobFileType,
+ mimeType: application,
+ });
+};
+
export const downloadWorkbook = (file: Blob, filename: string): void => {
const nav = window.navigator as any;
if (nav.msSaveOrOpenBlob)
diff --git a/tests/mashupDocumentParser.test.ts b/tests/mashupDocumentParser.test.ts
index e483f98..8a1e832 100644
--- a/tests/mashupDocumentParser.test.ts
+++ b/tests/mashupDocumentParser.test.ts
@@ -2,7 +2,7 @@
// Licensed under the MIT license.
import { TextDecoder, TextEncoder } from "util";
-import { replaceSingleQuery, getPackageComponents, editSingleQueryMetadata } from "../src/utils/mashupDocumentParser";
+import { replaceSingleQuery, getPackageComponents, editSingleQueryMetadata, addConnectionOnlyQueriesToMetadataStr } from "../src/utils/mashupDocumentParser";
import { arrayUtils, pqUtils } from "../src/utils";
import { section1mNewQueryNameSimpleMock, pqMetadataXmlMockPart1, pqMetadataXmlMockPart2 } from "./mocks";
import base64 from "base64-js";
@@ -11,6 +11,7 @@ import { SIMPLE_QUERY_WORKBOOK_TEMPLATE } from "../src/workbookTemplate";
import { section1mPath } from "../src/utils/constants";
import util from "util";
+import { pqSingleQueryMetadataXmlMock, pqConnectionOnlyMetadataXmlMockPart1, pqConnectionOnlyMetadataXmlMockPart2 } from "./mocks/xmlMocks";
(global as any).TextDecoder = TextDecoder;
(global as any).TextEncoder = TextEncoder;
@@ -47,4 +48,22 @@ describe("Mashup Document Parser tests", () => {
expect(metadataString.replace(/ /g, "")).toContain(pqMetadataXmlMockPart2.replace(/ /g, ""));
}
});
+
+ test("Power Query Multiple Queries MetadataXml test", async () => {
+ const metadataStr: string = pqSingleQueryMetadataXmlMock;
+ const newMetadataString: string = addConnectionOnlyQueriesToMetadataStr(metadataStr, ["Query2", "Query3"]);
+ expect(newMetadataString.replace(/ /g, "")).toContain(pqConnectionOnlyMetadataXmlMockPart1("Query2").replace(/ /g, ""));
+ expect(newMetadataString.replace(/ /g, "")).toContain(pqConnectionOnlyMetadataXmlMockPart2("Query2").replace(/ /g, ""));
+ expect(newMetadataString.replace(/ /g, "")).toContain(pqConnectionOnlyMetadataXmlMockPart1("Query3").replace(/ /g, ""));
+ expect(newMetadataString.replace(/ /g, "")).toContain(pqConnectionOnlyMetadataXmlMockPart2("Query3").replace(/ /g, ""));
+ // checks that there are exactly 7 - tags in the metadata
+ expect((newMetadataString.replace(/ /g, "").match(/
- /g) || []).length).toEqual(7);
+
+ });
+
+ test("Power Query Add Empty ConnectionOnly Array test", async () => {
+ const metadataStr: string = pqSingleQueryMetadataXmlMock;
+ const newMetadataStr: string = addConnectionOnlyQueriesToMetadataStr(metadataStr, []);
+ expect(newMetadataStr.replace(/ /g, "")).toEqual(pqSingleQueryMetadataXmlMock.replace(/ /g, ""));
+ });
});
diff --git a/tests/mocks/xmlMocks.ts b/tests/mocks/xmlMocks.ts
index e6aff56..d22e98b 100644
--- a/tests/mocks/xmlMocks.ts
+++ b/tests/mocks/xmlMocks.ts
@@ -15,3 +15,13 @@ export const pqMetadataXmlMockPart1 =
'
- AllFormulas
- Formula Section1/newQueryName ';
export const pqMetadataXmlMockPart2 =
'
- Formula Section1/newQueryName/Source
';
+
+ export const pqConnectionOnlyMetadataXmlMockPart1 = (queryName: string) => {
+ return '- FormulaSection1/'+ queryName + '';
+};
+
+export const pqConnectionOnlyMetadataXmlMockPart2 = (queryName: string) => {
+ return '
- FormulaSection1/'+ queryName + '/Source
';
+};
+
+export const pqSingleQueryMetadataXmlMock = `- AllFormulas
- FormulaSection1/Query1
- FormulaSection1/Query1/Source
`;
diff --git a/tests/pqUtils.test.ts b/tests/pqUtils.test.ts
new file mode 100644
index 0000000..e142fd1
--- /dev/null
+++ b/tests/pqUtils.test.ts
@@ -0,0 +1,44 @@
+import { pqUtils } from "../src/utils/";
+
+describe("Pq Utils tests", () => {
+ test("tests that validation fails when non unique query names are given", () => {
+ try {
+ pqUtils.validateMultipleQueryNames([{ queryName: " QuErY2 ", queryMashup: "" }, { queryName: "queRy2 ", queryMashup: "" }], "Query1");
+ // If the above line doesn't throw an error, the test fails
+ expect(true).toEqual(false);
+ } catch (e) {
+ expect(e.message).toEqual("Queries must have unique names");
+ }
+ try {
+ pqUtils.validateMultipleQueryNames([{ queryName: " qUeRy1 ", queryMashup: "" }, { queryName: "Query2", queryMashup: "" }], " QuERy1 ");
+ // If the above line doesn't throw an error, the test fails
+ expect(true).toEqual(false);
+ } catch (e) {
+ expect(e.message).toEqual("Queries must have unique names");
+ }
+ });
+
+ test("tests that validation succeeds when valid unique query names are given", () => {
+ try {
+ pqUtils.validateMultipleQueryNames([{ queryName: "Query 1", queryMashup: "" }, { queryName: "Query1", queryMashup: "" }], "Query2");
+ expect(true).toEqual(true);
+ } catch (e) {
+ // If the above line throws an error, the test fails
+ expect(true).toEqual(false);
+ }
+ try {
+ pqUtils.validateMultipleQueryNames([{ queryName: "Query 1", queryMashup: "" }, { queryName: "Query1", queryMashup: "" }], "Query 1");
+ expect(true).toEqual(true);
+ } catch (e) {
+ // If the above line throws an error, the test fails
+ expect(true).toEqual(false);
+ }
+ });
+
+ test("tests generated query name", () => {
+ expect(pqUtils.generateUniqueQueryName(["connection only query-1", "connection only query-2", "connection only query-4"], " Connection only query-3 ")).toEqual("Connection only query-5");
+ expect(pqUtils.generateUniqueQueryName(["connection only query-1", "connection only query-2", "connection only query-3"], "connection only query -4")).toEqual("Connection only query-4");
+ expect(pqUtils.generateUniqueQueryName(["Connection only query - 1", "connection only query-2", "connection only query-3"], "connection only query-4")).toEqual("Connection only query-1");
+ });
+
+});
\ No newline at end of file
diff --git a/tests/xmlInnerPartsUtils.test.ts b/tests/xmlInnerPartsUtils.test.ts
index 94ecc75..5e64087 100644
--- a/tests/xmlInnerPartsUtils.test.ts
+++ b/tests/xmlInnerPartsUtils.test.ts
@@ -61,4 +61,13 @@ describe("Workbook Manager tests", () => {
expect(sharedStringIndex).toEqual(2);
expect(newSharedStrings.replace(/ /g, "")).toContain(sharedStringsXmlMock.replace(/ /g, ""));
});
+
+ test("Connections XML contains new connection", async () => {
+ const serializer = new XMLSerializer();
+ const mockXml = new DOMParser().parseFromString(mockConnectionString, "text/xml");
+ const newConnectionsXml: Document = await xmlInnerPartsUtils.addNewConnection(mockXml, "newQueryName");
+ const newConnectionsXmlString = serializer.serializeToString(newConnectionsXml);
+ expect((newConnectionsXmlString.match(/