1.1.5 • Published 1 month ago

@hiotlabs/hiot-api-interface v1.1.5

Weekly downloads
-
License
ISC
Repository
github
Last release
1 month ago

hiot-api-interface

Publishing

Prerequisites:

  • You have to be part of the @hiotlabs organization on npm
  • You have to be logged in to npm

Then, in root, run:

// Install dependencies
npm i

// Build
tsc

// Make sure tests pass
npm t

// Publish
npm publish --access public

Examples: // link to Example in this file

Exports

Available exports

import { get, post, put, patch, del,  internal, JsonResponse, prepareJsonSchemas, Method } from "hiot-api-interface";

Endpoints

The get, post, put, patch, del & internal functions are for registering endpoints. They share the same api:

export default get({
   /** The path to the endpoint */
  path: string,

  /** The activity that is required to access this endpoint */
  authorized: string,

  /** Whether you need to be logged in or not to access the endpoint. Only needed if authorized is not used. */
  authenticated?: boolean;

   /**
    * The request schema, either a JSON schema or the name of a typescript type
    * (See below. The name must match the name of the file in ./api/schemas).
    **/
  requestSchema: string | object;,

  /** The request handler function */
  requestHandler: (req: Request<any>, res: Response<any>, next: Next) => Promise<JsonResponse<any> | void>,

  /** Additional route options */
  opts?: RouteOptions;

   /** The documentation for the endpoint */
  documentation: {
    /** A short summary of the endpoint */
    summary: string,

    /** A longer description of the endpoint */
    description: string,

    /** Optional array of tags */
    tags?: string[];

    /**
     * Optional description for query params (can also be defined and described in the json schema,
     * since that's how we validate them)
     **/
    query?: Record<string, string>;

    /**
     * Optional description of url parameters.
     * Usually unnecessary since url params should be self descriptive, e.g. /dashboards/:dashboardId
     **/
    parameters?: Record<string, string>;

    /** The responses that can be returned by the endpoint */
    responses: [
      {
        /** The http status code of the response */
        status: number,

        /**
         * An optional response schema, either a JSON schema or the name of a typescript type
         * (See below. The name must match the name of the file in ./api/schemas).
         **/
        schema: string,

        /** An optional description of the response */
        description?: string,
      },
    ],
  },
});

Example

import { get, JsonResponse } from "hiot-api-interface";

export const GET_DASHBOARDS_PATH = "/dashboards";
export const GET_DASHBOARDS_ACTIVITY = ["dashboards", "getDashboards"];

export default get({
  path: GET_DASHBOARDS_PATH,
  authorized: GET_DASHBOARDS_ACTIVITY,
  requestSchema: GET_DASHBOARDS_REQUEST_SCHEMA,
  requestHandler: getDashboardsEndpoint,
  documentation: {
    summary: "Get dashboards",
    description: "Gets all dashboards",
    responses: [
      {
        status: 200,
        schema: GET_DASHBOARDS_RESPONSE_SCHEMA,
      },
    ],
  },
});

async function getDashboardsEndpoint(req: Request<GetDashboardsRequestSchema>): Promise<JsonResponse<DashboardModelViewModel[]>> {
  const { q, page = 0, size = 50, sortBy = "updatedAt", sortOrder = -1 } = req.params;

   // ...

  /** Throwing exceptions */
  if(!dashboard) {
    throw new NotFoundError("Dashboard not found");
  }

   /** Return status/data/headers */
  return {
    status: 200 // optional if 200
    body: dashboard.toViewModel(),
    headers: {
      totalcount: dashboardCount,
    }
  };
}

JsonResponse<T> is the return type. Setting a request handler to return it will validate that the responses are matching the intended response type. E.g. Promise<JsonResponse<DashboardModelViewModel[]>>. However, using res and next is also possible, they are passed in as second and third arguments to the requestHandler function.

Request schema

When using typescript types as json schemas:

Since types are not an actual entity in javascript, they have to be referenced by name (string). Exporting a constant with the name of the type is the easiest way to do this (see export const GET_DASHBOARDS_REQUEST_SCHEMA = "GetDashboardsRequestSchema"; below). Not that the name must match the name of the file in ./api/schemas. Having an export inside the schema file makes it easier to navigate to the schema from the endpoint file (e.g. ctrl/cmd + click. Whereas just using a string mean you would have to search for it manually).

The conversion is done by typescript-json-schema and supports annotations for things such as max, min etc, read more about how to annotate types here.

They can be exported as:

