@pleasure-js/api
Installation
$ npm i @pleasure-js/api --save
# or
$ yarn add @pleasure-js/api
Features
- Connects socket.io
- Load api plugins
- Accepts complex params via post through the get method
- Provides information about available endpoints / schemas / entities
- Filters access
- Provides a way of querying multiple endpoints at a time
- Restricts access via get variables
- Restricts post / patch / delete body
- converts client into a crud-endpoint
- Hooks ApiEndpoint into a koa router
- converts a crud endpoint in an open api route
- Converts an entity into an array of crud endpoints
- translates directory into routes
- Load entities from directory
- Signs JWT sessions
- Validates provided token via head or cookie and sets $pleasure.user when valid
- Filters response data
- converts route tree into crud endpoint
- Parses query objects
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
function
responseAccessMiddleware(levelResolver, permissionByLevel) ⇒ Param | Type |
---|---|
levelResolver | function |
permissionByLevel |
crudEndpointIntoRouter(router, crudEndpoint)
Param |
---|
router |
crudEndpoint |
Description:
Takes given crudEndpoint as defined
Array.<CRUDEndpoint>
loadApiCrudDir(dir) ⇒ 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
function
loadPlugin(pluginName, [baseDir]) ⇒ 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.
ApiPlugin
jwtAccess(jwtKey, authorizer, options) ⇒ 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 } })
]
}
Object
EndpointHandler : 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) |
Object
CRUD : 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.
Object
CRUDEndpoint : Extends: CRUD
Properties
Name | Type |
---|---|
path | String |
Description:
A CRUD representation of an endpoint
Object
Entity : Properties
Name | Type | Description |
---|---|---|
file | String |
|
path | String |
URL path of the entity |
schema |
Schema | Object
|
|
statics | Object |
|
methods | Object |
function
ApiPlugin : Param | Description |
---|---|
app | The koa app |
server | The http server |
io | The socket.io instance |
router | Main koa router |
Object
Authorization : Properties
Name | Type |
---|---|
user | Object |
[expiration] | Number |
[algorithm] | String |
Authorization
| void
Authorizer ⇒ Param | Type | Description |
---|---|---|
payload | Object |
Given payload (matched by given schema body, if any) |
License
© 2020-present Martin Rafael Gonzalez tin@devtin.io