New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

joint-kit

Package Overview
Dependencies
Maintainers
1
Versions
23
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

joint-kit

A Node server library & development kit for rapidly implementing data layers and RESTful endpoints | Part of the Joint Stack

  • 0.0.16
  • npm
  • Socket score

Version published
Weekly downloads
15
decreased by-6.25%
Maintainers
1
Weekly downloads
 
Created
Source

Joint Kit

A Node server library & development kit for rapidly implementing data layers and RESTful endpoints.

Designed to be flexible. Mix it with existing code -or- use it to generate an entire server-side method library and isomorphic HTTP API from scratch.


DB model configuration, robust CRUD and relational data logic, resource-level & user-level authorization, field validation, data transformation, paginated & non-paginated datasets, rich error handling, payload serialization, HTTP router generation (for RESTful endpoints), and more.


WIP

Not ready for public use until version 0.1.0 - Syntax and logic are in frequent flux.


Table of Contents

Installation

  • Prerequisites
  • Install

Overview

API

Guide

Examples

License


Prerequisites

To use the Joint Kit, you need:

  • a supported persistence solution (e.g. Postgres)
  • a configured data schema (e.g. database & tables)
  • a supported service interface / ORM

The Joint Kit currently supports:

ServiceRequired PluginsPersistence Options
Bookshelfregistry, paginationPostgres, MySQL, SQLite3

---

If you wish to generate RESTful API endpoints, you need:

  • a supported server framework

The Joint Kit currently supports:

ServerRequired Middleware
Expressbody-parser, cookie-parser

Install

$ npm install joint-kit --save

The Joint Concept (In Code)

[TBC]


Joint in Practice

To implement solutions with the Joint Kit, you create Joints.

A Joint connects to:

  • your persistence service  ➔  to implement a data method layer

  • your server framework  ➔  to implement an HTTP API layer

The Joint Kit provides a set of data actions that are abstracted to handle common data operations for your resources. Actions are implemented using a config-like JSON syntax, making development simple and quick.

Leverage the built-in features to satisfy 100% of your required functionality, or use them as a base to augment with your own specialized logic.

Create a Joint

import express from 'express';
import Joint from 'joint-kit';
import bookshelf from './services/bookshelf'; // your configured bookshelf service

const joint = new Joint({
  service: bookshelf,
  server: express,
});

---

Define Data Models

You can continue configuring data models with your service natively, or you can dynamically generate them with Joint using a JSON descriptor:

/model-config.js

export default {
  models: {
    // Define a model named: "Profile":
    Profile: {
      tableName: 'blog_profiles',
      timestamps: { created: 'created_at', updated: 'updated_at' },
      associations: {
        user: {
          type: 'toOne',
          path: 'user_id => User.id', // one-to-one
        },
        posts: {
          type: 'toMany',
          path: 'id => BlogPost.profile_id', // one-to-many
        },
        tags: {
          type: 'toMany',
          path: 'id => ProfileTag.profile_id => ProfileTag.tag_id => Tag.id', // many-to-many
        },
      },
    },
  },
};

Use the generate function to dynamically build and register your models:

import modelConfig from './model-config'; // your defined models

// Dynamically generate the defined models:
joint.generate({ modelConfig });

// You can access all models using the syntax joint.model.<modelName>:
if (joint.model.Profile) console.log('The Profile model exists !!!');

---

Create a Method Library

From the provided set of abstract data actions (Joint Actions), you can quickly implement a customized method library.

You can hand-roll the methods yourself:

Hand-rolling a CRUD set of methods
/methods/profile.js
export function createProfile(input) {
  const spec = {
    modelName: 'Profile',
    fields: [
      { name: 'user_id', type: 'Number', required: true },
      { name: 'slug', type: 'String', required: true },
      { name: 'title', type: 'String', required: true },
      { name: 'tagline', type: 'String' },
      { name: 'is_live', type: 'Boolean', defaultValue: false },
    ],
  };

  return joint.createItem(spec, input);
}

