@pleasure-js/api

1.3.4-beta • Public • Published

@pleasure-js/api

Version

Installation

$ npm i @pleasure-js/api --save
# or
$ yarn add @pleasure-js/api

Features

Connects socket.io

return new Promise((resolve, reject) => {
  const socket = io('http://localhost:3000')
  socket.on('connect', () => {
    t.pass()
    resolve()
  })
  socket.on('error', reject)
  setTimeout(reject, 3000)
})

Load api plugins

const { data: response } = await axios.get('http://localhost:3000/some-plugin')
t.is(response.data, 'some plugin here!')

Accepts complex params via post through the get method

const $params = {
  name: 'Martin',
  skills: [{
    name: 'developer'
  }]
}
const { data: response } = await axios.get('http://localhost:3000/params', {
  data: {
    $params
  }
})
t.deepEqual(response.data, $params)

Provides information about available endpoints / schemas / entities

const { data: response } = await axios.get('http://localhost:3000/racks')
t.true(typeof response.data === 'object')
t.true(response.data.hasOwnProperty('test'))
/*
  t.deepEqual(response.data.test, {
    schema: {
      name: {
        type: 'String'
      }
    }
  })
*/

Filters access

const { data: response1 } = await axios.get('http://localhost:3000/racks/test/clean')
t.deepEqual(response1.data, {})

const { data: response2 } = await axios.get('http://localhost:3000/racks/test/clean?level=1')
t.deepEqual(response2.data, { name: 'Martin', email: 'tin@devtin.io' })

const { data: response3 } = await axios.get('http://localhost:3000/racks/test/clean?level=2')
t.deepEqual(response3.data, { name: 'Martin' })

Provides a way of querying multiple endpoints at a time

Restricts access via get variables

const Api = apiSchemaValidationMiddleware({
  // passes the schema
  get: {
    quantity: {
      type: Number,
      required: false
    }
  }
})

const none = Object.assign({}, ctxStub, requestCtx.none)
Api(none, fnStub)

t.truthy(none.$pleasure.get)
t.is(Object.keys(none.$pleasure.get).length, 0)

const quantity = Object.assign({}, ctxStub, requestCtx.quantity)
Api(quantity, fnStub)

t.truthy(quantity.$pleasure.get)
t.is(quantity.$pleasure.get.quantity, 3)

const wrongQuantity = Object.assign({}, ctxStub, requestCtx.wrongQuantity)
const error = t.throws(() => Api(wrongQuantity, fnStub))

t.is(error.message, 'Data is not valid')
t.is(error.errors.length, 1)
t.is(error.errors[0].message, 'Invalid number')
t.is(error.errors[0].field.fullPath, 'quantity')

Restricts post / patch / delete body

const Api = apiSchemaValidationMiddleware({
  body: {
    name: {
      type: String,
      required: [true, `Please enter your full name`]
    },
    birthday: {
      type: Date
    }
  }
})

const fullContact = Object.assign({}, ctxStub, requestCtx.fullContact)
const wrongContact = Object.assign({}, ctxStub, requestCtx.wrongContact)
t.notThrows(() => Api(fullContact, fnStub))

t.is(fullContact.$pleasure.body.name, 'Martin Rafael Gonzalez')
t.true(fullContact.$pleasure.body.birthday instanceof Date)

const error = t.throws(() => Api(wrongContact, fnStub))
t.is(error.message, 'Data is not valid')
t.is(error.errors.length, 1)
t.is(error.errors[0].message, `Invalid date`)
t.is(error.errors[0].field.fullPath, `birthday`)

converts client into a crud-endpoint

const client = Client.parse({
  name: 'PayPal',
  methods: {
    issueTransaction: {
      description: 'Issues a transaction',
      input: {
        name: String
      },
      handler ({ name }) {
        return name
      }
    },
    issueRefund: {
      description: 'Issues a refund',
      input: {
        transactionId: Number
      },
      handler ({transactionId}) {
        return transactionId
      }
    }
  }
})

const crudEndpoints = clientToCrudEndpoints(client)
crudEndpoints.forEach(crudEndpoint => {
  t.true(CRUDEndpoint.isValid(crudEndpoint))
})

Hooks ApiEndpoint into a koa router

crudEndpointIntoRouter(koaRouterMock, {
  create: { handler () { } },
  read: { handler () { } },
  update: { handler () { } },
  delete: { handler () { } }
})
t.true(koaRouterMock.post.calledOnce)
t.true(koaRouterMock.get.calledOnce)
t.true(koaRouterMock.patch.calledOnce)
t.true(koaRouterMock.delete.calledOnce)

converts a crud endpoint in an open api route

const swaggerEndpoint = crudEndpointToOpenApi(crudEndpoint)
t.truthy(swaggerEndpoint)
t.snapshot(swaggerEndpoint)

