From 156d6f1f23bc1f564355278f5f2af53e15e34329 Mon Sep 17 00:00:00 2001 From: Alex Lyzun Date: Mon, 2 Mar 2026 11:18:08 +0100 Subject: [PATCH 1/3] Add optionsSource support for dynamic list field options - Extended fields.ts schema to support optionsSource property with action/url - Made options property optional when optionsSource is provided - Updated get-config-schema template to resolve dynamic options at runtime --- .../get-config-schema.js.template | 98 ++++++++++++++++++- .../source/modules/schema/fields.ts | 34 ++++++- 2 files changed, 123 insertions(+), 9 deletions(-) diff --git a/packages/aio-commerce-lib-app/source/commands/generate/actions/templates/business-configuration/get-config-schema.js.template b/packages/aio-commerce-lib-app/source/commands/generate/actions/templates/business-configuration/get-config-schema.js.template index 0f1e515d5..9efc4ec32 100644 --- a/packages/aio-commerce-lib-app/source/commands/generate/actions/templates/business-configuration/get-config-schema.js.template +++ b/packages/aio-commerce-lib-app/source/commands/generate/actions/templates/business-configuration/get-config-schema.js.template @@ -22,11 +22,98 @@ import { } from "@adobe/aio-commerce-sdk/core/responses"; import AioLogger from "@adobe/aio-lib-core-logging"; -// Shorthand to inspect an object. const inspect = (obj) => util.inspect(obj, { depth: null }); /** - * Get the configuration schema. + * Resolves optionsSource for a field by calling the specified action. + * This allows dynamic dropdowns to work with Adobe's App Management UI. + * @param {Object} field - The schema field to process + * @param {Object} params - Request parameters including auth headers + * @param {Object} logger - Logger instance + * @returns {Object} Field with resolved options array + */ +async function resolveOptionsSource(field, params, logger) { + if (!field.optionsSource) { + return field; + } + + const { action, url } = field.optionsSource; + + try { + let options = []; + + if (action) { + const namespace = process.env.__OW_NAMESPACE; + const apiHost = process.env.__OW_API_HOST || 'https://adobeioruntime.net'; + const actionUrl = `${apiHost}/api/v1/web/${namespace}/${action}`; + + logger.debug(`Fetching dynamic options from action: ${actionUrl}`); + + const response = await fetch(actionUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': params.__ow_headers?.authorization || '', + 'x-gw-ims-org-id': params.__ow_headers?.['x-gw-ims-org-id'] || '', + }, + body: JSON.stringify({ + store_code: params.store_code, + store_level: params.store_level, + field_name: field.name, + }), + }); + + if (response.ok) { + const result = await response.json(); + options = result?.body?.options || result?.options || []; + logger.debug(`Resolved ${options.length} options for field: ${field.name}`); + } else { + logger.warn(`Failed to fetch options for ${field.name}: HTTP ${response.status}`); + } + } else if (url) { + logger.debug(`Fetching dynamic options from URL: ${url}`); + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ field_name: field.name }), + }); + + if (response.ok) { + const result = await response.json(); + options = result?.options || []; + } + } + + const { optionsSource, ...fieldWithoutSource } = field; + return { + ...fieldWithoutSource, + options: options.length > 0 ? options : [{ label: 'No options available', value: '' }], + }; + } catch (error) { + logger.error(`Error resolving optionsSource for ${field.name}: ${error.message}`); + const { optionsSource, ...fieldWithoutSource } = field; + return { + ...fieldWithoutSource, + options: [{ label: 'Error loading options', value: '' }], + }; + } +} + +/** + * Process schema to resolve all optionsSource fields into static options arrays. + * @param {Array} schema - The configuration schema + * @param {Object} params - Request parameters + * @param {Object} logger - Logger instance + * @returns {Array} Schema with all optionsSource fields resolved + */ +async function resolveAllOptionsSource(schema, params, logger) { + return Promise.all( + schema.map(field => resolveOptionsSource(field, params, logger)) + ); +} + +/** + * Get the configuration schema with dynamic options resolved. * @returns The response object containing the configuration schema. */ export async function main(params) { @@ -38,13 +125,16 @@ export async function main(params) { logger.debug("Retrieving configuration schema..."); const configSchema = await getConfigSchema(); + logger.debug("Resolving dynamic optionsSource fields..."); + const resolvedSchema = await resolveAllOptionsSource(configSchema, params, logger); + logger.debug( - `Successfully retrieved configSchema: ${inspect(configSchema)}`, + `Successfully retrieved configSchema: ${inspect(resolvedSchema)}`, ); return ok({ body: { - configSchema, + configSchema: resolvedSchema, }, }); } catch (error) { diff --git a/packages/aio-commerce-lib-config/source/modules/schema/fields.ts b/packages/aio-commerce-lib-config/source/modules/schema/fields.ts index 53f993419..e36359fb7 100644 --- a/packages/aio-commerce-lib-config/source/modules/schema/fields.ts +++ b/packages/aio-commerce-lib-config/source/modules/schema/fields.ts @@ -30,6 +30,23 @@ const ListOptionSchema = v.object({ value: v.string("Expected a string for the option value"), }); +/** + * Schema for dynamic options source configuration. + * Allows list fields to fetch options from a runtime action or external URL + * instead of requiring static options defined in the schema. + */ +const OptionsSourceSchema = v.object({ + /** The name of the runtime action to invoke for fetching options (e.g., "app-management/get-store-options") */ + action: v.optional(v.string("Expected a string for the action name")), + /** A URL endpoint to fetch options from */ + url: v.optional( + v.pipe( + v.string("Expected a string for the options source URL"), + v.url("The optionsSource URL must be a valid URL"), + ), + ), +}); + /** Schema for a list field that allows single selection from a list of options */ const SingleListSchema = v.object({ ...BaseOptionSchema.entries, @@ -39,11 +56,13 @@ const SingleListSchema = v.object({ "single", "Expected the selectionMode to be 'single'", ), - options: v.array(ListOptionSchema, "Expected an array of list options"), - default: v.pipe( - v.string("Expected a string for the default value"), - v.nonEmpty("The default value must not be empty"), + /** Static options array - optional when using optionsSource */ + options: v.optional( + v.array(ListOptionSchema, "Expected an array of list options"), ), + /** Dynamic options source - alternative to static options */ + optionsSource: v.optional(OptionsSourceSchema), + default: v.optional(v.string("Expected a string for the default value")), }); /** Schema for a list field that allows multiple selections from a list of options */ @@ -55,7 +74,12 @@ const MultipleListSchema = v.object({ "multiple", "Expected the selectionMode to be 'multiple'", ), - options: v.array(ListOptionSchema, "Expected an array of list options"), + /** Static options array - optional when using optionsSource */ + options: v.optional( + v.array(ListOptionSchema, "Expected an array of list options"), + ), + /** Dynamic options source - alternative to static options */ + optionsSource: v.optional(OptionsSourceSchema), default: v.optional( v.array( v.pipe( From b7a58429518abb78e6d0da56a963ff1316b7384f Mon Sep 17 00:00:00 2001 From: Alex Lyzun Date: Mon, 2 Mar 2026 12:01:22 +0100 Subject: [PATCH 2/3] Change Template of get config to remove onptional paramaeters --- .../get-config-schema.js.template | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/packages/aio-commerce-lib-app/source/commands/generate/actions/templates/business-configuration/get-config-schema.js.template b/packages/aio-commerce-lib-app/source/commands/generate/actions/templates/business-configuration/get-config-schema.js.template index 9efc4ec32..46d8f7317 100644 --- a/packages/aio-commerce-lib-app/source/commands/generate/actions/templates/business-configuration/get-config-schema.js.template +++ b/packages/aio-commerce-lib-app/source/commands/generate/actions/templates/business-configuration/get-config-schema.js.template @@ -44,23 +44,16 @@ async function resolveOptionsSource(field, params, logger) { if (action) { const namespace = process.env.__OW_NAMESPACE; - const apiHost = process.env.__OW_API_HOST || 'https://adobeioruntime.net'; + const apiHost = process.env.__OW_API_HOST; const actionUrl = `${apiHost}/api/v1/web/${namespace}/${action}`; logger.debug(`Fetching dynamic options from action: ${actionUrl}`); const response = await fetch(actionUrl, { - method: 'POST', headers: { - 'Content-Type': 'application/json', 'Authorization': params.__ow_headers?.authorization || '', 'x-gw-ims-org-id': params.__ow_headers?.['x-gw-ims-org-id'] || '', }, - body: JSON.stringify({ - store_code: params.store_code, - store_level: params.store_level, - field_name: field.name, - }), }); if (response.ok) { @@ -72,11 +65,7 @@ async function resolveOptionsSource(field, params, logger) { } } else if (url) { logger.debug(`Fetching dynamic options from URL: ${url}`); - const response = await fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ field_name: field.name }), - }); + const response = await fetch(url); if (response.ok) { const result = await response.json(); From d95b4f6ce6094e33a8bf8707082d5a44fe312b82 Mon Sep 17 00:00:00 2001 From: Alex Lyzun Date: Thu, 19 Mar 2026 11:07:02 +0100 Subject: [PATCH 3/3] Change target branch and reimplement code from template to new location --- .../source/actions/config.ts | 84 ++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/packages/aio-commerce-lib-app/source/actions/config.ts b/packages/aio-commerce-lib-app/source/actions/config.ts index 728f0d052..46abb576f 100644 --- a/packages/aio-commerce-lib-app/source/actions/config.ts +++ b/packages/aio-commerce-lib-app/source/actions/config.ts @@ -42,6 +42,7 @@ type ConfigActionFactoryArgs = { type ConfigActionParams = RuntimeActionParams & ConfigActionFactoryArgs & { AIO_COMMERCE_CONFIG_ENCRYPTION_KEY?: string; + __ow_headers?: Record; }; /** The context for the config action. */ @@ -52,6 +53,82 @@ interface ConfigActionContext extends BaseContext { // Placeholder value for password fields. const MASKED_PASSWORD_VALUE = "*****"; +/** + * Resolves optionsSource for a field by calling the specified action or URL. + * This allows dynamic dropdowns to work with Adobe's App Management UI. + */ +async function resolveOptionsSource( + field: BusinessConfigSchema[number], + params: ConfigActionParams, +): Promise { + if (!("optionsSource" in field && field.optionsSource)) { + return field; + } + + const { action, url } = field.optionsSource; + + try { + let options: Array<{ label: string; value: string }> = []; + + if (action) { + const namespace = process.env.__OW_NAMESPACE; + const apiHost = process.env.__OW_API_HOST; + const actionUrl = `${apiHost}/api/v1/web/${namespace}/${action}`; + + const response = await fetch(actionUrl, { + headers: { + Authorization: params.__ow_headers?.authorization || "", + "x-gw-ims-org-id": params.__ow_headers?.["x-gw-ims-org-id"] || "", + }, + }); + + if (response.ok) { + const result = (await response.json()) as { + body?: { options?: Array<{ label: string; value: string }> }; + options?: Array<{ label: string; value: string }>; + }; + options = result?.body?.options || result?.options || []; + } + } else if (url) { + const response = await fetch(url); + + if (response.ok) { + const result = (await response.json()) as { + options?: Array<{ label: string; value: string }>; + }; + options = result?.options || []; + } + } + + const { optionsSource, ...fieldWithoutSource } = field; + return { + ...fieldWithoutSource, + options: + options.length > 0 + ? options + : [{ label: "No options available", value: "" }], + } as BusinessConfigSchema[number]; + } catch { + const { optionsSource, ...fieldWithoutSource } = field; + return { + ...fieldWithoutSource, + options: [{ label: "Error loading options", value: "" }], + } as BusinessConfigSchema[number]; + } +} + +/** + * Process schema to resolve all optionsSource fields into static options arrays. + */ +async function resolveAllOptionsSource( + schema: BusinessConfigSchema, + params: ConfigActionParams, +): Promise { + return Promise.all( + schema.map((field) => resolveOptionsSource(field, params)), + ) as Promise; +} + /** * Filters password fields from the configuration values. * @param schema - The schema to use to filter the values. @@ -93,6 +170,11 @@ router.get("/", { initialize({ schema: validatedSchema }); + const resolvedSchema = await resolveAllOptionsSource( + validatedSchema, + rawParams, + ); + const { scopeId } = req.query; logger.debug(`Retrieving configuration with scope id: ${scopeId}`); const appConfiguration = await getConfiguration(byScopeId(scopeId), { @@ -106,7 +188,7 @@ router.get("/", { ); return ok({ - body: { schema: validatedSchema, values: appConfiguration }, + body: { schema: resolvedSchema, values: appConfiguration }, }); }, });