diff --git a/demos/compareJSON/package-lock.json b/demos/compareJSON/package-lock.json index 57405da8a..03f909374 100644 --- a/demos/compareJSON/package-lock.json +++ b/demos/compareJSON/package-lock.json @@ -2344,4 +2344,4 @@ "license": "MIT" } } -} \ No newline at end of file +} diff --git a/demos/compareSolutions/package-lock.json b/demos/compareSolutions/package-lock.json index 69cd11ab2..152478d48 100644 --- a/demos/compareSolutions/package-lock.json +++ b/demos/compareSolutions/package-lock.json @@ -2328,4 +2328,4 @@ "license": "MIT" } } -} \ No newline at end of file +} diff --git a/demos/copyItemInfo/package-lock.json b/demos/copyItemInfo/package-lock.json index 5adf36605..e9dfabeb1 100644 --- a/demos/copyItemInfo/package-lock.json +++ b/demos/copyItemInfo/package-lock.json @@ -2344,4 +2344,4 @@ "license": "MIT" } } -} \ No newline at end of file +} diff --git a/demos/copySolutions/package-lock.json b/demos/copySolutions/package-lock.json index aa341f8d6..2cd5cbbc5 100644 --- a/demos/copySolutions/package-lock.json +++ b/demos/copySolutions/package-lock.json @@ -2719,4 +2719,4 @@ "license": "MIT" } } -} \ No newline at end of file +} diff --git a/demos/createSolution/package-lock.json b/demos/createSolution/package-lock.json index 2f6df7c3e..d0bcfa1d3 100644 --- a/demos/createSolution/package-lock.json +++ b/demos/createSolution/package-lock.json @@ -2350,4 +2350,4 @@ "license": "MIT" } } -} \ No newline at end of file +} diff --git a/demos/deleteSolution/package-lock.json b/demos/deleteSolution/package-lock.json index 56de541e8..d45061029 100644 --- a/demos/deleteSolution/package-lock.json +++ b/demos/deleteSolution/package-lock.json @@ -2351,4 +2351,4 @@ "license": "MIT" } } -} \ No newline at end of file +} diff --git a/demos/deploySolution/index.html b/demos/deploySolution/index.html index 30087eed1..2f8df4de3 100644 --- a/demos/deploySolution/index.html +++ b/demos/deploySolution/index.html @@ -100,7 +100,25 @@

Deploy one or more Solutions

Custom Params
- +

+ Paste a JSON object whose properties will be added to the deploy's templateDictionary.params. + Templates can reference these via {{params.<key>...}}. +

+

+ To exercise a buildSolution payload (e.g. from solutions-components), wrap it under a + buildSolution key so templates can resolve it as {{params.buildSolution.items.<id>.title}}: +

+
{
+  "buildSolution": {
+    "solution": { "item": { "id": "<solutionItemId>", ... }, ... },
+    "items": { "<itemId>": { "title": "...", ... } }
+  }
+}
+

+ If the Solution Id field above is empty, it will be pulled from + buildSolution.solution.item.id. +

+


