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 = `AllFormulasFormulaSection1/Query1FormulaSection1/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(/