Skip to content
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 {
loadedQueryName: string;
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",
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);
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[]) => {
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);
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);
newItem.appendChild(stableEntries);

return newItem;
};

const createConnectionOnlyEntries = (metadataDoc: Document) => {
const stableEntries: Element = metadataDoc.createElementNS(metadataDoc.documentElement.namespaceURI, element.stableEntries);

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);
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");
newConnection.setAttribute(elementAttributes.refreshedVersion, falseValue);
newConnection.setAttribute(elementAttributes.background, trueValue);
const newDbPr = connectionsDoc.createElementNS(connectionsDoc.documentElement.namespaceURI, element.dbpr);
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);
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);
});

};

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);
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