export function updateProfile(input) {
  const spec = {
    modelName: 'Profile',
    fields: [
      { name: 'id', type: 'Number', required: true, lookup: true },
      { name: 'slug', type: 'String' },
      { name: 'title', type: 'String' },
      { name: 'tagline', type: 'String' },
      { name: 'is_live', type: 'Boolean' },
    ],
  };

  return joint.updateItem(spec, input);
}

export function getProfile(input) {
  const spec = {
    modelName: 'Profile',
    fields: [
      { name: 'id', type: 'Number', requiredOr: true },
      { name: 'slug', type: 'String', requiredOr: true },
    ],
  };

  return joint.getItem(spec, input);
}

export function getProfiles(input) {
  const spec = {
    modelName: 'Profile',
    fields: [
      { name: 'user_id', type: 'Number' },
      { name: 'is_live', type: 'Boolean' },
    ],
  };

  return joint.getItems(spec, input);
}

export function deleteProfile(input) {
  const spec = {
    modelName: 'Profile',
    fields: [
      { name: 'id', type: 'Number', requiredOr: true },
      { name: 'slug', type: 'String', requiredOr: true },
    ],
  };

  return joint.deleteItem(spec, input);
}

-OR-

You can dynamically generate them from a JSON descriptor:

/method-config.js

export default {
  resources: [
    {
      modelName: 'Profile',
      methods: [
        {
          name: 'createProfile',
          action: 'createItem',
          spec: {
            fields: [
              { name: 'user_id', type: 'Number', required: true },
              { name: 'slug', type: 'String', required: true },
              { name: 'title', type: 'String' },
              { name: 'tagline', type: 'String' },
              { name: 'is_live', type: 'Boolean', defaultValue: false },
            ],
          },
        },
        {
          name: 'getProfiles',
          action: 'getItems',
          spec: {
            fields: [
              { name: 'user_id', type: 'Number', requiredOr: true },
              { name: 'is_live', type: 'String', requiredOr: true },
            ],
            defaultOrderBy: '-created_at,title',
          },
        },
      ],
    },
  ],
};

Use the generate function to dynamically generate your methods:

import methodConfig from './method-config'; // your defined method logic

// Dynamically generate the defined methods:
joint.generate({ methodConfig });

// You can now utilize the methods using the syntax:
const input = {
  fields: { is_live: true },
};
joint.method.Profile.getProfiles(input)
  .then((result) => { ... })
  .catch((error) => { ... });

---

Create RESTful Endpoints

On top of your Joint methods, you can easily expose a RESTful API layer.

You can hand-roll the router logic yourself:

-OR-

You can dynamically generate a router from a JSON descriptor:

/route-config.js

export default {
  routes: [
    {
      uri: '/profile',
      post: { method: 'Profile.createProfile', successStatus: 201, body: true },
    },
    {
      uri: '/profiles',
      get: { method: 'Profile.getProfiles' },
    },
  ],
};

Use the generate function to dynamically generate your router logic:

import routeConfig from './route-config'; // your defined router logic

// Dynamically generate the defined routes:
joint.generate({ routeConfig });

// Provide the generated router to your server:
const app = express();
app.use('/api', joint.router);

Joint Constructor

The Joint Kit module is an instantiable Class. Its instances are Joints.

Multiple Joint instances can be utilized within a single application.

Example Joint Instantiation:

import express from 'express';
import Joint from 'joint-lib';
import bookshelf from './services/bookshelf'; // your configured bookshelf service

const joint = new Joint({
  service: bookshelf,
  server: express,
  output: 'json-api',
});

Constructor Options

NameDescriptionRequired?
serviceThe configured service instance for your persistence solution.Yes
serverThe server instance for your HTTP router handling.No
outputThe format of the returned data payloads. (defaults to 'native')No
settingsThe configurable settings available for a Joint instance.No

service

[TBC]

server

[TBC]

output

[TBC]

settings

[TBC]


Joint Instance

When a Joint has been instantiated, the following properties and functions are available on the instance:

Properties

