configuration-management 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "configuration-management",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Schema-driven system configuration for Adobe Commerce App Builder sync apps. Magento-style scoped config in Adobe App Builder Database (ABDB) with encryption, Commerce REST helpers, and React Admin UI.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Adobe Inc.",
@@ -28,7 +28,9 @@
28
28
  "./crypto": "./src/system-config-crypto.js",
29
29
  "./shared": "./src/system-config-shared.js",
30
30
  "./oauth1a": "./src/oauth1a.js",
31
- "./web": "./web/src/index.js",
31
+ "./web": "./web/index.js",
32
+ "./web/index.js": "./web/index.js",
33
+ "./web/src/styles/index.css": "./web/src/styles/index.css",
32
34
  "./web/styles.css": "./web/src/styles/index.css",
33
35
  "./actions/utils": "./actions/utils.js",
34
36
  "./actions/ext.config.yaml": "./actions/configurations/ext.config.yaml"
@@ -52,11 +54,11 @@
52
54
  "@adobe/react-spectrum": "^3.30.0",
53
55
  "@adobe/uix-guest": "^0.8.3",
54
56
  "@spectrum-icons/workflow": "^4.2.4",
57
+ "dotenv": "^16.4.5",
55
58
  "react": "^18.2.0",
56
59
  "react-dom": "^18.2.0",
57
60
  "react-error-boundary": "^3.1.4",
58
- "react-router-dom": "^6.8.1",
59
- "dotenv": "^16.4.5"
61
+ "react-router-dom": "^6.8.1"
60
62
  },
