npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rytass/bpm-core-client

v0.1.10

Published

Framework-agnostic TypeScript client for the Rytass BPM approval workflow GraphQL surface: fetch-based GraphQL transport, REST auth client, pre-baked typed operations for organization, member, form, template, and workflow.

Readme

@rytass/bpm-core-client

Framework-agnostic TypeScript client for the BPM approval workflow GraphQL surface published by @rytass/bpm-core-nestjs-module.

This package contains:

  • A tiny fetch-based GraphQL transport (requestGraphQl).
  • Endpoint resolution helpers (readGraphQlEndpoint, readApiBaseUrl).
  • The BPM host auth REST client (loginApi, logoutApi, readApiCurrentMember, listApiTestMembers).
  • Pre-baked typed GraphQL operations for organization, member directory, form definition, approval template, workflow instance, task, notification, attachment, signature, and delegation flows.

It does not ship React components, hooks, or UI. Hosts can call the functions directly from any framework (Next.js / Vite / Remix / plain Node).

Package Status

Current version: 0.1.10

The package intentionally stays framework-agnostic so it can be reused from Next.js Server Components, Server Actions, plain React, or non-React runtimes. There is no Apollo Client setup.

Install

pnpm add @rytass/bpm-core-client @rytass/bpm-core-shared

@rytass/bpm-core-shared is a peerDependency and must be installed alongside.

The browser bundle uses the standard Fetch API; Node 20+ also has fetch built in. No graphql runtime package is required.

TypeScript moduleResolution: prefer node16, nodenext, or bundler in your tsconfig.json so the package's exports field is honored. Classic moduleResolution: "node" also works through the typesVersions fallback shipped with this package, but is on TypeScript's long-term deprecation path — new projects should opt in to modern resolution.

Runtime Compatibility

TL;DR — the transport is isomorphic (always fetch), but session is browser-first. Out of the box every function works in a browser tab. Server Components, Server Actions, Node CLIs, workers, and Edge Functions can also call the same functions, but the caller is responsible for resolving the endpoint to an absolute URL and for forwarding any session cookie.

What is identical across runtimes

| Concern | Behavior | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | Transport | Always globalThis.fetch. No axios, no node-fetch, no cross-fetch, no Apollo Link. | | Serialization | Content-Type: application/json; POST { query, variables }. | | Error model | Throws Error on non-2xx HTTP, on payload.errors[], or on missing data. | | Type guarantees | Every function returns a typed Promise<T> — return shape is identical regardless of where the call runs. |

What differs across runtimes

| Concern | Browser | Node SSR / Server Component / CLI | | ---------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | Default endpoint | Same-origin /graphql resolves to https://your-host/graphql. | Same-origin /graphql throws TypeError: Failed to parse URL. Set NEXT_PUBLIC_API_URL to an absolute URL. | | Session cookie | credentials: 'include' automatically sends the HttpOnly cookie issued by /auth/login.| fetch has no cookie jar; the host must forward the incoming request's Cookie header manually. | | loginApi() storage | Cookie lands in browser jar, persists across navigations. | Returned Set-Cookie header is dropped — caller must read and reuse it explicitly. | | window.location | Used to detect localhost for the dev fallback endpoint. | typeof window === 'undefined' → endpoint resolver returns /graphql (relative), which is invalid in Node unless overridden. |

Server Component / Server Action recipe

Next.js App Router server runtime needs two things: an absolute endpoint and a way to relay the inbound Cookie header. The recommended pattern is to set NEXT_PUBLIC_API_URL at build time and pass cookies via the underlying fetch headers using a wrapper such as Next's cookies():

// app/inbox/page.tsx — Server Component
import { cookies } from 'next/headers';

async function fetchInbox(): Promise<unknown> {
  const cookieHeader = (await cookies()).toString();
  const response = await fetch(`${process.env.BPM_API_URL}/graphql`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Cookie: cookieHeader,
    },
    body: JSON.stringify({
      query: 'query Ping { memberCount }',
    }),
  });
  return response.json();
}

For SSR cases that need to call many typed functions, the cleanest long-term option is to add a configureBPMClient({ fetch, endpoint }) hook so the typed wrappers can be reused. That is not yet shipped — if you need it, file an issue. Today, Server Component callers either talk to the host directly (as above) or proxy through a Route Handler that runs inside the consumer's browser session.

Node CLI recipe

CLIs that drive BPM from outside a browser need both an absolute endpoint and explicit session management. The auth REST client (loginApi, logoutApi, readApiCurrentMember) returns typed bodies but does not persist cookies in Node — capture them yourself:

// scripts/dump-running-instances.ts
import { listApprovalInstancesPage } from '@rytass/bpm-core-client/workflow';

process.env.NEXT_PUBLIC_API_URL ??= 'https://bpm.your-host.example/graphql';

