@rytass/bpm-core-client 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/eslint.config.mjs +19 -0
  3. package/jest.config.cts +10 -0
  4. package/package.json +3 -4
  5. package/project.json +66 -0
  6. package/src/lib/api-auth-client.ts +182 -0
  7. package/src/lib/client-config.ts +99 -0
  8. package/src/lib/form/form-api.spec.ts +81 -0
  9. package/src/lib/form/form-api.ts +402 -0
  10. package/src/lib/form/form-rendering.ts +580 -0
  11. package/src/lib/graphql-client.spec.ts +65 -0
  12. package/src/lib/graphql-client.ts +128 -0
  13. package/src/lib/member-api.ts +129 -0
  14. package/src/lib/organization/organization-api.spec.ts +90 -0
  15. package/src/lib/organization/organization-api.ts +711 -0
  16. package/src/lib/template/template-api.spec.ts +88 -0
  17. package/src/lib/template/template-api.ts +894 -0
  18. package/src/lib/workflow/workflow-api.spec.ts +132 -0
  19. package/src/lib/workflow/workflow-api.ts +2124 -0
  20. package/tsconfig.json +27 -0
  21. package/tsconfig.lib.json +16 -0
  22. package/tsconfig.spec.json +16 -0
  23. package/src/index.js +0 -8
  24. package/src/index.js.map +0 -1
  25. package/src/lib/api-auth-client.d.ts +0 -73
  26. package/src/lib/api-auth-client.js +0 -129
  27. package/src/lib/api-auth-client.js.map +0 -1
  28. package/src/lib/client-config.d.ts +0 -81
  29. package/src/lib/client-config.js +0 -63
  30. package/src/lib/client-config.js.map +0 -1
  31. package/src/lib/form/form-api.d.ts +0 -54
  32. package/src/lib/form/form-api.js +0 -240
  33. package/src/lib/form/form-api.js.map +0 -1
  34. package/src/lib/form/form-rendering.d.ts +0 -52
  35. package/src/lib/form/form-rendering.js +0 -341
  36. package/src/lib/form/form-rendering.js.map +0 -1
  37. package/src/lib/form/index.js +0 -6
  38. package/src/lib/form/index.js.map +0 -1
  39. package/src/lib/graphql-client.d.ts +0 -45
  40. package/src/lib/graphql-client.js +0 -101
  41. package/src/lib/graphql-client.js.map +0 -1
  42. package/src/lib/member-api.d.ts +0 -49
  43. package/src/lib/member-api.js +0 -72
  44. package/src/lib/member-api.js.map +0 -1
  45. package/src/lib/organization/index.js +0 -5
  46. package/src/lib/organization/index.js.map +0 -1
  47. package/src/lib/organization/organization-api.d.ts +0 -173
  48. package/src/lib/organization/organization-api.js +0 -394
  49. package/src/lib/organization/organization-api.js.map +0 -1
  50. package/src/lib/template/index.js +0 -5
  51. package/src/lib/template/index.js.map +0 -1
  52. package/src/lib/template/template-api.d.ts +0 -174
  53. package/src/lib/template/template-api.js +0 -469
  54. package/src/lib/template/template-api.js.map +0 -1
  55. package/src/lib/workflow/index.js +0 -5
  56. package/src/lib/workflow/index.js.map +0 -1
  57. package/src/lib/workflow/workflow-api.d.ts +0 -517
  58. package/src/lib/workflow/workflow-api.js +0 -1068
  59. package/src/lib/workflow/workflow-api.js.map +0 -1
  60. /package/src/{index.d.ts → index.ts} +0 -0
  61. /package/src/lib/form/{index.d.ts → index.ts} +0 -0
  62. /package/src/lib/organization/{index.d.ts → index.ts} +0 -0
  63. /package/src/lib/template/{index.d.ts → index.ts} +0 -0
  64. /package/src/lib/workflow/{index.d.ts → index.ts} +0 -0
