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 }, }); }, }); 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 acce4c6f1..c4de95d97 100644 --- a/packages/aio-commerce-lib-config/source/modules/schema/fields.ts +++ b/packages/aio-commerce-lib-config/source/modules/schema/fields.ts @@ -33,6 +33,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, @@ -42,11 +59,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 */ @@ -58,7 +77,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(