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

Fixed templatizing of Forms and Workflows #1468

Merged
merged 5 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions packages/common/src/completeItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,8 @@ export async function getCompleteItem(
);

} else if (itemBase.type === "Workflow") {
const user = await restHelpersGet.getUser(authentication);
let server;
const portal = new URL(authentication.portal);
if (!portal.origin.endsWith(".arcgis.com") && !portal.origin.endsWith(".esri.com")) {
server = authentication.portal.replace("/sharing/rest", "");
}
const workflowConfigZip = await restHelpers.getWorkflowConfigurationZip(itemBase.id, authentication, user.orgId, server);
const workflowBaseUrl = await workflowHelpers.getWorkflowBaseURL(authentication);
const workflowConfigZip = await restHelpers.getWorkflowConfigurationZip(itemBase.id, workflowBaseUrl, authentication);
completeItem.workflowConfiguration = await workflowHelpers.extractWorkflowFromZipFile(workflowConfigZip);
}

Expand Down
3 changes: 2 additions & 1 deletion packages/common/src/deleteHelpers/removeItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ export function removeItems(
const options = await createHubRequestOptions(authentication);
return hubSites.removeSite(itemToDelete.id, options);
} else if (itemToDelete.type === "Workflow") {
return workflowHelpers.deleteWorkflowItem(itemToDelete.id, authentication);
const workflowBaseUrl = await workflowHelpers.getWorkflowBaseURL(authentication);
return workflowHelpers.deleteWorkflowItem(itemToDelete.id, workflowBaseUrl, authentication);
} else {
return restHelpers.removeItem(itemToDelete.id, authentication);
}
Expand Down
48 changes: 0 additions & 48 deletions packages/common/src/formHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,61 +14,13 @@
* limitations under the License.
*/

import * as generalHelpers from "./generalHelpers";
import * as interfaces from "./interfaces";
import * as zipUtils from "./zip-utils";
import { updateItem } from "./restHelpers";
import { IItemUpdate, UserSession } from "./interfaces";
import JSZip from "jszip";

// ------------------------------------------------------------------------------------------------------------------ //

/**
* Gets the webhooks from a Form zip object's *.info file.
*
* @param zipObject Zip file object from which to get the webhooks
* @returns Promise that resolves to an array of webhooks
*/
export async function getWebHooksFromZipObject(
zipObject: JSZip
): Promise<string[]> {
const zipObjectContents: interfaces.IZipObjectContentItem[] = await zipUtils.getZipObjectContents(zipObject);
let webhooks: string[] = [];
zipObjectContents.forEach(
(zipFile: interfaces.IZipObjectContentItem) => {
if (zipFile.file.endsWith(".info")) {
const infoFileJson = JSON.parse(zipFile.content as string);
webhooks = generalHelpers.getProp(infoFileJson, "notificationsInfo.webhooks") || [];
}
}
);
return Promise.resolve(webhooks);
}

/**
* Sets the webhooks in a Form zip object's *.info file.
*
* @param zipObject Zip file object in which to set the webhooks
* @param webHooks Array of webhooks to set
* @returns Promise that resolves to the updated zip object
*/
export async function setWebHooksInZipObject(
zipObject: JSZip,
webHooks: any[]
): Promise<JSZip> {
const zipObjectContents: interfaces.IZipObjectContentItem[] = await zipUtils.getZipObjectContents(zipObject);
zipObjectContents.forEach(
(zipFile: interfaces.IZipObjectContentItem) => {
if (zipFile.file.endsWith(".info")) {
const infoFileJson = JSON.parse(zipFile.content as string);
generalHelpers.setProp(infoFileJson, "notificationsInfo.webhooks", webHooks);
zipObject.file(zipFile.file, JSON.stringify(infoFileJson));
}
}
);
return Promise.resolve(zipObject);
}

