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
24 changes: 24 additions & 0 deletions src/generators.ts
Original file line number Diff line number Diff line change
@@ -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 =>
`<?xml version="1.0" encoding="utf-16"?><DataMashup xmlns="http://schemas.microsoft.com/DataMashup">${base64}</DataMashup>`;

Expand All @@ -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};`;
shanialbeck marked this conversation as resolved.
Show resolved Hide resolved
});

return mashup;
}

export const generateCustomXmlFilePath = (i: number): string => `customXml/item${i}.xml`;
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 QueriesInfo {
loadedQuery: QueryInfo;
connectionOnlyQueries: ConnectionOnlyQueryInfo[];
}

export type ConnectionOnlyQueryInfo = Omit<QueryInfo, 'refreshOnOpen'>;

export interface DocProps {
title?: string | null;
subject?: string | null;
Expand Down
34 changes: 33 additions & 1 deletion 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 @@ -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";
Expand Down Expand Up @@ -85,6 +89,9 @@ export const element = {
dimension: "dimension",
selection: "selection",
kindCell: "c",
connection: "connection",
connections: "connections",
databaseProps: "dbPr",
};

export const elementAttributes = {
Expand All @@ -99,6 +106,7 @@ export const elementAttributes = {
name: "name",
description: "description",
id: "id",
typeLowerCase: "type",
type: "Type",
value: "Value",
relationshipInfo: "RelationshipInfoContainer",
Expand All @@ -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 = {
Expand All @@ -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",
};
Expand Down
102 changes: 101 additions & 1 deletion src/utils/mashupDocumentParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
divider,
elementAttributes,
elementAttributesValues,
itemPathTextContext,
powerQueryResultType,
} from "./constants";
import { arrayUtils } from ".";
import { Metadata } from "../types";
Expand All @@ -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<string> => {
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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
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 = 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;
};
49 changes: 48 additions & 1 deletion src/utils/pqUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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[] => {
shanialbeck marked this conversation as resolved.
Show resolved Hide resolved
// Generate unique name for queries without a name
queries.forEach((query: ConnectionOnlyQueryInfo) => {
if (!query.queryName) {
query.queryName = generateUniqueQueryName(queryNames, loadedQueryName);
shanialbeck marked this conversation as resolved.
Show resolved Hide resolved
queryNames.push(query.queryName.toLowerCase());
}
});

return queries;
};

const generateUniqueQueryName = (queryNames: string[], loadedQueryName: string,): string => {
shanialbeck marked this conversation as resolved.
Show resolved Hide resolved
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
};
22 changes: 22 additions & 0 deletions src/utils/xmlInnerPartsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,27 @@ const updatePivotTable = (tableXmlString: string, connectionId: string, refreshO
return { isPivotTableUpdated, newPivotTable };
};

const addNewConnection = async (connectionsDoc: Document, queryName: string): Promise<Document> => {
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);
shanialbeck marked this conversation as resolved.
Show resolved Hide resolved
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,
Expand All @@ -277,4 +298,5 @@ export default {
updatePivotTablesandQueryTables,
updateQueryTable,
updatePivotTable,
addNewConnection,
};
Loading