@adobe/helix-config-storage 2.5.2 → 2.8.1
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 +70 -0
- package/package.json +5 -5
- package/src/config-store.js +95 -10
- package/src/config-versioning.js +188 -0
- package/src/index.js +1 -0
- package/src/schemas/org.schema.json +0 -1
- package/src/schemas/profile.schema.json +0 -1
- package/src/schemas/sidekick.schema.json +4 -0
- package/src/schemas/site.schema.json +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,73 @@
|
|
|
1
|
+
## [2.8.1](https://github.com/adobe/helix-config-storage/compare/v2.8.0...v2.8.1) (2025-08-22)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* back to adobe npm org ([#170](https://github.com/adobe/helix-config-storage/issues/170)) ([092be53](https://github.com/adobe/helix-config-storage/commit/092be5397d19ad2e7151f5def3412ffebd4d5e1f))
|
|
7
|
+
|
|
8
|
+
# [2.8.0](https://github.com/adobe/helix-config-storage/compare/v2.7.6...v2.8.0) (2025-08-21)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* use POST for restore version ([#169](https://github.com/adobe/helix-config-storage/issues/169)) ([0723da3](https://github.com/adobe/helix-config-storage/commit/0723da321920eb1b4ebd86fc7dc27049e302e30c))
|
|
14
|
+
|
|
15
|
+
## [2.7.6](https://github.com/adobe/helix-config-storage/compare/v2.7.5...v2.7.6) (2025-08-19)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* add status code when initial validation fails ([#167](https://github.com/adobe/helix-config-storage/issues/167)) ([64705e9](https://github.com/adobe/helix-config-storage/commit/64705e9cf90b1fbde3ee3b1ff7faf55da1d715dc))
|
|
21
|
+
|
|
22
|
+
## [2.7.5](https://github.com/adobe/helix-config-storage/compare/v2.7.4...v2.7.5) (2025-08-14)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Bug Fixes
|
|
26
|
+
|
|
27
|
+
* **deps:** update dependency @adobe/helix-shared-storage to v1.4.1 ([#166](https://github.com/adobe/helix-config-storage/issues/166)) ([9d3f510](https://github.com/adobe/helix-config-storage/commit/9d3f510a9d8e702582e843b9f4be30ff920375bc))
|
|
28
|
+
|
|
29
|
+
## [2.7.4](https://github.com/adobe/helix-config-storage/compare/v2.7.3...v2.7.4) (2025-08-13)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
### Bug Fixes
|
|
33
|
+
|
|
34
|
+
* proper list sites and profiles ([#165](https://github.com/adobe/helix-config-storage/issues/165)) ([1017a91](https://github.com/adobe/helix-config-storage/commit/1017a91773fd29bcc498f2f20c9b63e06c142dd9))
|
|
35
|
+
|
|
36
|
+
## [2.7.3](https://github.com/adobe/helix-config-storage/compare/v2.7.2...v2.7.3) (2025-08-12)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
### Bug Fixes
|
|
40
|
+
|
|
41
|
+
* **deps:** update adobe fixes ([#164](https://github.com/adobe/helix-config-storage/issues/164)) ([c688e5d](https://github.com/adobe/helix-config-storage/commit/c688e5dc6375f14be529967c4650ab7d60e2e63e))
|
|
42
|
+
|
|
43
|
+
## [2.7.2](https://github.com/adobe/helix-config-storage/compare/v2.7.1...v2.7.2) (2025-08-12)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
### Bug Fixes
|
|
47
|
+
|
|
48
|
+
* update npm package ([77e5607](https://github.com/adobe/helix-config-storage/commit/77e56079a3842ac39cbf71dfcb9eb0531d0086b2))
|
|
49
|
+
|
|
50
|
+
## [2.7.1](https://github.com/adobe/helix-config-storage/compare/v2.7.0...v2.7.1) (2025-08-12)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
### Bug Fixes
|
|
54
|
+
|
|
55
|
+
* **deps:** update adobe fixes ([#163](https://github.com/adobe/helix-config-storage/issues/163)) ([0af5fc4](https://github.com/adobe/helix-config-storage/commit/0af5fc4953a7d106c6f16ac7877dde3dc2d9c090))
|
|
56
|
+
|
|
57
|
+
# [2.7.0](https://github.com/adobe/helix-config-storage/compare/v2.6.0...v2.7.0) (2025-08-11)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
### Features
|
|
61
|
+
|
|
62
|
+
* add versioning apis ([#160](https://github.com/adobe/helix-config-storage/issues/160)) ([1eca459](https://github.com/adobe/helix-config-storage/commit/1eca459d781c3a9f1ed03c539022d55f1146e7c5))
|
|
63
|
+
|
|
64
|
+
# [2.6.0](https://github.com/adobe/helix-config-storage/compare/v2.5.2...v2.6.0) (2025-08-05)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
### Features
|
|
68
|
+
|
|
69
|
+
* include wordSaveDelay in sidekick schema ([0ad3d04](https://github.com/adobe/helix-config-storage/commit/0ad3d048a5b3ee8ee5dbe621b3638ce352d0fc6c))
|
|
70
|
+
|
|
1
71
|
## [2.5.2](https://github.com/adobe/helix-config-storage/compare/v2.5.1...v2.5.2) (2025-07-23)
|
|
2
72
|
|
|
3
73
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/helix-config-storage",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.1",
|
|
4
4
|
"description": "Helix Config Storage",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"reporter-options": "configFile=.mocha-multi.json"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@adobe/eslint-config-helix": "3.0.
|
|
40
|
-
"@eslint/config-helpers": "0.3.
|
|
39
|
+
"@adobe/eslint-config-helix": "3.0.9",
|
|
40
|
+
"@eslint/config-helpers": "0.3.1",
|
|
41
41
|
"@semantic-release/changelog": "6.0.3",
|
|
42
42
|
"@semantic-release/git": "10.0.1",
|
|
43
43
|
"@semantic-release/npm": "12.0.2",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"husky": "9.1.7",
|
|
48
48
|
"json-schema-to-typescript": "15.0.4",
|
|
49
49
|
"junit-report-builder": "5.1.1",
|
|
50
|
-
"lint-staged": "16.1.
|
|
50
|
+
"lint-staged": "16.1.5",
|
|
51
51
|
"mocha": "11.7.1",
|
|
52
52
|
"mocha-multi-reporters": "1.5.1",
|
|
53
53
|
"mocha-suppress-logs": "0.6.0",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"@adobe/helix-shared-config": "^11.1.4",
|
|
65
65
|
"@adobe/helix-shared-git": "^3.0.18",
|
|
66
66
|
"@adobe/helix-shared-process-queue": "3.1.3",
|
|
67
|
-
"@adobe/helix-shared-storage": "^1.
|
|
67
|
+
"@adobe/helix-shared-storage": "^1.4.0",
|
|
68
68
|
"@adobe/helix-shared-string": "^2.1.0",
|
|
69
69
|
"@adobe/helix-shared-utils": "^3.0.2",
|
|
70
70
|
"ajv": "8.17.1",
|
package/src/config-store.js
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
import { validate as validateSchema } from './config-validator.js';
|
|
26
26
|
import { getMergedConfig } from './config-merge.js';
|
|
27
27
|
import { ValidationError } from './ValidationError.js';
|
|
28
|
+
import { ConfigVersioning } from './config-versioning.js';
|
|
28
29
|
|
|
29
30
|
const FRAGMENTS_COMMON = {
|
|
30
31
|
content: 'object',
|
|
@@ -324,6 +325,11 @@ export class ConfigStore {
|
|
|
324
325
|
*/
|
|
325
326
|
#profiles = {};
|
|
326
327
|
|
|
328
|
+
/**
|
|
329
|
+
* @member {ConfigVersioning} versioning the versioning manager
|
|
330
|
+
*/
|
|
331
|
+
#versioning;
|
|
332
|
+
|
|
327
333
|
/**
|
|
328
334
|
* @param {string} org the org id
|
|
329
335
|
* @param {string} type store type (org, sites, profiles, secrets, users)
|
|
@@ -331,27 +337,28 @@ export class ConfigStore {
|
|
|
331
337
|
*/
|
|
332
338
|
constructor(org, type = 'org', name = '') {
|
|
333
339
|
if (!org) {
|
|
334
|
-
throw new
|
|
340
|
+
throw new StatusCodeError(400, 'org required');
|
|
335
341
|
}
|
|
336
342
|
if (org.includes('/') || type.includes('/') || name.includes('/')) {
|
|
337
|
-
throw new
|
|
343
|
+
throw new StatusCodeError(400, 'orgId, type and name must not contain slashes');
|
|
338
344
|
}
|
|
339
345
|
if (org.toLowerCase() !== org) {
|
|
340
|
-
throw new
|
|
346
|
+
throw new StatusCodeError(400, 'orgId must be lowercase');
|
|
341
347
|
}
|
|
342
348
|
if (type === 'sites' && name.toLowerCase() !== name) {
|
|
343
|
-
throw new
|
|
349
|
+
throw new StatusCodeError(400, 'site must be lowercase');
|
|
344
350
|
}
|
|
345
351
|
this.org = org;
|
|
346
352
|
this.type = type;
|
|
347
353
|
this.name = name;
|
|
348
354
|
this.key = type === 'org'
|
|
349
|
-
? `/orgs/${org}
|
|
355
|
+
? `/orgs/${org}/config.json`
|
|
350
356
|
: `/orgs/${org}/${type}/${name}.json`;
|
|
351
357
|
this.now = new Date();
|
|
352
358
|
this.isAdmin = false;
|
|
353
359
|
this.isOps = false;
|
|
354
360
|
this.listDetails = false;
|
|
361
|
+
this.#versioning = new ConfigVersioning(org, type, name);
|
|
355
362
|
}
|
|
356
363
|
|
|
357
364
|
/**
|
|
@@ -399,7 +406,7 @@ export class ConfigStore {
|
|
|
399
406
|
async #list(ctx) {
|
|
400
407
|
const storage = HelixStorage.fromContext(ctx).configBus();
|
|
401
408
|
const key = `orgs/${this.org}/${this.type}/`;
|
|
402
|
-
const list = await storage.list(key);
|
|
409
|
+
const list = await storage.list(key, true);
|
|
403
410
|
let entries = list.map((entry) => {
|
|
404
411
|
const siteKey = entry.key;
|
|
405
412
|
if (siteKey.endsWith('.json')) {
|
|
@@ -523,6 +530,7 @@ export class ConfigStore {
|
|
|
523
530
|
throw new StatusCodeError(409, 'create not supported on substructures.');
|
|
524
531
|
}
|
|
525
532
|
const storage = HelixStorage.fromContext(ctx).configBus();
|
|
533
|
+
|
|
526
534
|
if (await storage.head(this.key)) {
|
|
527
535
|
throw new StatusCodeError(409, 'config already exists.');
|
|
528
536
|
}
|
|
@@ -544,13 +552,14 @@ export class ConfigStore {
|
|
|
544
552
|
}
|
|
545
553
|
data.users = users;
|
|
546
554
|
}
|
|
547
|
-
|
|
548
555
|
if (this.type !== 'org') {
|
|
549
556
|
updateContentSource(ctx, data.content);
|
|
550
557
|
updateCodeSource(ctx, data.code);
|
|
551
558
|
}
|
|
559
|
+
|
|
552
560
|
// we don't allow to define created
|
|
553
561
|
delete data.created;
|
|
562
|
+
|
|
554
563
|
this.#updateTimeStamps(data);
|
|
555
564
|
let oldConfig = {};
|
|
556
565
|
if (this.type === 'sites') {
|
|
@@ -561,6 +570,9 @@ export class ConfigStore {
|
|
|
561
570
|
const config = await this.getAggregatedConfig(ctx, data);
|
|
562
571
|
await this.validate(ctx, config);
|
|
563
572
|
await this.validatePermissions(ctx, oldConfig, config);
|
|
573
|
+
|
|
574
|
+
// Create a new version before saving
|
|
575
|
+
await this.#versioning.createVersion(ctx, data, 'initial');
|
|
564
576
|
await storage.put(this.key, JSON.stringify(data), 'application/json');
|
|
565
577
|
await this.purge(ctx, null, config);
|
|
566
578
|
}
|
|
@@ -569,6 +581,12 @@ export class ConfigStore {
|
|
|
569
581
|
if (this.name === '' && (this.type === 'sites' || this.type === 'profiles')) {
|
|
570
582
|
return this.#list(ctx);
|
|
571
583
|
}
|
|
584
|
+
if (relPath === 'versions') {
|
|
585
|
+
return this.listVersions(ctx);
|
|
586
|
+
}
|
|
587
|
+
if (relPath.startsWith('versions/')) {
|
|
588
|
+
return this.getVersion(ctx, Number.parseInt(relPath.substring('versions/'.length), 10));
|
|
589
|
+
}
|
|
572
590
|
|
|
573
591
|
const storage = HelixStorage.fromContext(ctx).configBus();
|
|
574
592
|
const buf = await storage.get(this.key);
|
|
@@ -584,10 +602,25 @@ export class ConfigStore {
|
|
|
584
602
|
}
|
|
585
603
|
|
|
586
604
|
async update(ctx, data, relPath = '') {
|
|
605
|
+
if (relPath.startsWith('versions/')) {
|
|
606
|
+
return this.#versioning.updateVersion(ctx, Number.parseInt(relPath.substring('versions/'.length), 10), data.name);
|
|
607
|
+
}
|
|
608
|
+
const frag = getFragmentInfo(this.type, relPath);
|
|
609
|
+
const { restoreVersion } = data ?? {};
|
|
610
|
+
if (restoreVersion && frag) {
|
|
611
|
+
throw new StatusCodeError(400, 'restoreVersion not allowed with object path.');
|
|
612
|
+
}
|
|
587
613
|
const storage = HelixStorage.fromContext(ctx).configBus();
|
|
588
614
|
const buf = await storage.get(this.key);
|
|
589
615
|
let old = buf ? JSON.parse(buf) : null;
|
|
590
|
-
|
|
616
|
+
let { versionName } = data ?? {};
|
|
617
|
+
delete data?.versionName;
|
|
618
|
+
if (restoreVersion) {
|
|
619
|
+
ctx.log.info(`restoring ${this.org}/${this.name} config from version ${restoreVersion}...`);
|
|
620
|
+
data = await this.#versioning.getVersion(ctx, restoreVersion, true);
|
|
621
|
+
versionName = `restored from version ${restoreVersion}`;
|
|
622
|
+
}
|
|
623
|
+
|
|
591
624
|
let config = data;
|
|
592
625
|
// set config to null if empty object
|
|
593
626
|
if (isDeepEqual(config, {})) {
|
|
@@ -761,8 +794,21 @@ export class ConfigStore {
|
|
|
761
794
|
updateCodeSource(ctx, config.code);
|
|
762
795
|
}
|
|
763
796
|
|
|
764
|
-
let oldConfig =
|
|
765
|
-
|
|
797
|
+
let oldConfig = null;
|
|
798
|
+
let modified = true;
|
|
799
|
+
if (buf) {
|
|
800
|
+
oldConfig = JSON.parse(buf);
|
|
801
|
+
modified = !isDeepEqual({
|
|
802
|
+
...oldConfig,
|
|
803
|
+
lastModified: 'ignore',
|
|
804
|
+
}, {
|
|
805
|
+
...config,
|
|
806
|
+
lastModified: 'ignore',
|
|
807
|
+
});
|
|
808
|
+
config.created = oldConfig.created;
|
|
809
|
+
} else if (!restoreVersion) {
|
|
810
|
+
delete config.created; // don't allow to set created on new config
|
|
811
|
+
}
|
|
766
812
|
this.#updateTimeStamps(config);
|
|
767
813
|
let newConfig = config;
|
|
768
814
|
let purgeConfig = oldConfig;
|
|
@@ -776,12 +822,22 @@ export class ConfigStore {
|
|
|
776
822
|
|
|
777
823
|
await this.validate(ctx, newConfig);
|
|
778
824
|
await this.validatePermissions(ctx, oldConfig, newConfig);
|
|
825
|
+
|
|
826
|
+
if (modified) {
|
|
827
|
+
// Create a new version before saving only when modified
|
|
828
|
+
await this.#versioning.createVersion(ctx, config, versionName);
|
|
829
|
+
}
|
|
779
830
|
await storage.put(this.key, JSON.stringify(config), 'application/json');
|
|
780
831
|
await this.purge(ctx, purgeConfig, newConfig);
|
|
781
832
|
return ret ?? redact(data, frag);
|
|
782
833
|
}
|
|
783
834
|
|
|
784
835
|
async remove(ctx, relPath) {
|
|
836
|
+
if (relPath.startsWith('versions/')) {
|
|
837
|
+
await this.deleteVersion(ctx, Number.parseInt(relPath.substring('versions/'.length), 10));
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
|
|
785
841
|
const storage = HelixStorage.fromContext(ctx).configBus();
|
|
786
842
|
const buf = await storage.get(this.key);
|
|
787
843
|
if (!buf) {
|
|
@@ -808,6 +864,35 @@ export class ConfigStore {
|
|
|
808
864
|
await this.purge(ctx, oldConfig, null);
|
|
809
865
|
}
|
|
810
866
|
|
|
867
|
+
/**
|
|
868
|
+
* List all versions for this config
|
|
869
|
+
* @param {Object} ctx - Context object
|
|
870
|
+
* @returns {Promise<Array>} Array of version information
|
|
871
|
+
*/
|
|
872
|
+
async listVersions(ctx) {
|
|
873
|
+
return this.#versioning.listVersions(ctx);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Get a specific version of the config
|
|
878
|
+
* @param {Object} ctx - Context object
|
|
879
|
+
* @param {number} version - Version number to retrieve
|
|
880
|
+
* @returns {Promise<Object>} The versioned config data
|
|
881
|
+
*/
|
|
882
|
+
async getVersion(ctx, version) {
|
|
883
|
+
return this.#versioning.getVersion(ctx, version);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Delete a specific version
|
|
888
|
+
* @param {Object} ctx - Context object
|
|
889
|
+
* @param {number} version - Version number to delete
|
|
890
|
+
* @returns {Promise<void>}
|
|
891
|
+
*/
|
|
892
|
+
async deleteVersion(ctx, version) {
|
|
893
|
+
return this.#versioning.deleteVersion(ctx, version);
|
|
894
|
+
}
|
|
895
|
+
|
|
811
896
|
// eslint-disable-next-line class-methods-use-this,no-unused-vars
|
|
812
897
|
async purge(ctx, oldConfig, newConfig) {
|
|
813
898
|
// override in subclass
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2024 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
|
+
import { HelixStorage } from '@adobe/helix-shared-storage';
|
|
14
|
+
import { StatusCodeError } from './status-code-error.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Version metadata structure
|
|
18
|
+
* @typedef {Object} VersionMetadata
|
|
19
|
+
* @property {number} version - Version number (auto-incrementing)
|
|
20
|
+
* @property {string} timestamp - ISO timestamp when version was created
|
|
21
|
+
* @property {string} [name] - Optional name/comment for the version
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Version information returned by listVersions
|
|
26
|
+
* @typedef {Object} VersionInfo
|
|
27
|
+
* @property {number} version - Version number
|
|
28
|
+
* @property {string} timestamp - ISO timestamp when version was created
|
|
29
|
+
* @property {string} [name] - Optional name/comment for the version
|
|
30
|
+
* @property {boolean} isCurrent - Whether this is the current version
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Config versioning manager
|
|
35
|
+
*/
|
|
36
|
+
export class ConfigVersioning {
|
|
37
|
+
/**
|
|
38
|
+
* Create a new ConfigVersioning instance
|
|
39
|
+
* @param {string} org - Organization name
|
|
40
|
+
* @param {string} type - Config type (sites, profiles, org)
|
|
41
|
+
* @param {string} name - Config name
|
|
42
|
+
*/
|
|
43
|
+
constructor(org, type = 'org', name = '') {
|
|
44
|
+
this.org = org;
|
|
45
|
+
this.type = type;
|
|
46
|
+
this.name = name;
|
|
47
|
+
this.prefix = type === 'org'
|
|
48
|
+
? `orgs/${org}/versions`
|
|
49
|
+
: `orgs/${org}/${type}/${name}/versions`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create a new version of the config
|
|
54
|
+
* @param {Object} ctx - Context object
|
|
55
|
+
* @param {Object} config - The config data to version
|
|
56
|
+
* @param {string} [name] - Optional name for the version
|
|
57
|
+
* @returns {Promise<VersionMetadata>} Version metadata
|
|
58
|
+
*/
|
|
59
|
+
async createVersion(ctx, config, name) {
|
|
60
|
+
const storage = HelixStorage.fromContext(ctx).configBus();
|
|
61
|
+
|
|
62
|
+
// Get current version number
|
|
63
|
+
const metadata = await this.getVersionMetadata(ctx);
|
|
64
|
+
const nextVersion = metadata.versions
|
|
65
|
+
.reduce((acc, current) => Math.max(acc, current.version), 0) + 1;
|
|
66
|
+
|
|
67
|
+
// eslint-disable-next-line no-param-reassign
|
|
68
|
+
config.version = nextVersion;
|
|
69
|
+
|
|
70
|
+
// Create version metadata
|
|
71
|
+
const versionMetadata = {
|
|
72
|
+
version: nextVersion,
|
|
73
|
+
created: new Date().toISOString(),
|
|
74
|
+
};
|
|
75
|
+
const meta = {
|
|
76
|
+
'x-version': String(nextVersion),
|
|
77
|
+
};
|
|
78
|
+
if (name) {
|
|
79
|
+
versionMetadata.name = name;
|
|
80
|
+
meta['x-version-name'] = name;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Store the versioned config
|
|
84
|
+
await storage.put(`${this.prefix}/${nextVersion}.json`, JSON.stringify(config, null, 2), 'application/json', meta, true);
|
|
85
|
+
|
|
86
|
+
// Update metadata
|
|
87
|
+
metadata.current = nextVersion;
|
|
88
|
+
metadata.versions.push(versionMetadata);
|
|
89
|
+
|
|
90
|
+
// Keep only the last 100 versions to prevent storage bloat
|
|
91
|
+
if (metadata.versions.length > 100) {
|
|
92
|
+
metadata.versions = metadata.versions.slice(-100);
|
|
93
|
+
}
|
|
94
|
+
await storage.put(`${this.prefix}.json`, JSON.stringify(metadata, null, 2), 'application/json');
|
|
95
|
+
return versionMetadata;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Update the name of a specific version
|
|
100
|
+
* @param ctx
|
|
101
|
+
* @param version
|
|
102
|
+
* @param name
|
|
103
|
+
* @returns {Promise<*>}
|
|
104
|
+
*/
|
|
105
|
+
async updateVersion(ctx, version, name) {
|
|
106
|
+
const storage = HelixStorage.fromContext(ctx).configBus();
|
|
107
|
+
const metadata = await this.getVersionMetadata(ctx);
|
|
108
|
+
const versionInfo = metadata.versions.find((v) => v.version === version);
|
|
109
|
+
if (!versionInfo) {
|
|
110
|
+
throw new StatusCodeError(404, `Version ${version} not found.`);
|
|
111
|
+
}
|
|
112
|
+
versionInfo.name = name;
|
|
113
|
+
await storage.put(`${this.prefix}.json`, JSON.stringify(metadata, null, 2), 'application/json');
|
|
114
|
+
return versionInfo;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* List all versions for this config
|
|
119
|
+
* @param {Object} ctx - Context object
|
|
120
|
+
* @returns {Promise<VersionInfo[]>} Array of version information
|
|
121
|
+
*/
|
|
122
|
+
async listVersions(ctx) {
|
|
123
|
+
return this.getVersionMetadata(ctx);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get a specific version of the config
|
|
128
|
+
* @param {Object} ctx - Context object
|
|
129
|
+
* @param {number} version - Version number to retrieve
|
|
130
|
+
* @param {boolean} [direct=false] - If true, fetch directly from storage without metadata
|
|
131
|
+
* @returns {Promise<Object>} The versioned config data
|
|
132
|
+
*/
|
|
133
|
+
async getVersion(ctx, version, direct) {
|
|
134
|
+
const storage = HelixStorage.fromContext(ctx).configBus();
|
|
135
|
+
const versionKey = `${this.prefix}/${version}.json`;
|
|
136
|
+
const buf = await storage.get(versionKey);
|
|
137
|
+
if (!buf) {
|
|
138
|
+
throw new StatusCodeError(404, `Version ${version} not found.`);
|
|
139
|
+
}
|
|
140
|
+
if (direct) {
|
|
141
|
+
return JSON.parse(buf);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const metadata = await this.getVersionMetadata(ctx);
|
|
145
|
+
const versionInfo = metadata.versions.find((v) => v.version === version) || {};
|
|
146
|
+
return {
|
|
147
|
+
...versionInfo,
|
|
148
|
+
data: JSON.parse(buf),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Delete a specific version
|
|
154
|
+
* @param {Object} ctx - Context object
|
|
155
|
+
* @param {number} version - Version number to delete
|
|
156
|
+
* @returns {Promise<void>}
|
|
157
|
+
*/
|
|
158
|
+
async deleteVersion(ctx, version) {
|
|
159
|
+
const storage = HelixStorage.fromContext(ctx).configBus();
|
|
160
|
+
|
|
161
|
+
// Get metadata to check if this is the current version
|
|
162
|
+
const metadata = await this.getVersionMetadata(ctx);
|
|
163
|
+
if (version === metadata.current) {
|
|
164
|
+
throw new StatusCodeError(400, 'Cannot delete the current version.');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Remove version from metadata
|
|
168
|
+
metadata.versions = metadata.versions.filter((v) => v.version !== version);
|
|
169
|
+
await storage.put(`${this.prefix}.json`, JSON.stringify(metadata, null, 2), 'application/json');
|
|
170
|
+
|
|
171
|
+
// Delete the versioned config
|
|
172
|
+
await storage.remove(`${this.prefix}/${version}.json`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get version metadata
|
|
177
|
+
* @param {Object} ctx - Context object
|
|
178
|
+
* @returns {Promise<Object>} Version metadata
|
|
179
|
+
*/
|
|
180
|
+
async getVersionMetadata(ctx) {
|
|
181
|
+
const storage = HelixStorage.fromContext(ctx).configBus();
|
|
182
|
+
const buf = await storage.get(`${this.prefix}.json`);
|
|
183
|
+
if (!buf) {
|
|
184
|
+
return { current: 0, versions: [] };
|
|
185
|
+
}
|
|
186
|
+
return JSON.parse(buf);
|
|
187
|
+
}
|
|
188
|
+
}
|
package/src/index.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
export { ConfigStore } from './config-store.js';
|
|
13
|
+
export { ConfigVersioning } from './config-versioning.js';
|
|
13
14
|
export { SCHEMAS } from './config-validator.js';
|
|
14
15
|
export * from './ValidationError.js';
|
|
15
16
|
export * from './config-merge.js';
|
|
@@ -250,6 +250,10 @@
|
|
|
250
250
|
"type": "string"
|
|
251
251
|
},
|
|
252
252
|
"description": "Additional hosts that are trusted to use the sidekick authentication"
|
|
253
|
+
},
|
|
254
|
+
"wordSaveDelay": {
|
|
255
|
+
"type": "number",
|
|
256
|
+
"description": "The delay to wait for Word to save the document before previewing (defaults to 1500)"
|
|
253
257
|
}
|
|
254
258
|
},
|
|
255
259
|
"additionalProperties": false
|