@adobe/helix-config 2.13.1 → 2.15.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/CHANGELOG.md +14 -0
- package/package.json +1 -1
- package/src/config-merge.js +143 -0
- package/src/config-view.js +40 -10
- package/src/schemas/site.schema.json +6 -0
- package/src/storage/config-store.js +10 -3
- package/types/site-config.d.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [2.15.0](https://github.com/adobe/helix-config/compare/v2.14.0...v2.15.0) (2024-05-07)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* support profile for site ([#67](https://github.com/adobe/helix-config/issues/67)) ([55c09c9](https://github.com/adobe/helix-config/commit/55c09c95919ec06f047393335bdbcbcc2d187eb2))
|
|
7
|
+
|
|
8
|
+
# [2.14.0](https://github.com/adobe/helix-config/compare/v2.13.1...v2.14.0) (2024-05-07)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* simplify token cretion on empty org ([#74](https://github.com/adobe/helix-config/issues/74)) ([327747a](https://github.com/adobe/helix-config/commit/327747a4a34e4b3ae37f732e1631a40184190b8b))
|
|
14
|
+
|
|
1
15
|
## [2.13.1](https://github.com/adobe/helix-config/compare/v2.13.0...v2.13.1) (2024-05-07)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2023 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
|
+
/* eslint-disable no-continue, no-param-reassign */
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef RootProperty
|
|
16
|
+
* @property {boolean} atomic whether the property should be overwritten atomically
|
|
17
|
+
* @property {boolean} isModified whether the property need special merging
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const ROOT_PROPERTIES = {
|
|
21
|
+
name: { atomic: true },
|
|
22
|
+
title: { atomic: true },
|
|
23
|
+
description: { atomic: true },
|
|
24
|
+
content: { atomic: true },
|
|
25
|
+
code: { atomic: true },
|
|
26
|
+
folders: {},
|
|
27
|
+
headers: { isModifier: true },
|
|
28
|
+
cdn: {},
|
|
29
|
+
access: {},
|
|
30
|
+
sidekick: {},
|
|
31
|
+
metadata: { isModifier: true },
|
|
32
|
+
robots: {},
|
|
33
|
+
extends: {},
|
|
34
|
+
tokens: {},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const FORCED_TYPES = {
|
|
38
|
+
'.cdn.prod.route': 'array',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Merges the `src` object into the `dst` object.
|
|
43
|
+
* - if the dst property is a scalar, it will be overwritten by the dst property
|
|
44
|
+
* - if the src property is an empty string, it will delete the dst property
|
|
45
|
+
* - if the dst property is an array, it will be expanded by the src property uniquely
|
|
46
|
+
* - some well known paths are forced to be arrays
|
|
47
|
+
* @param {object} dst
|
|
48
|
+
* @param {object} src
|
|
49
|
+
* @param {boolean} isModifier specifies that the objects are modifiers sheets and need special
|
|
50
|
+
* merging
|
|
51
|
+
*/
|
|
52
|
+
function merge(dst, src, path, isModifier) {
|
|
53
|
+
for (const [key, value] of Object.entries(src)) {
|
|
54
|
+
const itemPath = `${path}.${key}`;
|
|
55
|
+
if (value === '') {
|
|
56
|
+
// remove key if value is empty string (keep false, 0)
|
|
57
|
+
delete dst[key];
|
|
58
|
+
} else if (typeof value !== 'object') {
|
|
59
|
+
// handle non object
|
|
60
|
+
if (key in dst) {
|
|
61
|
+
let dstValue = dst[key];
|
|
62
|
+
if (!Array.isArray(dstValue) && FORCED_TYPES[itemPath] === 'array') {
|
|
63
|
+
dstValue = [dst[key]];
|
|
64
|
+
dst[key] = dstValue;
|
|
65
|
+
}
|
|
66
|
+
if (!Array.isArray(dstValue)) {
|
|
67
|
+
dst[key] = value;
|
|
68
|
+
} else if (!dstValue.includes(value)) {
|
|
69
|
+
// only add to array if not exist yet
|
|
70
|
+
dstValue.push(value);
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
dst[key] = value;
|
|
74
|
+
}
|
|
75
|
+
} else if (Array.isArray(value)) {
|
|
76
|
+
// handle array
|
|
77
|
+
if (key in dst) {
|
|
78
|
+
if (!Array.isArray(dst[key])) {
|
|
79
|
+
dst[key] = [dst[key]];
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
dst[key] = [];
|
|
83
|
+
}
|
|
84
|
+
const dstArray = dst[key];
|
|
85
|
+
for (const item of value) {
|
|
86
|
+
if (typeof item === 'object') {
|
|
87
|
+
// todo: handle arrays in arrays eventually
|
|
88
|
+
// special case for modifier sheets
|
|
89
|
+
if (isModifier && item.key) {
|
|
90
|
+
const idx = dstArray.findIndex((i) => i.key === item.key);
|
|
91
|
+
if (idx >= 0) {
|
|
92
|
+
dstArray.splice(idx, 1);
|
|
93
|
+
}
|
|
94
|
+
if (!item.value) {
|
|
95
|
+
// don't set empty modifier
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const dstObj = Object.create(null);
|
|
100
|
+
dstArray.push(dstObj);
|
|
101
|
+
merge(dstObj, item, itemPath, isModifier);
|
|
102
|
+
} else if (!dstArray.includes(item)) {
|
|
103
|
+
dstArray.push(item);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
// handle plain object
|
|
108
|
+
// eslint-disable-next-line no-use-before-define
|
|
109
|
+
mergeObject(dst, key, value, `${path}.${key}`, isModifier);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function mergeObject(dst, key, value, path, isModifier) {
|
|
115
|
+
const dstObj = dst[key] ?? Object.create(null);
|
|
116
|
+
if (!dst[key]) {
|
|
117
|
+
dst[key] = dstObj;
|
|
118
|
+
}
|
|
119
|
+
merge(dstObj, value, path, isModifier);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function mergeConfig(dst, src) {
|
|
123
|
+
Object.entries(ROOT_PROPERTIES).forEach(([key, { atomic, isModifier }]) => {
|
|
124
|
+
if (src[key]) {
|
|
125
|
+
if (atomic) {
|
|
126
|
+
dst[key] = src[key];
|
|
127
|
+
} else {
|
|
128
|
+
mergeObject(dst, key, src[key], `.${key}`, isModifier);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function getMergedConfig(site, profile) {
|
|
135
|
+
if (!profile) {
|
|
136
|
+
return site;
|
|
137
|
+
}
|
|
138
|
+
const ret = Object.create(null);
|
|
139
|
+
ret.version = site.version;
|
|
140
|
+
mergeConfig(ret, profile);
|
|
141
|
+
mergeConfig(ret, site);
|
|
142
|
+
return ret;
|
|
143
|
+
}
|
package/src/config-view.js
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
SCOPE_RAW,
|
|
20
20
|
} from './ConfigContext.js';
|
|
21
21
|
import { resolveLegacyConfig, fetchRobotsTxt, toArray } from './config-legacy.js';
|
|
22
|
+
import { getMergedConfig } from './config-merge.js';
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* @typedef Config
|
|
@@ -81,7 +82,7 @@ export function getAccessConfig(config, partition) {
|
|
|
81
82
|
// if an allow is defined but no apiKeyId, create a fake one so that auth is still
|
|
82
83
|
// enforced. later we can remove the allow and the apiKeyId in favor of the tokenHash
|
|
83
84
|
if (allow.length && !cfg.apiKeyId.length) {
|
|
84
|
-
cfg.apiKeyId.push('
|
|
85
|
+
cfg.apiKeyId.push('dummy');
|
|
85
86
|
}
|
|
86
87
|
// if an apiKeyId is defined but no tokenHash, create a fake one so that auth is still
|
|
87
88
|
// enforced.
|
|
@@ -167,7 +168,7 @@ function retainProperty(obj, prop) {
|
|
|
167
168
|
*/
|
|
168
169
|
async function resolveConfig(ctx, rso, scope) {
|
|
169
170
|
// try to load site config from config-bus
|
|
170
|
-
|
|
171
|
+
const key = `orgs/${rso.org}/sites/${rso.site}.json`;
|
|
171
172
|
let res = await ctx.loader.getObject(HELIX_CONFIG_BUS, key);
|
|
172
173
|
if (!res.body) {
|
|
173
174
|
if (scope !== SCOPE_ADMIN) {
|
|
@@ -176,7 +177,17 @@ async function resolveConfig(ctx, rso, scope) {
|
|
|
176
177
|
return null;
|
|
177
178
|
}
|
|
178
179
|
}
|
|
179
|
-
const
|
|
180
|
+
const site = res.json();
|
|
181
|
+
let profile;
|
|
182
|
+
|
|
183
|
+
if (site.extends?.profile) {
|
|
184
|
+
const profileKey = `orgs/${rso.org}/profiles/${site.extends.profile}.json`;
|
|
185
|
+
res = await ctx.loader.getObject(HELIX_CONFIG_BUS, profileKey);
|
|
186
|
+
if (res.body) {
|
|
187
|
+
profile = res.json();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const config = getMergedConfig(site, profile);
|
|
180
191
|
if (scope === SCOPE_PIPELINE) {
|
|
181
192
|
config.metadata = {
|
|
182
193
|
preview: await loadMetadata(ctx, config, 'preview'),
|
|
@@ -194,8 +205,8 @@ async function resolveConfig(ctx, rso, scope) {
|
|
|
194
205
|
}
|
|
195
206
|
|
|
196
207
|
// check for org config
|
|
197
|
-
|
|
198
|
-
res = await ctx.loader.getObject(HELIX_CONFIG_BUS,
|
|
208
|
+
const orgKey = `orgs/${rso.org}/config.json`;
|
|
209
|
+
res = await ctx.loader.getObject(HELIX_CONFIG_BUS, orgKey);
|
|
199
210
|
if (res.body) {
|
|
200
211
|
const orgConfig = res.json();
|
|
201
212
|
if (orgConfig.tokens) {
|
|
@@ -205,11 +216,22 @@ async function resolveConfig(ctx, rso, scope) {
|
|
|
205
216
|
return config;
|
|
206
217
|
}
|
|
207
218
|
|
|
208
|
-
async function getSurrogateKey(opts) {
|
|
219
|
+
async function getSurrogateKey(opts, profile) {
|
|
209
220
|
const { site, org } = opts;
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
221
|
+
const keys = [
|
|
222
|
+
await computeSurrogateKey(`${org}_config.json`),
|
|
223
|
+
await computeSurrogateKey(`${site}--${org}_config.json`),
|
|
224
|
+
];
|
|
225
|
+
if (profile) {
|
|
226
|
+
keys.push(await computeSurrogateKey(`p:${profile}--${org}_config.json`));
|
|
227
|
+
}
|
|
228
|
+
return keys.join(' ');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function loadOrgConfig(ctx, org) {
|
|
232
|
+
const key = `orgs/${org}/config.json`;
|
|
233
|
+
const res = await ctx.loader.getObject(HELIX_CONFIG_BUS, key);
|
|
234
|
+
return res.body ? res.json() : null;
|
|
213
235
|
}
|
|
214
236
|
|
|
215
237
|
export async function getConfigResponse(ctx, opts) {
|
|
@@ -219,7 +241,7 @@ export async function getConfigResponse(ctx, opts) {
|
|
|
219
241
|
const rso = { ref, site, org };
|
|
220
242
|
const config = await resolveConfig(ctx, rso, scope);
|
|
221
243
|
const surrogateHeaders = {
|
|
222
|
-
'x-surrogate-key': await getSurrogateKey(opts),
|
|
244
|
+
'x-surrogate-key': await getSurrogateKey(opts, config?.extends?.profile),
|
|
223
245
|
};
|
|
224
246
|
if (!config) {
|
|
225
247
|
return new PipelineResponse('', {
|
|
@@ -230,6 +252,14 @@ export async function getConfigResponse(ctx, opts) {
|
|
|
230
252
|
},
|
|
231
253
|
});
|
|
232
254
|
}
|
|
255
|
+
if (config.extends && scope !== SCOPE_RAW) {
|
|
256
|
+
delete config.extends;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const orgConfig = await loadOrgConfig(ctx, org);
|
|
260
|
+
if (orgConfig?.tokens) {
|
|
261
|
+
config.tokens = orgConfig.tokens;
|
|
262
|
+
}
|
|
233
263
|
|
|
234
264
|
if (config.access) {
|
|
235
265
|
// normalize access config
|
|
@@ -47,7 +47,10 @@ const FRAGMENTS_COMMON = {
|
|
|
47
47
|
};
|
|
48
48
|
|
|
49
49
|
const FRAGMENTS = {
|
|
50
|
-
sites:
|
|
50
|
+
sites: {
|
|
51
|
+
...FRAGMENTS_COMMON,
|
|
52
|
+
extends: 'object',
|
|
53
|
+
},
|
|
51
54
|
profiles: FRAGMENTS_COMMON,
|
|
52
55
|
org: {
|
|
53
56
|
tokens: {
|
|
@@ -191,7 +194,7 @@ export class ConfigStore {
|
|
|
191
194
|
async update(ctx, data, relPath = '') {
|
|
192
195
|
const storage = HelixStorage.fromContext(ctx).configBus();
|
|
193
196
|
const buf = await storage.get(this.key);
|
|
194
|
-
|
|
197
|
+
let old = buf ? JSON.parse(buf) : null;
|
|
195
198
|
const frag = getFragmentInfo(this.type, relPath);
|
|
196
199
|
let config = data;
|
|
197
200
|
// set config to null if empty object
|
|
@@ -205,7 +208,11 @@ export class ConfigStore {
|
|
|
205
208
|
}
|
|
206
209
|
if (frag) {
|
|
207
210
|
if (!old) {
|
|
208
|
-
|
|
211
|
+
if (this.type === 'org' && frag.type === 'tokens') {
|
|
212
|
+
old = {};
|
|
213
|
+
} else {
|
|
214
|
+
throw new StatusCodeError(404, 'config not found.');
|
|
215
|
+
}
|
|
209
216
|
}
|
|
210
217
|
if (relPath === 'code') {
|
|
211
218
|
updateCodeSource(ctx, data);
|