@serve.zone/interfaces 32.57.0 → 32.58.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 +26 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/appstore/catalog.d.ts +1 -1
- package/dist_ts/appstore/catalog.js +2 -1
- package/dist_ts/appstore/types.d.ts +43 -0
- package/dist_ts/appstore/types.js +15 -1
- package/dist_ts/appstore/versionconfig.d.ts +12 -3
- package/dist_ts/appstore/versionconfig.js +131 -17
- package/package.json +3 -3
- package/readme.md +24 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/appstore/catalog.ts +1 -0
- package/ts/appstore/types.ts +46 -0
- package/ts/appstore/versionconfig.ts +144 -13
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* `platform.normalizeStorageRequest`), reporting through this module's rejection. What this module
|
|
16
16
|
* adds are the rules a request can only break inside a version config: its class is declared and of
|
|
17
17
|
* its kind, the class backs a declared limit or protection, ids and destinations are used once, the
|
|
18
|
-
* legacy names stay reserved, a backup exclusion names an object-storage request,
|
|
19
|
-
* feature ids are named.
|
|
18
|
+
* legacy names stay reserved, a backup exclusion names an object-storage request, a database backup
|
|
19
|
+
* exclusion names a provisioned database, and the storage feature ids are named.
|
|
20
20
|
*/
|
|
21
21
|
import { normalizeServiceAbsolutePath, validateServiceContainerArgs } from '../data/service.js';
|
|
22
22
|
import { normalizeServicePortConfig } from '../data/serviceports.js';
|
|
@@ -34,13 +34,16 @@ import { createStorageContractReaders } from '../private/storagecontract.js';
|
|
|
34
34
|
import type { TObjectStorageDelivery } from '../platform/storage.js';
|
|
35
35
|
import {
|
|
36
36
|
appStoreBackupExclusionLimits,
|
|
37
|
+
appStoreDatabaseBackupExclusionLimits,
|
|
37
38
|
appStoreStorageFeatureIds,
|
|
38
39
|
validateAppStoreEnvironmentDeclarations,
|
|
39
40
|
validateAppStoreVersionPlatformOidc,
|
|
40
41
|
} from './types.js';
|
|
41
42
|
import type {
|
|
42
43
|
IAppStoreBackupExclusion,
|
|
44
|
+
IAppStoreDatabaseBackupExclusion,
|
|
43
45
|
IAppStoreVersionConfig,
|
|
46
|
+
TAppStoreBackupExclusionDatabase,
|
|
44
47
|
TAppStoreStorageRequest,
|
|
45
48
|
} from './types.js';
|
|
46
49
|
|
|
@@ -56,6 +59,7 @@ const versionConfigMemberList = [
|
|
|
56
59
|
'storageClasses',
|
|
57
60
|
'storageRequests',
|
|
58
61
|
'backupExclusions',
|
|
62
|
+
'databaseBackupExclusions',
|
|
59
63
|
'publishedPorts',
|
|
60
64
|
'platformRequirements',
|
|
61
65
|
'platformOidc',
|
|
@@ -101,6 +105,12 @@ const legacyObjectStorageRequestId = 'legacy-s3';
|
|
|
101
105
|
const knownStorageFeatureIds: readonly string[] = Object.values(appStoreStorageFeatureIds);
|
|
102
106
|
const platformRequirementKeys = ['mongodb', 's3', 'clickhouse', 'valkey', 'mariadb', 'mail'] as const;
|
|
103
107
|
const controlCharacterPattern = /[\u0000-\u001f\u007f]/;
|
|
108
|
+
/** The C0 and C1 controls and DEL: what the database engine refuses in a namespace component. */
|
|
109
|
+
const collectionNameControlPattern = /[\u0000-\u001f\u007f-\u009f]/;
|
|
110
|
+
/** Each database a version can declare exclusions for, with the platform requirement provisioning it. */
|
|
111
|
+
const backupExclusionDatabaseRequirements: Readonly<Record<TAppStoreBackupExclusionDatabase, 'mongodb'>> = {
|
|
112
|
+
mongodb: 'mongodb',
|
|
113
|
+
};
|
|
104
114
|
const utf8Encoder = new TextEncoder();
|
|
105
115
|
|
|
106
116
|
/** Thrown inside this module only; every exported validator answers it as its result. */
|
|
@@ -612,6 +622,7 @@ function checkFeatureDeclaration(
|
|
|
612
622
|
requiresFeaturesArg: unknown,
|
|
613
623
|
explicitRequestsArg: readonly TAppStoreStorageRequest[],
|
|
614
624
|
backupExclusionsArg: readonly IAppStoreBackupExclusion[],
|
|
625
|
+
databaseBackupExclusionsArg: readonly IAppStoreDatabaseBackupExclusion[],
|
|
615
626
|
labelArg: string,
|
|
616
627
|
): void {
|
|
617
628
|
const derivedFeatures = new Set<string>();
|
|
@@ -628,6 +639,9 @@ function checkFeatureDeclaration(
|
|
|
628
639
|
if (backupExclusionsArg.length > 0) {
|
|
629
640
|
derivedFeatures.add(appStoreStorageFeatureIds.objectStorageBackupExclusions);
|
|
630
641
|
}
|
|
642
|
+
if (databaseBackupExclusionsArg.length > 0) {
|
|
643
|
+
derivedFeatures.add(appStoreStorageFeatureIds.databaseBackupExclusions);
|
|
644
|
+
}
|
|
631
645
|
|
|
632
646
|
const declaredEntries = requiresFeaturesArg === undefined
|
|
633
647
|
? []
|
|
@@ -669,6 +683,19 @@ const hasUnpairedSurrogate = (valueArg: string): boolean => {
|
|
|
669
683
|
return false;
|
|
670
684
|
};
|
|
671
685
|
|
|
686
|
+
/** An exclusion's `reason`: one trimmed line an operator reads. */
|
|
687
|
+
function checkBackupExclusionReason(valueArg: unknown, maximumLengthArg: number, entryLabelArg: string): string {
|
|
688
|
+
if (typeof valueArg !== 'string'
|
|
689
|
+
|| valueArg.length === 0
|
|
690
|
+
|| valueArg.length > maximumLengthArg
|
|
691
|
+
|| valueArg.trim() !== valueArg
|
|
692
|
+
|| controlCharacterPattern.test(valueArg)
|
|
693
|
+
|| hasUnpairedSurrogate(valueArg)) {
|
|
694
|
+
return reject(`Invalid ${entryLabelArg}.reason: must be one trimmed line of 1 to ${maximumLengthArg} characters`);
|
|
695
|
+
}
|
|
696
|
+
return valueArg;
|
|
697
|
+
}
|
|
698
|
+
|
|
672
699
|
/** One object key prefix: a relative key path that names a whole subtree. */
|
|
673
700
|
function checkBackupExclusionKeyPrefix(valueArg: unknown, labelArg: string): string {
|
|
674
701
|
if (typeof valueArg !== 'string') return reject(`Invalid ${labelArg}: expected a string`);
|
|
@@ -741,16 +768,99 @@ function checkBackupExclusions(valueArg: unknown, labelArg: string): IAppStoreBa
|
|
|
741
768
|
previousRequest = storageRequest;
|
|
742
769
|
|
|
743
770
|
const keyPrefixes = checkBackupExclusionKeyPrefixes(entry.keyPrefixes, `${entryLabel}.keyPrefixes`);
|
|
771
|
+
return { storageRequest, keyPrefixes, reason: checkBackupExclusionReason(reason, maximumReasonLength, entryLabel) };
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* One collection name a database backup exclusion carries: a name the database engine stores
|
|
777
|
+
* (`database` resources run on NoSQLDB, a MongoDB-compatible engine) that is not a system collection.
|
|
778
|
+
*/
|
|
779
|
+
function checkDatabaseBackupExclusionCollection(valueArg: unknown, labelArg: string): string {
|
|
780
|
+
const { maximumCollectionNameBytes } = appStoreDatabaseBackupExclusionLimits;
|
|
781
|
+
if (typeof valueArg !== 'string') return reject(`Invalid ${labelArg}: expected a string`);
|
|
782
|
+
if (collectionNameControlPattern.test(valueArg) || hasUnpairedSurrogate(valueArg)) {
|
|
783
|
+
reject(`Invalid ${labelArg}: must not contain a control character or an unpaired surrogate`);
|
|
784
|
+
}
|
|
785
|
+
const byteLength = utf8Encoder.encode(valueArg).byteLength;
|
|
786
|
+
if (byteLength < 1 || byteLength > maximumCollectionNameBytes) {
|
|
787
|
+
reject(`Invalid ${labelArg}: must be 1 to ${maximumCollectionNameBytes} UTF-8 bytes`);
|
|
788
|
+
}
|
|
789
|
+
if (valueArg.startsWith('.')) reject(`Invalid ${labelArg}: must not start with '.'`);
|
|
790
|
+
if (valueArg.startsWith('system.')) reject(`Invalid ${labelArg}: must not name a 'system.' collection`);
|
|
791
|
+
if (/[/\\$]/.test(valueArg)) reject(`Invalid ${labelArg}: must not contain '/', '\\' or '$'`);
|
|
792
|
+
return valueArg;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* One exclusion's collection list: 1 to 64 names, strictly increasing.
|
|
797
|
+
* `validateAppStoreDatabaseBackupExclusionCollections` exports it, so every reader of a collection
|
|
798
|
+
* list judges it by this one rule.
|
|
799
|
+
*/
|
|
800
|
+
function checkDatabaseBackupExclusionCollections(valueArg: unknown, labelArg: string): string[] {
|
|
801
|
+
const { maximumCollections } = appStoreDatabaseBackupExclusionLimits;
|
|
802
|
+
const collectionEntries = readDataElements(valueArg, maximumCollections);
|
|
803
|
+
if (collectionEntries === null || collectionEntries.length === 0) {
|
|
804
|
+
return reject(`Invalid ${labelArg}: expected an array of 1 to ${maximumCollections} collection names`);
|
|
805
|
+
}
|
|
806
|
+
const collections: string[] = [];
|
|
807
|
+
collectionEntries.forEach((collectionArg, indexArg) => {
|
|
808
|
+
const collection = checkDatabaseBackupExclusionCollection(collectionArg, `${labelArg}[${indexArg}]`);
|
|
809
|
+
const previousCollection = collections[collections.length - 1];
|
|
810
|
+
if (previousCollection !== undefined && compareWireTokens(previousCollection, collection) >= 0) {
|
|
811
|
+
reject(`Invalid ${labelArg}: must be strictly increasing`);
|
|
812
|
+
}
|
|
813
|
+
collections.push(collection);
|
|
814
|
+
});
|
|
815
|
+
return collections;
|
|
816
|
+
}
|
|
744
817
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
818
|
+
/**
|
|
819
|
+
* The members of each database backup exclusion, its collections and the entry order. Whether the
|
|
820
|
+
* version provisions the database an entry names is judged with its platform requirements
|
|
821
|
+
* (`checkDatabaseBackupExclusionTargets`).
|
|
822
|
+
*/
|
|
823
|
+
function checkDatabaseBackupExclusions(valueArg: unknown, labelArg: string): IAppStoreDatabaseBackupExclusion[] {
|
|
824
|
+
if (valueArg === undefined) return [];
|
|
825
|
+
const { maximumEntries, maximumReasonLength } = appStoreDatabaseBackupExclusionLimits;
|
|
826
|
+
const entries = readDataElements(valueArg, maximumEntries);
|
|
827
|
+
if (entries === null || entries.length === 0) {
|
|
828
|
+
return reject(`Invalid ${labelArg}: databaseBackupExclusions must contain between 1 and ${maximumEntries} entries`);
|
|
829
|
+
}
|
|
830
|
+
let previousDatabase: string | null = null;
|
|
831
|
+
return entries.map((entryArg, indexArg): IAppStoreDatabaseBackupExclusion => {
|
|
832
|
+
const entryLabel = `${labelArg} databaseBackupExclusions[${indexArg}]`;
|
|
833
|
+
const entry = readRecord(entryArg, entryLabel);
|
|
834
|
+
assertExactKeys(entry, ['database', 'collections', 'reason'], entryLabel);
|
|
835
|
+
const { database } = entry;
|
|
836
|
+
if (typeof database !== 'string' || !Object.hasOwn(backupExclusionDatabaseRequirements, database)) {
|
|
837
|
+
return reject(`Invalid ${entryLabel}.database: expected one of ${Object.keys(backupExclusionDatabaseRequirements).join(', ')}`);
|
|
838
|
+
}
|
|
839
|
+
if (previousDatabase !== null && compareWireTokens(previousDatabase, database) >= 0) {
|
|
840
|
+
reject(previousDatabase === database
|
|
841
|
+
? `Invalid ${labelArg}: databaseBackupExclusions name database '${database}' more than once`
|
|
842
|
+
: `Invalid ${labelArg}: databaseBackupExclusions must be in strictly increasing database order`);
|
|
843
|
+
}
|
|
844
|
+
previousDatabase = database;
|
|
845
|
+
return {
|
|
846
|
+
database: database as TAppStoreBackupExclusionDatabase,
|
|
847
|
+
collections: checkDatabaseBackupExclusionCollections(entry.collections, `${entryLabel}.collections`),
|
|
848
|
+
reason: checkBackupExclusionReason(entry.reason, maximumReasonLength, entryLabel),
|
|
849
|
+
};
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/** Each database backup exclusion names a database the version provisions. */
|
|
854
|
+
function checkDatabaseBackupExclusionTargets(
|
|
855
|
+
exclusionsArg: readonly IAppStoreDatabaseBackupExclusion[],
|
|
856
|
+
platformRequirementsArg: Record<string, unknown>,
|
|
857
|
+
labelArg: string,
|
|
858
|
+
): void {
|
|
859
|
+
exclusionsArg.forEach((exclusionArg, indexArg) => {
|
|
860
|
+
const requirement = backupExclusionDatabaseRequirements[exclusionArg.database];
|
|
861
|
+
if (platformRequirementsArg[requirement] !== true) {
|
|
862
|
+
reject(`Invalid ${labelArg} databaseBackupExclusions[${indexArg}].database: '${exclusionArg.database}' requires platformRequirements.${requirement}`);
|
|
752
863
|
}
|
|
753
|
-
return { storageRequest, keyPrefixes, reason };
|
|
754
864
|
});
|
|
755
865
|
}
|
|
756
866
|
|
|
@@ -842,7 +952,15 @@ function checkStorage(configArg: Record<string, unknown>, labelArg: string): voi
|
|
|
842
952
|
labelArg,
|
|
843
953
|
);
|
|
844
954
|
const backupExclusions = checkBackupExclusions(configArg.backupExclusions, labelArg);
|
|
845
|
-
|
|
955
|
+
const databaseBackupExclusions = checkDatabaseBackupExclusions(configArg.databaseBackupExclusions, labelArg);
|
|
956
|
+
checkFeatureDeclaration(
|
|
957
|
+
configArg.requiresFeatures,
|
|
958
|
+
explicitRequests,
|
|
959
|
+
backupExclusions,
|
|
960
|
+
databaseBackupExclusions,
|
|
961
|
+
labelArg,
|
|
962
|
+
);
|
|
963
|
+
checkDatabaseBackupExclusionTargets(databaseBackupExclusions, platformRequirements, labelArg);
|
|
846
964
|
const legacyS3 = platformRequirements.s3 === true;
|
|
847
965
|
if (legacyS3 && explicitRequests.some((requestArg) => requestArg.kind === 'objectStorage')) {
|
|
848
966
|
reject(`Invalid ${labelArg}: platformRequirements.s3 cannot coexist with explicit objectStorage requests`);
|
|
@@ -923,8 +1041,9 @@ export const validateAppStorePublishedPorts = (publishedPortsArg: unknown, label
|
|
|
923
1041
|
|
|
924
1042
|
/**
|
|
925
1043
|
* The config's member set and its storage: legacy `volumes`, `storageClasses`, `storageRequests`,
|
|
926
|
-
* `platformRequirements`, `backupExclusions`, the storage feature ids
|
|
927
|
-
* and every environment, secret-file and mount destination being used
|
|
1044
|
+
* `platformRequirements`, `backupExclusions`, `databaseBackupExclusions`, the storage feature ids
|
|
1045
|
+
* `requiresFeatures` must name, and every environment, secret-file and mount destination being used
|
|
1046
|
+
* once.
|
|
928
1047
|
*/
|
|
929
1048
|
export const validateAppStoreVersionStorage = (configArg: unknown, labelArg = defaultLabel): string[] =>
|
|
930
1049
|
firstRejection(() => checkStorage(readConfig(configArg, labelArg), labelArg));
|
|
@@ -941,6 +1060,18 @@ export const validateAppStoreBackupExclusionKeyPrefixes = (
|
|
|
941
1060
|
labelArg = 'keyPrefixes',
|
|
942
1061
|
): string[] => firstRejection(() => checkBackupExclusionKeyPrefixes(valueArg, labelArg));
|
|
943
1062
|
|
|
1063
|
+
/**
|
|
1064
|
+
* One database backup exclusion's `collections`, the rule `validateAppStoreVersionStorage` applies
|
|
1065
|
+
* to each `IAppStoreDatabaseBackupExclusion`: 1 to 64 names in strictly increasing UTF-16
|
|
1066
|
+
* code-unit order, each 1 to 128 UTF-8 bytes, not starting with `.` or `system.`, without `/`,
|
|
1067
|
+
* `\`, `$`, a control character or an unpaired surrogate. A reader of a list that carries such
|
|
1068
|
+
* names elsewhere, such as Corestore's export request or a backup's provenance, judges it here.
|
|
1069
|
+
*/
|
|
1070
|
+
export const validateAppStoreDatabaseBackupExclusionCollections = (
|
|
1071
|
+
valueArg: unknown,
|
|
1072
|
+
labelArg = 'collections',
|
|
1073
|
+
): string[] => firstRejection(() => checkDatabaseBackupExclusionCollections(valueArg, labelArg));
|
|
1074
|
+
|
|
944
1075
|
/**
|
|
945
1076
|
* Every member rule of a version config, in the order a host applies them: image, port model,
|
|
946
1077
|
* container arguments, environment declarations, storage, platform OIDC, published ports.
|