NameDescription
serviceThe underlying service implementation (for persistence) provided at instantiation.
serviceKeyA string value identifying the persistence service being used.
serverThe underlying server implementation, if configured.
serverKeyA string value identifying the server being used.
null if not configured.
outputThe string value for the globally configured output format.
'native' by default.
settingsThe active settings of the instance.
modelConfigThe active "model config" descriptor, if provided with the generate function.
methodConfigThe active "method config" descriptor, if provided with the generate function.
routeConfigThe active "route config" descriptor, if provided with the generate function.

Operational Functions

FunctionDescription
generate( options )Executes the dynamic generation of models, methods, and routes, per the config descriptors provided.
setServer( server )Allows configuration of the server implementation, post-instantiation.
setOutput( output )Allows configuration of the output format, post-instantiation.
updateSettings( settings )Allows modification of the Joint settings, post-instantiation.
<action>( spec, input, output )The action logic provided by the Joint instance. This is the backbone of the solution. See Joint Actions for the full list and usage details.

Convenience Functions

FunctionDescription
info( )

Generated Models

SyntaxDescription
model.<modelName>                                  The registered Model object with name <modelName>.
Any existing Models registered to the service instance will be mixed-in with those generated by Joint.

Generated Methods

SyntaxDescription
method.<modelName>.<methodName>( input )

Generated Router

SyntaxDescription
router

Registries/Lookups

NameDescription
model.<modelName>Accesses the registered Model object with name <modelName>.
modelByTable.<tableName>Accesses the registered Model object by its <tableName>.
modelNameByTable.<tableName>Accesses the registered Model name by its <tableName>.
specByMethod.<modelName>.<methodName>Accesses the configured spec definition for a generated method by its <modelName>.<methodName> syntax.

Joint Actions

The Joint Action set is the backbone of the Joint Kit solution.

Each Joint instance provides a robust set of abstract data actions that hook directly to your persistence layer, handling the core logic for common data operations.

The actions are implemented using a config-like JSON syntax.

---

Base Actions (CRUD)

ActionDescription
createItemCreate operation for a single item.
upsertItemUpsert operation for a single item.
updateItemUpdate operation for a single item.
getItemRead operation for retrieving a single item.
getItemsRead operation for retrieving a collection of items.
deleteItemDelete operation for single item.

Association Actions (Relational)

ActionDescription
addAssociatedItemsOperation for associating one to many items of a type to a main resource.
hasAssociatedItemOperation for checking the existence of an association on a main resource.
getAllAssociatedItemsOperation for retrieving all associations of a type from a main resource.
removeAssociatedItemsOperation for disassociating one to many items of a type from a main resource.
removeAllAssociatedItemsOperation for removing all associations of a type from a main resource.

Joint Action Syntax

All Joint Actions return Promises, and have the signature:

joint.<action>(spec = {}, input = {}, output = 'native')
  .then((payload) => { ... })
  .catch((error) => { ... });

---

Each action has two required parts: the spec and the input.

  • The spec    ➔  defines the functionality of the action.

  • The input  ➔  supplies the data for an individual action request.

Each action also supports the optional parameter: output.

  • The output  ➔  specifies the format of the returned payload.

Spec Options

OptionDescriptionTypeActions SupportedRequired?
modelNameThe model name of the resource for the action.String(all)Yes
fieldsThe root property for defining accepted fields.Array(all)Yes (*except getItems)
fields.nameThe field name.String(all)Yes
fields.typeThe field data type.String(all)Yes
fields.requiredIf the field is required for the action.Boolean(all)No
fields.requiredOrIf the field is required for the action (within an OR set).Boolean(all)No
fields.lookupDenotes the field is required to pre-fetch the resource before an update can occur.Boolean(all)Yes for upsertItem, updateItem
fields.lookupOrDenotes the field is required (within an OR set) to pre-fetch the resource before an update can occur.Boolean(all)Yes for upsertItem, updateItem
fields.defaultValueThe default value to persist, if the field is not provided in the input.MixedcreateItem, upsertItem, getItemNo
fieldsToReturnThe fields to return with the payload. Returns all fields when not provided.Array
-or-
Object
getItem, getItemsNo
defaultOrderByThe default sort order for a collection payload.StringgetItemsNo
forceAssociationsBinds the associations to return for all action requests.ArraygetItem, getItemsNo
forceLoadDirectBinds the loadDirect associations to return for all action requests.ArraygetItem, getItemsNo
authThe root property for defining authorization on the action.Object(all)No
auth.ownerCredsSpecifies the fields that can verify resource ownership (for owner auth rules).Array(all)No
mainThe root property to wrap the spec options for the main resource, in an association action.Object(association actions)Yes
associationThe root property to wrap the spec options for the associated resource, in an association action.Object(association actions)Yes

