Skip to content

Resolve backend operations via the API documentation - #64

Open
mihailefter wants to merge 14 commits into
masterfrom
feature/34-api-docs-discovery
Open

Resolve backend operations via the API documentation#64
mihailefter wants to merge 14 commits into
masterfrom
feature/34-api-docs-discovery

Conversation

@mihailefter

Copy link
Copy Markdown
Collaborator

In line with #34, the client now stops hardcoding API paths. Instead, it tries to discover the FDP's API document via dcat:endpointDescription, provided in the root Turtle response by FDP 1.22+ (see FAIRDataTeam/FAIRDataPoint#952). If no usable document is declared there, it falls back to the /v3/api-docs guess. If the document is found, requests are resolved by operationId (bindOperation) instead of hardcoded paths.

As a result, affected UI elements (buttons, menu links, forms) are only shown when the connected FDP's API document offers the corresponding operation. I also added route guards for /login, /users, /users/create, /users/:id, /users/current, and /search, so direct navigation cannot bypass the hidden UI and hit a raw operation-resolution failure.

If no usable API document is found at all, the affected UI simply stays hidden, with no explicit message shown for that case yet.

Since the discovery flow depends on the root URI from runtime config, I also added a visible startup error instead of a blank page for when that config fails to load.

The following things are still open:

  • Capability-gated route guards currently redirect silently to / with no explanation. Worth discussing whether direct navigation to an unavailable route should tell the user why (e.g. "this FDP does not support user management") rather than just bouncing home. The same applies more broadly: if the API document itself can't be found, there's no visible indication of that either, just an app that quietly offers less.

  • The API document also carries requestBody/parameters schemas (required fields, formats, enums) that could eventually drive client-side form validation instead of the current hardcoded checks.

@mihailefter
mihailefter requested a review from dennisvang August 18, 2026 11:38

@dennisvang dennisvang left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @mihailefter this looks really good. 🙂

I do have a few questions/suggestions.

Details are in the comments, but, in summary:

  • Note that cached api-docs need to be refreshed if a ResourceDefinition is created or updated. This may be good to remember when implementing the corresponding admin functionality later.
  • Looks like fdpApi.ts has some repetition that could be replaced by a generic performOperation() function? See comment for detailed example.
  • The changes are more complex than I expected, due to the async handling of operations with operationBinding etc. Would it be possible to simplify by awaiting the api-docs at the very start, (like loadClientConfig) and then handling operations synchronously? I would think that there's not much to do anyway if api-docs fail to load. I do like the async implementation, but my main concern is complexity.

const subjectUri = resolveSubjectUri(store, rootUri)
const declaredUrls = subjectUri ? getNodeRefs(store, subjectUri, DCAT_ENDPOINT_DESCRIPTION) : []
const fallbackUrl = new URL('/v3/api-docs', rootUri).toString()
return [...new Set([...declaredUrls, fallbackUrl])]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although JavaScript Set preserves insertion order, in many other languages this is not guaranteed.
Maybe a comment to point this out?

Suggested change
return [...new Set([...declaredUrls, fallbackUrl])]
// Note that JavaScript Set preserves insertion order
return [...new Set([...declaredUrls, fallbackUrl])]

type OpenApiOperation = { operationId?: string }
type OpenApiDoc = { paths?: Record<string, Record<string, OpenApiOperation>> }

function isOpenApiDoc(doc: unknown): doc is OpenApiDoc {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function isOpenApiDoc(doc: unknown): doc is OpenApiDoc {
/** Duck-typing: If it looks like an OpenApiDoc, treat it as one. */
function isOpenApiDoc(doc: unknown): doc is OpenApiDoc {


let apiDocsPromise: Promise<unknown> | null = null

/** Fetches the FDP's OpenAPI doc once per session and reuses it for all subsequent lookups. */

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the API docs are (supposed to be) updated by the backend whenever a ResourceDefinition is added or changed.
That means the client should refresh the API docs after creating/editing ResourceDefinition objects.

const userId = computed(() => (route.params.id as string | undefined) ?? 'current')

/**
* Self-service profile routes use current-user operations. Admin routes use uuid-based user

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not immediately clear to me what "self-service" and self refer to.
Does it refer to current user?

Comment thread src/composables/fdpApi.ts

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like there's quite a bit of repetition in this file.

Perhaps you could define a generic method performOperation(<operation-id>, <data>) that performs the actual request, based on operation details, and call that method from the relevant locations.

Using searchResources() as an example, the following code from SearchView.vue

    const { url, method } = await searchBinding
    results.value = (await searchResources(q, url, method)) as SearchResult[]

could then be replaced by something like (please excuse the sloppy pseudo-code):

    operationResult = (await performOperation(
        <search-operation-id>, 
        <object-containing-query-string-and-other-relevant-data>
    )) as OperationResult
    results.value = ... // extract SearchResult[] from operationResult

A similar approach applies to all the other functions.

To illustrate the idea, here's an example from one of my Python-based FDP clients (synchronous instead of async):

class APIClient(object):

    ...

    def release_schema_version(
        self, uuid: str, version: str, description: str = "", public: bool = False
    ) -> OperationResult:
        """Creates a metadata-schema-version by releasing the metadata-schema-draft"""
        # minimal post body
        metadata_schema_version = {
            "description": description,
            "published": public,
            "version": version,
        }

        # perform operation
        return self.perform_operation(
            key="releaseSchemaVersion", uuid=uuid, json=metadata_schema_version
        )

    ...

    def perform_operation(self, key: str, **kwargs) -> OperationResult:
        """
        Performs an operation defined in the OpenAPI docs.

        Path parameters must be speficied as kwargs, e.g. uuid=<string>. Additional
        kwargs, if any, are passed on to the requests method call, e.g. json=<dict>.
        """
        # get operation info
        try:
            operation = self.api_operations[key]
            logger.info("performing operation: %s", key)
        except KeyError as e:
            logger.error("unknown operation: %s", key)
            self.list_operations()
            raise e

        # remove path parameters from kwargs and format uri
        path_parameters = {
            parameter_name: kwargs.pop(parameter_name, None)
            for parameter_name in self._get_api_parameters(
                operation=operation, param_type="path"
            )
        }
        url = self.url + operation["uri"].format(**path_parameters)

        # perform request
        response = getattr(self.session, operation["method"])(url=url, **kwargs)

        # handle response
        if response.ok:
            # handle content type
            content_type = response.headers.get("content-type")
            if "json" in content_type:
                content = response.json()
            elif content_type.startswith("text"):
                # same as response.content.decode("utf-8")
                content = response.text
            else:
                logger.warning("unknown content-type: %s", content_type)
                content = response.content
            logger.info("operation successful: %s", key)
            logger.debug("result: %s", content)
            return OperationResult(
                location=response.headers.get("location"),
                content_type=content_type,
                content=content,
            )
        self._log_api_operation_requirements(operation=operation)
        raise Exception(
            f"operation failed: {key}\n\t"
            f"request: {response.request.method} {response.request.url} "
            f"{response.request.body}\n\t"
            f"response: {response.content or '-'}"
        )

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread src/router/index.ts

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread src/main.ts

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great improvement. :)

Comment thread tests/config.test.ts

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice! :)

@dennisvang

dennisvang commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

From the PR description above:

The API document also carries requestBody/parameters schemas (required fields, formats, enums) that could eventually drive client-side form validation instead of the current hardcoded checks.

@mihailefter i think the current PR is a great start. This advanced stuff can be done at a later stage. Perhaps good to keep this in mind when implementing the admin functionality for "resource definitions" and "metadata schemas?"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants