npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@quickcase/express-sdk

v0.9.0

Published

Development kit to build QuickCase-flavoured ExpressJS applications

Downloads

230

Readme

express-sdk

CI

Development kit to build QuickCase-flavoured ExpressJS applications.

Supported NodeJS versions:

  • 16 (deprecated)
  • 18
  • 20

Documentation

ACL

Set of helpers to work with QuickCase's Access Control List.

check(verb)(userRoles)(acl)

Parameters:

  • verb: string, one of 'create', 'read', 'update' or 'delete'
  • userRoles: string[], list of roles assigned to the user
  • acl: object[], the ACL object to evaluate

Returns a truthy value (the effective permission) if the ACL grants the verb to any of the provided userRoles.

checkAny(verbs)(userRoles)(acl)

Parameters:

  • verbs: string[], list of one or many of 'create', 'read', 'update' or 'delete'
  • userRoles: string[], list of roles assigned to the user
  • acl: object[], the ACL object to evaluate

Returns a truthy value (the effective permission) if the ACL grants at least one (any) of the verbs to any of the provided userRoles.

checkAll(verbs)(userRoles)(acl)

Parameters:

  • verbs: string[], list of one or many of 'create', 'read', 'update' or 'delete'
  • userRoles: string[], list of roles assigned to the user
  • acl: object[], the ACL object to evaluate

Returns a truthy value (the effective permission) if the ACL grants every one (all) of the verbs through one or many of the provided userRoles.

ACL v2

Set of helpers to work with QuickCase's Access Control List version 2.

Differences in version 2 compared to version 1 (legacy):

  • Use 4-bit binary numbers to represent permissions where bits from left to rights are C, R, U and D
  • Group permissions by role into an object where keys are the role and values the permissions

Where in v1 an ACL would be:

const acl = [
  {role: 'role-1', create: true, read: true, update: false, delete: false},
  {role: 'role-2', create: true, read: true, update: true, delete: true},
]

In v2, that same ACL will be:

const acl = {
  'role-1': 0b1100, // 12
  'role-2': 0b1111, // 15
}

check(verb)(userRoles)(acl)

Parameters:

  • verb: number, a single verb to check for (one of AclV2.CREATE, AclV2.READ, AclV2.UPDATE or AclV2.DELETE)
  • userRoles: string[], list of roles assigned to the user
  • acl: Object.<string, number>, the ACL object to evaluate

Returns a truthy value (the effective role) if the ACL grants the verb to any of the provided userRoles.

import {AclV2} from '@quickcase/express-sdk';

const userRoles = ['role-1', 'role-2'];
const acl = {
  'role-1': AclV2.CREATE,
  'role-2': AclV2.READ | AclV2.UPDATE,
};

AclV2.check(AclV2.CREATE)(userRoles)(acl);
// Returns: 'role-1'

AclV2.check(AclV2.READ)(userRoles)(acl);
// Returns: 'role-2'

AclV2.check(AclV2.DELETE)(userRoles)(acl);
// Returns: false

checkAny(verbs)(userRoles)(acl)

Parameters:

  • verbs: number[], one or many of AclV2.CREATE, AclV2.READ, AclV2.UPDATE or AclV2.DELETE
  • userRoles: string[], list of roles assigned to the user
  • acl: Object.<string, number>, the ACL object to evaluate

Returns a truthy value (the effective role) if the ACL grants at least one (any) of the verbs to any of the provided userRoles.

checkAll(verbs)(userRoles)(acl)

Parameters:

  • verbs: number[], one or many of AclV2.CREATE, AclV2.READ, AclV2.UPDATE or AclV2.DELETE
  • userRoles: string[], list of roles assigned to the user
  • acl: Object.<string, number>, the ACL object to evaluate

Returns a truthy value (the effective permission) if the ACL grants every one (all) of the verbs through one or many of the provided userRoles.

fromLegacy(legacyAcl)

Parameters:

  • legacyAcl: Object[], a legacy ACL as per version 1

