@rytass/bpm-core-client 0.1.1 → 0.1.3
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.
- package/CHANGELOG.md +6 -0
- package/README.md +113 -3
- package/eslint.config.mjs +19 -0
- package/jest.config.cts +10 -0
- package/package.json +4 -5
- package/project.json +66 -0
- package/src/lib/api-auth-client.ts +169 -0
- package/src/lib/form/form-api.spec.ts +81 -0
- package/src/lib/form/form-api.ts +388 -0
- package/src/lib/form/form-rendering.ts +580 -0
- package/src/lib/graphql-client.spec.ts +65 -0
- package/src/lib/graphql-client.ts +114 -0
- package/src/lib/member-api.ts +129 -0
- package/src/lib/organization/organization-api.spec.ts +90 -0
- package/src/lib/organization/organization-api.ts +710 -0
- package/src/lib/template/template-api.spec.ts +88 -0
- package/src/lib/template/template-api.ts +772 -0
- package/src/lib/workflow/workflow-api.spec.ts +132 -0
- package/src/lib/workflow/workflow-api.ts +1749 -0
- package/tsconfig.json +27 -0
- package/tsconfig.lib.json +16 -0
- package/tsconfig.spec.json +16 -0
- package/LICENSE +0 -21
- package/src/index.js +0 -7
- package/src/index.js.map +0 -1
- package/src/lib/api-auth-client.d.ts +0 -73
- package/src/lib/api-auth-client.js +0 -121
- package/src/lib/api-auth-client.js.map +0 -1
- package/src/lib/form/form-api.d.ts +0 -48
- package/src/lib/form/form-api.js +0 -228
- package/src/lib/form/form-api.js.map +0 -1
- package/src/lib/form/form-rendering.d.ts +0 -52
- package/src/lib/form/form-rendering.js +0 -341
- package/src/lib/form/form-rendering.js.map +0 -1
- package/src/lib/form/index.js +0 -6
- package/src/lib/form/index.js.map +0 -1
- package/src/lib/graphql-client.d.ts +0 -45
- package/src/lib/graphql-client.js +0 -92
- package/src/lib/graphql-client.js.map +0 -1
- package/src/lib/member-api.d.ts +0 -49
- package/src/lib/member-api.js +0 -72
- package/src/lib/member-api.js.map +0 -1
- package/src/lib/organization/index.js +0 -5
- package/src/lib/organization/index.js.map +0 -1
- package/src/lib/organization/organization-api.d.ts +0 -168
- package/src/lib/organization/organization-api.js +0 -394
- package/src/lib/organization/organization-api.js.map +0 -1
- package/src/lib/template/index.js +0 -5
- package/src/lib/template/index.js.map +0 -1
- package/src/lib/template/template-api.d.ts +0 -150
- package/src/lib/template/template-api.js +0 -407
- package/src/lib/template/template-api.js.map +0 -1
- package/src/lib/workflow/index.js +0 -5
- package/src/lib/workflow/index.js.map +0 -1
- package/src/lib/workflow/workflow-api.d.ts +0 -376
- package/src/lib/workflow/workflow-api.js +0 -867
- package/src/lib/workflow/workflow-api.js.map +0 -1
- /package/src/{index.d.ts → index.ts} +0 -0
- /package/src/lib/form/{index.d.ts → index.ts} +0 -0
- /package/src/lib/organization/{index.d.ts → index.ts} +0 -0
- /package/src/lib/template/{index.d.ts → index.ts} +0 -0
- /package/src/lib/workflow/{index.d.ts → index.ts} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,12 @@ 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.1.2 — 2026-05-27
|
|
12
|
+
|
|
13
|
+
No source changes. Bumped in lockstep with
|
|
14
|
+
`@rytass/bpm-core-nestjs-module@0.1.2` (see its CHANGELOG for the
|
|
15
|
+
migration class-name fix).
|
|
16
|
+
|
|
11
17
|
## 0.1.1 — 2026-05-27
|
|
12
18
|
|
|
13
19
|
No source changes. Version bumped in lockstep with
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ functions directly from any framework (Next.js / Vite / Remix / plain Node).
|
|
|
18
18
|
|
|
19
19
|
## Package Status
|
|
20
20
|
|
|
21
|
-
Current version: `0.
|
|
21
|
+
Current version: `0.1.2`
|
|
22
22
|
|
|
23
23
|
The package intentionally stays framework-agnostic so it can be reused from
|
|
24
24
|
Next.js Server Components, Server Actions, plain React, or non-React
|
|
@@ -42,6 +42,113 @@ your `tsconfig.json` so the package's `exports` field is honored. Classic
|
|
|
42
42
|
shipped with this package, but is on TypeScript's long-term deprecation
|
|
43
43
|
path — new projects should opt in to modern resolution.
|
|
44
44
|
|
|
45
|
+
## Runtime Compatibility
|
|
46
|
+
|
|
47
|
+
> **TL;DR — the transport is isomorphic (always `fetch`), but session is
|
|
48
|
+
> browser-first.** Out of the box every function works in a browser tab.
|
|
49
|
+
> Server Components, Server Actions, Node CLIs, workers, and Edge Functions
|
|
50
|
+
> can also call the same functions, but the caller is responsible for
|
|
51
|
+
> resolving the endpoint to an absolute URL and for forwarding any
|
|
52
|
+
> session cookie.
|
|
53
|
+
|
|
54
|
+
### What is identical across runtimes
|
|
55
|
+
|
|
56
|
+
| Concern | Behavior |
|
|
57
|
+
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
58
|
+
| Transport | Always `globalThis.fetch`. No `axios`, no `node-fetch`, no `cross-fetch`, no Apollo Link. |
|
|
59
|
+
| Serialization | `Content-Type: application/json`; `POST { query, variables }`. |
|
|
60
|
+
| Error model | Throws `Error` on non-2xx HTTP, on `payload.errors[]`, or on missing `data`. |
|
|
61
|
+
| Type guarantees | Every function returns a typed `Promise<T>` — return shape is identical regardless of where the call runs. |
|
|
62
|
+
|
|
63
|
+
### What differs across runtimes
|
|
64
|
+
|
|
65
|
+
| Concern | Browser | Node SSR / Server Component / CLI |
|
|
66
|
+
| ---------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
|
67
|
+
| 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. |
|
|
68
|
+
| 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. |
|
|
69
|
+
| `loginApi()` storage | Cookie lands in browser jar, persists across navigations. | Returned `Set-Cookie` header is dropped — caller must read and reuse it explicitly. |
|
|
70
|
+
| `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. |
|
|
71
|
+
|
|
72
|
+
### Server Component / Server Action recipe
|
|
73
|
+
|
|
74
|
+
Next.js App Router server runtime needs two things: an absolute endpoint and
|
|
75
|
+
a way to relay the inbound `Cookie` header. The recommended pattern is to
|
|
76
|
+
set `NEXT_PUBLIC_API_URL` at build time and pass cookies via the underlying
|
|
77
|
+
`fetch` headers using a wrapper such as Next's `cookies()`:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
// app/inbox/page.tsx — Server Component
|
|
81
|
+
import { cookies } from 'next/headers';
|
|
82
|
+
|
|
83
|
+
async function fetchInbox(): Promise<unknown> {
|
|
84
|
+
const cookieHeader = (await cookies()).toString();
|
|
85
|
+
const response = await fetch(`${process.env.BPM_API_URL}/graphql`, {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: {
|
|
88
|
+
'Content-Type': 'application/json',
|
|
89
|
+
Cookie: cookieHeader,
|
|
90
|
+
},
|
|
91
|
+
body: JSON.stringify({
|
|
92
|
+
query: 'query Ping { memberCount }',
|
|
93
|
+
}),
|
|
94
|
+
});
|
|
95
|
+
return response.json();
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
> For SSR cases that need to call **many** typed functions, the cleanest
|
|
100
|
+
> long-term option is to add a `configureBPMClient({ fetch, endpoint })`
|
|
101
|
+
> hook so the typed wrappers can be reused. That is not yet shipped — if
|
|
102
|
+
> you need it, file an issue. Today, Server Component callers either talk
|
|
103
|
+
> to the host directly (as above) or proxy through a Route Handler that
|
|
104
|
+
> runs inside the consumer's browser session.
|
|
105
|
+
|
|
106
|
+
### Node CLI recipe
|
|
107
|
+
|
|
108
|
+
CLIs that drive BPM from outside a browser need both an absolute endpoint
|
|
109
|
+
and explicit session management. The auth REST client (`loginApi`,
|
|
110
|
+
`logoutApi`, `readApiCurrentMember`) returns typed bodies but does not
|
|
111
|
+
persist cookies in Node — capture them yourself:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
// scripts/dump-running-instances.ts
|
|
115
|
+
import { listApprovalInstancesPage } from '@rytass/bpm-core-client/workflow';
|
|
116
|
+
|
|
117
|
+
process.env.NEXT_PUBLIC_API_URL ??= 'https://bpm.your-host.example/graphql';
|
|
118
|
+
|
|
119
|
+
// 1. Log in and capture the session cookie manually.
|
|
120
|
+
const loginResponse = await fetch(
|
|
121
|
+
'https://bpm.your-host.example/auth/login',
|
|
122
|
+
{
|
|
123
|
+
method: 'POST',
|
|
124
|
+
headers: { 'Content-Type': 'application/json' },
|
|
125
|
+
body: JSON.stringify({ identifier: 'svc-bot', password: '...' }),
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
const cookieJar = loginResponse.headers.get('set-cookie') ?? '';
|
|
129
|
+
|
|
130
|
+
// 2. Hand the cookie to subsequent fetches via a small fetch override.
|
|
131
|
+
const originalFetch = globalThis.fetch;
|
|
132
|
+
globalThis.fetch = ((input, init = {}) =>
|
|
133
|
+
originalFetch(input, {
|
|
134
|
+
...init,
|
|
135
|
+
headers: { ...(init.headers ?? {}), Cookie: cookieJar },
|
|
136
|
+
})) as typeof fetch;
|
|
137
|
+
|
|
138
|
+
// 3. Call typed BPM functions as usual.
|
|
139
|
+
const { instances } = await listApprovalInstancesPage({
|
|
140
|
+
view: 'ALL', state: 'RUNNING', page: 1, pageSize: 1000,
|
|
141
|
+
searchText: '', templateId: null,
|
|
142
|
+
});
|
|
143
|
+
console.log(instances.length, 'running instances');
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Edge / Workers
|
|
147
|
+
|
|
148
|
+
Cloudflare Workers, Vercel Edge Functions, and similar V8-isolate runtimes
|
|
149
|
+
ship `fetch` natively and follow the **Node SSR** column above — no
|
|
150
|
+
default endpoint, no cookie jar. Treat them the same as Server Components.
|
|
151
|
+
|
|
45
152
|
## Endpoint Resolution
|
|
46
153
|
|
|
47
154
|
By default the client targets the same origin (no `/api` prefix) — see
|
|
@@ -161,8 +268,11 @@ export function useMembers(memberIds: readonly string[]) {
|
|
|
161
268
|
}
|
|
162
269
|
```
|
|
163
270
|
|
|
164
|
-
For Next.js App Router
|
|
165
|
-
|
|
271
|
+
For Next.js App Router Client Components the typed wrappers work
|
|
272
|
+
out of the box (same-origin endpoint, browser cookie jar). For Server
|
|
273
|
+
Components, Server Actions, Route Handlers, Edge runtimes, or plain Node
|
|
274
|
+
CLIs, see **[Runtime Compatibility](#runtime-compatibility)** — those
|
|
275
|
+
runtimes need an absolute endpoint and manual cookie forwarding.
|
|
166
276
|
|
|
167
277
|
## Local Development
|
|
168
278
|
|
|
@@ -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
|
+
];
|
package/jest.config.cts
ADDED
|
@@ -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.1.
|
|
3
|
+
"version": "0.1.3",
|
|
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.1.
|
|
76
|
+
"@rytass/bpm-core-shared": "^0.1.3"
|
|
77
77
|
},
|
|
78
78
|
"dependencies": {
|
|
79
79
|
"tslib": "^2.3.0"
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
}
|
|
80
|
+
}
|
|
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,169 @@
|
|
|
1
|
+
import { readGraphQlEndpoint } from './graphql-client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Authenticated BPM member returned by {@link loginApi} and
|
|
5
|
+
* {@link readApiCurrentMember}.
|
|
6
|
+
*
|
|
7
|
+
* Mirrors the shape that BPM-compatible wrapper hosts emit from their
|
|
8
|
+
* `POST /auth/login` and `GET /auth/me` endpoints. Roles and permissions
|
|
9
|
+
* are used by BPM's `BPMAdminGuard` / `BPMDesignerGuard`.
|
|
10
|
+
*/
|
|
11
|
+
export interface ApiMember {
|
|
12
|
+
readonly email: string;
|
|
13
|
+
readonly expiresAt: string;
|
|
14
|
+
readonly memberId: string;
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly permissions: readonly string[];
|
|
17
|
+
readonly roles: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Reduced member shape returned by {@link listApiTestMembers}. Excludes
|
|
22
|
+
* `permissions` and `expiresAt` because the test-members endpoint is meant
|
|
23
|
+
* for picker UIs in dev / demo, not for session establishment.
|
|
24
|
+
*/
|
|
25
|
+
export interface ApiPublicMember {
|
|
26
|
+
readonly email: string;
|
|
27
|
+
readonly memberId: string;
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly roles: readonly string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Lists seeded test members exposed by the host's `/auth/test-members`
|
|
34
|
+
* endpoint. Hosts targeting production typically disable this endpoint or
|
|
35
|
+
* gate it behind a feature flag.
|
|
36
|
+
*/
|
|
37
|
+
export async function listApiTestMembers(): Promise<
|
|
38
|
+
readonly ApiPublicMember[]
|
|
39
|
+
> {
|
|
40
|
+
return requestApi<readonly ApiPublicMember[]>('/auth/test-members');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Performs a login against the host's `POST /auth/login` endpoint. On
|
|
45
|
+
* success the host issues an HTTP-only session cookie that subsequent
|
|
46
|
+
* {@link requestGraphQl} calls automatically carry, because
|
|
47
|
+
* {@link requestApi} uses `credentials: 'include'`.
|
|
48
|
+
*
|
|
49
|
+
* Throws on HTTP errors with a message extracted from the response body
|
|
50
|
+
* when available.
|
|
51
|
+
*
|
|
52
|
+
* @param input.identifier - Member id, email, or any other identifier the
|
|
53
|
+
* host understands.
|
|
54
|
+
* @param input.password - Cleartext credential — TLS is the consumer's
|
|
55
|
+
* responsibility.
|
|
56
|
+
*/
|
|
57
|
+
export async function loginApi(input: {
|
|
58
|
+
readonly identifier: string;
|
|
59
|
+
readonly password: string;
|
|
60
|
+
}): Promise<ApiMember> {
|
|
61
|
+
return requestApi<ApiMember>('/auth/login', {
|
|
62
|
+
body: JSON.stringify(input),
|
|
63
|
+
headers: { 'Content-Type': 'application/json' },
|
|
64
|
+
method: 'POST',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Calls `POST /auth/logout` on the host. The host clears the session
|
|
70
|
+
* cookie. Safe to call when not logged in (host should return 200).
|
|
71
|
+
*/
|
|
72
|
+
export async function logoutApi(): Promise<void> {
|
|
73
|
+
await requestApi<{ readonly ok: true }>('/auth/logout', {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Reads the currently authenticated member from `GET /auth/me`.
|
|
80
|
+
*
|
|
81
|
+
* Returns `null` when the host replies `401`, which is the conventional
|
|
82
|
+
* way to signal "no active session". Any other HTTP failure throws.
|
|
83
|
+
*/
|
|
84
|
+
export async function readApiCurrentMember(): Promise<ApiMember | null> {
|
|
85
|
+
try {
|
|
86
|
+
return await requestApi<ApiMember>('/auth/me');
|
|
87
|
+
} catch (error: unknown) {
|
|
88
|
+
if (error instanceof ApiRequestError && error.status === 401) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolves the base URL used by the REST `/auth/*` calls.
|
|
98
|
+
*
|
|
99
|
+
* Resolution order:
|
|
100
|
+
* 1. `process.env.NEXT_PUBLIC_API_AUTH_URL` if non-empty.
|
|
101
|
+
* 2. Derived from {@link readGraphQlEndpoint}'s origin — the host is
|
|
102
|
+
* assumed to expose both `/graphql` and `/auth/*` at the same origin.
|
|
103
|
+
*/
|
|
104
|
+
export function readApiBaseUrl(): string {
|
|
105
|
+
const explicitApiBaseUrl = process.env.NEXT_PUBLIC_API_AUTH_URL?.trim();
|
|
106
|
+
|
|
107
|
+
if (explicitApiBaseUrl) {
|
|
108
|
+
return explicitApiBaseUrl.replace(/\/$/, '');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const graphQlEndpoint = readGraphQlEndpoint();
|
|
112
|
+
|
|
113
|
+
return resolveApiBaseUrlFromGraphQlEndpoint(graphQlEndpoint);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function resolveApiBaseUrlFromGraphQlEndpoint(
|
|
117
|
+
graphQlEndpoint: string,
|
|
118
|
+
): string {
|
|
119
|
+
if (!isAbsoluteUrl(graphQlEndpoint)) {
|
|
120
|
+
return '';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const endpoint = new URL(graphQlEndpoint);
|
|
124
|
+
endpoint.pathname = '/';
|
|
125
|
+
endpoint.search = '';
|
|
126
|
+
endpoint.hash = '';
|
|
127
|
+
return endpoint.toString().replace(/\/$/, '');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function requestApi<TData>(
|
|
131
|
+
path: string,
|
|
132
|
+
init?: RequestInit,
|
|
133
|
+
): Promise<TData> {
|
|
134
|
+
const response = await fetch(`${readApiBaseUrl()}${path}`, {
|
|
135
|
+
credentials: 'include',
|
|
136
|
+
...init,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
throw new ApiRequestError(response.status, await readError(response));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return (await response.json()) as TData;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function readError(response: Response): Promise<string> {
|
|
147
|
+
try {
|
|
148
|
+
const payload = (await response.json()) as { readonly message?: unknown };
|
|
149
|
+
|
|
150
|
+
return typeof payload.message === 'string'
|
|
151
|
+
? payload.message
|
|
152
|
+
: `BPM API request failed with HTTP ${response.status}`;
|
|
153
|
+
} catch {
|
|
154
|
+
return `BPM API request failed with HTTP ${response.status}`;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
class ApiRequestError extends Error {
|
|
159
|
+
constructor(
|
|
160
|
+
readonly status: number,
|
|
161
|
+
message: string,
|
|
162
|
+
) {
|
|
163
|
+
super(message);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function isAbsoluteUrl(value: string): boolean {
|
|
168
|
+
return value.startsWith('http://') || value.startsWith('https://');
|
|
169
|
+
}
|
|
@@ -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
|
+
});
|