@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.
Files changed (62) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +113 -3
  3. package/eslint.config.mjs +19 -0
  4. package/jest.config.cts +10 -0
  5. package/package.json +4 -5
  6. package/project.json +66 -0
  7. package/src/lib/api-auth-client.ts +169 -0
  8. package/src/lib/form/form-api.spec.ts +81 -0
  9. package/src/lib/form/form-api.ts +388 -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 +114 -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 +710 -0
  16. package/src/lib/template/template-api.spec.ts +88 -0
  17. package/src/lib/template/template-api.ts +772 -0
  18. package/src/lib/workflow/workflow-api.spec.ts +132 -0
  19. package/src/lib/workflow/workflow-api.ts +1749 -0
  20. package/tsconfig.json +27 -0
  21. package/tsconfig.lib.json +16 -0
  22. package/tsconfig.spec.json +16 -0
  23. package/LICENSE +0 -21
  24. package/src/index.js +0 -7
  25. package/src/index.js.map +0 -1
  26. package/src/lib/api-auth-client.d.ts +0 -73
  27. package/src/lib/api-auth-client.js +0 -121
  28. package/src/lib/api-auth-client.js.map +0 -1
  29. package/src/lib/form/form-api.d.ts +0 -48
  30. package/src/lib/form/form-api.js +0 -228
  31. package/src/lib/form/form-api.js.map +0 -1
  32. package/src/lib/form/form-rendering.d.ts +0 -52
  33. package/src/lib/form/form-rendering.js +0 -341
  34. package/src/lib/form/form-rendering.js.map +0 -1
  35. package/src/lib/form/index.js +0 -6
  36. package/src/lib/form/index.js.map +0 -1
  37. package/src/lib/graphql-client.d.ts +0 -45
  38. package/src/lib/graphql-client.js +0 -92
  39. package/src/lib/graphql-client.js.map +0 -1
  40. package/src/lib/member-api.d.ts +0 -49
  41. package/src/lib/member-api.js +0 -72
  42. package/src/lib/member-api.js.map +0 -1
  43. package/src/lib/organization/index.js +0 -5
  44. package/src/lib/organization/index.js.map +0 -1
  45. package/src/lib/organization/organization-api.d.ts +0 -168
  46. package/src/lib/organization/organization-api.js +0 -394
  47. package/src/lib/organization/organization-api.js.map +0 -1
  48. package/src/lib/template/index.js +0 -5
  49. package/src/lib/template/index.js.map +0 -1
  50. package/src/lib/template/template-api.d.ts +0 -150
  51. package/src/lib/template/template-api.js +0 -407
  52. package/src/lib/template/template-api.js.map +0 -1
  53. package/src/lib/workflow/index.js +0 -5
  54. package/src/lib/workflow/index.js.map +0 -1
  55. package/src/lib/workflow/workflow-api.d.ts +0 -376
  56. package/src/lib/workflow/workflow-api.js +0 -867
  57. package/src/lib/workflow/workflow-api.js.map +0 -1
  58. /package/src/{index.d.ts → index.ts} +0 -0
  59. /package/src/lib/form/{index.d.ts → index.ts} +0 -0
  60. /package/src/lib/organization/{index.d.ts → index.ts} +0 -0
  61. /package/src/lib/template/{index.d.ts → index.ts} +0 -0
  62. /package/src/lib/workflow/{index.d.ts → index.ts} +0 -0