Converts an entity into an array of crud endpoints

const parsedEntity = Entity.parse({
  file: '/papo.js',
  duckModel: {
    schema: {
      name: String
    },
    methods: {
      huelePega: {
        description: 'Creates huelepega',
        input: {
          title: String
        },
        handler ({ title }) {
          return `${ title } camina por las calles del mundo`
        }
      }
    }
  },
  statics: {
    sandyPapo: {
      create: {
        description: 'Creates sandy',
        handler (ctx) {
          ctx.body = 'con su merengazo'
        }
      }
    }
  },
})
const converted = await entityToCrudEndpoints(DuckStorage, parsedEntity)
t.true(Array.isArray(converted))
t.is(converted.length, 4)

converted.forEach(entity => {
  t.notThrows(() => CRUDEndpoint.parse(entity))
})

t.is(converted[0].path, '/papo')
t.truthy(converted[0].create)
t.truthy(converted[0].read)
t.truthy(converted[0].update)
t.truthy(converted[0].delete)
t.truthy(converted[0].list)
t.is(converted[1].path, '/papo/sandy-papo')
t.truthy(converted[1].create)
t.is(converted[3].path, '/papo/:id/huele-pega')

translates directory into routes

const routes = await loadApiCrudDir(path.join(__dirname, './fixtures/app-test/api'))
t.is(routes.length, 5)

Load entities from directory

const entities = await loadEntitiesFromDir(path.join(__dirname, './fixtures/app-test/entities'))
t.is(entities.length, 1)
t.truthy(typeof entities[0].duckModel.clean)

Signs JWT sessions

// initialize
const plugin = jwtAccess(jwtKey, v => v)

const ctx = ctxMock({
  body: {
    name: 'Martin',
    email: 'tin@devtin.io'
  }
})

const router = routerMock()
plugin({ router })

// 0 = index of the use() call; 2 = index of the argument passed to the use() fn
const middleware = findMiddlewareByPath(router, 'auth', 1)

// running middleware
await t.notThrowsAsync(() => middleware(ctx))

const { accessToken } = ctx.body

t.truthy(accessToken)
t.log(`An access token was returned in the http response`)

t.truthy(ctx.cookies.get('accessToken'))
t.log(`A cookie named 'accessToken' was set`)

t.is(accessToken, ctx.cookies.get('accessToken'))
t.log(`Access cookie token and http response token match`)

const decodeToken = jwtDecode(accessToken)

t.is(decodeToken.name, ctx.$pleasure.body.name)
t.is(decodeToken.email, ctx.$pleasure.body.email)
t.log(`Decoded token contains the data requested to sign`)

t.notThrows(() => verify(accessToken, jwtKey))
t.log(`token was signed using given secret`)

Validates provided token via head or cookie and sets $pleasure.user when valid

// initialize
const plugin = jwtAccess(jwtKey, v => v)
const router = routerMock()

plugin({ router })

const middleware = findMiddlewareByPath(router, 0)
const accessToken = sign({ name: 'Martin' }, jwtKey)

const ctx = ctxMock({
  cookies: {
    accessToken
  }
})

t.notThrows(() => middleware(ctx, next))

t.truthy(ctx.$pleasure.user)
t.is(ctx.$pleasure.user.name, 'Martin')

const err = await t.throwsAsync(() => middleware(ctxMock({
  cookies: {
    accessToken: sign({ name: 'Martin' }, '123')
  }
}), next))

t.is(err.message, 'Unauthorized')
t.is(err.code, 401)

const err2 = await t.throwsAsync(() => middleware(ctxMock({
  cookies: {
    accessToken
  },
  headers: {
    authorization: `Bearer ${ accessToken }1`
  }
}), next))

t.is(err2.message, 'Bad request')
t.is(err2.code, 400)

Filters response data

const next = (ctx) => () => {
  Object.assign(ctx, { body: Body })
}
const Body = {
  firstName: 'Martin',
  lastName: 'Gonzalez',
  address: {
    street: '2451 Brickell Ave',
    zip: 33129
  }
}
const ctx = (level = 'nobody', body = Body) => {
  return {
    body: {},
    $pleasure: {
      state: {}
    },
    user: {
      level
    }
  }
}
const middleware = responseAccessMiddleware(EndpointHandler.schemaAtPath('access').parse({
  permissionByLevel: {
    nobody: false,
    admin: true,
    user: ['firstName', 'lastName', 'address.zip']
  },
  levelResolver (ctx) {
    return ctx.user.level || 'nobody'
  }
}))

const nobodyCtx = ctx('nobody')
await middleware(nobodyCtx, next(nobodyCtx))
t.deepEqual(nobodyCtx.body, {})