modelName

[TBC]

fields

[TBC]

fieldsToReturn

[TBC]

defaultOrderBy

[TBC]

forceAssociations

[TBC]

forceLoadDirect

[TBC]

auth

[TBC]

main

[TBC]

association

[TBC]


Input Options

OptionDescriptionActions SupportedRequired?
fields(all)Yes (* except getItems)
fieldSetgetItem, getItemsNo
associationsgetItem, getItemsNo
loadDirectgetItem, getItemsNo
orderBygetItemsNo
paginategetItemsNo
trx(all)No
authBundle(all)No

fields

[TBC]

fieldSet

[TBC]

associations

[TBC]

loadDirect

[TBC]

orderBy

[TBC]

paginate

[TBC]

trx

[TBC]

authBundle

[TBC]


Output Values

The output value configures the format of the returned payload.

NOTE: This setting can be configured globally on the Joint instance itself. (See the Joint Instance API)

ValueDescription
'native'Returns the queried data in the format generated natively by the service. This is the default setting.
'json-api'Transforms the data into a JSON API Spec-like format, making the data suitable for HTTP transport.

---

output = 'native' (default)

By default, the output is set to 'native', which effectively returns the queried data in the format generated natively by the service you are using.

Item Example:

Joint.getItem
const spec = {
  modelName: 'Profile',
  fields: [
    { name: 'id', type: 'Number', required: true },
  ],
};

const input = {
  fields: { id: 1 },
  associations: ['user'],
};

joint.getItem(spec, input, 'native')
  .then((payload) => { ... })
  .catch((error) => { ... });

**Returns:**
Item payload ( Bookshelf )
{
  cid: 'c1',
  _knex: null,
  id: 1,
  attributes: {
    id: 1,
    user_id: 333,
    slug: 'functional-fanatic',
    title: 'Functional Fanatic',
    tagline: 'I don\'t have habits, I have algorithms.',
    is_live: false,
  },
  _previousAttributes: { ... },
  changed: {},
  relations: {
    user: {
      cid: 'c2',
      id: 333,
      attributes: {
        display_name: '|M|',
        username: 'manicprone',
        sites: [
          { gitlab: 'https://gitlab.com/manicprone' },
          { github: 'https://github.com/manicprone' },
        ],
      },
      _previousAttributes: { ... },
      changed: {},
      relations: {},
      relatedData: { ... },
    },
  },
}

Collection Example:

Joint.getItems
const spec = {
  modelName: 'Profile',
};

const input = {
  associations: ['user'],
  paginate: { skip: 0, limit: 3 },
};

joint.getItems(spec, input, 'native')
  .then((payload) => { ... })
  .catch((error) => { ... });

**Returns:**
Collection payload ( Bookshelf )

output = 'json-api'

When the output is set to 'json-api', the returned payload is transformed into a JSON API Spec-like format, making it suitable for HTTP data transport.

Item Example:

Joint.getItem
const spec = {
  modelName: 'Profile',
  fields: [
    { name: 'id', type: 'Number', required: true },
  ],
};

const input = {
  fields: { id: 1 },
  associations: ['user'],
};

joint.getItem(spec, input, 'json-api')
  .then((payload) => { ... })
  .catch((error) => { ... });