61
63
  "dependencies": {
62
64
  "got": "^11.8.5",
package/web/index.js ADDED
@@ -0,0 +1,8 @@
1
+ /*
2
+ Copyright 2025 Adobe. All rights reserved.
3
+ Licensed under the Apache License, Version 2.0
4
+ */
5
+
6
+ // Parcel / App Builder resolve `configuration-management/web` as this file
7
+ // (directory index), without relying on package.json "exports" subpaths.
8
+ export * from './src/index.js'
package/README.md DELETED
@@ -1,199 +0,0 @@
1
- # configuration-management
2
-
3
- Schema-driven system configuration for **Adobe Commerce** and **Adobe App Builder** sync applications.
4
-
5
- Mirrors Magento's `core_config_data` model: scoped configuration (`default` / `websites` / `stores`) stored in **Adobe App Builder Database (ABDB)** with AES-256-GCM encryption for sensitive fields.
6
-
7
- ## Install
8
-
9
- ```bash
10
- npm install configuration-management
11
- ```
12
-
13
- Your App Builder project must also have Adobe I/O runtime dependencies installed (peer dependencies):
14
-
15
- ```bash
16
- npm install @adobe/aio-lib-core-auth @adobe/aio-lib-db @adobe/aio-lib-ims @adobe/aio-sdk dotenv
17
- ```
18
-
19
- For the React Admin UI, also install Spectrum and React peers:
20
-
21
- ```bash
22
- npm install react react-dom @adobe/react-spectrum @adobe/uix-guest @adobe/exc-app react-router-dom react-error-boundary @spectrum-icons/workflow
23
- ```
24
-
25
- ## Quick start
26
-
27
- Read a config value from ABDB inside an App Builder action:
28
-
29
- ```js
30
- const { getConfig } = require('configuration-management')
31
-
32
- async function main (params) {
33
- const apiUrl = await getConfig('sync_general/api/url', params, {
34
- scope: 'websites',
35
- scopeCode: 'base'
36
- })
37
- // ...
38
- }
39
- ```
40
-
41
- ## API
42
-
43
- ### Config resolution
44
-
45
- | Export | Description |
46
- |--------|-------------|
47
- | `getConfig(path, params, options)` | Read a value with Magento-style scope inheritance |
48
- | `clearAbdbConfigCache()` | Clear the in-process lookup cache |
49
-
50
- ### ABDB helpers (`configuration-management/abdb`)
51
-
52
- | Export | Description |
53
- |--------|-------------|
54
- | `getClient(params, options)` | Connect to ABDB using IMS credentials from action params |
55
- | `withDbClient(params, fn, options)` | Run work with auto-close |
56
- | `findOne`, `insertOne`, `updateOne`, … | Mongo-style collection helpers |
57
-
58
- ### Scope / path model (`configuration-management/shared`)
59
-
60
- | Export | Description |
61
- |--------|-------------|
62
- | `toStateKey(scope, scopeId, path)` | Encode `section/group/field` as ABDB document `_id` |
63
- | `buildInheritanceChain(scope, scopeId, parentWebsiteId)` | Magento-style fallback chain |
64
- | `isValidPath`, `normalizeScope`, `normalizeScopeId` | Validation helpers |
65
-
66
- ### Encryption (`configuration-management/crypto`)
67
-
68
- | Export | Description |
69
- |--------|-------------|
70
- | `encrypt(plaintext, params)` | AES-256-GCM encrypt for at-rest storage |
71
- | `decrypt(ciphertext, params)` | Decrypt stored values |
72
- | `isEncrypted(value)` | Detect `enc:v1:` wire format |
73
-
74
- ### Commerce REST (`configuration-management/oauth1a`)
75
-
76
- | Export | Description |
77
- |--------|-------------|
78
- | `getCommerceOauthClient(options, logger)` | OAuth 1.0a client for Adobe Commerce REST API |
79
-
80
- ### React Admin UI (`configuration-management/web`)
81
-
82
- Spectrum-based Commerce Admin extension UI for schema-driven system configuration.
83
-
84
- ```js
85
- import { createRoot } from 'react-dom/client'
86
- import {
87
- ConfigurationManagementApp,
88
- configureWeb,
89
- SystemConfig
90
- } from 'configuration-management/web'
91
- import actionUrls from './config.json' // deploy-time URLs from aio app deploy
92
- import 'configuration-management/web/styles.css'
93
-
94
- configureWeb({ actionUrls })
95
-
96
- createRoot(document.getElementById('root')).render(
97
- <ConfigurationManagementApp runtime={runtime} ims={ims} />
98
- )
99
- ```
100
-
101
- | Export | Description |
102
- |--------|-------------|
103
- | `ConfigurationManagementApp` | Full app shell (router + Spectrum provider + UIX registration) |
104
- | `SystemConfig` | Dynamic config form UI |
105
- | `SystemConfigSchemaEditor` | Schema designer |
106
- | `useSystemConfig`, `useSystemConfigSchema` | Data hooks |
107
- | `configureWeb({ actionUrls, extensionId, actionKeys })` | Wire deploy-time action URLs before render |
108
-
109
- Styles: `import 'configuration-management/web/styles.css'`
110
-
111
- ### App Builder actions (`configuration-management/actions`)
112
-
113
- OpenWhisk runtime actions and the Commerce Admin extension manifest ship with the package.
114
-
115
- #### Automatic wiring on `npm install`
116
-
117
- The package runs a **postinstall** script that patches your project's `app.config.yaml`
118
- (if it exists) with:
119
-
120
- ```yaml
121
- extensions:
122
- commerce/backend-ui/1:
123
- $include: node_modules/configuration-management/actions/configurations/ext.config.yaml
124
- ```
125
-
126
- Requirements:
127
-
128
- - Run `npm install` from your App Builder project root (where `app.config.yaml` lives).
129
- - Do not use `npm install --ignore-scripts` (that skips postinstall).
130
-
131
- Opt out for a single install:
132
-
133
- ```bash
134
- CONFIGURATION_MANAGEMENT_SKIP_SETUP=1 npm install configuration-management
135
- ```
136
-
137
- Re-run manually anytime:
138
-
139
- ```bash
140
- npx configuration-management-setup
141
- ```
142
-
143
- #### Manual wiring
144
-
145
- If you prefer to edit `app.config.yaml` yourself:
146
-
147
- ```yaml
148
- extensions:
149
- commerce/backend-ui/1:
150
- $include: node_modules/configuration-management/actions/configurations/ext.config.yaml
151
- ```
152
-
153
- The bundled `ext.config.yaml` declares all actions under the `ConfigurationManagement` package
154
- (`system-config-list`, `system-config-save`, `system-config-schema`, `export-config`,
155
- `import-config`, `commerce-rest-get`, `sync-store-mappings-from-commerce`) plus admin menu
156
- `registration`. It expects a `web-src/` folder at your project root for the UI shell.
157
-
158
- **Minimal host project layout:**
159
-
160
- ```
161
- my-app/
162
- ├── app.config.yaml ← $include ext.config from node_modules (above)
163
- ├── web-src/
164
- │ ├── index.html
165
- │ └── src/
166
- │ ├── index.js ← import ConfigurationManagementApp + configureWeb
167
- │ └── config.json ← generated by aio app deploy
168
- ├── .env
169
- └── package.json ← depends on configuration-management
170
- ```
171
-
172
- Action helper utilities are also exported for custom actions:
173
-
174
- ```js
175
- const { errorResponse, checkMissingRequestInputs } = require('configuration-management/actions/utils')
176
- ```
177
-
178
- ## Environment / action inputs
179
-
180
- | Variable | Purpose |
181
- |----------|---------|
182
- | `AIO_DB_REGION` | ABDB region (`amer`, `emea`, …) |
183
- | `OAUTH_CLIENT_ID`, `OAUTH_CLIENT_SECRET`, `OAUTH_ORG_ID`, `OAUTH_SCOPES` | IMS credentials for ABDB |
184
- | `SYSTEM_CONFIG_CRYPT_KEY` | Preferred encryption key (fallback: `OAUTH_CLIENT_SECRET`) |
185
- | `COMMERCE_BASE_URL`, `COMMERCE_CONSUMER_*`, `COMMERCE_ACCESS_TOKEN*` | Commerce REST (for scope code resolution) |
186
-
187
- ## Storage model
188
-
189
- Documents in the `system_config_data` collection:
190
-
191
- ```
192
- { _id, scope, scope_id, path, value, createdAt, updatedAt }
193
- ```
194
-
195
- Config paths use the format `section/group/field` (e.g. `sync_general/api/url`).
196
-
197
- ## License
198
-
199
- Apache-2.0
@@ -1,151 +0,0 @@
1
- operations:
2
- view:
3
- - type: web
4
- impl: index.html
5
- # Action sources live alongside this manifest (same directory).
6
- actions: .
7
- # Host App Builder apps override `web` to point at their UI shell (see README).
8
- web: ../../../../web-src
9
- runtimeManifest:
10
- packages:
11
- admin-ui-sdk:
12
- license: Apache-2.0
13
- actions:
14
- registration:
15
- function: registration/index.js
16
- web: 'yes'
17
- runtime: 'nodejs:20'
18
- inputs:
19
- LOG_LEVEL: debug
20
- annotations:
21
- require-adobe-auth: true
22
- final: true
23
- ConfigurationManagement:
24
- license: Apache-2.0
25
- actions:
26
- commerce-rest-get:
27
- function: commerce/index.js
28
- web: 'yes'
29
- runtime: 'nodejs:20'
30
- inputs:
31
- LOG_LEVEL: debug
32
- COMMERCE_BASE_URL: $COMMERCE_BASE_URL
33
- COMMERCE_CONSUMER_KEY: $COMMERCE_CONSUMER_KEY
34
- COMMERCE_CONSUMER_SECRET: $COMMERCE_CONSUMER_SECRET
35
- COMMERCE_ACCESS_TOKEN: $COMMERCE_ACCESS_TOKEN
36
- COMMERCE_ACCESS_TOKEN_SECRET: $COMMERCE_ACCESS_TOKEN_SECRET
37
- annotations:
38
- require-adobe-auth: false
39
- final: true
40
- system-config-list:
41
- function: system-config-list/index.js
42
- web: 'yes'
43
- runtime: 'nodejs:20'
44
- inputs:
45
- LOG_LEVEL: debug
46
- SYSTEM_CONFIG_CRYPT_KEY: $SYSTEM_CONFIG_CRYPT_KEY
47
- OAUTH_CLIENT_ID: $OAUTH_CLIENT_ID
48
- OAUTH_CLIENT_SECRET: $OAUTH_CLIENT_SECRET
49
- OAUTH_ORG_ID: $OAUTH_ORG_ID
50
- OAUTH_SCOPES: $OAUTH_SCOPES
51
- AIO_DB_REGION: $AIO_DB_REGION
52
- annotations:
53
- require-adobe-auth: false
54
- include-ims-credentials: true
55
- final: true
56
- system-config-save:
57
- function: system-config-save/index.js
58
- web: 'yes'
59
- runtime: 'nodejs:20'
60
- inputs:
61
- LOG_LEVEL: debug
62
- SYSTEM_CONFIG_CRYPT_KEY: $SYSTEM_CONFIG_CRYPT_KEY
63
- OAUTH_CLIENT_ID: $OAUTH_CLIENT_ID
64
- OAUTH_CLIENT_SECRET: $OAUTH_CLIENT_SECRET
65
- OAUTH_ORG_ID: $OAUTH_ORG_ID
66
- OAUTH_SCOPES: $OAUTH_SCOPES
67
- AIO_DB_REGION: $AIO_DB_REGION
68
- annotations:
69
- require-adobe-auth: false
70
- include-ims-credentials: true
71
- final: true
72
- system-config-schema:
73
- function: system-config-schema/index.js
74
- web: 'yes'
75
- runtime: 'nodejs:20'
76
- inputs:
77
- LOG_LEVEL: debug
78
- OAUTH_CLIENT_ID: $OAUTH_CLIENT_ID
79
- OAUTH_CLIENT_SECRET: $OAUTH_CLIENT_SECRET
80
- OAUTH_ORG_ID: $OAUTH_ORG_ID
81
- OAUTH_SCOPES: $OAUTH_SCOPES
82
- AIO_DB_REGION: $AIO_DB_REGION
83
- annotations:
84
- require-adobe-auth: false
85
- include-ims-credentials: true
86
- final: true
87
- export-config:
88
- function: export-config/index.js
89
- web: 'yes'
90
- runtime: 'nodejs:20'
91
- inputs:
92
- LOG_LEVEL: debug
93
- SYSTEM_CONFIG_CRYPT_KEY: $SYSTEM_CONFIG_CRYPT_KEY
94
- OAUTH_CLIENT_ID: $OAUTH_CLIENT_ID
95
- OAUTH_CLIENT_SECRET: $OAUTH_CLIENT_SECRET
96
- OAUTH_ORG_ID: $OAUTH_ORG_ID
97
- OAUTH_SCOPES: $OAUTH_SCOPES
98
- AIO_DB_REGION: $AIO_DB_REGION
99
- COMMERCE_BASE_URL: $COMMERCE_BASE_URL
100
- COMMERCE_CONSUMER_KEY: $COMMERCE_CONSUMER_KEY
101
- COMMERCE_CONSUMER_SECRET: $COMMERCE_CONSUMER_SECRET
102
- COMMERCE_ACCESS_TOKEN: $COMMERCE_ACCESS_TOKEN
103
- COMMERCE_ACCESS_TOKEN_SECRET: $COMMERCE_ACCESS_TOKEN_SECRET
104
- annotations:
105
- require-adobe-auth: false
106
- include-ims-credentials: true
107
- final: true
108
- import-config:
109
- function: import-config/index.js
110
- web: 'yes'
111
- runtime: 'nodejs:20'
112
- limits:
113
- timeout: 300000
114
- memory: 512
115
- inputs:
116
- LOG_LEVEL: debug
117
- SYSTEM_CONFIG_CRYPT_KEY: $SYSTEM_CONFIG_CRYPT_KEY
118
- OAUTH_CLIENT_ID: $OAUTH_CLIENT_ID
119
- OAUTH_CLIENT_SECRET: $OAUTH_CLIENT_SECRET
120
- OAUTH_ORG_ID: $OAUTH_ORG_ID
121
- OAUTH_SCOPES: $OAUTH_SCOPES
122
- AIO_DB_REGION: $AIO_DB_REGION
123
- COMMERCE_BASE_URL: $COMMERCE_BASE_URL
124
- COMMERCE_CONSUMER_KEY: $COMMERCE_CONSUMER_KEY
125
- COMMERCE_CONSUMER_SECRET: $COMMERCE_CONSUMER_SECRET
126
- COMMERCE_ACCESS_TOKEN: $COMMERCE_ACCESS_TOKEN
127
- COMMERCE_ACCESS_TOKEN_SECRET: $COMMERCE_ACCESS_TOKEN_SECRET
128
- annotations:
129
- require-adobe-auth: false
130
- include-ims-credentials: true
131
- final: true
132
- sync-store-mappings-from-commerce:
133
- function: sync-store-mappings-from-commerce/index.js
134
- web: 'yes'
135
- runtime: 'nodejs:20'
136
- inputs:
137
- LOG_LEVEL: debug
138
- OAUTH_CLIENT_ID: $OAUTH_CLIENT_ID
139
- OAUTH_CLIENT_SECRET: $OAUTH_CLIENT_SECRET
140
- OAUTH_ORG_ID: $OAUTH_ORG_ID
141
- OAUTH_SCOPES: $OAUTH_SCOPES
142
- AIO_DB_REGION: $AIO_DB_REGION
143
- COMMERCE_BASE_URL: $COMMERCE_BASE_URL
144
- COMMERCE_CONSUMER_KEY: $COMMERCE_CONSUMER_KEY
145
- COMMERCE_CONSUMER_SECRET: $COMMERCE_CONSUMER_SECRET
146
- COMMERCE_ACCESS_TOKEN: $COMMERCE_ACCESS_TOKEN
147
- COMMERCE_ACCESS_TOKEN_SECRET: $COMMERCE_ACCESS_TOKEN_SECRET
148
- annotations:
149
- require-adobe-auth: false
150
- include-ims-credentials: true
151
- final: true
@@ -1,241 +0,0 @@
1
- /*
2
- Copyright 2025 Adobe. All rights reserved.
3
- This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
- you may not use this file except in compliance with the License. You may obtain a copy
5
- of the License at http://www.apache.org/licenses/LICENSE-2.0
6
- */
7
-
8
- const { getCommerceOauthClient } = require('./oauth1a')
9
- const {
10
- isValidPath,
11
- toStateKey,
12
- buildInheritanceChain,
13
- normalizeScope,
14
- normalizeScopeId
15
- } = require('./system-config-shared')
16
- const { getClient } = require('./abdb-helper')
17
- const { isEncrypted, decrypt } = require('./system-config-crypto')
18
-
19
- const COLLECTION = 'system_config_data'
20
- const CACHE_TTL_MS = 5 * 60 * 1000
21
-
22
- // Per-lookup result cache.
23
- const cache = new Map() // key: `${scope}:${scopeId}:${path}` → { value, expiresAt }
24
-
25
- // Commerce code → numeric id maps. Refreshed at most every CACHE_TTL_MS.
26
- let websiteCodeToId = null // Map<code, id>
27
- let websiteCodeToIdAt = 0
28
- let storeCodeToId = null // Map<code, id> + parentWebsiteId
29
- let storeCodeToIdAt = 0
30
-
31
- function maybeParseJson (value) {
32
- if (typeof value !== 'string') return value
33
- const trimmed = value.trim()
34
- if (!trimmed) return value
35
- if (!(trimmed.startsWith('{') || trimmed.startsWith('['))) return value
36
- try { return JSON.parse(trimmed) } catch { return value }
37
- }
38
-
39
- async function tryFindOne (collection, query) {
40
- try {
41
- const arr = await collection.find(query).limit(1).toArray()
42
- return arr && arr.length ? arr[0] : null
43
- } catch (err) {
44
- const msg = err && err.message ? String(err.message) : String(err)
45
- if (/not found/i.test(msg)) return null
46
- throw err
47
- }
48
- }
49
-
50
- function pickCommerceCreds (params) {
51
- return {
52
- url: params.COMMERCE_BASE_URL || process.env.COMMERCE_BASE_URL,
53
- consumerKey: params.COMMERCE_CONSUMER_KEY || process.env.COMMERCE_CONSUMER_KEY,
54
- consumerSecret: params.COMMERCE_CONSUMER_SECRET || process.env.COMMERCE_CONSUMER_SECRET,
55
- accessToken: params.COMMERCE_ACCESS_TOKEN || process.env.COMMERCE_ACCESS_TOKEN,
56
- accessTokenSecret: params.COMMERCE_ACCESS_TOKEN_SECRET || process.env.COMMERCE_ACCESS_TOKEN_SECRET
57
- }
58
- }
59
-
60
- async function loadWebsiteCodeMap (params) {
61
- const now = Date.now()
62
- if (websiteCodeToId && (now - websiteCodeToIdAt) < CACHE_TTL_MS) return websiteCodeToId
63
-
64
- const creds = pickCommerceCreds(params)
65
- if (!creds.url) return websiteCodeToId || new Map()
66
-
67
- try {
68
- const oauth = getCommerceOauthClient(creds, { error: () => {}, info: () => {} })
69
- const websites = await oauth.get('store/websites')
70
- const map = new Map()
71
- if (Array.isArray(websites)) {
72
- for (const w of websites) {
73
- if (w && w.code != null && w.id != null) {
74
- map.set(String(w.code), String(w.id))
75
- }
76
- }
77
- }
78
- websiteCodeToId = map
79
- websiteCodeToIdAt = now
80
- return map
81
- } catch (_) {
82
- return websiteCodeToId || new Map()
83
- }
84
- }
85
-
86
- async function loadStoreViewCodeMap (params) {
87
- const now = Date.now()
88
- if (storeCodeToId && (now - storeCodeToIdAt) < CACHE_TTL_MS) return storeCodeToId
89
-
90
- const creds = pickCommerceCreds(params)
91
- if (!creds.url) return storeCodeToId || new Map()
92
-
93
- try {
94
- const oauth = getCommerceOauthClient(creds, { error: () => {}, info: () => {} })
95
- const stores = await oauth.get('store/storeViews')
96
- const map = new Map()
97
- if (Array.isArray(stores)) {
98
- for (const s of stores) {
99
- if (s && s.code != null && s.id != null) {
100
- map.set(String(s.code), { id: String(s.id), websiteId: s.website_id != null ? String(s.website_id) : null })
101
- }
102
- }
103
- }
104
- storeCodeToId = map
105
- storeCodeToIdAt = now
106
- return map
107
- } catch (_) {
108
- return storeCodeToId || new Map()
109
- }
110
- }
111
-
112
- /**
113
- * Resolve a scope code (e.g. website 'ch', store view 'en_ch') to its numeric
114
- * id via Commerce REST. Returns null when the code can't be resolved AND no
115
- * verbatim fallback is wanted.
116
- */
117
- async function resolveScopeId (scope, code, params) {
118
- if (!code) return null
119
- if (scope === 'websites') {
120
- const map = await loadWebsiteCodeMap(params)
121
- return map.get(String(code)) || null
122
- }
123
- if (scope === 'stores') {
124
- const map = await loadStoreViewCodeMap(params)
125
- return map.get(String(code))?.id || null
126
- }
127
- return null
128
- }
129
-
130
- /**
131
- * Look up a single config value from ABDB.
132
- *
133
- * @param {string} path `<section>/<group>/<field>` (e.g. 'campaign_general/url/url')
134
- * @param {object} [params] action params containing OAuth + crypto + Commerce creds.
135
- * Falls back to process.env when omitted.
136
- * @param {object} [options]
137
- * @param {string} [options.scope='default'] 'default' | 'websites' | 'stores'
138
- * @param {string} [options.scopeId] website / store id (numeric string); takes precedence over scopeCode
139
- * @param {string} [options.scopeCode] website / store-view CODE — resolved to numeric id via Commerce REST
140
- * @param {string|number} [options.parentWebsiteId] used when scope='stores' to fall back to the parent website
141
- * @param {string} [options.parentWebsiteCode] same as parentWebsiteId but resolved from a website code
142
- * @param {boolean} [options.fresh] bypass the cache
143
- * @returns {Promise<*|null>}
144
- */
145
- async function getConfig (path, params = {}, options = {}) {
146
- if (!isValidPath(path)) return null
147
-
148
- let scope
149
- try {
150
- scope = normalizeScope(options.scope)
151
- } catch (_) {
152
- return null
153
- }
154
-
155
- // 1. Resolve the active scope id (numeric).
156
- let resolvedScopeId
157
- if (scope === 'default') {
158
- resolvedScopeId = '0'
159
- } else if (options.scopeId != null && options.scopeId !== '') {
160
- resolvedScopeId = String(options.scopeId)
161
- } else if (options.scopeCode) {
162
- const fromCommerce = await resolveScopeId(scope, options.scopeCode, params)
163
- // If Commerce isn't reachable, fall back to using the code verbatim — the
164
- // value still gets queried, and the legacy `getSystemConfig` shim writes
165
- // under the literal code so this keeps working.
166
- resolvedScopeId = fromCommerce || String(options.scopeCode)
167
- } else {
168
- return null
169
- }
170
-
171
- // 2. Resolve the parent website id for store-scope inheritance.
172
- let parentWebsiteId = options.parentWebsiteId
173
- if (parentWebsiteId == null && options.parentWebsiteCode) {
174
- parentWebsiteId =
175
- await resolveScopeId('websites', options.parentWebsiteCode, params) ||
176
- String(options.parentWebsiteCode)
177
- }
178
- // Auto-derive parentWebsiteId from the store view itself when not given.
179
- if (parentWebsiteId == null && scope === 'stores' && options.scopeCode) {
180
- const sMap = await loadStoreViewCodeMap(params)
181
- parentWebsiteId = sMap.get(String(options.scopeCode))?.websiteId || undefined
182
- }
183
-
184
- let normalizedScopeId
185
- try {
186
- normalizedScopeId = normalizeScopeId(scope, resolvedScopeId)
187
- } catch (_) {
188
- return null
189
- }
190
-
191
- const cacheKey = `${scope}:${normalizedScopeId}:${path}`
192
- const now = Date.now()
193
- if (!options.fresh) {
194
- const c = cache.get(cacheKey)
195
- if (c && c.expiresAt > now) return c.value
196
- }
197
-
198
- let handle
199
- try {
200
- handle = await getClient(params)
201
- } catch (_) {
202
- return null
203
- }
204
-
205
- try {
206
- const collection = await handle.client.collection(COLLECTION)
207
- const chain = buildInheritanceChain(scope, normalizedScopeId, parentWebsiteId)
208
-
209
- let resolved = null
210
- for (const link of chain) {
211
- const id = toStateKey(link.scope, link.scopeId, path)
212
- const doc = await tryFindOne(collection, { _id: id })
213
- if (!doc || doc.value === undefined) continue
214
- let value = doc.value
215
- if (isEncrypted(value)) {
216
- try { value = decrypt(value, params) } catch (_) { /* keep raw */ }
217
- }
218
- value = maybeParseJson(value)
219
- resolved = value
220
- break
221
- }
222
-
223
- cache.set(cacheKey, { value: resolved, expiresAt: now + CACHE_TTL_MS })
224
- return resolved
225
- } finally {
226
- try { await handle.close() } catch (_) { /* noop */ }
227
- }
228
- }
229
-
230
- /** Clear the entire in-process cache (e.g. after a re-migration). */
231
- function clearAbdbConfigCache () {
232
- cache.clear()
233
- websiteCodeToId = null
234
- storeCodeToId = null
235
- }
236
-
237
- module.exports = {
238
- COLLECTION,
239
- getConfig,
240
- clearAbdbConfigCache
241
- }