Skip to content

Repository files navigation

oslc-client

npm Discourse status Gitter

A JavaScript library for consuming OSLC 3.0 servers. Uses axios for HTTP, rdflib for RDF parsing, and supports Basic, JEE Form, and JAS Bearer token authentication. Works in both Node.js and browser environments.

Install

npm install oslc-client

Or, from a workspace monorepo, add a file dependency in your package.json:

{
  "dependencies": {
    "oslc-client": "file:../oslc-client"
  }
}

Quick Start

import OSLCClient from 'oslc-client';

const client = new OSLCClient('user', 'password');

// Connect to a server and select a service provider
await client.use('https://example.com/ccm', 'My Project', 'CM');

// Fetch a resource by URL
const resource = await client.getResource('https://example.com/ccm/resource/1');

console.log(resource.getTitle());
console.log(resource.getIdentifier());
console.log(resource.getDescription());

// Read any RDF property
const modified = resource.get('http://purl.org/dc/terms/modified');

// Update and save
resource.setTitle('Updated title');
await client.putResource(resource, resource.etag);

// Delete
await client.deleteResource(resource);

API

OSLCClient

import OSLCClient from 'oslc-client';

new OSLCClient(user, password, configurationContext?)

Creates a client instance. An axios HTTP client is configured internally with cookie jar support (Node.js) or withCredentials (browser). If configurationContext is provided, a Configuration-Context header is sent with every request.

Host-supplied credentials

Servers that accept only bearer tokens — anything behind an OAuth 2.0 issuer — cannot be read with a username and password. Supply the credential yourself:

const client = new OSLCClient(user, password, configContext, {
  getAuthorization: async ({ url, forceRefresh }) => {
    // Inspect the URL: one axios instance reaches more than one host, and returning a
    // credential for a host it does not belong to is worse than returning none.
    if (!url.startsWith('https://oauth-protected.example.com/')) return null;
    // A complete header value, or null to use the built-in mechanisms for this URL.
    return `Bearer ${await myTokenStore.get({ forceRefresh })}`;
  }
});

The provider is called before every request. oslc-client caches nothing — memoize in your provider, because only you know the credential's lifetime.

Your provider must be single-flight. Callers commonly issue requests in parallel; without coalescing, one expired credential makes every in-flight request call your provider with forceRefresh: true at once. Start the refresh on the first call and have concurrent callers await the same promise.

Returning null leaves that request to the built-in mechanisms, exactly as before — so a provider may decline for URLs it has no credential for, such as a different host the same client reaches. A rejected credential raises CredentialRejectedError (carrying status, url and wwwAuthenticate) after exactly one forced refresh — it never falls back to Basic.

client.use(serverUrl, serviceProviderName, domain?)

Connects to an OSLC server, reads its rootservices document, discovers the ServiceProviderCatalog for the given domain ('CM', 'RM', or 'QM'; defaults to 'CM'), and selects the named service provider. Must be called before createResource, queryResources, or query.

client.getResource(url, oslcVersion?, accept?)

Fetches an OSLC resource and returns an OSLCResource. The response body is parsed into an rdflib graph. Defaults to OSLC version '2.0' and Accept: application/rdf+xml.

For non-RDF content types (text/xml, application/xml), returns { etag, xml }. For Atom feeds, returns { etag, feed }.

client.getCompactResource(url, oslcVersion?, accept?)

Fetches an OSLC Compact (UI Preview) resource. Returns a Compact object with getTitle() and getShortTitle() accessors. Defaults to Accept: application/x-oslc-compact+xml.

client.putResource(resource, eTag?, oslcVersion?)

Serializes the resource's RDF graph to application/rdf+xml and PUTs it back to the server. eTag is optional: when omitted, resource.etag (the value captured by a prior getResource/putResource/createResource call) is sent as If-Match automatically, so a typical read-modify-write round trip does not need to pass it explicitly. Pass '*' to force an unconditional update regardless of resource.etag. On a successful response, the server's ETag header is written back to resource.etag, so the resource is immediately ready for another update without re-fetching. Sends X-Jazz-CSRF-Prevent automatically. Throws a typed error from errors.js on failure (see Error Handling below) — in particular a PreconditionFailedError on a 412 If-Match mismatch.

client.createResource(resourceType, resource, oslcVersion?)

