Browse Source

chore: initial commit

e22m4u 2 days ago
parent
commit
e4736e35fd

+ 41 - 135
README.md

@@ -9,6 +9,7 @@ JavaScript модуль для работы с проекцией данных.
 ## Содержание
 
 - [Установка](#установка)
+- [Схема проекции](#схема-проекции)
 - [Использование](#использование)
   - [Создание проекции](#создание-проекции)
   - [Проекция массива](#проекция-массива)
@@ -17,10 +18,6 @@ JavaScript модуль для работы с проекцией данных.
   - [Область проекции](#область-проекции)
   - [Фабричные функции](#фабричные-функции)
   - [Именованные схемы](#именованные-схемы)
-- [Функция `projectData`](#функция-projectdata)
-- [Класс `DataProjector`](#класс-dataprojector)
-  - [Метод `defineSchema`](#метод-defineschema)
-  - [Метод `project`](#метод-project)
 - [Тесты](#тесты)
 - [Лицензия](#лицензия)
 
@@ -30,6 +27,46 @@ JavaScript модуль для работы с проекцией данных.
 npm install @e22m4u/js-data-projection
 ```
 
+Модуль поддерживает ESM и CommonJS стандарты.
+
+*ESM*
+
+```js
+import {projectData} from '@e22m4u/js-data-projection';
+```
+
+*CommonJS*
+
+```js
+const {projectData} = require('@e22m4u/js-data-projection');
+```
+
+## Схема проекции
+
+Определение правил видимости полей.
+
+```js
+{
+  foo: true, // поле доступно
+  bar: false // поле скрыто
+}
+```
+
+Определение вложенной схемы.
+
+```js
+{
+  name: true, // поле name доступно
+  address: {  // настройки поля address
+    select: true, // поле address доступно
+    schema: {     // вложенная схема
+      city: true,     // поле city доступно
+      zip: false      // поле zip скрыто
+    }
+  }
+}
+```
+
 ## Использование
 
 Модуль экспортирует функцию `projectData` для создания проекций
@@ -403,137 +440,6 @@ console.log(result);
 // }
 ```
 
-## Функция `projectData`
-
-Функция создает проекцию данных на основе переданного объекта схемы.
-Принимает исходные данные и дополнительные настройки для управления
-режимом строгости и областью видимости.
-
-**Сигнатура вызова:**
-
-```js
-projectData(schemaOrFactory, data, [options])
-```
-
-**schemaOrFactory**  
-
-*Тип: `object` | `Function` | `string`*
-
-Определяет правила проекции. Принимает:
-- `object`: объект схемы проекции;
-- `Function`: фабрика, возвращающая схему (или имя схемы);
-- `string`: имя для поиска схемы через функцию `resolver`;
-
-**data**  
-
-*Тип: `object` | `object[]` | `any`*
-
-Исходные данные для проекции.
-
-**options** (необязательно)  
-
-*Тип: `object`*
-
-Объект с дополнительными настройками:
-
-- `strict?: boolean`: включает строгий режим;
-- `scope?: string`: имя активной области проекции;
-- `resolver?: Function`: функция для получения схемы по имени;
-
-**Возвращаемое значение:**
-
-Функция возвращает новую структуру, соответствующую исходным данным. Если
-передан объект, результатом будет его копия, содержащая свойства, разрешенные
-правилами схемы с учетом вложенности, строгого режима и активной области
-проекции.
-
-Для массивов возвращается новый список, к каждому элементу которого рекурсивно
-применена схема проекции. Примитивные значения возвращаются без изменений.
-
-## Класс `DataProjector`
-
-Для централизованного управления схемами и удобной работы в рамках
-сервис-ориентированной архитектуры модуль предоставляет класс `DataProjector`.
-Он позволяет регистрировать именованные схемы в реестре и применять их по имени
-без необходимости вручную передавать функцию `resolver` при каждом вызове.
-
-Создание экземпляра:
-
-```js
-import {DataProjector} from '@e22m4u/js-data-projection';
-
-const projector = new DataProjector();
-```
-
-### Метод `defineSchema`
-
-Регистрирует новую схему в реестре проектора. Метод принимает объект
-определения, содержащий уникальное имя схемы и правила проекции. Возвращает
-текущий экземпляр `DataProjector`, что позволяет использовать цепочки вызовов.
-
-**Сигнатура вызова:**
-
-```js
-defineSchema(schemaDef)
-```
-
-**schemaDef**
-
-*Тип: `{name: string, schema: object}`*
-
-Объект определения схемы, связывающий название схемы с правилами проекции.
-
-- `name: string`: название новой схемы;
-- `schema: object`: объект схемы проекции;
-
-**Возвращаемое значение:**
-
-Текущий экземпляр `DataProjector`.
-
-### Метод `project`
-
-Выполняет проекцию данных, используя возможности внутреннего реестра
-схем. Метод работает аналогично функции `projectData`, но автоматически
-предоставляет функционал разрешения имен, связанный с добавленными ранее
-схемами. Метод позволяет передавать в качестве первого аргумента строковое
-имя зарегистрированной схемы без необходимости указывать функцию `resolver`
-в настройках проекции.
-
-**Сигнатура вызова:**
-
-```js
-project(schemaOrFactory, data, [options])
-```
-
-**schemaOrFactory**  
-
-*Тип: `object` | `Function` | `string`*
-
-Определяет правила проекции. Принимает:
-- `object`: объект схемы проекции;
-- `Function`: фабрика, возвращающая схему (или имя схемы);
-- `string`: зарегистрированное имя схемы;
-
-**data**  
-
-*Тип: `object` | `object[]` | `any`*
-
-Исходные данные для проекции.
-
-**options** (необязательно)  
-
-*Тип: `object`*
-
-Объект с дополнительными настройками:
-
-- `strict?: boolean`: включает строгий режим;
-- `scope?: string`: имя активной области проекции;
-
-**Возвращаемое значение:**
-
-Метод возвращает проекцию исходных данных в соответствии с переданной схемой
-и настройками.
-
 ## Тесты
 
 ```bash

+ 98 - 145
dist/cjs/index.cjs

@@ -1,4 +1,3 @@
-"use strict";
 var __defProp = Object.defineProperty;
 var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
 var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -23,7 +22,6 @@ var index_exports = {};
 __export(index_exports, {
   DataProjector: () => DataProjector,
   ProjectionSchemaRegistry: () => ProjectionSchemaRegistry,
-  ProjectionScope: () => ProjectionScope,
   projectData: () => projectData,
   validateProjectionSchema: () => validateProjectionSchema,
   validateProjectionSchemaDefinition: () => validateProjectionSchemaDefinition
@@ -36,196 +34,169 @@ 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) {
-  if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
+  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, but %v was given.",
+      "Projection schema must be an Object, a Function or a non-empty String, but %v was given.",
       schema
     );
   }
-  if (typeof shallowMode !== "boolean") {
-    throw new import_js_format.InvalidArgumentError(
-      'Parameter "shallowMode" must be a Boolean, but %v was given.',
-      shallowMode
-    );
+  if (typeof schema !== "object") {
+    return;
   }
-  Object.keys(schema).forEach((fieldName) => {
-    const options = schema[fieldName];
-    if (options === void 0) {
+  Object.keys(schema).forEach((propName) => {
+    const propOptions = schema[propName];
+    if (propOptions === void 0) {
       return;
     }
-    if (options === null || typeof options !== "boolean" && typeof options !== "object" || Array.isArray(options)) {
+    if (propOptions === null || typeof propOptions !== "object" && typeof propOptions !== "boolean" || Array.isArray(propOptions)) {
       throw new import_js_format.InvalidArgumentError(
-        "Field options must be a Boolean or an Object, but %v was given.",
-        options
+        "Property options must be an Object or a Boolean, but %v was given.",
+        propOptions
       );
     }
-    if (typeof options === "boolean") {
+    if (typeof propOptions === "boolean") {
       return;
     }
-    if (options.select !== void 0 && typeof options.select !== "boolean") {
+    if (propOptions.select !== void 0 && typeof propOptions.select !== "boolean") {
       throw new import_js_format.InvalidArgumentError(
-        'Field option "select" must be a Boolean, but %v was given.',
-        options.select
+        'Property option "select" must be a Boolean, but %v was given.',
+        propOptions.select
       );
     }
-    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 (propOptions.scopes !== void 0) {
+      if (!propOptions.scopes || typeof propOptions.scopes !== "object" || Array.isArray(propOptions.scopes)) {
         throw new import_js_format.InvalidArgumentError(
-          "Embedded schema must be an Object, a Function or a non-empty String, but %v was given.",
-          options.schema
+          'Property option "scopes" must be an Object, but %v was given.',
+          propOptions.scopes
         );
       }
-      if (!shallowMode && typeof options.schema === "object") {
-        validateProjectionSchema(options.schema, shallowMode);
-      }
-    }
-    if (options.scopes !== void 0) {
-      if (!options.scopes || typeof options.scopes !== "object" || Array.isArray(options.scopes)) {
-        throw new import_js_format.InvalidArgumentError(
-          'Field option "scopes" must be an Object, but %v was given.',
-          options.scopes
-        );
-      }
-      Object.keys(options.scopes).forEach((scopeName) => {
-        const scopeOptions = options.scopes[scopeName];
+      Object.values(propOptions.scopes).forEach((scopeOptions) => {
         if (scopeOptions === void 0) {
           return;
         }
-        if (scopeOptions === null || typeof scopeOptions !== "boolean" && typeof scopeOptions !== "object" || Array.isArray(scopeOptions)) {
+        if (scopeOptions === null || typeof scopeOptions !== "object" && typeof scopeOptions !== "boolean" || Array.isArray(scopeOptions)) {
           throw new import_js_format.InvalidArgumentError(
-            "Scope options must be a Boolean or an Object, but %v was given.",
+            "Scope options must be an Object or a Boolean, but %v was given.",
             scopeOptions
           );
         }
-        if (typeof scopeOptions === "boolean") {
-          return;
-        }
-        if (scopeOptions.select !== void 0) {
-          if (typeof scopeOptions.select !== "boolean") {
-            throw new import_js_format.InvalidArgumentError(
-              'Scope option "select" must be a Boolean, but %v was given.',
-              scopeOptions.select
-            );
-          }
+        if (scopeOptions.select !== void 0 && typeof scopeOptions.select !== "boolean") {
+          throw new import_js_format.InvalidArgumentError(
+            'Scope option "select" must be a Boolean, but %v was given.',
+            scopeOptions.select
+          );
         }
       });
     }
+    if (propOptions.schema !== void 0) {
+      if (!propOptions.schema || typeof propOptions.schema !== "object" && typeof propOptions.schema !== "function" && typeof propOptions.schema !== "string" || Array.isArray(propOptions.schema)) {
+        throw new import_js_format.InvalidArgumentError(
+          'Property option "schema" must be an Object, a Function or a non-empty String, but %v was given.',
+          propOptions.schema
+        );
+      }
+      if (!shallowMode && typeof propOptions.schema === "object") {
+        validateProjectionSchema(propOptions.schema, shallowMode);
+      }
+    }
   });
 }
 __name(validateProjectionSchema, "validateProjectionSchema");
 
 // src/project-data.js
-function projectData(schemaOrFactory, data, options) {
-  if (!schemaOrFactory || typeof schemaOrFactory !== "object" && typeof schemaOrFactory !== "function" && typeof schemaOrFactory !== "string" || 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.",
-      schemaOrFactory
-    );
-  }
+function projectData(schema, data, options) {
   if (options !== void 0) {
     if (!options || typeof options !== "object" || Array.isArray(options)) {
       throw new import_js_format2.InvalidArgumentError(
-        'Parameter "options" must be an Object, but %v was given.',
+        "Projection options must be an Object, but %v was given.",
         options
       );
     }
     if (options.strict !== void 0 && typeof options.strict !== "boolean") {
       throw new import_js_format2.InvalidArgumentError(
-        'Option "strict" must be a Boolean, but %v was given.',
+        'Projection option "strict" must be a Boolean, but %v was given.',
         options.strict
       );
     }
-    if (options.scope !== void 0 && (!options.scope || typeof options.scope !== "string")) {
+    if (options.scope !== void 0 && (options.scope === "" || typeof options.scope !== "string")) {
       throw new import_js_format2.InvalidArgumentError(
-        'Option "scope" must be a non-empty String, but %v was given.',
+        'Projection option "scope" must be a non-empty String, but %v was given.',
         options.scope
       );
     }
     if (options.resolver !== void 0 && typeof options.resolver !== "function") {
       throw new import_js_format2.InvalidArgumentError(
-        'Option "resolver" must be a Function, but %v was given.',
+        'Projection option "resolver" must be a Function, but %v was given.',
         options.resolver
       );
     }
   }
-  const strict = Boolean(options && options.strict);
-  const scope = options && options.scope || void 0;
-  let schemaOrName = schemaOrFactory;
-  if (typeof schemaOrFactory === "function") {
-    schemaOrName = schemaOrFactory();
-    if (!schemaOrName || typeof schemaOrName !== "object" && typeof schemaOrName !== "string" || Array.isArray(schemaOrName)) {
+  if (typeof schema === "function") {
+    schema = schema();
+    if (!schema || typeof schema !== "object" && typeof schema !== "string" || Array.isArray(schema)) {
       throw new import_js_format2.InvalidArgumentError(
-        "Projection schema factory must return an Object or a non-empty String, but %v was given.",
-        schemaOrName
+        "Schema factory must return an Object or a non-empty String, but %v was given.",
+        schema
       );
     }
   }
-  let schema = schemaOrName;
-  if (schemaOrName && typeof schemaOrName === "string") {
+  if (typeof schema === "string") {
     if (!options || !options.resolver) {
       throw new import_js_format2.InvalidArgumentError(
-        "Unable to resolve the projection schema %v without a provided resolver.",
-        schemaOrName
+        'Projection option "resolver" is required to resolve %v schema.',
+        schema
       );
     }
-    schema = options.resolver(schemaOrName);
+    schema = options.resolver(schema);
     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.",
+        "Schema resolver must return an Object, but %v was given.",
         schema
       );
     }
   }
   validateProjectionSchema(schema, true);
-  if (data === null || typeof data !== "object") {
+  if (data == null || typeof data !== "object") {
     return data;
   }
   if (Array.isArray(data)) {
     return data.map((item) => projectData(schema, item, options));
   }
   const result = {};
-  const fields = Object.keys(strict ? schema : data);
-  for (const field of fields) {
-    if (!(field in data)) {
-      continue;
-    }
-    if (!Object.prototype.hasOwnProperty.call(data, field)) {
-      continue;
-    }
-    const fieldOptionsOrBoolean = schema[field];
-    if (_shouldSelect(fieldOptionsOrBoolean, strict, scope)) {
-      const value = data[field];
-      if (fieldOptionsOrBoolean && typeof fieldOptionsOrBoolean === "object" && fieldOptionsOrBoolean.schema) {
-        result[field] = projectData(
-          fieldOptionsOrBoolean.schema,
-          value,
-          options
-        );
+  const strict = Boolean(options && options.strict);
+  const scope = options && options.scope || void 0;
+  const propNames = Object.keys(strict ? schema : data);
+  propNames.forEach((propName) => {
+    if (!(propName in data)) return;
+    const propOptions = schema[propName];
+    if (_shouldSelect(propOptions, strict, scope)) {
+      const value = data[propName];
+      if (propOptions && typeof propOptions === "object" && propOptions.schema) {
+        result[propName] = projectData(propOptions.schema, value, options);
       } else {
-        result[field] = value;
+        result[propName] = value;
       }
     }
-  }
+  });
   return result;
 }
 __name(projectData, "projectData");
-function _shouldSelect(fieldOptionsOrBoolean, strict, scope) {
-  if (typeof fieldOptionsOrBoolean === "boolean") {
-    return fieldOptionsOrBoolean;
+function _shouldSelect(propOptions, strict, scope) {
+  if (typeof propOptions === "boolean") {
+    return propOptions;
   }
-  if (typeof fieldOptionsOrBoolean === "object") {
-    const fieldOptions = fieldOptionsOrBoolean;
-    if (scope && fieldOptions.scopes && typeof fieldOptions.scopes === "object" && fieldOptions.scopes[scope] != null) {
-      const scopeOptionsOrBoolean = fieldOptions.scopes[scope];
-      if (typeof scopeOptionsOrBoolean === "boolean") {
-        return scopeOptionsOrBoolean;
+  if (propOptions && typeof propOptions === "object" && !Array.isArray(propOptions)) {
+    if (scope && typeof scope === "string" && propOptions.scopes && typeof propOptions.scopes === "object" && propOptions.scopes[scope] !== void 0) {
+      const scopeOptions = propOptions.scopes[scope];
+      if (typeof scopeOptions === "boolean") {
+        return scopeOptions;
       }
-      if (scopeOptionsOrBoolean && typeof scopeOptionsOrBoolean === "object" && typeof scopeOptionsOrBoolean.select === "boolean") {
-        return scopeOptionsOrBoolean.select;
+      if (scopeOptions && typeof scopeOptions === "object" && !Array.isArray(scopeOptions) && typeof scopeOptions.select === "boolean") {
+        return scopeOptions.select;
       }
     }
-    if (typeof fieldOptionsOrBoolean.select === "boolean") {
-      return fieldOptionsOrBoolean.select;
+    if (typeof propOptions.select === "boolean") {
+      return propOptions.select;
     }
   }
   return !strict;
@@ -235,28 +206,28 @@ __name(_shouldSelect, "_shouldSelect");
 // src/data-projector.js
 var import_js_service2 = require("@e22m4u/js-service");
 
-// src/definitions/projection-schema-registry.js
+// src/projection-schema-registry.js
 var import_js_service = require("@e22m4u/js-service");
 var import_js_format4 = require("@e22m4u/js-format");
 
-// src/definitions/validate-projection-schema-definition.js
+// src/validate-projection-schema-definition.js
 var import_js_format3 = require("@e22m4u/js-format");
 function validateProjectionSchemaDefinition(schemaDef) {
   if (!schemaDef || typeof schemaDef !== "object" || Array.isArray(schemaDef)) {
     throw new import_js_format3.InvalidArgumentError(
-      "Projection schema definition must be an Object, but %v was given.",
+      "Schema definition must be an Object, but %v was given.",
       schemaDef
     );
   }
   if (!schemaDef.name || typeof schemaDef.name !== "string") {
     throw new import_js_format3.InvalidArgumentError(
-      "Projection schema name must be a non-empty String, but %v was given.",
+      'Definition option "name" must be a non-empty String, but %v was given.',
       schemaDef.name
     );
   }
   if (!schemaDef.schema || typeof schemaDef.schema !== "object" || Array.isArray(schemaDef.schema)) {
     throw new import_js_format3.InvalidArgumentError(
-      "Projection schema must be an Object, but %v was given.",
+      'Definition option "schema" must be an Object, but %v was given.',
       schemaDef.schema
     );
   }
@@ -264,14 +235,12 @@ function validateProjectionSchemaDefinition(schemaDef) {
 }
 __name(validateProjectionSchemaDefinition, "validateProjectionSchemaDefinition");
 
-// src/definitions/projection-schema-registry.js
+// src/projection-schema-registry.js
 var _ProjectionSchemaRegistry = class _ProjectionSchemaRegistry extends import_js_service.Service {
   /**
-   * Schema map.
-   *
-   * @type {Map<string, object>}
+   * Definitions.
    */
-  definitions = /* @__PURE__ */ new Map();
+  _definitions = /* @__PURE__ */ new Map();
   /**
    * Define schema.
    *
@@ -280,13 +249,13 @@ var _ProjectionSchemaRegistry = class _ProjectionSchemaRegistry extends import_j
    */
   defineSchema(schemaDef) {
     validateProjectionSchemaDefinition(schemaDef);
-    if (this.definitions.has(schemaDef.name)) {
+    if (this._definitions.has(schemaDef.name)) {
       throw new import_js_format4.InvalidArgumentError(
         "Projection schema %v is already registered.",
         schemaDef.name
       );
     }
-    this.definitions.set(schemaDef.name, schemaDef);
+    this._definitions.set(schemaDef.name, schemaDef);
     return this;
   }
   /**
@@ -296,7 +265,7 @@ var _ProjectionSchemaRegistry = class _ProjectionSchemaRegistry extends import_j
    * @returns {boolean}
    */
   hasSchema(schemaName) {
-    return this.definitions.has(schemaName);
+    return this._definitions.has(schemaName);
   }
   /**
    * Get schema.
@@ -305,7 +274,7 @@ var _ProjectionSchemaRegistry = class _ProjectionSchemaRegistry extends import_j
    * @returns {object}
    */
   getSchema(schemaName) {
-    const schemaDef = this.definitions.get(schemaName);
+    const schemaDef = this._definitions.get(schemaName);
     if (!schemaDef) {
       throw new import_js_format4.InvalidArgumentError(
         "Projection schema %v is not found.",
@@ -321,10 +290,10 @@ var _ProjectionSchemaRegistry = class _ProjectionSchemaRegistry extends import_j
    * @returns {object}
    */
   getDefinition(schemaName) {
-    const schemaDef = this.definitions.get(schemaName);
+    const schemaDef = this._definitions.get(schemaName);
     if (!schemaDef) {
       throw new import_js_format4.InvalidArgumentError(
-        "Projection definition %v is not found.",
+        "Schema definition %v is not found.",
         schemaName
       );
     }
@@ -346,44 +315,28 @@ var _DataProjector = class _DataProjector extends import_js_service2.Service {
     this.getService(ProjectionSchemaRegistry).defineSchema(schemaDef);
     return this;
   }
-  /**
-   * Has schema.
-   *
-   * @param {string} schemaName
-   * @returns {boolean}
-   */
-  hasSchema(schemaName) {
-    return this.getService(ProjectionSchemaRegistry).hasSchema(schemaName);
-  }
   /**
    * Project.
    *
-   * @param {object|Function|string} schemaOrFactory
-   * @param {*} data
+   * @param {object|Function|string} schema
+   * @param {object|object[]|*} data
    * @param {object} [options]
    * @returns {*}
    */
-  project(schemaOrFactory, data, options) {
+  project(schema, data, options) {
     const registry = this.getService(ProjectionSchemaRegistry);
-    const resolver = /* @__PURE__ */ __name((schemaName) => {
-      return registry.getSchema(schemaName);
+    const resolver = /* @__PURE__ */ __name((name) => {
+      return registry.getSchema(name);
     }, "resolver");
-    return projectData(schemaOrFactory, data, { ...options, resolver });
+    return projectData(schema, data, { ...options, resolver });
   }
 };
 __name(_DataProjector, "DataProjector");
 var DataProjector = _DataProjector;
-
-// src/projection-scope.js
-var ProjectionScope = {
-  INPUT: "input",
-  OUTPUT: "output"
-};
 // Annotate the CommonJS export names for ESM import in node:
 0 && (module.exports = {
   DataProjector,
   ProjectionSchemaRegistry,
-  ProjectionScope,
   projectData,
   validateProjectionSchema,
   validateProjectionSchemaDefinition

+ 7 - 0
jsconfig.json

@@ -0,0 +1,7 @@
+{
+  "compilerOptions": {
+    "target": "es2022",
+    "module": "NodeNext",
+    "moduleResolution": "NodeNext"
+  }
+}

+ 5 - 9
package.json

@@ -8,8 +8,7 @@
     "data",
     "input",
     "output",
-    "projection",
-    "filtering"
+    "projection"
   ],
   "homepage": "https://gitrepos.ru/e22m4u/js-data-projection",
   "repository": {
@@ -17,11 +16,9 @@
     "url": "git+https://gitrepos.ru/e22m4u/js-data-projection.git"
   },
   "type": "module",
-  "types": "./src/index.d.ts",
   "module": "./src/index.js",
   "main": "./dist/cjs/index.cjs",
   "exports": {
-    "types": "./src/index.d.ts",
     "import": "./src/index.js",
     "require": "./dist/cjs/index.cjs"
   },
@@ -29,8 +26,8 @@
     "node": ">=12"
   },
   "scripts": {
-    "lint": "tsc && eslint ./src",
-    "lint:fix": "tsc && eslint ./src --fix",
+    "lint": "eslint ./src",
+    "lint:fix": "eslint ./src --fix",
     "format": "prettier --write \"./src/**/*.js\"",
     "test": "npm run lint && c8 --reporter=text-summary mocha --bail",
     "test:coverage": "npm run lint && c8 --reporter=text mocha --bail",
@@ -39,7 +36,7 @@
   },
   "dependencies": {
     "@e22m4u/js-format": "~0.3.2",
-    "@e22m4u/js-service": "^0.5.1"
+    "@e22m4u/js-service": "~0.5.1"
   },
   "devDependencies": {
     "@commitlint/cli": "~20.2.0",
@@ -60,7 +57,6 @@
     "husky": "~9.1.7",
     "mocha": "~11.7.5",
     "prettier": "~3.7.4",
-    "rimraf": "~6.1.2",
-    "typescript": "~5.9.3"
+    "rimraf": "~6.1.2"
   }
 }

+ 0 - 44
src/data-projector.d.ts

@@ -1,44 +0,0 @@
-import {Service} from '@e22m4u/js-service';
-import {ProjectDataOptions} from './project-data.js';
-import {ProjectionSchemaDefinition} from './definitions/index.js';
-
-import {
-  ProjectionSchema,
-  ProjectionSchemaName,
-  ProjectionSchemaFactory,
-} from './projection-schema.js';
-
-/**
- * Data projector.
- */
-export class DataProjector extends Service {
-  /**
-   * Define schema.
-   *
-   * @param schemaDef
-   */
-  defineSchema(schemaDef: ProjectionSchemaDefinition): this;
-
-  /**
-   * Has schema.
-   * 
-   * @param schemaName
-   */
-  hasSchema(schemaName: ProjectionSchemaName): boolean;
-
-  /**
-   * Project.
-   *
-   * @param schemaOrFactory
-   * @param data
-   * @param options
-   */
-  project<T>(
-    schemaOrFactory:
-      | ProjectionSchema
-      | ProjectionSchemaFactory
-      | ProjectionSchemaName,
-    data: T,
-    options?: Omit<ProjectDataOptions, 'resolver'>,
-  ): T;
-}

+ 7 - 17
src/data-projector.js

@@ -1,6 +1,6 @@
 import {Service} from '@e22m4u/js-service';
 import {projectData} from './project-data.js';
-import {ProjectionSchemaRegistry} from './definitions/index.js';
+import {ProjectionSchemaRegistry} from './projection-schema-registry.js';
 
 /**
  * Data projector.
@@ -17,29 +17,19 @@ export class DataProjector extends Service {
     return this;
   }
 
-  /**
-   * Has schema.
-   *
-   * @param {string} schemaName
-   * @returns {boolean}
-   */
-  hasSchema(schemaName) {
-    return this.getService(ProjectionSchemaRegistry).hasSchema(schemaName);
-  }
-
   /**
    * Project.
    *
-   * @param {object|Function|string} schemaOrFactory
-   * @param {*} data
+   * @param {object|Function|string} schema
+   * @param {object|object[]|*} data
    * @param {object} [options]
    * @returns {*}
    */
-  project(schemaOrFactory, data, options) {
+  project(schema, data, options) {
     const registry = this.getService(ProjectionSchemaRegistry);
-    const resolver = schemaName => {
-      return registry.getSchema(schemaName);
+    const resolver = name => {
+      return registry.getSchema(name);
     };
-    return projectData(schemaOrFactory, data, {...options, resolver});
+    return projectData(schema, data, {...options, resolver});
   }
 }

+ 38 - 40
src/data-projector.spec.js

@@ -1,69 +1,67 @@
 import {expect} from 'chai';
-import {format} from '@e22m4u/js-format';
 import {DataProjector} from './data-projector.js';
+import {ProjectionSchemaRegistry} from './projection-schema-registry.js';
 
 describe('DataProjector', function () {
   describe('defineSchema', function () {
-    it('should register the given projection schema', function () {
+    it('should validate the given definition', function () {
       const S = new DataProjector();
-      const name = 'mySchema';
-      const schema = {foo: true, bar: false};
-      expect(S.hasSchema(name)).to.be.false;
-      S.defineSchema({name, schema});
-      expect(S.hasSchema(name)).to.be.true;
+      const throwable = () => S.defineSchema({});
+      expect(throwable).to.throw(
+        'Definition option "name" must be a non-empty String, ' +
+          'but undefined was given.',
+      );
     });
 
-    it('should throw an error if the given name is already registered', function () {
+    it('should register the given definition', function () {
+      const def = {name: 'mySchema', schema: {}};
       const S = new DataProjector();
-      const name = 'mySchema';
-      S.defineSchema({name, schema: {}});
-      const throwable = () => S.defineSchema({name, schema: {}});
-      const error = format('Projection schema %v is already registered.', name);
-      expect(throwable).to.throw(error);
+      S.defineSchema(def);
+      const registry = S.getService(ProjectionSchemaRegistry);
+      const res = registry.getDefinition(def.name);
+      expect(res).to.be.eql(def);
+    });
+
+    it('should throw an error if the schema name is already registered', function () {
+      const S = new DataProjector();
+      const def = {name: 'mySchema', schema: {}};
+      S.defineSchema(def);
+      const throwable = () => S.defineSchema(def);
+      expect(throwable).to.throw(
+        'Projection schema "mySchema" is already registered.',
+      );
     });
-  });
 
-  describe('hasSchema', function () {
-    it('should return true if the given name is registered', function () {
+    it('should return the current instance', function () {
       const S = new DataProjector();
-      const name = 'mySchema';
-      const schema = {foo: true, bar: false};
-      const res1 = S.hasSchema(name);
-      expect(res1).to.be.false;
-      S.defineSchema({name, schema});
-      const res2 = S.hasSchema(name);
-      expect(res2).to.be.true;
+      const res = S.defineSchema({name: 'mySchema', schema: {}});
+      expect(res).to.be.eq(S);
     });
   });
 
   describe('project', function () {
-    it('should project the given data by the registered schema name', function () {
+    it('should project the data object by the given schema', function () {
       const S = new DataProjector();
-      const name = 'mySchema';
-      const schema = {foo: true, bar: false};
-      S.defineSchema({name, schema});
-      const res = S.project(name, {foo: 10, bar: 20});
+      const res = S.project({foo: true, bar: false}, {foo: 10, bar: 20});
       expect(res).to.be.eql({foo: 10});
     });
 
-    it('should enable the strict mode by the strict option', function () {
+    it('should project the data object by the schema name', function () {
       const S = new DataProjector();
+      S.defineSchema({name: 'mySchema', schema: {foo: true, bar: false}});
+      const res = S.project('mySchema', {foo: 10, bar: 20, baz: 30});
+      expect(res).to.be.eql({foo: 10, baz: 30});
+    });
+
+    it('should exclude properties without rules in the strict mode', function () {
+      const S = new DataProjector();
+      S.defineSchema({name: 'mySchema', schema: {foo: true, bar: false}});
       const res = S.project(
-        {foo: true, bar: false},
+        'mySchema',
         {foo: 10, bar: 20, baz: 30},
         {strict: true},
       );
       expect(res).to.be.eql({foo: 10});
     });
-
-    it('should project the given data by the scope option', function () {
-      const S = new DataProjector();
-      const data = {foo: 10, bar: 20};
-      const schema = {foo: true, bar: {scopes: {input: true, output: false}}};
-      const res1 = S.project(schema, data, {scope: 'input'});
-      const res2 = S.project(schema, data, {scope: 'output'});
-      expect(res1).to.be.eql({foo: 10, bar: 20});
-      expect(res2).to.be.eql({foo: 10});
-    });
   });
 });

+ 0 - 3
src/definitions/index.d.ts

@@ -1,3 +0,0 @@
-export * from './projection-schema-registry.js';
-export * from './projection-schema-definition.js';
-export * from './validate-projection-schema-definition.js';

+ 0 - 3
src/definitions/index.js

@@ -1,3 +0,0 @@
-export * from './projection-schema-registry.js';
-export * from './projection-schema-definition.js';
-export * from './validate-projection-schema-definition.js';

+ 0 - 9
src/definitions/projection-schema-definition.d.ts

@@ -1,9 +0,0 @@
-import {ProjectionSchema, ProjectionSchemaName} from '../projection-schema.js';
-
-/**
- * Projection schema definition.
- */
-export type ProjectionSchemaDefinition = {
-  name: ProjectionSchemaName;
-  schema: ProjectionSchema;
-};

+ 0 - 1
src/definitions/projection-schema-definition.js

@@ -1 +0,0 @@
-export {};

+ 0 - 41
src/definitions/projection-schema-registry.d.ts

@@ -1,41 +0,0 @@
-import {Service} from '@e22m4u/js-service';
-import {ProjectionSchemaDefinition} from './projection-schema-definition.js';
-import {ProjectionSchema, ProjectionSchemaName} from '../projection-schema.js';
-
-/**
- * Projection schema registry.
- */
-export class ProjectionSchemaRegistry extends Service {
-  /**
-   * Definitions.
-   */
-  protected definitions: Map<ProjectionSchemaName, ProjectionSchemaDefinition>;
-
-  /**
-   * Define schema.
-   *
-   * @param schemaDef
-   */
-  defineSchema(schemaDef: ProjectionSchemaDefinition): this;
-
-  /**
-   * Has schema.
-   *
-   * @param schemaName
-   */
-  hasSchema(schemaName: ProjectionSchemaName): boolean;
-
-  /**
-   * Get schema.
-   *
-   * @param schemaName
-   */
-  getSchema(schemaName: ProjectionSchemaName): ProjectionSchema;
-
-  /**
-   * Get definition.
-   *
-   * @param schemaName
-   */
-  getDefinition(schemaName: ProjectionSchemaName): ProjectionSchemaDefinition;
-}

+ 0 - 75
src/definitions/projection-schema-registry.spec.js

@@ -1,75 +0,0 @@
-import {expect} from 'chai';
-import {format} from '@e22m4u/js-format';
-import {ProjectionSchemaRegistry} from './projection-schema-registry.js';
-
-const SCHEMA = {foo: true, bar: false};
-const SCHEMA_NAME = 'mySchema';
-const SCHEMA_DEF = {name: SCHEMA_NAME, schema: SCHEMA};
-
-describe('ProjectionSchemaRegistry', function () {
-  describe('defineSchema', function () {
-    it('should add the given definition to registry', function () {
-      const S = new ProjectionSchemaRegistry();
-      S.defineSchema(SCHEMA_DEF);
-      const res = S.getDefinition(SCHEMA_NAME);
-      expect(res).to.be.eq(SCHEMA_DEF);
-    });
-
-    it('should throw an error if the given name is already registered', function () {
-      const S = new ProjectionSchemaRegistry();
-      S.defineSchema(SCHEMA_DEF);
-      const throwable = () => S.defineSchema(SCHEMA_DEF);
-      const error = format(
-        'Projection schema %v is already registered.',
-        SCHEMA_NAME,
-      );
-      expect(throwable).to.throw(error);
-    });
-  });
-
-  describe('hasSchema', function () {
-    it('should return true if the given name is registered', function () {
-      const S = new ProjectionSchemaRegistry();
-      const res1 = S.hasSchema(SCHEMA_NAME);
-      expect(res1).to.be.false;
-      S.defineSchema(SCHEMA_DEF);
-      const res2 = S.hasSchema(SCHEMA_NAME);
-      expect(res2).to.be.true;
-    });
-  });
-
-  describe('getSchema', function () {
-    it('should throw an error if the given name is not registered', function () {
-      const S = new ProjectionSchemaRegistry();
-      const throwable = () => S.getSchema(SCHEMA_NAME);
-      const error = format('Projection schema %v is not found.', SCHEMA_NAME);
-      expect(throwable).to.throw(error);
-    });
-
-    it('should return registered schema', function () {
-      const S = new ProjectionSchemaRegistry();
-      S.defineSchema(SCHEMA_DEF);
-      const res = S.getSchema(SCHEMA_NAME);
-      expect(res).to.be.eq(SCHEMA);
-    });
-  });
-
-  describe('getDefinition', function () {
-    it('should throw an error if the given name is not registered', function () {
-      const S = new ProjectionSchemaRegistry();
-      const throwable = () => S.getDefinition(SCHEMA_NAME);
-      const error = format(
-        'Projection definition %v is not found.',
-        SCHEMA_NAME,
-      );
-      expect(throwable).to.throw(error);
-    });
-
-    it('should return registered schema', function () {
-      const S = new ProjectionSchemaRegistry();
-      S.defineSchema(SCHEMA_DEF);
-      const res = S.getDefinition(SCHEMA_NAME);
-      expect(res).to.be.eq(SCHEMA_DEF);
-    });
-  });
-});

+ 0 - 10
src/definitions/validate-projection-schema-definition.d.ts

@@ -1,10 +0,0 @@
-import {ProjectionSchemaDefinition} from './projection-schema-definition.js';
-
-/**
- * Validate projection schema definition.
- *
- * @param def
- */
-export function validateProjectionSchemaDefinition(
-  schemaDef: ProjectionSchemaDefinition,
-): void;

+ 0 - 6
src/index.d.ts

@@ -1,6 +0,0 @@
-export * from './project-data.js';
-export * from './data-projector.js';
-export * from './projection-scope.js';
-export * from './projection-schema.js';
-export * from './definitions/index.js';
-export * from './validate-projection-schema.js';

+ 2 - 3
src/index.js

@@ -1,6 +1,5 @@
 export * from './project-data.js';
 export * from './data-projector.js';
-export * from './projection-scope.js';
-export * from './projection-schema.js';
-export * from './definitions/index.js';
+export * from './projection-schema-registry.js';
 export * from './validate-projection-schema.js';
+export * from './validate-projection-schema-definition.js';

+ 0 - 37
src/project-data.d.ts

@@ -1,37 +0,0 @@
-import {
-  ProjectionSchema,
-  ProjectionSchemaName,
-  ProjectionSchemaFactory,
-} from './projection-schema.js';
-
-/**
- * Projection schema resolver.
- */
-export type ProjectionSchemaResolver = (
-  schemaName: ProjectionSchemaName,
-) => ProjectionSchema;
-
-/**
- * Project data options.
- */
-export type ProjectDataOptions = {
-  strict?: boolean;
-  scope?: string;
-  resolver?: ProjectionSchemaResolver;
-};
-
-/**
- * Project data.
- *
- * @param schemaOrSource
- * @param data
- * @param options
- */
-export declare function projectData<T>(
-  schemaOrFactory:
-    | ProjectionSchema
-    | ProjectionSchemaFactory
-    | ProjectionSchemaName,
-  data: T,
-  options?: ProjectDataOptions,
-): T;

+ 95 - 111
src/project-data.js

@@ -4,48 +4,35 @@ import {validateProjectionSchema} from './validate-projection-schema.js';
 /**
  * Project data.
  *
- * @param {object|Function|string} schemaOrFactory
- * @param {*} data
+ * @param {object|Function|string} schema
+ * @param {object|object[]|*} data
  * @param {object} [options]
  * @returns {*}
  */
-export function projectData(schemaOrFactory, data, options) {
-  // schemaOrFactory
-  if (
-    !schemaOrFactory ||
-    (typeof schemaOrFactory !== 'object' &&
-      typeof schemaOrFactory !== 'function' &&
-      typeof schemaOrFactory !== 'string') ||
-    Array.isArray(schemaOrFactory)
-  ) {
-    throw new InvalidArgumentError(
-      'Projection schema must be an Object, a Function ' +
-        'or a non-empty String, but %v was given.',
-      schemaOrFactory,
-    );
-  }
+export function projectData(schema, data, options) {
   // options
   if (options !== undefined) {
     if (!options || typeof options !== 'object' || Array.isArray(options)) {
       throw new InvalidArgumentError(
-        'Parameter "options" must be an Object, but %v was given.',
+        'Projection options must be an Object, but %v was given.',
         options,
       );
     }
     // options.strict
     if (options.strict !== undefined && typeof options.strict !== 'boolean') {
       throw new InvalidArgumentError(
-        'Option "strict" must be a Boolean, but %v was given.',
+        'Projection option "strict" must be a Boolean, but %v was given.',
         options.strict,
       );
     }
     // options.scope
     if (
       options.scope !== undefined &&
-      (!options.scope || typeof options.scope !== 'string')
+      (options.scope === '' || typeof options.scope !== 'string')
     ) {
       throw new InvalidArgumentError(
-        'Option "scope" must be a non-empty String, but %v was given.',
+        'Projection option "scope" must be a non-empty String, ' +
+          'but %v was given.',
         options.scope,
       );
     }
@@ -55,166 +42,163 @@ export function projectData(schemaOrFactory, data, options) {
       typeof options.resolver !== 'function'
     ) {
       throw new InvalidArgumentError(
-        'Option "resolver" must be a Function, but %v was given.',
+        'Projection option "resolver" must be a Function, ' +
+          'but %v was given.',
         options.resolver,
       );
     }
   }
-  const strict = Boolean(options && options.strict);
-  const scope = (options && options.scope) || undefined;
-  // если вместо схемы передана фабрика,
+  // если схема является фабрикой,
   // то извлекается фабричное значение
-  let schemaOrName = schemaOrFactory;
-  if (typeof schemaOrFactory === 'function') {
-    schemaOrName = schemaOrFactory();
-    // если фабричное значение не является объектом
-    // или строкой, то выбрасывается ошибка
+  if (typeof schema === 'function') {
+    schema = schema();
     if (
-      !schemaOrName ||
-      (typeof schemaOrName !== 'object' && typeof schemaOrName !== 'string') ||
-      Array.isArray(schemaOrName)
+      !schema ||
+      (typeof schema !== 'object' && typeof schema !== 'string') ||
+      Array.isArray(schema)
     ) {
       throw new InvalidArgumentError(
-        'Projection schema factory must return an Object ' +
+        'Schema factory must return an Object ' +
           'or a non-empty String, but %v was given.',
-        schemaOrName,
+        schema,
       );
     }
   }
-  // если вместо схемы передана строка,
-  // то строка передается в разрешающую функцию
-  let schema = schemaOrName;
-  if (schemaOrName && typeof schemaOrName === 'string') {
+  // если схема является строкой,
+  // то выполняется разрешение имени
+  if (typeof schema === 'string') {
     // если разрешающая функция не определена,
     // то выбрасывается ошибка
     if (!options || !options.resolver) {
       throw new InvalidArgumentError(
-        'Unable to resolve the projection schema %v ' +
-          'without a provided resolver.',
-        schemaOrName,
+        'Projection option "resolver" is required to resolve %v schema.',
+        schema,
       );
     }
-    schema = options.resolver(schemaOrName);
-    // если не удалось извлечь схему проекции,
-    // то выбрасывается ошибка
+    schema = options.resolver(schema);
+    // если результат разрешающей функции не является
+    // объектом, то выбрасывается ошибка
     if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
       throw new InvalidArgumentError(
-        'Projection schema resolver must return an Object, but %v was given.',
+        'Schema resolver must return an Object, but %v was given.',
         schema,
       );
     }
   }
-  // валидация полученной схемы проекции
-  // без проверки вложенных схем (shallowMode)
+  // после нормализации схемы в объект,
+  // выполняется поверхностная проверка
   validateProjectionSchema(schema, true);
-  // если данные не являются объектом (null, undefined, примитив),
+  // если данные не являются объектом или массивом,
   // то значение возвращается без изменений
-  if (data === null || typeof data !== 'object') {
+  if (data == null || typeof data !== 'object') {
     return data;
   }
-  // если данные являются массивом, то проекция
-  // применяется к каждому элементу
+  // если данные являются массивом,
+  // то схема применяется к каждому элементу
   if (Array.isArray(data)) {
     return data.map(item => projectData(schema, item, options));
   }
   // если данные являются объектом,
-  // то создается проекция согласно схеме
+  // то проекция создается согласно схеме
   const result = {};
+  const strict = Boolean(options && options.strict);
+  const scope = (options && options.scope) || undefined;
   // в обычном режиме итерация выполняется по ключам исходного
-  // объекта, но в строгом режиме по ключам, описанным в схеме
+  // объекта, а в строгом режиме по ключам, описанным в схеме
   // (исключая ключи прототипа Object.keys(x))
-  const fields = Object.keys(strict ? schema : data);
-  for (const field of fields) {
-    // если поле отсутствует в исходных
-    // данных, то поле игнорируется
-    if (!(field in data)) {
-      continue;
-    }
-    // если поле принадлежит прототипу,
-    // то поле игнорируется
-    if (!Object.prototype.hasOwnProperty.call(data, field)) {
-      continue;
-    }
-    const fieldOptionsOrBoolean = schema[field];
-    // проверка доступности поля для данной
+  const propNames = Object.keys(strict ? schema : data);
+  propNames.forEach(propName => {
+    // если свойство отсутствует в исходных
+    // данных, то свойство игнорируется
+    if (!(propName in data)) return;
+    const propOptions = schema[propName];
+    // проверка доступности свойства для данной
     // области проекции (если определена)
-    if (_shouldSelect(fieldOptionsOrBoolean, strict, scope)) {
-      const value = data[field];
+    if (_shouldSelect(propOptions, strict, scope)) {
+      const value = data[propName];
       // если определена вложенная схема,
       // то проекция применяется рекурсивно
       if (
-        fieldOptionsOrBoolean &&
-        typeof fieldOptionsOrBoolean === 'object' &&
-        fieldOptionsOrBoolean.schema
+        propOptions &&
+        typeof propOptions === 'object' &&
+        propOptions.schema
       ) {
-        result[field] = projectData(
-          fieldOptionsOrBoolean.schema,
-          value,
-          options,
-        );
+        result[propName] = projectData(propOptions.schema, value, options);
       }
       // иначе значение присваивается
-      // полю без изменений
+      // свойству без изменений
       else {
-        result[field] = value;
+        result[propName] = value;
       }
     }
-  }
+  });
   return result;
 }
 
 /**
  * Should select (internal).
  *
- * Определяет, следует ли включать поле в результат.
- * Приоритет: правило для области -> общее правило -> по умолчанию true.
+ * Определяет, следует ли включать свойство в результат.
+ *
+ * Приоритет:
+ *   1. Правило для области.
+ *   2. Общее правило.
+ *   4. Режим проекции.
  *
- * @param {object|boolean|undefined} fieldOptionsOrBoolean
+ * @param {object|boolean} propOptions
  * @param {boolean|undefined} strict
  * @param {string|undefined} scope
  * @returns {boolean}
  */
-function _shouldSelect(fieldOptionsOrBoolean, strict, scope) {
-  // если настройки поля являются логическим значением,
+function _shouldSelect(propOptions, strict, scope) {
+  // если настройки свойства являются логическим значением,
   // то значение используется как индикатор видимости
-  if (typeof fieldOptionsOrBoolean === 'boolean') {
-    return fieldOptionsOrBoolean;
+  if (typeof propOptions === 'boolean') {
+    return propOptions;
   }
-  // если настройки поля являются объектом,
+  // если настройки свойства являются объектом,
   // то проверяется правило области и общее правило
-  if (typeof fieldOptionsOrBoolean === 'object') {
-    const fieldOptions = fieldOptionsOrBoolean;
+  if (
+    propOptions &&
+    typeof propOptions === 'object' &&
+    !Array.isArray(propOptions)
+  ) {
     // если определена область проекции,
     // то выполняется проверка правила области
     if (
       scope &&
-      fieldOptions.scopes &&
-      typeof fieldOptions.scopes === 'object' &&
-      fieldOptions.scopes[scope] != null
+      typeof scope === 'string' &&
+      propOptions.scopes &&
+      typeof propOptions.scopes === 'object' &&
+      propOptions.scopes[scope] !== undefined
     ) {
-      const scopeOptionsOrBoolean = fieldOptions.scopes[scope];
-      // если настройки области являются логическим значением,
-      // то значение используется как индикатор видимости
-      if (typeof scopeOptionsOrBoolean === 'boolean') {
-        return scopeOptionsOrBoolean;
+      const scopeOptions = propOptions.scopes[scope];
+      // если настройки активной области проекции
+      // являются логическим значением, то значение
+      // возвращается в качестве результата
+      if (typeof scopeOptions === 'boolean') {
+        return scopeOptions;
       }
-      // если настройки области являются объектом,
-      // то используется опция select
+      // если настройки активной области проекции
+      // являются объектом и содержат опцию select,
+      // то значение опции возвращается как результат
       if (
-        scopeOptionsOrBoolean &&
-        typeof scopeOptionsOrBoolean === 'object' &&
-        typeof scopeOptionsOrBoolean.select === 'boolean'
+        scopeOptions &&
+        typeof scopeOptions === 'object' &&
+        !Array.isArray(scopeOptions) &&
+        typeof scopeOptions.select === 'boolean'
       ) {
-        return scopeOptionsOrBoolean.select;
+        return scopeOptions.select;
       }
     }
-    // если область проекции не указана,
-    // то проверяется общее правило
-    if (typeof fieldOptionsOrBoolean.select === 'boolean') {
-      return fieldOptionsOrBoolean.select;
+    // если правило видимости для активной области
+    // проекции не определено, то проверяется наличие
+    // общего правила
+    if (typeof propOptions.select === 'boolean') {
+      return propOptions.select;
     }
   }
-  // если для поля нет правил, то поле доступно
-  // по умолчанию (недоступно в режиме strict)
+  // если правила видимости не определены
+  // то результат будет зависеть от режима
   return !strict;
 }

+ 239 - 291
src/project-data.spec.js

@@ -3,14 +3,11 @@ import {format} from '@e22m4u/js-format';
 import {projectData} from './project-data.js';
 
 describe('projectData', function () {
-  it('should require the parameter "schemaOrFactory" to be a valid value', function () {
-    const throwable = v => () => projectData(v, {});
+  it('should require the options argument to be an object', function () {
+    const throwable = v => () => projectData({}, 10, v);
     const error = s =>
-      format(
-        'Projection schema must be an Object, a Function ' +
-          'or a non-empty String, but %s was given.',
-        s,
-      );
+      format('Projection options must be 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'));
@@ -18,48 +15,57 @@ describe('projectData', function () {
     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'));
-    projectData({}, {});
-    projectData(() => ({}), {});
-    projectData('mySchema', {}, {resolver: () => ({})});
+    throwable({})();
+    throwable(undefined)();
   });
 
-  it('should require the parameter "options" to be an object', function () {
-    const throwable = v => () => projectData({}, {}, v);
+  it('should require the strict option to be a boolean', function () {
+    const throwable = v => () => projectData({}, 10, {strict: v});
     const error = s =>
-      format('Parameter "options" must be an Object, but %s was given.', s);
+      format(
+        'Projection option "strict" 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(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'));
-    throwable({})();
+    throwable(true)();
+    throwable(false)();
     throwable(undefined)();
   });
 
-  it('should require the option "strict" to be a boolean', function () {
-    const throwable = v => () => projectData({}, {}, {strict: v});
+  it('should require the scope option to be a non-empty string', function () {
+    const throwable = v => () => projectData({}, 10, {scope: v});
     const error = s =>
-      format('Option "strict" must be a Boolean, but %s was given.', s);
-    expect(throwable('str')).to.throw(error('"str"'));
+      format(
+        'Projection option "scope" must be a non-empty String, ' +
+          'but %s was given.',
+        s,
+      );
     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'));
-    throwable(true)();
-    throwable(false)();
+    throwable('str')();
     throwable(undefined)();
   });
 
-  it('should require the option "scope" to be a non-empty string', function () {
-    const throwable = v => () => projectData({}, {}, {scope: v});
+  it('should require the resolver option to be a function', function () {
+    const throwable = v => () => projectData({}, 10, {resolver: v});
     const error = s =>
-      format('Option "scope" must be a non-empty String, but %s was given.', s);
+      format(
+        'Projection option "resolver" must be a Function, 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'));
@@ -68,317 +74,259 @@ describe('projectData', function () {
     expect(throwable([])).to.throw(error('Array'));
     expect(throwable({})).to.throw(error('Object'));
     expect(throwable(null)).to.throw(error('null'));
-    throwable('str')();
+    throwable(() => ({}))();
     throwable(undefined)();
   });
 
-  it('should require the option "resolver" to be a Function', function () {
-    const throwable = v => () => projectData({}, {}, {resolver: v});
+  it('should project with the schema factory', function () {
+    let invoked = 0;
+    const factory = () => {
+      invoked++;
+      return {foo: true, bar: false};
+    };
+    const res = projectData(factory, {foo: 10, bar: 20});
+    expect(res).to.be.eql({foo: 10});
+    expect(invoked).to.be.eq(1);
+  });
+
+  it('should require a factory value to be an object or a non-empty string', function () {
+    const throwable = v => () => projectData(() => v, {});
     const error = s =>
-      format('Option "resolver" must be a Function, but %s was given.', s);
+      format(
+        'Schema factory must return an Object ' +
+          'or a non-empty String, but %s was given.',
+        s,
+      );
     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(undefined)).to.throw(error('undefined'));
     expect(throwable(null)).to.throw(error('null'));
-    throwable(() => undefined)();
-    throwable(undefined)();
+    expect(throwable(() => undefined)).to.throw(error('Function'));
+    projectData(() => ({}), {});
+    projectData(() => 'str', {}, {resolver: () => ({})});
   });
 
-  it('should return a non-object and non-array data as is', function () {
-    const fn = v => projectData({foo: true}, v);
-    expect(fn('str')).to.be.eql('str');
-    expect(fn('')).to.be.eql('');
-    expect(fn(10)).to.be.eql(10);
-    expect(fn(0)).to.be.eql(0);
-    expect(fn(true)).to.be.eql(true);
-    expect(fn(false)).to.be.eql(false);
-    expect(fn(null)).to.be.eql(null);
-    expect(fn(undefined)).to.be.eql(undefined);
+  it('should resolve the schema name by the given resolver', function () {
+    let invoked = 0;
+    const resolver = name => {
+      invoked++;
+      expect(name).to.be.eql('mySchema');
+      return {foo: true, bar: false};
+    };
+    const res = projectData('mySchema', {foo: 10, bar: 20}, {resolver});
+    expect(res).to.be.eql({foo: 10});
+    expect(invoked).to.be.eq(1);
   });
 
-  it('should apply projection for each array element', function () {
-    const schema = {foo: true, bar: false};
-    const data = [
-      {foo: 10, bar: 20, baz: 30},
-      {foo: 40, bar: 50, baz: 60},
-    ];
-    const res = projectData(schema, data);
-    expect(res).to.be.eql([
-      {foo: 10, baz: 30},
-      {foo: 40, baz: 60},
-    ]);
+  it('should require the resolver option when the schema name is given', function () {
+    const throwable = () => projectData('mySchema', {});
+    expect(throwable).to.throw(
+      'Projection option "resolver" is required to resolve "mySchema" schema.',
+    );
   });
 
-  it('should add fields without rules by default', function () {
-    const res = projectData({}, {foo: 10, bar: 20});
-    expect(res).to.be.eql({foo: 10, bar: 20});
+  it('should require a resolver result to be an object', function () {
+    const throwable = v => () =>
+      projectData('mySchema', {}, {resolver: () => v});
+    const error = s =>
+      format('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(undefined)).to.throw(error('undefined'));
+    expect(throwable(null)).to.throw(error('null'));
+    expect(throwable(() => undefined)).to.throw(error('Function'));
+    throwable({})();
   });
 
-  it('should project fields by a boolean value', function () {
-    const res = projectData({foo: true, bar: false}, {foo: 10, bar: 20});
+  it('should resolver the schema name from the factory function', function () {
+    let invoked = 0;
+    const resolver = name => {
+      invoked++;
+      expect(name).to.be.eql('mySchema');
+      return {foo: true, bar: false};
+    };
+    const res = projectData(() => 'mySchema', {foo: 10, bar: 20}, {resolver});
     expect(res).to.be.eql({foo: 10});
+    expect(invoked).to.be.eq(1);
   });
 
-  it('should project fields by the select option', function () {
-    const res = projectData(
-      {foo: {select: true}, bar: {select: false}},
-      {foo: 10, bar: 20},
-    );
-    expect(res).to.be.eql({foo: 10});
+  it('should resolve the named schema in the the nested object', function () {
+    let invoked = 0;
+    const resolver = name => {
+      invoked++;
+      if (name === 'schema1') {
+        return {foo: true, bar: {schema: 'schema2'}};
+      } else if (name === 'schema2') {
+        return {baz: true, qux: false};
+      }
+    };
+    const data = {foo: 10, bar: {baz: 20, qux: 30}};
+    const res = projectData('schema1', data, {resolver});
+    expect(res).to.be.eql({foo: 10, bar: {baz: 20}});
+    expect(invoked).to.be.eq(2);
   });
 
-  it('should ignore scope-related rules by default', function () {
-    const res = projectData(
-      {foo: {scopes: {input: false, output: false}}},
-      {foo: 10},
+  it('should validate the given schema in the shallow mode', function () {
+    const schema1 = {foo: '?'};
+    const schema2 = {foo: true, bar: {schema: {baz: '?'}}};
+    expect(() => projectData(schema1, {foo: 10})).to.throw(
+      'Property options must be an Object or a Boolean, but "?" was given.',
     );
+    const res = projectData(schema2, {foo: 10});
     expect(res).to.be.eql({foo: 10});
-  });
-
-  it('should create nested projection by the schema option', function () {
-    const res = projectData(
-      {foo: true, bar: false, qux: {schema: {abc: true, def: false}}},
-      {foo: 10, bar: 20, qux: {abc: 30, def: 40}},
+    expect(() => projectData(schema2, {bar: {baz: 20}})).to.throw(
+      'Property options must be an Object or a Boolean, but "?" was given.',
     );
-    expect(res).to.be.eql({foo: 10, qux: {abc: 30}});
   });
 
-  describe('schema factory', function () {
-    it('should throw an error if the schema factory returns an invalid value', 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.',
-          s,
-        );
-      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'));
-      projectData(() => ({}), {});
-      projectData(() => 'mySchema', {}, {resolver: () => ({})});
-    });
-
-    it('should resolve a schema object from the given factory', function () {
-      let invoked = 0;
-      const factory = () => {
-        invoked++;
-        return {foo: true, bar: false};
-      };
-      const res = projectData(factory, {foo: 10, bar: 20});
-      expect(res).to.be.eql({foo: 10});
-      expect(invoked).to.be.eq(1);
-    });
-
-    it('should use the schema factory in the nested schema', function () {
-      let invoked = 0;
-      const factory = () => {
-        invoked++;
-        return {baz: true, qux: false};
-      };
-      const res = projectData(
-        {foo: true, bar: {schema: factory}},
-        {foo: 10, bar: {baz: 20, qux: 30}},
-      );
-      expect(res).to.be.eql({foo: 10, bar: {baz: 20}});
-      expect(invoked).to.be.eq(1);
-    });
+  it('should return primitive value as is', function () {
+    expect(projectData({}, 'str')).to.be.eq('str');
+    expect(projectData({}, '')).to.be.eq('');
+    expect(projectData({}, 10)).to.be.eq(10);
+    expect(projectData({}, 0)).to.be.eq(0);
+    expect(projectData({}, true)).to.be.eq(true);
+    expect(projectData({}, false)).to.be.eq(false);
+    expect(projectData({}, undefined)).to.be.eq(undefined);
+    expect(projectData({}, null)).to.be.eq(null);
   });
 
-  describe('named schema', function () {
-    it('should throw an error if the schema resolver returns an invalid value', function () {
-      const throwable = v => () =>
-        projectData('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 project an array items', function () {
+    const list = [{foo: 10, bar: 20, baz: 30}, {qux: 30}];
+    const expectedList = [{foo: 10, baz: 30}, {qux: 30}];
+    const res = projectData({foo: true, bar: false}, list);
+    expect(res).to.be.eql(expectedList);
+  });
 
-    it('should throw an error if no schema resolver is provided when a schema name is given', function () {
-      const throwable = () => projectData('mySchema', {});
-      expect(throwable).to.throw(
-        'Unable to resolve the projection schema "mySchema" ' +
-          'without a provided resolver.',
-      );
-    });
+  it('should project an array items in the strict mode', function () {
+    const list = [{foo: 10, bar: 20, baz: 30}, {qux: 30}];
+    const expectedList = [{foo: 10}, {}];
+    const res = projectData({foo: true, bar: false}, list, {strict: true});
+    expect(res).to.be.eql(expectedList);
+  });
 
-    it('should pass the schema name to the schema resolver and project the given data', function () {
-      let invoked = 0;
-      const resolver = key => {
-        expect(key).to.be.eq('mySchema');
-        invoked++;
-        return {foo: true, bar: false};
-      };
-      const res = projectData('mySchema', {foo: 10, bar: 20}, {resolver});
-      expect(res).to.be.eql({foo: 10});
-      expect(invoked).to.be.eq(1);
-    });
+  it('should exclude properties without rules when the strict mode is enabled', function () {
+    const res = projectData(
+      {foo: true, bar: false},
+      {foo: 10, bar: 20, baz: 30},
+      {strict: true},
+    );
+    expect(res).to.be.eql({foo: 10});
+  });
 
-    it('should use the schema resolver in the nested schema', function () {
-      let invoked = 0;
-      const resolver = key => {
-        expect(key).to.be.eq('mySchema');
-        invoked++;
-        return {baz: true, qux: false};
-      };
-      const res = projectData(
-        {foo: true, bar: {schema: 'mySchema'}},
-        {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 ignore prototype properties', function () {
+    const data = Object.create({baz: 30});
+    data.foo = 10;
+    data.bar = 20;
+    expect(data).to.be.eql({foo: 10, bar: 20, baz: 30});
+    const res = projectData({foo: true, bar: false}, data);
+    expect(res).to.be.eql({foo: 10});
+  });
 
-    it('should use the schema name from the schema factory', function () {
-      let invoked = 0;
-      const resolver = key => {
-        expect(key).to.be.eq('mySchema');
-        invoked++;
-        return {foo: true, bar: false};
-      };
-      const res = projectData(() => 'mySchema', {foo: 10, bar: 20}, {resolver});
-      expect(res).to.be.eql({foo: 10});
-      expect(invoked).to.be.eq(1);
-    });
+  it('should project the property by a boolean rule', function () {
+    const res = projectData({foo: true, bar: false}, {foo: 10, bar: 20});
+    expect(res).to.be.eql({foo: 10});
   });
 
-  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});
-      expect(res).to.be.eql({foo: 10});
-    });
+  it('should project the property by the select option', function () {
+    const res = projectData(
+      {foo: {select: true}, bar: {select: false}},
+      {foo: 10, bar: 20},
+    );
+    expect(res).to.be.eql({foo: 10});
+  });
 
-    it('should remove fields without rules when the strict mode is enabled', function () {
-      const res = projectData({}, {foo: 10}, {strict: true});
-      expect(res).to.be.eql({});
-    });
+  it('should ignore scope options when no active scope is provided', function () {
+    const schema = {
+      foo: {select: true, scopes: {input: false}},
+      bar: {select: false, scopes: {output: true}},
+    };
+    const res = projectData(schema, {foo: 10, bar: 20});
+    expect(res).to.be.eql({foo: 10});
+  });
 
-    it('should project fields by a boolean value', function () {
-      const res = projectData(
-        {foo: true, bar: false},
-        {foo: 10, bar: 20},
-        {strict: true},
-      );
-      expect(res).to.be.eql({foo: 10});
-    });
+  it('should project the active scope by the boolean rule', function () {
+    const schema = {
+      foo: {scopes: {input: true}},
+      bar: {scopes: {input: false}},
+    };
+    const res = projectData(schema, {foo: 10, bar: 20}, {scope: 'input'});
+    expect(res).to.be.eql({foo: 10});
+  });
 
-    it('should project fields by the select option', function () {
-      const res = projectData(
-        {foo: {select: true}, bar: {select: false}},
-        {foo: 10, bar: 20},
-        {strict: true},
-      );
-      expect(res).to.be.eql({foo: 10});
-    });
+  it('should project the active scope by the select option', function () {
+    const schema = {
+      foo: {scopes: {input: {select: true}}},
+      bar: {scopes: {input: {select: false}}},
+    };
+    const res = projectData(schema, {foo: 10, bar: 20}, {scope: 'input'});
+    expect(res).to.be.eql({foo: 10});
+  });
 
-    it('should propagate the strict mode to nested schema', function () {
-      const res = projectData(
-        {foo: false, bar: {select: true, schema: {baz: true}}},
-        {foo: 10, bar: {baz: 20, qux: 30}},
-        {strict: true},
-      );
-      expect(res).to.be.eql({bar: {baz: 20}});
-    });
+  it('should prioritize the scope rule over the general options', function () {
+    const schema = {
+      foo: {select: false, scopes: {input: true}},
+      bar: {select: true, scopes: {input: false}},
+    };
+    const res = projectData(schema, {foo: 10, bar: 20}, {scope: 'input'});
+    expect(res).to.be.eql({foo: 10});
+  });
 
-    it('should ignore inherited fields', function () {
-      const res = projectData(
-        {bar: true, toString: true},
-        {bar: 10},
-        {strict: true},
-      );
-      expect(res).to.be.eql({bar: 10});
-    });
+  it('should ignore scope options not matched with the active scope', function () {
+    const schema = {
+      foo: {scopes: {input: true, output: false}},
+      bar: {scopes: {input: false, output: true}},
+    };
+    const res = projectData(schema, {foo: 10, bar: 20}, {scope: 'input'});
+    expect(res).to.be.eql({foo: 10});
   });
 
-  describe('projection scope', function () {
-    it('should apply scope-specific selection rule by a boolean value', function () {
-      const schema = {
-        foo: {
-          select: false,
-          scopes: {
-            input: true,
-          },
-        },
-        bar: true,
-      };
-      const data = {foo: 10, bar: 20};
-      const res1 = projectData(schema, data);
-      const res2 = projectData(schema, data, {scope: 'input'});
-      expect(res1).to.be.eql({bar: 20});
-      expect(res2).to.be.eql({foo: 10, bar: 20});
-    });
+  it('should exclude properties without selection rule in the strict mode when the active scope is provided', function () {
+    const schema = {
+      foo: {scopes: {input: true}},
+      bar: {scopes: {input: false}},
+      baz: {scopes: {output: true}},
+    };
+    const data = {foo: 10, bar: 20, baz: 30, qux: 40};
+    const res = projectData(schema, data, {strict: true, scope: 'input'});
+    expect(res).to.be.eql({foo: 10});
+  });
 
-    it('should apply scope-specific selection rule by the select option', function () {
-      const schema = {
-        foo: {
-          select: false,
-          scopes: {
-            input: {select: true},
-          },
-        },
-        bar: {select: true},
-      };
-      const data = {foo: 10, bar: 20};
-      const res1 = projectData(schema, data);
-      const res2 = projectData(schema, data, {scope: 'input'});
-      expect(res1).to.be.eql({bar: 20});
-      expect(res2).to.be.eql({foo: 10, bar: 20});
-    });
+  it('should prioritize the scope options over the general options in the strict mode', function () {
+    const schema = {
+      foo: {select: false, scopes: {input: true}},
+      bar: {select: false, scopes: {input: {select: true}}},
+    };
+    const data = {foo: 10, bar: 20, baz: 30};
+    const res = projectData(schema, data, {strict: true, scope: 'input'});
+    expect(res).to.be.eql({foo: 10, bar: 20});
+  });
 
-    it('should fallback to general rule if scope rule is missing', function () {
-      const schema = {
-        foo: {
-          select: true,
-          scopes: {
-            output: {select: false},
-          },
-        },
-      };
-      const data = {foo: 10};
-      const res = projectData(schema, data, {scope: 'input'});
-      expect(res).to.be.eql({foo: 10});
-    });
+  it('should project the nested object by the given schema', function () {
+    const schema = {
+      foo: true,
+      bar: {schema: {baz: true, qux: false}},
+    };
+    const data = {foo: 10, bar: {baz: 20, qux: 30, buz: 40}};
+    const res = projectData(schema, data, {scope: 'input'});
+    expect(res).to.be.eql({foo: 10, bar: {baz: 20, buz: 40}});
+  });
 
-    it('should fallback to the general rule if the scope options exists but lacks the select option', function () {
-      const schema = {
-        foo: {
-          select: true,
-          scopes: {
-            input: {},
-          },
-        },
-        bar: {
-          select: false,
-          scopes: {
-            input: {},
-          },
-        },
-      };
-      const data = {foo: 10, bar: 20};
-      const res = projectData(schema, data, {scope: 'input'});
-      expect(res).to.be.eql({foo: 10});
-    });
+  it('should exclude nested properties without rules in the strict mode', function () {
+    const schema = {
+      foo: true,
+      bar: {select: true, schema: {baz: true, qux: false}},
+    };
+    const data = {foo: 10, bar: {baz: 20, qux: 30, buz: 40}};
+    const res = projectData(schema, data, {strict: true, scope: 'input'});
+    expect(res).to.be.eql({foo: 10, bar: {baz: 20}});
   });
 });

+ 8 - 10
src/definitions/projection-schema-registry.js → src/projection-schema-registry.js

@@ -7,11 +7,9 @@ import {validateProjectionSchemaDefinition} from './validate-projection-schema-d
  */
 export class ProjectionSchemaRegistry extends Service {
   /**
-   * Schema map.
-   *
-   * @type {Map<string, object>}
+   * Definitions.
    */
-  definitions = new Map();
+  _definitions = new Map();
 
   /**
    * Define schema.
@@ -21,13 +19,13 @@ export class ProjectionSchemaRegistry extends Service {
    */
   defineSchema(schemaDef) {
     validateProjectionSchemaDefinition(schemaDef);
-    if (this.definitions.has(schemaDef.name)) {
+    if (this._definitions.has(schemaDef.name)) {
       throw new InvalidArgumentError(
         'Projection schema %v is already registered.',
         schemaDef.name,
       );
     }
-    this.definitions.set(schemaDef.name, schemaDef);
+    this._definitions.set(schemaDef.name, schemaDef);
     return this;
   }
 
@@ -38,7 +36,7 @@ export class ProjectionSchemaRegistry extends Service {
    * @returns {boolean}
    */
   hasSchema(schemaName) {
-    return this.definitions.has(schemaName);
+    return this._definitions.has(schemaName);
   }
 
   /**
@@ -48,7 +46,7 @@ export class ProjectionSchemaRegistry extends Service {
    * @returns {object}
    */
   getSchema(schemaName) {
-    const schemaDef = this.definitions.get(schemaName);
+    const schemaDef = this._definitions.get(schemaName);
     if (!schemaDef) {
       throw new InvalidArgumentError(
         'Projection schema %v is not found.',
@@ -65,10 +63,10 @@ export class ProjectionSchemaRegistry extends Service {
    * @returns {object}
    */
   getDefinition(schemaName) {
-    const schemaDef = this.definitions.get(schemaName);
+    const schemaDef = this._definitions.get(schemaName);
     if (!schemaDef) {
       throw new InvalidArgumentError(
-        'Projection definition %v is not found.',
+        'Schema definition %v is not found.',
         schemaName,
       );
     }

+ 74 - 0
src/projection-schema-registry.spec.js

@@ -0,0 +1,74 @@
+import {expect} from 'chai';
+import {ProjectionSchemaRegistry} from './projection-schema-registry.js';
+
+describe('ProjectionSchemaRegistry', function () {
+  describe('defineSchema', function () {
+    it('should validate the given definition', function () {
+      const S = new ProjectionSchemaRegistry();
+      const throwable = () => S.defineSchema({});
+      expect(throwable).to.throw(
+        'Definition option "name" must be a non-empty String, ' +
+          'but undefined was given.',
+      );
+    });
+
+    it('should register the given definition', function () {
+      const def = {name: 'mySchema', schema: {}};
+      const S = new ProjectionSchemaRegistry();
+      S.defineSchema(def);
+      const res = S.getDefinition(def.name);
+      expect(res).to.be.eql(def);
+    });
+
+    it('should throw an error if the schema name is already registered', function () {
+      const S = new ProjectionSchemaRegistry();
+      const def = {name: 'mySchema', schema: {}};
+      S.defineSchema(def);
+      const throwable = () => S.defineSchema(def);
+      expect(throwable).to.throw(
+        'Projection schema "mySchema" is already registered.',
+      );
+    });
+  });
+
+  describe('hasSchema', function () {
+    it('should return true when the given name is registered', function () {
+      const S = new ProjectionSchemaRegistry();
+      expect(S.hasSchema('mySchema')).to.be.false;
+      S.defineSchema({name: 'mySchema', schema: {}});
+      expect(S.hasSchema('mySchema')).to.be.true;
+    });
+  });
+
+  describe('getSchema', function () {
+    it('should throw an error if the given name is not registered', function () {
+      const S = new ProjectionSchemaRegistry();
+      const throwable = () => S.getSchema('mySchema');
+      expect(throwable).to.throw('Projection schema "mySchema" is not found.');
+    });
+
+    it('should return a registered schema by the given name', function () {
+      const def = {name: 'mySchema', schema: {foo: true, bar: false}};
+      const S = new ProjectionSchemaRegistry();
+      S.defineSchema(def);
+      const res = S.getSchema(def.name);
+      expect(res).to.be.eql(def.schema);
+    });
+  });
+
+  describe('getDefinition', function () {
+    it('should throw an error if the given name is not registered', function () {
+      const S = new ProjectionSchemaRegistry();
+      const throwable = () => S.getDefinition('mySchema');
+      expect(throwable).to.throw('Schema definition "mySchema" is not found.');
+    });
+
+    it('should return a registered definition by the given name', function () {
+      const def = {name: 'mySchema', schema: {}};
+      const S = new ProjectionSchemaRegistry();
+      S.defineSchema(def);
+      const res = S.getDefinition(def.name);
+      expect(res).to.be.eql(def);
+    });
+  });
+});

+ 0 - 42
src/projection-schema.d.ts

@@ -1,42 +0,0 @@
-
-/**
- * Projection schema.
- */
-export type ProjectionSchema = {
-  [field: string]: boolean | ProjectionSchemaFieldOptions | undefined;
-};
-
-/**
- * Projection schema name.
- */
-export type ProjectionSchemaName = string;
-
-/**
- * Projection schema factory.
- */
-export type ProjectionSchemaFactory = () =>
-  | ProjectionSchema
-  | ProjectionSchemaName;
-
-/**
- * Projection schema field options.
- */
-export type ProjectionSchemaFieldOptions = {
-  select?: boolean;
-  scopes?: ProjectionSchemaScopes;
-  schema?: ProjectionSchema | ProjectionSchemaFactory | ProjectionSchemaName;
-};
-
-/**
- * Projection schema scopes.
- */
-export type ProjectionSchemaScopes = {
-  [scope: string]: boolean | ProjectionSchemaScopeOptions | undefined;
-};
-
-/**
- * Projection schema scope options.
- */
-export type ProjectionSchemaScopeOptions = {
-  select?: boolean;
-};

+ 0 - 1
src/projection-schema.js

@@ -1 +0,0 @@
-export {};

+ 0 - 13
src/projection-scope.d.ts

@@ -1,13 +0,0 @@
-/**
- * Projection scope.
- */
-export declare const ProjectionScope: {
-  INPUT: 'input',
-  OUTPUT: 'output',
-};
-
-/**
- * Projection scope.
- */
-export type ProjectionScope =
-  (typeof ProjectionScope)[keyof typeof ProjectionScope];

+ 0 - 9
src/projection-scope.js

@@ -1,9 +0,0 @@
-/**
- * Projection scope.
- *
- * @type {{INPUT: string, OUTPUT: string}}
- */
-export const ProjectionScope = {
-  INPUT: 'input',
-  OUTPUT: 'output',
-};

+ 4 - 4
src/definitions/validate-projection-schema-definition.js → src/validate-projection-schema-definition.js

@@ -1,5 +1,5 @@
 import {InvalidArgumentError} from '@e22m4u/js-format';
-import {validateProjectionSchema} from '../validate-projection-schema.js';
+import {validateProjectionSchema} from './validate-projection-schema.js';
 
 /**
  * Validate projection schema definition.
@@ -9,13 +9,13 @@ import {validateProjectionSchema} from '../validate-projection-schema.js';
 export function validateProjectionSchemaDefinition(schemaDef) {
   if (!schemaDef || typeof schemaDef !== 'object' || Array.isArray(schemaDef)) {
     throw new InvalidArgumentError(
-      'Projection schema definition must be an Object, but %v was given.',
+      'Schema definition must be an Object, but %v was given.',
       schemaDef,
     );
   }
   if (!schemaDef.name || typeof schemaDef.name !== 'string') {
     throw new InvalidArgumentError(
-      'Projection schema name must be a non-empty String, but %v was given.',
+      'Definition option "name" must be a non-empty String, but %v was given.',
       schemaDef.name,
     );
   }
@@ -25,7 +25,7 @@ export function validateProjectionSchemaDefinition(schemaDef) {
     Array.isArray(schemaDef.schema)
   ) {
     throw new InvalidArgumentError(
-      'Projection schema must be an Object, but %v was given.',
+      'Definition option "schema" must be an Object, but %v was given.',
       schemaDef.schema,
     );
   }

+ 15 - 19
src/definitions/validate-projection-schema-definition.spec.js → src/validate-projection-schema-definition.spec.js

@@ -3,13 +3,10 @@ import {format} from '@e22m4u/js-format';
 import {validateProjectionSchemaDefinition} from './validate-projection-schema-definition.js';
 
 describe('validateProjectionSchemaDefinition', function () {
-  it('should require the first argument to be an object', function () {
+  it('should require the schema definition to be an object', function () {
     const throwable = v => () => validateProjectionSchemaDefinition(v);
     const error = s =>
-      format(
-        'Projection schema definition must be an Object, but %s was given.',
-        s,
-      );
+      format('Schema definition must be 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'));
@@ -17,17 +14,19 @@ describe('validateProjectionSchemaDefinition', function () {
     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'));
+    expect(throwable(null)).to.throw(error('null'));
+    expect(throwable(() => undefined)).to.throw(error('Function'));
     throwable({name: 'mySchema', schema: {}})();
   });
 
-  it('should require the "name" option to be a string', function () {
+  it('should require the name option to be a non-empty string', function () {
     const throwable = v => () =>
       validateProjectionSchemaDefinition({name: v, schema: {}});
     const error = s =>
       format(
-        'Projection schema name must be a non-empty String, but %s was given.',
+        'Definition option "name" must be a non-empty String, ' +
+          'but %s was given.',
         s,
       );
     expect(throwable('')).to.throw(error('""'));
@@ -37,14 +36,18 @@ describe('validateProjectionSchemaDefinition', function () {
     expect(throwable({})).to.throw(error('Object'));
     expect(throwable(null)).to.throw(error('null'));
     expect(throwable(undefined)).to.throw(error('undefined'));
-    throwable('mySchema')();
+    expect(throwable(() => ({}))).to.throw(error('Function'));
+    throwable('str')();
   });
 
-  it('should require the "schema" option to be an object', function () {
+  it('should require the schema option to be an object', function () {
     const throwable = v => () =>
       validateProjectionSchemaDefinition({name: 'mySchema', schema: v});
     const error = s =>
-      format('Projection schema must be an Object, but %s was given.', s);
+      format(
+        'Definition option "schema" must be 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'));
@@ -52,14 +55,7 @@ describe('validateProjectionSchemaDefinition', function () {
     expect(throwable([])).to.throw(error('Array'));
     expect(throwable(null)).to.throw(error('null'));
     expect(throwable(undefined)).to.throw(error('undefined'));
+    expect(throwable(() => ({}))).to.throw(error('Function'));
     throwable({})();
   });
-
-  it('should validate the projection schema', function () {
-    const throwable = () =>
-      validateProjectionSchemaDefinition({name: 'mySchema', schema: {foo: 10}});
-    expect(throwable).to.throw(
-      'Field options must be a Boolean or an Object, but 10 was given.',
-    );
-  });
 });

+ 0 - 12
src/validate-projection-schema.d.ts

@@ -1,12 +0,0 @@
-import {ProjectionSchema} from './projection-schema.js';
-
-/**
- * Validate projection schema.
- *
- * @param schema
- * @param shallowMode
- */
-export declare function validateProjectionSchema(
-  schema: ProjectionSchema,
-  shallowMode?: boolean
-): void;

+ 67 - 65
src/validate-projection-schema.js

@@ -3,112 +3,114 @@ import {InvalidArgumentError} from '@e22m4u/js-format';
 /**
  * Validate projection schema.
  *
- * @param {object} schema
- * @param {boolean} shallowMode
- * @returns {undefined}
+ * @param {object|Function|string} schema
+ * @param {boolean} [shallowMode]
  */
 export function validateProjectionSchema(schema, shallowMode = false) {
   // schema
-  if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
+  if (
+    !schema ||
+    (typeof schema !== 'object' &&
+      typeof schema !== 'function' &&
+      typeof schema !== 'string') ||
+    Array.isArray(schema)
+  ) {
     throw new InvalidArgumentError(
-      'Projection schema must be an Object, but %v was given.',
+      'Projection schema must be an Object, a Function ' +
+        'or a non-empty String, but %v was given.',
       schema,
     );
   }
-  // shallowMode
-  if (typeof shallowMode !== 'boolean') {
-    throw new InvalidArgumentError(
-      'Parameter "shallowMode" must be a Boolean, but %v was given.',
-      shallowMode,
-    );
+  if (typeof schema !== 'object') {
+    return;
   }
-  Object.keys(schema).forEach(fieldName => {
-    // schema[k]
-    const options = schema[fieldName];
-    if (options === undefined) {
+  // schema[k]
+  Object.keys(schema).forEach(propName => {
+    const propOptions = schema[propName];
+    if (propOptions === undefined) {
       return;
     }
     if (
-      options === null ||
-      (typeof options !== 'boolean' && typeof options !== 'object') ||
-      Array.isArray(options)
+      propOptions === null ||
+      (typeof propOptions !== 'object' && typeof propOptions !== 'boolean') ||
+      Array.isArray(propOptions)
     ) {
       throw new InvalidArgumentError(
-        'Field options must be a Boolean or an Object, but %v was given.',
-        options,
+        'Property options must be an Object or a Boolean, but %v was given.',
+        propOptions,
       );
     }
-    if (typeof options === 'boolean') {
+    if (typeof propOptions === 'boolean') {
       return;
     }
     // schema[k].select
-    if (options.select !== undefined && typeof options.select !== 'boolean') {
+    if (
+      propOptions.select !== undefined &&
+      typeof propOptions.select !== 'boolean'
+    ) {
       throw new InvalidArgumentError(
-        'Field option "select" must be a Boolean, but %v was given.',
-        options.select,
+        'Property option "select" must be a Boolean, but %v was given.',
+        propOptions.select,
       );
     }
-    // schema[k].schema
-    if (options.schema !== undefined) {
-      if (
-        !options.schema ||
-        (typeof options.schema !== 'object' &&
-          typeof options.schema !== 'function' &&
-          typeof options.schema !== 'string') ||
-        Array.isArray(options.schema)
-      ) {
-        throw new InvalidArgumentError(
-          'Embedded schema must be an Object, a Function ' +
-            'or a non-empty String, but %v was given.',
-          options.schema,
-        );
-      }
-      if (!shallowMode && typeof options.schema === 'object') {
-        validateProjectionSchema(options.schema, shallowMode);
-      }
-    }
     // schema[k].scopes
-    if (options.scopes !== undefined) {
+    if (propOptions.scopes !== undefined) {
       if (
-        !options.scopes ||
-        typeof options.scopes !== 'object' ||
-        Array.isArray(options.scopes)
+        !propOptions.scopes ||
+        typeof propOptions.scopes !== 'object' ||
+        Array.isArray(propOptions.scopes)
       ) {
         throw new InvalidArgumentError(
-          'Field option "scopes" must be an Object, but %v was given.',
-          options.scopes,
+          'Property option "scopes" must be an Object, but %v was given.',
+          propOptions.scopes,
         );
       }
-      Object.keys(options.scopes).forEach(scopeName => {
-        // schema[k].scopes[k]
-        const scopeOptions = options.scopes[scopeName];
+      Object.values(propOptions.scopes).forEach(scopeOptions => {
         if (scopeOptions === undefined) {
           return;
         }
+        // schema[k].scopes[k]
         if (
           scopeOptions === null ||
-          (typeof scopeOptions !== 'boolean' &&
-            typeof scopeOptions !== 'object') ||
+          (typeof scopeOptions !== 'object' &&
+            typeof scopeOptions !== 'boolean') ||
           Array.isArray(scopeOptions)
         ) {
           throw new InvalidArgumentError(
-            'Scope options must be a Boolean or an Object, but %v was given.',
+            'Scope options must be an Object or a Boolean, but %v was given.',
             scopeOptions,
           );
         }
-        if (typeof scopeOptions === 'boolean') {
-          return;
-        }
         // schema[k].scopes[k].select
-        if (scopeOptions.select !== undefined) {
-          if (typeof scopeOptions.select !== 'boolean') {
-            throw new InvalidArgumentError(
-              'Scope option "select" must be a Boolean, but %v was given.',
-              scopeOptions.select,
-            );
-          }
+        if (
+          scopeOptions.select !== undefined &&
+          typeof scopeOptions.select !== 'boolean'
+        ) {
+          throw new InvalidArgumentError(
+            'Scope option "select" must be a Boolean, but %v was given.',
+            scopeOptions.select,
+          );
         }
       });
     }
+    // schema[k].schema
+    if (propOptions.schema !== undefined) {
+      if (
+        !propOptions.schema ||
+        (typeof propOptions.schema !== 'object' &&
+          typeof propOptions.schema !== 'function' &&
+          typeof propOptions.schema !== 'string') ||
+        Array.isArray(propOptions.schema)
+      ) {
+        throw new InvalidArgumentError(
+          'Property option "schema" must be an Object, a Function' +
+            ' or a non-empty String, but %v was given.',
+          propOptions.schema,
+        );
+      }
+      if (!shallowMode && typeof propOptions.schema === 'object') {
+        validateProjectionSchema(propOptions.schema, shallowMode);
+      }
+    }
   });
 }

+ 62 - 73
src/validate-projection-schema.spec.js

@@ -3,43 +3,32 @@ import {format} from '@e22m4u/js-format';
 import {validateProjectionSchema} from './validate-projection-schema.js';
 
 describe('validateProjectionSchema', function () {
-  it('should require the schema argument to be a object', function () {
+  it('should require the schema to be a valid value', function () {
     const throwable = v => () => validateProjectionSchema(v);
     const error = s =>
-      format('Projection schema must be an Object, but %s was given.', s);
-    expect(throwable('str')).to.throw(error('"str"'));
+      format(
+        'Projection schema must be an Object, a Function ' +
+          'or a non-empty String, but %s was given.',
+        s,
+      );
     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 require the shallowMode parameter to be a boolean', function () {
-    const throwable = v => () => validateProjectionSchema({}, v);
-    const error = s =>
-      format('Parameter "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'));
-    throwable(true)();
-    throwable(false)();
-    throwable(undefined)();
+    throwable('str')();
+    throwable({})();
+    throwable(() => ({}))();
   });
 
-  it('should require the field options to be a boolean or an object', function () {
+  it('should require property options to be an object or a boolean', function () {
     const throwable = v => () => validateProjectionSchema({foo: v});
     const error = s =>
       format(
-        'Field options must be a Boolean or an Object, but %s was given.',
+        'Property options must be an Object or a Boolean, but %s was given.',
         s,
       );
     expect(throwable('str')).to.throw(error('"str"'));
@@ -48,16 +37,20 @@ describe('validateProjectionSchema', function () {
     expect(throwable(0)).to.throw(error('0'));
     expect(throwable([])).to.throw(error('Array'));
     expect(throwable(null)).to.throw(error('null'));
+    expect(throwable(() => ({}))).to.throw(error('Function'));
     throwable({})();
     throwable(true)();
     throwable(false)();
     throwable(undefined)();
   });
 
-  it('should require the field option "select" 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('Field option "select" must be a Boolean, but %s was given.', s);
+      format(
+        'Property option "select" 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'));
@@ -65,36 +58,19 @@ describe('validateProjectionSchema', function () {
     expect(throwable({})).to.throw(error('Object'));
     expect(throwable([])).to.throw(error('Array'));
     expect(throwable(null)).to.throw(error('null'));
+    expect(throwable(() => ({}))).to.throw(error('Function'));
     throwable(true)();
     throwable(false)();
     throwable(undefined)();
   });
 
-  it('should require the field option "schema" to be a valid value', function () {
-    const throwable = v => () => validateProjectionSchema({foo: {schema: v}});
+  it('should require the scopes option to be an object', function () {
+    const throwable = v => () => validateProjectionSchema({foo: {scopes: v}});
     const error = s =>
       format(
-        'Embedded schema must be an Object, a Function ' +
-          'or a non-empty String, but %s was given.',
+        'Property option "scopes" must be an Object, but %s was given.',
         s,
       );
-    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'));
-    throwable('mySchema')();
-    throwable({})();
-    throwable(() => ({}))();
-    throwable(undefined)();
-  });
-
-  it('should require the field option "scopes" to be an object', function () {
-    const throwable = v => () => validateProjectionSchema({foo: {scopes: v}});
-    const error = s =>
-      format('Field option "scopes" must be 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'));
@@ -103,16 +79,17 @@ describe('validateProjectionSchema', function () {
     expect(throwable(false)).to.throw(error('false'));
     expect(throwable([])).to.throw(error('Array'));
     expect(throwable(null)).to.throw(error('null'));
+    expect(throwable(() => ({}))).to.throw(error('Function'));
     throwable({})();
     throwable(undefined)();
   });
 
-  it('should require scope options to be a boolean or an object', function () {
+  it('should require the scope options to be an object or a boolean', function () {
     const throwable = v => () =>
       validateProjectionSchema({foo: {scopes: {input: v}}});
     const error = s =>
       format(
-        'Scope options must be a Boolean or an Object, but %s was given.',
+        'Scope options must be an Object or a Boolean, but %s was given.',
         s,
       );
     expect(throwable('str')).to.throw(error('"str"'));
@@ -121,13 +98,14 @@ describe('validateProjectionSchema', function () {
     expect(throwable(0)).to.throw(error('0'));
     expect(throwable([])).to.throw(error('Array'));
     expect(throwable(null)).to.throw(error('null'));
+    expect(throwable(() => ({}))).to.throw(error('Function'));
     throwable({})();
     throwable(true)();
     throwable(false)();
     throwable(undefined)();
   });
 
-  it('should require the scope option "select" 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 =>
@@ -136,43 +114,54 @@ describe('validateProjectionSchema', function () {
     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([])).to.throw(error('Array'));
     expect(throwable(null)).to.throw(error('null'));
+    expect(throwable(() => ({}))).to.throw(error('Function'));
     throwable(true)();
     throwable(false)();
     throwable(undefined)();
   });
 
-  it('should allow nested schema', function () {
-    validateProjectionSchema({
-      foo: {
-        select: true,
-        schema: {
-          bar: {
-            select: true,
-          },
-        },
-      },
-      baz: {
-        select: false,
-        schema: {
-          qux: {
-            select: false,
-          },
-        },
-      },
-    });
+  it('should require the schema option to be a valid value', function () {
+    const throwable = v => () => validateProjectionSchema({foo: {schema: v}});
+    const error = s =>
+      format(
+        'Property option "schema" must be an Object, a Function ' +
+          'or a non-empty String, but %s was given.',
+        s,
+      );
+    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'));
+    throwable('str')();
+    throwable({})();
+    throwable(() => ({}))();
+    throwable(undefined)();
   });
 
-  it('should validate root schema in shallow mode', function () {
-    const throwable = () => validateProjectionSchema({foo: 10}, true);
+  it('should validate the embedded schema', function () {
+    const throwable = () =>
+      validateProjectionSchema({foo: {schema: {bar: 10}}});
     expect(throwable).to.throw(
-      'Field options must be a Boolean or an Object, but 10 was given.',
+      'Property options must be an Object or a Boolean, but 10 was given.',
     );
   });
 
-  it('should skip nested schema checking in shallow mode', function () {
-    validateProjectionSchema({foo: {schema: {bar: 10}}}, true);
+  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 skip embedded schema validation', function () {
+      validateProjectionSchema({foo: {schema: {bar: 10}}}, true);
+    });
   });
 });

+ 0 - 14
tsconfig.json

@@ -1,14 +0,0 @@
-{
-  "compilerOptions": {
-    "strict": true,
-    "target": "es2022",
-    "module": "NodeNext",
-    "moduleResolution": "NodeNext",
-    "noEmit": true,
-    "allowJs": true
-  },
-  "include": [
-    "./src/**/*.ts",
-    "./src/**/*.js"
-  ]
-}