configuration-management 0.1.2 → 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 +4 -2
- package/README.md +0 -199
- package/actions/configurations/ext.config.yaml +0 -151
- package/src/abdb-config.js +0 -241
- package/src/abdb-helper.js +0 -476
- package/src/index.js +0 -20
- package/src/oauth1a.js +0 -135
- package/src/system-config-crypto.js +0 -113
- package/src/system-config-shared.js +0 -89
- package/web/styles.css +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "configuration-management",
|
|
3
|
-
"version": "0.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.",
|
|
@@ -29,7 +29,9 @@
|
|
|
29
29
|
"./shared": "./src/system-config-shared.js",
|
|
30
30
|
"./oauth1a": "./src/oauth1a.js",
|
|
31
31
|
"./web": "./web/index.js",
|
|
32
|
-
"./web/
|
|
32
|
+
"./web/index.js": "./web/index.js",
|
|
33
|
+
"./web/src/styles/index.css": "./web/src/styles/index.css",
|
|
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"
|
|
35
37
|
},
|
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
|
package/src/abdb-config.js
DELETED
|
@@ -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
|
-
}
|