CEXT-6160: store commerce instance on association and expose retrieval helpers#511
CEXT-6160: store commerce instance on association and expose retrieval helpers#511vinayrao2000 wants to merge 50 commits into
Conversation
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 detectedLatest commit: dd4c61c The changes in this PR will be included in the next version bump. This PR includes changesets to release 11 packages
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 |
…ent to IMS, return 204 from POST /association
…urn 204 from POST /association
| 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`). |
There was a problem hiding this comment.
A link to what fetchOptions is could be added. Not a blocker.
| 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"; |
There was a problem hiding this comment.
| 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"; |
| * IMS auth: resolve params with `resolveImsAuthParams`, or pass an | ||
| * `ImsAuthProvider` built with `getImsAuthProvider` / `forwardImsAuthProvider` | ||
| * from `@adobe/aio-commerce-lib-auth`. |
There was a problem hiding this comment.
Maybe a bit too verbose, a JSDoc doesn't need to be a calc of docs.
| expect(MockAdobeCommerceHttpClient).toHaveBeenCalledWith({ | ||
| auth, | ||
| config: { baseUrl: data.baseUrl, flavor: "paas" }, | ||
| fetchOptions, | ||
| }); |
There was a problem hiding this comment.
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 :)
| 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); | ||
| } |
There was a problem hiding this comment.
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.
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:getCommerceInstance(){ baseUrl, env }getCommerceClient(auth)AdobeCommerceHttpClient(ready to call APIs)getCommerceInstance()takes no arguments — it reads directly from storage.getCommerceClient(auth)is composable: it accepts auth already resolved by the caller (viaresolveAuthParamsfrom@adobe/aio-commerce-lib-auth) and combines it with the stored base URL and deployment type. Both throwAppNotAssociatedErrorif the app isn't currently associated.Storage (generic system-config primitives in
@adobe/aio-commerce-lib-config):getSystemConfigByKey/setSystemConfigByKeyAPI — reusable for other "system-level" config beyond associationconfiguration-repository'sloadConfig/persistConfig/deleteConfig, parameterized by a separatesystem.*storage namespacesystem.associationfor this feature, stored undersystem/{key}.json, kept distinct from Business Configuration'sconfiguration.*/scope/namespaceNew 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:
paramsAdobeCommerceHttpClientThis 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:
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.tscovers set/get/clear, state-as-cache + files-as-source-of-truth, null/undefined clears, corrupt-JSON fallback, type safetylib-app:errors/app-not-associated-error.test.ts— class, message, optionsmodules/association/association-repository.test.ts— round-trip via system configactions/association.test.ts— router (POST/DELETE), validation, logger middlewareindex.test.ts—getCommerceInstanceandgetCommerceClienthappy + error pathsTypecheck: clean across all packages (
pnpm typecheck)Lint: clean (
pnpm check:ci)E2E (in
commerce-app-purchase-approvaldemo app):POST .../association→ 200 OKcommerce-config-demoaction →{ associated: true, instance: { baseUrl, env }, clientType: "AdobeCommerceHttpClient" }DELETE .../association→ 204 No Content{ associated: false, ... }Both helpers verified working end-to-end through deployed actions.
Types of changes
Checklist:
Changesets
aio-commerce-lib-app— minor (new helpers + new action; additive only)aio-commerce-lib-config— minor (new root exportsgetSystemConfigByKey/setSystemConfigByKey; additive only)Affected packages
@adobe/aio-commerce-lib-appgetCommerceInstance,getCommerceClient,AppNotAssociatedError, association runtime action + scaffolding template@adobe/aio-commerce-lib-configgetSystemConfigByKey/setSystemConfigByKey