Skip to content
This repository was archived by the owner on May 12, 2026. It is now read-only.

Commit dc19651

Browse files
committed
Dynamic Resource Docs
1 parent 62711f4 commit dc19651

14 files changed

Lines changed: 1374 additions & 7 deletions

File tree

src/lib/components/DynamicEndpointOperationsTable.svelte

Lines changed: 306 additions & 5 deletions
Large diffs are not rendered by default.

src/lib/components/DynamicResourceDocForm.svelte

Lines changed: 480 additions & 0 deletions
Large diffs are not rendered by default.

src/lib/config/navigation.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,28 @@ export function getActiveDynamicEndpointsMenuItem(pathname: string) {
491491
return found || dynamicEndpointsItems[0]; // fallback to first item
492492
}
493493

494+
// Dynamic Resource Docs navigation items
495+
function buildDynamicResourceDocsItems(): NavigationItem[] {
496+
return [
497+
{
498+
href: "/dynamic-resource-docs/system",
499+
label: "System",
500+
iconComponent: Plug,
501+
},
502+
];
503+
}
504+
505+
export const dynamicResourceDocsItems = buildDynamicResourceDocsItems();
506+
507+
export function getActiveDynamicResourceDocsMenuItem(pathname: string) {
508+
const found = dynamicResourceDocsItems.find((item) => {
509+
if (item.external) return false;
510+
return pathname.startsWith(item.href);
511+
});
512+
513+
return found || dynamicResourceDocsItems[0];
514+
}
515+
494516
// Products navigation items
495517
function buildProductsItems(): NavigationItem[] {
496518
const items: NavigationItem[] = [
@@ -721,6 +743,7 @@ export const navSections: NavigationSection[] = [
721743
{ id: "account-access", label: "Account Access", iconComponent: Landmark, items: accountAccessItems, basePaths: ["/account-access", "/mandates"] },
722744
{ id: "dynamic-entities", label: "Dynamic Entities", iconComponent: Box, items: dynamicEntitiesItems, basePaths: ["/dynamic-entities"] },
723745
{ id: "dynamic-endpoints", label: "Dynamic Endpoints", iconComponent: Plug, items: dynamicEndpointsItems, basePaths: ["/dynamic-endpoints"] },
746+
{ id: "dynamic-resource-docs", label: "Dynamic Resource Docs", iconComponent: FileText, items: dynamicResourceDocsItems, basePaths: ["/dynamic-resource-docs"] },
724747
{ id: "chat-rooms", label: "Chat Rooms", iconComponent: MessageSquare, items: chatRoomsItems, basePaths: ["/chat-rooms"] },
725748
{ id: "management-docs", label: "Management Docs", iconComponent: BookOpen, items: managementDocsItems, basePaths: ["/management-docs"] },
726749
];

src/lib/utils/roleChecker.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,10 @@ export const SITE_MAP: Record<string, PageRoleConfig> = {
328328
{ role: "CanCreateJsonSchemaValidation" },
329329
{ role: "CanUpdateJsonSchemaValidation" },
330330
{ role: "CanDeleteJsonSchemaValidation" },
331+
{ role: "CanGetAllEndpointMappings" },
332+
{ role: "CanCreateEndpointMapping" },
333+
{ role: "CanUpdateEndpointMapping" },
334+
{ role: "CanDeleteEndpointMapping" },
331335
],
332336
},
333337
"/dynamic-endpoints/system/create": {
@@ -358,6 +362,14 @@ export const SITE_MAP: Record<string, PageRoleConfig> = {
358362
{ role: "CanCreateJsonSchemaValidation" },
359363
{ role: "CanUpdateJsonSchemaValidation" },
360364
{ role: "CanDeleteJsonSchemaValidation" },
365+
{ role: "CanGetAllBankLevelEndpointMappings", bankScoped: true },
366+
{ role: "CanGetAllEndpointMappings" },
367+
{ role: "CanCreateBankLevelEndpointMapping", bankScoped: true },
368+
{ role: "CanCreateEndpointMapping" },
369+
{ role: "CanUpdateBankLevelEndpointMapping", bankScoped: true },
370+
{ role: "CanUpdateEndpointMapping" },
371+
{ role: "CanDeleteBankLevelEndpointMapping", bankScoped: true },
372+
{ role: "CanDeleteEndpointMapping" },
361373
],
362374
},
363375
"/dynamic-endpoints/bank/[bank_id]/create": {
@@ -366,6 +378,26 @@ export const SITE_MAP: Record<string, PageRoleConfig> = {
366378
{ role: "CanCreateDynamicEndpoint" },
367379
],
368380
},
381+
382+
// ── Dynamic Resource Docs ─────────────────────────────
383+
"/dynamic-resource-docs/system": {
384+
required: [{ role: "CanGetDynamicResourceDoc" }],
385+
optional: [
386+
{ role: "CanCreateDynamicResourceDoc" },
387+
{ role: "CanUpdateDynamicResourceDoc" },
388+
{ role: "CanDeleteDynamicResourceDoc" },
389+
],
390+
},
391+
"/dynamic-resource-docs/system/create": {
392+
required: [{ role: "CanCreateDynamicResourceDoc" }],
393+
},
394+
"/dynamic-resource-docs/system/[id]": {
395+
required: [{ role: "CanGetDynamicResourceDoc" }],
396+
optional: [
397+
{ role: "CanUpdateDynamicResourceDoc" },
398+
{ role: "CanDeleteDynamicResourceDoc" },
399+
],
400+
},
369401
};
370402

