@rytass/bpm-core-client 0.3.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 +22 -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
@@ -0,0 +1,128 @@
1
+ import {
2
+ readBPMClientConfig,
3
+ readBPMConfiguredHeaders,
4
+ resolveBPMFetch,
5
+ } from './client-config';
6
+
7
+ const LOCAL_GRAPHQL_ENDPOINT = 'http://localhost:17603/graphql';
8
+ const SAME_ORIGIN_GRAPHQL_ENDPOINT = '/graphql';
9
+
10
+ interface GraphQlError {
11
+ readonly message: string;
12
+ }
13
+
14
+ interface GraphQlResponse<TData> {
15
+ readonly data?: TData;
16
+ readonly errors?: readonly GraphQlError[];
17
+ }
18
+
19
+ /**
20
+ * Sends a single GraphQL operation to the BPM host and returns the typed data.
21
+ *
22
+ * Uses the standard Fetch API with `credentials: 'include'`, so any
23
+ * HTTP-only session cookie issued by the host's `POST /auth/login` endpoint
24
+ * flows back automatically on subsequent calls.
25
+ *
26
+ * The endpoint URL is resolved by {@link readGraphQlEndpoint} — set
27
+ * `NEXT_PUBLIC_API_URL` to override the default localhost / same-origin
28
+ * resolution.
29
+ *
30
+ * Throws an `Error` when the HTTP response is non-2xx, when the GraphQL
31
+ * payload carries an `errors` array, or when the response has no `data`
32
+ * field.
33
+ *
34
+ * @typeParam TData - Shape of the `data` field for the operation.
35
+ * @param query - GraphQL operation document (query or mutation source).
36
+ * @param variables - Variables passed to the GraphQL operation.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * interface PingData { readonly memberCount: number; }
41
+ * const data = await requestGraphQl<PingData>('query Ping { memberCount }');
42
+ * console.log(data.memberCount);
43
+ * ```
44
+ */
45
+ export async function requestGraphQl<TData>(
46
+ query: string,
47
+ variables?: Readonly<Record<string, unknown>>,
48
+ ): Promise<TData> {
49
+ const fetchImpl = resolveBPMFetch();
50
+ const response = await fetchImpl(readGraphQlEndpoint(), {
51
+ body: JSON.stringify({ query, variables }),
52
+ credentials: 'include',
53
+ headers: buildGraphQlHeaders(),
54
+ method: 'POST',
55
+ });
56
+
57
+ if (!response.ok) {
58
+ throw new Error(`GraphQL request failed with HTTP ${response.status}`);
59
+ }
60
+
61
+ const payload = (await response.json()) as GraphQlResponse<TData>;
62
+
63
+ if (payload.errors?.length) {
64
+ throw new Error(payload.errors.map((error) => error.message).join('; '));
65
+ }
66
+
67
+ if (!payload.data) {
68
+ throw new Error('GraphQL response did not include data');
69
+ }
70
+
71
+ return payload.data;
72
+ }
73
+
74
+ /**
75
+ * Resolves the GraphQL endpoint URL used by {@link requestGraphQl}.
76
+ *
77
+ * Resolution order:
78
+ * 1. `process.env.NEXT_PUBLIC_API_URL` if non-empty.
79
+ * 2. `http://localhost:17603/graphql` when the browser is on
80
+ * `localhost` / `127.0.0.1`.
81
+ * 3. Same-origin `/graphql` for any other hostname (and for non-browser
82
+ * runtimes such as Node SSR).
83
+ */
84
+ export function readGraphQlEndpoint(): string {
85
+ const configured = readBPMClientConfig().baseUrl?.trim();
86
+ if (configured) {
87
+ return configured.endsWith('/graphql')
88
+ ? configured
89
+ : `${configured.replace(/\/$/, '')}/graphql`;
90
+ }
91
+ return (
92
+ readOptionalPublicEnvValue(process.env.NEXT_PUBLIC_API_URL) ??
93
+ resolveDefaultGraphQlEndpoint(readBrowserHostname())
94
+ );
95
+ }
96
+
97
+ /**
98
+ * Pure helper used by {@link readGraphQlEndpoint}.
99
+ *
100
+ * Exposed mainly for tests and for hosts that want to derive their own
101
+ * endpoint URLs without coupling to environment variables.
102
+ */
103
+ export function resolveDefaultGraphQlEndpoint(
104
+ hostname: string | null,
105
+ ): string {
106
+ return isLocalHost(hostname)
107
+ ? LOCAL_GRAPHQL_ENDPOINT
108
+ : SAME_ORIGIN_GRAPHQL_ENDPOINT;
109
+ }
110
+
111
+ function buildGraphQlHeaders(): Readonly<Record<string, string>> {
112
+ return {
113
+ ...readBPMConfiguredHeaders(),
114
+ 'Content-Type': 'application/json',
115
+ };
116
+ }
117
+
118
+ function readOptionalPublicEnvValue(value: string | undefined): string | null {
119
+ return value?.trim() || null;
120
+ }
121
+
122
+ function readBrowserHostname(): string | null {
123
+ return typeof window === 'undefined' ? null : window.location.hostname;
124
+ }
125
+
126
+ function isLocalHost(hostname: string | null): boolean {
127
+ return hostname === 'localhost' || hostname === '127.0.0.1';
128
+ }
@@ -0,0 +1,129 @@
1
+ import { requestGraphQl } from './graphql-client';
2
+
3
+ /**
4
+ * Member profile record exposed by the BPM identity GraphQL surface.
5
+ *
6
+ * `customFieldsJson` is a stringified JSON blob — BPM does not impose a
7
+ * schema on host-side member metadata. Consumers parse it according to
8
+ * their own host conventions.
9
+ */
10
+ export interface MemberProfileRecord {
11
+ readonly customFieldsJson: string;
12
+ readonly email: string;
13
+ readonly memberId: string;
14
+ readonly name: string;
15
+ }
16
+
17
+ /**
18
+ * A paginated slice of the host member directory, returned by
19
+ * {@link listMemberDirectoryPage}.
20
+ */
21
+ export interface MemberDirectoryPage {
22
+ readonly members: readonly MemberProfileRecord[];
23
+ readonly totalCount: number;
24
+ }
25
+
26
+ interface MembersQueryData {
27
+ readonly members: readonly MemberProfileRecord[];
28
+ }
29
+
30
+ interface SearchMembersQueryData {
31
+ readonly searchMembers: readonly MemberProfileRecord[];
32
+ }
33
+
34
+ interface MemberDirectoryPageQueryData extends SearchMembersQueryData {
35
+ readonly memberCount: number;
36
+ }
37
+
38
+ /**
39
+ * Resolves a set of `memberId`s into full member profile records by hitting
40
+ * BPM's `members(memberIds: [String!]!)` GraphQL query, which in turn calls
41
+ * the host-provided `BPMMemberResolver.resolveMany`.
42
+ *
43
+ * Returns an empty array when given an empty input — does not make a
44
+ * network call in that case.
45
+ */
46
+ export async function resolveMembers(
47
+ memberIds: readonly string[],
48
+ ): Promise<readonly MemberProfileRecord[]> {
49
+ if (!memberIds.length) {
50
+ return [];
51
+ }
52
+
53
+ const data = await requestGraphQl<MembersQueryData>(
54
+ `query AdminMembers($memberIds: [String!]!) {
55
+ members(memberIds: $memberIds) {
56
+ customFieldsJson
57
+ email
58
+ memberId
59
+ name
60
+ }
61
+ }`,
62
+ { memberIds },
63
+ );
64
+
65
+ return data.members;
66
+ }
67
+
68
+ /**
69
+ * Free-text search over the host's member directory through BPM's
70
+ * `searchMembers(searchText: String!)` query. The matching strategy is
71
+ * defined by the host's `BPMMemberResolver.search` implementation.
72
+ */
73
+ export async function searchMembers(
74
+ searchText: string,
75
+ ): Promise<readonly MemberProfileRecord[]> {
76
+ const data = await requestGraphQl<SearchMembersQueryData>(
77
+ `query AdminSearchMembers($searchText: String!) {
78
+ searchMembers(searchText: $searchText) {
79
+ customFieldsJson
80
+ email
81
+ memberId
82
+ name
83
+ }
84
+ }`,
85
+ { searchText },
86
+ );
87
+
88
+ return data.searchMembers;
89
+ }
90
+
91
+ /**
92
+ * Reads a single page of the member directory (search + total count in a
93
+ * single GraphQL roundtrip).
94
+ *
95
+ * @param page - 1-based page index.
96
+ * @param pageSize - Items per page.
97
+ * @param searchText - Optional free-text filter; empty string returns all.
98
+ */
99
+ export async function listMemberDirectoryPage({
100
+ page,
101
+ pageSize,
102
+ searchText = '',
103
+ }: {
104
+ readonly page: number;
105
+ readonly pageSize: number;
106
+ readonly searchText?: string;
107
+ }): Promise<MemberDirectoryPage> {
108
+ const data = await requestGraphQl<MemberDirectoryPageQueryData>(
109
+ `query AdminMemberDirectoryPage(
110
+ $page: Int
111
+ $pageSize: Int
112
+ $searchText: String!
113
+ ) {
114
+ searchMembers(page: $page, pageSize: $pageSize, searchText: $searchText) {
115
+ customFieldsJson
116
+ email
117
+ memberId
118
+ name
119
+ }
120
+ memberCount(searchText: $searchText)
121
+ }`,
122
+ { page, pageSize, searchText },
123
+ );
124
+
125
+ return {
126
+ members: data.searchMembers,
127
+ totalCount: data.memberCount,
128
+ };
129
+ }
@@ -0,0 +1,90 @@
1
+ import { createOrgUnit, deleteOrgUnit } from './organization-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/organization', () => {
40
+ describe('createOrgUnit', () => {
41
+ it('forwards the input under the GraphQL "input" variable', async (): Promise<void> => {
42
+ const input = {
43
+ code: 'ENG',
44
+ metadataJson: '{}',
45
+ name: 'Engineering',
46
+ parentId: null,
47
+ type: 'DEPARTMENT' as const,
48
+ };
49
+ const harness = installFetchMock({
50
+ createOrgUnit: {
51
+ code: 'ENG',
52
+ createdAt: '2026-05-26T00:00:00.000Z',
53
+ deletedAt: null,
54
+ id: 'unit-1',
55
+ name: 'Engineering',
56
+ parentId: null,
57
+ path: 'unit-1',
58
+ type: 'DEPARTMENT',
59
+ updatedAt: '2026-05-26T00:00:00.000Z',
60
+ },
61
+ });
62
+ try {
63
+ const result = await createOrgUnit(input);
64
+ const request = harness.capture();
65
+
66
+ expect(request.query).toContain('mutation AdminCreateOrgUnit');
67
+ expect(request.variables).toEqual({ input });
68
+ expect(result.id).toBe('unit-1');
69
+ } finally {
70
+ harness.restore();
71
+ }
72
+ });
73
+ });
74
+
75
+ describe('deleteOrgUnit', () => {
76
+ it('passes the id and returns the boolean payload', async (): Promise<void> => {
77
+ const harness = installFetchMock({ deleteOrgUnit: true });
78
+ try {
79
+ const result = await deleteOrgUnit('unit-1');
80
+ const request = harness.capture();
81
+
82
+ expect(request.query).toContain('deleteOrgUnit');
83
+ expect(request.variables).toEqual({ id: 'unit-1' });
84
+ expect(result).toBe(true);
85
+ } finally {
86
+ harness.restore();
87
+ }
88
+ });
89
+ });
90
+ });