Morphism
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
- :atom_symbol: Write your schema once, Transform your data everywhere
- :zero: Zero dependencies
- 💪🏽 Typescript Support
Getting started
Installation
npm install --save morphism
Example (JavaScript)
import { morphism } from 'morphism';
const source = {
foo: 'baz',
bar: ['bar', 'foo'],
baz: {
qux: 'bazqux'
}
};
class Destination {
foo = null;
bar = null;
bazqux = null;
}
const schema = {
foo: 'bar[1]',
bar: (iteratee, source, destination) => {
return iteratee.bar[0];
},
bazqux: {
path: 'baz.qux',
fn: (propertyValue, source) => {
return propertyValue;
}
}
};
const classObjects = morphism(schema, source, Destination);
const jsObjects = morphism(schema, source);
Example (TypeScript)
import { morphism, StrictSchema } from 'morphism';
interface Source {
ugly_field: string;
}
interface Destination {
field: string;
}
const source: Source = {
ugly_field: 'field value'
};
morphism<StrictSchema<Destination, Source>>({ field: 'ugly_field' }, source);
const sources = [source];
const schema: StrictSchema<Destination, Source> = { field: 'ugly_field' };
morphism(schema, sources);
▶️ 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
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.
You can use 4 kind of values in your schema:
ActionString
: A string that allows to perform a projection from a propertyActionSelector
: An Object that allows to perform a function over a source property's valueActionFunction
: A Function that allows to perform a function over source propertyActionAggregator
: 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',
quux: (iteratee, source, destination) => {
return iteratee.foo;
},
corge: {
path: 'foo.baz',
fn: (propertyValue, source) => {
return propertyValue;
}
}
};
morphism(schema, input);
▶️ 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);
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(schema: Schema, items?: any, type?: any): any
📚 Currying Function Docs
Currying Function Example
const fn = morphism(schema);
const result = fn(data);
const result = morphism(schema, data);
const result = morphism(schema, data, Foo);
3. Morphism as Function 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();
}
}
--------------------------------
class Target {
a: string = null;
b: string = null;
}
class Service {
@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();
}
}
- 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();
}
@morph(schema, Target)
async fetch2() {
const response = await fetch('https://api.com');
return response.json();
}
}
4. Morphism as Mixin
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 mixin plus the internal registry:
Morphism(schema: Schema, items?: any, type?: any): any
Morphism.register(type: any, schema?: Schema);
Morphism.map(type: any, data?: any);
Morphism.setMapper(type: any, schema: Schema);
Morphism.getMapper(type);
Morphism.deleteMapper(type);
Morphism.mappers
🔗 Registry API Documentation
More Schema examples
Flattening or Projection
import { morphism } from 'morphism';
const source = {
foo: 'baz',
bar: ['bar', 'foo'],
baz: {
qux: 'bazqux'
}
};
const schema = {
foo: 'foo',
bazqux: 'baz.qux'
};
morphism(schema, source);
▶️ Test with Repl.it
Function over a source property's value
import { morphism } from 'morphism';
const source = {
foo: {
bar: 'bar'
}
};
let schema = {
barqux: {
path: 'foo.bar',
fn: value => `${value}qux`
}
};
morphism(schema, source);
▶️ Test with Repl.it
Function over a source property
import { morphism } from 'morphism';
const source = {
foo: {
bar: 'bar'
}
};
let schema = {
bar: iteratee => {
return iteratee.foo.bar;
}
};
morphism(schema, source);
▶️ Test with Repl.it
Properties Aggregation
import { morphism } from 'morphism';
const source = {
foo: 'foo',
bar: 'bar'
};
let schema = {
fooAndBar: ['foo', 'bar']
};
morphism(schema, source);
▶️ 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(type: any, data?: any);
Get or Set an existing mapper configuration
Morphism.setMapper(type: any, schema: Schema);
Morphism.getMapper(type);
Delete a registered mapper
Morphism.deleteMapper(type);
List registered mappers
Morphism.mappers;
Contribution
Similar Projects
License
MIT © Yann Renaudin