const userCtx = ctx('user')
await middleware(userCtx, next(userCtx))
t.deepEqual(userCtx.body, {
  firstName: 'Martin',
  lastName: 'Gonzalez',
  address: {
    zip: 33129
  }
})

const adminCtx = ctx('admin')
await middleware(adminCtx, next(adminCtx))
t.deepEqual(adminCtx.body, Body)

converts route tree into crud endpoint

const routeTree = {
  somePath: {
    to: {
      someMethod: {
        read: {
          description: 'Some method description',
          handler () {

          },
          get: {
            name: String
          }
        }
      }
    }
  },
  and: {
    otherMethod: {
      create: {
        description: 'create one',
        handler () {

        }
      },
      read: {
        description: 'read one',
        handler () {

        }
      }
    },
    anotherMethod: {
      create: {
        description: 'another one (another one)',
        handler () {

        }
      }
    }
  }
}

const endpoints = routeToCrudEndpoints(routeTree)
t.truthy(endpoints)
t.snapshot(endpoints)

Parses query objects

const parsed = Query.parse({
  address: {
    zip: {
      $gt: 34
    }
  }
})

t.deepEqual(parsed, {
  address: {
    zip: {
      $gt: 34
    }
  }
})


apiSchemaValidationMiddleware([get], [body]) ⇒

Throws:

  • Schema~ValidationError if any validation fails
Param Type Default Description
[get] Schema, Object, Boolean true Get (querystring) schema. true for all; false for none; schema for validation
[body] Schema, Object, Boolean true Post / Delete / Patch (body) schema. true for all; false for none; schema for validation

Returns: Function - Koa middleware
Description:

Validates incoming traffic against given schemas


responseAccessMiddleware(levelResolver, permissionByLevel) ⇒ function

Param Type
levelResolver function
permissionByLevel


crudEndpointIntoRouter(router, crudEndpoint)

Param
router
crudEndpoint

Description:

Takes given crudEndpoint as defined


loadApiCrudDir(dir) ⇒ Array.<CRUDEndpoint>

Param Type Description
dir String The directory to look for files

Description:

Look for JavaScript files in given directory


loadEntitiesFromDir(dir)

Param
dir

Description:

Reads given directory looking for *.js files and parses them into


loadPlugin(pluginName, [baseDir]) ⇒ function

Param Type Description
pluginName String, Array, function
[baseDir] String Path to the plugins dir. Defaults to project's local.

Description:

Resolves given plugin by trying to globally resolve it, otherwise looking in the plugins.dir directory or resolving the giving absolute path. If the given pluginName is a function, it will be returned with no further logic.


jwtAccess(jwtKey, authorizer, options) ⇒ ApiPlugin

Emits: event:{Object} created - When a token has been issued, event:{Object} created - When a token has been issued

Param Type Default Description
jwtKey String SSL private key to issue JWT
authorizer Authorizer
options Object
[options.jwt.headerName] String authorization
[options.jwt.cookieName] String accessToken
[options.jwt.body] Schema, Boolean true
[options.jwt.algorithm] String HS256
[options.jwt.expiresIn] String 15 * 60

Example

// pleasure.config.js
{
  plugins: [
    jwtAccess(jwtKey, authorizer, { jwt: { body: true, algorithm: 'HS256', expiryIn: 15 * 60 * 60 } })
  ]
}


EndpointHandler : Object

Properties

Name Type Description
handler function
[access] Access Schema for the url get query
[get] Schema Schema for the url get query
[body] Schema Schema for the post body object (not available for get endpoints)


CRUD : Object

Properties

Name Type Description
[*] EndpointHandler Traps any kind of requests
[create] EndpointHandler Traps post request
[read] EndpointHandler Traps get requests to an /:id
[update] EndpointHandler Traps patch requests
[delete] EndpointHandler Traps delete requests
[list] EndpointHandler Traps get requests to an entity with optional filters

Description:

An object representing all CRUD operations including listing and optional hook for any request.


CRUDEndpoint : Object

Extends: CRUD
Properties

Name Type
path String

Description:

A CRUD representation of an endpoint


Entity : Object

Properties

Name Type Description
file String
path String URL path of the entity
schema Schema | Object
statics Object
methods Object


ApiPlugin : function

Param Description
app The koa app
server The http server
io The socket.io instance
router Main koa router


Authorization : Object

Properties

Name Type
user Object
[expiration] Number
[algorithm] String


Authorizer ⇒ Authorization | void

Param Type Description
payload Object Given payload (matched by given schema body, if any)

License

MIT

© 2020-present Martin Rafael Gonzalez tin@devtin.io

Readme

Keywords

none

Package Sidebar

Install

npm i @pleasure-js/api

Weekly Downloads

1

Version

1.3.4-beta

License

MIT

Unpacked Size

232 kB

Total Files

59

Last publish

Collaborators

  • tin_r