Skip to content
Open
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
84 changes: 83 additions & 1 deletion packages/aio-commerce-lib-app/source/actions/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type ConfigActionFactoryArgs = {
type ConfigActionParams = RuntimeActionParams &
ConfigActionFactoryArgs & {
AIO_COMMERCE_CONFIG_ENCRYPTION_KEY?: string;
__ow_headers?: Record<string, string>;
};

/** The context for the config action. */
Expand All @@ -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<BusinessConfigSchema[number]> {
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<BusinessConfigSchema> {
return Promise.all(
schema.map((field) => resolveOptionsSource(field, params)),
) as Promise<BusinessConfigSchema>;
}

/**
* Filters password fields from the configuration values.
* @param schema - The schema to use to filter the values.
Expand Down Expand Up @@ -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), {
Expand All @@ -106,7 +188,7 @@ router.get("/", {
);

return ok({
body: { schema: validatedSchema, values: appConfiguration },
body: { schema: resolvedSchema, values: appConfiguration },
});
},
});
Expand Down
34 changes: 29 additions & 5 deletions packages/aio-commerce-lib-config/source/modules/schema/fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 */
Expand All @@ -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(
Expand Down