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", () => {