package/CHANGELOG.md CHANGED
@@ -8,6 +8,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
  Releases are managed by [`nx release`](https://nx.dev/recipes/nx-release) with
9
9
  Conventional Commits — see `nx.json` for the release config.
10
10
 
11
+ ## 0.5.0 — 2026-07-06
12
+
13
+ No source changes. Version aligned with `@rytass/bpm-core-shared` and
14
+ `@rytass/bpm-core-nestjs-module` 0.5.0 (fixed release group).
15
+
16
+ ### Changed
17
+
18
+ - Peer dependency `@rytass/bpm-core-shared` now requires ^0.5.0.
19
+
11
20
  ## 0.4.0 — 2026-07-02
12
21
 
13
22
  ### Fixed
@@ -0,0 +1,19 @@
1
+ import baseConfig from '../../eslint.config.mjs';
2
+
3
+ export default [
4
+ ...baseConfig,
5
+ {
6
+ files: ['**/*.json'],
7
+ rules: {
8
+ '@nx/dependency-checks': [
9
+ 'error',
10
+ {
11
+ ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}'],
12
+ },
13
+ ],
14
+ },
15
+ languageOptions: {
16
+ parser: await import('jsonc-eslint-parser'),
17
+ },
18
+ },
19
+ ];
@@ -0,0 +1,10 @@
1
+ module.exports = {
2
+ displayName: 'bpm-core-client',
3
+ preset: '../../jest.preset.js',
4
+ testEnvironment: 'node',
5
+ transform: {
6
+ '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
7
+ },
8
+ moduleFileExtensions: ['ts', 'js', 'html'],
9
+ coverageDirectory: '../../coverage/libs/bpm-core-client',
10
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rytass/bpm-core-client",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "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.",
5
5
  "keywords": [
6
6
  "bpm",
@@ -73,10 +73,9 @@
73
73
  }
74
74
  },
75
75
  "peerDependencies": {
76
- "@rytass/bpm-core-shared": "^0.4.0"
76
+ "@rytass/bpm-core-shared": "^0.5.0"
77
77
  },
78
78
  "dependencies": {
79
79
  "tslib": "^2.3.0"
80
- },
81
- "type": "commonjs"
80
+ }
82
81
  }