@@ -0,0 +1,114 @@
1
+ const LOCAL_GRAPHQL_ENDPOINT = 'http://localhost:17603/graphql';
2
+ const SAME_ORIGIN_GRAPHQL_ENDPOINT = '/graphql';
3
+
4
+ interface GraphQlError {
5
+ readonly message: string;
6
+ }
7
+
8
+ interface GraphQlResponse<TData> {
9
+ readonly data?: TData;
10
+ readonly errors?: readonly GraphQlError[];
11
+ }
12
+
13
+ /**
14
+ * Sends a single GraphQL operation to the BPM host and returns the typed data.
15
+ *
16
+ * Uses the standard Fetch API with `credentials: 'include'`, so any
17
+ * HTTP-only session cookie issued by the host's `POST /auth/login` endpoint
18
+ * flows back automatically on subsequent calls.
19
+ *
20
+ * The endpoint URL is resolved by {@link readGraphQlEndpoint} — set
21
+ * `NEXT_PUBLIC_API_URL` to override the default localhost / same-origin
22
+ * resolution.
23
+ *
24
+ * Throws an `Error` when the HTTP response is non-2xx, when the GraphQL
25
+ * payload carries an `errors` array, or when the response has no `data`
26
+ * field.
27
+ *
28
+ * @typeParam TData - Shape of the `data` field for the operation.
29
+ * @param query - GraphQL operation document (query or mutation source).
30
+ * @param variables - Variables passed to the GraphQL operation.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * interface PingData { readonly memberCount: number; }
35
+ * const data = await requestGraphQl<PingData>('query Ping { memberCount }');
36
+ * console.log(data.memberCount);
37
+ * ```
38
+ */
39
+ export async function requestGraphQl<TData>(
40
+ query: string,
41
+ variables?: Readonly<Record<string, unknown>>,
42
+ ): Promise<TData> {
43
+ const response = await fetch(readGraphQlEndpoint(), {
44
+ body: JSON.stringify({ query, variables }),
45
+ credentials: 'include',
46
+ headers: buildGraphQlHeaders(),
47
+ method: 'POST',
48
+ });
49
+
50
+ if (!response.ok) {
51
+ throw new Error(`GraphQL request failed with HTTP ${response.status}`);
52
+ }
53
+
54
+ const payload = (await response.json()) as GraphQlResponse<TData>;
55
+
56
+ if (payload.errors?.length) {
57
+ throw new Error(payload.errors.map((error) => error.message).join('; '));
58
+ }
59
+
60
+ if (!payload.data) {
61
+ throw new Error('GraphQL response did not include data');
62
+ }
63
+
64
+ return payload.data;
65
+ }
66
+
67
+ /**
68
+ * Resolves the GraphQL endpoint URL used by {@link requestGraphQl}.
69
+ *
70
+ * Resolution order:
71
+ * 1. `process.env.NEXT_PUBLIC_API_URL` if non-empty.
72
+ * 2. `http://localhost:17603/graphql` when the browser is on
73
+ * `localhost` / `127.0.0.1`.
74
+ * 3. Same-origin `/graphql` for any other hostname (and for non-browser
75
+ * runtimes such as Node SSR).
76
+ */
77
+ export function readGraphQlEndpoint(): string {
78
+ return (
79
+ readOptionalPublicEnvValue(process.env.NEXT_PUBLIC_API_URL) ??
80
+ resolveDefaultGraphQlEndpoint(readBrowserHostname())
81
+ );
82
+ }
83
+
84
+ /**
85
+ * Pure helper used by {@link readGraphQlEndpoint}.
86
+ *
87
+ * Exposed mainly for tests and for hosts that want to derive their own
88
+ * endpoint URLs without coupling to environment variables.
89
+ */
90
+ export function resolveDefaultGraphQlEndpoint(
91
+ hostname: string | null,
92
+ ): string {
93
+ return isLocalHost(hostname)
94
+ ? LOCAL_GRAPHQL_ENDPOINT
95
+ : SAME_ORIGIN_GRAPHQL_ENDPOINT;
96
+ }
97
+
98
+ function buildGraphQlHeaders(): Readonly<Record<string, string>> {
99
+ return {
100
+ 'Content-Type': 'application/json',
101
+ };
102
+ }
103
+
104
+ function readOptionalPublicEnvValue(value: string | undefined): string | null {
105
+ return value?.trim() || null;
106
+ }
107
+
108
+ function readBrowserHostname(): string | null {
109
+ return typeof window === 'undefined' ? null : window.location.hostname;
110
+ }
111
+
112
+ function isLocalHost(hostname: string | null): boolean {
113
+ return hostname === 'localhost' || hostname === '127.0.0.1';
114
+ }
@@ -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
+ });