Write your own mixin #

TL-DR #

  • create a js file what exports 2 function
module.exports = {
  initDomain: null,
  initEntity: initEntityForMyMixin
}
/**
 * My mixin descriptin
 * @param {UBEntity} entity Entity for initialization
 * @param {UBEntityMixin} mixinCfg Mixin configuration from entity metafile
 */
function initEntityForTestMixin(entity, mixinCfg) {
  // verify mixinCfg
  // add methods / events etc for entity
}
  • in model entrypoint register a mixin
const UB = require('@unitybase/ub')
const myMixinImpl = require('./myMixin.js')
UB.registerMixinModule('myMixin', myMixinImpl)
  • now new mixin can be used in entity *.meta file as such
"mixins": {
  "myMixin": {
    "mixinCfgParam": "param value"
  }
}

Enabling\disabling mixins #

Each mixin can be implicitly enable on domain level for all entities by adding it code into domain.implicitlyAddedMixins array in ubConfig. This is how audit mixin is adding and multitenancy (in case security.multitenancy.enabled: true). If implicitlyAddedMixins is defined in config, audit should be added into array to be enabled

For individual entity any mixin can be implicitly disabled by specifying enabled: false in entity meta file. For example, to disable an audit mixin in entity.meta:

{
  "mixins": {
    "audit": {
      "enabled": false
    }
  }
}

Implementation tips #

Adding new attributes/entities #

Sometimes mixin should add a new attributes\entities into domain. This can be done using metadata transformation hook what can mutate a Domain JSON.

About metadata hooks see Application initialization section of @unitybase/ub module documentation

Metadata transformation hook is called on the domain loading stage while server is in single-thread mode.

In opposite mixin module is evaluated for each working thread

The real-life code what mutates a Domain and adds additional attributes can be found in [@unitybase/ub] model - see sources @unitybase/ub/_hookMetadataTransformation.js

Logging with log level #

Wrap methods in logEnter / logLeave block to get a better logging + time profile for free. Since @unitybase/ub@5.22.39 there is a service function App.wrapEnterLeaveForUbMethod

const App = require('@unitybase/ub').App
// use pattern below for method enter text - the same as native method do
entytModule.select = App.wrapEnterLeaveForUbMethod(`method(${MIXIN_NAME}) ${entity.name}.select`, myMixinSelect)
entityModule.entity.addMethod('select')
function myMixinSelect(cxt) {
  console.debug('some debug (shifted by recursion level automatically)')
}

Before 5.33.39 use a polyfill

function wrapEnterLeave (enterText, methodImpl) {
  return function enterLeave(ctx) {
    App.logEnter(enterText)
    try {
      methodImpl(ctx)
    } finally {
      App.logLeave()
    }
  }
}

The logging will be (first and last lines are added by App.logEnter / App.logLeave, all console.* output from inside select method are shifted by recursion level)

20210314 09224807  "  +    	method(myMixin) my_entity.select
20210314 09224807  " debug 		some debug (shifted by recursion level automatically)
20210314 09224807  "  -    	00.005.124

Events order #

Mixin's initialization are executed AFTER all models entry point is evaluated, so if some model code adds an event handler

// my_entity.js
me.on('insert:before', function myEntityInsertBefore(ctx) {
  console.debug('my_entity insert:before')
})

handler insert:before, added by mixin entity initialization function will be executed after myEntityInsertBefore.

To add an event handler what executed BEFORE model handlers use a prependListener instead of on (or even both ot them if needed)

// myMixin.js
function initEntityForMyMixin(entity, mixinCfg) {
  /** @type {EntityNamespace} */
  const entityModule = global[entity.name]
  function myMixinInsert(ctx) {
    console.debug('insert called')
  }
  entityModule.insert = wrapEnterLeave(`method(${MIXIN_NAME}) ${entity.name}.insert`, myMixinInsert)
  entityModule.entity.addMethod('insert')
  entityModule.prependListener('insert:before', (ctx) => {
    console.debug('myMixin insert:before (called BEFORE myEntityInsertBefore)')
  })
  entityModule.on('insert:before', (ctx) => {
    console.debug('myMixin insert:before (called AFTER myEntityInsertBefore)')
  })
  
}

in the sample above execution of insert method produce such log output:

20210314 09224807  "  +    ubql?rq=my_entity.insert&uitag=frm-my_entity
20210314 09224807  " http  	127.0.0.1 -> POST ubql?rq=my_entity.insert&uitag=frm-my_entity
20210314 09224807  " debug 	myMixin insert:before (called BEFORE myEntityInsertBefore)
20210314 09224807  " debug 	my_entity insert:before
20210314 09224807  " debug 	myMixin insert:before (called AFTER myEntityInsertBefore)
20210314 09224807  "  +    	method(myMixin) my_entity.insert
20210314 09224807  " debug 		some debug (shifted by recursion level automatically)
20210314 09224807  "  -    	00.005.124
20210314 09224807  "  +    	method(TubAuditMixin) my_entity.afterinsert
.....
20210314 09224807  "  -    	00.000.656
20210314 09224807  " DB    	Commit connection "main" in 123us
20210314 09224807  " http  	127.0.0.1 <- 200
20210314 09224807  "  -    00.009.560

Debugging log performance #

In production mode Debug log level is usually disable, so for debug output prefer

console.debug('some debug', i) // fast in case debug log level is off

instead of template string

console.debug(`some debug ${i}`) // calculate a string from template before pass it as argument even if debug is off

Debugging experience #

For better debugging experience try to avoid anonymous functions and lodash. Most Lodash functions creates a deep call stack. Anonymous functions are shown with pure names in call stack.

// my_entity.js
me.on('insert:before', function myEntityInsertBefore(ctx) {
  console.debug('my_entity insert:before')
})