@business-nxt/app-messaging
v3.0.2
Published
Helper library to build applications for Business NXT
Readme
@business-nxt/app-messaging
Helper library to build applications for Business NXT
How it fits together
Your app runs inside an iframe served by the Business NXT apps proxy:
Business NXT (parent)
└─ apps proxy frame (holds the auth token; relays messages; injects appName)
└─ your app (uses this library; posts to window.parent = the proxy)Communication is postMessage both ways. This library wraps that protocol: SendMessage for the request/reply message contract, and ExecuteGraphQL for authenticated GraphQL through the proxy relay (your app never sees the bearer token).
Messages that fail validation come back as { messageType: "error", reason: "schema-violation" }. A few fields are owned by the proxy, not the app: appName on dialog-request is injected by the proxy on every relayed message and cannot be set by the consumer.
Listen for changes from Business NXT
Your app and Business NXT communicate using postMessage back and forth. This means that you have to listen for messages with a messageType set.
The message types Business NXT will post are selection-state, edit-session, dialog-response, and error.
import type { Message } from "@business-nxt/app-messaging";
window.addEventListener("message", (event) => {
const data = event.data as Message;
switch (data?.messageType) {
case "selection-state":
// do something as selection changed
break;
case "edit-session":
// do something as the session have changed edit mode
break;
case "dialog-response":
// user closed a dialog you opened with `dialog-request`
break;
case "error":
// a previous request failed (e.g. schema-violation or overlay-already-open)
break;
}
});Request the current selection state
import { SendMessage } from "@business-nxt/app-messaging";
const selection = await SendMessage({ messageType: "selection-state-request" });Pass toParent: true if you want the parent table that your app is joined to instead of the currently focused table. The default is false.
const parentSelection = await SendMessage({
messageType: "selection-state-request",
toParent: true,
});selection
{
"messageType": "selection-state",
"id": "3dc32970-c15e-4227-84dc-0f8c3bba7e13",
"table": "associate",
"focusedRow": {
"associateNo": 9
},
"selectedRows": [{ "associateNo": 7 }, { "associateNo": 8 }],
"fromParent": false
}fromParent mirrors the toParent flag on the request: it is true when the selection came from the parent table this app is joined to, false when it came from the focused table.
Request a table refresh
This will refresh the order table in the current layout
import { SendMessage } from "@business-nxt/app-messaging";
const refreshResponse = await SendMessage({
messageType: "refresh-data-request",
table: "order",
});refreshResponse
{
"messageType": "ok"
"id": "3dc32970-c15e-4227-84dc-0f8c3bba7e13",
}Request the current edit session state
import { SendMessage } from "@business-nxt/app-messaging";
const editSessionStatus = await SendMessage({
messageType: "edit-session-request",
});editSessionStatus
{
"messageType": "edit-session",
"id": "3dc32970-c15e-4227-84dc-0f8c3bba7e13",
"editing": true,
"editSessionId": "a1b2c3d4-..."
}editSessionId identifies the edit session and is optional - the host includes it when known (including on saved / discarded, where it is the id of the session that just ended).
Open a dialog in Business NXT
Ask the host to show a modal dialog rendered with your markdown content. At least one of primaryButtonCaption / secondaryButtonCaption must be provided.
import { SendMessage, DialogResolution } from "@business-nxt/app-messaging";
const response = await SendMessage({
messageType: "dialog-request",
title: "Confirm delete",
content: "Are you sure you want to delete **3 orders**?",
primaryButtonCaption: "Delete",
secondaryButtonCaption: "Cancel",
});
switch (response.result) {
case DialogResolution.Primary:
// user clicked the primary button
break;
case DialogResolution.Secondary:
// user clicked the secondary button
break;
case DialogResolution.Closed:
// user dismissed the dialog without choosing
break;
}When the host cannot serve the request (reason: "overlay-already-open" if a blocking overlay is open, reason: "schema-violation" if the message is malformed) SendMessage rejects with a MessagingError. Catch it to react to the failure:
import { SendMessage, MessagingError } from "@business-nxt/app-messaging";
try {
const response = await SendMessage({ messageType: "dialog-request", ... });
} catch (err) {
if (err instanceof MessagingError && err.reason === "overlay-already-open") {
// queue the dialog for later, etc.
}
}Execute a GraphQL query against Business NXT
When your app is loaded through the Business NXT apps proxy, you can run authenticated GraphQL queries without ever seeing the bearer token. The proxy attaches the user's access token and forwards the request to the internal GraphQL API.
import {
ExecuteGraphQL,
GraphQLRequestError,
} from "@business-nxt/app-messaging";
const Query_GetCustomer = /* GraphQL */ `
query GetCustomer($id: ID!) {
customer(id: $id) {
name
}
}
`;
try {
const data = await ExecuteGraphQL<
{ customer: { name: string } | null },
{ id: string }
>(
Query_GetCustomer,
{ id: "42" },
{
operationName: "GetCustomer",
signal: abortController.signal,
timeout: 30000,
},
);
} catch (err) {
if (err instanceof GraphQLRequestError) {
// err.errors holds the structured GraphQL errors
}
}The promise rejects with GraphQLRequestError when the response carries errors, with a DOMException (AbortError) when the supplied signal aborts, and with a DOMException (TimeoutError) when the timeout (default 30s) is exceeded.
With GraphQL codegen
ExecuteGraphQL accepts any document whose type is structurally compatible with TypedDocumentNode<TResult, TVariables> - the standard shape emitted by @graphql-codegen (client-preset, typed-document-node) and @graphql-typed-document-node/core. When you pass such a document, TResult and TVariables flow through automatically and TypeScript will require the right shape:
import { GetCustomerDocument } from "./generated/graphql";
// TResult and TVariables are inferred from GetCustomerDocument
const data = await ExecuteGraphQL(GetCustomerDocument, { id: "42" });At runtime the helper accepts:
- a plain string,
- a
TypedDocumentString(theStringsubclass form from codegen client-preset), - a
DocumentNodewhoseloc.source.bodyis populated (default forgqltemplate tags and most codegen output).
Pre-print AST documents whose loc is stripped before passing them in.
