configuration-management 0.1.3 → 0.1.6
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 +216 -0
- package/actions/configurations/ext.config.yaml +151 -0
- package/package.json +9 -6
- package/scripts/build-web.js +49 -0
- package/scripts/setup.js +260 -0
- package/scripts/templates/exc-runtime.js +14 -0
- package/scripts/templates/web-src-index.html +16 -0
- package/scripts/templates/web-src-index.js +61 -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/dist/index.js +2717 -0
- package/web/index.js +1 -3
- package/scripts/setup-app-config.js +0 -154
package/scripts/setup.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
Copyright 2025 Adobe. All rights reserved.
|
|
4
|
+
Licensed under the Apache License, Version 2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs')
|
|
8
|
+
const path = require('path')
|
|
9
|
+
|
|
10
|
+
const EXTENSION_POINT = 'commerce/backend-ui/1'
|
|
11
|
+
const INCLUDE_REL = 'node_modules/configuration-management/actions/configurations/ext.config.yaml'
|
|
12
|
+
const MARKER = '# configuration-management (auto-linked on npm install)'
|
|
13
|
+
const WEB_BOOTSTRAP_MARKER = 'configuration-management: auto-generated bootstrap'
|
|
14
|
+
|
|
15
|
+
const TEMPLATES_DIR = path.join(__dirname, 'templates')
|
|
16
|
+
|
|
17
|
+
function escapeRe (str) {
|
|
18
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function findProjectRoot (startDir) {
|
|
22
|
+
let dir = startDir
|
|
23
|
+
while (dir && dir !== path.dirname(dir)) {
|
|
24
|
+
if (
|
|
25
|
+
fs.existsSync(path.join(dir, 'app.config.yaml')) ||
|
|
26
|
+
fs.existsSync(path.join(dir, 'web-src'))
|
|
27
|
+
) {
|
|
28
|
+
return dir
|
|
29
|
+
}
|
|
30
|
+
dir = path.dirname(dir)
|
|
31
|
+
}
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resolveProjectRoot () {
|
|
36
|
+
const initCwd = process.env.INIT_CWD
|
|
37
|
+
if (initCwd) {
|
|
38
|
+
const fromInit = findProjectRoot(initCwd)
|
|
39
|
+
if (fromInit) return fromInit
|
|
40
|
+
}
|
|
41
|
+
return findProjectRoot(process.cwd())
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readTemplate (name) {
|
|
45
|
+
return fs.readFileSync(path.join(TEMPLATES_DIR, name), 'utf8')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function writeIfMissingOrManaged (filePath, content, marker) {
|
|
49
|
+
if (fs.existsSync(filePath)) {
|
|
50
|
+
const existing = fs.readFileSync(filePath, 'utf8')
|
|
51
|
+
if (existing.includes(marker)) {
|
|
52
|
+
const dir = path.dirname(filePath)
|
|
53
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
54
|
+
const changed = existing !== content
|
|
55
|
+
if (changed) {
|
|
56
|
+
fs.writeFileSync(filePath, content, 'utf8')
|
|
57
|
+
}
|
|
58
|
+
return { changed, reason: changed ? 'updated-managed' : 'unchanged-managed', path: filePath }
|
|
59
|
+
}
|
|
60
|
+
return { changed: false, reason: 'skipped-custom-file', path: filePath }
|
|
61
|
+
}
|
|
62
|
+
const dir = path.dirname(filePath)
|
|
63
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
64
|
+
fs.writeFileSync(filePath, content, 'utf8')
|
|
65
|
+
return { changed: true, reason: 'written', path: filePath }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function writeWebSrcIndex (projectRoot) {
|
|
69
|
+
const filePath = path.join(projectRoot, 'web-src', 'src', 'index.js')
|
|
70
|
+
const content = readTemplate('web-src-index.js')
|
|
71
|
+
|
|
72
|
+
if (!fs.existsSync(filePath)) {
|
|
73
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
74
|
+
fs.writeFileSync(filePath, content, 'utf8')
|
|
75
|
+
return { changed: true, reason: 'written', path: filePath }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const existing = fs.readFileSync(filePath, 'utf8')
|
|
79
|
+
if (existing.includes(WEB_BOOTSTRAP_MARKER)) {
|
|
80
|
+
const changed = existing !== content
|
|
81
|
+
if (changed) {
|
|
82
|
+
fs.writeFileSync(filePath, content, 'utf8')
|
|
83
|
+
}
|
|
84
|
+
return { changed, reason: changed ? 'updated-managed' : 'unchanged-managed', path: filePath }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (existing.includes('configuration-management/web')) {
|
|
88
|
+
fs.writeFileSync(filePath, content, 'utf8')
|
|
89
|
+
return { changed: true, reason: 'migrated-stale-bootstrap', path: filePath }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { changed: false, reason: 'skipped-custom-file', path: filePath }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function alreadyLinked (content) {
|
|
96
|
+
return content.includes('configuration-management/actions/configurations/ext.config.yaml')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function hasExtensionPoint (content) {
|
|
100
|
+
return /^[ \t]*commerce\/backend-ui\/1:/m.test(content)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function buildExtensionBlock () {
|
|
104
|
+
return [
|
|
105
|
+
MARKER,
|
|
106
|
+
'extensions:',
|
|
107
|
+
` ${EXTENSION_POINT}:`,
|
|
108
|
+
` $include: ${INCLUDE_REL}`
|
|
109
|
+
].join('\n')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function updateExistingExtensionBlock (content) {
|
|
113
|
+
const match = content.match(/^([ \t]*)commerce\/backend-ui\/1:/m)
|
|
114
|
+
if (!match) return null
|
|
115
|
+
|
|
116
|
+
const indent = match[1]
|
|
117
|
+
const includeIndent = `${indent} `
|
|
118
|
+
const blockRe = new RegExp(
|
|
119
|
+
`^${escapeRe(indent)}commerce/backend-ui/1:[ \\t]*\\n` +
|
|
120
|
+
`(?:${escapeRe(includeIndent)}\\$include:[^\\n]*\\n)?`,
|
|
121
|
+
'm'
|
|
122
|
+
)
|
|
123
|
+
const replacement =
|
|
124
|
+
`${indent}${EXTENSION_POINT}:\n${includeIndent}$include: ${INCLUDE_REL}\n`
|
|
125
|
+
const next = content.replace(blockRe, replacement)
|
|
126
|
+
return next !== content ? next : null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function patchAppConfig (content) {
|
|
130
|
+
if (alreadyLinked(content)) {
|
|
131
|
+
return { content, changed: false, reason: 'already-linked' }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (hasExtensionPoint(content)) {
|
|
135
|
+
const updated = updateExistingExtensionBlock(content)
|
|
136
|
+
if (updated) {
|
|
137
|
+
return { content: updated, changed: true, reason: 'updated-existing-extension' }
|
|
138
|
+
}
|
|
139
|
+
return { content, changed: false, reason: 'extension-exists-unmodified' }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (/^extensions:[ \t]*\n/m.test(content)) {
|
|
143
|
+
const injection = ` ${EXTENSION_POINT}:\n $include: ${INCLUDE_REL}\n`
|
|
144
|
+
const next = content.replace(/^extensions:[ \t]*\n/m, `extensions:\n${injection}`)
|
|
145
|
+
if (next !== content) {
|
|
146
|
+
return { content: next, changed: true, reason: 'added-under-extensions' }
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!/^extensions:/m.test(content)) {
|
|
151
|
+
const trimmed = content.replace(/\s+$/, '')
|
|
152
|
+
const separator = trimmed.length > 0 ? '\n\n' : ''
|
|
153
|
+
return {
|
|
154
|
+
content: `${trimmed}${separator}${buildExtensionBlock()}\n`,
|
|
155
|
+
changed: true,
|
|
156
|
+
reason: 'appended'
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { content, changed: false, reason: 'no-change' }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function setupAppConfig (projectRoot) {
|
|
164
|
+
const appConfigPath = path.join(projectRoot, 'app.config.yaml')
|
|
165
|
+
if (!fs.existsSync(appConfigPath)) {
|
|
166
|
+
return { changed: false, reason: 'no-app-config' }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const original = fs.readFileSync(appConfigPath, 'utf8')
|
|
170
|
+
const { content, changed, reason } = patchAppConfig(original)
|
|
171
|
+
if (!changed) {
|
|
172
|
+
return { changed: false, reason }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
fs.writeFileSync(appConfigPath, content, 'utf8')
|
|
176
|
+
return { changed: true, reason, detail: INCLUDE_REL }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function setupWebSrc (projectRoot) {
|
|
180
|
+
const webSrcDir = path.join(projectRoot, 'web-src')
|
|
181
|
+
const results = []
|
|
182
|
+
|
|
183
|
+
const indexJs = writeWebSrcIndex(projectRoot)
|
|
184
|
+
results.push(indexJs)
|
|
185
|
+
|
|
186
|
+
const indexHtml = writeIfMissingOrManaged(
|
|
187
|
+
path.join(webSrcDir, 'index.html'),
|
|
188
|
+
readTemplate('web-src-index.html'),
|
|
189
|
+
'configuration-management'
|
|
190
|
+
)
|
|
191
|
+
results.push(indexHtml)
|
|
192
|
+
|
|
193
|
+
const excRuntime = writeIfMissingOrManaged(
|
|
194
|
+
path.join(webSrcDir, 'src', 'exc-runtime.js'),
|
|
195
|
+
readTemplate('exc-runtime.js'),
|
|
196
|
+
'Module Runtime: Needs to be within an iframe'
|
|
197
|
+
)
|
|
198
|
+
results.push(excRuntime)
|
|
199
|
+
|
|
200
|
+
const configPath = path.join(webSrcDir, 'src', 'config.json')
|
|
201
|
+
if (!fs.existsSync(configPath)) {
|
|
202
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true })
|
|
203
|
+
fs.writeFileSync(configPath, '{}\n', 'utf8')
|
|
204
|
+
results.push({ changed: true, reason: 'created-config-json', path: configPath })
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const changed = results.some((r) => r.changed)
|
|
208
|
+
return { changed, results }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function main () {
|
|
212
|
+
if (process.env.CONFIGURATION_MANAGEMENT_SKIP_SETUP === '1') {
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const projectRoot = resolveProjectRoot()
|
|
217
|
+
if (!projectRoot) {
|
|
218
|
+
console.log(
|
|
219
|
+
'[configuration-management] No App Builder project found — skip setup. ' +
|
|
220
|
+
'Run `npx configuration-management-setup` from your project root after `aio app init`.'
|
|
221
|
+
)
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const app = setupAppConfig(projectRoot)
|
|
226
|
+
if (app.changed) {
|
|
227
|
+
console.log(
|
|
228
|
+
`[configuration-management] Updated app.config.yaml (${app.reason}):\n` +
|
|
229
|
+
` $include: ${app.detail}`
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const web = setupWebSrc(projectRoot)
|
|
234
|
+
for (const r of web.results || []) {
|
|
235
|
+
if (r.changed) {
|
|
236
|
+
console.log(`[configuration-management] Wrote ${r.path}`)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!app.changed && !web.changed) {
|
|
241
|
+
console.log('[configuration-management] Project already configured.')
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (require.main === module) {
|
|
246
|
+
try {
|
|
247
|
+
main()
|
|
248
|
+
} catch (err) {
|
|
249
|
+
console.error('[configuration-management] setup failed:', err.message)
|
|
250
|
+
process.exitCode = 1
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
module.exports = {
|
|
255
|
+
patchAppConfig,
|
|
256
|
+
setupAppConfig,
|
|
257
|
+
setupWebSrc,
|
|
258
|
+
INCLUDE_REL,
|
|
259
|
+
EXTENSION_POINT
|
|
260
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
/* eslint-disable-next-line */
|
|
14
|
+
(function(e,t){if(t.location===t.parent.location)throw new Error("Module Runtime: Needs to be within an iframe!");var o=function(e){var t=new URL(e.location.href).searchParams.get("_mr");return t||!e.EXC_US_HMR?t:e.sessionStorage.getItem("unifiedShellMRScript")}(t);if(!o)throw new Error("Module Runtime: Missing script!");if("https:"!==(o=new URL(decodeURIComponent(o))).protocol)throw new Error("Module Runtime: Must be HTTPS!");if(!/^(exc-unifiedcontent\.)?experience(-qa|-stage|-cdn|-cdn-stage)?\.adobe\.(com|net)$/.test(o.hostname)&&!/localhost\.corp\.adobe\.com$/.test(o.hostname))throw new Error("Module Runtime: Invalid domain!");if(!/\.js$/.test(o.pathname))throw new Error("Module Runtime: Must be a JavaScript file!");t.EXC_US_HMR&&t.sessionStorage.setItem("unifiedShellMRScript",o.toString());var n=e.createElement("script");n.async=1,n.src=o.toString(),n.onload=n.onreadystatechange=function(){n.readyState&&!/loaded|complete/.test(n.readyState)||(n.onload=n.onreadystatechange=null,n=void 0,"EXC_MR_READY"in t&&t.EXC_MR_READY())},e.head.appendChild(n)})(document,window);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
|
6
|
+
<meta name="theme-color" content="#1473e6">
|
|
7
|
+
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
8
|
+
<link rel="icon" type="image/svg+xml" href="./favicon.svg">
|
|
9
|
+
<title>Sync Management</title>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
13
|
+
<div id="root"></div>
|
|
14
|
+
<script src="./src/index.js" async="true" type="module"></script>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
// configuration-management: auto-generated bootstrap (updated on npm install)
|
|
8
|
+
|
|
9
|
+
import 'core-js/stable'
|
|
10
|
+
import 'regenerator-runtime/runtime'
|
|
11
|
+
|
|
12
|
+
import React from 'react'
|
|
13
|
+
import { createRoot } from 'react-dom/client'
|
|
14
|
+
|
|
15
|
+
import Runtime, { init } from '@adobe/exc-app'
|
|
16
|
+
import { ConfigurationManagementApp as App, configureWeb } from 'configuration-management/web'
|
|
17
|
+
import actions from './config.json'
|
|
18
|
+
import 'configuration-management/web/styles.css'
|
|
19
|
+
|
|
20
|
+
configureWeb({ actionUrls: actions })
|
|
21
|
+
|
|
22
|
+
window.React = React
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
require('./exc-runtime')
|
|
26
|
+
init(bootstrapInExcShell)
|
|
27
|
+
} catch (e) {
|
|
28
|
+
console.log('application not running in Adobe Experience Cloud Shell')
|
|
29
|
+
bootstrapRaw()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function renderApp (runtime, ims) {
|
|
33
|
+
createRoot(document.getElementById('root')).render(
|
|
34
|
+
React.createElement(App, { runtime, ims })
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function bootstrapRaw () {
|
|
39
|
+
renderApp({ on: () => {} }, {})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function bootstrapInExcShell () {
|
|
43
|
+
const runtime = Runtime()
|
|
44
|
+
runtime.favicon = './favicon.svg'
|
|
45
|
+
|
|
46
|
+
runtime.on('ready', ({ imsOrg, imsToken, imsProfile }) => {
|
|
47
|
+
runtime.done()
|
|
48
|
+
renderApp(runtime, {
|
|
49
|
+
profile: imsProfile,
|
|
50
|
+
org: imsOrg,
|
|
51
|
+
token: imsToken
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
runtime.solution = {
|
|
56
|
+
icon: 'AdobeExperienceCloud',
|
|
57
|
+
title: 'Sync Management',
|
|
58
|
+
shortTitle: 'Sync'
|
|
59
|
+
}
|
|
60
|
+
runtime.title = 'Sync Management'
|
|
61
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
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
|
+
}
|