Returns the equivalent v2 ACL.

toBinary(permission)

Parameters:

  • permission: string or Object, either a 'CRUD' string or a legacy permission object (v1)

Returns the 4-bit binary representation of the permission.

API clients

ApiClient(factory)(options)(req)

Build a new instance of an API client where:

  • factory: A function called with an Axios instance as argument and returning a map of available API functions to be exposed by the client
  • options: An object composed of:
    • baseURL: String, required. The base URL to use for all API calls made by the Axios instance.
    • accessTokenProvider: Function, optional. Async function taking the ExpressJS Request as argument and returning the Promise of an access token to use as Authorization Bearer.
  • req: ExpressJS Request object to tie API call abort signal to the request's own abort signal.

Example usage:

import {ApiClient} from '@quickcase/express-sdk';

const DefinitionApiClient = ApiClient((axiosInstance) => ({
  getType: (typeId) => axiosInstance.get(`/api/data/case-type/${typeId}`, {
    headers: {
      'accept': 'application/vnd.app.quickcase.store.definition.api.case-type.v2+json;charset=UTF-8',
    },
  }),
}));

const client = DefinitionApiClient({
  baseURL: 'https://test.quickcase.app',
  accessTokenProvider: (req) => Promise.resolve('access-token-123'),
})(req);

const type1 = await client.getType('type-1');

Async

asyncMiddleware(middleware)

In Express v4, errors thrown in async functions within a middleware must be manually caught and passed to next(). In Express v5, this will be done automatically. In the meantime, this decorator offers a behaviour similar to the one of Express v5 where any decorated middleware returning a Promise will automatically call next(error) whenever the promise is rejected.

Usage:

import {asyncMiddleware} from '@quickcase/express-sdk';

const unsafeMiddleware = (req, res) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // Some async code throwing an error
      reject('error');
    }, 1);
  })
};

const safeMiddleware = asyncMiddleware(unsafeMiddleware);

Condition

Parsing and evaluation of conditional logic for cases.

evaluate(extractor)(condition)

Evaluates a parsed condition against an instance of a record.

Parameters:

  • extractor: Instance of Record.extractor partially applied with a record
  • condition: Condition object, as returned by Condition.parse

Returns a boolean with a value of true if the condition matched the record instance, false otherwise.

import {Condition, Record} from '@quickcase/express-sdk';

const record = {...};
const extractor = Record.extractor(record);
const condition = Condition.parse(...);

Condition.evaluate(extractor)(condition);
// -> true or false

parse(conditionString)

Validate condition syntax and parse the condition into a normalised 2-dimensional array of criteria where the first dimension represents disjunctions (OR) and the second dimension represents conjunctions (AND).

Usage:

import {Condition} from '@quickcase/express-sdk';

const condition = Condition.parse(`
  complex.field1 = "value 1" AND (
    field2 === "value 2" OR NOT field3 MATCHES "^[a-z]+"
  )
`);

//Output:
{
  disjunctions: [
    [
      {path: 'complex.field1', operator: 'EQUALS', value: 'value 1', ignoreCase: true},
      {path: 'field2', operator: 'EQUALS', value: 'value 2'},
    ],
    [
      {path: 'complex.field1', operator: 'EQUALS', value: 'value 1', ignoreCase: true},
      {path: 'field3', operator: 'MATCHES', value: '^[a-z]+', negated: true},
    ],
  ],
  fieldPaths: ['complex.field1', 'field2', 'field3'],
}

Config

Uses config combined with js-yaml to load and consume YAML-based configurations using type-safe wrapper and shorthands.

Example

Given config:

level1:
  level2:
    null: null
    number: 1337
    numberStr: '42'
    trueStr: 'true'

Then:

import {Config} from '@quickcase/express-sdk';

const {asBoolean, asNumber, asString, nullOr, raw, shorthand} = Config;

const level2 = shorthand('level1.level2.');

level2('null'); // => null
level2('number'); // => '1337'

