Skip to content

CEXT-6160: store commerce instance on association and expose retrieval helpers#511

Open
vinayrao2000 wants to merge 50 commits into
mainfrom
CEXT-6160-commerce-system-config-on-association
Open

CEXT-6160: store commerce instance on association and expose retrieval helpers#511
vinayrao2000 wants to merge 50 commits into
mainfrom
CEXT-6160-commerce-system-config-on-association

Conversation

@vinayrao2000

@vinayrao2000 vinayrao2000 commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Description

Adds a new feature that stores the Commerce instance details (Base URL + deployment type) when an app is associated with a Commerce instance, and exposes typed async helpers so any runtime action can call Commerce APIs without custom storage setup.

Two new helpers in @adobe/aio-commerce-lib-app:

Helper Returns Use when
getCommerceInstance() { baseUrl, env } You need the raw stored values (URL/env)
getCommerceClient(auth) AdobeCommerceHttpClient (ready to call APIs) You want to call Commerce APIs directly

getCommerceInstance() takes no arguments — it reads directly from storage. getCommerceClient(auth) is composable: it accepts auth already resolved by the caller (via resolveAuthParams from @adobe/aio-commerce-lib-auth) and combines it with the stored base URL and deployment type. Both throw AppNotAssociatedError if the app isn't currently associated.

Storage (generic system-config primitives in @adobe/aio-commerce-lib-config):

  • Two-layer pattern: state (cache) → files (persistent source of truth)
  • Generic getSystemConfigByKey / setSystemConfigByKey API — reusable for other "system-level" config beyond association
  • Exported from the package root (no dedicated subpath). Rather than reimplementing the two-layer cache, they reuse configuration-repository's loadConfig / persistConfig / deleteConfig, parameterized by a separate system.* storage namespace
  • Key namespace: system.association for this feature, stored under system/{key}.json, kept distinct from Business Configuration's configuration.* / scope/ namespace

New runtime action auto-scaffolded into apps:

  • POST /association — store { commerceBaseUrl, commerceEnv } (called by App Management UI on associate)
  • DELETE /association — clear stored data (called on unassociate)

Related Issue

CEXT-6160

Motivation and Context

Before this change, every action that wanted to call Commerce APIs had to:

  1. Read auth params from params
  2. Read the base URL from env vars or a custom-managed config
  3. Manually construct AdobeCommerceHttpClient
  4. Handle missing config in every action

This pushed boilerplate and inconsistent storage patterns into every app. Customers had no standard way to know which Commerce instance their app was associated with at runtime.

After this change, an action resolves auth once and gets a configured client:

const client = await getCommerceClient(resolveAuthParams(params));

The SDK owns where association data is stored (state + files), how it's read, and how it propagates from the association event to runtime actions.

How Has This Been Tested?

Unit tests (~1,865 tests passing across the monorepo):

  • lib-config: system-config.test.ts covers set/get/clear, state-as-cache + files-as-source-of-truth, null/undefined clears, corrupt-JSON fallback, type safety
  • lib-app:
    • errors/app-not-associated-error.test.ts — class, message, options
    • modules/association/association-repository.test.ts — round-trip via system config
    • actions/association.test.ts — router (POST/DELETE), validation, logger middleware
    • index.test.tsgetCommerceInstance and getCommerceClient happy + error paths

Typecheck: clean across all packages (pnpm typecheck)

Lint: clean (pnpm check:ci)

E2E (in commerce-app-purchase-approval demo app):

  • Associate via App Management UI → POST .../association → 200 OK
  • Invoke commerce-config-demo action → { associated: true, instance: { baseUrl, env }, clientType: "AdobeCommerceHttpClient" }
  • Unassociate via App Management UI → DELETE .../association → 204 No Content
  • Invoke demo action again → { associated: false, ... }
  • Re-associate after unassociate → flow works cleanly

Both helpers verified working end-to-end through deployed actions.

Types of changes

  • New feature (non-breaking change which adds functionality)

Checklist:

  • I have signed the Adobe Open Source CLA.
  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING document.
  • I have read the DEVELOPMENT document.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

Changesets

  • aio-commerce-lib-app — minor (new helpers + new action; additive only)
  • aio-commerce-lib-config — minor (new root exports getSystemConfigByKey / setSystemConfigByKey; additive only)

Affected packages