**Returns:**
Item payload
{
  data: {
    type: 'Profile',
    id: 1,
    attributes: {
      user_id: 333,
      slug: 'functional-fanatic',
      title: 'Functional Fanatic',
      tagline: 'I don\'t have habits, I have algorithms.',
      is_live: false,
    },
    relationships: {
      user: {
        data: {
          type: 'User',
          id: 333,
        },
      },
    },
  },
  included: [
    {
      type: 'User',
      id: 333,
      attributes: {
        display_name: '|M|',
        username: 'manicprone',
        sites: [
          { gitlab: 'https://gitlab.com/manicprone' },
          { github: 'https://github.com/manicprone' },
        ],
      },
    },
  ],
}

Collection Example:

Joint.getItems
const spec = {
  modelName: 'Profile',
};

const input = {
  associations: ['user'],
  paginate: { skip: 0, limit: 3 },
};

joint.getItems(spec, input, 'json-api')
  .then((payload) => { ... })
  .catch((error) => { ... });

**Returns:**
Collection payload

Joint Action Errors

[TBC]


Joint Action Authorization

[TBC]


Model Config Syntax

[TBC]


Method Config Syntax

[TBC]


Route Config Syntax

[TBC]


Defining Data Models

[TBC]

You can continue to define data models using your service implementation, or you can dynamically generate them with Joint. Both approaches are supported simultaneously. Any existing models registered to your service instance will be mixed-in with those generated by the Joint instance.


Building a Method Library

[TBC]


Building a RESTful API

[TBC]


Example Solutions

Writing a custom Express Router:
import express from 'express';
import Joint from 'joint-kit';
import bookshelf from './services/bookshelf'; // your configured bookshelf service
import modelConfig from './model-config';     // your defined models
import methodConfig from './method-config';   // your defined method logic

// Initialize a Joint using your service implementation:
const joint = new Joint({
  service: bookshelf,
  output: 'json-api', // auto-serialize to JSON API Spec format
});

// Dynamically generate the defined models and methods:
joint.generate({ modelConfig, methodConfig });

// Expose your data logic via Express router:
const router = express.Router();

router.route('/user')
  .post((req, res) => {
    const input = {};
    input.fields = Object.assign({}, req.body, req.query);

    return joint.method.User.createUser(input)
      .then(payload => handleDataResponse(payload, res, 201))
      .catch(error => handleErrorResponse(error, res));
  });

router.route('/user/:id')
  .get((req, res) => {
    const input = {
      fields: {
        id: req.params.id,
      },
      loadDirect: ['profile:{title,tagline,avatar_url,is_live}', 'roles:name'],
      associations: ['friends'],
    };

    return joint.method.User.getUser(input)
      .then(payload => handleDataResponse(payload, res, 200))
      .catch(error => handleErrorResponse(error, res));
  })
  .post((req, res) => {
    const input = {};
    input.fields = Object.assign({}, req.body, req.query, { id: req.params.id });

    return joint.method.User.updateUser(input)
      .then(payload => handleDataResponse(payload, res, 200))
      .catch(error => handleErrorResponse(error, res));
  })
  .delete((req, res) => {
    const input = {
      fields: {
        id: req.params.id,
      },
    };

    return joint.method.User.deleteUser(input)
      .then(payload => handleDataResponse(payload, res, 204))
      .catch(error => handleErrorResponse(error, res));
  });

router.route('/users')
  .get((req, res) => {
    const input = {};
    input.fields = Object.assign({}, req.query);
    input.loadDirect = ['profile:{title,tagline,avatar_url,is_live}', 'roles:name'];
    input.associations = ['friends'],

    return joint.method.User.getUsers(input)
      .then(payload => handleDataResponse(payload, res, 200))
      .catch(error => handleErrorResponse(error, res));
  });

function handleDataResponse(data, res, status = 200) {
  res.status(status).json(data);
}

function handleErrorResponse(error, res) {
  const status = error.status || 500;
  res.status(status).json(error);
}

module.exports = router;

License

[TBD]

FAQs

Package last updated on 05 Nov 2017

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc