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
63 changes: 60 additions & 3 deletions packages/aio-commerce-lib-app/source/actions/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

import {
byScopeId,
filterBusinessConfigSchemaByContext,
filterBusinessConfigSchemaByFlavor,
getConfiguration,
initialize,
setConfiguration,
Expand Down Expand Up @@ -42,6 +44,13 @@ type ConfigActionFactoryArgs = {
type ConfigActionParams = RuntimeActionParams &
ConfigActionFactoryArgs & {
AIO_COMMERCE_CONFIG_ENCRYPTION_KEY?: string;
AIO_COMMERCE_API_FLAVOR?: unknown;
AIO_COMMERCE_API_BASE_URL?: unknown;
commerceEnv?: unknown;
commerceBaseUrl?: unknown;
commerceUrl?: unknown;
commerceInstanceUrl?: unknown;
instanceUrl?: unknown;
};

/** The context for the config action. */
Expand All @@ -52,6 +61,31 @@ interface ConfigActionContext extends BaseContext {
// Placeholder value for password fields.
const MASKED_PASSWORD_VALUE = "*****";

/** Normalizes a possible flavor value to a supported Commerce flavor. */
function normalizeCommerceFlavor(value: unknown): "paas" | "saas" | undefined {
if (typeof value !== "string") {
return;
}

const normalized = value.trim().toLowerCase();
if (normalized === "paas" || normalized === "saas") {
return normalized;
}

return;
}

/** Picks the first non-empty string value from a list of candidates. */
function firstNonEmptyString(...values: unknown[]): string | undefined {
for (const value of values) {
if (typeof value === "string" && value.trim().length > 0) {
return value;
}
}

return;
}

/**
* Filters password fields from the configuration values.
* @param schema - The schema to use to filter the values.
Expand Down Expand Up @@ -79,6 +113,8 @@ const router = new HttpActionRouter<ConfigActionContext>().use(logger());
router.get("/", {
query: v.object({
scopeId: nonEmptyStringValueSchema("scopeId"),
commerceEnv: v.optional(v.picklist(["paas", "saas"] as const)),
commerceBaseUrl: v.optional(nonEmptyStringValueSchema("commerceBaseUrl")),
}),

handler: async (req, ctx) => {
Expand All @@ -91,7 +127,28 @@ router.get("/", {
"businessConfig.schema",
);

initialize({ schema: validatedSchema });
const explicitFlavor =
req.query.commerceEnv ?? normalizeCommerceFlavor(rawParams.commerceEnv);
const commerceEnvAsBaseUrl =
explicitFlavor === undefined ? rawParams.commerceEnv : undefined;

const filteredSchema = explicitFlavor
? filterBusinessConfigSchemaByFlavor(validatedSchema, explicitFlavor)
: filterBusinessConfigSchemaByContext(validatedSchema, {
AIO_COMMERCE_API_FLAVOR:
rawParams.AIO_COMMERCE_API_FLAVOR ?? rawParams.commerceEnv,
AIO_COMMERCE_API_BASE_URL: firstNonEmptyString(
rawParams.AIO_COMMERCE_API_BASE_URL,
req.query.commerceBaseUrl,
rawParams.commerceBaseUrl,
rawParams.commerceUrl,
rawParams.commerceInstanceUrl,
rawParams.instanceUrl,
commerceEnvAsBaseUrl,
),
});

initialize({ schema: filteredSchema });

const { scopeId } = req.query;
logger.debug(`Retrieving configuration with scope id: ${scopeId}`);
Expand All @@ -101,12 +158,12 @@ router.get("/", {

logger.debug("Masking password values...");
appConfiguration.config = filterPasswordFields(
configSchema,
filteredSchema,
appConfiguration.config,
);

return ok({
body: { schema: validatedSchema, values: appConfiguration },
body: { schema: filteredSchema, values: appConfiguration },
});
},
});
Expand Down
51 changes: 38 additions & 13 deletions packages/aio-commerce-lib-config/docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,19 +305,6 @@ Use text fields for free-form input values like merchant identifiers or custom s
}
```

**Boolean Field:**

Use boolean fields for flag-like fields that only accept true/false values. Boolean fields support optional default values.

```javascript
{
name: "enabled",
label: "Enable Feature",
type: "boolean",
default: true
}
```

**Email Field:**

Use email fields for email addresses with automatic validation. Email fields support optional default values.
Expand Down Expand Up @@ -419,6 +406,44 @@ Multiple selection example:
> [!NOTE]
> For `selectionMode: "multiple"`, the `default` value must be an array of strings, even if only one option is selected by default.

### Conditional Fields by Commerce Flavor

Each schema field accepts an optional `env` property to scope it to specific Commerce flavors. The property is an array of flavors (`"paas"`, `"saas"`) and supports any combination of one or more values.

When `env` is omitted, the field applies to all flavors. When `env` is provided, the field is only relevant for the listed flavors and can be filtered out for the others — for example, when rendering the configuration form in the App Management UI for an app associated with a SaaS Commerce instance.

```javascript
{
name: "commerceTenantId",
label: "Commerce Tenant ID",
type: "text",
env: ["saas"]
},
{
name: "magentoCloudProjectId",
label: "Magento Cloud Project ID",
type: "text",
env: ["paas"]
},
{
name: "sharedApiKey",
label: "Shared API Key",
type: "password"
// No `env` -> applies to all flavors
}
```

Use `filterBusinessConfigSchemaByFlavor` to keep only the fields applicable to a given flavor:

```typescript
import { filterBusinessConfigSchemaByFlavor } from "@adobe/aio-commerce-lib-config";

const saasFields = filterBusinessConfigSchemaByFlavor(schema, "saas");
const paasFields = filterBusinessConfigSchemaByFlavor(schema, "paas");
```

Fields without `env` are always included. Field order is preserved.

## CLI Commands

The library provides CLI commands to help manage encryption for password fields:
Expand Down
8 changes: 7 additions & 1 deletion packages/aio-commerce-lib-config/source/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ export {
type SelectorByCodeAndLevel,
type SelectorByScopeId,
} from "./config-utils";
export { SchemaBusinessConfig } from "./modules/schema";
export {
filterBusinessConfigSchemaByContext,
filterBusinessConfigSchemaByFlavor,
resolveCommerceFlavorFromContext,
SchemaBusinessConfig,
} from "./modules/schema";
export * from "./types";
export {
generateEncryptionKey,
Expand All @@ -39,5 +44,6 @@ export type {
BusinessConfigSchemaField,
BusinessConfigSchemaListOption,
BusinessConfigSchemaValue,
CommerceFlavor,
} from "./modules/schema";
export type { ScopeNode, ScopeTree } from "./modules/scope-tree";
22 changes: 22 additions & 0 deletions packages/aio-commerce-lib-config/source/modules/schema/fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,27 @@ const DEFAULT_BOOLEAN_VALUE = false as const;
const DEFAULT_STRING_VALUE = "" as const;
const DEFAULT_MULTIPLE_LIST_VALUE = [] as const;

/** The list of supported Commerce flavors a configuration field can be scoped to. */
export const COMMERCE_FLAVORS = ["paas", "saas"] as const;

/** Schema for a single Commerce flavor a configuration field can be scoped to. */
const CommerceFlavorSchema = v.picklist(
COMMERCE_FLAVORS,
`Expected one of: ${COMMERCE_FLAVORS.map((f) => `"${f}"`).join(", ")}`,
);

/**
* Schema for the optional `env` property used to scope a configuration field to
* specific Commerce flavors. When omitted, the field applies to all flavors.
*/
const EnvSchema = v.pipe(
v.array(
CommerceFlavorSchema,
"Expected an array of Commerce flavors for the field env",
),
v.nonEmpty("The env array must contain at least one Commerce flavor"),
);

/** Base schema for configuration field options with name, optional label, and optional description */
const BaseOptionSchema = v.object({
name: v.pipe(
Expand All @@ -26,6 +47,7 @@ const BaseOptionSchema = v.object({
description: v.optional(
v.string("Expected a string for the field description"),
),
env: v.optional(EnvSchema),
});

/** Schema for a single option in a list field, containing a display label and a value */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ import * as v from "valibot";

import { SchemaBusinessConfigSchema } from "./fields";

export {
filterBusinessConfigSchemaByContext,
filterBusinessConfigSchemaByFlavor,
resolveCommerceFlavorFromContext,
} from "./utils";

/** The schema used to validate the the business configuration settings. */
export const SchemaBusinessConfig = v.object({
schema: v.optional(SchemaBusinessConfigSchema, []),
Expand Down
12 changes: 11 additions & 1 deletion packages/aio-commerce-lib-config/source/modules/schema/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,17 @@
*/

import type * as v from "valibot";
import type { FieldSchema, SchemaBusinessConfigSchema } from "./fields";
import type {
COMMERCE_FLAVORS,
FieldSchema,
SchemaBusinessConfigSchema,
} from "./fields";

/**
* The flavor of an Adobe Commerce application a configuration field can be
* scoped to via the `env` property.
*/
export type CommerceFlavor = (typeof COMMERCE_FLAVORS)[number];

/** Context needed for schema operations. */
export type SchemaContext = {
Expand Down
Loading