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,327 @@
|
|
|
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 { Core } = require('@adobe/aio-sdk')
|
|
9
|
+
const { errorResponse, checkMissingRequestInputs } = require('../../utils')
|
|
10
|
+
const { getClient } = require('configuration-management/abdb')
|
|
11
|
+
|
|
12
|
+
const COLLECTION = 'system_config_schema'
|
|
13
|
+
const DOC_ID = 'v1'
|
|
14
|
+
const FIELD_TYPES = new Set(['text', 'textarea', 'password', 'number', 'select', 'boolean'])
|
|
15
|
+
const SCOPES = ['default', 'websites', 'stores']
|
|
16
|
+
const ID_RE = /^[a-zA-Z][a-zA-Z0-9_]*$/
|
|
17
|
+
|
|
18
|
+
function emptySchema () {
|
|
19
|
+
return { sections: [] }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isString (v) {
|
|
23
|
+
return typeof v === 'string'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function validateField (field, ctx) {
|
|
27
|
+
if (!field || typeof field !== 'object') throw new Error(`${ctx}: field must be an object`)
|
|
28
|
+
if (!isString(field.id) || !ID_RE.test(field.id)) {
|
|
29
|
+
throw new Error(`${ctx}: invalid field id "${field.id}"`)
|
|
30
|
+
}
|
|
31
|
+
if (!isString(field.label) || !field.label.trim()) {
|
|
32
|
+
throw new Error(`${ctx}.${field.id}: label is required`)
|
|
33
|
+
}
|
|
34
|
+
if (!FIELD_TYPES.has(field.type)) {
|
|
35
|
+
throw new Error(`${ctx}.${field.id}: unknown field type "${field.type}"`)
|
|
36
|
+
}
|
|
37
|
+
if (!Array.isArray(field.showIn) || field.showIn.length === 0) {
|
|
38
|
+
throw new Error(`${ctx}.${field.id}: showIn must be a non-empty array`)
|
|
39
|
+
}
|
|
40
|
+
for (const s of field.showIn) {
|
|
41
|
+
if (!SCOPES.includes(s)) {
|
|
42
|
+
throw new Error(`${ctx}.${field.id}: invalid scope "${s}" in showIn`)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (field.type === 'select') {
|
|
46
|
+
if (!Array.isArray(field.options) || field.options.length === 0) {
|
|
47
|
+
throw new Error(`${ctx}.${field.id}: select field requires options[]`)
|
|
48
|
+
}
|
|
49
|
+
for (const opt of field.options) {
|
|
50
|
+
if (!opt || !isString(opt.value) || !isString(opt.label)) {
|
|
51
|
+
throw new Error(`${ctx}.${field.id}: each option needs string value & label`)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function validateSchema (schema) {
|
|
58
|
+
if (!schema || typeof schema !== 'object') throw new Error('schema must be an object')
|
|
59
|
+
if (!Array.isArray(schema.sections)) throw new Error('schema.sections must be an array')
|
|
60
|
+
const seenSection = new Set()
|
|
61
|
+
for (const section of schema.sections) {
|
|
62
|
+
if (!isString(section.id) || !ID_RE.test(section.id)) {
|
|
63
|
+
throw new Error(`section id "${section.id}" is invalid`)
|
|
64
|
+
}
|
|
65
|
+
if (seenSection.has(section.id)) throw new Error(`duplicate section id "${section.id}"`)
|
|
66
|
+
seenSection.add(section.id)
|
|
67
|
+
if (!isString(section.label) || !section.label.trim()) {
|
|
68
|
+
throw new Error(`section "${section.id}": label required`)
|
|
69
|
+
}
|
|
70
|
+
if (!Array.isArray(section.groups)) throw new Error(`section "${section.id}": groups must be array`)
|
|
71
|
+
const seenGroup = new Set()
|
|
72
|
+
for (const group of section.groups) {
|
|
73
|
+
if (!isString(group.id) || !ID_RE.test(group.id)) {
|
|
74
|
+
throw new Error(`section ${section.id}: group id "${group.id}" is invalid`)
|
|
75
|
+
}
|
|
76
|
+
if (seenGroup.has(group.id)) {
|
|
77
|
+
throw new Error(`section ${section.id}: duplicate group id "${group.id}"`)
|
|
78
|
+
}
|
|
79
|
+
seenGroup.add(group.id)
|
|
80
|
+
if (!isString(group.label) || !group.label.trim()) {
|
|
81
|
+
throw new Error(`${section.id}.${group.id}: label required`)
|
|
82
|
+
}
|
|
83
|
+
if (!Array.isArray(group.fields)) {
|
|
84
|
+
throw new Error(`${section.id}.${group.id}: fields must be array`)
|
|
85
|
+
}
|
|
86
|
+
const seenField = new Set()
|
|
87
|
+
for (const field of group.fields) {
|
|
88
|
+
validateField(field, `${section.id}.${group.id}`)
|
|
89
|
+
if (seenField.has(field.id)) {
|
|
90
|
+
throw new Error(`${section.id}.${group.id}: duplicate field id "${field.id}"`)
|
|
91
|
+
}
|
|
92
|
+
seenField.add(field.id)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Ensure the system_config_schema collection exists. ABDB will throw if
|
|
100
|
+
* createCollection is called for an existing collection; the helper swallows
|
|
101
|
+
* the duplicate error like ensureImportCollectionsExist does.
|
|
102
|
+
*/
|
|
103
|
+
async function ensureCollection (client) {
|
|
104
|
+
try {
|
|
105
|
+
await client.createCollection(COLLECTION)
|
|
106
|
+
} catch (err) {
|
|
107
|
+
const msg = (err && err.message) ? String(err.message) : String(err)
|
|
108
|
+
if (!/exist|already|duplicate/i.test(msg)) throw err
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* ABDB's `findOne` throws "Document not found" on a miss in this driver
|
|
114
|
+
* version. `find().limit(1).toArray()` always returns an array, so use that
|
|
115
|
+
* to mean "fetch one or null". Belt-and-braces try/catch in case `find`
|
|
116
|
+
* itself starts throwing in a future version.
|
|
117
|
+
*/
|
|
118
|
+
async function tryFindOne (collection, query) {
|
|
119
|
+
try {
|
|
120
|
+
const arr = await collection.find(query).limit(1).toArray()
|
|
121
|
+
return arr && arr.length ? arr[0] : null
|
|
122
|
+
} catch (err) {
|
|
123
|
+
const msg = (err && err.message) ? String(err.message) : String(err)
|
|
124
|
+
if (/not found/i.test(msg)) return null
|
|
125
|
+
throw err
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Flatten a schema to the set of fully-qualified field paths it defines.
|
|
131
|
+
*/
|
|
132
|
+
function pathsInSchema (schema) {
|
|
133
|
+
const out = new Set()
|
|
134
|
+
if (!schema || !Array.isArray(schema.sections)) return out
|
|
135
|
+
for (const section of schema.sections) {
|
|
136
|
+
if (!section || !Array.isArray(section.groups)) continue
|
|
137
|
+
for (const group of section.groups) {
|
|
138
|
+
if (!group || !Array.isArray(group.fields)) continue
|
|
139
|
+
for (const field of group.fields) {
|
|
140
|
+
if (field && field.id) {
|
|
141
|
+
out.add(`${section.id}/${group.id}/${field.id}`)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return out
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Delete every system_config_data document whose `path` is in the given set,
|
|
151
|
+
* across all scopes. Uses deleteMany when available, falls back to per-doc
|
|
152
|
+
* deletes if the driver doesn't support $in or deleteMany.
|
|
153
|
+
*/
|
|
154
|
+
async function cascadeDeleteData (client, removedPaths, logger) {
|
|
155
|
+
if (!removedPaths || removedPaths.size === 0) {
|
|
156
|
+
return { deletedCount: 0, deletedPaths: [] }
|
|
157
|
+
}
|
|
158
|
+
const dataCollection = await client.collection('system_config_data')
|
|
159
|
+
const paths = [...removedPaths]
|
|
160
|
+
let deletedCount = 0
|
|
161
|
+
try {
|
|
162
|
+
const res = await dataCollection.deleteMany({ path: { $in: paths } })
|
|
163
|
+
deletedCount = (res && (res.deletedCount ?? res.deleted ?? 0)) || 0
|
|
164
|
+
} catch (e) {
|
|
165
|
+
logger.warn(`deleteMany unsupported (${e.message}); falling back to find+deleteOne`)
|
|
166
|
+
for (const path of paths) {
|
|
167
|
+
try {
|
|
168
|
+
const docs = await dataCollection.find({ path }).toArray()
|
|
169
|
+
for (const doc of docs) {
|
|
170
|
+
await dataCollection.deleteOne({ _id: doc._id })
|
|
171
|
+
deletedCount++
|
|
172
|
+
}
|
|
173
|
+
} catch (innerErr) {
|
|
174
|
+
logger.warn(`Failed to delete docs for path ${path}: ${innerErr.message}`)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
logger.info(`Cascade-deleted ${deletedCount} document(s) for paths: ${paths.join(', ')}`)
|
|
179
|
+
return { deletedCount, deletedPaths: paths }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function main (params) {
|
|
183
|
+
const logger = Core.Logger('system-config-schema', { level: params.LOG_LEVEL || 'info' })
|
|
184
|
+
|
|
185
|
+
const errorMessage = checkMissingRequestInputs(params, ['operation'], [])
|
|
186
|
+
if (errorMessage) return errorResponse(400, errorMessage, logger)
|
|
187
|
+
|
|
188
|
+
let dbHandle
|
|
189
|
+
try {
|
|
190
|
+
dbHandle = await getClient(params)
|
|
191
|
+
} catch (e) {
|
|
192
|
+
logger.error(`ABDB connect failed: ${e.message}`)
|
|
193
|
+
return errorResponse(500, `ABDB connect failed: ${e.message}`, logger)
|
|
194
|
+
}
|
|
195
|
+
const { client, close } = dbHandle
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const op = params.operation
|
|
199
|
+
await ensureCollection(client)
|
|
200
|
+
const collection = await client.collection(COLLECTION)
|
|
201
|
+
|
|
202
|
+
if (op === 'get') {
|
|
203
|
+
const doc = await tryFindOne(collection, { _id: DOC_ID })
|
|
204
|
+
const schema = (doc && doc.schema) || emptySchema()
|
|
205
|
+
return {
|
|
206
|
+
statusCode: 200,
|
|
207
|
+
body: { message: 'Schema fetched', schema }
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (op === 'save') {
|
|
212
|
+
if (!params.schema) {
|
|
213
|
+
return errorResponse(400, 'schema is required', logger)
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
validateSchema(params.schema)
|
|
217
|
+
} catch (e) {
|
|
218
|
+
return errorResponse(400, `Invalid schema: ${e.message}`, logger)
|
|
219
|
+
}
|
|
220
|
+
const now = new Date().toISOString()
|
|
221
|
+
const existing = await tryFindOne(collection, { _id: DOC_ID })
|
|
222
|
+
|
|
223
|
+
// Detect removed paths against the previously stored schema so we can
|
|
224
|
+
// cascade-delete their values from system_config_data after the save.
|
|
225
|
+
const prevPaths = pathsInSchema(existing && existing.schema)
|
|
226
|
+
const nextPaths = pathsInSchema(params.schema)
|
|
227
|
+
const removedPaths = new Set(
|
|
228
|
+
[...prevPaths].filter((p) => !nextPaths.has(p))
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
// Caller must explicitly opt into the cascade by passing
|
|
232
|
+
// `confirmCascade: true` (or the equivalent string). Without it we
|
|
233
|
+
// refuse to save when removals are detected so the UI can confirm
|
|
234
|
+
// with the user first.
|
|
235
|
+
const cascadeConfirmed =
|
|
236
|
+
params.confirmCascade === true || params.confirmCascade === 'true'
|
|
237
|
+
if (removedPaths.size > 0 && !cascadeConfirmed) {
|
|
238
|
+
return {
|
|
239
|
+
statusCode: 409,
|
|
240
|
+
body: {
|
|
241
|
+
error: 'Schema removes paths — confirmation required',
|
|
242
|
+
removedPaths: [...removedPaths]
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let writeResult
|
|
248
|
+
if (existing) {
|
|
249
|
+
writeResult = await collection.updateOne(
|
|
250
|
+
{ _id: DOC_ID },
|
|
251
|
+
{ $set: { schema: params.schema, updatedAt: now } }
|
|
252
|
+
)
|
|
253
|
+
} else {
|
|
254
|
+
writeResult = await collection.insertOne({
|
|
255
|
+
_id: DOC_ID,
|
|
256
|
+
schema: params.schema,
|
|
257
|
+
createdAt: now,
|
|
258
|
+
updatedAt: now
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
logger.info(
|
|
262
|
+
`Schema saved (existing=${!!existing}, removed=${removedPaths.size}, result=${JSON.stringify(writeResult)})`
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
// Round-trip verify so silent no-op writes surface immediately.
|
|
266
|
+
const verify = await tryFindOne(collection, { _id: DOC_ID })
|
|
267
|
+
if (!verify || !verify.schema) {
|
|
268
|
+
return errorResponse(
|
|
269
|
+
500,
|
|
270
|
+
'Schema write completed but document is missing — check ABDB region/permissions',
|
|
271
|
+
logger
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Cascade delete only AFTER the schema upsert verifies — we don't
|
|
276
|
+
// want to lose data if the schema write fails.
|
|
277
|
+
let cascade = { deletedCount: 0, deletedPaths: [] }
|
|
278
|
+
if (removedPaths.size > 0) {
|
|
279
|
+
cascade = await cascadeDeleteData(client, removedPaths, logger)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
statusCode: 200,
|
|
284
|
+
body: {
|
|
285
|
+
message: 'Schema saved',
|
|
286
|
+
schema: verify.schema,
|
|
287
|
+
removedPaths: [...removedPaths],
|
|
288
|
+
deletedCount: cascade.deletedCount
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (op === 'reset') {
|
|
294
|
+
// Reset wipes the schema doc; cascade-delete every stored value so the
|
|
295
|
+
// store doesn't keep orphan rows pointing at nothing.
|
|
296
|
+
const existing = await tryFindOne(collection, { _id: DOC_ID })
|
|
297
|
+
const prevPaths = pathsInSchema(existing && existing.schema)
|
|
298
|
+
await collection.deleteOne({ _id: DOC_ID })
|
|
299
|
+
let cascade = { deletedCount: 0, deletedPaths: [] }
|
|
300
|
+
if (prevPaths.size > 0) {
|
|
301
|
+
cascade = await cascadeDeleteData(client, prevPaths, logger)
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
statusCode: 200,
|
|
305
|
+
body: {
|
|
306
|
+
message: 'Schema reset',
|
|
307
|
+
schema: emptySchema(),
|
|
308
|
+
removedPaths: [...prevPaths],
|
|
309
|
+
deletedCount: cascade.deletedCount
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return errorResponse(400, `Unknown operation "${op}". Expected get|save|reset`, logger)
|
|
315
|
+
} catch (error) {
|
|
316
|
+
logger.error(error)
|
|
317
|
+
return errorResponse(500, error.message || 'Schema action failed', logger)
|
|
318
|
+
} finally {
|
|
319
|
+
try {
|
|
320
|
+
await close()
|
|
321
|
+
} catch (_) {
|
|
322
|
+
// ignore
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
exports.main = main
|
package/actions/utils.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
require('dotenv').config()
|
|
9
|
+
const { Core } = require('@adobe/aio-sdk')
|
|
10
|
+
|
|
11
|
+
function getMissingKeys (obj, required) {
|
|
12
|
+
return required.filter((r) => {
|
|
13
|
+
const splits = r.split('.')
|
|
14
|
+
const last = splits[splits.length - 1]
|
|
15
|
+
const traverse = splits.slice(0, -1).reduce((tObj, split) => (tObj[split] || {}), obj)
|
|
16
|
+
return traverse[last] === undefined || traverse[last] === ''
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validate that required params (and optionally headers) are present on the
|
|
22
|
+
* OpenWhisk action input. Returns null when complete, or an error string the
|
|
23
|
+
* caller can hand straight to `errorResponse`.
|
|
24
|
+
*/
|
|
25
|
+
function checkMissingRequestInputs (params, requiredParams = [], requiredHeaders = []) {
|
|
26
|
+
let errorMessage = null
|
|
27
|
+
const safeParams = params ?? {}
|
|
28
|
+
requiredHeaders = requiredHeaders.map((h) => h.toLowerCase())
|
|
29
|
+
|
|
30
|
+
const missingHeaders = getMissingKeys(safeParams.__ow_headers || {}, requiredHeaders)
|
|
31
|
+
if (missingHeaders.length > 0) {
|
|
32
|
+
errorMessage = `missing header(s) '${missingHeaders}'`
|
|
33
|
+
}
|
|
34
|
+
const missingParams = getMissingKeys(safeParams, requiredParams)
|
|
35
|
+
if (missingParams.length > 0) {
|
|
36
|
+
errorMessage = errorMessage ? `${errorMessage} and ` : ''
|
|
37
|
+
errorMessage += `missing parameter(s) '${missingParams}'`
|
|
38
|
+
}
|
|
39
|
+
return errorMessage
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Standard OpenWhisk-friendly error envelope.
|
|
44
|
+
*/
|
|
45
|
+
function errorResponse (statusCode, message, logger) {
|
|
46
|
+
if (logger && typeof logger.info === 'function') {
|
|
47
|
+
logger.info(`${statusCode}: ${message}`)
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
error: {
|
|
51
|
+
statusCode,
|
|
52
|
+
body: { error: message }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Lightweight logger wrapper used by action handlers that need to emit a
|
|
59
|
+
* log line under a different module name without holding a Core.Logger
|
|
60
|
+
* reference.
|
|
61
|
+
*/
|
|
62
|
+
function logDetails (logName, message, type = 'info') {
|
|
63
|
+
const logger = Core.Logger(logName, { level: type || 'info' })
|
|
64
|
+
if (type === 'debug') logger.debug(message)
|
|
65
|
+
else if (type === 'error') logger.error(message)
|
|
66
|
+
else logger.info(message)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = {
|
|
70
|
+
errorResponse,
|
|
71
|
+
checkMissingRequestInputs,
|
|
72
|
+
logDetails
|
|
73
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "configuration-management",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "Adobe Inc.",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"adobe-io",
|
|
9
|
+
"aio",
|
|
10
|
+
"app-builder",
|
|
11
|
+
"adobe-commerce",
|
|
12
|
+
"magento",
|
|
13
|
+
"configuration-management",
|
|
14
|
+
"abdb",
|
|
15
|
+
"react-spectrum"
|
|
16
|
+
],
|
|
17
|
+
"main": "./src/index.js",
|
|
18
|
+
"bin": {
|
|
19
|
+
"configuration-management-setup": "./scripts/setup-app-config.js"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"postinstall": "node ./scripts/setup-app-config.js"
|
|
23
|
+
},
|
|
24
|
+
"exports": {
|
|
25
|
+
".": "./src/index.js",
|
|
26
|
+
"./abdb": "./src/abdb-helper.js",
|
|
27
|
+
"./config": "./src/abdb-config.js",
|
|
28
|
+
"./crypto": "./src/system-config-crypto.js",
|
|
29
|
+
"./shared": "./src/system-config-shared.js",
|
|
30
|
+
"./oauth1a": "./src/oauth1a.js",
|
|
31
|
+
"./web": "./web/src/index.js",
|
|
32
|
+
"./web/styles.css": "./web/src/styles/index.css",
|
|
33
|
+
"./actions/utils": "./actions/utils.js",
|
|
34
|
+
"./actions/ext.config.yaml": "./actions/configurations/ext.config.yaml"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"src",
|
|
38
|
+
"web",
|
|
39
|
+
"actions",
|
|
40
|
+
"scripts",
|
|
41
|
+
"README.md"
|
|
42
|
+
],
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@adobe/aio-lib-core-auth": "^1.1.0",
|
|
48
|
+
"@adobe/aio-lib-db": "^1.0.1",
|
|
49
|
+
"@adobe/aio-lib-ims": "^8.0.0",
|
|
50
|
+
"@adobe/aio-sdk": "^6.0.0",
|
|
51
|
+
"@adobe/exc-app": "^1.1.3",
|
|
52
|
+
"@adobe/react-spectrum": "^3.30.0",
|
|
53
|
+
"@adobe/uix-guest": "^0.8.3",
|
|
54
|
+
"@spectrum-icons/workflow": "^4.2.4",
|
|
55
|
+
"react": "^18.2.0",
|
|
56
|
+
"react-dom": "^18.2.0",
|
|
57
|
+
"react-error-boundary": "^3.1.4",
|
|
58
|
+
"react-router-dom": "^6.8.1",
|
|
59
|
+
"dotenv": "^16.4.5"
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"got": "^11.8.5",
|
|
63
|
+
"oauth-1.0a": "^2.2.6"
|
|
64
|
+
},
|
|
65
|
+
"repository": {
|
|
66
|
+
"type": "git",
|
|
67
|
+
"url": "https://github.com/adobe/config-management-poc.git",
|
|
68
|
+
"directory": "packages/configuration-management"
|
|
69
|
+
},
|
|
70
|
+
"bugs": {
|
|
71
|
+
"url": "https://github.com/adobe/config-management-poc/issues"
|
|
72
|
+
},
|
|
73
|
+
"homepage": "https://github.com/adobe/config-management-poc/tree/main/packages/configuration-management#readme"
|
|
74
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
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
|
+
|
|
14
|
+
function findProjectRoot (startDir) {
|
|
15
|
+
let dir = startDir
|
|
16
|
+
while (dir && dir !== path.dirname(dir)) {
|
|
17
|
+
if (fs.existsSync(path.join(dir, 'app.config.yaml'))) {
|
|
18
|
+
return dir
|
|
19
|
+
}
|
|
20
|
+
dir = path.dirname(dir)
|
|
21
|
+
}
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolveProjectRoot () {
|
|
26
|
+
const initCwd = process.env.INIT_CWD
|
|
27
|
+
if (initCwd) {
|
|
28
|
+
const fromInit = findProjectRoot(initCwd)
|
|
29
|
+
if (fromInit) return fromInit
|
|
30
|
+
}
|
|
31
|
+
return findProjectRoot(process.cwd())
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function alreadyLinked (content) {
|
|
35
|
+
return content.includes('configuration-management/actions/configurations/ext.config.yaml')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildExtensionBlock () {
|
|
39
|
+
return [
|
|
40
|
+
MARKER,
|
|
41
|
+
'extensions:',
|
|
42
|
+
` ${EXTENSION_POINT}:`,
|
|
43
|
+
` $include: ${INCLUDE_REL}`
|
|
44
|
+
].join('\n')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function patchAppConfig (content) {
|
|
48
|
+
if (alreadyLinked(content)) {
|
|
49
|
+
return { content, changed: false, reason: 'already-linked' }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (/^extensions:\s*$/m.test(content) || /^extensions:\n/m.test(content)) {
|
|
53
|
+
const injection = [
|
|
54
|
+
` ${EXTENSION_POINT}:`,
|
|
55
|
+
` $include: ${INCLUDE_REL}`
|
|
56
|
+
].join('\n')
|
|
57
|
+
const next = content.replace(
|
|
58
|
+
/^extensions:\s*\n/m,
|
|
59
|
+
`extensions:\n${injection}\n`
|
|
60
|
+
)
|
|
61
|
+
if (next !== content) {
|
|
62
|
+
return { content: next, changed: true, reason: 'merged-into-extensions' }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const trimmed = content.replace(/\s+$/, '')
|
|
67
|
+
const separator = trimmed.length > 0 ? '\n\n' : ''
|
|
68
|
+
return {
|
|
69
|
+
content: `${trimmed}${separator}${buildExtensionBlock()}\n`,
|
|
70
|
+
changed: true,
|
|
71
|
+
reason: 'appended'
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function main () {
|
|
76
|
+
if (process.env.CONFIGURATION_MANAGEMENT_SKIP_SETUP === '1') {
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const projectRoot = resolveProjectRoot()
|
|
81
|
+
if (!projectRoot) {
|
|
82
|
+
console.log(
|
|
83
|
+
'[configuration-management] No app.config.yaml found — skip auto-link. ' +
|
|
84
|
+
'Run `npx configuration-management-setup` after creating your App Builder app.'
|
|
85
|
+
)
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const appConfigPath = path.join(projectRoot, 'app.config.yaml')
|
|
90
|
+
const original = fs.readFileSync(appConfigPath, 'utf8')
|
|
91
|
+
const { content, changed, reason } = patchAppConfig(original)
|
|
92
|
+
|
|
93
|
+
if (!changed) {
|
|
94
|
+
console.log(`[configuration-management] app.config.yaml already links actions (${reason}).`)
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
fs.writeFileSync(appConfigPath, content, 'utf8')
|
|
99
|
+
console.log(
|
|
100
|
+
`[configuration-management] Updated app.config.yaml (${reason}):\n` +
|
|
101
|
+
` $include: ${INCLUDE_REL}`
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (require.main === module) {
|
|
106
|
+
try {
|
|
107
|
+
main()
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error('[configuration-management] setup failed:', err.message)
|
|
110
|
+
process.exitCode = 1
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = { patchAppConfig, INCLUDE_REL, EXTENSION_POINT }
|