@featurevisor/core 0.50.0 → 0.51.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.
Files changed (51) hide show
  1. package/.eslintcache +1 -1
  2. package/CHANGELOG.md +19 -0
  3. package/coverage/clover.xml +2 -2
  4. package/coverage/lcov-report/index.html +1 -1
  5. package/coverage/lcov-report/lib/allocator.js.html +1 -1
  6. package/coverage/lcov-report/lib/index.html +1 -1
  7. package/coverage/lcov-report/lib/traffic.js.html +1 -1
  8. package/coverage/lcov-report/src/allocator.ts.html +1 -1
  9. package/coverage/lcov-report/src/index.html +1 -1
  10. package/coverage/lcov-report/src/traffic.ts.html +1 -1
  11. package/lib/linter/attributeSchema.d.ts +2 -0
  12. package/lib/linter/attributeSchema.js +15 -0
  13. package/lib/linter/attributeSchema.js.map +1 -0
  14. package/lib/linter/checkCircularDependency.d.ts +3 -0
  15. package/lib/linter/checkCircularDependency.js +30 -0
  16. package/lib/linter/checkCircularDependency.js.map +1 -0
  17. package/lib/linter/conditionSchema.d.ts +3 -0
  18. package/lib/linter/conditionSchema.js +44 -0
  19. package/lib/linter/conditionSchema.js.map +1 -0
  20. package/lib/linter/featureSchema.d.ts +3 -0
  21. package/lib/linter/featureSchema.js +147 -0
  22. package/lib/linter/featureSchema.js.map +1 -0
  23. package/lib/linter/groupSchema.d.ts +4 -0
  24. package/lib/linter/groupSchema.js +51 -0
  25. package/lib/linter/groupSchema.js.map +1 -0
  26. package/lib/linter/index.d.ts +2 -0
  27. package/lib/linter/index.js +241 -0
  28. package/lib/linter/index.js.map +1 -0
  29. package/lib/linter/printJoiError.d.ts +2 -0
  30. package/lib/linter/printJoiError.js +14 -0
  31. package/lib/linter/printJoiError.js.map +1 -0
  32. package/lib/linter/segmentSchema.d.ts +3 -0
  33. package/lib/linter/segmentSchema.js +14 -0
  34. package/lib/linter/segmentSchema.js.map +1 -0
  35. package/lib/linter/testSchema.d.ts +3 -0
  36. package/lib/linter/testSchema.js +33 -0
  37. package/lib/linter/testSchema.js.map +1 -0
  38. package/package.json +5 -5
  39. package/src/linter/attributeSchema.ts +12 -0
  40. package/src/linter/checkCircularDependency.ts +45 -0
  41. package/src/linter/conditionSchema.ts +85 -0
  42. package/src/linter/featureSchema.ts +205 -0
  43. package/src/linter/groupSchema.ts +63 -0
  44. package/src/linter/index.ts +179 -0
  45. package/src/linter/printJoiError.ts +11 -0
  46. package/src/linter/segmentSchema.ts +13 -0
  47. package/src/linter/testSchema.ts +43 -0
  48. package/lib/linter.d.ts +0 -11
  49. package/lib/linter.js +0 -541
  50. package/lib/linter.js.map +0 -1
  51. package/src/linter.ts +0 -626
