Browse Source

fix: circular schema validation

e22m4u 1 day ago
parent
commit
3067408f73
3 changed files with 129 additions and 19 deletions
  1. 22 2
      dist/cjs/index.cjs
  2. 32 2
      src/validate-projection-schema.js
  3. 75 15
      src/validate-projection-schema.spec.js

+ 22 - 2
dist/cjs/index.cjs

@@ -33,7 +33,22 @@ var import_js_format2 = require("@e22m4u/js-format");
 
 // src/validate-projection-schema.js
 var import_js_format = require("@e22m4u/js-format");
-function validateProjectionSchema(schema, shallowMode = false) {
+function validateProjectionSchema(schema, shallowMode = false, validatedSchemas = /* @__PURE__ */ new Set()) {
+  if (typeof shallowMode !== "boolean") {
+    throw new import_js_format.InvalidArgumentError(
+      'Argument "shallowMode" must be a Boolean, but %v was given.',
+      shallowMode
+    );
+  }
+  if (!(validatedSchemas instanceof Set)) {
+    throw new import_js_format.InvalidArgumentError(
+      'Argument "validatedSchemas" must be an instance of Set, but %v was given.',
+      validatedSchemas
+    );
+  }
+  if (validatedSchemas.has(schema)) {
+    return;
+  }
   if (!schema || typeof schema !== "object" && typeof schema !== "function" && typeof schema !== "string" || Array.isArray(schema)) {
     throw new import_js_format.InvalidArgumentError(
       "Projection schema must be an Object, a Function or a non-empty String, but %v was given.",
@@ -43,6 +58,7 @@ function validateProjectionSchema(schema, shallowMode = false) {
   if (typeof schema !== "object") {
     return;
   }
+  validatedSchemas.add(schema);
   Object.keys(schema).forEach((propName) => {
     const propOptions = schema[propName];
     if (propOptions === void 0) {
@@ -96,7 +112,11 @@ function validateProjectionSchema(schema, shallowMode = false) {
         );
       }
       if (!shallowMode && typeof propOptions.schema === "object") {
-        validateProjectionSchema(propOptions.schema, shallowMode);
+        validateProjectionSchema(
+          propOptions.schema,
+          shallowMode,
+          validatedSchemas
+        );
       }
     }
   });

+ 32 - 2
src/validate-projection-schema.js

@@ -5,8 +5,31 @@ import {InvalidArgumentError} from '@e22m4u/js-format';
  *
  * @param {object|Function|string} schema
  * @param {boolean} [shallowMode]
+ * @param {Set} [validatedSchemas]
  */
-export function validateProjectionSchema(schema, shallowMode = false) {
+export function validateProjectionSchema(
+  schema,
+  shallowMode = false,
+  validatedSchemas = new Set(),
+) {
+  if (typeof shallowMode !== 'boolean') {
+    throw new InvalidArgumentError(
+      'Argument "shallowMode" must be a Boolean, but %v was given.',
+      shallowMode,
+    );
+  }
+  if (!(validatedSchemas instanceof Set)) {
+    throw new InvalidArgumentError(
+      'Argument "validatedSchemas" must be ' +
+        'an instance of Set, but %v was given.',
+      validatedSchemas,
+    );
+  }
+  // если схема уже была проверена,
+  // то проверка пропускается
+  if (validatedSchemas.has(schema)) {
+    return;
+  }
   // schema
   if (
     !schema ||
@@ -24,6 +47,9 @@ export function validateProjectionSchema(schema, shallowMode = false) {
   if (typeof schema !== 'object') {
     return;
   }
+  // для исключения бесконечного цикла,
+  // текущая схема добавляется в историю
+  validatedSchemas.add(schema);
   // schema[k]
   Object.keys(schema).forEach(propName => {
     const propOptions = schema[propName];
@@ -109,7 +135,11 @@ export function validateProjectionSchema(schema, shallowMode = false) {
         );
       }
       if (!shallowMode && typeof propOptions.schema === 'object') {
-        validateProjectionSchema(propOptions.schema, shallowMode);
+        validateProjectionSchema(
+          propOptions.schema,
+          shallowMode,
+          validatedSchemas,
+        );
       }
     }
   });

+ 75 - 15
src/validate-projection-schema.spec.js

@@ -3,7 +3,53 @@ import {format} from '@e22m4u/js-format';
 import {validateProjectionSchema} from './validate-projection-schema.js';
 
 describe('validateProjectionSchema', function () {
-  it('should require the schema to be a valid value', function () {
+  it('should require the "shallowMode" argument to be a Boolean', function () {
+    const throwable = v => () => validateProjectionSchema({}, v);
+    const error = s =>
+      format('Argument "shallowMode" must be a Boolean, but %s was given.', s);
+    expect(throwable('str')).to.throw(error('"str"'));
+    expect(throwable('')).to.throw(error('""'));
+    expect(throwable(10)).to.throw(error('10'));
+    expect(throwable(0)).to.throw(error('0'));
+    expect(throwable([])).to.throw(error('Array'));
+    expect(throwable({})).to.throw(error('Object'));
+    expect(throwable(null)).to.throw(error('null'));
+    expect(throwable(() => undefined)).to.throw(error('Function'));
+    throwable(true)();
+    throwable(false)();
+    throwable(undefined)();
+  });
+
+  it('should require the "validatedSchemas" argument to be a Set instance', function () {
+    const throwable = v => () => validateProjectionSchema({}, false, v);
+    const error = s =>
+      format(
+        'Argument "validatedSchemas" must be ' +
+          'an instance of Set, but %s was given.',
+        s,
+      );
+    expect(throwable('str')).to.throw(error('"str"'));
+    expect(throwable('')).to.throw(error('""'));
+    expect(throwable(10)).to.throw(error('10'));
+    expect(throwable(0)).to.throw(error('0'));
+    expect(throwable(true)).to.throw(error('true'));
+    expect(throwable(false)).to.throw(error('false'));
+    expect(throwable([])).to.throw(error('Array'));
+    expect(throwable({})).to.throw(error('Object'));
+    expect(throwable(null)).to.throw(error('null'));
+    expect(throwable(() => undefined)).to.throw(error('Function'));
+    throwable(new Set())();
+    throwable(undefined)();
+  });
+
+  it('should skip validation if the given schema is already validated', function () {
+    const validatedSchemas = new Set();
+    const schema = {foo: 10};
+    validatedSchemas.add(schema);
+    validateProjectionSchema(schema, false, validatedSchemas);
+  });
+
+  it('should require the "schema" argument to be a valid value', function () {
     const throwable = v => () => validateProjectionSchema(v);
     const error = s =>
       format(
@@ -44,7 +90,7 @@ describe('validateProjectionSchema', function () {
     throwable(undefined)();
   });
 
-  it('should require the select option to be a boolean', function () {
+  it('should require the "select" option to be a boolean', function () {
     const throwable = v => () => validateProjectionSchema({foo: {select: v}});
     const error = s =>
       format(
@@ -64,7 +110,7 @@ describe('validateProjectionSchema', function () {
     throwable(undefined)();
   });
 
-  it('should require the scopes option to be an object', function () {
+  it('should require the "scopes" option to be an object', function () {
     const throwable = v => () => validateProjectionSchema({foo: {scopes: v}});
     const error = s =>
       format(
@@ -105,7 +151,7 @@ describe('validateProjectionSchema', function () {
     throwable(undefined)();
   });
 
-  it('should require the select option of scope to be a boolean', function () {
+  it('should require the "select" option of scope to be a boolean', function () {
     const throwable = v => () =>
       validateProjectionSchema({foo: {scopes: {input: {select: v}}}});
     const error = s =>
@@ -123,7 +169,7 @@ describe('validateProjectionSchema', function () {
     throwable(undefined)();
   });
 
-  it('should require the schema option to be a valid value', function () {
+  it('should require the "schema" option to be a valid value', function () {
     const throwable = v => () => validateProjectionSchema({foo: {schema: v}});
     const error = s =>
       format(
@@ -152,16 +198,30 @@ describe('validateProjectionSchema', function () {
     );
   });
 
-  describe('shallowMode', function () {
-    it('should validate the given schema', function () {
-      const throwable = () => validateProjectionSchema({foo: 10}, true);
-      expect(throwable).to.throw(
-        'Property options must be an Object or a Boolean, but 10 was given.',
-      );
-    });
+  it('should validate the given schema in the shallow mode', function () {
+    const throwable = () => validateProjectionSchema({foo: 10}, true);
+    expect(throwable).to.throw(
+      'Property options must be an Object or a Boolean, but 10 was given.',
+    );
+  });
+
+  it('should skip embedded schema validation in the shallow mode', function () {
+    validateProjectionSchema({foo: {schema: {bar: 10}}}, true);
+  });
+
+  it('should allow circular schema validation', function () {
+    const schemaA = {foo: {select: true}};
+    const schemaB = {bar: {select: true}};
+    schemaA.foo.schema = schemaB;
+    schemaB.bar.schema = schemaA;
+    validateProjectionSchema(schemaA);
+  });
 
-    it('should skip embedded schema validation', function () {
-      validateProjectionSchema({foo: {schema: {bar: 10}}}, true);
-    });
+  it('should allow circular schema validation in the shallow mode', function () {
+    const schemaA = {foo: {select: true}};
+    const schemaB = {bar: {select: true}};
+    schemaA.foo.schema = schemaB;
+    schemaB.bar.schema = schemaA;
+    validateProjectionSchema(schemaA, true);
   });
 });