5.0.44 • Published 2 years ago

@dta5/npm-dmx-utils v5.0.44

Weekly downloads
-
License
UNLICENSED
Repository
gitlab
Last release
2 years ago

npm-dmx-utils

DMXSchema

The DMXSchema introduces an extension to mongoose.Schema with built-in support for DMX Entitlements.

The basic configuration allows for an entitlements field on each of the schema's defined fields with the ability to configure visibility and editability for each field in the schema. For processing data before sending it to a user the DMXSchema adds sanitize methods on the instance and the model. For protecting data from being modified by those without the correct entitlement it provides an instance method setForUser.

Schema configuration

docinfo

DMXSchema updates automatically docinfo.createdAt and docinfo.updatedAt fields.

To avoid adding docinfo field, pass dmx.skipDocinfo (a nested field inside dmx field) param to the options (second param of DMXSchema constructor) e.g. for subschemas.

As default docinfo has the following fields available:

createdAt: Date,
createdBy: String,
updatedAt: Date,
updatedBy: String,
deletedAt: Date,
deletedBy: String,

If you want to add any more fields to docinfo, just pass docinfo.yourFieldName (a nested field inside docinfo field) with the field definition to the schema (first param of DMXSchema constructor). Then docinfo will contain all the default fields and your field as well. This doesn't mean DMXSchema will modify your field, it's only a definition so without defining custom fields, an error will be thrown for an attempt of using it.

Audits

DMXSchema can be configured to store an audit trail on documents stored in the database. This feature is configurable per field within a schema. When a document is updated, all fields which are configured to be auditable, will be stored in a separate collection with information on what the previous and new values are as well as information about the user that made those changes. To enable audits for a schema, you must define the collection to which the audit documents will be stored.

const schema = new DMXSchema({
  auditField: { type: Boolean, audit: true },
  dontAuditField: { type: Boolean },
}, {
  dmx: {
    audit: {
      collection: 'auditCollection'
    }
  }
})

Entitlements

The DMXSchema supports a custom option field called entitlements which contains up to four sub-options:

  • view an array of entitlement names of which the user must have at least one to be able to view this field
  • conditionalView a method for custom logic which adds another layer to the view above to further reduce visibility. It should return true if the field should be visible to the user and false if it should not be visible.
  • edit an array of entitlement names of which the user must have at least one to be able to edit the field
  • conditionalEdit a method for custom logic which adds another layer to the edit above to further reduce editability. It should throw an EntitlementError if the edit is disallowed.
const mongoose = require('mongoose');
const { getSchema, EntitlementError } = require('@dmx/npm-dmx-utils').DMXSchema;

const DMXSchema = getSchema(mongoose);

const someSchema = new DMXSchema({
  // Fields defined without an `entitlements` option will be invisible to and uneditable by users.
  hiddenA: String,
  hiddenB: { type: Number },

  // Fields with a `*` view are visible to all users
  visibleToAll: {
    type: String,
    entitlements: {
      view: ['*']
    }
  },

  // Users with either `entitlementA` or `entitlementB` will be able to view this field but only
  // those with `entitlementA` will be able to modify it.
  basicField: {
    type: String,
    entitlements: {
      view: ['entitlementA', 'entitlementB'],
      edit: ['entitlementA']
    }
  },

  // Users with any entitlement beginning with `entitlementC` will be able to view this field
  // e.g. `entitlementC.create`, `entitlementC.remove`, `entitlementC.all`
  visibleToAnyEntitlementC: {
    type: String,
    entitlements: {
      view: ['entitlementC.*'],
    }
  },

  // `conditionalView` builds on the `view` limitations. In this example the user must
  // at least have `entitlementA`. The other restrictions are described below.
  customViewField: {
    type: Boolean,
    entitlements: {
      view: ['entitlementA'],
      conditionalView: function (options) {
        // This function is run so that `this` refers to the current document which provides access
        // to the document's fields and virtuals.
        if (this.hiddenB < 10) {
          return false;
        }

        // The `options` argument can contain additional information about the user and its
        // entitlements, `orgId`, etc. Entitlement restrictions don't have to follow any
        // specific pattern but can be created as needed.
        const { restriction } = options.entitlements.entitlementA;
        if (restriction.hiddenA && restriction.hiddenA !== this.hiddenA) {
          return false;
        }
      }
    },

    // `conditionalEdit` builds on the `edit` limitations. In this example the user must
    // at least have `entitlementB`. The other restrictions are described below.
    customEditField: {
      type: Number,
      entitlements: {
        edit: ['entitlementB'],
        conditionalEdit: function (value, options) {
          if (this.hiddenA !== 'active') {
            throw new EntitlementError('cannot edit `customEditField` while inactive');
          }

          const { restriction } = options.entitlements.entitlementA;
          if (value > restriction.maxCustomEditFieldValue) {
            throw new EntitlementError('you cannot set `customeEditField` to greater than ...');
          }
        }
      }
    }
  }
});

setForUser instance method

The DMXSchema provides an instance method setForUser which is an analog to the standard mongoose set method but takes an additional argument. This argument is passed in as the options argument to the conditionalEdit function. It is expected to at least contain entitlements, userId and orgId but any custom fields can be included.

The document.setForUser method supports two different argument formats:

  • document.setForUser(key, value, options) e.g. foo.setForUser('bar', 'baz', { entitlements...})
  • document.setForUser(object, options) e.g. foo.setForUser({ bar: 'baz' }, { entitlements...})

Virtual fields

The schema also supports entitlements configuration for viewing/editing limitations on virtuals

schema.virtual('virtualField', {
  entitlements: {
    view: ['entitlementA'],
    conditionalView: function(options) {
      return options.orgId === this.dealerId;
    },
    edit: ['entitlementB'],
    conditionalEdit: function (value, options) {
      if (value > this.someOtherField && !options.isDMXUser) {
        throw new EntitlementError('nope');
      }
    }
  }
}).get(...).set(...);

EntitlementError

The EntitlementError class extends Error and supports the following fields

  • requiredEntitlements an array of entitlements required to successfully execute the rejected action
  • field string containing the name of the field which was unable to be updated
  • collection name of the model collection
  throw new EntitlementError('Some error message', {
    requiredEntitlements: ['entitlementA', 'entitlementB'],
    field: 'some.field.path',
    collection: 'someCollection'
  });

Model.sanitize method

This method will return plain object(s) which only include the fields which should be visible to the user (based on the options object). It supports single documents and arrays of documents

  • sanitizedArray = Model.sanitize([documents], options)
  • sanitizedDocument = Model.sanitize(document, options)

document.sanitize

This method will return a plain object which only includes the fields which should be visible to the user (based on the options object)

  • document.sanitize(options)

Message queue clients

In addition to publishing/subscribing clients encapsulate logic to create queues, exchanges and bindings to avoid the need to create queues manually in RabbitMQ console in all environments. These operations are idempotent.

PubMessageQueueClient

publish()

Publishes message with routingKey (that is basically event name) to exchange with exchangeName name. In case such exchange is missing it'll be created to avoid getting Exchange does not exist error.

SubMessageQueueClient

Subscribes handlers to messages from certain queues. In case corresponding queues and exchanges are missing they'll be created to avoid getting Queue does not exist and similar errors. In addition queues are ensured to be bounded to exchanges.