@featurevisor/core 2.20.0 → 2.22.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 +19 -0
- package/coverage/clover.xml +1286 -515
- package/coverage/coverage-final.json +17 -16
- package/coverage/lcov-report/builder/allocator.ts.html +1 -1
- package/coverage/lcov-report/builder/buildDatafile.ts.html +1 -1
- package/coverage/lcov-report/builder/buildScopedConditions.ts.html +1 -1
- package/coverage/lcov-report/builder/buildScopedDatafile.ts.html +1 -1
- package/coverage/lcov-report/builder/buildScopedSegments.ts.html +1 -1
- package/coverage/lcov-report/builder/convertToV1.ts.html +1 -1
- package/coverage/lcov-report/builder/getFeatureRanges.ts.html +1 -1
- package/coverage/lcov-report/builder/hashes.ts.html +1 -1
- package/coverage/lcov-report/builder/index.html +1 -1
- package/coverage/lcov-report/builder/mutateVariables.ts.html +1 -1
- package/coverage/lcov-report/builder/mutator.ts.html +1 -1
- package/coverage/lcov-report/builder/revision.ts.html +1 -1
- package/coverage/lcov-report/builder/traffic.ts.html +1 -1
- package/coverage/lcov-report/config/index.html +1 -1
- package/coverage/lcov-report/config/index.ts.html +1 -1
- package/coverage/lcov-report/config/projectConfig.ts.html +42 -42
- package/coverage/lcov-report/datasource/adapter.ts.html +2 -2
- package/coverage/lcov-report/datasource/datasource.ts.html +43 -43
- package/coverage/lcov-report/datasource/filesystemAdapter.ts.html +48 -48
- package/coverage/lcov-report/datasource/index.html +19 -19
- package/coverage/lcov-report/datasource/index.ts.html +4 -4
- package/coverage/lcov-report/generate-code/index.html +116 -0
- package/coverage/lcov-report/generate-code/typescript.ts.html +3004 -0
- package/coverage/lcov-report/index.html +44 -29
- package/coverage/lcov-report/linter/attributeSchema.ts.html +1815 -30
- package/coverage/lcov-report/linter/checkCircularDependency.ts.html +1 -1
- package/coverage/lcov-report/linter/checkPercentageExceedingSlot.ts.html +1 -1
- package/coverage/lcov-report/linter/conditionSchema.ts.html +1606 -157
- package/coverage/lcov-report/linter/featureSchema.ts.html +56 -56
- package/coverage/lcov-report/linter/groupSchema.ts.html +1 -1
- package/coverage/lcov-report/linter/index.html +39 -39
- package/coverage/lcov-report/linter/lintProject.ts.html +189 -78
- package/coverage/lcov-report/linter/mutationNotation.ts.html +1 -1
- package/coverage/lcov-report/linter/printError.ts.html +1 -1
- package/coverage/lcov-report/linter/schema.ts.html +59 -59
- package/coverage/lcov-report/linter/segmentSchema.ts.html +3 -3
- package/coverage/lcov-report/linter/testSchema.ts.html +6 -6
- package/coverage/lcov-report/list/index.html +1 -1
- package/coverage/lcov-report/list/matrix.ts.html +1 -1
- package/coverage/lcov-report/parsers/index.html +1 -1
- package/coverage/lcov-report/parsers/index.ts.html +4 -4
- package/coverage/lcov-report/parsers/json.ts.html +2 -2
- package/coverage/lcov-report/parsers/yml.ts.html +6 -6
- package/coverage/lcov-report/tester/cliFormat.ts.html +1 -1
- package/coverage/lcov-report/tester/helpers.ts.html +1 -1
- package/coverage/lcov-report/tester/index.html +1 -1
- package/coverage/lcov-report/utils/extractKeys.ts.html +1 -1
- package/coverage/lcov-report/utils/git.ts.html +3 -3
- package/coverage/lcov-report/utils/index.html +1 -1
- package/coverage/lcov-report/utils/index.ts.html +1 -1
- package/coverage/lcov-report/utils/pretty.ts.html +1 -1
- package/coverage/lcov.info +2624 -926
- package/lib/generate-code/typescript.js +57 -29
- package/lib/generate-code/typescript.js.map +1 -1
- package/lib/generate-code/typescript.spec.d.ts +1 -0
- package/lib/generate-code/typescript.spec.js +78 -0
- package/lib/generate-code/typescript.spec.js.map +1 -0
- package/lib/linter/attributeSchema.d.ts +352 -20
- package/lib/linter/attributeSchema.js +426 -16
- package/lib/linter/attributeSchema.js.map +1 -1
- package/lib/linter/attributeSchema.spec.d.ts +1 -0
- package/lib/linter/attributeSchema.spec.js +266 -0
- package/lib/linter/attributeSchema.spec.js.map +1 -0
- package/lib/linter/conditionSchema.d.ts +2 -1
- package/lib/linter/conditionSchema.js +370 -100
- package/lib/linter/conditionSchema.js.map +1 -1
- package/lib/linter/conditionSchema.spec.js +200 -153
- package/lib/linter/conditionSchema.spec.js.map +1 -1
- package/lib/linter/featureSchema.spec.js +114 -3
- package/lib/linter/featureSchema.spec.js.map +1 -1
- package/lib/linter/lintProject.js +45 -14
- package/lib/linter/lintProject.js.map +1 -1
- package/lib/linter/segmentSchema.spec.js +71 -1
- package/lib/linter/segmentSchema.spec.js.map +1 -1
- package/package.json +5 -5
- package/src/generate-code/typescript.spec.ts +108 -0
- package/src/generate-code/typescript.ts +80 -34
- package/src/linter/attributeSchema.spec.ts +342 -0
- package/src/linter/attributeSchema.ts +613 -18
- package/src/linter/conditionSchema.spec.ts +231 -189
- package/src/linter/conditionSchema.ts +582 -99
- package/src/linter/featureSchema.spec.ts +129 -4
- package/src/linter/lintProject.ts +55 -18
- package/src/linter/segmentSchema.spec.ts +82 -1
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* rules, force). Ensures every place a variable value can be set is validated against
|
|
5
5
|
* the variable's schema.
|
|
6
6
|
*/
|
|
7
|
-
import type { Schema } from "@featurevisor/types";
|
|
7
|
+
import type { Attribute, Schema } from "@featurevisor/types";
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
|
|
10
10
|
import type { ProjectConfig } from "../config";
|
|
@@ -42,7 +42,44 @@ function minimalProjectConfig(overrides: Partial<ProjectConfig> = {}): ProjectCo
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/** Attributes and segments that appear in conditions; keep small for tests. */
|
|
45
|
-
const TEST_ATTRIBUTES:
|
|
45
|
+
const TEST_ATTRIBUTES: Record<string, Attribute> = {
|
|
46
|
+
userId: {
|
|
47
|
+
description: "User ID",
|
|
48
|
+
type: "string",
|
|
49
|
+
},
|
|
50
|
+
country: {
|
|
51
|
+
description: "Country",
|
|
52
|
+
type: "string",
|
|
53
|
+
},
|
|
54
|
+
device: {
|
|
55
|
+
description: "Device",
|
|
56
|
+
type: "string",
|
|
57
|
+
},
|
|
58
|
+
permissions: {
|
|
59
|
+
description: "Permissions",
|
|
60
|
+
type: "array",
|
|
61
|
+
items: {
|
|
62
|
+
type: "string",
|
|
63
|
+
enum: ["read", "write", "admin"],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
version: {
|
|
67
|
+
description: "Version",
|
|
68
|
+
oneOf: [{ type: "string" }, { type: "double" }],
|
|
69
|
+
},
|
|
70
|
+
traits: {
|
|
71
|
+
description: "Traits",
|
|
72
|
+
type: "object",
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
const TEST_ATTRIBUTE_KEYS: [string, ...string[]] = [
|
|
76
|
+
"userId",
|
|
77
|
+
"country",
|
|
78
|
+
"device",
|
|
79
|
+
"permissions",
|
|
80
|
+
"version",
|
|
81
|
+
"traits",
|
|
82
|
+
];
|
|
46
83
|
const TEST_SEGMENTS: [string, ...string[]] = ["*", "countries/germany", "countries/france"];
|
|
47
84
|
const TEST_FEATURES: [string, ...string[]] = ["testFeature"];
|
|
48
85
|
const TEST_SCHEMA_KEYS = ["link", "slugSchema"];
|
|
@@ -76,7 +113,7 @@ function getFeatureSchema() {
|
|
|
76
113
|
return getFeatureZodSchema(
|
|
77
114
|
projectConfig,
|
|
78
115
|
conditionsZodSchema,
|
|
79
|
-
|
|
116
|
+
TEST_ATTRIBUTE_KEYS,
|
|
80
117
|
TEST_SEGMENTS,
|
|
81
118
|
TEST_FEATURES,
|
|
82
119
|
TEST_SCHEMA_KEYS,
|
|
@@ -140,6 +177,94 @@ function expectErrorSurfaces(
|
|
|
140
177
|
}
|
|
141
178
|
|
|
142
179
|
describe("featureSchema.ts :: getFeatureZodSchema (variablesSchema and variable values)", () => {
|
|
180
|
+
describe("attribute-aware rule and force conditions", () => {
|
|
181
|
+
it("accepts force conditions using semver operators on oneOf attributes", () => {
|
|
182
|
+
expectParseSuccess(
|
|
183
|
+
baseFeature({
|
|
184
|
+
variations: [
|
|
185
|
+
{ value: "control", weight: 50 },
|
|
186
|
+
{ value: "treatment", weight: 50 },
|
|
187
|
+
],
|
|
188
|
+
force: {
|
|
189
|
+
staging: [
|
|
190
|
+
{
|
|
191
|
+
conditions: [
|
|
192
|
+
{ attribute: "version", operator: "semverGreaterThan", value: "5.0.0" },
|
|
193
|
+
],
|
|
194
|
+
variation: "treatment",
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
production: [],
|
|
198
|
+
},
|
|
199
|
+
}),
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("accepts force conditions using flat object nested paths", () => {
|
|
204
|
+
expectParseSuccess(
|
|
205
|
+
baseFeature({
|
|
206
|
+
variations: [
|
|
207
|
+
{ value: "control", weight: 50 },
|
|
208
|
+
{ value: "treatment", weight: 50 },
|
|
209
|
+
],
|
|
210
|
+
force: {
|
|
211
|
+
staging: [
|
|
212
|
+
{
|
|
213
|
+
conditions: [
|
|
214
|
+
{ attribute: "traits.favoriteTheme", operator: "equals", value: "dark" },
|
|
215
|
+
],
|
|
216
|
+
variation: "treatment",
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
production: [],
|
|
220
|
+
},
|
|
221
|
+
}),
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("rejects force conditions when oneOf attribute value matches no branch", () => {
|
|
226
|
+
expectParseFailure(
|
|
227
|
+
baseFeature({
|
|
228
|
+
variations: [
|
|
229
|
+
{ value: "control", weight: 50 },
|
|
230
|
+
{ value: "treatment", weight: 50 },
|
|
231
|
+
],
|
|
232
|
+
force: {
|
|
233
|
+
staging: [
|
|
234
|
+
{
|
|
235
|
+
conditions: [{ attribute: "version", operator: "equals", value: true }],
|
|
236
|
+
variation: "treatment",
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
production: [],
|
|
240
|
+
},
|
|
241
|
+
}),
|
|
242
|
+
'Value does not match the schema of attribute "version"',
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("rejects force conditions when array item value violates enum constraints", () => {
|
|
247
|
+
expectParseFailure(
|
|
248
|
+
baseFeature({
|
|
249
|
+
variations: [
|
|
250
|
+
{ value: "control", weight: 50 },
|
|
251
|
+
{ value: "treatment", weight: 50 },
|
|
252
|
+
],
|
|
253
|
+
force: {
|
|
254
|
+
staging: [
|
|
255
|
+
{
|
|
256
|
+
conditions: [{ attribute: "permissions", operator: "includes", value: "delete" }],
|
|
257
|
+
variation: "treatment",
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
production: [],
|
|
261
|
+
},
|
|
262
|
+
}),
|
|
263
|
+
'Value does not match the item schema of attribute "permissions"',
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
143
268
|
describe("variablesSchema: schema reference", () => {
|
|
144
269
|
it("accepts variable with schema reference and valid defaultValue", () => {
|
|
145
270
|
expectParseSuccess(
|
|
@@ -174,7 +299,7 @@ describe("featureSchema.ts :: getFeatureZodSchema (variablesSchema and variable
|
|
|
174
299
|
const schemaWithEmptySchemasByKey = getFeatureZodSchema(
|
|
175
300
|
projectConfig,
|
|
176
301
|
conditionsZodSchema,
|
|
177
|
-
|
|
302
|
+
TEST_ATTRIBUTE_KEYS,
|
|
178
303
|
TEST_SEGMENTS,
|
|
179
304
|
TEST_FEATURES,
|
|
180
305
|
["link"],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// for use in node only
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
|
|
4
|
-
import type { Schema } from "@featurevisor/types";
|
|
4
|
+
import type { Attribute, Schema } from "@featurevisor/types";
|
|
5
5
|
import type { ZodError } from "zod";
|
|
6
6
|
|
|
7
7
|
import { getAttributeZodSchema } from "./attributeSchema";
|
|
@@ -59,6 +59,28 @@ async function getAuthorsOfEntity(datasource, entityType, entityKey): Promise<st
|
|
|
59
59
|
return authors;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
function collectExplicitAttributePaths(
|
|
63
|
+
attributeKey: string,
|
|
64
|
+
attribute: Attribute,
|
|
65
|
+
result: Set<string>,
|
|
66
|
+
): void {
|
|
67
|
+
result.add(attributeKey);
|
|
68
|
+
|
|
69
|
+
function visit(prefix: string, schema: Attribute | Attribute["properties"][string]) {
|
|
70
|
+
if (!schema || schema.type !== "object" || !schema.properties) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const propertyKey of Object.keys(schema.properties)) {
|
|
75
|
+
const propertyPath = `${prefix}.${propertyKey}`;
|
|
76
|
+
result.add(propertyPath);
|
|
77
|
+
visit(propertyPath, schema.properties[propertyKey]);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
visit(attributeKey, attribute);
|
|
82
|
+
}
|
|
83
|
+
|
|
62
84
|
export async function lintProject(
|
|
63
85
|
deps: Dependencies,
|
|
64
86
|
options: LintProjectOptions = {},
|
|
@@ -326,9 +348,29 @@ export async function lintProject(
|
|
|
326
348
|
log("");
|
|
327
349
|
}
|
|
328
350
|
|
|
351
|
+
// List schemas and load parsed schemas for attribute and feature validation.
|
|
352
|
+
const schemas = await datasource.listSchemas();
|
|
353
|
+
const schemasByKey: Record<string, Schema> = {};
|
|
354
|
+
for (const key of schemas) {
|
|
355
|
+
try {
|
|
356
|
+
schemasByKey[key] = await datasource.readSchema(key);
|
|
357
|
+
} catch {
|
|
358
|
+
// Schema file may be invalid; skip for cross-entity resolution and let schema linting report it.
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
329
362
|
// lint attributes
|
|
330
363
|
const attributes = await datasource.listAttributes();
|
|
331
364
|
const attributeZodSchema = getAttributeZodSchema();
|
|
365
|
+
const attributesByKey: Record<string, Attribute> = {};
|
|
366
|
+
|
|
367
|
+
for (const key of attributes) {
|
|
368
|
+
try {
|
|
369
|
+
attributesByKey[key] = await datasource.readAttribute(key);
|
|
370
|
+
} catch {
|
|
371
|
+
// Attribute read failures are reported during attribute linting below.
|
|
372
|
+
}
|
|
373
|
+
}
|
|
332
374
|
|
|
333
375
|
if (!options.entityType || options.entityType === "attribute") {
|
|
334
376
|
const filteredKeys = !keyPattern
|
|
@@ -354,7 +396,7 @@ export async function lintProject(
|
|
|
354
396
|
}
|
|
355
397
|
|
|
356
398
|
try {
|
|
357
|
-
const parsed = await datasource.readAttribute(key);
|
|
399
|
+
const parsed = attributesByKey[key] ?? (await datasource.readAttribute(key));
|
|
358
400
|
const result = attributeZodSchema.safeParse(parsed);
|
|
359
401
|
|
|
360
402
|
if (!result.success && "error" in result) {
|
|
@@ -366,14 +408,20 @@ export async function lintProject(
|
|
|
366
408
|
}
|
|
367
409
|
}
|
|
368
410
|
|
|
369
|
-
const
|
|
411
|
+
const explicitAttributePaths = new Set<string>();
|
|
412
|
+
for (const key of attributes) {
|
|
413
|
+
const parsed = attributesByKey[key];
|
|
414
|
+
if (!parsed) {
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
collectExplicitAttributePaths(key, parsed, explicitAttributePaths);
|
|
419
|
+
}
|
|
420
|
+
const flattenedAttributes = Array.from(explicitAttributePaths);
|
|
370
421
|
|
|
371
422
|
// lint segments
|
|
372
423
|
const segments = await datasource.listSegments();
|
|
373
|
-
const conditionsZodSchema = getConditionsZodSchema(
|
|
374
|
-
projectConfig,
|
|
375
|
-
flattenedAttributes as [string, ...string[]],
|
|
376
|
-
);
|
|
424
|
+
const conditionsZodSchema = getConditionsZodSchema(projectConfig, attributesByKey, schemasByKey);
|
|
377
425
|
const segmentZodSchema = getSegmentZodSchema(projectConfig, conditionsZodSchema);
|
|
378
426
|
|
|
379
427
|
if (!options.entityType || options.entityType === "segment") {
|
|
@@ -410,17 +458,6 @@ export async function lintProject(
|
|
|
410
458
|
}
|
|
411
459
|
}
|
|
412
460
|
|
|
413
|
-
// List schemas and load parsed schemas for feature variable validation (schema references)
|
|
414
|
-
const schemas = await datasource.listSchemas();
|
|
415
|
-
const schemasByKey: Record<string, Schema> = {};
|
|
416
|
-
for (const key of schemas) {
|
|
417
|
-
try {
|
|
418
|
-
schemasByKey[key] = await datasource.readSchema(key);
|
|
419
|
-
} catch {
|
|
420
|
-
// Schema file may be invalid; skip for feature variable resolution
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
461
|
// lint features
|
|
425
462
|
const features = await datasource.listFeatures();
|
|
426
463
|
const featureZodSchema = getFeatureZodSchema(
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
|
|
8
|
+
import type { Attribute } from "@featurevisor/types";
|
|
8
9
|
import type { ProjectConfig } from "../config";
|
|
9
10
|
import { getConditionsZodSchema } from "./conditionSchema";
|
|
10
11
|
import { getSegmentZodSchema } from "./segmentSchema";
|
|
@@ -36,7 +37,36 @@ function minimalProjectConfig(): ProjectConfig {
|
|
|
36
37
|
};
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
const TEST_ATTRIBUTES:
|
|
40
|
+
const TEST_ATTRIBUTES: Record<string, Attribute> = {
|
|
41
|
+
userId: {
|
|
42
|
+
description: "User ID",
|
|
43
|
+
type: "string",
|
|
44
|
+
},
|
|
45
|
+
country: {
|
|
46
|
+
description: "Country",
|
|
47
|
+
type: "string",
|
|
48
|
+
},
|
|
49
|
+
device: {
|
|
50
|
+
description: "Device",
|
|
51
|
+
type: "string",
|
|
52
|
+
},
|
|
53
|
+
permissions: {
|
|
54
|
+
description: "Permissions",
|
|
55
|
+
type: "array",
|
|
56
|
+
items: {
|
|
57
|
+
type: "string",
|
|
58
|
+
enum: ["read", "write", "admin"],
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
version: {
|
|
62
|
+
description: "Version",
|
|
63
|
+
oneOf: [{ type: "string" }, { type: "double" }],
|
|
64
|
+
},
|
|
65
|
+
traits: {
|
|
66
|
+
description: "Traits",
|
|
67
|
+
type: "object",
|
|
68
|
+
},
|
|
69
|
+
};
|
|
40
70
|
|
|
41
71
|
function getSegmentSchema() {
|
|
42
72
|
const projectConfig = minimalProjectConfig();
|
|
@@ -187,6 +217,57 @@ describe("segmentSchema.ts :: getSegmentZodSchema", () => {
|
|
|
187
217
|
"Unknown attribute",
|
|
188
218
|
);
|
|
189
219
|
});
|
|
220
|
+
|
|
221
|
+
it("accepts segment conditions using semver operators on oneOf attributes", () => {
|
|
222
|
+
expectSegmentSuccess({
|
|
223
|
+
description: "Version above v5",
|
|
224
|
+
conditions: {
|
|
225
|
+
and: [
|
|
226
|
+
{ attribute: "device", operator: "equals", value: "desktop" },
|
|
227
|
+
{ attribute: "version", operator: "semverGreaterThan", value: "5.0.0" },
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("accepts segment conditions using flat object nested paths", () => {
|
|
234
|
+
expectSegmentSuccess({
|
|
235
|
+
description: "Dark mode users",
|
|
236
|
+
conditions: {
|
|
237
|
+
attribute: "traits.favoriteTheme",
|
|
238
|
+
operator: "equals",
|
|
239
|
+
value: "dark",
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("rejects segment conditions when oneOf attribute value matches no branch", () => {
|
|
245
|
+
expectSegmentFailure(
|
|
246
|
+
{
|
|
247
|
+
description: "Bad version",
|
|
248
|
+
conditions: {
|
|
249
|
+
attribute: "version",
|
|
250
|
+
operator: "equals",
|
|
251
|
+
value: true,
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
'Value does not match the schema of attribute "version"',
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("rejects segment conditions when array item value violates enum constraints", () => {
|
|
259
|
+
expectSegmentFailure(
|
|
260
|
+
{
|
|
261
|
+
description: "Bad permission",
|
|
262
|
+
conditions: {
|
|
263
|
+
attribute: "permissions",
|
|
264
|
+
operator: "includes",
|
|
265
|
+
value: "delete",
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
'Value does not match the item schema of attribute "permissions"',
|
|
269
|
+
);
|
|
270
|
+
});
|
|
190
271
|
});
|
|
191
272
|
|
|
192
273
|
describe("optional archived", () => {
|