configuration-management 0.1.0
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/README.md +199 -0
- package/actions/configurations/commerce/index.js +55 -0
- package/actions/configurations/export-config/index.js +259 -0
- package/actions/configurations/ext.config.yaml +151 -0
- package/actions/configurations/import-config/index.js +544 -0
- package/actions/configurations/registration/index.js +37 -0
- package/actions/configurations/sync-store-mappings-from-commerce/index.js +199 -0
- package/actions/configurations/system-config-list/index.js +127 -0
- package/actions/configurations/system-config-save/index.js +160 -0
- package/actions/configurations/system-config-schema/index.js +327 -0
- package/actions/utils.js +73 -0
- package/package.json +74 -0
- package/scripts/setup-app-config.js +114 -0
- package/src/abdb-config.js +241 -0
- package/src/abdb-helper.js +476 -0
- package/src/index.js +20 -0
- package/src/oauth1a.js +135 -0
- package/src/system-config-crypto.js +113 -0
- package/src/system-config-shared.js +89 -0
- package/web/src/components/App.js +47 -0
- package/web/src/components/AppSectionNav.js +49 -0
- package/web/src/components/ExtensionRegistration.js +33 -0
- package/web/src/components/MainPage.js +46 -0
- package/web/src/components/SystemConfig.js +1464 -0
- package/web/src/components/SystemConfigSchemaEditor.js +459 -0
- package/web/src/hooks/useConfirm.js +355 -0
- package/web/src/hooks/useSystemConfig.js +238 -0
- package/web/src/hooks/useSystemConfigSchema.js +102 -0
- package/web/src/index.js +41 -0
- package/web/src/schema/systemConfigSchema.js +82 -0
- package/web/src/settings.js +57 -0
- package/web/src/styles/index.css +326 -0
- package/web/src/theme.js +104 -0
- package/web/src/utils/storeMappingsFromCommerceRest.js +73 -0
- package/web/src/utils.js +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
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
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { Core } = require('@adobe/aio-sdk')
|
|
14
|
+
const { errorResponse, checkMissingRequestInputs } = require('../../utils')
|
|
15
|
+
const { getCommerceOauthClient } = require('configuration-management/oauth1a')
|
|
16
|
+
|
|
17
|
+
async function main(params) {
|
|
18
|
+
const logger = Core.Logger('main', { level: params.LOG_LEVEL || 'info' })
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const requiredParams = ['operation', 'COMMERCE_BASE_URL']
|
|
22
|
+
const requiredHeaders = ['Authorization']
|
|
23
|
+
const errorMessage = checkMissingRequestInputs(params, requiredParams, requiredHeaders)
|
|
24
|
+
if (errorMessage) {
|
|
25
|
+
// return and log client errors
|
|
26
|
+
return errorResponse(400, errorMessage, logger)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { operation } = params
|
|
30
|
+
const oauth = getCommerceOauthClient(
|
|
31
|
+
{
|
|
32
|
+
url: params.COMMERCE_BASE_URL,
|
|
33
|
+
consumerKey: params.COMMERCE_CONSUMER_KEY,
|
|
34
|
+
consumerSecret: params.COMMERCE_CONSUMER_SECRET,
|
|
35
|
+
accessToken: params.COMMERCE_ACCESS_TOKEN,
|
|
36
|
+
accessTokenSecret: params.COMMERCE_ACCESS_TOKEN_SECRET
|
|
37
|
+
},
|
|
38
|
+
logger
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const content = await oauth.get(operation)
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
statusCode: 200,
|
|
45
|
+
body: content
|
|
46
|
+
}
|
|
47
|
+
} catch (error) {
|
|
48
|
+
// log any server errors
|
|
49
|
+
logger.error(error)
|
|
50
|
+
// return with 500
|
|
51
|
+
return errorResponse(500, error, logger)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
exports.main = main
|
|
@@ -0,0 +1,259 @@
|
|
|
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
|
+
// Export the entire system_config (schema + values) to a portable JSON
|
|
9
|
+
// document. The result includes:
|
|
10
|
+
// - schema : the document stored under system_config_schema._id = 'v1'
|
|
11
|
+
// - values : every row in system_config_data, as { scope, scope_id, path, value }
|
|
12
|
+
// - meta : timestamp + counts so the operator can sanity-check the dump
|
|
13
|
+
//
|
|
14
|
+
// Sensitive fields are exported as their ENCRYPTED ciphertext so the dump is
|
|
15
|
+
// safe to share, but it can only be re-imported into a workspace whose
|
|
16
|
+
// SYSTEM_CONFIG_CRYPT_KEY matches the one used when these values were saved.
|
|
17
|
+
//
|
|
18
|
+
// Trigger from the UI's "Export → JSON" button, or invoke directly:
|
|
19
|
+
// POST .../system-config-export
|
|
20
|
+
// body: {} → full dump
|
|
21
|
+
// body: { schemaOnly: true } → omit values
|
|
22
|
+
// body: { valuesOnly: true } → omit schema
|
|
23
|
+
// body: { scopes: ['default','websites'] } → filter by scope tuple
|
|
24
|
+
//
|
|
25
|
+
// Response: { ok, dump }, where `dump` is the JSON the caller saves as a file.
|
|
26
|
+
|
|
27
|
+
const { Core } = require('@adobe/aio-sdk')
|
|
28
|
+
const { errorResponse } = require('../../utils')
|
|
29
|
+
const { getClient } = require('configuration-management/abdb')
|
|
30
|
+
const { isEncrypted, decrypt } = require('configuration-management/crypto')
|
|
31
|
+
const { getCommerceOauthClient } = require('configuration-management/oauth1a')
|
|
32
|
+
|
|
33
|
+
const SCHEMA_COLLECTION = 'system_config_schema'
|
|
34
|
+
const SCHEMA_DOC_ID = 'v1'
|
|
35
|
+
const DATA_COLLECTION = 'system_config_data'
|
|
36
|
+
// Bumped to v2 when we started decrypting sensitive values into plaintext on
|
|
37
|
+
// export. Importers ≥ v2 know to re-encrypt with the target env's key.
|
|
38
|
+
const EXPORT_VERSION = 2
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Walk a schema doc and return the set of "section/group/field" paths whose
|
|
42
|
+
* field is marked `sensitive: true`. Used by both export (decide what to
|
|
43
|
+
* decrypt) and import (decide what to re-encrypt against the target's key).
|
|
44
|
+
*/
|
|
45
|
+
function deriveLanguageCode (code) {
|
|
46
|
+
const m = String(code || '').toLowerCase().match(/^([a-z]{2})_/)
|
|
47
|
+
return m ? m[1] : 'en'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build a Commerce-style store_mappings blob from live REST data:
|
|
52
|
+
* { storeId: { code, language_code, website_code, website_id } }
|
|
53
|
+
* Returns null if Commerce credentials aren't configured or the call fails.
|
|
54
|
+
* Embedding this in every export lets cross-env imports translate
|
|
55
|
+
* website_id / store_id by matching `website_code` / store `code` against the
|
|
56
|
+
* target env's own Commerce, regardless of whether sync-store-mappings was
|
|
57
|
+
* ever run on the source.
|
|
58
|
+
*/
|
|
59
|
+
async function fetchSourceStoreMappingsFromCommerce (params, logger) {
|
|
60
|
+
if (!params.COMMERCE_BASE_URL || !params.COMMERCE_CONSUMER_KEY) return null
|
|
61
|
+
try {
|
|
62
|
+
const oauth = getCommerceOauthClient({
|
|
63
|
+
url: params.COMMERCE_BASE_URL,
|
|
64
|
+
consumerKey: params.COMMERCE_CONSUMER_KEY,
|
|
65
|
+
consumerSecret: params.COMMERCE_CONSUMER_SECRET,
|
|
66
|
+
accessToken: params.COMMERCE_ACCESS_TOKEN,
|
|
67
|
+
accessTokenSecret: params.COMMERCE_ACCESS_TOKEN_SECRET
|
|
68
|
+
}, logger)
|
|
69
|
+
const [storeViews, websites] = await Promise.all([
|
|
70
|
+
oauth.get('store/storeViews'),
|
|
71
|
+
oauth.get('store/websites')
|
|
72
|
+
])
|
|
73
|
+
const websiteById = new Map()
|
|
74
|
+
for (const w of websites || []) {
|
|
75
|
+
if (w && w.id != null) websiteById.set(String(w.id), w)
|
|
76
|
+
}
|
|
77
|
+
const mapping = {}
|
|
78
|
+
for (const sv of storeViews || []) {
|
|
79
|
+
if (!sv || sv.id == null) continue
|
|
80
|
+
const storeId = String(sv.id)
|
|
81
|
+
if (storeId === '0' || sv.code === 'admin') continue
|
|
82
|
+
const websiteId = sv.website_id != null ? String(sv.website_id) : ''
|
|
83
|
+
const website = websiteById.get(websiteId)
|
|
84
|
+
mapping[storeId] = {
|
|
85
|
+
code: String(sv.code || ''),
|
|
86
|
+
language_code: deriveLanguageCode(sv.code),
|
|
87
|
+
website_code: website ? String(website.code || '') : '',
|
|
88
|
+
website_id: websiteId
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return Object.keys(mapping).length ? mapping : null
|
|
92
|
+
} catch (err) {
|
|
93
|
+
if (logger) logger.warn(`Export: Commerce store_mappings lookup failed: ${err.message}`)
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function collectSensitivePaths (schema) {
|
|
99
|
+
const out = new Set()
|
|
100
|
+
if (!schema || !Array.isArray(schema.sections)) return out
|
|
101
|
+
for (const s of schema.sections) {
|
|
102
|
+
if (!s || !Array.isArray(s.groups)) continue
|
|
103
|
+
for (const g of s.groups) {
|
|
104
|
+
if (!g || !Array.isArray(g.fields)) continue
|
|
105
|
+
for (const f of g.fields) {
|
|
106
|
+
if (f && f.sensitive) out.add(`${s.id}/${g.id}/${f.id}`)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return out
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function tryFindOne (collection, query) {
|
|
114
|
+
try {
|
|
115
|
+
const arr = await collection.find(query).limit(1).toArray()
|
|
116
|
+
return arr && arr.length ? arr[0] : null
|
|
117
|
+
} catch (err) {
|
|
118
|
+
const msg = err && err.message ? String(err.message) : String(err)
|
|
119
|
+
if (/not found/i.test(msg)) return null
|
|
120
|
+
throw err
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function main (params) {
|
|
125
|
+
const logger = Core.Logger('export-config', { level: params.LOG_LEVEL || 'info' })
|
|
126
|
+
|
|
127
|
+
const schemaOnly = params.schemaOnly === true || params.schemaOnly === 'true'
|
|
128
|
+
const valuesOnly = params.valuesOnly === true || params.valuesOnly === 'true'
|
|
129
|
+
const scopeFilter = Array.isArray(params.scopes) && params.scopes.length
|
|
130
|
+
? new Set(params.scopes.map(String))
|
|
131
|
+
: null
|
|
132
|
+
|
|
133
|
+
let dbHandle
|
|
134
|
+
try {
|
|
135
|
+
dbHandle = await getClient(params)
|
|
136
|
+
} catch (e) {
|
|
137
|
+
logger.error(`ABDB connect failed: ${e.message}`)
|
|
138
|
+
return errorResponse(500, `ABDB connect failed: ${e.message}`, logger)
|
|
139
|
+
}
|
|
140
|
+
const { client, close } = dbHandle
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
let schema = null
|
|
144
|
+
if (!valuesOnly) {
|
|
145
|
+
const schemaCol = await client.collection(SCHEMA_COLLECTION)
|
|
146
|
+
const doc = await tryFindOne(schemaCol, { _id: SCHEMA_DOC_ID })
|
|
147
|
+
schema = doc && doc.schema ? doc.schema : { sections: [] }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// We need the schema to know which paths are sensitive, even when the
|
|
151
|
+
// caller asked for valuesOnly — load it locally without including it in
|
|
152
|
+
// the dump.
|
|
153
|
+
let schemaForFlags = schema
|
|
154
|
+
if (!schemaForFlags) {
|
|
155
|
+
try {
|
|
156
|
+
const schemaCol = await client.collection(SCHEMA_COLLECTION)
|
|
157
|
+
const doc = await tryFindOne(schemaCol, { _id: SCHEMA_DOC_ID })
|
|
158
|
+
schemaForFlags = doc && doc.schema ? doc.schema : null
|
|
159
|
+
} catch (_) { /* ok if missing */ }
|
|
160
|
+
}
|
|
161
|
+
const sensitivePaths = collectSensitivePaths(schemaForFlags)
|
|
162
|
+
|
|
163
|
+
// Always pull the SOURCE env's Commerce mapping so we can stamp every
|
|
164
|
+
// website/store-scoped row with its scope_code (website_code / store
|
|
165
|
+
// view code). The importer then needs only the target's Commerce — no
|
|
166
|
+
// separate storeMappings blob has to be carried between envs.
|
|
167
|
+
const storeMappingsFromCommerce = await fetchSourceStoreMappingsFromCommerce(params, logger)
|
|
168
|
+
// Build quick lookup tables: websiteId → website_code, storeId → store code.
|
|
169
|
+
const websiteCodeById = new Map()
|
|
170
|
+
const storeCodeById = new Map()
|
|
171
|
+
if (storeMappingsFromCommerce) {
|
|
172
|
+
for (const [storeId, m] of Object.entries(storeMappingsFromCommerce)) {
|
|
173
|
+
if (!m) continue
|
|
174
|
+
if (m.website_id != null && m.website_code) {
|
|
175
|
+
websiteCodeById.set(String(m.website_id), String(m.website_code))
|
|
176
|
+
}
|
|
177
|
+
if (m.code) storeCodeById.set(String(storeId), String(m.code))
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let values = []
|
|
182
|
+
let decryptedCount = 0
|
|
183
|
+
let decryptFailedCount = 0
|
|
184
|
+
if (!schemaOnly) {
|
|
185
|
+
const dataCol = await client.collection(DATA_COLLECTION)
|
|
186
|
+
const docs = await dataCol.find({}).toArray().catch(() => [])
|
|
187
|
+
for (const d of docs) {
|
|
188
|
+
if (!d || typeof d.path !== 'string') continue
|
|
189
|
+
if (scopeFilter && !scopeFilter.has(d.scope)) continue
|
|
190
|
+
let value = d.value
|
|
191
|
+
// Decrypt sensitive ciphertext using THIS env's key so the dump is
|
|
192
|
+
// portable to any other workspace. The recipient will re-encrypt
|
|
193
|
+
// with its own key based on schema.sensitive flags. This means the
|
|
194
|
+
// exported JSON file contains plaintext secrets — treat it as
|
|
195
|
+
// sensitive.
|
|
196
|
+
if (isEncrypted(value)) {
|
|
197
|
+
try {
|
|
198
|
+
value = decrypt(value, params)
|
|
199
|
+
decryptedCount++
|
|
200
|
+
} catch (e) {
|
|
201
|
+
// Couldn't decrypt — keep the ciphertext envelope so a target
|
|
202
|
+
// workspace with the matching key can still pick it up via the
|
|
203
|
+
// legacy sourceCryptKey path.
|
|
204
|
+
decryptFailedCount++
|
|
205
|
+
logger.warn(`Export: failed to decrypt ${d.path} @ ${d.scope}:${d.scope_id}: ${e.message}`)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// Tag the row with the source env's code (website_code or store
|
|
209
|
+
// view code). At import time the recipient looks up its own Commerce
|
|
210
|
+
// and resolves scope_code → target scope_id directly, with no need
|
|
211
|
+
// to ship a separate storeMappings blob.
|
|
212
|
+
let scopeCode
|
|
213
|
+
if (d.scope === 'websites') scopeCode = websiteCodeById.get(String(d.scope_id))
|
|
214
|
+
else if (d.scope === 'stores') scopeCode = storeCodeById.get(String(d.scope_id))
|
|
215
|
+
values.push({
|
|
216
|
+
scope: d.scope,
|
|
217
|
+
scope_id: d.scope_id,
|
|
218
|
+
...(scopeCode ? { scope_code: scopeCode } : {}),
|
|
219
|
+
path: d.path,
|
|
220
|
+
value
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
// Stable ordering for diffable dumps.
|
|
224
|
+
values.sort((a, b) => {
|
|
225
|
+
if (a.scope !== b.scope) return a.scope.localeCompare(b.scope)
|
|
226
|
+
if (a.scope_id !== b.scope_id) return String(a.scope_id).localeCompare(String(b.scope_id))
|
|
227
|
+
return a.path.localeCompare(b.path)
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const dump = {
|
|
232
|
+
__format: 'adobe-commerce-app-builder/system-config-export',
|
|
233
|
+
__version: EXPORT_VERSION,
|
|
234
|
+
exportedAt: new Date().toISOString(),
|
|
235
|
+
// List of sensitive paths so the importer can re-encrypt them with the
|
|
236
|
+
// target env's key without needing to re-derive from the schema.
|
|
237
|
+
sensitivePaths: Array.from(sensitivePaths),
|
|
238
|
+
sensitiveDecrypted: decryptedCount,
|
|
239
|
+
sensitiveDecryptFailed: decryptFailedCount,
|
|
240
|
+
counts: {
|
|
241
|
+
sections: schema ? (schema.sections || []).length : 0,
|
|
242
|
+
values: values.length,
|
|
243
|
+
scopeCoded: values.filter(v => v.scope_code).length
|
|
244
|
+
},
|
|
245
|
+
...(valuesOnly ? {} : { schema }),
|
|
246
|
+
...(schemaOnly ? {} : { values })
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
logger.info(`Exported: ${dump.counts.sections} section(s), ${dump.counts.values} value(s)`)
|
|
250
|
+
return { statusCode: 200, body: { ok: true, dump } }
|
|
251
|
+
} catch (error) {
|
|
252
|
+
logger.error(error)
|
|
253
|
+
return errorResponse(500, error.message || 'Export failed', logger)
|
|
254
|
+
} finally {
|
|
255
|
+
try { await close() } catch (_) {}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
exports.main = main
|
|
@@ -0,0 +1,151 @@
|
|
|
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
|