@isi-ui7/bos7-shared 0.3.0 → 0.3.2
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/package.json +78 -28
- package/src/crud-client-error.test.ts +53 -0
- package/src/workflow/crud-client.ts +33 -2
- package/src/workflow/starter.ts +37 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@isi-ui7/bos7-shared",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Shared auth7 and layout primitives for bos7 applications.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
"directory": "packages/bos7-shared"
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|
|
12
|
-
"main": "
|
|
13
|
-
"module": "
|
|
14
|
-
"types": "
|
|
12
|
+
"main": "src/index.ts",
|
|
13
|
+
"module": "src/index.ts",
|
|
14
|
+
"types": "src/index.ts",
|
|
15
15
|
"exports": {
|
|
16
|
-
".": "./
|
|
16
|
+
".": "./src/index.ts",
|
|
17
17
|
"./auth7": "./src/auth7/index.ts",
|
|
18
18
|
"./page-layout": "./src/page-layout.tsx",
|
|
19
19
|
"./modal-layout": "./src/modal-layout.tsx",
|
|
@@ -50,23 +50,79 @@
|
|
|
50
50
|
"./bff/health": "./src/bff/health.ts",
|
|
51
51
|
"./observability/otel-init": "./src/observability/otel-init.ts",
|
|
52
52
|
"./form-types": "./src/form-types.ts",
|
|
53
|
+
"./form-value-schema": "./src/form-value-schema.ts",
|
|
53
54
|
"./form-numeric": "./src/form-numeric.ts",
|
|
54
55
|
"./form-renderer": "./src/form-renderer.tsx",
|
|
55
56
|
"./form-renderer-utils": "./src/form-renderer-utils.ts",
|
|
56
57
|
"./form-rules": "./src/form-rules.ts",
|
|
57
58
|
"./i18n": "./src/i18n.tsx",
|
|
58
59
|
"./workflow/use-crud-form": "./src/workflow/use-crud-form.ts",
|
|
59
|
-
"./styles": "./src/form-contract.css"
|
|
60
|
-
"./form-value-schema": "./src/form-value-schema.ts"
|
|
60
|
+
"./styles": "./src/form-contract.css"
|
|
61
61
|
},
|
|
62
62
|
"publishConfig": {
|
|
63
63
|
"registry": "https://registry.npmjs.org",
|
|
64
|
-
"access": "public"
|
|
64
|
+
"access": "public",
|
|
65
|
+
"main": "dist/index.js",
|
|
66
|
+
"module": "dist/index.js",
|
|
67
|
+
"types": "dist/index.d.ts",
|
|
68
|
+
"exports": {
|
|
69
|
+
".": "./dist/index.js",
|
|
70
|
+
"./auth7": "./src/auth7/index.ts",
|
|
71
|
+
"./page-layout": "./src/page-layout.tsx",
|
|
72
|
+
"./modal-layout": "./src/modal-layout.tsx",
|
|
73
|
+
"./form-layout": "./src/form-layout.tsx",
|
|
74
|
+
"./backend": "./src/backend.ts",
|
|
75
|
+
"./export": "./src/export.ts",
|
|
76
|
+
"./types/notification": "./src/types/notification.ts",
|
|
77
|
+
"./nats/client": "./src/nats/client.ts",
|
|
78
|
+
"./event-bus": "./src/event-bus/index.ts",
|
|
79
|
+
"./event-bus/useEventBus": "./src/event-bus/useEventBus.ts",
|
|
80
|
+
"./api": "./src/api.ts",
|
|
81
|
+
"./types/core": "./src/types/core.ts",
|
|
82
|
+
"./style-contract": "./src/style-contract.ts",
|
|
83
|
+
"./crud-components": "./src/crud-components.tsx",
|
|
84
|
+
"./workflow/backend": "./src/workflow/backend.ts",
|
|
85
|
+
"./workflow/notif7": "./src/workflow/notif7.ts",
|
|
86
|
+
"./workflow/api-client": "./src/workflow/api-client.ts",
|
|
87
|
+
"./workflow/starter": "./src/workflow/starter.ts",
|
|
88
|
+
"./workflow/use-workflow-tracker": "./src/workflow/use-workflow-tracker.ts",
|
|
89
|
+
"./workflow/crud-client": "./src/workflow/crud-client.ts",
|
|
90
|
+
"./data-table/cursor-adapter": "./src/data-table/cursor-adapter.ts",
|
|
91
|
+
"./data-table/proxy": "./src/data-table/proxy.ts",
|
|
92
|
+
"./notification/routing": "./src/notification/routing.ts",
|
|
93
|
+
"./shell/interaction": "./src/shell/interaction.ts",
|
|
94
|
+
"./shell/branch": "./src/shell/branch.ts",
|
|
95
|
+
"./notif7": "./src/notif7.ts",
|
|
96
|
+
"./shell/provider": "./src/shell/provider.tsx",
|
|
97
|
+
"./shell/app-shell-layout": "./src/shell/app-shell-layout.tsx",
|
|
98
|
+
"./notifications-bff": "./src/notifications-bff.ts",
|
|
99
|
+
"./bff/auth-me": "./src/bff/auth-me.ts",
|
|
100
|
+
"./bff/auth-refresh": "./src/bff/auth-refresh.ts",
|
|
101
|
+
"./bff/auth-logout": "./src/bff/auth-logout.ts",
|
|
102
|
+
"./bff/auth-preferences": "./src/bff/auth-preferences.ts",
|
|
103
|
+
"./bff/health": "./src/bff/health.ts",
|
|
104
|
+
"./observability/otel-init": "./src/observability/otel-init.ts",
|
|
105
|
+
"./form-types": "./src/form-types.ts",
|
|
106
|
+
"./form-numeric": "./src/form-numeric.ts",
|
|
107
|
+
"./form-renderer": "./src/form-renderer.tsx",
|
|
108
|
+
"./form-renderer-utils": "./src/form-renderer-utils.ts",
|
|
109
|
+
"./form-rules": "./src/form-rules.ts",
|
|
110
|
+
"./i18n": "./src/i18n.tsx",
|
|
111
|
+
"./workflow/use-crud-form": "./src/workflow/use-crud-form.ts",
|
|
112
|
+
"./styles": "./src/form-contract.css",
|
|
113
|
+
"./form-value-schema": "./src/form-value-schema.ts"
|
|
114
|
+
}
|
|
65
115
|
},
|
|
66
116
|
"files": [
|
|
67
117
|
"dist",
|
|
68
118
|
"src"
|
|
69
119
|
],
|
|
120
|
+
"scripts": {
|
|
121
|
+
"build": "vite build",
|
|
122
|
+
"lint": "eslint \"src/**/*.{ts,tsx}\"",
|
|
123
|
+
"test": "vitest run --passWithNoTests",
|
|
124
|
+
"typecheck": "tsc --noEmit"
|
|
125
|
+
},
|
|
70
126
|
"peerDependencies": {
|
|
71
127
|
"@carbon/icons-react": "^11.71.0",
|
|
72
128
|
"@carbon/react": "^1.97.0",
|
|
@@ -87,6 +143,14 @@
|
|
|
87
143
|
"@carbon/react": "^1.97.0",
|
|
88
144
|
"@eslint/eslintrc": "^3.3.1",
|
|
89
145
|
"@eslint/js": "^9.39.0",
|
|
146
|
+
"@isi-ui7/corporate-themes": "workspace:*",
|
|
147
|
+
"@isi-ui7/data-table": "workspace:*",
|
|
148
|
+
"@isi-ui7/editable-table": "workspace:*",
|
|
149
|
+
"@isi-ui7/i18n": "workspace:*",
|
|
150
|
+
"@isi-ui7/lookup-input": "workspace:*",
|
|
151
|
+
"@isi-ui7/modal-manager": "workspace:*",
|
|
152
|
+
"@isi-ui7/realtime": "workspace:*",
|
|
153
|
+
"@isi-ui7/ui-shell": "workspace:*",
|
|
90
154
|
"@testing-library/dom": "^10.0.0",
|
|
91
155
|
"@testing-library/react": "^16.0.0",
|
|
92
156
|
"@testing-library/user-event": "^14.5.0",
|
|
@@ -105,19 +169,14 @@
|
|
|
105
169
|
"typescript": "^5.3.3",
|
|
106
170
|
"vite": "^5.0.0",
|
|
107
171
|
"vite-plugin-dts": "^3.6.0",
|
|
108
|
-
"vitest": "^1.0.0"
|
|
109
|
-
"@isi-ui7/editable-table": "0.2.3",
|
|
110
|
-
"@isi-ui7/lookup-input": "0.2.4",
|
|
111
|
-
"@isi-ui7/i18n": "0.2.3",
|
|
112
|
-
"@isi-ui7/data-table": "0.2.4",
|
|
113
|
-
"@isi-ui7/corporate-themes": "0.2.6",
|
|
114
|
-
"@isi-ui7/realtime": "0.2.3",
|
|
115
|
-
"@isi-ui7/modal-manager": "0.2.2",
|
|
116
|
-
"@isi-ui7/ui-shell": "0.2.8"
|
|
172
|
+
"vitest": "^1.0.0"
|
|
117
173
|
},
|
|
118
174
|
"dependencies": {
|
|
119
175
|
"@carbon/icons-react": "^11.71.0",
|
|
120
176
|
"@carbon/react": "^1.97.0",
|
|
177
|
+
"@isi-ui7/data-table": "workspace:*",
|
|
178
|
+
"@isi-ui7/editable-table": "workspace:*",
|
|
179
|
+
"@isi-ui7/lookup-input": "workspace:*",
|
|
121
180
|
"@opentelemetry/auto-instrumentations-node": "^0.57.0",
|
|
122
181
|
"@opentelemetry/exporter-trace-otlp-http": "^0.57.0",
|
|
123
182
|
"@opentelemetry/sdk-node": "^0.57.0",
|
|
@@ -125,15 +184,6 @@
|
|
|
125
184
|
"exceljs": "^4.4.0",
|
|
126
185
|
"jose": "^5.9.6",
|
|
127
186
|
"jspdf": "^4.2.1",
|
|
128
|
-
"jspdf-autotable": "^5.0.7"
|
|
129
|
-
"@isi-ui7/data-table": "0.2.4",
|
|
130
|
-
"@isi-ui7/editable-table": "0.2.3",
|
|
131
|
-
"@isi-ui7/lookup-input": "0.2.4"
|
|
132
|
-
},
|
|
133
|
-
"scripts": {
|
|
134
|
-
"build": "vite build",
|
|
135
|
-
"lint": "eslint \"src/**/*.{ts,tsx}\"",
|
|
136
|
-
"test": "vitest run --passWithNoTests",
|
|
137
|
-
"typecheck": "tsc --noEmit"
|
|
187
|
+
"jspdf-autotable": "^5.0.7"
|
|
138
188
|
}
|
|
139
|
-
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
+
import { fetchDetailData } from './workflow/crud-client';
|
|
3
|
+
|
|
4
|
+
// Bentuk error yang BENAR-BENAR dikirim service Go di estate ini. Menyerahkan
|
|
5
|
+
// objek langsung ke `new Error()` menghasilkan pesan literal "[object Object]",
|
|
6
|
+
// dan penyebab sebenarnya tertelan — operator hanya melihat "[object Object]"
|
|
7
|
+
// sementara yang terjadi 403 "access denied".
|
|
8
|
+
function mockRes(status: number, body: unknown) {
|
|
9
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
10
|
+
ok: status >= 200 && status < 300,
|
|
11
|
+
status,
|
|
12
|
+
json: () => Promise.resolve(body),
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
afterEach(() => vi.restoreAllMocks());
|
|
17
|
+
|
|
18
|
+
describe('fetchDetailData — pesan error', () => {
|
|
19
|
+
it('membuka error berbentuk OBJEK {code, message} — bukan "[object Object]"', async () => {
|
|
20
|
+
mockRes(403, { error: { code: 'FORBIDDEN', message: 'access denied' } });
|
|
21
|
+
await expect(fetchDetailData('/x')).rejects.toThrow('access denied (FORBIDDEN)');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('tidak pernah memunculkan "[object Object]" apa pun bentuk error-nya', async () => {
|
|
25
|
+
for (const body of [
|
|
26
|
+
{ error: { code: 'FORBIDDEN', message: 'access denied' } },
|
|
27
|
+
{ error: { message: 'boom' } },
|
|
28
|
+
{ error: { code: 'ONLY_CODE' } },
|
|
29
|
+
{ error: {} },
|
|
30
|
+
{ message: { code: 'X', message: 'y' } },
|
|
31
|
+
]) {
|
|
32
|
+
mockRes(500, body);
|
|
33
|
+
await expect(fetchDetailData('/x')).rejects.toThrow(
|
|
34
|
+
expect.not.stringContaining('[object Object]') as unknown as string,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('tetap menerima error berbentuk string', async () => {
|
|
40
|
+
mockRes(401, { error: 'unauthorized' });
|
|
41
|
+
await expect(fetchDetailData('/x')).rejects.toThrow('unauthorized');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('jatuh ke pesan HTTP ketika body tidak memuat error yang bisa dibaca', async () => {
|
|
45
|
+
mockRes(502, {});
|
|
46
|
+
await expect(fetchDetailData('/x')).rejects.toThrow('Gagal memuat data (HTTP 502)');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('sukses tetap membuka envelope {data}', async () => {
|
|
50
|
+
mockRes(200, { data: { id: 7, name: 'ok' } });
|
|
51
|
+
await expect(fetchDetailData<{ id: number }>('/x')).resolves.toEqual({ id: 7, name: 'ok' });
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -1,9 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ambil pesan error dari body respons.
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ Service Go di estate ini mengembalikan `error` sebagai OBJEK
|
|
5
|
+
* (`{"error": {"code": "FORBIDDEN", "message": "access denied"}}`), bukan
|
|
6
|
+
* string. Versi sebelumnya menyerahkan objek itu langsung ke `new Error()`,
|
|
7
|
+
* yang menghasilkan pesan literal "[object Object]" — jadi penyebab sebenarnya
|
|
8
|
+
* (403, pesan "access denied") TERTELAN, dan yang dilihat operator hanya
|
|
9
|
+
* "[object Object]". Kena 2026-08-14 pada detail nasabah cif: penyebab aslinya
|
|
10
|
+
* token BFF yang salah bentuk, tapi tak seorang pun bisa melihatnya dari UI.
|
|
11
|
+
*
|
|
12
|
+
* Menerima ketiga bentuk: string, `{message}`, dan `{code, message}`.
|
|
13
|
+
*/
|
|
14
|
+
function errorMessage(err: unknown, fallback: string): string {
|
|
15
|
+
if (typeof err === 'string' && err.trim() !== '') return err;
|
|
16
|
+
if (err && typeof err === 'object') {
|
|
17
|
+
const o = err as { message?: unknown; code?: unknown };
|
|
18
|
+
if (typeof o.message === 'string' && o.message.trim() !== '') {
|
|
19
|
+
return typeof o.code === 'string' ? `${o.message} (${o.code})` : o.message;
|
|
20
|
+
}
|
|
21
|
+
if (typeof o.code === 'string' && o.code.trim() !== '') return o.code;
|
|
22
|
+
}
|
|
23
|
+
return fallback;
|
|
24
|
+
}
|
|
25
|
+
|
|
1
26
|
export async function fetchDetailData<T>(url: string): Promise<T> {
|
|
2
27
|
const res = await fetch(url);
|
|
3
|
-
const body = (await res.json().catch(() => ({}))) as {
|
|
28
|
+
const body = (await res.json().catch(() => ({}))) as {
|
|
29
|
+
data?: T;
|
|
30
|
+
error?: unknown;
|
|
31
|
+
message?: unknown;
|
|
32
|
+
};
|
|
4
33
|
|
|
5
34
|
if (!res.ok) {
|
|
6
|
-
throw new Error(
|
|
35
|
+
throw new Error(
|
|
36
|
+
errorMessage(body.error ?? body.message, `Gagal memuat data (HTTP ${res.status})`),
|
|
37
|
+
);
|
|
7
38
|
}
|
|
8
39
|
if (!body.data) {
|
|
9
40
|
throw new Error('Gagal memuat data');
|
package/src/workflow/starter.ts
CHANGED
|
@@ -9,10 +9,43 @@
|
|
|
9
9
|
// workflow7 EventRecorder dispatch SSE event `instance.completed` balik ke
|
|
10
10
|
// initiator. Kalau initiatedBy kosong → no dispatch → notifikasi silent.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
// 🔑 WORKFLOW7_URL only. NEXT_PUBLIC_BOS7_WORKFLOW_URL is deliberately NOT
|
|
13
|
+
// consulted here, and that is the fix — it used to be consulted FIRST.
|
|
14
|
+
//
|
|
15
|
+
// That variable names the workflow WEBAPP, in both environments:
|
|
16
|
+
//
|
|
17
|
+
// local workflow.bos7.local → workflow7-web :3002 (webapp)
|
|
18
|
+
// api.workflow.bos7.local → workflow7-svc :8081 (service)
|
|
19
|
+
// Railway workflow.ibos7.com → bos7-workflow (webapp)
|
|
20
|
+
// core7-service … no public domain — internal only
|
|
21
|
+
//
|
|
22
|
+
// This helper POSTs to /wf/instances/start on the SERVICE. Pointing it at the
|
|
23
|
+
// webapp produced a redirect chain that ends in a misleading 405, reproduced
|
|
24
|
+
// end to end on Railway:
|
|
25
|
+
//
|
|
26
|
+
// POST https://workflow.ibos7.com/wf/instances/start
|
|
27
|
+
// → 307 Location: /api/auth/login?returnTo=/wf/instances/start
|
|
28
|
+
// → fetch preserves POST across a 307
|
|
29
|
+
// → /api/auth/login accepts GET only
|
|
30
|
+
// → 405 Method Not Allowed
|
|
31
|
+
//
|
|
32
|
+
// Control, same path against the service: 401 — the route exists and only
|
|
33
|
+
// wants auth.
|
|
34
|
+
//
|
|
35
|
+
// ⚠️ The old order only broke where NEXT_PUBLIC_BOS7_WORKFLOW_URL is SET.
|
|
36
|
+
// Locally it is usually unset, so the chain fell through to the service and
|
|
37
|
+
// every flow worked — which is why this survived local testing across four
|
|
38
|
+
// webapps. On Railway it is set on all of them, so it won.
|
|
39
|
+
//
|
|
40
|
+
// The NEXT_PUBLIC fallback is removed rather than demoted: for this endpoint
|
|
41
|
+
// it can only ever produce that 405, and a fallback whose sole outcome is a
|
|
42
|
+
// misleading error is worse than none. With it gone, an unconfigured
|
|
43
|
+
// container fails to connect — which names the real problem.
|
|
44
|
+
//
|
|
45
|
+
// NEXT_PUBLIC_BOS7_WORKFLOW_URL stays correct in notifications-bff.ts, which
|
|
46
|
+
// builds user-facing LINKS to the workflow webapp. Same variable, different
|
|
47
|
+
// job; only this file was using it for the wrong one.
|
|
48
|
+
const WORKFLOW7_URL = process.env.WORKFLOW7_URL ?? 'http://localhost:8081';
|
|
16
49
|
|
|
17
50
|
type CookieReqLike = Request & {
|
|
18
51
|
cookies?: { get?: (name: string) => { value?: string } | undefined };
|