package/project.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "bpm-core-client",
3
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
+ "sourceRoot": "libs/bpm-core-client/src",
5
+ "projectType": "library",
6
+ "tags": [],
7
+ "targets": {
8
+ "build": {
9
+ "executor": "@nx/js:tsc",
10
+ "outputs": ["{options.outputPath}"],
11
+ "options": {
12
+ "outputPath": "dist/libs/bpm-core-client",
13
+ "main": "libs/bpm-core-client/src/index.ts",
14
+ "tsConfig": "libs/bpm-core-client/tsconfig.lib.json",
15
+ "generatePackageJson": true,
16
+ "assets": [
17
+ "libs/bpm-core-client/*.md",
18
+ { "input": ".", "glob": "LICENSE", "output": "." }
19
+ ]
20
+ }
21
+ },
22
+ "test": {
23
+ "executor": "@nx/jest:jest",
24
+ "outputs": ["{workspaceRoot}/coverage/libs/bpm-core-client"],
25
+ "options": {
26
+ "jestConfig": "libs/bpm-core-client/jest.config.cts",
27
+ "passWithNoTests": true
28
+ }
29
+ },
30
+ "typecheck": {
31
+ "executor": "nx:run-commands",
32
+ "options": {
33
+ "command": "tsc --noEmit -p libs/bpm-core-client/tsconfig.lib.json"
34
+ }
35
+ },
36
+ "finalize-dist": {
37
+ "executor": "nx:run-commands",
38
+ "outputs": ["{workspaceRoot}/dist/libs/bpm-core-client"],
39
+ "dependsOn": ["build"],
40
+ "options": {
41
+ "command": "node tools/publish/finalize-dist-package.mjs dist/libs/bpm-core-client"
42
+ }
43
+ },
44
+ "pkg-quality": {
45
+ "executor": "nx:run-commands",
46
+ "dependsOn": ["finalize-dist"],
47
+ "options": {
48
+ "commands": [
49
+ "pnpm exec publint dist/libs/bpm-core-client"
50
+ ],
51
+ "parallel": false
52
+ }
53
+ },
54
+ "pkg-quality-attw": {
55
+ "executor": "nx:run-commands",
56
+ "dependsOn": ["finalize-dist"],
57
+ "options": {
58
+ "command": "pnpm exec attw --pack dist/libs/bpm-core-client"
59
+ }
60
+ },
61
+ "prepublish-check": {
62
+ "executor": "nx:noop",
63
+ "dependsOn": ["typecheck", "lint", "test", "finalize-dist", "pkg-quality"]
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,182 @@
1
+ import { readGraphQlEndpoint } from './graphql-client';
2
+ import {
3
+ readBPMClientConfig,
4
+ readBPMConfiguredHeaders,
5
+ resolveBPMFetch,
6
+ } from './client-config';
7
+
8
+ /**
9
+ * Authenticated BPM member returned by {@link loginApi} and
10
+ * {@link readApiCurrentMember}.
11
+ *
12
+ * Mirrors the shape that BPM-compatible wrapper hosts emit from their
13
+ * `POST /auth/login` and `GET /auth/me` endpoints. Roles and permissions
14
+ * are used by BPM's `BPMAdminGuard` / `BPMDesignerGuard`.
15
+ */
16
+ export interface ApiMember {
17
+ readonly email: string;
18
+ readonly expiresAt: string;
19
+ readonly memberId: string;
20
+ readonly name: string;
21
+ readonly permissions: readonly string[];
22
+ readonly roles: readonly string[];
23
+ }
24
+
25
+ /**
26
+ * Reduced member shape returned by {@link listApiTestMembers}. Excludes
27
+ * `permissions` and `expiresAt` because the test-members endpoint is meant
28
+ * for picker UIs in dev / demo, not for session establishment.
29
+ */
30
+ export interface ApiPublicMember {
31
+ readonly email: string;
32
+ readonly memberId: string;
33
+ readonly name: string;
34
+ readonly roles: readonly string[];
35
+ }
36
+
37
+ /**
38
+ * Lists seeded test members exposed by the host's `/auth/test-members`
39
+ * endpoint. Hosts targeting production typically disable this endpoint or
40
+ * gate it behind a feature flag.
41
+ */
42
+ export async function listApiTestMembers(): Promise<
43
+ readonly ApiPublicMember[]
44
+ > {
45
+ return requestApi<readonly ApiPublicMember[]>('/auth/test-members');
46
+ }
47
+
48
+ /**
49
+ * Performs a login against the host's `POST /auth/login` endpoint. On
50
+ * success the host issues an HTTP-only session cookie that subsequent
51
+ * {@link requestGraphQl} calls automatically carry, because
52
+ * {@link requestApi} uses `credentials: 'include'`.
53
+ *
54
+ * Throws on HTTP errors with a message extracted from the response body
55
+ * when available.
56
+ *
57
+ * @param input.identifier - Member id, email, or any other identifier the
58
+ * host understands.
59
+ * @param input.password - Cleartext credential — TLS is the consumer's
60
+ * responsibility.
61
+ */
62
+ export async function loginApi(input: {
63
+ readonly identifier: string;
64
+ readonly password: string;
65
+ }): Promise<ApiMember> {
66
+ return requestApi<ApiMember>('/auth/login', {
67
+ body: JSON.stringify(input),
68
+ headers: { 'Content-Type': 'application/json' },
69
+ method: 'POST',
70
+ });
71
+ }
72
+
73
+ /**
74
+ * Calls `POST /auth/logout` on the host. The host clears the session
75
+ * cookie. Safe to call when not logged in (host should return 200).
76
+ */
77
+ export async function logoutApi(): Promise<void> {
78
+ await requestApi<{ readonly ok: true }>('/auth/logout', {
79
+ method: 'POST',
80
+ });
81
+ }
82
+
83
+ /**
84
+ * Reads the currently authenticated member from `GET /auth/me`.
85
+ *
86
+ * Returns `null` when the host replies `401`, which is the conventional
87
+ * way to signal "no active session". Any other HTTP failure throws.
88
+ */
89
+ export async function readApiCurrentMember(): Promise<ApiMember | null> {
90
+ try {
91
+ return await requestApi<ApiMember>('/auth/me');
92
+ } catch (error: unknown) {
93
+ if (error instanceof ApiRequestError && error.status === 401) {
94
+ return null;
95
+ }
96
+
97
+ throw error;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Resolves the base URL used by the REST `/auth/*` calls.
103
+ *
104
+ * Resolution order:
105
+ * 1. `process.env.NEXT_PUBLIC_API_AUTH_URL` if non-empty.
106
+ * 2. Derived from {@link readGraphQlEndpoint}'s origin — the host is
107
+ * assumed to expose both `/graphql` and `/auth/*` at the same origin.
108
+ */
109
+ export function readApiBaseUrl(): string {
110
+ const config = readBPMClientConfig();
111
+ if (config.authBaseUrl?.trim()) {
112
+ return config.authBaseUrl.trim().replace(/\/$/, '');
113
+ }
114
+
115
+ const explicitApiBaseUrl = process.env.NEXT_PUBLIC_API_AUTH_URL?.trim();
116
+
117
+ if (explicitApiBaseUrl) {
118
+ return explicitApiBaseUrl.replace(/\/$/, '');
119
+ }
120
+
121
+ const graphQlEndpoint = readGraphQlEndpoint();
122
+
123
+ return resolveApiBaseUrlFromGraphQlEndpoint(graphQlEndpoint);
124
+ }
125
+
126
+ export function resolveApiBaseUrlFromGraphQlEndpoint(
127
+ graphQlEndpoint: string,
128
+ ): string {
129
+ if (!isAbsoluteUrl(graphQlEndpoint)) {
130
+ return '';
131
+ }
132
+
133
+ const endpoint = new URL(graphQlEndpoint);
134
+ endpoint.pathname = '/';
135
+ endpoint.search = '';
136
+ endpoint.hash = '';
137
+ return endpoint.toString().replace(/\/$/, '');
138
+ }
139
+
140
+ async function requestApi<TData>(
141
+ path: string,
142
+ init?: RequestInit,
143
+ ): Promise<TData> {
144
+ const fetchImpl = resolveBPMFetch();
145
+ const configuredHeaders = readBPMConfiguredHeaders();
146
+ const response = await fetchImpl(`${readApiBaseUrl()}${path}`, {
147
+ credentials: 'include',
148
+ ...init,
149
+ headers: { ...configuredHeaders, ...(init?.headers ?? {}) },
150
+ });
151
+
152
+ if (!response.ok) {
153
+ throw new ApiRequestError(response.status, await readError(response));
154
+ }
155
+
156
+ return (await response.json()) as TData;
157
+ }
158
+
159
+ async function readError(response: Response): Promise<string> {
160
+ try {
161
+ const payload = (await response.json()) as { readonly message?: unknown };
162
+
163
+ return typeof payload.message === 'string'
164
+ ? payload.message
165
+ : `BPM API request failed with HTTP ${response.status}`;
166
+ } catch {
167
+ return `BPM API request failed with HTTP ${response.status}`;
168
+ }
169
+ }
170
+
171
+ class ApiRequestError extends Error {
172
+ constructor(
173
+ readonly status: number,
174
+ message: string,
175
+ ) {
176
+ super(message);
177
+ }
178
+ }
179
+
180
+ function isAbsoluteUrl(value: string): boolean {
181
+ return value.startsWith('http://') || value.startsWith('https://');
182
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Programmatic configuration override for the BPM client transport.
3
+ *
4
+ * The default endpoint resolution (`NEXT_PUBLIC_API_URL` → localhost →
5
+ * same-origin) covers Next.js consumers. Node-side scripts (cron
6
+ * workers, one-off org seeds, integration tests) usually cannot rely on
7
+ * environment variables and need a programmatic hook. Call
8
+ * {@link configureBPMClient} once at startup; subsequent
9
+ * `requestGraphQl` and REST auth calls honor the override.
10
+ */
11
+
12
+ export interface BPMClientConfig {
13
+ /**
14
+ * Override the GraphQL endpoint URL. When provided, takes precedence
15
+ * over `NEXT_PUBLIC_API_URL` and the default localhost / same-origin
16
+ * resolution. Pass an absolute URL ending without `/graphql` to also
17
+ * derive the REST `/auth/*` base URL automatically, or pass the full
18
+ * `https://host/graphql` form to set the GraphQL endpoint directly.
19
+ *
20
+ * If you need different bases for GraphQL vs `/auth/*` (rare — most
21
+ * hosts colocate them), set both `baseUrl` and `authBaseUrl`.
22
+ */
23
+ readonly baseUrl?: string;
24
+
25
+ /**
26
+ * Override the REST `/auth/*` base URL independently of `baseUrl`.
27
+ * Most hosts colocate GraphQL and auth on the same origin; this knob
28
+ * exists for the rare split-origin deployment.
29
+ */
30
+ readonly authBaseUrl?: string;
31
+
32
+ /**
33
+ * Replace the global `fetch` implementation. Useful when running on a
34
+ * runtime without a global `fetch` (older Node versions, edge runtimes
35
+ * with custom polyfills) or when you want to inject a tracing wrapper.
36
+ */
37
+ readonly fetch?: typeof fetch;
38
+
39
+ /**
40
+ * Additional headers merged into every outbound request. Use this to
41
+ * pass service tokens, tracing headers, or a server-side session
42
+ * cookie that the browser would normally send automatically.
43
+ *
44
+ * Note: `Content-Type` is set internally for GraphQL POSTs and
45
+ * cannot be overridden through this option.
46
+ */
47
+ readonly headers?: Readonly<Record<string, string>>;
48
+ }
49
+
50
+ let currentConfig: BPMClientConfig = {};
51
+
52
+ /**
53
+ * Sets the active {@link BPMClientConfig}. Subsequent calls **replace**
54
+ * the previous configuration; pass `{}` to reset to environment-based
55
+ * defaults.
56
+ *
57
+ * Idiomatic call site is the very top of a Node worker's bootstrap:
58
+ *
59
+ * ```ts
60
+ * import { configureBPMClient } from '@rytass/bpm-core-client';
61
+ *
62
+ * configureBPMClient({
63
+ * baseUrl: 'https://api.shuttle.example.com',
64
+ * headers: { 'X-Service-Token': process.env.BPM_SYNC_TOKEN ?? '' },
65
+ * });
66
+ * ```
67
+ *
68
+ * Browser consumers under Next.js typically do **not** need to call
69
+ * this — `NEXT_PUBLIC_API_URL` plus same-origin defaults cover the
70
+ * common case.
71
+ */
72
+ export function configureBPMClient(config: BPMClientConfig): void {
73
+ currentConfig = { ...config };
74
+ }
75
+
76
+ /**
77
+ * Reads the active {@link BPMClientConfig}. Exposed primarily for tests
78
+ * and for inspecting the current override at runtime — consumers should
79
+ * use {@link configureBPMClient} to mutate, never reach in directly.
80
+ */
81
+ export function readBPMClientConfig(): BPMClientConfig {
82
+ return currentConfig;
83
+ }
84
+
85
+ /**
86
+ * Resolves the effective `fetch` implementation honoring the active
87
+ * override, falling back to the global `fetch`.
88
+ */
89
+ export function resolveBPMFetch(): typeof fetch {
90
+ return currentConfig.fetch ?? globalThis.fetch;
91
+ }
92
+
93
+ /**
94
+ * Returns the headers configured via {@link configureBPMClient}.
95
+ * Internal transports merge these into every outbound request.
96
+ */
97
+ export function readBPMConfiguredHeaders(): Readonly<Record<string, string>> {
98
+ return currentConfig.headers ?? {};
99
+ }
@@ -0,0 +1,81 @@
1
+ import { createFormDefinition, listFormDefinitions } from './form-api';
2
+
3
+ interface CapturedRequest {
4
+ readonly query: string;
5
+ readonly variables?: Readonly<Record<string, unknown>>;
6
+ }
7
+
8
+ function installFetchMock<TData>(payload: TData): {
9
+ readonly capture: () => CapturedRequest;
10
+ readonly restore: () => void;
11
+ } {
12
+ const originalFetch = global.fetch;
13
+ const fetchMock = jest.fn<
14
+ Promise<Response>,
15
+ [RequestInfo | URL, RequestInit?]
16
+ >();
17
+ fetchMock.mockImplementation(
18
+ async () =>
19
+ new Response(JSON.stringify({ data: payload }), {
20
+ headers: { 'Content-Type': 'application/json' },
21
+ status: 200,
22
+ }),
23
+ );
24
+ (global as { fetch: typeof fetch }).fetch =
25
+ fetchMock as unknown as typeof fetch;
26
+
27
+ return {
28
+ capture: (): CapturedRequest => {
29
+ expect(fetchMock).toHaveBeenCalledTimes(1);
30
+ const init = fetchMock.mock.calls[0]?.[1];
31
+ return JSON.parse(String(init?.body ?? '{}')) as CapturedRequest;
32
+ },
33
+ restore: (): void => {
34
+ (global as { fetch: typeof fetch }).fetch = originalFetch;
35
+ },
36
+ };
37
+ }
38
+
39
+ describe('@rytass/bpm-core-client/form', () => {
40
+ describe('listFormDefinitions', () => {
41
+ it('issues the FormDefinitions query and returns the array', async (): Promise<void> => {
42
+ const harness = installFetchMock({ formDefinitions: [] });
43
+ try {
44
+ const records = await listFormDefinitions();
45
+ const request = harness.capture();
46
+
47
+ expect(request.query).toContain('query FormDefinitions');
48
+ expect(request.variables).toBeUndefined();
49
+ expect(records).toEqual([]);
50
+ } finally {
51
+ harness.restore();
52
+ }
53
+ });
54
+ });
55
+
56
+ describe('createFormDefinition', () => {
57
+ it('wraps the name in a CreateFormDefinitionInput and returns the new id', async (): Promise<void> => {
58
+ const harness = installFetchMock({
59
+ createFormDefinition: { id: 'form-1' },
60
+ });
61
+ try {
62
+ const id = await createFormDefinition('Vacation request');
63
+ const request = harness.capture();
64
+
65
+ expect(request.query).toContain('mutation CreateFormDefinition');
66
+ expect(request.variables).toEqual({
67
+ input: {
68
+ createdByMemberId: null,
69
+ description: null,
70
+ name: 'Vacation request',
71
+ schemaJson: null,
72
+ uiSchemaJson: null,
73
+ },
74
+ });
75
+ expect(id).toBe('form-1');
76
+ } finally {
77
+ harness.restore();
78
+ }
79
+ });
80
+ });
81
+ });