@@ -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/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,179 @@
1
+ // for use in node only
2
+ import * as fs from "fs";
3
+
4
+ import * as Joi from "joi";
5
+
6
+ import { Datasource } from "../datasource/datasource";
7
+ import { ProjectConfig } from "../config";
8
+
9
+ import { getAttributeJoiSchema } from "./attributeSchema";
10
+ import { getConditionsJoiSchema } from "./conditionSchema";
11
+ import { getSegmentJoiSchema } from "./segmentSchema";
12
+ import { getGroupJoiSchema } from "./groupSchema";
13
+ import { getFeatureJoiSchema } from "./featureSchema";
14
+ import { getTestsJoiSchema } from "./testSchema";
15
+
16
+ import { checkForCircularDependencyInRequired } from "./checkCircularDependency";
17
+ import { printJoiError } from "./printJoiError";
18
+
19
+ export async function lintProject(projectConfig: ProjectConfig): Promise<boolean> {
20
+ let hasError = false;
21
+ const datasource = new Datasource(projectConfig);
22
+
23
+ const availableAttributeKeys: string[] = [];
24
+ const availableSegmentKeys: string[] = [];
25
+ const availableFeatureKeys: string[] = [];
26
+
27
+ // lint attributes
28
+ const attributes = datasource.listAttributes();
29
+ console.log(`Linting ${attributes.length} attributes...\n`);
30
+
31
+ const attributeJoiSchema = getAttributeJoiSchema();
32
+
33
+ for (const key of attributes) {
34
+ const parsed = datasource.readAttribute(key);
35
+ availableAttributeKeys.push(key);
36
+
37
+ try {
38
+ await attributeJoiSchema.validateAsync(parsed);
39
+ } catch (e) {
40
+ console.log(" =>", key);
41
+
42
+ if (e instanceof Joi.ValidationError) {
43
+ printJoiError(e);
44
+ } else {
45
+ console.log(e);
46
+ }
47
+
48
+ hasError = true;
49
+ }
50
+ }
51
+
52
+ // lint segments
53
+ const segments = datasource.listSegments();
54
+ console.log(`\nLinting ${segments.length} segments...\n`);
55
+
56
+ const conditionsJoiSchema = getConditionsJoiSchema(projectConfig, availableAttributeKeys);
57
+ const segmentJoiSchema = getSegmentJoiSchema(projectConfig, conditionsJoiSchema);
58
+
59
+ for (const key of segments) {
60
+ const parsed = datasource.readSegment(key);
61
+ availableSegmentKeys.push(key);
62
+
63
+ try {
64
+ await segmentJoiSchema.validateAsync(parsed);
65
+ } catch (e) {
66
+ console.log(" =>", key);
67
+
68
+ if (e instanceof Joi.ValidationError) {
69
+ printJoiError(e);
70
+ } else {
71
+ console.log(e);
72
+ }
73
+
74
+ hasError = true;
75
+ }
76
+ }
77
+
78
+ // lint groups
79
+
80
+ if (fs.existsSync(projectConfig.groupsDirectoryPath)) {
81
+ const groups = datasource.listGroups();
82
+ console.log(`\nLinting ${groups.length} groups...\n`);
83
+
84
+ // @TODO: feature it slots can be from availableFeatureKeys only
85
+ const groupJoiSchema = getGroupJoiSchema(projectConfig, datasource, availableFeatureKeys);
86
+
87
+ for (const key of groups) {
88
+ const parsed = datasource.readGroup(key);
89
+
90
+ try {
91
+ await groupJoiSchema.validateAsync(parsed);
92
+ } catch (e) {
93
+ console.log(" =>", key);
94
+
95
+ if (e instanceof Joi.ValidationError) {
96
+ printJoiError(e);
97
+ } else {
98
+ console.log(e);
99
+ }
100
+
101
+ hasError = true;
102
+ }
103
+ }
104
+ }
105
+
106
+ // @TODO: feature cannot exist in multiple groups
107
+
108
+ // lint features
109
+ const features = datasource.listFeatures();
110
+ console.log(`\nLinting ${features.length} features...\n`);
111
+
112
+ const featureJoiSchema = getFeatureJoiSchema(
113
+ projectConfig,
114
+ conditionsJoiSchema,
115
+ availableSegmentKeys,
116
+ availableFeatureKeys,
117
+ );
118
+
119
+ for (const key of features) {
120
+ const parsed = datasource.readFeature(key);
121
+ availableFeatureKeys.push(key);
122
+
123
+ try {
124
+ await featureJoiSchema.validateAsync(parsed);
125
+ } catch (e) {
126
+ console.log(" =>", key);
127
+
128
+ if (e instanceof Joi.ValidationError) {
129
+ printJoiError(e);
130
+ } else {
131
+ console.log(e);
132
+ }
133
+
134
+ hasError = true;
135
+ }
136
+
137
+ if (parsed.required) {
138
+ try {
139
+ checkForCircularDependencyInRequired(datasource, key, parsed.required);
140
+ } catch (e) {
141
+ console.log(" =>", key);
142
+ console.log(" => Error:", e.message);
143
+ hasError = true;
144
+ }
145
+ }
146
+ }
147
+
148
+ // lint tests
149
+ if (fs.existsSync(projectConfig.testsDirectoryPath)) {
150
+ const tests = datasource.listTests();
151
+ console.log(`\nLinting ${tests.length} tests...\n`);
152
+
153
+ const testsJoiSchema = getTestsJoiSchema(
154
+ projectConfig,
155
+ availableFeatureKeys,
156
+ availableSegmentKeys,
157
+ );
158
+
159
+ for (const key of tests) {
160
+ const parsed = datasource.readTest(key);
161
+
162
+ try {
163
+ await testsJoiSchema.validateAsync(parsed);
164
+ } catch (e) {
165
+ console.log(" =>", key);
166
+
167
+ if (e instanceof Joi.ValidationError) {
168
+ printJoiError(e);
169
+ } else {
170
+ console.log(e);
171
+ }
172
+
173
+ hasError = true;
174
+ }
175
+ }
176
+ }
177
+
178
+ return hasError;
179
+ }
@@ -0,0 +1,11 @@
1
+ import * as Joi from "joi";
2
+
3
+ export function printJoiError(e: Joi.ValidationError) {
4
+ const { details } = e;
5
+
6
+ details.forEach((detail) => {
7
+ console.error(" => Error:", detail.message);
8
+ console.error(" => Path:", detail.path.join("."));
9
+ console.error(" => Value:", detail.context?.value);
10
+ });
11
+ }
@@ -0,0 +1,13 @@
1
+ import * as Joi from "joi";
2
+
3
+ import { ProjectConfig } from "../config";
4
+
5
+ export function getSegmentJoiSchema(projectConfig: ProjectConfig, conditionsJoiSchema) {
6
+ const segmentJoiSchema = Joi.object({
7
+ archived: Joi.boolean().optional(),
8
+ description: Joi.string().required(),
9
+ conditions: conditionsJoiSchema.required(),
10
+ });
11
+
12
+ return segmentJoiSchema;
13
+ }
@@ -0,0 +1,43 @@
1
+ import * as Joi from "joi";
2
+
3
+ import { ProjectConfig } from "../config";
4
+
5
+ export function getTestsJoiSchema(
6
+ projectConfig: ProjectConfig,
7
+ availableFeatureKeys: string[],
8
+ availableSegmentKeys: string[],
9
+ ) {
10
+ const segmentTestJoiSchema = Joi.object({
11
+ segment: Joi.string()
12
+ .valid(...availableSegmentKeys)
13
+ .required(),
14
+ assertions: Joi.array().items(
15
+ Joi.object({
16
+ description: Joi.string().optional(),
17
+ context: Joi.object(),
18
+ expectedToMatch: Joi.boolean(),
19
+ }),
20
+ ),
21
+ });
22
+
23
+ const featureTestJoiSchema = Joi.object({
24
+ feature: Joi.string()
25
+ .valid(...availableFeatureKeys)
26
+ .required(),
27
+ assertions: Joi.array().items(
28
+ Joi.object({
29
+ description: Joi.string().optional(),
30
+ at: Joi.number().precision(3).min(0).max(100),
31
+ environment: Joi.string().valid(...projectConfig.environments),
32
+ context: Joi.object(),
33
+
34
+ // @TODO: one or all below
35
+ expectedToBeEnabled: Joi.boolean().required(),
36
+ expectedVariation: Joi.alternatives().try(Joi.string(), Joi.number(), Joi.boolean()),
37
+ expectedVariables: Joi.object(),
38
+ }),
39
+ ),
40
+ });
41
+
42
+ return Joi.alternatives().try(segmentTestJoiSchema, featureTestJoiSchema);
43
+ }
package/lib/linter.d.ts DELETED
@@ -1,11 +0,0 @@
1
- import * as Joi from "joi";
2
- import { Datasource } from "./datasource/datasource";
3
- import { ProjectConfig } from "./config";
4
- export declare function getAttributeJoiSchema(): Joi.ObjectSchema<any>;
5
- export declare function getConditionsJoiSchema(projectConfig: ProjectConfig, availableAttributeKeys: string[]): Joi.AlternativesSchema<any>;
6
- export declare function getSegmentJoiSchema(projectConfig: ProjectConfig, conditionsJoiSchema: any): Joi.ObjectSchema<any>;
7
- export declare function getGroupJoiSchema(projectConfig: ProjectConfig, datasource: Datasource, availableFeatureKeys: string[]): Joi.ObjectSchema<any>;
8
- export declare function getFeatureJoiSchema(projectConfig: ProjectConfig, conditionsJoiSchema: any, availableSegmentKeys: string[], availableFeatureKeys: string[]): Joi.ObjectSchema<any>;
9
- export declare function getTestsJoiSchema(projectConfig: ProjectConfig, availableFeatureKeys: string[], availableSegmentKeys: string[]): Joi.AlternativesSchema<any>;
10
- export declare function printJoiError(e: Joi.ValidationError): void;
11
- export declare function lintProject(projectConfig: ProjectConfig): Promise<boolean>;