371403

src/routes/(protected)/dynamic-endpoints/bank/[bank_id]/[id]/+page.server.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,29 @@ export const load: PageServerLoad = async ({ params, locals }) => {
5252
logger.warn("Could not fetch JSON schema validations:", e);
5353
}
5454

55+
// Fetch bank-level Endpoint Mappings (per-operation_id) — non-fatal.
56+
let mappings: Array<{
57+
endpoint_mapping_id: string;
58+
operation_id: string;
59+
request_mapping: any;
60+
response_mapping: any;
61+
}> = [];
62+
try {
63+
const resp = await obp_requests.get(
64+
`/obp/v4.0.0/management/banks/${bank_id}/endpoint-mappings`,
65+
accessToken,
66+
);
67+
mappings = resp?.["endpoint-mappings"] || [];
68+
logger.debug(`Retrieved ${mappings.length} bank endpoint mappings`);
69+
} catch (e) {
70+
logger.warn("Could not fetch bank endpoint mappings:", e);
71+
}
72+
5573
return {
5674
endpoint: dynamicEndpoint,
5775
bank_id,
5876
validations,
77+
mappings,
5978
};
6079
} catch (err) {
6180
logger.error("Error fetching bank dynamic endpoint:", err);

src/routes/(protected)/dynamic-endpoints/bank/[bank_id]/[id]/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@
356356

357357
<!-- Operations Section -->
358358
<div class="mb-6">
359-
<DynamicEndpointOperationsTable {swagger} validations={data.validations || []} />
359+
<DynamicEndpointOperationsTable {swagger} validations={data.validations || []} mappings={data.mappings || []} bankId={bankId} />
360360
</div>
361361

362362
<!-- Raw Swagger View -->

src/routes/(protected)/dynamic-endpoints/system/[id]/+page.server.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,30 @@ export const load: PageServerLoad = async ({ params, locals }) => {
4848
logger.warn("Could not fetch JSON schema validations:", e);
4949
}
5050

51+
// Fetch all Endpoint Mappings (per-operation_id) — non-fatal. Only meaningful
52+
// when the endpoint's host is dynamic_entity; still fetched unconditionally so
53+
// the client can filter/display without an extra roundtrip.
54+
let mappings: Array<{
55+
endpoint_mapping_id: string;
56+
operation_id: string;
57+
request_mapping: any;
58+
response_mapping: any;
59+
}> = [];
60+
try {
61+
const resp = await obp_requests.get(
62+
`/obp/v4.0.0/management/endpoint-mappings`,
63+
accessToken,
64+
);
65+
mappings = resp?.["endpoint-mappings"] || [];
66+
logger.debug(`Retrieved ${mappings.length} endpoint mappings`);
67+
} catch (e) {
68+
logger.warn("Could not fetch endpoint mappings:", e);
69+
}
70+
5171
return {
5272
endpoint: dynamicEndpoint,
5373
validations,
74+
mappings,
5475
};
5576
} catch (err) {
5677
logger.error("Error fetching system dynamic endpoint:", err);

src/routes/(protected)/dynamic-endpoints/system/[id]/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@
347347

348348
<!-- Operations Section -->
349349
<div class="mb-6">
350-
<DynamicEndpointOperationsTable {swagger} validations={data.validations || []} />
350+
<DynamicEndpointOperationsTable {swagger} validations={data.validations || []} mappings={data.mappings || []} />
351351
</div>
352352

353353
<!-- Raw Swagger View -->
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { PageServerLoad } from "./$types";
2+
import { error } from "@sveltejs/kit";
3+
import { createLogger } from "$lib/utils/logger";
4+
import { SessionOAuthHelper } from "$lib/oauth/sessionHelper";
5+
import { obp_requests } from "$lib/obp/requests";
6+
7+
const logger = createLogger("SystemDynamicResourceDocsPageServer");
8+
9+
export const load: PageServerLoad = async ({ locals }) => {
10+
const session = locals.session;
11+
12+
if (!session?.data?.user) {
13+
throw error(401, "Unauthorized");
14+
}
15+
16+
const sessionOAuth = SessionOAuthHelper.getSessionOAuth(session);
17+
const accessToken = sessionOAuth?.accessToken;
18+
19+
if (!accessToken) {
20+
throw error(401, "No API access token available");
21+
}
22+
23+
try {
24+
const resp = await obp_requests.get(
25+
`/obp/v4.0.0/management/dynamic-resource-docs`,
26+
accessToken,
27+
);
28+
const docs = resp?.["dynamic-resource-docs"] || [];
29+
logger.info(`Retrieved ${docs.length} system dynamic resource docs`);
30+
return { docs };
31+
} catch (err: any) {
32+
logger.error("Error fetching system dynamic resource docs:", err);
33+
return {
34+
docs: [],
35+
error:
36+
err instanceof Error
37+
? err.message
38+
: "Failed to fetch system dynamic resource docs",
39+
};
40+
}
41+
};
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
<script lang="ts">
2+
import type { PageData } from "./$types";
3+
import {
4+
extractErrorFromResponse,
5+
formatErrorForDisplay,
6+
logErrorDetails,
7+
} from "$lib/utils/errorHandler";
8+
9+
let { data }: { data: PageData } = $props();
10+
11+
let searchQuery = $state("");
12+
let deleteError = $state<string | null>(null);
13+
let successMessage = $state<string | null>(null);
14+
15+
interface ResourceDoc {
16+
dynamic_resource_doc_id: string;
17+
partial_function_name: string;
18+
request_verb: string;
19+
request_url: string;
20+
summary: string;
21+
tags: string;
22+
roles: string;
23+
}
24+
25+
const filteredDocs = $derived(
26+
(data.docs || []).filter((doc: ResourceDoc) => {
27+
if (!searchQuery) return true;
28+
const q = searchQuery.toLowerCase();
29+
return (
30+
doc.partial_function_name?.toLowerCase().includes(q) ||
31+
doc.request_url?.toLowerCase().includes(q) ||
32+
doc.request_verb?.toLowerCase().includes(q) ||
33+
doc.summary?.toLowerCase().includes(q) ||
34+
doc.tags?.toLowerCase().includes(q) ||
35+
doc.roles?.toLowerCase().includes(q)
36+
);
37+
}),
38+
);
39+
40+
function methodBadgeClass(verb: string): string {
41+
switch (verb?.toUpperCase()) {
42+
case "GET": return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200";
43+
case "POST": return "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200";
44+
case "PUT": return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200";
45+
case "DELETE": return "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200";
46+
default: return "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200";
47+
}
48+
}
49+
50+
async function deleteDoc(id: string) {
51+
deleteError = null;
52+
successMessage = null;
53+
try {
54+
const response = await fetch(
55+
`/proxy/obp/v4.0.0/management/dynamic-resource-docs/${encodeURIComponent(id)}`,
56+
{ method: "DELETE", credentials: "include" },
57+
);
58+
if (!response.ok) {
59+
const errorDetails = await extractErrorFromResponse(
60+
response,
61+
"Failed to delete dynamic resource doc",
62+
);
63+
logErrorDetails("Delete Dynamic Resource Doc", errorDetails);
64+
deleteError = formatErrorForDisplay(errorDetails);
65+
return;
66+
}
67+
successMessage = "Dynamic resource doc deleted.";
68+
// Optimistically remove the deleted row locally so we avoid a full reload.
69+
data = { ...data, docs: (data.docs || []).filter((d: ResourceDoc) => d.dynamic_resource_doc_id !== id) };
70+
} catch (e) {
71+
deleteError = e instanceof Error ? e.message : "Failed to delete";
72+
}
73+
}
74+
</script>
75+
76+
<svelte:head>
77+
<title>System Dynamic Resource Docs - API Manager</title>
78+
</svelte:head>
79+
80+
<div class="container mx-auto px-4 py-8">
81+
<div class="mb-6 flex items-center justify-between">
82+
<div>
83+
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100">
84+
System Dynamic Resource Docs
85+
</h1>
86+
<p class="mt-1 text-gray-600 dark:text-gray-400">
87+
Endpoints implemented by Scala method bodies compiled at runtime. Served under
88+
<code class="rounded bg-gray-100 px-1 dark:bg-gray-900">/obp/dynamic-resource-doc/...</code>
89+
</p>
90+
</div>
91+
<a
92+
href="/dynamic-resource-docs/system/create"
93+
data-testid="create-link"
94+
class="inline-flex items-center rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600"
95+
>
96+
Create Resource Doc
97+
</a>
98+
</div>
99+
100+
{#if data.error}
101+
<div class="mb-6 rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-700 dark:border-red-800 dark:bg-red-900/20 dark:text-red-300">
102+
{data.error}
103+
</div>
104+
{/if}
105+
{#if deleteError}
106+
<div class="mb-6 rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-700 dark:border-red-800 dark:bg-red-900/20 dark:text-red-300" data-testid="delete-error">
107+
{deleteError}
108+
</div>
109+
{/if}
110+
{#if successMessage}
111+
<div class="mb-6 rounded-lg border border-green-200 bg-green-50 p-4 text-sm text-green-700 dark:border-green-800 dark:bg-green-900/20 dark:text-green-300" data-testid="success-message">
112+
{successMessage}
113+
</div>
114+
{/if}
115+
116+
<div class="mb-4">
117+
<input
118+
type="search"
119+
name="search"
120+
bind:value={searchQuery}
121+
placeholder="Filter by name, URL, verb, tag, role..."
122+
data-testid="search-input"
123+
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
124+
/>
125+
</div>
126+
127+
{#if filteredDocs.length === 0}
128+
<div class="rounded-lg border border-gray-200 bg-white p-8 text-center text-sm text-gray-600 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400">
129+
{#if (data.docs || []).length === 0}
130+
No dynamic resource docs yet. <a href="/dynamic-resource-docs/system/create" class="text-blue-600 hover:underline dark:text-blue-400">Create one</a> to get started.
131+
{:else}
132+
No docs match "{searchQuery}".
133+
{/if}
134+
</div>
135+
{:else}
136+
<div class="overflow-x-auto rounded-lg border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800">
137+
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
138+
<thead class="bg-gray-50 dark:bg-gray-900">
139+
<tr>
140+
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Method</th>
141+
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">URL</th>
142+
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Function Name</th>
143+
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Summary</th>
144+
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Roles</th>
145+
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Actions</th>
146+
</tr>
147+
</thead>
148+
<tbody class="divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-800">
149+
{#each filteredDocs as doc (doc.dynamic_resource_doc_id)}
150+
<tr data-testid="doc-row" data-doc-id={doc.dynamic_resource_doc_id}>
151+
<td class="whitespace-nowrap px-4 py-3 align-top">
152+
<span class="inline-flex w-16 items-center justify-center rounded px-2 py-0.5 text-xs font-medium {methodBadgeClass(doc.request_verb)}">
153+
{doc.request_verb}
154+
</span>
155+
</td>
156+
<td class="whitespace-nowrap px-4 py-3 align-top font-mono text-sm text-gray-900 dark:text-gray-100">
157+
{doc.request_url || ""}
158+
</td>
159+
<td class="whitespace-nowrap px-4 py-3 align-top text-sm text-gray-900 dark:text-gray-100">
160+
{doc.partial_function_name || ""}
161+
</td>
162+
<td class="px-4 py-3 align-top text-sm text-gray-700 dark:text-gray-300">
163+
{doc.summary || ""}
164+
</td>
165+
<td class="whitespace-nowrap px-4 py-3 align-top text-xs text-gray-600 dark:text-gray-400">
166+
{doc.roles || ""}
167+
</td>
168+
<td class="whitespace-nowrap px-4 py-3 align-top text-xs">
169+
<a
170+
href={`/dynamic-resource-docs/system/${doc.dynamic_resource_doc_id}`}
171+
data-testid="view-link"
172+
class="mr-3 text-blue-600 hover:underline dark:text-blue-400"
173+
>View / Edit</a>
174+
<button
175+
type="button"
176+
onclick={() => deleteDoc(doc.dynamic_resource_doc_id)}
177+
data-testid="delete-btn"
178+
class="text-red-600 hover:underline dark:text-red-400"
179+
>Delete</button>
180+
</td>
181+
</tr>
182+
{/each}
183+
</tbody>
184+
</table>
185+
</div>
186+
{/if}
187+
</div>

0 commit comments

Comments
 (0)