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
|
@@ -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
|
+
}
|