level2('null', raw); // => null
level2('null', asString); // => 'null'
level2('numberStr', asNumber); // => 42
level2('trueStr', asBoolean); // => true
level2('numberStr', asBoolean); // => false

level2('null', nullOr(asNumber)); // => null
level2('number', nullOr(asNumber)); // => 1337

Definition

extractField(normalisedType)(path)

Extract the definition of a field from the given normalised type definition using field paths. This also supports extracting metadata definitions as if they were standard fields with ACLs inherited from type.

:warning: Please note, this relies on normalised type definition, as returned by Definition.normaliseType(type).

When a checkAcl function is provided as an option, the extractor only return items which passed the ACL check. Items which did not pass the check will be returned as undefined (like if they didn't exist).

  • When path is a string: Definition of the specified field; or undefined if path cannot be found in fields.
  • When path is an array: An array of the same size, with extracted definitions in the same position as their respective path. Paths not found are extracted as undefined.
  • When path is an object: An object of the same shape, with extracted definitions in place of the paths. Paths not found are extracted as undefined.

This function aims to mirror Record.extractor().

import {Definition} from '@quickcase/express-sdk';

const type = {...}; // As returned by Definition.normaliseType(rawType)

const extractor = Definition.extractField(type);

// Extracting metadata definition
extractor('[workspace]');
extractor('[type]');
extractor('[state]');
extractor('[id]');
extractor('[classification]');
extractor('[created]');
extractor('[modified]');

// Extracting field definition
extractor('field1');
extractor('level1.level2.nestedField');

// Extracting definition of collection items
extractor('collectionField[].value'); // Any item
extractor('collectionField[0].value'); // By item index, zero-based
extractor('collectionField[id:abc123].value'); // By item ID

// Extracting multiple paths at once
extractor(['[state]', 'field1', 'field2']);
extractor({state: '[state]', someField: 'path.to.some.field'});

normaliseFields(fields)

Normalises an array of fields returned as part of a case type by definition-store into a structure easier to consume.

This includes:

  • Trimming all null/empty properties to reduce payloads
  • Explicitly inheriting ACL from complex parents to their members and from collection to their content
  • Applying ACL overrides on complex members and collection content
  • Converting ACLs into objects of binary permissions (ACL v2)
  • Sorting array elements with explicit order property and dropping the order property

The resulting structure is closely aligned with anticipated contract of definition-store v5, hence future-proofing consumers of the normalised structure. Upon release of definition-store v5, this normalisation process should not be required any longer.

import {Definition} from '@quickcase/express-sdk';

const fields = [
  // Array of field definitions as returned by definition-store 
];

Definition.normaliseFields(fields)
// Returns: Object, normalised fields indexed by ID

normaliseActionLayout(fields)(action, layout)

Normalises the layout of an action (steps and submit). This includes:

  • Ordering steps, columns, fields and composite field members (removing need for order property)
  • Normalising step structure
  • Normalising step fields
  • Explicitly expanding the layout of composite fields without overrides from their definition (incl. member condition)
  • Explicitly expanding the layout of composite fields with overrides from their overrides

The resulting structure is closely aligned with anticipated contract of definition-store v5, hence future-proofing consumers of the normalised structure. Upon release of definition-store v5, this normalisation process should not be required any longer.

import {Definition} from '@quickcase/express-sdk';

const fields = {
  // Object of fields, as returned by `Definition.normaliseFields()`
};

const action = {
  // Object as returned by definition-store as part of case type (JSON path: .events)
};

const layout = {
  // Object as returned by definition-store as wizard pages
}

Definition.normaliseActionLayout(fields)(action, layout)
// Returns: Object, normalised steps and submit layout

normaliseSearchInputsLayout(type)(layout)

Normalises the layout of the search inputs view. This includes:

  • Ordering fields and composite field members (removing need for order property)
  • Explicitly populating layout of complex members from definition

The resulting structure is closely aligned with anticipated contract of definition-store v5, hence future-proofing consumers of the normalised structure. Upon release of definition-store v5, this normalisation process should not be required any longer.

import {Definition} from '@quickcase/express-sdk';

const type = {
  // Normalised type, as returned by `Definition.normaliseType(type)`
};

const layout = {
  // Object as returned by definition-store for search or workbasket inputs
}

Definition.normaliseSearchInputsLayout(type)(layout)
// Returns: Object, normalised inputs layout

normaliseSearchResultsLayout(type)(layout)

Normalises the layout of the search results view. This includes:

  • Ordering fields and composite field members (removing need for order property)
  • Explicitly populating layout of complex members from definition

The resulting structure is closely aligned with anticipated contract of definition-store v5, hence future-proofing consumers of the normalised structure. Upon release of definition-store v5, this normalisation process should not be required any longer.

import {Definition} from '@quickcase/express-sdk';

const type = {
  // Normalised type, as returned by `Definition.normaliseType(type)`
};

const layout = {
  // Object as returned by definition-store for search or workbasket results
}

Definition.normaliseSearchResultsLayout(type)(layout)
// Returns: Object, normalised results layout

normaliseStates(states)

Normalises an array of states returned as part of a case type by definition-store into a structure easier to consume.

This includes:

  • Trimming all null/empty properties to reduce payloads
  • Converting ACLs into objects of binary permissions (ACL v2)

The resulting structure is closely aligned with anticipated contract of definition-store v5, hence future-proofing consumers of the normalised structure. Upon release of definition-store v5, this normalisation process should not be required any longer.

import {Definition} from '@quickcase/express-sdk';

const states = [
  // Array of states definitions as returned by definition-store 
];

Definition.normaliseStates(states)
// Returns: Object, normalised states indexed by ID

normaliseType(type)

Normalises schema, workflow and security parts of a type definition (ie. everything but layouts).

Internally, this delegates normalisation to specialised helpers like normaliseFields(fields) and normaliseStates(states).

The resulting structure is closely aligned with anticipated contract of definition-store v5, hence future-proofing consumers of the normalised structure. Upon release of definition-store v5, this normalisation process should not be required any longer.

import {Definition} from '@quickcase/express-sdk';

const type = {
  // Raw type as returned by definition-store 
};

Definition.normaliseType(type)
// Returns: Object, normalised type

normaliseViewLayout(fields)(layout)

Normalises the layout of the record view (tabs). This includes:

  • Ordering groups, fields and composite field members (removing need for order property)
  • Normalising group structure
  • Normalising group fields
  • Explicitly expanding the layout of composite fields without overrides from their definition (incl. member condition)

The resulting structure is closely aligned with anticipated contract of definition-store v5, hence future-proofing consumers of the normalised structure. Upon release of definition-store v5, this normalisation process should not be required any longer.

import {Definition} from '@quickcase/express-sdk';

const fields = {
  // Object of fields, as returned by `Definition.normaliseFields()`
};

const layout = {
  // Object as returned by definition-store as display/tab-structure
}

Definition.normaliseViewLayout(fields)(action, layout)
// Returns: Object, normalised groups for record view

Logging

AccessLogger

Express middleware generating access log upon response termination (finished or interrupted).

import {AccessLogger} from '@quickcase/express-sdk';

//...

const app = express();

app.use(AccessLogger());

The access logger middleware can be customised by providing an optional config object formed of:

  • logger: Optional, a Winston's logger instance (see Logger below); use default logger if omitted
  • formatter: Optional, a function formatting the req/res pair into a string message; use default formatter if omitted
  • levelSupplier: Optional, a function supplying the logging level for a given req/res pair; use default level supplier if omitted

Logger

Pre-configured Winston logger factory.

import {Logger} from '@quickcase/express-sdk';

const logger = Logger('a-logger-name');

logger.debug('a debug message');
logger.http('a http message');
logger.info('an info message');
logger.warn('a warn message');
logger.error('an error message');

// Or using the object form:
logger.info({
  message: 'an info message',
  other: 'property',
});

// Or using the generic log method:
logger.log({
  level: 'info',
  message: 'an info message',
  other: 'property',
});

OpenID

OpenID Relying Party integration built on top of node-openid-client. openid-client is a peer dependency and must be explicitly installed in addition to @quickcase/express-sdk:

npm install openid-client

See quickcase/express-react-template for example usage.

Record

extractor(record)(path)

Extract the value of a metadata or field from the given record using the field path.

  • When path is a string: Value of the specified field; or undefined if path cannot be found in case data.
  • When path is an array: An array of the same size, with extracted values in the same position as their respective path. Paths not found are extracted as undefined.
  • When path is an object: An object of the same shape, with extracted values in place of the paths. Paths not found are extracted as undefined.
import {Record} from '@quickcase/express-sdk';

const record = {...}; // As retrieved from data-store

const extractor = Record.extractor(record);

// Extracting metadata
extractor('[workspace]');
extractor('[type]');
extractor('[state]');
extractor('[id]');
extractor('[classification]');
extractor('[created]');
extractor('[modified]');

// Extracting data field
extractor('field1');
extractor('level1.level2.nestedField');

// Extracting from collection items
extractor('collectionField[0].value'); // By item index, zero-based
extractor('collectionField[id:abc123].value'); // By item ID

// Extracting multiple path at once
extractor(['[state]', 'field1', 'field2']); // -> ['Created', 'Value 1', 'Value 2']
extractor({state: '[state]', someField: 'path.to.some.field'}); // -> {state: 'Created', someField: 'some value'}

relativeExtractor(extractor, basePath)(path)

Decorate an extractor with support for field paths relative to the provided base path. Relative paths are always prefixed with @.. When using a relative extractor, absolute field paths (ie. not starting with relative prefix) are still supported.

import {Record} from '@quickcase/express-sdk';

const record = {
  data: {
    level0: {
      field1: 'value 1',
      field2: 'value 2',
    },
    rootField: 'root value',
  },
};

const rootExtractor = Record.extractor(record);
const level0Extractor = Record.relativeExtractor(rootExtractor, 'level0');

level0Extractor([
  '@.field1', // <-- path relative to base 'level0'
  '@.field2',
  '@.field3',
  'rootField', // <-- absolute path
]);

Template

Mustache templates can be rendered using field paths extracted from a record. This templating ability allows for example the injection of field values in otherwise static elements such as labels, hints and descriptions.

parse(template)

Parse and return all field paths used in a template. This is useful to assess whether a template can be rendered definitively or depends on field paths which could change.

import {Template} from '@quickcase/express-sdk';

Template.parse(`Hello {{firstName}}`);
// Output: ['firstName']

renderer(extractor)(template)

Using an extractor as context, renders a template into a string. Both sections and inverted sections are supported in templates. Fields with values Yes or No (case-insensitive) can be coerced into booleans by suffixing them with ? for use as section conditions.

import {Record, Template} from '@quickcase/express-sdk';

const record = {
  data: {
    firstName: 'Henry'
  },
};
const extractor = Record.extractor(record);

Template.renderer(extractor)(`Hello {{firstName}}`);
// Output: 'Hello Henry'

Test

givenMiddleware(middleware)

Given/when/then-like syntax for asynchronously executing ExpressJS middlewares.

This helper is either used to expect and wait for a response:

const res = await givenMiddleware(middleware).when(req).expectResponse();

Or to expect and wait for a call to next():

const next = await givenMiddleware(middleware).when(req).expectNext();

For example:

import {givenMiddleware} from '@quickcase/express-sdk';

test('should resolve with response when response expected', async () => {
  const middleware = (req, res) => res.status(201).json({foo: 'bar'});

  const res = await givenMiddleware(middleware).when({}).expectResponse();

  expect(res).toEqual({
    status: 201,
    headers: {},
    body: {foo: 'bar'},
  });
});

test('should resolve with next when next expected', async () => {
  const middleware = (req, res, next) => next('error');

  const next = await givenMiddleware(middleware).when({}).expectNext();

  expect(next).toEqual('error');
});