Package Change type
@adobe/aio-commerce-lib-app new public API: getCommerceInstance, getCommerceClient, AppNotAssociatedError, association runtime action + scaffolding template
@adobe/aio-commerce-lib-config new public API: root exports getSystemConfigByKey / setSystemConfigByKey

Drop the Associated prefix from the public helpers per Daniel's
review feedback - cleaner, more discoverable API.
… CEXT-6160-commerce-system-config-on-association
Per Ivan's review feedback - 'not associated' is exceptional state,
throwing keeps happy-path code clean without forcing null checks at
every call site.
Per Daniel's review feedback - puts SDK-managed config under a
system.* namespace, cleanly separated from user-defined
configuration.* keys.
lib-config now exposes generic system config primitives
(setSystemConfigByKey/getSystemConfigByKey), and lib-app has the
typed association wrappers on top. Drops the delete operation -
clearing is done by setting null.
Typed association helpers (setAssociationData, getAssociationData,
clearAssociationData) live in lib-app per the layered design, not
lib-config. lib-config only exposes the generic system config
primitives.
lib-config provides persistence via lib-files with lib-state as a performance cache. Cache expiry
triggers automatic re-fetch from files, so no TTL refresh logic
is needed in our code.
Adds setSystemConfigByKey / getSystemConfigByKey primitives under
modules/configuration/system/. Uses lib-files for persistent storage
with lib-state as a performance cache. Passing null to set clears
the entry. Exposed via the new @adobe/aio-commerce-lib-config/system
subpath export.

Domain-agnostic - operates purely on opaque keys and values. Will be
used by lib-app's association module via the public subpath export.
- AppNotAssociatedError class (extends CommerceSdkErrorBase)
- Internal association module: setAssociationData / getAssociationData /
  clearAssociationData (calls into lib-config system submodule)
- Public root entrypoint: getCommerceInstance and getCommerceClient
  helpers, both throw AppNotAssociatedError when no data is stored
- Add  root export to package.json and tsdown.config.ts
- New association runtime action with POST / and DELETE / handlers,
  protected with require-adobe-auth: true
- POST / accepts { commerceBaseUrl, commerceEnv } and stores it via
  the internal association module
- DELETE / clears the stored data
- Add association.js.template for app scaffolding
- Update buildAppManagementExtConfig to deploy the association action
  alongside app-config (always deployed, no feature gating)
- Update OpenAPI spec with the new POST /association and DELETE /association
  routes, bump info.version to 1.1.0
- Update test fixtures and config test expectations
- Unit tests for AppNotAssociatedError, association repository, public
  helpers (getCommerceInstance / getCommerceClient), and the association
  runtime action handlers (POST / and DELETE /)
- changesets for minor bumps in lib-app and lib-config
- Document the new helpers in lib-app usage.md
@changeset-bot

changeset-bot Bot commented Jun 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: dd4c61c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
@adobe/aio-commerce-lib-app Minor
@adobe/aio-commerce-lib-core Minor
@adobe/aio-commerce-lib-config Minor
@adobe/aio-commerce-lib-admin-ui Patch
@adobe/aio-commerce-lib-api Patch
@adobe/aio-commerce-lib-auth Patch
@adobe/aio-commerce-lib-events Patch
@adobe/aio-commerce-lib-webhooks Patch
@adobe/aio-commerce-sdk Patch
@aio-commerce-sdk/common-utils Patch
@aio-commerce-sdk/scripting-utils Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added with-changeset The PR contains a Changeset file. pkg: aio-commerce-lib-config Includes changes in `packages/aio-commerce-lib-config` pkg: aio-commerce-lib-app Includes changes in `packages/aio-commerce-lib-app` labels Jun 9, 2026
Comment thread packages/aio-commerce-lib-app/docs/usage.md Outdated
Comment thread packages/aio-commerce-lib-app/source/actions/association/schema.ts Outdated
@github-actions github-actions Bot added the pkg: aio-commerce-lib-core Includes changes in `packages/aio-commerce-lib-core` label Jun 12, 2026
@github-actions github-actions Bot added the spec Includes changes in `specs/features` label Jun 16, 2026
Comment thread packages/aio-commerce-lib-config/source/modules/configuration/system-config.ts Outdated
Comment thread packages/aio-commerce-lib-config/source/modules/configuration/system-config.ts Outdated
Comment thread packages/aio-commerce-lib-app/source/modules/association/types.ts Outdated
Two helpers are exposed from the root entrypoint:

