quick-model

data modeling with serialization

Usage no npm install needed!

<script type="module">
  import quickModel from 'https://cdn.skypack.dev/quick-model';
</script>

README

quick-model

It provides serialization and deserialization using a defined data model. That's it.

What is it for?

Mapping data from A to B.

E.g. your database query returns snake_cased keys and embedded relationships but you want to send it to the client with camelCase keys and sideloaded relationships. quick-model can do that.

Another usecase is a client sends data to your server in a client-friendly format and you need to 'deserialize' it into yet another format and insert it into some 3rd party database like a SalesForce table with a wacky__c schema.

Quick install:

npm install quick-model

Quick model setup:

const {
  Model,
  Transforms
} = require('quick-model');

const {
  attr,
  one
} = Model;

const {
  stringTransform
} = Transforms;

const personModel = new Model({
  name: attr(stringTransform)
});

const bookModel = new Model({
  title: attr(stringTransform),

  author: one(personModel)
});

Quick serializer setup:

Imagine your database query returns an object (continuing the example above) :

const book = await db.books.findByTitle('foundation');
console.log(book);
// {
//   book_title: 'Foundation',
//   book_author: {
//     full_name: 'Isaac Asimov'
//   }
// }

Notice the differences between db result and defined model.
database -> model
book_title -> title
book_author -> author
full_name -> name

This is where quick-model shines. Let us make a quick serializer!

const { Serializer } = require('quick-model');

const personSerializer = new Serializer({
  model: personModel,

  keyForNonSerializedAttribute(attribute) {
    const { name } = attribute;

    // attribute.name is the key you defined when creating an attr() in your model.
    if (name === 'name') {
      return 'full_name';
    }

    // default behavior
    return name;
  }
});

const bookSerializer = new Serializer({
  model: bookModel,

  serializers: {
    author: personSerializer
  },

  keyForNonSerializedAttribute(attribute) {
    const { name } = attribute;

    return `book_${name}`;
  },

  keyForNonSerializedRelationship(relationship) {
    const { name } = relationship;

    if (name === 'author') {
      return 'book_author';
    }

    return name;
  }
});

Now you can call bookSerializer.serialize(dataFromDatabase).
It will properly extract the fields from the raw data and , by default, return an object that resembles how the model was defined:

bookSerializer.serialize({
  book_title: 'Foundation',
  book_author: {
    full_name: 'Isaac Asimov'
  }
});
// returns:
{
  title: 'Foundation',
  author: {
    name: 'Isaac Asimov'
  }
}

This is a simple example. But you can model some really unfriendly data and serialize it into something friendly :)

Full docs coming as soon...

Some ideas I'd personally like to explore (or see explored) in future:

  • Building a JSON API Serializer
  • Building an XML Serializer

TODO FOR 1.0:

  • implement primaryKey in model and write tests!

  • create a map-compact util & test

  • tests working for both directories: lib, utils

  • test for deserializeAttribute, keyForDeserializedRelationship

  • test for serialize

  • test for deserialize

  • Cache for camelize and underscore

  • tests for camelize and underscore

  • move utils into files that represent the data type they operate on/with

    • array
    • string
    • function
    • object
  • deepAssign when overwriting transform's deserializer, serializer, and validator objects. I.e. don't overwrite entire object, just merge the defined properties

deepAssign({ serializer: { foo: bar } }, { serializer: { baz: 'boo' } })
// returns:
{ serializer: {
    foo: 'bar',
    baz: 'boo'
  }
}
  • in each directory, combine tests for that directory and place in tests/ directory relative to where each test currently is

  • split deserialize, serialize, and normalize functionality into separate mixins

  • re-organize serializer tests into the appropriate mixins/tests/*-test.js file

  • { include: [], exclude: [] } options support during serialize, deserialize, and normalize

  • serializer.serialize/deserialize should accept a hash of 3rd party serializers in the event of embedded relationships. So that serializing an embedded relationship can use the correct serializer.

  • serializer.normalize

  • accept hash of filter functions that can be applied to attributes or relationships.e.g. { filters: { password(x) { return x.replace('.+', '*') } } }