@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.
Files changed (110) hide show
  1. package/.eslintcache +1 -1
  2. package/CHANGELOG.md +16 -0
  3. package/coverage/clover.xml +8 -8
  4. package/coverage/coverage-final.json +4 -4
  5. package/coverage/lcov-report/index.html +3 -3
  6. package/coverage/lcov-report/lib/{allocator.js.html → builder/allocator.js.html} +10 -10
  7. package/coverage/lcov-report/lib/{index.html → builder/index.html} +10 -10
  8. package/coverage/lcov-report/lib/{traffic.js.html → builder/traffic.js.html} +10 -10
  9. package/coverage/lcov-report/src/{allocator.ts.html → builder/allocator.ts.html} +10 -10
  10. package/coverage/lcov-report/src/{index.html → builder/index.html} +10 -10
  11. package/coverage/lcov-report/src/{traffic.ts.html → builder/traffic.ts.html} +10 -10
  12. package/coverage/lcov.info +4 -4
  13. package/lib/builder/allocator.js.map +1 -0
  14. package/lib/builder/allocator.spec.js.map +1 -0
  15. package/lib/builder/buildDatafile.d.ts +11 -0
  16. package/lib/{builder.js → builder/buildDatafile.js} +5 -106
  17. package/lib/builder/buildDatafile.js.map +1 -0
  18. package/lib/builder/buildProject.d.ts +8 -0
  19. package/lib/builder/buildProject.js +63 -0
  20. package/lib/builder/buildProject.js.map +1 -0
  21. package/lib/builder/getFeatureRanges.d.ts +11 -0
  22. package/lib/builder/getFeatureRanges.js +50 -0
  23. package/lib/builder/getFeatureRanges.js.map +1 -0
  24. package/lib/builder/index.d.ts +2 -0
  25. package/lib/builder/index.js +19 -0
  26. package/lib/builder/index.js.map +1 -0
  27. package/lib/builder/traffic.js.map +1 -0
  28. package/lib/builder/traffic.spec.js.map +1 -0
  29. package/lib/datasource/index.d.ts +2 -0
  30. package/lib/datasource/index.js +19 -0
  31. package/lib/datasource/index.js.map +1 -0
  32. package/lib/generate-code/index.js +1 -1
  33. package/lib/generate-code/index.js.map +1 -1
  34. package/lib/generate-code/typescript.d.ts +1 -1
  35. package/lib/linter/attributeSchema.d.ts +2 -0
  36. package/lib/linter/attributeSchema.js +15 -0
  37. package/lib/linter/attributeSchema.js.map +1 -0
  38. package/lib/linter/checkCircularDependency.d.ts +3 -0
  39. package/lib/linter/checkCircularDependency.js +30 -0
  40. package/lib/linter/checkCircularDependency.js.map +1 -0
  41. package/lib/linter/conditionSchema.d.ts +3 -0
  42. package/lib/linter/conditionSchema.js +44 -0
  43. package/lib/linter/conditionSchema.js.map +1 -0
  44. package/lib/linter/featureSchema.d.ts +3 -0
  45. package/lib/linter/featureSchema.js +147 -0
  46. package/lib/linter/featureSchema.js.map +1 -0
  47. package/lib/linter/groupSchema.d.ts +4 -0
  48. package/lib/linter/groupSchema.js +51 -0
  49. package/lib/linter/groupSchema.js.map +1 -0
  50. package/lib/linter/index.d.ts +1 -0
  51. package/lib/linter/index.js +18 -0
  52. package/lib/linter/index.js.map +1 -0
  53. package/lib/linter/lintProject.d.ts +2 -0
  54. package/lib/linter/lintProject.js +241 -0
  55. package/lib/linter/lintProject.js.map +1 -0
  56. package/lib/linter/printJoiError.d.ts +2 -0
  57. package/lib/linter/printJoiError.js +14 -0
  58. package/lib/linter/printJoiError.js.map +1 -0
  59. package/lib/linter/segmentSchema.d.ts +3 -0
  60. package/lib/linter/segmentSchema.js +14 -0
  61. package/lib/linter/segmentSchema.js.map +1 -0
  62. package/lib/linter/testSchema.d.ts +3 -0
  63. package/lib/linter/testSchema.js +33 -0
  64. package/lib/linter/testSchema.js.map +1 -0
  65. package/lib/site.js +1 -1
  66. package/lib/site.js.map +1 -1
  67. package/lib/tester.js +1 -1
  68. package/lib/tester.js.map +1 -1
  69. package/package.json +2 -2
  70. package/src/{builder.ts → builder/buildDatafile.ts} +5 -148
  71. package/src/builder/buildProject.ts +95 -0
  72. package/src/builder/getFeatureRanges.ts +61 -0
  73. package/src/builder/index.ts +2 -0
  74. package/src/datasource/index.ts +2 -0
  75. package/src/generate-code/index.ts +1 -1
  76. package/src/generate-code/typescript.ts +1 -1
  77. package/src/linter/attributeSchema.ts +12 -0
  78. package/src/linter/checkCircularDependency.ts +45 -0
  79. package/src/linter/conditionSchema.ts +85 -0
  80. package/src/linter/featureSchema.ts +205 -0
  81. package/src/linter/groupSchema.ts +63 -0
  82. package/src/linter/index.ts +1 -0
  83. package/src/linter/lintProject.ts +179 -0
  84. package/src/linter/printJoiError.ts +11 -0
  85. package/src/linter/segmentSchema.ts +13 -0
  86. package/src/linter/testSchema.ts +43 -0
  87. package/src/site.ts +1 -1
  88. package/src/tester.ts +1 -1
  89. package/lib/allocator.js.map +0 -1
  90. package/lib/allocator.spec.js.map +0 -1
  91. package/lib/builder.d.ts +0 -26
  92. package/lib/builder.js.map +0 -1
  93. package/lib/linter.d.ts +0 -11
  94. package/lib/linter.js +0 -542
  95. package/lib/linter.js.map +0 -1
  96. package/lib/traffic.js.map +0 -1
  97. package/lib/traffic.spec.js.map +0 -1
  98. package/src/linter.ts +0 -627
  99. /package/lib/{allocator.d.ts → builder/allocator.d.ts} +0 -0
  100. /package/lib/{allocator.js → builder/allocator.js} +0 -0
  101. /package/lib/{allocator.spec.d.ts → builder/allocator.spec.d.ts} +0 -0
  102. /package/lib/{allocator.spec.js → builder/allocator.spec.js} +0 -0
  103. /package/lib/{traffic.d.ts → builder/traffic.d.ts} +0 -0
  104. /package/lib/{traffic.js → builder/traffic.js} +0 -0
  105. /package/lib/{traffic.spec.d.ts → builder/traffic.spec.d.ts} +0 -0
  106. /package/lib/{traffic.spec.js → builder/traffic.spec.js} +0 -0
  107. /package/src/{allocator.spec.ts → builder/allocator.spec.ts} +0 -0
  108. /package/src/{allocator.ts → builder/allocator.ts} +0 -0
  109. /package/src/{traffic.spec.ts → builder/traffic.spec.ts} +0 -0
  110. /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
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./buildDatafile";
2
+ export * from "./buildProject";
@@ -0,0 +1,2 @@
1
+ export * from "./datasource";
2
+ export * from "./parsers";
@@ -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/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/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";