@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isi-ui7/bos7-shared",
3
- "version": "0.3.0",
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": "dist/index.js",
13
- "module": "dist/index.js",
14
- "types": "dist/index.d.ts",
12
+ "main": "src/index.ts",
13
+ "module": "src/index.ts",
14
+ "types": "src/index.ts",
15
15
  "exports": {
16
- ".": "./dist/index.js",
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 { data?: T; error?: string };
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(body.error ?? `Gagal memuat data (HTTP ${res.status})`);
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');
@@ -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
- const WORKFLOW7_URL =
13
- process.env.NEXT_PUBLIC_BOS7_WORKFLOW_URL ??
14
- process.env.WORKFLOW7_URL ??
15
- 'http://localhost:8081';
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 };