export const GET_DASHBOARDS_REQUEST_SCHEMA = "GetDashboardsRequestSchema";

/** Query params */
export type GetDashboardsRequestSchema = {
  q?: string;

  sortBy?: "name" | "createdAt";

  /**
   * @minimum -1
   * @maximum 1
   */
  sortOrder?: string;

  /** Number */
  size?: string;

  /** Number */
  page?: string;
};

Is translated into, at run time:

export const GetDashboardsRequestSchema = {
  type: "object",
  description: "Query params",
  properties: {
    q: {
      type: "string",
    },
    sortBy: {
      type: "string",
      enum: ["name", "createdAt"],
    },
    sortOrder: {
      type: "string",
      minimum: -1,
      maximum: 1,
    },
    size: {
      type: "string",
      description: "Number",
    },
    page: {
      type: "string",
      description: "Number",
    },
  },
};

Response schema

import { DashboardModel } from "../../models/DashboardModel";

export const GET_DASHBOARDS_RESPONSE_SCHEMA = "GetDashboardsResponseSchema";

export type GetDashboardsResponseSchema = DashboardModel[];

Json schema setup

Using typescript types as json schemas require them to be converted to json schemas during run time. This is done during the service startup. Schemas are cached so that they are only converted once when running tests (that are restarting the service for each test file).

import { setupApiInterface } from "hiot-api-interface";

const started = hiot
  .startApp({ logger, onUncaughtException: handleException, handleException })
  .then((locator)=>{
    await setupApiInterface({
      port: PORT,
      apiVersion: "v1",
      serviceLogLevel: "info",
      serviceName: "visualisation-svc",
      typescript: true,
    });

    routes();

    return locator;
  }) 
  .then(waitFor)
  .then(db)
  .then(shutdown)
  .then(setDatabaseIndexes)
  .catch(hiot.failed(logger));

routes.ts

To maintain control over when the endpoints are registered, they still have to be initiated, this is still done in ./api/routes.ts, by importing the endpoint files and calling the exported function.

import getDashboardByIdEndpoint from "./dashboard/getDashboardByIdEndpoint";

export default function () {
  getDashboardsEndpoint();
}

This is equal to doing (since all the endpoint details are set in the endpoint file):

import getDashboardByIdEndpoint from "./dashboard/getDashboardByIdEndpoint";

export default function (server: restify.Server) {
  /*
   * @api [get] /dashboards
   * tags:
   *   - dashboards
   * summary: "Get dashboards"
   * description: "Gets all dashboards"
   * responses:
   *   "200":
   *     description: "The dashboards found"
   *     schema:
   *       type: "object"
   *       description: "Query params"
   *       // ...the rest of the schema as a manual swagger comment
   */
  api.get(
    route(GET_DASHBOARDS_PATH),
    validate(GetDashboardsRequestSchema),
    mustBe.authorized(GET_DASHBOARDS_ACTIVITY),
    getDashboardsEndpoint
  );
}

Testing

Testing is done like any other test.

Through sandbox

describe("getDashboardsEndpoint", () => {
  it("should...", async () => {
    const { status, body, headers } = await box.api
      .get(`/v1${GET_DASHBOARDS_PATH}`)
      .set(AUTH_USER_HEADER, user.userId)
      .set(AUTH_ACTIVITIES_HEADER, GET_DASHBOARDS_ACTIVITY);

    // ...
  });
});

Directly on handler

If an endpoint needs to be tested directly, it can be done like this:

import getDashboardByIdEndpoint from "../api/dashboard/getDashboardByIdEndpoint";

describe("getDashboardsEndpoint", () => {
  it("should...", async () => {
    const { status, body, headers } = await getDashboardsEndpoint.requestHandler({
      params: {
        q: "test",
        page: "0",
        size: "50",
        sortBy: "updatedAt",
        sortOrder: "-1",
      },
      user: {
        userId: user.userId,
        activities: GET_DASHBOARDS_ACTIVITY,
      },
    });

    // ...
  });
});

The export of the endpoint file is both a function for registering the endpoint and an object with the request handler.

1.1.5

1 month ago

1.1.4

1 month ago

1.1.3

1 month ago

1.1.1

2 months ago

1.1.2

2 months ago

1.1.0

2 months ago

1.0.26

2 months ago

1.0.25

3 months ago

1.0.24

3 months ago

1.0.23

3 months ago

1.0.22

3 months ago

1.0.21

5 months ago

1.0.2

5 months ago

1.0.1

5 months ago

1.0.0

5 months ago