morphism
    TypeScript icon, indicating that this package has built-in type declarations

    1.12.3 • Public • Published

    Morphism

    npm npm bundle size (minified) npm Coveralls github CircleCI (all branches) Deps Greenkeeper badge

    In many fields of mathematics, morphism refers to a structure-preserving map from one mathematical structure to another. A morphism f with source X and target Y is written f : X → Y. Thus a morphism is represented by an arrow from its source to its target.

    https://en.wikipedia.org/wiki/Morphism

    • ⚛️ Write your schema once, Transform your data everywhere
    • 0️⃣ Zero dependencies
    • 💪🏽 Typescript Support

    Getting started

    Installation

    npm install --save morphism

    or in the browser

    <script src="https://unpkg.com/morphism/dist/morphism.js"></script>
    <script>
      const { morphism, createSchema } = Morphism
    </script> 

    Usage

    The entry point of a morphism is the schema. The keys represent the shape of your target object, and the values represents one of the several ways to access the properties of the incoming source.

    const schema = {
      targetProperty: 'sourceProperty'
    };

    Then use the morphism function along with the schema to transform any source to your desired target

    import { morphism } from 'morphism';
     
    const source = {
      _firstName: 'Mirza'
    };
     
    const schema = {
      name: '_firstName'
    };
     
    morphism(schema, source);
    {
      "name""Mirza"
    }

    You may specify properties deep within the source object to be copied to your desired target by using dot notation in the mapping value. This is one of the actions available to transform the source data

    const schema = {
      foo: 'deep.foo',
      bar: {
        baz: 'deep.foo'
      }
    };
     
    const source = {
      deep: {
        foo: 'value'
      }
    };
     
    morphism(schema, source);
    {
      "foo""value",
      "bar"{
        "baz""value"
      }
    }

    One important rule of Morphism is that it will always return a result respecting the dimension of the source data. If the source data is an array, morphism will outputs an array, if the source data is an object you'll have an object

    const schema = {
      foo: 'bar'
    };
     
    // The source is a single object
    const object = {
      bar: 'value'
    };
     
    morphism(schema, object);
    {
      "foo""value"
    }
     
    // The source is a collection of objects
    const multipleObjects = [{
      bar: 'value'
    }];
     
    morphism(schema, multipleObjects);
    [{
      "foo": "value"
    }]

    Example (TypeScript)

    import { morphism, StrictSchema } from 'morphism';
     
    // What we have
    interface Source {
      ugly_field: string;
    }
     
    // What we want
    interface Destination {
      field: string;
    }
     
    const source: Source = {
      ugly_field: 'field value'
    };
     
    // Destination and Source types are optional
    morphism<StrictSchema<Destination, Source>>({ field: 'ugly_field' }, source);
    // => {field: "field value"}
     
    // Or
    const sources = [source];
    const schema: StrictSchema<Destination, Source> = { field: 'ugly_field' };
    morphism(schema, sources);
    // => [{field: "field value"}]

    ▶️ Test with Repl.it

    Motivation

    We live in a era where we deal with mutiple data contracts coming from several sources (Rest API, Services, Raw JSON...). When it comes to transform multiple data contracts to match with your domain objects, it's common to create your objects with Object.assign, new Object(sourceProperty1, sourceProperty2) or by simply assigning each source properties to your destination. This can leads you to have your business logic spread all over the place.

    Morphism allows you to keep this business logic centralized and brings you a top-down view of your data transformation. When a contract change occurs, it helps to track the bug since you just need to refer to your schema

    TypeScript integration

    When you type your schema, this library will require you to specify each transformation for your required fields.

    schema

    schema-required-fields

    This library uses TypeScript extensively. The target type will be inferred from the defined schema.

    inferred field type

    When using an ActionFunction the input type is also inferred to enforce your transformations

    typed action function

    See below the different options you have for the schema.

    Docs

    📚 API documentation

    Morphism comes with 3 artifacts to achieve your transformations:

    1. The Schema

    A schema is an object-preserving map from one data structure to another.

    The keys of the schema match the desired destination structure. Each value corresponds to an Action applied by Morphism when iterating over the input data.

    Schema actions

    You can use 4 kind of values for the keys of your schema:

    • ActionString: A string that allows to perform a projection from a property
    • ActionSelector: An Object that allows to perform a function over a source property's value
    • ActionFunction: A Function that allows to perform a function over source property
    • ActionAggregator: An Array of Strings that allows to perform a function over source property

    Schema Example

    import { morphism } from 'morphism';
     
    const input = {
      foo: {
        baz: 'value1'
      }
    };
     
    const schema = {
      bar: 'foo', // ActionString: Allows to perform a projection from a property
      qux: ['foo', 'foo.baz'], // ActionAggregator: Allows to aggregate multiple properties
      quux: (iteratee, source, destination) => {
        // ActionFunction: Allows to perform a function over source property
        return iteratee.foo;
      },
      corge: {
        // ActionSelector: Allows to perform a function over a source property's value
        path: 'foo.baz',
        fn: (propertyValue, source) => {
          return propertyValue;
        }
      }
    };
     
    morphism(schema, input);
    // {
    //   "bar": {
    //     "baz": "value1"
    //   },
    //   "qux": {
    //     "foo": {
    //       "baz": "value1"
    //     }
    //   },
    //   "quux": {
    //     "baz": "value1"
    //   },
    //   "corge": "value1"
    // }

    ▶️ Test with Repl.it

    More Schema examples

    📚 Schema Docs

    1.1 Using a strict Schema

    You might want to enforce the keys provided in your schema using Typescript. This is possible using a StrictSchema. Doing so will require to map every field of the Target type provided.

    interface IFoo {
      foo: string;
      bar: number;
    }
    const schema: StrictSchema<IFoo> = { foo: 'qux', bar: () => 'test' };
    const source = { qux: 'foo' };
    const target = morphism(schema, source);
    // {
    //   "foo": "qux",
    //   "bar": "test"
    // }

    2. Morphism as Currying Function

    The simplest way to use morphism is to import the currying function:

    import { morphism } from 'morphism';

    morphism either outputs a mapping function or the transformed data depending on the usage:

    API

    morphism(schemaSchema, items?: any, type?: any)any

    📚 Currying Function Docs

    Currying Function Example

    // Outputs a function when only a schema is provided
    const fn = morphism(schema);
    const result = fn(data);
     
    // Outputs the transformed data when a schema and the source data are provided
    const result = morphism(schema, data);
     
    // Outputs the transformed data as an ES6 Class Object when a schema, the source data and an ES6 Class are provided
    const result = morphism(schema, data, Foo);
    // => Items in result are instance of Foo

    3. Morphism Function as Decorators

    You can also use Function Decorators on your method or functions to transform the return value using Morphism:

    toJsObject Decorator

    import { toJSObject } from 'morphism';
     
    class Service {
      @toJSObject({
        foo: currentItem => currentItem.foo,
        baz: 'bar.baz'
      })
      async fetch() {
        const response = await fetch('https://api.com');
        return response.json();
        // =>
        // {
        //   foo: 'fooValue'
        //   bar: {
        //     baz: 'bazValue'
        //   }
        // };
      }
    }
     
    // await service.fetch() will return
    // =>
    // {
    //   foo: 'fooValue',
    //   baz: 'bazValue'
    // }
     
    --------------------------------
     
    // Using Typescript will enforce the key from the target to be required
    class Target {
      a: string = null;
      b: string = null;
    }
    class Service {
      // By Using <Target>, Mapping for Properties `a` and `b` will be required
      @toJSObject<Target>({
        a: currentItem => currentItem.foo,
        b: 'bar.baz'
      })
      fetch();
    }

    toClassObject Decorator

    import { toClassObject } from 'morphism';
     
    class Target {
      foo = null;
      bar = null;
    }
    const schema = {
      foo: currentItem => currentItem.foo,
      baz: 'bar.baz'
    };
    class Service {
      @toClassObject(schema, Target)
      async fetch() {
        const response = await fetch('https://api.com');
        return response.json();
        // =>
        // {
        //   foo: 'fooValue'
        //   bar: {
        //     baz: 'bazValue'
        //   }
        // };
      }
    }
     
    // await service.fetch() will be instanceof Target
    // =>
    // Target {
    //   foo: 'fooValue',
    //   baz: 'bazValue'
    // }

    morph Decorator

    Utility decorator wrapping toClassObject and toJSObject decorators

    import { toClassObject } from 'morphism';
     
    class Target {
      foo = null;
      bar = null;
    }
    const schema = {
      foo: currentItem => currentItem.foo,
      baz: 'bar.baz'
    };
    class Service {
      @morph(schema)
      async fetch() {
        const response = await fetch('https://api.com');
        return response.json();
        // =>
        // {
        //   foo: 'fooValue'
        //   bar: {
        //     baz: 'bazValue'
        //   }
        // };
      }
      @morph(schema, Target)
      async fetch2() {
        const response = await fetch('https://api.com');
        return response.json();
      }
    }
    // await service.fetch() will be
    // =>
    // {
    //   foo: 'fooValue',
    //   baz: 'bazValue'
    // }
     
    // await service.fetch() will be instanceof Target
    // =>
    // Target {
    //   foo: 'fooValue',
    //   baz: 'bazValue'
    // }

    4. Default export: Morphism object

    Morphism comes along with an internal registry you can use to save your schema attached to a specific ES6 Class.

    In order to use the registry, you might want to use the default export:

    import Morphism from 'morphism';

    All features available with the currying function are also available when using the plain object plus the internal registry:

    // Currying Function
    Morphism(schemaSchema, items?: any, type?: any)any
     
    // Registry API
    Morphism.register(typeany, schema?: Schema);
    Morphism.map(typeany, data?: any);
    Morphism.setMapper(typeany, schemaSchema);
    Morphism.getMapper(type);
    Morphism.deleteMapper(type);
    Morphism.mappers

    🔗 Registry API Documentation

    More Schema examples

    Flattening or Projection

    import { morphism } from 'morphism';
    // Source data coming from an API.
    const source = {
      foo: 'baz',
      bar: ['bar', 'foo'],
      baz: {
        qux: 'bazqux'
      }
    };
    const schema = {
      foo: 'foo', // Simple Projection
      bazqux: 'baz.qux' // Grab a value from a deep path
    };
     
    morphism(schema, source);
    //=> { foo: 'baz', bazqux: 'bazqux' }

    ▶️ Test with Repl.it

    Function over a source property's value

    import { morphism } from 'morphism';
    // Source data coming from an API.
    const source = {
      foo: {
        bar: 'bar'
      }
    };
    let schema = {
      barqux: {
        path: 'foo.bar',
        fn: value => `${value}qux` // Apply a function over the source property's value
      }
    };
     
    morphism(schema, source);
    //=> { barqux: 'barqux' }

    ▶️ Test with Repl.it

    Function over a source property

    import { morphism } from 'morphism';
    // Source data coming from an API.
    const source = {
      foo: {
        bar: 'bar'
      }
    };
    let schema = {
      bar: iteratee => {
        // Apply a function over the source propery
        return iteratee.foo.bar;
      }
    };
     
    morphism(schema, source);
    //=> { bar: 'bar' }

    ▶️ Test with Repl.it

    Properties Aggregation

    import { morphism } from 'morphism';
    // Source data coming from an API.
    const source = {
      foo: 'foo',
      bar: 'bar'
    };
    let schema = {
      fooAndBar: ['foo', 'bar'] // Grab these properties into fooAndBar
    };
     
    morphism(schema, source);
    //=> { fooAndBar: { foo: 'foo', bar: 'bar' } }

    ▶️ Test with Repl.it

    Registry API

    📚 Registry API Documentation

    Register

    Register a mapper for a specific type. The schema is optional.

    Morphism.register(type: any, schema?: Schema);

    Map

    Map a collection of objects to the specified type

    Morphism.map(typeany, data?: any);

    Get or Set an existing mapper configuration

    Morphism.setMapper(typeany, schemaSchema);
    Morphism.getMapper(type);

    Delete a registered mapper

    Morphism.deleteMapper(type);

    List registered mappers

    Morphism.mappers;

    Contribution

    Similar Projects

    License

    MIT © Yann Renaudin

    Install

    npm i morphism

    DownloadsWeekly Downloads

    5,124

    Version

    1.12.3

    License

    MIT

    Unpacked Size

    90.5 kB

    Total Files

    12

    Last publish

    Collaborators

    • emyann