diff --git a/demos/deploySolution/package-lock.json b/demos/deploySolution/package-lock.json index 7314d5db0..b6bfdbda9 100644 --- a/demos/deploySolution/package-lock.json +++ b/demos/deploySolution/package-lock.json @@ -2357,4 +2357,4 @@ "license": "MIT" } } -} \ No newline at end of file +} diff --git a/demos/deploySolution/src/index.ts b/demos/deploySolution/src/index.ts index 5111058f2..1bb95a757 100644 --- a/demos/deploySolution/src/index.ts +++ b/demos/deploySolution/src/index.ts @@ -156,7 +156,7 @@ function go( document.getElementById("output").style.display = "block"; // use the manually entered value, falling back to the select lists - const solutionId = + let solutionId = htmlUtil.getHTMLValue("solutionId") || htmlUtil.getHTMLValue("solutionPicklist"); const folderId = htmlUtil.getHTMLValue("foldersPicklist"); @@ -170,6 +170,23 @@ function go( // Custom Params const customParams = htmlUtil.getHTMLValue("customParams"); + // If no Solution Id was entered, fall back to buildSolution.solution.item.id + // when present in the custom params. + if (!solutionId && typeof customParams === "string" && customParams.trim().length > 0) { + try { + const paramsObj = JSON.parse(customParams); + const embeddedId = paramsObj?.buildSolution?.solution?.item?.id; + if (typeof embeddedId === "string" && embeddedId.length > 0) { + solutionId = embeddedId; + } + } catch (e) { + document.getElementById("input").style.display = "block"; + document.getElementById("output").style.display = "none"; + alert("Custom Params is not valid JSON: " + (e as Error).message); + return; + } + } + // Source credentials const srcHtmlValue = htmlUtil.getHTMLValue("srcPortal"); const srcPortalStr = srcHtmlValue.endsWith('/') ? srcHtmlValue.slice(0, -1) : srcHtmlValue; diff --git a/demos/getItemInfo/package-lock.json b/demos/getItemInfo/package-lock.json index efdf9244c..2354878a5 100644 --- a/demos/getItemInfo/package-lock.json +++ b/demos/getItemInfo/package-lock.json @@ -2344,4 +2344,4 @@ "license": "MIT" } } -} \ No newline at end of file +} diff --git a/demos/implementedTypes/package-lock.json b/demos/implementedTypes/package-lock.json index 59b7b28ab..48d042236 100644 --- a/demos/implementedTypes/package-lock.json +++ b/demos/implementedTypes/package-lock.json @@ -2336,4 +2336,4 @@ "license": "MIT" } } -} \ No newline at end of file +} diff --git a/demos/recreateSolution/package-lock.json b/demos/recreateSolution/package-lock.json index 06a06fe94..cae5f3344 100644 --- a/demos/recreateSolution/package-lock.json +++ b/demos/recreateSolution/package-lock.json @@ -2314,4 +2314,4 @@ "license": "MIT" } } -} \ No newline at end of file +} diff --git a/demos/reuseDeployedItems/package-lock.json b/demos/reuseDeployedItems/package-lock.json index 848f1f7ba..b2276c4d1 100644 --- a/demos/reuseDeployedItems/package-lock.json +++ b/demos/reuseDeployedItems/package-lock.json @@ -2379,4 +2379,4 @@ "license": "MIT" } } -} \ No newline at end of file +} diff --git a/demos/verifySolution/package-lock.json b/demos/verifySolution/package-lock.json index fcecaff79..33c0bfb9d 100644 --- a/demos/verifySolution/package-lock.json +++ b/demos/verifySolution/package-lock.json @@ -2350,4 +2350,4 @@ "license": "MIT" } } -} \ No newline at end of file +} diff --git a/packages/common/src/featureServiceHelpers.ts b/packages/common/src/featureServiceHelpers.ts index 88f89ba10..8eeffbffa 100644 --- a/packages/common/src/featureServiceHelpers.ts +++ b/packages/common/src/featureServiceHelpers.ts @@ -553,10 +553,6 @@ export function setNamesAndTitles(templates: IItemTemplate[]): IItemTemplate[] { // If the name already contains a GUID remove it baseName = baseName.replace(/_[0-9A-F]{32}/gi, ""); - // Replace characters that are not allowed in a feature service name with "_" - // Disallowed: '#', '%', '&', '"', '\', '/', '+', '?', ':', '*', '<', '>', ' ', '\t' - baseName = baseName.replace(/[#%&"\\/+?:*<> \t]/g, "_"); - // The name length limit is 98 // Limit the baseName to 50 characters before the _ // If the baseName includes '{{params' it is likely being used in a template replacement, so do not truncate. diff --git a/packages/common/src/restHelpers.ts b/packages/common/src/restHelpers.ts index 9a61c7c74..249026724 100644 --- a/packages/common/src/restHelpers.ts +++ b/packages/common/src/restHelpers.ts @@ -2105,6 +2105,17 @@ export function _countRelationships(layers: any[]): number { return layers.reduce(reducer, 0); } +/** + * Remove spaces and replace other characters that are not allowed in a feature service name with "_". + * Spaces are removed entirely. Disallowed (replaced with "_"): '#', '%', '&', '"', '\', '/', '+', '?', ':', '*', '<', '>', '\t' + * + * @param name The candidate service name + * @returns The sanitized name, or the input unchanged if it is not a string + */ +export function sanitizeFeatureServiceName(name: string): string { + return typeof name === "string" ? name.replace(/ /g, "").replace(/[#%&"\\/+?:*<>\t]/g, "_") : name; +} + /** * Gets the full definitions of the layers affiliated with a hosted service. * @@ -2171,6 +2182,7 @@ export function _getCreateServiceOptions( } createOptions.item = replaceInTemplate(createOptions.item, templateDictionary); createOptions.params = replaceInTemplate(createOptions.params, templateDictionary); + createOptions.item.name = sanitizeFeatureServiceName(createOptions.item.name); if (newItemTemplate.item.thumbnail) { // Pass thumbnail file in via params because item property is serialized, which discards a blob diff --git a/packages/common/test/featureServiceHelpers.test.ts b/packages/common/test/featureServiceHelpers.test.ts index e56a4b2c0..4067e57ef 100644 --- a/packages/common/test/featureServiceHelpers.test.ts +++ b/packages/common/test/featureServiceHelpers.test.ts @@ -1973,27 +1973,6 @@ describe("Module `featureServiceHelpers`: utility functions for feature-service expect(actual).toEqual(expected); }); - it("should replace disallowed characters in the name with '_'", () => { - const t: IItemTemplate = templates.getItemTemplateSkeleton(); - t.item.type = "Feature Service"; - // Includes each disallowed character: # % & " \ / + ? : * < > space tab - const disallowedName = 'a#b%c&d"e\\f/g+h?i:j*km n\to'; - t.item.name = disallowedName; - t.item.title = disallowedName; - const _templates: IItemTemplate[] = [t]; - - spyOn(generalHelpers, "generateGUID").and.returnValue("212dbc19b03943008fdfaf8d6adca00e"); - - const expectedTemplate: IItemTemplate = templates.getItemTemplateSkeleton(); - expectedTemplate.item.type = "Feature Service"; - expectedTemplate.item.name = `a_b_c_d_e_f_g_h_i_j_k_l_m_n_o_212dbc19b03943008fdfaf8d6adca00e`; - expectedTemplate.item.title = disallowedName; - const expected: IItemTemplate[] = [expectedTemplate]; - - const actual: IItemTemplate[] = setNamesAndTitles(_templates); - expect(actual).toEqual(expected); - }); - it("should not truncate if '{{param' is used", () => { const t: IItemTemplate = templates.getItemTemplateSkeleton(); t.item.type = "Feature Service"; diff --git a/packages/common/test/restHelpers.test.ts b/packages/common/test/restHelpers.test.ts index e79da3b84..9b9ed141d 100644 --- a/packages/common/test/restHelpers.test.ts +++ b/packages/common/test/restHelpers.test.ts @@ -4573,6 +4573,58 @@ describe("Module `restHelpers`: common REST utility functions shared across pack expect(templateDictionary[itemId].solutionExtent).toBeUndefined(); expect(templateDictionary[itemId].defaultSpatialReference).toEqual({ wkid: serviceWkid }); }); + + it("sanitizes disallowed characters in service name resolved from a {{params}} placeholder", async () => { + const userSession: UserSession = new UserSession({ + username: "jsmith", + password: "123456", + }); + + const templateDictionary: any = { + folderId: "aabb123456", + isPortal: false, + solutionItemId: "sol1234567890", + ab766cba0dd44ec080420acc10990282: {}, + organization: organization, + solutionItemExtent: solutionItemExtent, + params: { + buildSolution: { + items: { + ab766cba0dd44ec080420acc10990282: { + title: 'My & Service #1: a/b\\c "e" +f?g*h%i j\tk', + }, + }, + }, + }, + }; + + itemTemplate.item.name = + "{{params.buildSolution.items.ab766cba0dd44ec080420acc10990282.title}}_0a25612a2fc54f6e8828c679e2300a49"; + itemTemplate.item.title = "{{params.buildSolution.items.ab766cba0dd44ec080420acc10990282.title}}"; + itemTemplate.properties.service.spatialReference = { wkid: 102100 }; + itemTemplate.itemId = "ab766cba0dd44ec080420acc10990282"; + + const options = await restHelpers._getCreateServiceOptions(itemTemplate, userSession, templateDictionary); + expect(options.item.name).toEqual("My_Service_1_a_b_c_d__e__f_g_h_ij_k_0a25612a2fc54f6e8828c679e2300a49"); + // Title is *not* sanitized — only the service name has the character restriction. + expect(options.item.title).toEqual('My & Service #1: a/b\\c "e" +f?g*h%i j\tk'); + }); + }); + + describe("sanitizeFeatureServiceName", () => { + it("removes spaces and replaces every other disallowed character with '_'", () => { + const disallowedName = 'a#b%c&d"e\\f/g+h?i:j*km n\to'; + expect(restHelpers.sanitizeFeatureServiceName(disallowedName)).toEqual("a_b_c_d_e_f_g_h_i_j_k_l_mn_o"); + }); + + it("returns allowed names unchanged", () => { + expect(restHelpers.sanitizeFeatureServiceName("AcceptableName_1")).toEqual("AcceptableName_1"); + }); + + it("returns non-string inputs unchanged", () => { + expect(restHelpers.sanitizeFeatureServiceName(undefined as any)).toBeUndefined(); + expect(restHelpers.sanitizeFeatureServiceName(null as any)).toBeNull(); + }); }); describe("_extentIsValid", () => {