Creates a new resource using the creation factory discovered from the current service provider. resourceType is the OSLC resource type URI. Sends X-Jazz-CSRF-Prevent automatically. Returns the newly created OSLCResource (fetched from the Location header). Throws an OSLCError if the server responds with success but no Location header, or a typed error (e.g. ConflictError on 409) for any HTTP failure.

client.deleteResource(resource, oslcVersion?)

Deletes the resource at its URI. resource may be an OSLCResource instance or a plain URL string. Throws a typed error from errors.js on failure.

client.queryResources(resourceType, query)

Queries for resources of the given type and returns an array of OSLCResource objects. The query object supports prefix, select, where, and orderBy properties corresponding to the OSLC query parameters. Handles paged results automatically.

client.query(resourceType, query)

Like queryResources, but returns the raw rdflib graph (store) instead of individual OSLCResource instances.

client.client

The underlying axios instance. Use this for direct HTTP requests when the higher-level API is insufficient.


OSLCResource

import OSLCResource from 'oslc-client/OSLCResource.js';

An RDF resource backed by an rdflib IndexedFormula (graph store). All OSLC properties are accessible through the generic get/set methods, with convenience accessors for common Dublin Core and OSLC Core properties.

new OSLCResource(uri?, store?, etag?)

Creates a resource. When called with no arguments, creates a blank node with an empty graph.

resource.getURI()

Returns the resource URI as a string.

resource.store

The rdflib IndexedFormula containing the resource's RDF statements.

resource.etag

The HTTP ETag from the server response, used for optimistic concurrency on updates.

resource.get(property)

Returns the value(s) of an RDF property. property can be a URI string or an rdflib NamedNode. Returns undefined if not present, a single string value if one triple matches, or an array of strings if multiple triples match.

resource.set(property, value)

Sets an RDF property, replacing all existing values. Pass undefined to remove the property. Pass an array to set multiple values.

resource.getTitle() / resource.setTitle(value)

Get or set dcterms:title.

resource.getDescription() / resource.setDescription(value)

Get or set dcterms:description.

resource.getIdentifier()

Get dcterms:identifier.

resource.getShortTitle()

Get oslc:shortTitle.

resource.getProperties()

Returns a plain object mapping predicate URIs to their values (string or array of strings) for all statements about this resource.

resource.getLinkTypes()

Returns a Set of predicate URIs where the object is a NamedNode (i.e., outgoing links).

resource.getOutgoingLinks(linkTypes?)

Returns an array of { sourceURL, linkType, targetURL } objects for outgoing links. Optionally filter by a Set or Array of link type URIs.


ServiceProvider

import ServiceProvider from 'oslc-client/ServiceProvider.js';

Encapsulates an OSLC ServiceProvider resource — the document that advertises a project area's (or configuration's) creation factories, query capabilities, and dialogs. An instance is obtained internally by client.use().

serviceProvider.getCreationFactory(resourceType)

Returns the single creation URL (a string) for the oslc:creationFactory matching resourceType, or null if none matches. Kept for backward compatibility; prefer getCreationFactories when a resource type may be advertised by more than one factory.

serviceProvider.getCreationFactories(resourceType?) / getCreationDialogs(resourceType?) / getSelectionDialogs(resourceType?)

Return the full list of descriptors for oslc:creationFactory, oslc:creationDialog, and oslc:selectionDialog capabilities respectively, optionally filtered to those advertising the given resourceType (a URI string or rdflib NamedNode; string matching is exact-or-suffix, same as getCreationFactory). Omit resourceType (or pass null) to get every capability of that kind.

Each descriptor has the shape:

{
  url: string | null,           // oslc:creation (factories) or oslc:dialog (dialogs)
  resourceTypes: string[],       // all oslc:resourceType URIs on the capability
  resourceShape: string | null,  // oslc:resourceShape
  label: string | null,          // oslc:label
  title: string | null,          // dcterms:title
  hintWidth: string | null,      // oslc:hintWidth — dialogs only
  hintHeight: string | null,     // oslc:hintHeight — dialogs only
  usages: string[]               // all oslc:usage URIs on the capability
}

getCreationFactories omits hintWidth/hintHeight since creation factories don't have dialog sizing hints.

