Просмотр исходного кода

feat: allows symbols as schema name

e22m4u 2 дней назад
Родитель
Сommit
d87af6d4b7

+ 8 - 8
README.md

@@ -3,7 +3,7 @@
 JavaScript модуль для работы с проекцией данных.
 
 Модуль использует декларативные схемы для определения правил видимости полей
-данных. Поддерживается вложенность, функции-фабрики, именованных схемы, области
+данных. Поддерживается вложенность, функции-фабрики, именованные схемы, области
 проекции и строгий режим.
 
 ## Содержание
@@ -40,7 +40,7 @@ npm install @e22m4u/js-data-projection
 Сигнатура:
 
 - `projectData(schemaOrFactory, data, [options])`: возвращает проекцию;
-  - `schemaOrFactory: object | Function | string`: схема, фабрика или имя схемы;
+  - `schemaOrFactory: object | Function | string | symbol`: схема, фабрика или имя схемы;
   - `data: object | object[]`: проектируемые данные;
   - `options?: object`: объект настроек;
     - `strict?: boolean`: строгий режим;
@@ -307,11 +307,11 @@ console.log(result);
 import {projectData} from '@e22m4u/js-data-projection';
 
 // функция для разрешения имен
-const resolver = (name) => {
-  if (name === 'user') {
+const resolver = key => {
+  if (key === 'user') {
     return {id: true, name: true, password: false};
   }
-  throw new Error(`Schema "${name}" is not found!`);
+  throw new Error(`Schema "${key}" is not found!`);
 };
 
 const data = {
@@ -347,11 +347,11 @@ const userSchema = {
 }
 
 // функция для разрешения имен
-const resolver = (name) => {
-  if (name === 'address') {
+const resolver = key => {
+  if (key === 'address') {
     return {city: true, zip: false};
   }
-  throw new Error(`Schema "${name}" is not found!`);
+  throw new Error(`Schema "${key}" is not found!`);
 };
 
 const data = {

+ 26 - 22
dist/cjs/index.cjs

@@ -66,9 +66,9 @@ function validateProjectionSchema(schema, shallowMode = false) {
       );
     }
     if (options.schema !== void 0) {
-      if (!options.schema || typeof options.schema !== "object" && typeof options.schema !== "function" && typeof options.schema !== "string" || Array.isArray(options.schema)) {
+      if (!options.schema || typeof options.schema !== "object" && typeof options.schema !== "function" && typeof options.schema !== "string" && typeof options.schema !== "symbol" || Array.isArray(options.schema)) {
         throw new import_js_format.InvalidArgumentError(
-          "Embedded schema must be an Object, a Function or a non-empty String, but %v was given.",
+          "Embedded schema must be an Object, a Function, a non-empty String or a Symbol, but %v was given.",
           options.schema
         );
       }
@@ -112,10 +112,10 @@ function validateProjectionSchema(schema, shallowMode = false) {
 __name(validateProjectionSchema, "validateProjectionSchema");
 
 // src/project-data.js
-function projectData(schemaOrFactory, data, options = void 0) {
-  if (!schemaOrFactory || typeof schemaOrFactory !== "object" && typeof schemaOrFactory !== "function" && typeof schemaOrFactory !== "string" || Array.isArray(schemaOrFactory)) {
+function projectData(schemaOrFactory, data, options) {
+  if (!schemaOrFactory || typeof schemaOrFactory !== "object" && typeof schemaOrFactory !== "function" && typeof schemaOrFactory !== "string" && typeof schemaOrFactory !== "symbol" || Array.isArray(schemaOrFactory)) {
     throw new import_js_format2.InvalidArgumentError(
-      "Projection schema must be an Object, a Function or a non-empty String, but %v was given.",
+      "Projection schema must be an Object, a Function, a non-empty String or a Symbol, but %v was given.",
       schemaOrFactory
     );
   }
@@ -147,25 +147,25 @@ function projectData(schemaOrFactory, data, options = void 0) {
   }
   const strict = Boolean(options && options.strict);
   const scope = options && options.scope || void 0;
-  let schemaOrName = schemaOrFactory;
+  let schemaOrKey = schemaOrFactory;
   if (typeof schemaOrFactory === "function") {
-    schemaOrName = schemaOrFactory();
-    if (!schemaOrName || typeof schemaOrName !== "object" && typeof schemaOrName !== "string" || Array.isArray(schemaOrName)) {
+    schemaOrKey = schemaOrFactory();
+    if (!schemaOrKey || typeof schemaOrKey !== "object" && typeof schemaOrKey !== "string" && typeof schemaOrKey !== "symbol" || Array.isArray(schemaOrKey)) {
       throw new import_js_format2.InvalidArgumentError(
-        "Projection schema factory must return an Object or a non-empty String, but %v was given.",
-        schemaOrName
+        "Projection schema factory must return an Object, a non-empty String or a Symbol, but %v was given.",
+        schemaOrKey
       );
     }
   }
-  let schema = schemaOrName;
-  if (schemaOrName && typeof schemaOrName === "string") {
+  let schema = schemaOrKey;
+  if (schemaOrKey && (typeof schemaOrKey === "string" || typeof schemaOrKey === "symbol")) {
     if (!options || !options.resolver) {
       throw new import_js_format2.InvalidArgumentError(
         "Unable to resolve the projection schema %v without a provided resolver.",
-        schemaOrName
+        schemaOrKey
       );
     }
-    schema = options.resolver(schemaOrName);
+    schema = options.resolver(schemaOrKey);
     if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
       throw new import_js_format2.InvalidArgumentError(
         "Projection schema resolver must return an Object, but %v was given.",
@@ -181,21 +181,25 @@ function projectData(schemaOrFactory, data, options = void 0) {
     return data.map((item) => projectData(schema, item, options));
   }
   const result = {};
-  const keys = Object.keys(strict ? schema : data);
-  for (const key of keys) {
-    if (!(key in data)) {
+  const fields = Object.keys(strict ? schema : data);
+  for (const field of fields) {
+    if (!(field in data)) {
       continue;
     }
-    if (!Object.prototype.hasOwnProperty.call(data, key)) {
+    if (!Object.prototype.hasOwnProperty.call(data, field)) {
       continue;
     }
-    const propOptionsOrBoolean = schema[key];
+    const propOptionsOrBoolean = schema[field];
     if (_shouldSelect(propOptionsOrBoolean, strict, scope)) {
-      const value = data[key];
+      const value = data[field];
       if (propOptionsOrBoolean && typeof propOptionsOrBoolean === "object" && propOptionsOrBoolean.schema) {
-        result[key] = projectData(propOptionsOrBoolean.schema, value, options);
+        result[field] = projectData(
+          propOptionsOrBoolean.schema,
+          value,
+          options
+        );
       } else {
-        result[key] = value;
+        result[field] = value;
       }
     }
   }

+ 1 - 1
package.json

@@ -38,7 +38,7 @@
     "prepare": "husky"
   },
   "dependencies": {
-    "@e22m4u/js-format": "~0.3.1"
+    "@e22m4u/js-format": "~0.3.2"
   },
   "devDependencies": {
     "@commitlint/cli": "~20.2.0",

+ 5 - 3
src/project-data.d.ts

@@ -1,13 +1,15 @@
 import {
   ProjectionSchema,
-  ProjectionSchemaName,
+  ProjectionSchemaKey,
   ProjectionSchemaFactory,
 } from './projection-schema.js';
 
 /**
  * Projection schema resolver.
  */
-export type ProjectionSchemaResolver = (schemaName: string) => ProjectionSchema;
+export type ProjectionSchemaResolver = (
+  schemaKey: ProjectionSchemaKey,
+) => ProjectionSchema;
 
 /**
  * Project data options.
@@ -29,7 +31,7 @@ export declare function projectData<T>(
   schemaOrFactory:
     | ProjectionSchema
     | ProjectionSchemaFactory
-    | ProjectionSchemaName,
+    | ProjectionSchemaKey,
   data: T,
   options?: ProjectDataOptions,
 ): T;

+ 41 - 31
src/project-data.js

@@ -6,21 +6,22 @@ import {validateProjectionSchema} from './validate-projection-schema.js';
  *
  * @param {object|Function|string} schemaOrFactory
  * @param {object} data
- * @param {object|undefined} options
+ * @param {object} [options]
  * @returns {*}
  */
-export function projectData(schemaOrFactory, data, options = undefined) {
+export function projectData(schemaOrFactory, data, options) {
   // schemaOrFactory
   if (
     !schemaOrFactory ||
     (typeof schemaOrFactory !== 'object' &&
       typeof schemaOrFactory !== 'function' &&
-      typeof schemaOrFactory !== 'string') ||
+      typeof schemaOrFactory !== 'string' &&
+      typeof schemaOrFactory !== 'symbol') ||
     Array.isArray(schemaOrFactory)
   ) {
     throw new InvalidArgumentError(
-      'Projection schema must be an Object, a Function ' +
-        'or a non-empty String, but %v was given.',
+      'Projection schema must be an Object, a Function, ' +
+        'a non-empty String or a Symbol, but %v was given.',
       schemaOrFactory,
     );
   }
@@ -64,37 +65,42 @@ export function projectData(schemaOrFactory, data, options = undefined) {
   const scope = (options && options.scope) || undefined;
   // если вместо схемы передана фабрика,
   // то извлекается фабричное значение
-  let schemaOrName = schemaOrFactory;
+  let schemaOrKey = schemaOrFactory;
   if (typeof schemaOrFactory === 'function') {
-    schemaOrName = schemaOrFactory();
-    // если фабричное значение не является строкой
-    // или объектом, то выбрасывается ошибка
+    schemaOrKey = schemaOrFactory();
+    // если фабричное значение не является объектом,
+    // строкой или символом, то выбрасывается ошибка
     if (
-      !schemaOrName ||
-      (typeof schemaOrName !== 'object' && typeof schemaOrName !== 'string') ||
-      Array.isArray(schemaOrName)
+      !schemaOrKey ||
+      (typeof schemaOrKey !== 'object' &&
+        typeof schemaOrKey !== 'string' &&
+        typeof schemaOrKey !== 'symbol') ||
+      Array.isArray(schemaOrKey)
     ) {
       throw new InvalidArgumentError(
-        'Projection schema factory must return an Object ' +
-          'or a non-empty String, but %v was given.',
-        schemaOrName,
+        'Projection schema factory must return an Object, ' +
+          'a non-empty String or a Symbol, but %v was given.',
+        schemaOrKey,
       );
     }
   }
-  // если вместо схемы передана строка,
-  // то строка используется как название схемы
-  let schema = schemaOrName;
-  if (schemaOrName && typeof schemaOrName === 'string') {
-    // если функция разрешения схемы не определена,
+  // если вместо схемы передана строка или символ,
+  // то значение передается в разрешающую функцию
+  let schema = schemaOrKey;
+  if (
+    schemaOrKey &&
+    (typeof schemaOrKey === 'string' || typeof schemaOrKey === 'symbol')
+  ) {
+    // если разрешающая функция не определена,
     // то выбрасывается ошибка
     if (!options || !options.resolver) {
       throw new InvalidArgumentError(
         'Unable to resolve the projection schema %v ' +
           'without a provided resolver.',
-        schemaOrName,
+        schemaOrKey,
       );
     }
-    schema = options.resolver(schemaOrName);
+    schema = options.resolver(schemaOrKey);
     // если не удалось извлечь схему проекции,
     // то выбрасывается ошибка
     if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
@@ -121,25 +127,25 @@ export function projectData(schemaOrFactory, data, options = undefined) {
   // то создается проекция согласно схеме
   const result = {};
   // в обычном режиме итерация выполняется по ключам исходного
-  // объекта, а в строгом режиме по ключам, описанным в схеме
+  // объекта, но в строгом режиме по ключам, описанным в схеме
   // (исключая ключи прототипа Object.keys(x))
-  const keys = Object.keys(strict ? schema : data);
-  for (const key of keys) {
+  const fields = Object.keys(strict ? schema : data);
+  for (const field of fields) {
     // если свойство отсутствует в исходных
     // данных, то свойство игнорируется
-    if (!(key in data)) {
+    if (!(field in data)) {
       continue;
     }
     // если свойство принадлежит прототипу,
     // то свойство игнорируется
-    if (!Object.prototype.hasOwnProperty.call(data, key)) {
+    if (!Object.prototype.hasOwnProperty.call(data, field)) {
       continue;
     }
-    const propOptionsOrBoolean = schema[key];
+    const propOptionsOrBoolean = schema[field];
     // проверка доступности свойства для данной
     // области проекции (если определена)
     if (_shouldSelect(propOptionsOrBoolean, strict, scope)) {
-      const value = data[key];
+      const value = data[field];
       // если определена вложенная схема,
       // то проекция применяется рекурсивно
       if (
@@ -147,12 +153,16 @@ export function projectData(schemaOrFactory, data, options = undefined) {
         typeof propOptionsOrBoolean === 'object' &&
         propOptionsOrBoolean.schema
       ) {
-        result[key] = projectData(propOptionsOrBoolean.schema, value, options);
+        result[field] = projectData(
+          propOptionsOrBoolean.schema,
+          value,
+          options,
+        );
       }
       // иначе значение присваивается
       // свойству без изменений
       else {
-        result[key] = value;
+        result[field] = value;
       }
     }
   }

+ 89 - 15
src/project-data.spec.js

@@ -3,12 +3,12 @@ import {format} from '@e22m4u/js-format';
 import {projectData} from './project-data.js';
 
 describe('projectData', function () {
-  it('should require the parameter "schemaOrSource" to be a valid value', function () {
+  it('should require the parameter "schemaOrFactory" to be a valid value', function () {
     const throwable = v => () => projectData(v, {});
     const error = s =>
       format(
-        'Projection schema must be an Object, a Function ' +
-          'or a non-empty String, but %s was given.',
+        'Projection schema must be an Object, a Function, ' +
+          'a non-empty String or a Symbol, but %s was given.',
         s,
       );
     expect(throwable('')).to.throw(error('""'));
@@ -152,8 +152,8 @@ describe('projectData', function () {
       const throwable = v => () => projectData(() => v, {});
       const error = s =>
         format(
-          'Projection schema factory must return an Object ' +
-            'or a non-empty String, but %s was given.',
+          'Projection schema factory must return an Object, ' +
+            'a non-empty String or a Symbol, but %s was given.',
           s,
         );
       expect(throwable('')).to.throw(error('""'));
@@ -166,6 +166,7 @@ describe('projectData', function () {
       expect(throwable(undefined)).to.throw(error('undefined'));
       projectData(() => ({}), {});
       projectData(() => 'mySchema', {}, {resolver: () => ({})});
+      projectData(() => Symbol('mySchema'), {}, {resolver: () => ({})});
     });
 
     it('should resolve a schema object from the given factory', function () {
@@ -194,7 +195,7 @@ describe('projectData', function () {
     });
   });
 
-  describe('named schema', function () {
+  describe('string key', function () {
     it('should throw an error if the schema resolver returns an invalid value', function () {
       const throwable = v => () =>
         projectData('mySchema', {}, {resolver: () => v});
@@ -215,7 +216,7 @@ describe('projectData', function () {
       throwable({})();
     });
 
-    it('should throw an error if no schema resolver is provided when a schema name is given', function () {
+    it('should throw an error if no schema resolver is provided when a string key is given', function () {
       const throwable = () => projectData('mySchema', {});
       expect(throwable).to.throw(
         'Unable to resolve the projection schema "mySchema" ' +
@@ -223,10 +224,10 @@ describe('projectData', function () {
       );
     });
 
-    it('should pass the schema name to the schema resolver and project the given data', function () {
+    it('should pass the string key to the schema resolver and project the given data', function () {
       let invoked = 0;
-      const resolver = name => {
-        expect(name).to.be.eq('mySchema');
+      const resolver = key => {
+        expect(key).to.be.eq('mySchema');
         invoked++;
         return {foo: true, bar: false};
       };
@@ -237,8 +238,8 @@ describe('projectData', function () {
 
     it('should use the schema resolver in the nested schema', function () {
       let invoked = 0;
-      const resolver = name => {
-        expect(name).to.be.eq('mySchema');
+      const resolver = key => {
+        expect(key).to.be.eq('mySchema');
         invoked++;
         return {baz: true, qux: false};
       };
@@ -251,10 +252,10 @@ describe('projectData', function () {
       expect(invoked).to.be.eq(1);
     });
 
-    it('should resolve the schema name from the schema factory', function () {
+    it('should resolve the string key from the schema factory', function () {
       let invoked = 0;
-      const resolver = name => {
-        expect(name).to.be.eq('mySchema');
+      const resolver = key => {
+        expect(key).to.be.eq('mySchema');
         invoked++;
         return {foo: true, bar: false};
       };
@@ -264,6 +265,79 @@ describe('projectData', function () {
     });
   });
 
+  describe('symbol key', function () {
+    it('should throw an error if the schema resolver returns an invalid value', function () {
+      const throwable = v => () =>
+        projectData(Symbol('mySchema'), {}, {resolver: () => v});
+      const error = s =>
+        format(
+          'Projection schema resolver must return an Object, 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(null)).to.throw(error('null'));
+      expect(throwable(undefined)).to.throw(error('undefined'));
+      throwable({})();
+    });
+
+    it('should throw an error if no schema resolver is provided when a symbol key is given', function () {
+      const throwable = () => projectData(Symbol('mySchema'), {});
+      expect(throwable).to.throw(
+        'Unable to resolve the projection schema Symbol("mySchema") ' +
+          'without a provided resolver.',
+      );
+    });
+
+    it('should pass the symbol key to the schema resolver and project the given data', function () {
+      let invoked = 0;
+      const symbolKey = Symbol('mySchema');
+      const resolver = key => {
+        expect(key).to.be.eq(symbolKey);
+        invoked++;
+        return {foo: true, bar: false};
+      };
+      const res = projectData(symbolKey, {foo: 10, bar: 20}, {resolver});
+      expect(res).to.be.eql({foo: 10});
+      expect(invoked).to.be.eq(1);
+    });
+
+    it('should use the schema resolver in the nested schema', function () {
+      let invoked = 0;
+      const symbolKey = Symbol('mySchema');
+      const resolver = key => {
+        expect(key).to.be.eq(symbolKey);
+        invoked++;
+        return {baz: true, qux: false};
+      };
+      const res = projectData(
+        {foo: true, bar: {schema: symbolKey}},
+        {foo: 10, bar: {baz: 20, qux: 30}},
+        {resolver},
+      );
+      expect(res).to.be.eql({foo: 10, bar: {baz: 20}});
+      expect(invoked).to.be.eq(1);
+    });
+
+    it('should resolve the symbol key from the schema factory', function () {
+      let invoked = 0;
+      const symbolKey = Symbol('mySchema');
+      const resolver = key => {
+        expect(key).to.be.eq(symbolKey);
+        invoked++;
+        return {foo: true, bar: false};
+      };
+      const res = projectData(() => symbolKey, {foo: 10, bar: 20}, {resolver});
+      expect(res).to.be.eql({foo: 10});
+      expect(invoked).to.be.eq(1);
+    });
+  });
+
   describe('strict mode', function () {
     it('should preserve fields not defined in the schema when the strict option is false', function () {
       const res = projectData({}, {foo: 10});

+ 5 - 4
src/projection-schema.d.ts

@@ -1,3 +1,4 @@
+
 /**
  * Projection schema.
  */
@@ -6,16 +7,16 @@ export type ProjectionSchema = {
 };
 
 /**
- * Projection schema name.
+ * Projection schema key.
  */
-export type ProjectionSchemaName = string;
+export type ProjectionSchemaKey = string | symbol;
 
 /**
  * Projection schema factory.
  */
 export type ProjectionSchemaFactory = () =>
   | ProjectionSchema
-  | ProjectionSchemaName;
+  | ProjectionSchemaKey;
 
 /**
  * Projection schema property options.
@@ -23,7 +24,7 @@ export type ProjectionSchemaFactory = () =>
 export type ProjectionSchemaPropertyOptions = {
   select?: boolean;
   scopes?: ProjectionSchemaScopes;
-  schema?: ProjectionSchema | ProjectionSchemaFactory | ProjectionSchemaName;
+  schema?: ProjectionSchema | ProjectionSchemaFactory | ProjectionSchemaKey;
 };
 
 /**

+ 4 - 3
src/validate-projection-schema.js

@@ -54,12 +54,13 @@ export function validateProjectionSchema(schema, shallowMode = false) {
         !options.schema ||
         (typeof options.schema !== 'object' &&
           typeof options.schema !== 'function' &&
-          typeof options.schema !== 'string') ||
+          typeof options.schema !== 'string' &&
+          typeof options.schema !== 'symbol') ||
         Array.isArray(options.schema)
       ) {
         throw new InvalidArgumentError(
-          'Embedded schema must be an Object, a Function ' +
-            'or a non-empty String, but %v was given.',
+          'Embedded schema must be an Object, a Function, ' +
+            'a non-empty String or a Symbol, but %v was given.',
           options.schema,
         );
       }

+ 3 - 2
src/validate-projection-schema.spec.js

@@ -77,8 +77,8 @@ describe('validateProjectionSchema', function () {
     const throwable = v => () => validateProjectionSchema({foo: {schema: v}});
     const error = s =>
       format(
-        'Embedded schema must be an Object, a Function ' +
-          'or a non-empty String, but %s was given.',
+        'Embedded schema must be an Object, a Function, ' +
+          'a non-empty String or a Symbol, but %s was given.',
         s,
       );
     expect(throwable('')).to.throw(error('""'));
@@ -91,6 +91,7 @@ describe('validateProjectionSchema', function () {
     throwable('mySchema')();
     throwable({})();
     throwable(() => ({}))();
+    throwable(Symbol('mySchema'))();
     throwable(undefined)();
   });