@featurevisor/core 0.51.0 → 0.51.2
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/.eslintcache +1 -1
- package/CHANGELOG.md +16 -0
- package/coverage/clover.xml +8 -8
- package/coverage/coverage-final.json +4 -4
- package/coverage/lcov-report/index.html +3 -3
- package/coverage/lcov-report/lib/{allocator.js.html → builder/allocator.js.html} +10 -10
- package/coverage/lcov-report/lib/{index.html → builder/index.html} +10 -10
- package/coverage/lcov-report/lib/{traffic.js.html → builder/traffic.js.html} +10 -10
- package/coverage/lcov-report/src/{allocator.ts.html → builder/allocator.ts.html} +10 -10
- package/coverage/lcov-report/src/{index.html → builder/index.html} +10 -10
- package/coverage/lcov-report/src/{traffic.ts.html → builder/traffic.ts.html} +10 -10
- package/coverage/lcov.info +4 -4
- package/lib/builder/allocator.js.map +1 -0
- package/lib/builder/allocator.spec.js.map +1 -0
- package/lib/builder/buildDatafile.d.ts +11 -0
- package/lib/{builder.js → builder/buildDatafile.js} +5 -106
- package/lib/builder/buildDatafile.js.map +1 -0
- package/lib/builder/buildProject.d.ts +8 -0
- package/lib/builder/buildProject.js +63 -0
- package/lib/builder/buildProject.js.map +1 -0
- package/lib/builder/getFeatureRanges.d.ts +11 -0
- package/lib/builder/getFeatureRanges.js +50 -0
- package/lib/builder/getFeatureRanges.js.map +1 -0
- package/lib/builder/index.d.ts +2 -0
- package/lib/builder/index.js +19 -0
- package/lib/builder/index.js.map +1 -0
- package/lib/builder/traffic.js.map +1 -0
- package/lib/builder/traffic.spec.js.map +1 -0
- package/lib/datasource/index.d.ts +2 -0
- package/lib/datasource/index.js +19 -0
- package/lib/datasource/index.js.map +1 -0
- package/lib/generate-code/index.js +1 -1
- package/lib/generate-code/index.js.map +1 -1
- package/lib/generate-code/typescript.d.ts +1 -1
- package/lib/linter/attributeSchema.d.ts +2 -0
- package/lib/linter/attributeSchema.js +15 -0
- package/lib/linter/attributeSchema.js.map +1 -0
- package/lib/linter/checkCircularDependency.d.ts +3 -0
- package/lib/linter/checkCircularDependency.js +30 -0
- package/lib/linter/checkCircularDependency.js.map +1 -0
- package/lib/linter/conditionSchema.d.ts +3 -0
- package/lib/linter/conditionSchema.js +44 -0
- package/lib/linter/conditionSchema.js.map +1 -0
- package/lib/linter/featureSchema.d.ts +3 -0
- package/lib/linter/featureSchema.js +147 -0
- package/lib/linter/featureSchema.js.map +1 -0
- package/lib/linter/groupSchema.d.ts +4 -0
- package/lib/linter/groupSchema.js +51 -0
- package/lib/linter/groupSchema.js.map +1 -0
- package/lib/linter/index.d.ts +1 -0
- package/lib/linter/index.js +18 -0
- package/lib/linter/index.js.map +1 -0
- package/lib/linter/lintProject.d.ts +2 -0
- package/lib/linter/lintProject.js +241 -0
- package/lib/linter/lintProject.js.map +1 -0
- package/lib/linter/printJoiError.d.ts +2 -0
- package/lib/linter/printJoiError.js +14 -0
- package/lib/linter/printJoiError.js.map +1 -0
- package/lib/linter/segmentSchema.d.ts +3 -0
- package/lib/linter/segmentSchema.js +14 -0
- package/lib/linter/segmentSchema.js.map +1 -0
- package/lib/linter/testSchema.d.ts +3 -0
- package/lib/linter/testSchema.js +33 -0
- package/lib/linter/testSchema.js.map +1 -0
- package/lib/site.js +1 -1
- package/lib/site.js.map +1 -1
- package/lib/tester.js +1 -1
- package/lib/tester.js.map +1 -1
- package/package.json +2 -2
- package/src/{builder.ts → builder/buildDatafile.ts} +5 -148
- package/src/builder/buildProject.ts +95 -0
- package/src/builder/getFeatureRanges.ts +61 -0
- package/src/builder/index.ts +2 -0
- package/src/datasource/index.ts +2 -0
- package/src/generate-code/index.ts +1 -1
- package/src/generate-code/typescript.ts +1 -1
- package/src/linter/attributeSchema.ts +12 -0
- package/src/linter/checkCircularDependency.ts +45 -0
- package/src/linter/conditionSchema.ts +85 -0
- package/src/linter/featureSchema.ts +205 -0
- package/src/linter/groupSchema.ts +63 -0
- package/src/linter/index.ts +1 -0
- package/src/linter/lintProject.ts +179 -0
- package/src/linter/printJoiError.ts +11 -0
- package/src/linter/segmentSchema.ts +13 -0
- package/src/linter/testSchema.ts +43 -0
- package/src/site.ts +1 -1
- package/src/tester.ts +1 -1
- package/lib/allocator.js.map +0 -1
- package/lib/allocator.spec.js.map +0 -1
- package/lib/builder.d.ts +0 -26
- package/lib/builder.js.map +0 -1
- package/lib/linter.d.ts +0 -11
- package/lib/linter.js +0 -542
- package/lib/linter.js.map +0 -1
- package/lib/traffic.js.map +0 -1
- package/lib/traffic.spec.js.map +0 -1
- package/src/linter.ts +0 -627
- /package/lib/{allocator.d.ts → builder/allocator.d.ts} +0 -0
- /package/lib/{allocator.js → builder/allocator.js} +0 -0
- /package/lib/{allocator.spec.d.ts → builder/allocator.spec.d.ts} +0 -0
- /package/lib/{allocator.spec.js → builder/allocator.spec.js} +0 -0
- /package/lib/{traffic.d.ts → builder/traffic.d.ts} +0 -0
- /package/lib/{traffic.js → builder/traffic.js} +0 -0
- /package/lib/{traffic.spec.d.ts → builder/traffic.spec.d.ts} +0 -0
- /package/lib/{traffic.spec.js → builder/traffic.spec.js} +0 -0
- /package/src/{allocator.spec.ts → builder/allocator.spec.ts} +0 -0
- /package/src/{allocator.ts → builder/allocator.ts} +0 -0
- /package/src/{traffic.spec.ts → builder/traffic.spec.ts} +0 -0
- /package/src/{traffic.ts → builder/traffic.ts} +0 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
|
|
4
|
+
import * as mkdirp from "mkdirp";
|
|
5
|
+
|
|
6
|
+
import { ExistingState, EnvironmentKey } from "@featurevisor/types";
|
|
7
|
+
|
|
8
|
+
import { SCHEMA_VERSION, ProjectConfig } from "../config";
|
|
9
|
+
import { Datasource } from "../datasource";
|
|
10
|
+
|
|
11
|
+
import { buildDatafile } from "./buildDatafile";
|
|
12
|
+
|
|
13
|
+
export function getDatafilePath(
|
|
14
|
+
projectConfig: ProjectConfig,
|
|
15
|
+
environment: EnvironmentKey,
|
|
16
|
+
tag: string,
|
|
17
|
+
): string {
|
|
18
|
+
const fileName = `datafile-tag-${tag}.json`;
|
|
19
|
+
|
|
20
|
+
return path.join(projectConfig.outputDirectoryPath, environment, fileName);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getExistingStateFilePath(
|
|
24
|
+
projectConfig: ProjectConfig,
|
|
25
|
+
environment: EnvironmentKey,
|
|
26
|
+
): string {
|
|
27
|
+
return path.join(projectConfig.stateDirectoryPath, `existing-state-${environment}.json`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface BuildCLIOptions {
|
|
31
|
+
revision?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function buildProject(
|
|
35
|
+
rootDirectoryPath,
|
|
36
|
+
projectConfig: ProjectConfig,
|
|
37
|
+
cliOptions: BuildCLIOptions = {},
|
|
38
|
+
) {
|
|
39
|
+
const tags = projectConfig.tags;
|
|
40
|
+
const environments = projectConfig.environments;
|
|
41
|
+
|
|
42
|
+
const pkg = require(path.join(rootDirectoryPath, "package.json"));
|
|
43
|
+
const datasource = new Datasource(projectConfig);
|
|
44
|
+
|
|
45
|
+
for (const environment of environments) {
|
|
46
|
+
console.log(`\nBuilding datafiles for environment: ${environment}`);
|
|
47
|
+
|
|
48
|
+
const existingStateFilePath = getExistingStateFilePath(projectConfig, environment);
|
|
49
|
+
const existingState: ExistingState = fs.existsSync(existingStateFilePath)
|
|
50
|
+
? require(existingStateFilePath)
|
|
51
|
+
: {
|
|
52
|
+
features: {},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
for (const tag of tags) {
|
|
56
|
+
console.log(`\n => Tag: ${tag}`);
|
|
57
|
+
const datafileContent = buildDatafile(
|
|
58
|
+
projectConfig,
|
|
59
|
+
datasource,
|
|
60
|
+
{
|
|
61
|
+
schemaVersion: SCHEMA_VERSION,
|
|
62
|
+
revision: cliOptions.revision || pkg.version,
|
|
63
|
+
environment: environment,
|
|
64
|
+
tag: tag,
|
|
65
|
+
},
|
|
66
|
+
existingState,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// write datafile for environment/tag
|
|
70
|
+
const outputEnvironmentDirPath = path.join(projectConfig.outputDirectoryPath, environment);
|
|
71
|
+
mkdirp.sync(outputEnvironmentDirPath);
|
|
72
|
+
|
|
73
|
+
const outputFilePath = getDatafilePath(projectConfig, environment, tag);
|
|
74
|
+
fs.writeFileSync(
|
|
75
|
+
outputFilePath,
|
|
76
|
+
projectConfig.prettyDatafile
|
|
77
|
+
? JSON.stringify(datafileContent, null, 2)
|
|
78
|
+
: JSON.stringify(datafileContent),
|
|
79
|
+
);
|
|
80
|
+
const shortPath = outputFilePath.replace(rootDirectoryPath + path.sep, "");
|
|
81
|
+
console.log(` Datafile generated: ${shortPath}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// write state for environment
|
|
85
|
+
if (!fs.existsSync(projectConfig.stateDirectoryPath)) {
|
|
86
|
+
mkdirp.sync(projectConfig.stateDirectoryPath);
|
|
87
|
+
}
|
|
88
|
+
fs.writeFileSync(
|
|
89
|
+
existingStateFilePath,
|
|
90
|
+
projectConfig.prettyState
|
|
91
|
+
? JSON.stringify(existingState, null, 2)
|
|
92
|
+
: JSON.stringify(existingState),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
|
|
3
|
+
import { FeatureKey, Group, Range } from "@featurevisor/types";
|
|
4
|
+
|
|
5
|
+
import { ProjectConfig } from "../config";
|
|
6
|
+
import { Datasource } from "../datasource";
|
|
7
|
+
|
|
8
|
+
export type FeatureRanges = Map<FeatureKey, Range[]>;
|
|
9
|
+
|
|
10
|
+
export interface FeatureRangesResult {
|
|
11
|
+
featureRanges: FeatureRanges;
|
|
12
|
+
featureIsInGroup: { [key: string]: boolean };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getFeatureRanges(
|
|
16
|
+
projectConfig: ProjectConfig,
|
|
17
|
+
datasource: Datasource,
|
|
18
|
+
): FeatureRangesResult {
|
|
19
|
+
const featureRanges = new Map<FeatureKey, Range[]>();
|
|
20
|
+
const featureIsInGroup = {}; // featureKey => boolean
|
|
21
|
+
|
|
22
|
+
const groups: Group[] = [];
|
|
23
|
+
if (fs.existsSync(projectConfig.groupsDirectoryPath)) {
|
|
24
|
+
const groupFiles = datasource.listGroups();
|
|
25
|
+
|
|
26
|
+
for (const groupKey of groupFiles) {
|
|
27
|
+
const parsedGroup = datasource.readGroup(groupKey);
|
|
28
|
+
|
|
29
|
+
groups.push({
|
|
30
|
+
...parsedGroup,
|
|
31
|
+
key: groupKey,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
let accumulatedPercentage = 0;
|
|
35
|
+
parsedGroup.slots.forEach(function (slot, slotIndex) {
|
|
36
|
+
const isFirstSlot = slotIndex === 0;
|
|
37
|
+
|
|
38
|
+
if (slot.feature) {
|
|
39
|
+
const featureKey = slot.feature;
|
|
40
|
+
|
|
41
|
+
if (typeof featureKey === "string") {
|
|
42
|
+
featureIsInGroup[featureKey] = true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const featureRangesForFeature = featureRanges.get(featureKey) || [];
|
|
46
|
+
|
|
47
|
+
const start = isFirstSlot ? accumulatedPercentage : accumulatedPercentage + 1;
|
|
48
|
+
const end = accumulatedPercentage + slot.percentage * 1000;
|
|
49
|
+
|
|
50
|
+
featureRangesForFeature.push([start, end]);
|
|
51
|
+
|
|
52
|
+
featureRanges.set(slot.feature, featureRangesForFeature);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
accumulatedPercentage += slot.percentage * 1000;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { featureRanges, featureIsInGroup };
|
|
61
|
+
}
|
|
@@ -4,7 +4,7 @@ import * as path from "path";
|
|
|
4
4
|
import * as mkdirp from "mkdirp";
|
|
5
5
|
|
|
6
6
|
import { ProjectConfig } from "../config";
|
|
7
|
-
import { Datasource } from "../datasource
|
|
7
|
+
import { Datasource } from "../datasource";
|
|
8
8
|
import { generateTypeScriptCodeForProject } from "./typescript";
|
|
9
9
|
|
|
10
10
|
export const ALLOWED_LANGUAGES_FOR_CODE_GENERATION = ["typescript"];
|
|
@@ -2,7 +2,7 @@ import * as fs from "fs";
|
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
|
|
4
4
|
import { ProjectConfig } from "../config";
|
|
5
|
-
import { Datasource } from "../datasource
|
|
5
|
+
import { Datasource } from "../datasource";
|
|
6
6
|
|
|
7
7
|
function convertFeaturevisorTypeToTypeScriptType(featurevisorType: string) {
|
|
8
8
|
switch (featurevisorType) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as Joi from "joi";
|
|
2
|
+
|
|
3
|
+
export function getAttributeJoiSchema() {
|
|
4
|
+
const attributeJoiSchema = Joi.object({
|
|
5
|
+
archived: Joi.boolean(),
|
|
6
|
+
type: Joi.string().allow("boolean", "string", "integer", "double", "date").required(),
|
|
7
|
+
description: Joi.string().required(),
|
|
8
|
+
capture: Joi.boolean(),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
return attributeJoiSchema;
|
|
12
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { FeatureKey, Required } from "@featurevisor/types";
|
|
2
|
+
|
|
3
|
+
import { Datasource } from "../datasource";
|
|
4
|
+
|
|
5
|
+
export function checkForCircularDependencyInRequired(
|
|
6
|
+
datasource: Datasource,
|
|
7
|
+
featureKey: FeatureKey,
|
|
8
|
+
required?: Required[],
|
|
9
|
+
chain: FeatureKey[] = [],
|
|
10
|
+
) {
|
|
11
|
+
if (!required) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const requiredKeys = required.map((r) => (typeof r === "string" ? r : r.key));
|
|
16
|
+
|
|
17
|
+
if (requiredKeys.length === 0) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
for (const requiredKey of requiredKeys) {
|
|
22
|
+
chain.push(requiredKey);
|
|
23
|
+
|
|
24
|
+
if (chain.indexOf(featureKey) > -1) {
|
|
25
|
+
throw new Error(`circular dependency found: ${chain.join(" -> ")}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const requiredFeatureExists = datasource.entityExists("feature", requiredKey);
|
|
29
|
+
|
|
30
|
+
if (!requiredFeatureExists) {
|
|
31
|
+
throw new Error(`required feature "${requiredKey}" not found`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const requiredParsedFeature = datasource.readFeature(requiredKey);
|
|
35
|
+
|
|
36
|
+
if (requiredParsedFeature.required) {
|
|
37
|
+
checkForCircularDependencyInRequired(
|
|
38
|
+
datasource,
|
|
39
|
+
featureKey,
|
|
40
|
+
requiredParsedFeature.required,
|
|
41
|
+
chain,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as Joi from "joi";
|
|
2
|
+
|
|
3
|
+
import { ProjectConfig } from "../config";
|
|
4
|
+
|
|
5
|
+
export function getConditionsJoiSchema(
|
|
6
|
+
projectConfig: ProjectConfig,
|
|
7
|
+
availableAttributeKeys: string[],
|
|
8
|
+
) {
|
|
9
|
+
const plainConditionJoiSchema = Joi.object({
|
|
10
|
+
attribute: Joi.string()
|
|
11
|
+
.valid(...availableAttributeKeys)
|
|
12
|
+
.required(),
|
|
13
|
+
operator: Joi.string()
|
|
14
|
+
.valid(
|
|
15
|
+
"equals",
|
|
16
|
+
"notEquals",
|
|
17
|
+
|
|
18
|
+
// numeric
|
|
19
|
+
"greaterThan",
|
|
20
|
+
"greaterThanOrEquals",
|
|
21
|
+
"lessThan",
|
|
22
|
+
"lessThanOrEquals",
|
|
23
|
+
|
|
24
|
+
// string
|
|
25
|
+
"contains",
|
|
26
|
+
"notContains",
|
|
27
|
+
"startsWith",
|
|
28
|
+
"endsWith",
|
|
29
|
+
|
|
30
|
+
// semver (string)
|
|
31
|
+
"semverEquals",
|
|
32
|
+
"semverNotEquals",
|
|
33
|
+
"semverGreaterThan",
|
|
34
|
+
"semverGreaterThanOrEquals",
|
|
35
|
+
"semverLessThan",
|
|
36
|
+
"semverLessThanOrEquals",
|
|
37
|
+
|
|
38
|
+
// date comparisons
|
|
39
|
+
"before",
|
|
40
|
+
"after",
|
|
41
|
+
|
|
42
|
+
// array of strings
|
|
43
|
+
"in",
|
|
44
|
+
"notIn",
|
|
45
|
+
)
|
|
46
|
+
.required(),
|
|
47
|
+
value: Joi.alternatives()
|
|
48
|
+
.try(
|
|
49
|
+
// @TODO: make them more specific
|
|
50
|
+
Joi.string(),
|
|
51
|
+
Joi.number(),
|
|
52
|
+
Joi.boolean(),
|
|
53
|
+
Joi.date(),
|
|
54
|
+
Joi.array().items(Joi.string()),
|
|
55
|
+
)
|
|
56
|
+
.required(),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const andOrNotConditionJoiSchema = Joi.alternatives()
|
|
60
|
+
.try(
|
|
61
|
+
Joi.object({
|
|
62
|
+
and: Joi.array().items(Joi.link("#andOrNotCondition"), plainConditionJoiSchema),
|
|
63
|
+
}),
|
|
64
|
+
Joi.object({
|
|
65
|
+
or: Joi.array().items(Joi.link("#andOrNotCondition"), plainConditionJoiSchema),
|
|
66
|
+
}),
|
|
67
|
+
Joi.object({
|
|
68
|
+
// @TODO: allow plainConditionJoiSchema as well?
|
|
69
|
+
not: Joi.array().items(Joi.link("#andOrNotCondition"), plainConditionJoiSchema),
|
|
70
|
+
}),
|
|
71
|
+
)
|
|
72
|
+
.id("andOrNotCondition");
|
|
73
|
+
|
|
74
|
+
const conditionJoiSchema = Joi.alternatives().try(
|
|
75
|
+
andOrNotConditionJoiSchema,
|
|
76
|
+
plainConditionJoiSchema,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const conditionsJoiSchema = Joi.alternatives().try(
|
|
80
|
+
conditionJoiSchema,
|
|
81
|
+
Joi.array().items(conditionJoiSchema),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
return conditionsJoiSchema;
|
|
85
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import * as Joi from "joi";
|
|
2
|
+
|
|
3
|
+
import { ProjectConfig } from "../config";
|
|
4
|
+
|
|
5
|
+
const tagRegex = /^[a-z0-9-]+$/;
|
|
6
|
+
|
|
7
|
+
export function getFeatureJoiSchema(
|
|
8
|
+
projectConfig: ProjectConfig,
|
|
9
|
+
conditionsJoiSchema,
|
|
10
|
+
availableSegmentKeys: string[],
|
|
11
|
+
availableFeatureKeys: string[],
|
|
12
|
+
) {
|
|
13
|
+
const variationValueJoiSchema = Joi.string().required();
|
|
14
|
+
const variableValueJoiSchema = Joi.alternatives()
|
|
15
|
+
.try(
|
|
16
|
+
// @TODO: make it stricter based on variableSchema.type
|
|
17
|
+
Joi.string(),
|
|
18
|
+
Joi.number(),
|
|
19
|
+
Joi.boolean(),
|
|
20
|
+
Joi.array().items(Joi.string()),
|
|
21
|
+
Joi.object().custom(function (value) {
|
|
22
|
+
let isFlat = true;
|
|
23
|
+
|
|
24
|
+
Object.keys(value).forEach((key) => {
|
|
25
|
+
if (typeof value[key] === "object") {
|
|
26
|
+
isFlat = false;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (!isFlat) {
|
|
31
|
+
throw new Error("object is not flat");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return value;
|
|
35
|
+
}),
|
|
36
|
+
)
|
|
37
|
+
.allow("");
|
|
38
|
+
|
|
39
|
+
const plainGroupSegment = Joi.string().valid("*", ...availableSegmentKeys);
|
|
40
|
+
|
|
41
|
+
const andOrNotGroupSegment = Joi.alternatives()
|
|
42
|
+
.try(
|
|
43
|
+
Joi.object({
|
|
44
|
+
and: Joi.array().items(Joi.link("#andOrNotGroupSegment"), plainGroupSegment),
|
|
45
|
+
}),
|
|
46
|
+
Joi.object({
|
|
47
|
+
or: Joi.array().items(Joi.link("#andOrNotGroupSegment"), plainGroupSegment),
|
|
48
|
+
}),
|
|
49
|
+
Joi.object({
|
|
50
|
+
// @TODO: allow plainGroupSegment as well?
|
|
51
|
+
not: Joi.array().items(Joi.link("#andOrNotGroupSegment"), plainGroupSegment),
|
|
52
|
+
}),
|
|
53
|
+
)
|
|
54
|
+
.id("andOrNotGroupSegment");
|
|
55
|
+
|
|
56
|
+
const groupSegment = Joi.alternatives().try(andOrNotGroupSegment, plainGroupSegment);
|
|
57
|
+
|
|
58
|
+
const groupSegmentsJoiSchema = Joi.alternatives().try(
|
|
59
|
+
Joi.array().items(groupSegment),
|
|
60
|
+
groupSegment,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const environmentJoiSchema = Joi.object({
|
|
64
|
+
expose: Joi.boolean(),
|
|
65
|
+
rules: Joi.array()
|
|
66
|
+
.items(
|
|
67
|
+
Joi.object({
|
|
68
|
+
key: Joi.string(),
|
|
69
|
+
segments: groupSegmentsJoiSchema,
|
|
70
|
+
percentage: Joi.number().precision(3).min(0).max(100),
|
|
71
|
+
|
|
72
|
+
enabled: Joi.boolean().optional(),
|
|
73
|
+
variation: variationValueJoiSchema.optional(), // @TODO: only allowed if feature.variations is present
|
|
74
|
+
variables: Joi.object().optional(), // @TODO: make it stricter
|
|
75
|
+
}),
|
|
76
|
+
)
|
|
77
|
+
.unique("key")
|
|
78
|
+
.required(),
|
|
79
|
+
force: Joi.array().items(
|
|
80
|
+
Joi.object({
|
|
81
|
+
// @TODO: either of the two below
|
|
82
|
+
segments: groupSegmentsJoiSchema.optional(),
|
|
83
|
+
conditions: conditionsJoiSchema.optional(),
|
|
84
|
+
|
|
85
|
+
enabled: Joi.boolean().optional(),
|
|
86
|
+
variation: variationValueJoiSchema.optional(),
|
|
87
|
+
variables: Joi.object().optional(), // @TODO: make it stricter
|
|
88
|
+
}),
|
|
89
|
+
),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const allEnvironmentsSchema = {};
|
|
93
|
+
projectConfig.environments.forEach((environmentKey) => {
|
|
94
|
+
allEnvironmentsSchema[environmentKey] = environmentJoiSchema.required();
|
|
95
|
+
});
|
|
96
|
+
const allEnvironmentsJoiSchema = Joi.object(allEnvironmentsSchema);
|
|
97
|
+
|
|
98
|
+
const featureJoiSchema = Joi.object({
|
|
99
|
+
archived: Joi.boolean().optional(),
|
|
100
|
+
deprecated: Joi.boolean().optional(),
|
|
101
|
+
description: Joi.string().required(),
|
|
102
|
+
tags: Joi.array()
|
|
103
|
+
.items(
|
|
104
|
+
Joi.string().custom((value) => {
|
|
105
|
+
if (!tagRegex.test(value)) {
|
|
106
|
+
throw new Error("tag must be lower cased and alphanumeric, and may contain hyphens.");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return value;
|
|
110
|
+
}),
|
|
111
|
+
)
|
|
112
|
+
.required(),
|
|
113
|
+
|
|
114
|
+
required: Joi.array()
|
|
115
|
+
.items(
|
|
116
|
+
Joi.alternatives().try(
|
|
117
|
+
Joi.string()
|
|
118
|
+
.required()
|
|
119
|
+
.valid(...availableFeatureKeys),
|
|
120
|
+
Joi.object({
|
|
121
|
+
key: Joi.string()
|
|
122
|
+
.required()
|
|
123
|
+
.valid(...availableFeatureKeys),
|
|
124
|
+
variation: Joi.string().optional(), // @TODO: can be made stricter
|
|
125
|
+
}),
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
.optional(),
|
|
129
|
+
|
|
130
|
+
bucketBy: Joi.alternatives()
|
|
131
|
+
.try(
|
|
132
|
+
// plain
|
|
133
|
+
Joi.string(),
|
|
134
|
+
|
|
135
|
+
// and
|
|
136
|
+
Joi.array().items(Joi.string()),
|
|
137
|
+
|
|
138
|
+
// or
|
|
139
|
+
Joi.object({
|
|
140
|
+
or: Joi.array().items(Joi.string()),
|
|
141
|
+
}),
|
|
142
|
+
)
|
|
143
|
+
.required(),
|
|
144
|
+
|
|
145
|
+
variablesSchema: Joi.array()
|
|
146
|
+
.items(
|
|
147
|
+
Joi.object({
|
|
148
|
+
key: Joi.string().disallow("variation"),
|
|
149
|
+
type: Joi.string().valid(
|
|
150
|
+
"string",
|
|
151
|
+
"integer",
|
|
152
|
+
"boolean",
|
|
153
|
+
"double",
|
|
154
|
+
"array",
|
|
155
|
+
"object",
|
|
156
|
+
"json",
|
|
157
|
+
),
|
|
158
|
+
description: Joi.string().optional(),
|
|
159
|
+
defaultValue: variableValueJoiSchema, // @TODO: make it stricter based on `type`
|
|
160
|
+
}),
|
|
161
|
+
)
|
|
162
|
+
.unique("key"),
|
|
163
|
+
|
|
164
|
+
variations: Joi.array()
|
|
165
|
+
.items(
|
|
166
|
+
Joi.object({
|
|
167
|
+
description: Joi.string(),
|
|
168
|
+
value: variationValueJoiSchema.required(),
|
|
169
|
+
weight: Joi.number().precision(3).min(0).max(100).required(),
|
|
170
|
+
variables: Joi.array()
|
|
171
|
+
.items(
|
|
172
|
+
Joi.object({
|
|
173
|
+
key: Joi.string(),
|
|
174
|
+
value: variableValueJoiSchema,
|
|
175
|
+
overrides: Joi.array().items(
|
|
176
|
+
Joi.object({
|
|
177
|
+
// @TODO: either segments or conditions prsent at a time
|
|
178
|
+
segments: groupSegmentsJoiSchema,
|
|
179
|
+
conditions: conditionsJoiSchema,
|
|
180
|
+
|
|
181
|
+
// @TODO: make it stricter based on `type`
|
|
182
|
+
value: variableValueJoiSchema,
|
|
183
|
+
}),
|
|
184
|
+
),
|
|
185
|
+
}),
|
|
186
|
+
)
|
|
187
|
+
.unique("key"),
|
|
188
|
+
}),
|
|
189
|
+
)
|
|
190
|
+
.custom((value) => {
|
|
191
|
+
const total = value.reduce((acc, v) => acc + v.weight, 0);
|
|
192
|
+
|
|
193
|
+
if (total !== 100) {
|
|
194
|
+
throw new Error(`Sum of all variation weights must be 100, got ${total}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return value;
|
|
198
|
+
})
|
|
199
|
+
.optional(),
|
|
200
|
+
|
|
201
|
+
environments: allEnvironmentsJoiSchema.required(),
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
return featureJoiSchema;
|
|
205
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as Joi from "joi";
|
|
2
|
+
|
|
3
|
+
import { ProjectConfig } from "../config";
|
|
4
|
+
import { Datasource } from "../datasource";
|
|
5
|
+
|
|
6
|
+
export function getGroupJoiSchema(
|
|
7
|
+
projectConfig: ProjectConfig,
|
|
8
|
+
datasource: Datasource,
|
|
9
|
+
availableFeatureKeys: string[],
|
|
10
|
+
) {
|
|
11
|
+
const groupJoiSchema = Joi.object({
|
|
12
|
+
description: Joi.string().required(),
|
|
13
|
+
slots: Joi.array()
|
|
14
|
+
.items(
|
|
15
|
+
Joi.object({
|
|
16
|
+
feature: Joi.string().valid(...availableFeatureKeys),
|
|
17
|
+
percentage: Joi.number().precision(3).min(0).max(100),
|
|
18
|
+
}),
|
|
19
|
+
)
|
|
20
|
+
.custom(function (value) {
|
|
21
|
+
const totalPercentage = value.reduce((acc, slot) => acc + slot.percentage, 0);
|
|
22
|
+
|
|
23
|
+
if (totalPercentage !== 100) {
|
|
24
|
+
throw new Error("total percentage is not 100");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const slot of value) {
|
|
28
|
+
const maxPercentageForRule = slot.percentage;
|
|
29
|
+
|
|
30
|
+
if (slot.feature) {
|
|
31
|
+
const featureKey = slot.feature;
|
|
32
|
+
const featureExists = datasource.entityExists("feature", featureKey);
|
|
33
|
+
|
|
34
|
+
if (!featureExists) {
|
|
35
|
+
throw new Error(`feature ${featureKey} not found`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const parsedFeature = datasource.readFeature(featureKey);
|
|
39
|
+
|
|
40
|
+
const environmentKeys = Object.keys(parsedFeature.environments);
|
|
41
|
+
for (const environmentKey of environmentKeys) {
|
|
42
|
+
const environment = parsedFeature.environments[environmentKey];
|
|
43
|
+
const rules = environment.rules;
|
|
44
|
+
|
|
45
|
+
for (const rule of rules) {
|
|
46
|
+
if (rule.percentage > maxPercentageForRule) {
|
|
47
|
+
// @TODO: this does not help with same feature belonging to multiple slots. fix that.
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Feature ${featureKey}'s rule ${rule.key} in ${environmentKey} has a percentage of ${rule.percentage} which is greater than the maximum percentage of ${maxPercentageForRule} for the slot`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return value;
|
|
58
|
+
})
|
|
59
|
+
.required(),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return groupJoiSchema;
|
|
63
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./lintProject";
|