- `getCommerceClient(auth)` — returns a ready-to-use [`AdobeCommerceHttpClient`](../../aio-commerce-lib-api/docs/usage.md). Use this when you need to call the Commerce API. The base URL and flavor come from the stored association data; you supply the resolved auth credentials (resolve them with `resolveAuthParams` from [`@adobe/aio-commerce-lib-auth`](../../aio-commerce-lib-auth/docs/usage.md)).
- `getCommerceClient(auth, fetchOptions?)` — returns a ready-to-use [`AdobeCommerceHttpClient`](../../aio-commerce-lib-api/docs/usage.md). Use this when you need to call the Commerce API. The base URL and flavor come from the stored association data; you supply the resolved IMS auth. App Management requires IMS, so this accepts only IMS auth: resolve params with `resolveImsAuthParams`, or pass an `ImsAuthProvider` built with `getImsAuthProvider` / `forwardImsAuthProvider` from [`@adobe/aio-commerce-lib-auth`](../../aio-commerce-lib-auth/docs/usage.md). The optional `fetchOptions` are forwarded to the underlying client (e.g. `headers`, `timeout`, `retry`).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A link to what fetchOptions is could be added. Not a blocker.

Comment on lines 15 to +23
import { AppNotAssociatedError } from "../errors/app-not-associated-error";
import { getAssociationData } from "../modules/association/association-repository";
import { getAssociationData } from "../management/association/association-repository";

import type { CommerceHttpClientParams } from "@adobe/aio-commerce-lib-api";
import type { AssociatedCommerceInstance } from "../modules/association/types";
import type {
ImsAuthParams,
ImsAuthProvider,
} from "@adobe/aio-commerce-lib-auth";
import type { AssociatedCommerceInstance } from "../management/association/types";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import { AppNotAssociatedError } from "../errors/app-not-associated-error";
import { getAssociationData } from "../modules/association/association-repository";
import { getAssociationData } from "../management/association/association-repository";
import type { CommerceHttpClientParams } from "@adobe/aio-commerce-lib-api";
import type { AssociatedCommerceInstance } from "../modules/association/types";
import type {
ImsAuthParams,
ImsAuthProvider,
} from "@adobe/aio-commerce-lib-auth";
import type { AssociatedCommerceInstance } from "../management/association/types";
import { AppNotAssociatedError } from "#errors/app-not-associated-error";
import { getAssociationData } from "#management/association/association-repository";
import type { CommerceHttpClientParams } from "@adobe/aio-commerce-lib-api";
import type {
ImsAuthParams,
ImsAuthProvider,
} from "@adobe/aio-commerce-lib-auth";
import type { AssociatedCommerceInstance } from "#management/association/types";

Comment on lines +59 to +61
* IMS auth: resolve params with `resolveImsAuthParams`, or pass an
* `ImsAuthProvider` built with `getImsAuthProvider` / `forwardImsAuthProvider`
* from `@adobe/aio-commerce-lib-auth`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a bit too verbose, a JSDoc doesn't need to be a calc of docs.

Comment on lines +122 to +126
expect(MockAdobeCommerceHttpClient).toHaveBeenCalledWith({
auth,
config: { baseUrl: data.baseUrl, flavor: "paas" },
fetchOptions,
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this test you don't need to assert it's called with auth and config, only fetchOptions. Keeps the test less likely to fail :)

Comment on lines +23 to +31
mockGetSystemConfigByKey: vi.fn(async (key: string) =>
store.has(key) ? store.get(key) : null,
),
mockSetSystemConfigByKey: vi.fn(async (key: string, value: unknown) => {
if (value === null || value === undefined) {
store.delete(key);
} else {
store.set(key, value);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mocking this provides little to no-value, as these helpers are directly invoked from within the tested API. Again, the underlying storage layer would be a more correct mocking target.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pkg: aio-commerce-lib-app Includes changes in `packages/aio-commerce-lib-app` pkg: aio-commerce-lib-config Includes changes in `packages/aio-commerce-lib-config` pkg: aio-commerce-lib-core Includes changes in `packages/aio-commerce-lib-core` spec Includes changes in `specs/features` with-changeset The PR contains a Changeset file.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants