Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

T shanim/enable multiple queries #83

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
7 changes: 7 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ export interface QueryInfo {
queryName?: string;
}

export interface MultipleQueriesInfo {
shanialbeck marked this conversation as resolved.
Show resolved Hide resolved
loadedQueryName: string;
shanialbeck marked this conversation as resolved.
Show resolved Hide resolved
refreshOnOpen: boolean;
connectionOnlyQueryNames: string[];
mashupDocument: string;
}

export interface DocProps {
title?: string | null;
subject?: string | null;
Expand Down
22 changes: 22 additions & 0 deletions src/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -85,6 +87,9 @@ export const element = {
dimension: "dimension",
selection: "selection",
kindCell: "c",
connection: "connection",
connections: "connections",
dbpr: "dbPr",
};

export const elementAttributes = {
Expand Down Expand Up @@ -117,6 +122,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 = {
Expand All @@ -131,6 +149,10 @@ export const elementAttributesValues = {
connection: (queryName: string) => `Provider=Microsoft.Mashup.OleDb.1;Data Source=$Workbook$;Location="${queryName}";`,
connectionCommand: (queryName: string) => `SELECT * FROM [${queryName}]`,
tableResultType: () => "sTable",
connectionOnlyResultType: () => "sConnectionOnly",
shanialbeck marked this conversation as resolved.
Show resolved Hide resolved
fillStatusComplete: () => "sComplete",
fillErrorCodeUnknown: () => "sUnknown",
randomizedUid: () => "{" + v4().toUpperCase() + "}",
};

export const defaults = {
Expand Down
139 changes: 139 additions & 0 deletions src/utils/mashupDocumentParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,17 @@ export const replaceSingleQuery = async (base64Str: string, queryName: string, q
return base64.fromByteArray(newMashup);
};

export const addConnectionOnlyQuery = async (base64Str: string, connectionOnlyQueryNames: string[]): Promise<string> => {
var { version, packageOPC, permissionsSize, permissions, metadata, endBuffer } = getPackageComponents(base64Str);
const packageSizeBuffer: Uint8Array = arrayUtils.getInt32Buffer(packageOPC.byteLength);
const permissionsSizeBuffer: Uint8Array = arrayUtils.getInt32Buffer(permissionsSize);
const newMetadataBuffer: Uint8Array = addConnectionOnlyQueryMetadata(metadata, connectionOnlyQueryNames);
const metadataSizeBuffer: Uint8Array = arrayUtils.getInt32Buffer(newMetadataBuffer.byteLength);
const newMashup: Uint8Array = arrayUtils.concatArrays(version, packageSizeBuffer, packageOPC, permissionsSizeBuffer, permissions, metadataSizeBuffer, newMetadataBuffer, endBuffer);

return base64.fromByteArray(newMashup);
}

type PackageComponents = {
version: Uint8Array;
packageOPC: Uint8Array;
Expand Down Expand Up @@ -150,3 +161,131 @@ export const editSingleQueryMetadata = (metadataArray: Uint8Array, metadata: Met

return newMetadataArray;
};

const addConnectionOnlyQueryMetadata = (metadataArray: Uint8Array, connectionOnlyQueryNames: string[]) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the name of the method is Query (one) but you send a list of queries

// extract metadataXml
const mashupArray: ArrayReader = new arrayUtils.ArrayReader(metadataArray.buffer);
shanialbeck marked this conversation as resolved.
Show resolved Hide resolved
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 = updateConnectionOnlyMetadataStr(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;
};

const updateConnectionOnlyMetadataStr = (metadataString: string, connectionOnlyQueryNames: string[]) => {
shanialbeck marked this conversation as resolved.
Show resolved Hide resolved
const parser: DOMParser = new DOMParser();
let updatedMetdataString: string = metadataString;
connectionOnlyQueryNames.forEach((queryName: string) => {
const metadataDoc: Document = parser.parseFromString(updatedMetdataString, xmlTextResultType);
const items: Element = metadataDoc.getElementsByTagName(element.items)[0];
const stableEntriesItem: Element = createStableEntriesItem(metadataDoc, queryName);
shanialbeck marked this conversation as resolved.
Show resolved Hide resolved
items.appendChild(stableEntriesItem);
const sourceItem: Element = createSourceItem(metadataDoc, queryName);
items.appendChild(sourceItem);
const serializer: XMLSerializer = new XMLSerializer();
updatedMetdataString = serializer.serializeToString(metadataDoc);
});

return updatedMetdataString;
};

const createSourceItem = (metadataDoc: Document, queryName: string) => {
const newItemSource: Element = metadataDoc.createElementNS(metadataDoc.documentElement.namespaceURI, element.item);
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 = `Section1/${queryName}/Source`;
newItemLocation.appendChild(newItemType);
newItemLocation.appendChild(newItemPath);
newItemSource.appendChild(newItemLocation);

return newItemSource;
};

const createStableEntriesItem = (metadataDoc: Document, queryName: string) => {
const newItem: Element = metadataDoc.createElementNS(metadataDoc.documentElement.namespaceURI, element.item);
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 = `Section1/${queryName}`;
newItemLocation.appendChild(newItemType);
newItemLocation.appendChild(newItemPath);
newItem.appendChild(newItemLocation);
const stableEntries: Element = createConnectionOnlyEntries(metadataDoc);
shanialbeck marked this conversation as resolved.
Show resolved Hide resolved
newItem.appendChild(stableEntries);

return newItem;
};

const createConnectionOnlyEntries = (metadataDoc: Document) => {
shanialbeck marked this conversation as resolved.
Show resolved Hide resolved
const stableEntries: Element = metadataDoc.createElementNS(metadataDoc.documentElement.namespaceURI, element.stableEntries);
shanialbeck marked this conversation as resolved.
Show resolved Hide resolved

const IsPrivate: Element = metadataDoc.createElementNS(metadataDoc.documentElement.namespaceURI, element.entry);
IsPrivate.setAttribute(elementAttributes.type, elementAttributes.isPrivate);
IsPrivate.setAttribute(elementAttributes.value, "l0");

stableEntries.appendChild(IsPrivate);
const FillEnabled: Element = metadataDoc.createElementNS(metadataDoc.documentElement.namespaceURI, element.entry);
FillEnabled.setAttribute(elementAttributes.type, elementAttributes.fillEnabled);
FillEnabled.setAttribute(elementAttributes.value, "l0");
stableEntries.appendChild(FillEnabled);

const FillObjectType: Element = metadataDoc.createElementNS(metadataDoc.documentElement.namespaceURI, element.entry);
FillObjectType.setAttribute(elementAttributes.type, elementAttributes.fillObjectType);
FillObjectType.setAttribute(elementAttributes.value, elementAttributesValues.connectionOnlyResultType());
stableEntries.appendChild(FillObjectType);

const FillToDataModelEnabled: Element = metadataDoc.createElementNS(metadataDoc.documentElement.namespaceURI, element.entry);
FillToDataModelEnabled.setAttribute(elementAttributes.type, elementAttributes.fillToDataModelEnabled);
FillToDataModelEnabled.setAttribute(elementAttributes.value, "l0");
stableEntries.appendChild(FillToDataModelEnabled);

const FillLastUpdated: Element = metadataDoc.createElementNS(metadataDoc.documentElement.namespaceURI, element.entry);
FillLastUpdated.setAttribute(elementAttributes.type, elementAttributes.fillLastUpdated);
const nowTime: string = new Date().toISOString();
FillLastUpdated.setAttribute(elementAttributes.value, (elementAttributes.day + nowTime).replace(/Z/, "0000Z"));
stableEntries.appendChild(FillLastUpdated);

const ResultType: Element = metadataDoc.createElementNS(metadataDoc.documentElement.namespaceURI, element.entry);
ResultType.setAttribute(elementAttributes.type, elementAttributes.resultType);
ResultType.setAttribute(elementAttributes.value, elementAttributesValues.tableResultType());
stableEntries.appendChild(ResultType);

const FilledCompleteResultToWorksheet: Element = metadataDoc.createElementNS(metadataDoc.documentElement.namespaceURI, element.entry);
FilledCompleteResultToWorksheet.setAttribute(elementAttributes.type, elementAttributes.filledCompleteResultToWorksheet);
FilledCompleteResultToWorksheet.setAttribute(elementAttributes.value, "l0");
stableEntries.appendChild(FilledCompleteResultToWorksheet);

const AddedToDataModel: Element = metadataDoc.createElementNS(metadataDoc.documentElement.namespaceURI, element.entry);
AddedToDataModel.setAttribute(elementAttributes.type, elementAttributes.addedToDataModel);
AddedToDataModel.setAttribute(elementAttributes.value, "l0");
stableEntries.appendChild(AddedToDataModel);

const FillErrorCode: Element = metadataDoc.createElementNS(metadataDoc.documentElement.namespaceURI, element.entry);
FillErrorCode.setAttribute(elementAttributes.type, elementAttributes.fillErrorCode);
FillErrorCode.setAttribute(elementAttributes.value, elementAttributesValues.fillErrorCodeUnknown());
stableEntries.appendChild(FillErrorCode);

const FillStatus: Element = metadataDoc.createElementNS(metadataDoc.documentElement.namespaceURI, element.entry);
FillStatus.setAttribute(elementAttributes.type, elementAttributes.fillStatus);
FillStatus.setAttribute(elementAttributes.value, elementAttributesValues.fillStatusComplete());
stableEntries.appendChild(FillStatus);

return stableEntries;
};
23 changes: 23 additions & 0 deletions src/utils/xmlInnerPartsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,28 @@ const updatePivotTable = (tableXmlString: string, connectionId: string, refreshO
return { isPivotTableUpdated, newPivotTable };
};

const addNewConnection = async (connectionsXmlString: string, queryName: string): Promise<string> => {
const parser: DOMParser = new DOMParser();
const serializer: XMLSerializer = new XMLSerializer();
const connectionsDoc: Document = parser.parseFromString(connectionsXmlString, xmlTextResultType);
const connections = connectionsDoc.getElementsByTagName(element.connections)[0];
const newConnection = connectionsDoc.createElementNS(connectionsDoc.documentElement.namespaceURI, element.connection);
connections.append(newConnection);
shanialbeck marked this conversation as resolved.
Show resolved Hide resolved
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.type, "5");
shanialbeck marked this conversation as resolved.
Show resolved Hide resolved
newConnection.setAttribute(elementAttributes.refreshedVersion, falseValue);
newConnection.setAttribute(elementAttributes.background, trueValue);
shanialbeck marked this conversation as resolved.
Show resolved Hide resolved
const newDbPr = connectionsDoc.createElementNS(connectionsDoc.documentElement.namespaceURI, element.dbpr);
shanialbeck marked this conversation as resolved.
Show resolved Hide resolved
newDbPr.setAttribute(elementAttributes.connection, elementAttributesValues.connection(queryName));
newDbPr.setAttribute(elementAttributes.command, elementAttributesValues.connectionCommand(queryName));
newConnection.appendChild(newDbPr);
return serializer.serializeToString(connectionsDoc);
};

export default {
updateDocProps,
clearLabelInfo,
Expand All @@ -277,4 +299,5 @@ export default {
updatePivotTablesandQueryTables,
updateQueryTable,
updatePivotTable,
addNewConnection,
};
24 changes: 21 additions & 3 deletions src/utils/xmlPartsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
sheetsXmlPath,
sheetsNotFoundErr,
} from "./constants";
import { replaceSingleQuery } from "./mashupDocumentParser";
import { addConnectionOnlyQuery, replaceSingleQuery } from "./mashupDocumentParser";
import { FileConfigs, TableData } from "../types";
import pqUtils from "./pqUtils";
import xmlInnerPartsUtils from "./xmlInnerPartsUtils";
Expand All @@ -27,15 +27,19 @@ const updateWorkbookDataAndConfigurations = async (zip: JSZip, fileConfigs?: Fil
await tableUtils.updateTableInitialDataIfNeeded(zip, tableData, updateQueryTable);
};

const updateWorkbookPowerQueryDocument = async (zip: JSZip, queryName: string, queryMashupDoc: string): Promise<void> => {
const updateWorkbookPowerQueryDocument = async (zip: JSZip, queryName: string, queryMashupDoc: string, connectionOnlyQueryNames?: string[]): Promise<void> => {
const old_base64: string | undefined = await pqUtils.getBase64(zip);

if (!old_base64) {
throw new Error(base64NotFoundErr);
}

const new_base64: string = await replaceSingleQuery(old_base64, queryName, queryMashupDoc);
shanialbeck marked this conversation as resolved.
Show resolved Hide resolved
await pqUtils.setBase64(zip, new_base64);
let updated_base64: string = new_base64;
if (connectionOnlyQueryNames) {
updated_base64 = await addConnectionOnlyQuery(new_base64, connectionOnlyQueryNames);
}
await pqUtils.setBase64(zip, updated_base64);
};

const updateWorkbookSingleQueryAttributes = async (zip: JSZip, queryName: string, refreshOnOpen: boolean): Promise<void> => {
Expand Down Expand Up @@ -70,8 +74,22 @@ const updateWorkbookSingleQueryAttributes = async (zip: JSZip, queryName: string
await xmlInnerPartsUtils.updatePivotTablesandQueryTables(zip, queryName, refreshOnOpen, connectionId!);
};

const addConnectionOnlyQueriesToWorkbook = async (zip: JSZip, connectionOnlyQueryNames: string[]): Promise<void> => {
// Update connections
let connectionsXmlString: string | undefined = await zip.file(connectionsXmlPath)?.async(textResultType);
if (connectionsXmlString === undefined) {
throw new Error(connectionsNotFoundErr);
}

connectionOnlyQueryNames.forEach(async (queryName: string) => {
connectionsXmlString = await xmlInnerPartsUtils.addNewConnection(connectionsXmlString!, queryName);
shanialbeck marked this conversation as resolved.
Show resolved Hide resolved
});

};

export default {
updateWorkbookDataAndConfigurations,
updateWorkbookPowerQueryDocument,
updateWorkbookSingleQueryAttributes,
addConnectionOnlyQueriesToWorkbook,
};
30 changes: 29 additions & 1 deletion src/workbookManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
tableNotFoundErr,
templateFileNotSupportedErr,
} from "./utils/constants";
import { QueryInfo, TableData, Grid, FileConfigs } from "./types";
import { QueryInfo, TableData, Grid, FileConfigs, MultipleQueriesInfo } from "./types";
import { generateSingleQueryMashup } from "./generators";

export const generateSingleQueryWorkbook = async (query: QueryInfo, initialDataGrid?: Grid, fileConfigs?: FileConfigs): Promise<Blob> => {
Expand All @@ -40,6 +40,22 @@ export const generateSingleQueryWorkbook = async (query: QueryInfo, initialDataG
return await generateSingleQueryWorkbookFromZip(zip, query, fileConfigs, tableData);
};

export const generateMultipleQueryWorkbook = async (queries: MultipleQueriesInfo, initialDataGrid?: Grid, fileConfigs?: FileConfigs): Promise<Blob> => {
const templateFile: File | undefined = fileConfigs?.templateFile;
if (templateFile !== undefined && initialDataGrid !== undefined) {
throw new Error(templateWithInitialDataErr);
}

pqUtils.validateQueryName(queries.loadedQueryName);

const zip: JSZip =
templateFile === undefined ? await JSZip.loadAsync(SIMPLE_QUERY_WORKBOOK_TEMPLATE, { base64: true }) : await JSZip.loadAsync(templateFile);

const tableData = initialDataGrid ? gridUtils.parseToTableData(initialDataGrid) : undefined;

return await generateMultipleQueryWorkbookFromZip(zip, queries, fileConfigs, tableData);
};

export const generateTableWorkbookFromHtml = async (htmlTable: HTMLTableElement, fileConfigs?: FileConfigs): Promise<Blob> => {
if (fileConfigs?.templateFile !== undefined) {
throw new Error(templateFileNotSupportedErr);
Expand Down Expand Up @@ -81,6 +97,18 @@ const generateSingleQueryWorkbookFromZip = async (zip: JSZip, query: QueryInfo,
});
};

const generateMultipleQueryWorkbookFromZip = async (zip: JSZip, queries: MultipleQueriesInfo, fileConfigs?: FileConfigs, tableData?: TableData): Promise<Blob> => {
await xmlPartsUtils.updateWorkbookPowerQueryDocument(zip, queries.loadedQueryName, queries.mashupDocument, queries.connectionOnlyQueryNames);
shanialbeck marked this conversation as resolved.
Show resolved Hide resolved
await xmlPartsUtils.updateWorkbookSingleQueryAttributes(zip, queries.loadedQueryName, queries.refreshOnOpen);
await xmlPartsUtils.updateWorkbookDataAndConfigurations(zip, fileConfigs, tableData, true /*updateQueryTable*/);
await xmlPartsUtils.addConnectionOnlyQueriesToWorkbook(zip, queries.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)
Expand Down