UNPKG

hasura-node-types

Version:

Add Hasura types, event trigger and action wrapper for common web frameworks

374 lines (293 loc) 8.76 kB
# Hasura Node Types Integrate type-safe nodejs backend application with Hasura, from TypeScript with love - [Hasura Node Types](#hasura-node-types) - [Installation](#installation) - [Event Trigger](#event-trigger) - [Action](#action) - [With Express](#with-express) - [Options](#options) - [useActions([handlerMap])](#useactionshandlermap) - [useAction([handler])](#useactionhandler) - [useEvents([handlerMap])](#useeventshandlermap) - [useEvent([handler])](#useeventhandler) - [Logging](#logging) - [Common Getters](#common-getters) - [Action](#action-1) - [Event Trigger](#event-trigger-1) - [Extra Resources](#extra-resources) ## Installation ```bash $ npm install --save hasura-node-types ``` ## Types ### WhereBoolExp Generic `where` condition type, with the help of Typescript inference type. ```ts type Profile = { id: number name: string phone: { code: string phoneNumber: string } addresses: [{ street: string city: string }] }; type ProfileBoolExp = WhereBoolExp<Profile>; const profileWhere: ProfileBoolExp = { _and: [{ id: { _is_null: true } }], id: { _eq: 10 }, name: { _ilike: "10" }, phone: { phoneNumber: { _in: ["012345678", "987654321"] } }, addresses: { street: { _like: "%a%" } } }; ``` ## Event Trigger Event payload follows [Hasura docs](https://hasura.io/docs/1.0/graphql/manual/event-triggers/payload.html) ```ts import { HasuraEventPayload, // trigger event operations HasuraEventUpdate, // HasuraEventInsert, // HasuraEventDelete, // HasuraEventManual, } from "hasura-node-types"; type EventPayload = HasuraEventPayload<HasuraEventUpdate<{ email: string password: string }>> const payload: EventPayload = res.body; // or you can use default any payload const payload: HasuraEventPayload = res.body; ``` ## Action Action payload follows [Hasura docs](https://hasura.io/docs/1.0/graphql/manual/actions/action-handlers.html#action-handlers) ```ts import { HasuraEventPayload, HasuraEventUpdate } from "hasura-node-types"; type LoginInput = { readonly email: string readonly password: string }; type ActionPayload = HasuraActionPayload<LoginInput, typeof ACTION_LOGIN>; const payload: ActionPayload = res.body; // or you can use default any payload const payload: HasuraActionPayload = res.body; ``` ## With Express Thank to GraphQL Engine payload structure, we can apply Factory Pattern that use single endpoint for multiple events, distinguished by event/action name ```ts type WithHasuraOptions<Ctx extends AnyRecord = AnyRecord> = { readonly logger?: Logger readonly debug?: boolean readonly logRequestBody?: boolean readonly logResponseData?: boolean readonly context?: Ctx }; const we = withExpress([options]) ``` This instance is Action and Event Trigger wrappers for Express handlers ### Options - *logger*: Logging instance, use `console.log` by default. Support common libraries that implement logger interface (`winston`, `bunyan`) - *debug*: show response data when printing logging. This field is also included in `context` - *context*: extra context data - *logRequestBody*: should log request body or not. This option is always true if debug is true - *logResponseData*: should log response data or not. This option is always true if debug is true ### useActions([handlerMap]) Wrap Express handler with pre-validation, select and run action function from handler map ```ts import { withExpress } from "hasura-node-types"; const ACTION_LOGIN = "login"; type LoginInput = { readonly email: string readonly password: string }; type LoginOutput = LoginInput; const loginAction: HasuraActionExpressHandler< HasuraActionPayload<LoginInput, typeof ACTION_LOGIN>, LoginOutput > = (ctx, { input }) => Promise.resolve(input); const handlerMap = { [ACTION_LOGIN]: loginAction }; export default withExpress().useActions(handlerMap); ``` With `HasuraActionHandler` function is defined as: ```ts type HasuraActionExpressContext = { logger: Logger request: Request // extra contexts } type HasuraActionHandler<Payload, Resp> = (ctx: HasuraActionExpressContext, payload: Payload) => Promise<Resp> ``` ### useAction([handler]) Use this function If you prefer using multiple routes instead ```ts const we = withExpress(); const router = express.Router(); router.post("/actions/login", we.useAction(loginAction)); router.post("/actions/logout", we.useAction(logoutAction)); ``` ### useEvents([handlerMap]) Wrap Express handler with pre-validation, select and run event trigger functions from handler map. Note: you can use `default` or `*` as default fallback handler ```ts const EVENT_TRIGGER_UPDATE_USER = "update_user"; type UserInput = { readonly email: string readonly password: string }; const userUpdateEvent: HasuraEventExpressHandler< HasuraEventUpdate<UserInput>, UserInput, typeof EVENT_TRIGGER_UPDATE_USER > = (_, { event }) => Promise.resolve(event.data.new); const handlerMap = { [EVENT_TRIGGER_UPDATE_USER]: userUpdateEvent // default event handler default: () => Promise.resolve({ "message": "default" }) }; export default withExpress().useEvents(handlerMap); ``` ### useEvent([handler]) Use this function If you prefer using multiple routes instead ```ts const we = withExpress(); const router = express.Router(); router.post("/events/update-user", we.useEvent(updateUser)); router.post("/events/delete-user", we.useEvent(deleteUser)); ``` ### Logging Logging structure follows GraphQL engine styles, using JSON format *Note*": Response - Success action log ```json { "action_name": "login", "session_variables": { "x-hasura-role": "anonymous" }, "request_headers": { "host": "127.0.0.1:40763", "accept-encoding": "gzip, deflate", "user-agent": "node-superagent/3.8.3", "content-type": "application/json", "content-length": "176", "connection": "close" }, "request_body": { "email": "example@domain.com", "password": "123456" }, "latency": 4, "level": "info", "message": "executed login successfully", "response": null, "http_code": 200 } ``` - Failure action log ```json { "action_name": null, "session_variables": { "x-hasura-role": "anonymous" }, "request_headers": { "host": "127.0.0.1:33013", "accept-encoding": "gzip, deflate", "user-agent": "node-superagent/3.8.3", "content-type": "application/json", "content-length": "50", "connection": "close" }, "request_body": null, "latency": 0, "level": "error", "message": "empty hasura action name", "error": { "code": "validation_error", "details": null }, "http_code": 400 } ``` - Success event trigger log ```json { "request_body": { "email": "example@domain.com", "password": "123456" }, "request_header": { "host": "127.0.0.1:36825", "accept-encoding": "gzip, deflate", "user-agent": "node-superagent/3.8.3", "content-type": "application/json", "content-length": "50", "connection": "close" }, "trigger_name": null, "latency": 0, "level": "error", "message": "empty hasura event trigger id", "error":{ "code": "validation_error", "details": null }, "http_code": 400 } ``` - Failure event trigger log ```json { "request_body": { "id": "2020-05-08T08:55:49.946Z", "event": { "session_variables": { "x-hasura-role": "anonymous" }, "op": "UPDATE", "data": { "email": "example@domain.com", "password": "123456" } }, "created_at": "2020-05-08T08:55:49.946Z", "trigger": { "name": "update_user" }, "table": { "name": "users", "schema": "public" } }, "request_header": { "host": "127.0.0.1:35223", "accept-encoding": "gzip, deflate", "user-agent": "node-superagent/3.8.3", "content-type": "application/json", "content-length": "342", "connection": "close" }, "trigger_name": "update_user", "latency": 1, "level": "info", "message": "executed trigger update_user successfully", "response": null, "http_code": 200 } ``` *Note*: request body and response data can be `null` by `withExpress` options ## Common Getters ### Action ```ts // get action user ID function getActionUserID(payload: HasuraActionPayload): string | null // get action user role function getActionUserRole(payload: HasuraActionPayload): string | null ``` ### Event Trigger ```ts // get event user ID function getEventUserID(payload: HasuraEventPayload): string | null // get event user role function getEventUserRole(payload: HasuraEventPayload): string | null ``` ## Extra Resources - [Real-world boilerplate for Hasura + TypeScript](https://github.com/hgiasac/hasura-typescript-boilerplate)