// 1. Log in and capture the session cookie manually.
const loginResponse = await fetch(
  'https://bpm.your-host.example/auth/login',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ identifier: 'svc-bot', password: '...' }),
  },
);
const cookieJar = loginResponse.headers.get('set-cookie') ?? '';

// 2. Hand the cookie to subsequent fetches via a small fetch override.
const originalFetch = globalThis.fetch;
globalThis.fetch = ((input, init = {}) =>
  originalFetch(input, {
    ...init,
    headers: { ...(init.headers ?? {}), Cookie: cookieJar },
  })) as typeof fetch;

// 3. Call typed BPM functions as usual.
const { instances } = await listApprovalInstancesPage({
  view: 'ALL', state: 'RUNNING', page: 1, pageSize: 1000,
  searchText: '', templateId: null,
});
console.log(instances.length, 'running instances');

Edge / Workers

Cloudflare Workers, Vercel Edge Functions, and similar V8-isolate runtimes ship fetch natively and follow the Node SSR column above — no default endpoint, no cookie jar. Treat them the same as Server Components.

Endpoint Resolution

By default the client targets the same origin (no /api prefix) — see the "Embedding & auth" section of @rytass/bpm-core-nestjs-module's README for the host-side contract. Endpoint resolution rules:

| Source | Behavior | | ----------------------------------------------- | -------------------------------------------------------------------------- | | process.env.NEXT_PUBLIC_API_URL | If set, used as the GraphQL endpoint URL. | | process.env.NEXT_PUBLIC_API_AUTH_URL | If set, used as the base URL for the REST /auth/* endpoints. | | Browser hostname localhost or 127.0.0.1 | GraphQL defaults to http://localhost:17603/graphql. | | Deployed hostname (anything else, or SSR) | GraphQL defaults to same-origin /graphql; auth uses same-origin root. |

import {
  loginApi,
  readApiCurrentMember,
  readApiBaseUrl,
  readGraphQlEndpoint,
  requestGraphQl,
} from '@rytass/bpm-core-client';

console.log(readGraphQlEndpoint());
console.log(readApiBaseUrl());

const member = await loginApi({ identifier: 'member-001', password: 'demo' });
const currentMember = await readApiCurrentMember();

interface PingQueryData {
  readonly memberCount: number;
}

const data = await requestGraphQl<PingQueryData>(`
  query Ping {
    memberCount
  }
`);

requestGraphQl always sends credentials: 'include', so the HTTP-only session cookie issued by the BPM host's /auth/login flows back automatically on subsequent calls.

Subpath Overview

| Import path | Contents | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------- | | @rytass/bpm-core-client | GraphQL transport, endpoint resolution, REST auth client, member directory queries. | | @rytass/bpm-core-client/organization | Org unit CRUD, position CRUD, membership CRUD, manager resolution. | | @rytass/bpm-core-client/form | Form definition CRUD, form version management, schema parsing helpers (form-rendering). | | @rytass/bpm-core-client/template | Approval template CRUD, category management, version publish/revert. | | @rytass/bpm-core-client/workflow | Instance submit / decide / cancel, task queries, notification queries, attachment & signature reads. |

Every subpath uses the same requestGraphQl transport from the root. Hosts can mix and match — for example a read-only dashboard might only import workflow queries, never reaching for /organization mutations.

Organization Mirror Pattern (Important)

TL;DR: BPM owns the org graph (org_units, positions, memberships, manager_resolutions). Hosts with their own org model mirror data into BPM via the /organization mutations rather than exposing a host-side resolver. See the "Organization Data Ownership" section of @rytass/bpm-core-nestjs-module's README for the rationale.

The integration shape is asymmetric with BPMMemberResolver: member identity stays in the host's user table and BPM reaches in via the resolver token; the org structure lives in BPM and the host pushes data in. Three rules keep the mirror idempotent:

  1. Use code as the natural key. Both OrgUnit and Position carry a unique code you control. On every sync, look up by code before deciding INSERT vs UPDATE.
  2. metadataJson is write-only. The mutations accept it for audit purposes — BPM stores the JSON verbatim — but the records returned by readOrganizationDashboard do NOT include the metadata field (kept out of paginated payloads). Always reconcile by code; treat metadataJson as an audit/debugging breadcrumb, not a live FK pointer.
  3. Soft-delete via the delete* mutations. They set deletedAt; live queries (orgUnits, orgUnitCount, etc.) filter soft-deleted rows automatically.

Quick wire-up using only this package's exports. Note the flat input shape on the create/update mutations (no {id, input: {...}} wrapper):

import {
  readOrganizationDashboard,
  createOrgUnit,
  updateOrgUnit,
} from '@rytass/bpm-core-client/organization';
import type { OrgUnitType } from '@rytass/bpm-core-shared';

async function upsertOrgUnit(hostUnit: {
  code: string;
  name: string;
  type: OrgUnitType;
  parentCode: string | null;
  hostId: string;
}): Promise<string> {
  // One round trip pulls every org unit; index by code in your sync loop.
  const dash = await readOrganizationDashboard({ orgUnitPageSize: null });
  const existing = dash.orgUnits.find((u) => u.code === hostUnit.code);
  const parentId = hostUnit.parentCode
    ? dash.orgUnits.find((u) => u.code === hostUnit.parentCode)?.id ?? null
    : null;
  const metadataJson = JSON.stringify({ hostId: hostUnit.hostId });

  if (existing) {
    return (
      await updateOrgUnit({
        id: existing.id,
        code: hostUnit.code,
        name: hostUnit.name,
        type: hostUnit.type,
        parentId,
        metadataJson,
      })
    ).id;
  }
  return (
    await createOrgUnit({
      code: hostUnit.code,
      name: hostUnit.name,
      type: hostUnit.type,
      parentId,
      metadataJson,
    })
  ).id;
}

createPosition, createMembership, updateMembership, and createManagerResolution follow the same flat-input pattern. For bulk tree moves in one transaction, see commitOrgUnitTreeDraft. Only that one mutation is transactional — sequential createMembership calls are independent and partial-failures stay committed.

Server-side base URL override

The GraphQL client resolves its endpoint via NEXT_PUBLIC_API_URL or same-origin. Node scripts that aren't running inside Next.js (cron workers, one-off org seeds, integration tests) should call configureBPMClient from the root package barrel once at startup:

import { configureBPMClient } from '@rytass/bpm-core-client';

configureBPMClient({
  baseUrl: 'https://api.shuttle.example.com',
  // Optional: inject your own fetch (e.g. node-fetch) or default headers.
  fetch: globalThis.fetch,
  headers: { 'X-Service-Token': process.env.BPM_SYNC_TOKEN ?? '' },
});

All subsequent calls to requestGraphQl and the REST auth client honor the override. Both baseUrl and headers are static for the process lifetime — call configureBPMClient again to replace.

Auth REST Endpoints

The BPM wrapper-host exposes the following endpoints under the host root (no /api prefix; do not enable Nest setGlobalPrefix on a BPM host):

| Method | Path | Function | | ------ | --------------------- | ----------------------------------------- | | GET | /auth/test-members | listApiTestMembers() | | POST | /auth/login | loginApi({ identifier, password }) | | GET | /auth/me | readApiCurrentMember() (returns null on 401) | | POST | /auth/logout | logoutApi() |

If your production host names its auth endpoints differently, set NEXT_PUBLIC_API_AUTH_URL so the client points at the right base URL, and implement those paths server-side.

GraphQL Operations

Each subpath exports typed wrapper functions over requestGraphQl. Records returned from these functions match the BPM GraphQL schema field-by-field. Examples:

import { resolveMembers, searchMembers } from '@rytass/bpm-core-client';
import {
  listApprovalInstances,
  decideTask,
  submitApprovalInstance,
} from '@rytass/bpm-core-client/workflow';
import { listApprovalTemplates } from '@rytass/bpm-core-client/template';
import { listFormDefinitions } from '@rytass/bpm-core-client/form';
import { listOrgUnits, listPositions } from '@rytass/bpm-core-client/organization';

const members = await resolveMembers(['member-001', 'member-002']);
const templates = await listApprovalTemplates({ status: 'PUBLISHED' });
const inbox = await listApprovalInstances({ assigneeMemberId: members[0]?.memberId ?? null });

The available operations track the BPM resolvers; consult the GraphQL schema file generated by the host's GraphQLModule (autoSchemaFile) for the authoritative list.

React / Next.js Integration

The client is intentionally hookless. Hosts compose their own hook layer on top — for example with React Query or SWR:

'use client';

import { useQuery } from '@tanstack/react-query';
import { resolveMembers } from '@rytass/bpm-core-client';

export function useMembers(memberIds: readonly string[]) {
  return useQuery({
    queryKey: ['members', memberIds],
    queryFn: () => resolveMembers(memberIds),
    enabled: memberIds.length > 0,
  });
}

For Next.js App Router Client Components the typed wrappers work out of the box (same-origin endpoint, browser cookie jar). For Server Components, Server Actions, Route Handlers, Edge runtimes, or plain Node CLIs, see Runtime Compatibility — those runtimes need an absolute endpoint and manual cookie forwarding.

Local Development

From this monorepo the client is automatically available through the @rytass/bpm-core-client TypeScript path alias; no pnpm link step is required.

External consumers should run against a host built with @rytass/bpm-core-nestjs-module (sample host: apps/api in this repo). The sample host exposes:

  • http://localhost:17603/graphql
  • http://localhost:17603/auth/*
  • http://localhost:17603/attachments/:id/download

Verification

pnpm nx typecheck bpm-core-client
pnpm nx test bpm-core-client
pnpm nx build bpm-core-client

License

MIT