Browse Source

feat: adds DataProjector

e22m4u 1 day ago
parent
commit
ed7a276a50

+ 150 - 2
dist/cjs/index.cjs

@@ -21,9 +21,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
 // src/index.js
 var index_exports = {};
 __export(index_exports, {
+  DataProjector: () => DataProjector,
+  ProjectionSchemaRegistry: () => ProjectionSchemaRegistry,
   ProjectionScope: () => ProjectionScope,
   projectData: () => projectData,
-  validateProjectionSchema: () => validateProjectionSchema
+  validateProjectionSchema: () => validateProjectionSchema,
+  validateProjectionSchemaDefinition: () => validateProjectionSchemaDefinition
 });
 module.exports = __toCommonJS(index_exports);
 
@@ -229,6 +232,148 @@ function _shouldSelect(propOptionsOrBoolean, strict, scope) {
 }
 __name(_shouldSelect, "_shouldSelect");
 
+// src/data-projector.js
+var import_js_service2 = require("@e22m4u/js-service");
+
+// src/definitions/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
+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.",
+      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.",
+      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.",
+      schemaDef.schema
+    );
+  }
+  validateProjectionSchema(schemaDef.schema);
+}
+__name(validateProjectionSchemaDefinition, "validateProjectionSchemaDefinition");
+
+// src/definitions/projection-schema-registry.js
+var _ProjectionSchemaRegistry = class _ProjectionSchemaRegistry extends import_js_service.Service {
+  /**
+   * Schema map.
+   *
+   * @type {Map<string, object>}
+   */
+  definitions = /* @__PURE__ */ new Map();
+  /**
+   * Define schema.
+   *
+   * @param {object} schemaDef
+   * @returns {this}
+   */
+  defineSchema(schemaDef) {
+    validateProjectionSchemaDefinition(schemaDef);
+    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);
+    return this;
+  }
+  /**
+   * Has schema.
+   *
+   * @param {string} schemaName
+   * @returns {boolean}
+   */
+  hasSchema(schemaName) {
+    return this.definitions.has(schemaName);
+  }
+  /**
+   * Get schema.
+   *
+   * @param {string} schemaName
+   * @returns {object}
+   */
+  getSchema(schemaName) {
+    const schemaDef = this.definitions.get(schemaName);
+    if (!schemaDef) {
+      throw new import_js_format4.InvalidArgumentError(
+        "Projection schema %v is not found.",
+        schemaName
+      );
+    }
+    return schemaDef.schema;
+  }
+  /**
+   * Get definition.
+   *
+   * @param {string} schemaName
+   * @returns {object}
+   */
+  getDefinition(schemaName) {
+    const schemaDef = this.definitions.get(schemaName);
+    if (!schemaDef) {
+      throw new import_js_format4.InvalidArgumentError(
+        "Projection definition %v is not found.",
+        schemaName
+      );
+    }
+    return schemaDef;
+  }
+};
+__name(_ProjectionSchemaRegistry, "ProjectionSchemaRegistry");
+var ProjectionSchemaRegistry = _ProjectionSchemaRegistry;
+
+// src/data-projector.js
+var _DataProjector = class _DataProjector extends import_js_service2.Service {
+  /**
+   * Define schema.
+   *
+   * @param {object} schemaDef
+   * @returns {this}
+   */
+  defineSchema(schemaDef) {
+    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} [options]
+   * @returns {*}
+   */
+  project(schemaOrFactory, data, options) {
+    const registry = this.getService(ProjectionSchemaRegistry);
+    const resolver = /* @__PURE__ */ __name((schemaName) => {
+      return registry.getSchema(schemaName);
+    }, "resolver");
+    return projectData(schemaOrFactory, data, { ...options, resolver });
+  }
+};
+__name(_DataProjector, "DataProjector");
+var DataProjector = _DataProjector;
+
 // src/projection-scope.js
 var ProjectionScope = {
   INPUT: "input",
@@ -236,7 +381,10 @@ var ProjectionScope = {
 };
 // Annotate the CommonJS export names for ESM import in node:
 0 && (module.exports = {
+  DataProjector,
+  ProjectionSchemaRegistry,
   ProjectionScope,
   projectData,
-  validateProjectionSchema
+  validateProjectionSchema,
+  validateProjectionSchemaDefinition
 });

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

@@ -0,0 +1,44 @@
+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;
+}

+ 45 - 0
src/data-projector.js

@@ -0,0 +1,45 @@
+import {Service} from '@e22m4u/js-service';
+import {projectData} from './project-data.js';
+import {ProjectionSchemaRegistry} from './definitions/index.js';
+
+/**
+ * Data projector.
+ */
+export class DataProjector extends Service {
+  /**
+   * Define schema.
+   *
+   * @param {object} schemaDef
+   * @returns {this}
+   */
+  defineSchema(schemaDef) {
+    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} [options]
+   * @returns {*}
+   */
+  project(schemaOrFactory, data, options) {
+    const registry = this.getService(ProjectionSchemaRegistry);
+    const resolver = schemaName => {
+      return registry.getSchema(schemaName);
+    };
+    return projectData(schemaOrFactory, data, {...options, resolver});
+  }
+}

+ 69 - 0
src/data-projector.spec.js

