@mazeltov/model

Mazeltov model library

Usage no npm install needed!

<script type="module">
  import mazeltovModel from 'https://cdn.skypack.dev/@mazeltov/model';
</script>

README

Requirements

This tool is designed to work with postgres (even though knexjs is used). It may work just fine with MySQL but

Defining a Model (Basic Example)

You have to make sure a table exists in your Postgres DB with these columns ahead of time.

const {
  modelFromContext,
  // these are action producers
  creator,
  getter,
  lister,
  updator,
  remover,
  iser,
} = require('./modelLib');

const iface = modelFromContext({
  // DB must be knexjs using postgres. Also should handle snake to camel case conversion
  // See Mazeltov app for example
  db,
  // it is recommended instead to pass @mazeltov/logger from higher context
  logger: global.console,
  entityName: 'fooOwner',
  // default of 'id' if not specified. Can be array for composite keys
  key: 'personId',
  selectColumns: [
    'personId',
    'favoriteFoo',
    'fooName',
    'createdAt',
    'updatedAt',
  ],
  createColumns: [
    'personId',
    'fooName',
    'favoriteFoo',
  ],
  updateColumns: [
    'favoriteFoo',
  ],
}, [
  creator,
  getter,
  lister,
  updator,
  remover,
  iser,
]);

(async () => {

  const newFoo = await iface.create({
    personId: 12,
    fooName: 'Giuseppe',
    favoriteFoo: 'Test',
  });

  console.log(newFoo);

})();

What is a model?

Lets start with MVC.

In the MVC paradigm, you have three components, a Model, a View, and a Controller

  • A controller accepts input from a previous view
  • The controller maps the input from the view into what the model needs
  • The model uses the data to "model" business logic and write to DB
  • A new view is produced (could be html, json, shell output)
  • The cycle repeats when the view passes data back to the controller (user submits html form or types shell input)

In this paradigm, the model should be the same regardless of whether

  • input comes from a CLI interface
  • input comes from an HTTP request
  • input comes from a message broker (like RabbitMQ)

I Need To Do X

There are always going to be special cases where much much more is needed and that is okay! There are plenty of ways to override each generated model method and insert hooks to change the arguments and result from the method call.

Transactions

This is the recommended pattern for transaction handling (when using multiple actions across models.


// someOtherModel would be passed from ctx.models
const someOtherModel = /*...*/

const fooModel = modelFromContext(/*...*/);

const createFoo = async ( args = {}, passedTrx = null) => {

  const trx = passedTrx === null ? await db.transaction() : passedTrx;

  try {
    await fooModel.create(args, trx);
    await someOtherModel.create(args, trx);
    trx.commit();
  } catch (e) {
    trx.rollback();
  }

};

Decorator Pattern (Recommended Approach)

If you need to extend the default behavior of the actions (create, get, update, remove, list), you can decorate default model

const {
  modelFromContext,
  creator,
  getter,
  lister,
  updator,
  remover,
  iser,
} = require('./modelLib');

module.exports = (ctx) => {

  const iface modelFromContext({
    ...ctx,
    entityName: 'fooOwner',
    key: 'personId',
    selectColumns: [
      'personId',
      'favoriteFoo',
      'fooName',
      'createdAt',
      'updatedAt',
    ],
    createColumns: [
      'fooName',
      'favoriteFoo',
    ],
    updateColumns: [
      'favoriteFoo',
    ],
  }, [
    creator,
    getter,
    lister,
    updator,
    remover,
    iser,
  ]);

  // You can wrap the default method here. This is what's meant by decorating. While ES6 class decorators
  // would be nice, the functional paradigm is generally used in code based.
  const createFooOwner = async ( args = {} ) => {

    // do something custom with the args
    args.favoriteFoo = args.favoriteFoo + ' is da best!';

    return iface.create(args);

  };

  // You must splat the default interface here
  return {
    ...iface,
    // It is advised to export a shorthand and fully qualified method name
    createFooOwner,
    create: createFooOwner,
  };

}

Action Hooks (An Alternative)

  • onWill{Action} : This will transform args before getting sent to database. For example, you can change which columns are inserted/updated for create/update. You can change what gets used in where clause of list. You can also use this just for general side-effects before the action is performed, but you MUST return the first argument of your callback.

  • on{Action}Result This modifies the result returned. This shouldn't be used for standard actions like list, get, create, update, remove, but could be used for a very custom action. (isMostSpecial)

Overwriting Context (ctx)

When calling modelFromContext, you can override the context JUST for a specific method:

module.exports = (ctx) => modelFromContext({
  ...ctx,
  entityName: 'fooOwner',
  key: 'personId',
  selectColumns: [
    'personId',
  ],
}, [
  creator,
  getter,
  lister,
  remover,
  iser,
  // here is where you pass an array with the function as key 0
  // and the context override as key 1
  [
    updator,
    {
      fnName: 'markFinished',
      defaultUpdateArgs: { isfinished: true }
    }
  ],
  [
    updator,
    {
      fnName: 'markUnfinished',
      defaultUpdateArgs: { isfinished: false }
    }
  ]
]);
``

Roadmap

Things that could always help

  • More warnings for misconfigurations
  • More integration and unit test coverage