The usages field is what lets a consumer tell capabilities apart when a server advertises more than one dialog of the same kind for the same resource type. For example, Jazz/ELM servers advertise the "Add Link" picker as an ordinary oslc:selectionDialog for the target resource type, distinguished only by a specific oslc:usage URI — without inspecting usages, it's indistinguishable from a plain "select an existing resource" dialog:

const AddLinkUsage = 'http://open-services.net/ns/rm#addLink'; // example — check the server's actual usage URI

const linkDialog = serviceProvider
  .getSelectionDialogs('http://open-services.net/ns/rm#Requirement')
  .find(d => d.usages.includes(AddLinkUsage));

Error Handling

import { OSLCError, PreconditionFailedError, ConflictError, CredentialRejectedError } from 'oslc-client';

Write methods on OSLCClient (putResource, createResource, deleteResource) throw typed errors from errors.js on failure instead of raw axios rejections. OSLCError is the base class, carrying status, statusText, serverMessage (the response body, if any), and url. Two subclasses map to specific HTTP statuses that write-path callers commonly need to branch on:

  • PreconditionFailedError — HTTP 412, an If-Match ETag mismatch (someone else updated the resource since it was read).
  • ConflictError — HTTP 409, typically a semantic conflict the server rejects (e.g. configuration or state conflict).

CredentialRejectedError is the fourth subclass and the one a host-supplied credential consumer must catch. It is raised on any request — not only writes — when the server refuses the credential your getAuthorization provider supplied, after exactly one forced refresh, or when the provider itself throws (with the provider's error as cause). It carries status, url and wwwAuthenticate, and it is raised in place of, never in addition to, a fallback to the built-in mechanisms. Treat it as "this identity must authenticate again", which is what distinguishes it from the opaque AUTH_EXHAUSTED rejection the built-in ladder produces.

import { PreconditionFailedError, ConflictError } from 'oslc-client';

try {
  await client.putResource(resource);
} catch (e) {
  if (e instanceof PreconditionFailedError) {
    // resource.etag is stale — re-fetch and retry, or prompt the user to reconcile
    console.warn('[save] stale ETag, refetching:', e.message);
  } else if (e instanceof ConflictError) {
    console.warn('[save] server rejected the update:', e.serverMessage);
  } else {
    throw e; // OSLCError or another error — not one we handle specially here
  }
}

Authentication

The client handles authentication transparently via an axios response interceptor. Three built-in mechanisms use the user id and password given to the constructor, tried in order when a server issues a challenge:

  1. JEE Form authentication -- triggered by the x-com-ibm-team-repository-web-auth-msg: authrequired header. The client POSTs credentials to j_security_check.
  2. JAS Bearer token -- triggered by a WWW-Authenticate: jauth realm header containing a token_uri. The client exchanges the credentials for a bearer token and retries the request. The token is cached per token URI and re-fetched only when the server rejects it; concurrent fetches for one token URI are coalesced into a single request, so the credential is not submitted once per parallel request.
  3. HTTP Basic authentication -- fallback for any 401 response. The client retries with an Authorization: Basic header.

Two SSO paths complete the ladder: a redirect to a known identity provider is followed programmatically on Node, and options.ssoCallback is offered the request URL as a last resort so the host can authenticate the user interactively.

No additional configuration is needed for these; provide your credentials to the constructor and authentication is handled automatically.

Servers that accept only a bearer token -- anything behind an OAuth 2.0 issuer -- have no username and password to give, so the credential comes from the host instead, through the getAuthorization option: see Host-supplied credentials. When your provider supplies a header for a request, every mechanism above stands down for that request and a rejected credential raises CredentialRejectedError rather than falling back to Basic.

RDF Handling

All OSLC resources are parsed into rdflib IndexedFormula graphs. The client negotiates RDF content types with the server, preferring application/rdf+xml but also accepting Turtle and JSON-LD.

Properties are accessed via rdflib NamedNode symbols or URI strings. The OSLCResource.get() and set() methods abstract the underlying triple store, but you can also work directly with resource.store for advanced queries using rdflib's API.

Contributors

  • Jim Amsden (IBM)

License

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

About

A simple Node.js asynchronous OSLC client API to facilitate rich application development in JavaScript.

Topics

Resources

Stars

23 stars

Watchers

7 watching

Forks

Releases

Packages

Used by

Contributors

Languages