duck-api

duck-api

Usage no npm install needed!

<script type="module">
  import duckApi from 'https://cdn.skypack.dev/duck-api';
</script>

README

duck-api

Version

Installation

$ npm i duck-api --save
# or
$ yarn add duck-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)
})

proxies emitted events via socket.io

const socket = io('http://localhost:3000')
await new Promise((r) => socket.on('connect', r))

const eventReceived = []

socket.on('method', p => {
  eventReceived.push(p)
})

const { data: { data } } = await axios.post('http://localhost:3000/racks/test', {
  name: 'Martin',
  email: 'tin@devtin.io',
})
t.truthy(data)

const { data: { data: data2 } } = await axios.post(`http://localhost:3000/racks/test/${ data._id }/query`, {
  name: 'Martin',
  email: 'tin@devtin.io',
})

t.true(Array.isArray(data2))
t.snapshot(data2)
t.true(eventReceived.length > 0)

loads api plugins

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

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(Object.hasOwnProperty.call(response.data, 'test'))

filters endpoints 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)
await Api(none, fnStub)

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

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

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

const wrongQuantity = Object.assign({}, ctxStub, requestCtx.wrongQuantity)
const error = await t.throwsAsync(() => 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)
await t.notThrowsAsync(() => Api(fullContact, fnStub))

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

const error = await t.throwsAsync(() => 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`)

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 converted = await duckRackToCrudEndpoints(anEntity, entityDriver)
t.true(Array.isArray(converted))

t.is(converted.length, 3)

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.snapshot(converted)

converts client into a crud-endpoint

const client = await 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 = gatewayToCrudEndpoints(client)
await Promise.each(crudEndpoints, async crudEndpoint => {
  t.true(await CRUDEndpoint.isValid(crudEndpoint))
})

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, 2)
t.truthy(typeof entities[0].duckModel.clean)

Signs JWT sessions

const res = await axios.post('http://0.0.0.0:3000/sign-in', {
  fullName: 'pablo marmol'
})

t.is(res.headers['set-cookie'].filter((line) => {
  return /^accessToken=/.test(line)
}).length, 1)

t.truthy(res.data.accessToken)
t.truthy(res.data.refreshToken)

Validates provided token via head setting $pleasure.user when valid

const userData = { name: 'pedro picapiedra' }
const user = (await axios.get('http://0.0.0.0:3000/user', {
  headers: {
    authorization: `Bearer ${sign(userData, privateKey, { expiresIn: '1m' })}`
  }
})).data

t.like(user, userData)

Validates provided token via cookie setting $pleasure.user when valid

const userData = { name: 'pedro picapiedra' }
const user = (await axios.get('http://0.0.0.0:3000/user', {
  headers: {
    Cookie: `accessToken=${sign(userData, privateKey, { expiresIn: '1m' })};`
  }
})).data

t.like(user, userData)

Rejects token when expired

const userData = { name: 'pedro picapiedra' }
const error = (await axios.get('http://0.0.0.0:3000/user', {
  headers: {
    Cookie: `accessToken=${sign(userData, privateKey, { expiresIn: '0s' })};`
  }
})).data


t.like(error, {
  code: 500,
  error: {
    message: 'Invalid token'
  }
})

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(await EndpointHandler.schemaAtPath('access').parse(ctx => {
  if (ctx.user.level === 'nobody') {
    return false
  }
  if (ctx.user.level === 'admin') {
    return true
  }
  return ['firstName', 'lastName', 'address.zip']
}))

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 = await routeToCrudEndpoints(routeTree)
t.truthy(endpoints)
t.snapshot(endpoints)

Parses query objects

const parsed = await 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(access, thisContext) ⇒ function

Param Type Description
access function callback function receives ctx
thisContext object callback function receives ctx


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


duckRackToCrudEndpoints(entity, duckRack) ⇒

Param Type
entity
duckRack Object

Returns: Promise<[]|*>


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(options) ⇒ function

Param Type Default Description
options Object
options.privateKey String
[options.headerName] String authorization
[options.cookieName] String accessToken
[options.algorithm] String HS256 see https://www.npmjs.com/package/jsonwebtoken#jwtverifytoken-secretorpublickey-options-callback
[options.expiresIn] Number, String 15 * 60


EndpointHandler : Object


CRUD : Object

Description:

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


CRUDEndpoint : Object

Extends: CRUD
Description:

A CRUD representation of an endpoint


Entity : Object


ApiPlugin : function

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

License

MIT

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