@@ -0,0 +1,69 @@
+import {expect} from 'chai';
+import {format} from '@e22m4u/js-format';
+import {DataProjector} from './data-projector.js';
+
+describe('DataProjector', function () {
+  describe('defineSchema', function () {
+    it('should register the given projection schema', 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;
+    });
+
+    it('should throw an error if the given name is already registered', function () {
+      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);
+    });
+  });
+
+  describe('hasSchema', function () {
+    it('should return true if the given name is registered', 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;
+    });
+  });
+
+  describe('project', function () {
+    it('should project the given data by the registered schema name', 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});
+      expect(res).to.be.eql({foo: 10});
+    });
+
+    it('should enable the strict mode by the strict option', function () {
+      const S = new DataProjector();
+      const res = S.project(
+        {foo: true, bar: false},
+        {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});
+    });
+  });
+});

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

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

+ 3 - 0
src/definitions/index.js

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

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

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

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

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

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

@@ -0,0 +1,41 @@
+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;
+}

+ 77 - 0
src/definitions/projection-schema-registry.js

@@ -0,0 +1,77 @@
+import {Service} from '@e22m4u/js-service';
+import {InvalidArgumentError} from '@e22m4u/js-format';
+import {validateProjectionSchemaDefinition} from './validate-projection-schema-definition.js';
+
+/**
+ * Projection schema registry.
+ */
+export class ProjectionSchemaRegistry extends Service {
+  /**
+   * Schema map.
+   *
+   * @type {Map<string, object>}
+   */
+  definitions = new Map();
+
+  /**
+   * Define schema.
+   *
+   * @param {object} schemaDef
+   * @returns {this}
+   */
+  defineSchema(schemaDef) {
+    validateProjectionSchemaDefinition(schemaDef);
+    if (this.definitions.has(schemaDef.name)) {
+      throw new InvalidArgumentError(
+        'Projection schema %v is already registered.',
+        schemaDef.name,
+      );
+    }
+    this.definitions.set(schemaDef.name, schemaDef);
+    return this;
+  }
+
+  /**
+   * Has schema.
+   *
+   * @param {string} schemaName
+   * @returns {boolean}
+   */
+  hasSchema(schemaName) {
+    return this.definitions.has(schemaName);
+  }
+
+  /**
+   * Get schema.
+   *
+   * @param {string} schemaName
+   * @returns {object}
+   */
+  getSchema(schemaName) {
+    const schemaDef = this.definitions.get(schemaName);
+    if (!schemaDef) {
+      throw new InvalidArgumentError(
+        'Projection schema %v is not found.',
+        schemaName,
+      );
+    }
+    return schemaDef.schema;
+  }
+
+  /**
+   * Get definition.
+   *
+   * @param {string} schemaName
+   * @returns {object}
+   */
+  getDefinition(schemaName) {
+    const schemaDef = this.definitions.get(schemaName);
+    if (!schemaDef) {
+      throw new InvalidArgumentError(
+        'Projection definition %v is not found.',
+        schemaName,
+      );
+    }
+    return schemaDef;
+  }
+}

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

@@ -0,0 +1,75 @@
+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);
+    });
+  });
+});

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

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

+ 33 - 0
src/definitions/validate-projection-schema-definition.js

@@ -0,0 +1,33 @@
+import {InvalidArgumentError} from '@e22m4u/js-format';
+import {validateProjectionSchema} from '../validate-projection-schema.js';
+
+/**
+ * Validate projection schema definition.
+ *
+ * @param {object} schemaDef
+ */
+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.',
+      schemaDef,
+    );
+  }
+  if (!schemaDef.name || typeof schemaDef.name !== 'string') {
+    throw new InvalidArgumentError(
+      'Projection schema 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 InvalidArgumentError(
+      'Projection schema must be an Object, but %v was given.',
+      schemaDef.schema,
+    );
+  }
+  validateProjectionSchema(schemaDef.schema);
+}

+ 65 - 0
src/definitions/validate-projection-schema-definition.spec.js

@@ -0,0 +1,65 @@
+import {expect} from 'chai';
+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 () {
+    const throwable = v => () => validateProjectionSchemaDefinition(v);
+    const error = s =>
+      format(
+        'Projection 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'));
+    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({name: 'mySchema', schema: {}})();
+  });
+
+  it('should require the "name" option to be a 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.',
+        s,
+      );
+    expect(throwable('')).to.throw(error('""'));
+    expect(throwable(10)).to.throw(error('10'));
+    expect(throwable(0)).to.throw(error('0'));
+    expect(throwable([])).to.throw(error('Array'));
+    expect(throwable({})).to.throw(error('Object'));
+    expect(throwable(null)).to.throw(error('null'));
+    expect(throwable(undefined)).to.throw(error('undefined'));
+    throwable('mySchema')();
+  });
+
+  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);
+    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(null)).to.throw(error('null'));
+    expect(throwable(undefined)).to.throw(error('undefined'));
+    throwable({})();
+  });
+
+  it('should validate the projection schema', function () {
+    const throwable = () =>
+      validateProjectionSchemaDefinition({name: 'mySchema', schema: {foo: 10}});
+    expect(throwable).to.throw(
+      'Property options must be a Boolean or an Object, but 10 was given.',
+    );
+  });
+});

+ 2 - 0
src/index.d.ts

@@ -1,4 +1,6 @@
 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 - 0
src/index.js

@@ -1,4 +1,6 @@
 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';

+ 1 - 1
src/project-data.js

@@ -5,7 +5,7 @@ import {validateProjectionSchema} from './validate-projection-schema.js';
  * Project data.
  *
  * @param {object|Function|string} schemaOrFactory
- * @param {object} data
+ * @param {*} data
  * @param {object} [options]
  * @returns {*}
  */