/**
* Updates an item with a zip object, including any webhooks.
*
Expand Down
29 changes: 10 additions & 19 deletions packages/common/src/restHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,6 @@ import {
addToServiceDefinition as svcAdminAddToServiceDefinition,
createFeatureService as svcAdminCreateFeatureService
} from "@esri/arcgis-rest-service-admin";
import {
getWorkflowManagerUrlRoot
} from "./workflowHelpers";
import {
getWorkforceDependencies,
isWorkforceProject,
Expand Down Expand Up @@ -1370,20 +1367,17 @@ export function getFeatureServiceProperties(
* Fetches the configuration of a workflow.
*
* @param itemId Id of the workflow item
* @param workflowBaseUrl URL of the workflow manager, e.g., "https://workflow.arcgis.com/orgId"
* @param authentication Credentials for the request to AGOL
* @param orgId Id of organization whose license is to be checked; only used if `enterpriseWebAdaptorUrl` is falsy
* @param workflowURL URL of the workflow manager, e.g., "https://workflow.arcgis.com"
* @returns Promise resolving with the workflow configuration in a zip file
* @throws {WorkflowJsonExceptionDTO} if request to workflow manager fails
*/
export async function getWorkflowConfigurationZip(
itemId: string,
authentication: UserSession,
orgId: string | undefined,
workflowURL: string
workflowBaseUrl: string,
authentication: UserSession
): Promise<File> {
const workflowUrlRoot = getWorkflowManagerUrlRoot(orgId, workflowURL);
const url = `${workflowUrlRoot}/admin/${itemId}/export`;
const url = `${workflowBaseUrl}/admin/${itemId}/export`;

return request(url, {
authentication,
Expand All @@ -1401,23 +1395,20 @@ export async function getWorkflowConfigurationZip(
/**
* Sets the configuration of a workflow.
*
* @param configurationZipFile Configuration files in a zip file
* @param itemId Id of the workflow item
* @param configurationZipFile Configuration files in a zip file
* @param workflowBaseUrl URL of the workflow manager, e.g., "https://workflow.arcgis.com/orgId"
* @param authentication Credentials for the request to AGOL
* @param orgId Id of organization whose license is to be checked; only used if `enterpriseWebAdaptorUrl` is falsy
* @param workflowURL URL of the workflow manager, e.g., "https://workflow.arcgis.com"
* @returns Promise resolving with the workflow configuration in a zip file
* @throws {WorkflowJsonExceptionDTO} if request to workflow manager fails
*/
export async function setWorkflowConfigurationZip(
configurationZipFile: File,
itemId: string,
authentication: UserSession,
orgId: string | undefined,
workflowURL: string
configurationZipFile: File,
workflowBaseUrl: string,
authentication: UserSession
): Promise<IStatusResponse> {
const workflowUrlRoot = getWorkflowManagerUrlRoot(orgId, workflowURL);
const url = `${workflowUrlRoot}/admin/${itemId}/import`;
const url = `${workflowBaseUrl}/admin/${itemId}/import`;

return request(url, {
authentication,
Expand Down
122 changes: 54 additions & 68 deletions packages/common/src/workflowHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import * as interfaces from "./interfaces";
import * as request from "@esri/arcgis-rest-request";
import * as restHelpersGet from "./restHelpersGet";
import * as zipUtils from "./zip-utils";
import { removeGroup } from "./restHelpers";
import { getEnterpriseServers, getItemDataAsJson } from "./restHelpersGet";
Expand Down Expand Up @@ -57,32 +58,21 @@ export async function compressWorkflowIntoZipFile(
* Deletes a workflow.
*
* @param itemId Id of the workflow item
* @param workflowBaseUrl URL of the workflow manager, e.g., "https://workflow.arcgis.com/orgId"
* @param authentication Credentials for the request to AGOL
* @returns Promise resolving with success or faliure of the request
*/
export async function deleteWorkflowItem(
itemId: string,
workflowBaseUrl: string,
authentication: interfaces.UserSession,
): Promise<boolean> {
// Get the user
const user: interfaces.IUser = await authentication.getUser(authentication);
const orgId = user.orgId;

const portal = await authentication.getPortal({ authentication});
const workflowURL: string | undefined = portal.helperServices?.workflowManager?.url;
if (!workflowURL) {
return Promise.reject({
message: "Workflow Manager is not enabled for this organization."
});
}

// Get the id of the Workflow Manager Admin group because the group has to be deleted separately
const data = await getItemDataAsJson(itemId, authentication);
const adminGroupId = data?.groupId;

// Delete the item
const workflowUrlRoot = getWorkflowManagerUrlRoot(orgId, workflowURL);
const url = `${workflowUrlRoot}/admin/${itemId}`;
const url = `${workflowBaseUrl}/admin/${itemId}`;

const options: request.IRequestOptions = {
authentication,
Expand Down Expand Up @@ -129,20 +119,17 @@ export async function extractWorkflowFromZipFile(
/**
* Check the license capability of Workflow Manager Server.
*
* @param orgId Id of organization whose license is to be checked; only used if `enterpriseWebAdaptorUrl` is falsy
* @param workflowURL URL of the workflow manager, e.g., "https://workflow.arcgis.com"
* @param workflowBaseUrl URL of the workflow manager, e.g., "https://workflow.arcgis.com/orgId"
* @param authentication Credentials for the request to AGO
* @returns Promise resolving with a boolean indicating whether the organization has the license
* @throws {WorkflowJsonExceptionDTO} if request to workflow manager fails
*/
export async function getWorkflowManagerAuthorized(
orgId: string | undefined,
workflowURL: string,
authentication?: interfaces.UserSession
workflowBaseUrl: string,
authentication: interfaces.UserSession
): Promise<boolean> {
try {
const workflowUrlRoot = getWorkflowManagerUrlRoot(orgId, workflowURL);
const url = `${workflowUrlRoot}/checkStatus`;
const url = `${workflowBaseUrl}/checkStatus`;

const options: request.IRequestOptions = {
authentication,
Expand All @@ -161,14 +148,58 @@ export async function getWorkflowManagerAuthorized(
}
}

/**
* Determines the URL to the Workflow Manager.
*
* @param authentication Authenticated user session
* @param portalResponse Response from portal "self" call; will be fetched if not supplied
* @param orgId Id of organization whose license is to be checked; if truthy, the URL will be for AGO;
* if falsy, the URL will be for Workflow Manager Enterprise
* @returns A URL based on ArcGIS Online or Enterprise, e.g., "https://abc123.esri.com:6443/arcgis"
*/
export async function getWorkflowBaseURL(
authentication: interfaces.UserSession,
portalResponse?: interfaces.IPortal,
orgId?: string
): Promise<string> {
let workflowServerUrl: string;

if (!portalResponse) {
const user = await restHelpersGet.getUser(authentication);
orgId = orgId ?? user.orgId;

portalResponse = await restHelpersGet.getPortal("", authentication);
}

const portalURL = `https://${portalResponse.portalHostname}`;
const portalRestURL = `${portalURL}/sharing/rest`;

if (portalResponse.isPortal) {
// Enterprise
workflowServerUrl = await _getWorkflowEnterpriseServerRootURL(portalRestURL, authentication);
} else {
// ArcGIS Online
workflowServerUrl = portalResponse.helperServices?.workflowManager?.url ?? portalURL;
}

return Promise.resolve(
orgId
? `${workflowServerUrl}/${orgId}`
: `${workflowServerUrl}/workflow`
);
}

// ------------------------------------------------------------------------------------------------------------------ //

/**
* Get the URL for the Workflow Manager Enterprise application.
*
* @param portalRestUrl URL of the portal REST endpoint, e.g., "https://gisserver.domain.com/server/rest/services"
* @param authentication Credentials for the request to AGO
* @returns URL for the Workflow Manager Enterprise application, or an empty string if Workflow Manager is not enabled
* @returns URL for the Workflow Manager Enterprise application (e.g., "https://abc123.esri.com:6443/arcgis"),
* or an empty string if Workflow Manager is not enabled
*/
export async function getWorkflowEnterpriseServerURL(
export async function _getWorkflowEnterpriseServerRootURL(
portalRestUrl: string,
authentication: interfaces.UserSession
): Promise<string> {
Expand All @@ -183,49 +214,4 @@ export async function getWorkflowEnterpriseServerURL(
return "";
}
return workflowServer.url as string;
}

/**
* Get the root URL for the Workflow Manager application.
*
* @param orgId Id of organization whose license is to be checked; if truthy, the URL will be for AGO;
* if falsy, the URL will be for Workflow Manager Enterprise
* @param workflowURL URL of the workflow manager, e.g., "https://workflow.arcgis.com"
* @returns URL for the Workflow Manager application
*/
export function getWorkflowManagerUrlRoot(
orgId: string | undefined,
workflowURL: string
): string {
return orgId
? `${workflowURL}/${orgId}`
: `${workflowURL}/workflow`;
}

/**
* Determines the Workflow Manager URL to use for the deployment if not supplied.
*
* @param workflowURL Existing workflow URL; if supplied, it's simply returned
* @param portalResponse Response from portal "self" call
* @param authentication Authenticated user session
* @returns workflowURL or a URL based on ArcGIS Online or Enterprise
*/
export async function getWorkflowURL(
workflowURL: string,
portalResponse: interfaces.IPortal,
authentication: interfaces.UserSession
): Promise<string> {
if (!workflowURL) {
const portalURL = `https://${portalResponse.portalHostname}`;
const portalRestURL = `${portalURL}/sharing/rest`;

if (portalResponse.isPortal) {
// Enterprise
workflowURL = await getWorkflowEnterpriseServerURL(portalRestURL, authentication);
} else {
// ArcGIS Online
workflowURL = portalResponse.helperServices?.workflowManager?.url ?? portalURL;
}
}
return Promise.resolve(workflowURL);
}
12 changes: 6 additions & 6 deletions packages/common/test/completeItem.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@ describe("Module `completeItem`: functions for accessing a complete item", () =>
const itemId = "abc";
const config = await zipUtils.jsonToZipFile("jobConfig.json", {"jobTemplates": "abc" }, "config");

const getConfigSpy = spyOn(restHelpers, "getWorkflowConfigurationZip").and.resolveTo(config);
spyOn(workflowHelpers, "getWorkflowBaseURL").and.resolveTo("https://myorg.maps.arcgis.com/myorgid");
spyOn(restHelpers, "getWorkflowConfigurationZip").and.resolveTo(config);
spyOn(restHelpersGet, "getItemBase").and.resolveTo(mockItems.getAGOLItem("Workflow"));
spyOn(restHelpersGet, "getItemDataAsFile").and.resolveTo(mockItems.getAGOLItemData("Workflow"));
spyOn(restHelpersGet, "getItemMetadataAsFile").and.resolveTo(utils.getSampleMetadataAsFile());
Expand All @@ -133,14 +134,14 @@ describe("Module `completeItem`: functions for accessing a complete item", () =>
expect(item.base.id).toEqual("wfw1234567890");
expect(item.thumbnail.name).toEqual("sampleImage");
expect(item.metadata.name).toEqual("metadata.xml");
expect(getConfigSpy.calls.argsFor(0)[3]).toBeUndefined();
});

it("should get a workflow item on esri.com", async () => {
const itemId = "abc";
const config = await zipUtils.jsonToZipFile("jobConfig.json", {"jobTemplates": "abc" }, "config");

const getConfigSpy = spyOn(restHelpers, "getWorkflowConfigurationZip").and.resolveTo(config);
spyOn(workflowHelpers, "getWorkflowBaseURL").and.resolveTo("https://myorg.maps.esri.com/myorgid");
spyOn(restHelpers, "getWorkflowConfigurationZip").and.resolveTo(config);
spyOn(restHelpersGet, "getItemBase").and.resolveTo(mockItems.getAGOLItem("Workflow"));
spyOn(restHelpersGet, "getItemDataAsFile").and.resolveTo(mockItems.getAGOLItemData("Workflow"));
spyOn(restHelpersGet, "getItemMetadataAsFile").and.resolveTo(utils.getSampleMetadataAsFile());
Expand All @@ -165,14 +166,14 @@ describe("Module `completeItem`: functions for accessing a complete item", () =>
expect(item.base.id).toEqual("wfw1234567890");
expect(item.thumbnail.name).toEqual("sampleImage");
expect(item.metadata.name).toEqual("metadata.xml");
expect(getConfigSpy.calls.argsFor(0)[3]).toBeUndefined();
});

it("should get a workflow item on Enterprise", async () => {
const itemId = "abc";
const config = await zipUtils.jsonToZipFile("jobConfig.json", {"jobTemplates": "abc" }, "config");

const getConfigSpy = spyOn(restHelpers, "getWorkflowConfigurationZip").and.resolveTo(config);
spyOn(workflowHelpers, "getWorkflowBaseURL").and.resolveTo("https://gisserver.domain.com/server/workflow");
spyOn(restHelpers, "getWorkflowConfigurationZip").and.resolveTo(config);
spyOn(restHelpersGet, "getItemBase").and.resolveTo(mockItems.getAGOLItem("Workflow"));
spyOn(restHelpersGet, "getItemDataAsFile").and.resolveTo(mockItems.getAGOLItemData("Workflow"));
spyOn(restHelpersGet, "getItemMetadataAsFile").and.resolveTo(utils.getSampleMetadataAsFile());
Expand All @@ -197,7 +198,6 @@ describe("Module `completeItem`: functions for accessing a complete item", () =>
expect(item.base.id).toEqual("wfw1234567890");
expect(item.thumbnail.name).toEqual("sampleImage");
expect(item.metadata.name).toEqual("metadata.xml");
expect(getConfigSpy.calls.argsFor(0)[3]).toEqual("https://gisserver.domain.com/server");
});

it("should handle failure to get an item", done => {
Expand Down
1 change: 1 addition & 0 deletions packages/common/test/deleteHelpers/removeItems.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ describe("Module `removeItems`: removing items from AGO", () => {

spyOn(portal, "unprotectItem").and.resolveTo(utils.getSuccessResponse());
spyOn(workflowHelpers, "deleteWorkflowItem").and.resolveTo(true);
spyOn(workflowHelpers, "getWorkflowBaseURL").and.resolveTo("https://workflow.arcgis.com/workflow");

const expectedResult: interfaces.ISolutionPrecis[] = [{
// Successful deletions
Expand Down
Loading
Loading