# Motivation
OpenText Content Server provides organizations with a centralized platform to store, manage, and access business documents and information. While Content Server offers an extensive range of functionality, it is sometimes necessary to write a custom application to meet a unique requirement.
When a custom application requires a database table, my advice is to develop a custom OScript module alongside RHCore. RHCore includes an object-relational mapper (ORM), which efficiently manages custom schemas, and generates an API for data querying and manipulation.
If an application doesn't require a custom schema, then the need to write an OScript module may be called into question. In such cases, LiveReports and WebReports can be helpful alternatives. However, it's worth noting that LiveReports only allows for database queries, and WebReports uses a proprietary syntax, is challenging to debug, and doesn't scale well.
Several years ago, I introduced ScriptNodes as a convenient method to write OScript directly in the browser. The approach has demonstrated significant utility, and has bypassed the need to write and install a module for simple OScript requirements.
Since the original post, ScriptNodes have been extended to include a template section. This permits a template to be defined as part of the ScriptNode, which can be rendered as output. For example, the following ScriptNode outputs something like "Hello John Doe!"
What's great about a ScriptNode is that it provides access to the entire Content Server OScript API. But even better, you have access to the RHCore API.
The solution has proven highly effective in developing simple server-rendered applications. I never encountered limitations on the server as I had the freedom to invoke any OScript function I needed. Nevertheless, I still faced challenges when it came to crafting interactive user interfaces.
For creating modern interfaces, it is easiest to use a reactive framework like Vue or React with a component framework. This ensures a cohesive design for buttons, menus, dialogs, etc. It is important to acknowledge that such front-end applications commonly retrieve data through asynchronous API calls.
ScriptNodes can store and deliver compiled JavaScript to the client. However, this means the entire application is delivered by Content Server on each page load, and no browser caching is used. This is inefficient, especially if the application is large.
Applications often require a custom API to perform operations on the server. ScriptNodes can fulfill this requirement since they can be invoked through the Content Server REST API. However, the approach entails the creation of a new ScriptNode for each endpoint, which can become burdensome to manage and maintain.
Upon encountering these and other challenges, I sought to find a better approach for developing such applications. Specifically, I wanted the process to:
- operate independent of any JavaScript library bundled with Content Server,
- eliminate the need to install additional modules, with the exception of RHCore,
- leverage browser caching to cache any compiled JavaScript,
- offer a rich and consistent user interface encompassing of menus, dialogs, buttons, and more,
- automatically manage the session without explicit handling (i.e., no fumbling with
otcsticket
or request headers), - define its own "services" (custom API calls) and permit easy invocation from the client,
- move development outside of CSIDE and the Content Server web interface,
- provide an excellent developer experience, using VSCode, TypeScript, and familiar tools,
- support hot reloading (i.e., the browser is immediately updated on code changes),
- allow for source code management and collaborative development using a version control systems like Git,
- work seamlessly between the classic interface and SmartUI (as a perspective),
- support multiple views (or routes), which can be bookmarked,
- have fast initial page loads,
- have deployments work seamlessly in a cluster without requiring a restart, and
- encapsulate everything within a single Content Server node, ensuring straightforward usability, deployment, and portability.
Let me show you how we accomplished this.
# Writing a single-node application
All development activities are performed on the desktop, which eliminates the need to code in a web browser text box. Development is performed with an IDE (e.g., VSCode), compiled, and uploaded to Content Server as a ScriptNode version.
There are three main parts:
- the front end,
- the back end, and
- a script that brings it together.
# The front end
At present, a project is based on Vite (opens new window), Vue (opens new window), and the @kweli/cs-vue-vite-plugin (opens new window) plugin. The later is a component library we created at Kwe.li GmbH (opens new window) for creating user interfaces, which renders nicely in Content Server. It also has a few components that are Content Server specific. For example:
<!-- Render a function menu -->
<KFunctionMenu :dataid="2000" />
<!-- Render a link to a node with the icon, function menu, and modified image callbacks -->
<KBrowseLink :dataid="2000" />
<!-- Render a node picker form field, to select a folder -->
<KNodePickerField v-model="selectedNode" :select-screen="[0]" />
2
3
4
5
6
7
8
There are additional components for dialogs, tabs, user lookups, buttons, snackbars, etc. Components that require an API call (such as KBrowseLink
, which makes an API call to retrieve the node name) performs the operation transparently, and eliminates the need for manual session management. Furthermore, most components are designed to work with the classic and SmartUI interfaces, which enables the possibility of universal applications.
# The back end
To generate back-end API services, you can add an ./oscript/
directory to the project, and populate it with text files to define the respective service. For example, if you require a service to retrieve user details, you could create a file named ./oscript/FetchUserInfo.txt
:
// @ts-hint {userId:number}
function Assoc FetchUserInfo()
Assoc results = .OKAssoc()
// get the userId from the request, and cast it to an integer
Integer userId = .request('userId.toInteger')
Frame user = .user(userId)
if user.isValid()
// results.data is returned to the client
results.data = user.userRec()
else
results = .ErrorAssoc('Invalid user.')
end
return results
end
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
This defines a service named FetchUserInfo
, and expects a single userId
parameter.
You may have noticed the @ts-hint
directive in the first line. This is used when generating the client TypeScript API, but more on this later.
A service can be anything you need. For example:
- start a workflow,
- create a user, or add one to a group,
- update permissions on a node,
- run a complex database query,
- run a LiveReport or WebReport,
- etc.
Services can be called asynchronously from the user interface. This pattern provides for fast initial page loads, since no heavy data loading or rendering is performed up front. However, if a subsequent sevice call is slow, then the user can be notified with an appropriate loading indicator.
# Bringing it together
The solution is brought together by the cs-scriptnode-bundler script. The first thing it does is generate a TypeScript client library as a Vue composable (opens new window). This can be used by the Vue app to make requests to the custom back end:
const services = useServices();
const userInfo = await services.FetchUserInfo({ userId: 12345 }); // TypeScript safe!
2
This works without having to manually manage the otcsticket
or any other session information. Helpers are also available to make standard REST API calls, if necessary.
Secondly, the script bundles the compiled JavaScript, the services, and some additional logic into a single ScriptNode, and uploads it to Content Server as a new version. It is immediately ready to use.
During the initial execution of the ScriptNode, the JavaScript bundle is deployed to a temporary location in the support/
directory. The ScriptNode returns a web page, which instructs the client to load the application from this location. The approach takes advantage of browser caching, which leads to better performance on subsequent page loads. The deployment method is also cluster-friendly.
Voila! The page is rendered, and the user sees the application.
# Notes
# Developer mode
The solution also provides a developer mode, which allows the application to be loaded from a local Vite development server. This facilitates development of the client within the Content Server environment, and supports hot reloading. No longer do you need to click a save button, wait a few seconds, and hit page reload to see the effects of a change.
# Routing
It's possible to use Vue Router (opens new window) in "hash mode" to create multiple routes, which can be bookmarked. It's also possible to use query parameters after the hash, which don't interfere with any Content Server query parameters.
# Version Control
Source code can be managed in Git or subversion since source files reside outside of Content Server. This makes it easy to track changes, trace bugs, and collaborate.
# In practice
We've created a few solutions with the approach, and are delighted with the results. Development and deployment was easy, and applications run fast with a rich and interactive user interface.
I see a lot more of this type of development in the future.
# Want to know more?
Please reach out to Kwe.li GmbH (opens new window) if you'd like to know more.