.. include:: /includes.rst.txt .. _scripting_web: Using ADS from Web Applications and React ========================================= This section explains how the APIs of Active Data Shapes (ADS) can be used in client-side code of Web Applications. While ADS scripts typically execute on the TopBraid server (using the GraalVM engine), TopBraid can also generate stand-alone JavaScript/TypeScript files of the ADS APIs, including features like ontology-specific JavaScript classes and the generic graph object. These libraries can then be used by JavaScript apps running in the browser, for example embedded in the React source code. Such apps can utilize the full flexibility of ADS which goes way beyond what could be expressed using SPARQL or GraphQL. The functions containing the queries and mutations are sent from the web client to the TopBraid server for execution. All this is enabled because both the TopBraid server and the Web applications can share the same APIs to declare ADS scripts. Overview -------- An important design question for interactive web applications is how to best communicate with the server. Modern web applications make "Ajax" requests based on languages like GraphQL to fetch JSON responses from the server. Against the TopBraid server, many requests also use SPARQL. Common to all these is that the client code needs to construct query strings (e.g., in GraphQL) based on the current state of the user interface, make the asynchronous call, and finally process the (JSON) result into the format that is really needed to update the user interface component. This section introduces a different way for web applications to interact with the TopBraid server. Instead of sending query strings, the client can send arbitrary JavaScript functions to the server. The server will use its embedded GraalVM engine to execute those functions and then return the results to the client. The API behind those functions is automatically created from the data models/ontologies using the Active Data Shapes API generator. So for example you can write code such as `skos.everyConcept()` if you need an array of all instances of `skos:Concept` from the current asset collection. This is typically a very convenient and powerful API to query or update TopBraid graphs. One "business benefit" of this design is that client-side code can contain functions that would otherwise have to be defined on the server and thus would need to live in separate parts of a system under different maintenance procedures using different tools. With ADS, web application code can benefit from compile-time checking, a rich editing experience using auto-complete etc. Furthermore, the client code can directly produce the exact JavaScript data structures that are needed by the component, without first having to go through the generic JSON formats produced by GraphQL or SPARQL. The following diagram illustrates the overall architecture of Active Data Shapes, when executed on the TopBraid EDG server versus from Web Browsers versus from Node.js. .. figure:: _images/ADS-Architecture-Tree.png :alt: ADS architecture diagram :align: center :class: edg-figure All this is best explained using examples, so let's dive straight in. .. _scripting_web_simple_example: Simple Example -------------- This example simply prints the number of triples in the current asset collection. The code is divided into two files: * A JavaScript file containing the ADS code with a function that implements the query logic * A React component for the rendering, importing the ADS file .. _scripting_web_simple_example_ads: The ADS File ^^^^^^^^^^^^ The file `HelloComponent.ads.js` declares the query function(s) that will be executed on the server. It also exports a dedicated function that serves as proxy for the remote function call. The code of that proxy function is basically always the same, calling a function `TopBraid.asyncFunction`, and mirrors the parameter passing of the remote function: .. literalinclude:: _code/_web/HelloComponent.ads.js :language: js :linenos: .. _scripting_web_simple_example_react: The React Component ^^^^^^^^^^^^^^^^^^^ The file `HelloComponent.jsx` defines the rendering only, and delegates the data loading to the ADS file: .. literalinclude:: _code/_web/HelloComponent.jsx :language: text :linenos: The JavaScript module `geography_ontology_ADS_generated_web.js` has been automatically generated by TopBraid. To produce such a file, use the Export tab of your Ontology (here, the Geography Ontology) and select *Generate API for External Scripts > JavaScript API for Web Applications*. Alternatively, you may want to rely on one of the standard ADS libraries, for example for SKOS, that can be generated in the same Export feature. The `.ads.js` file defines all helper functions of the component that may execute on the server. These functions must only use the generated ADS APIs, because that very same API will also exist on the TopBraid server at execution time. The client code base has a copy of that API, but that is only used for compilation and syntax checking, while the execution happens against the identical ADS API on the server. Given that the source code of the JavaScript functions is sent to the server, it will execute in a different environment from the client. These functions may therefore not call the React API or other client-side features, nor can they access variables from surrounding scopes on the client, unless they are passed into the function as parameters. In other words, the script functions need to be stateless, self-contained and rely on ECMAScript only. See :ref:`scripting_web_scope` for details. In the example above, only the `triplesCount` function will be sent to the server for execution. The function `loadTriplesCount` serves as a wrapper or proxy to expose this function to the client code from the React source file. `TopBraid.asyncFunction` returns a JavaScript `Promise` object, i.e. it will start an asynchronous call and return instantly. The `await` keyword may be used to process the result of the `Promise` in the most readable syntax. As shown in the HelloComponent source code, the function surrounding the await must be declared `async`. Alternatively, use the conventional `Promise` syntax such as `loadTriplesCount().then((data) => ... )`. Use `TopBraid.asyncFunction` for any read-only operation, and `TopBraid.asyncMutation` for operations that may also modify the graph. .. _scripting_web_complex_example: Complex Example --------------- For this example we want to render a `skos:ConceptScheme` together with a list of its top concepts and the number of their children each. Embedded into an EDG Panel it will look as shown: .. figure:: _images/Web-TestPanel.png :alt: A screenshot of the panel produced by this example :align: center :class: edg-figure **The Test panel created by this example** .. _scripting_web_complex_example_ads: The ADS File ^^^^^^^^^^^^ The query logic here requires a helper function that recursively walks down the hierarchy of narrower concepts. Furthermore, this example is complicated by the fact that we want to pass an argument from the client (the selected concept scheme) into the ADS code, and the ADS code represents this concept scheme as instance of the JavaScript class `skos_ConceptScheme` while the client simple uses an instance of `RDFValue` or any other object that has a uri field. This complicates the invocation a little bit, because the URI needs to be cast into the correct ADS API class. The file is also quite bloated because it has been littered with JSDocs comments that help with type-safety and documentation. This can of course be achieved in more compact form in TypeScript, or simply removed. .. literalinclude:: _code/_web/TestComponent.ads.js :language: js :linenos: Note the additional arguments of the `TopBraid.asyncFunction` call: * `[scheme]` is the list of parameter values, as an array in the same order as expected by `getChildInfo` * `[skos_ConceptScheme]` tells the engine that it needs to type-cast the first argument scheme into an instance of the ADS class `skos_ConceptScheme` * `[countChildren]` is an array of any helper functions that also need to be sent to the server for the execution * The template for those load functions is however always the same and quite trivial to derive with a bit of practice. What matters is that the actual business logic can be written down as part of the component's JavaScript source code, and not as some string. .. _scripting_web_complex_example_react: The React Component ^^^^^^^^^^^^^^^^^^^ The file `TestComponent.jsx` defines the rendering only, and delegates the data loading to the ADS file: .. literalinclude:: _code/_web/TestComponent.jsx :language: text :linenos: .. _scripting_web_scope: Variables and Scope ------------------- When JavaScript functions are sent to the server for execution using `TopBraid.asyncFunction` they operate in a very different environment from the (Web) client. In particular the scoping of variables is different and requires attention to detail. As a general rule, any function that gets sent to the server can only process its direct arguments and can not rely on any variables outside of the scope. So if your web component keeps its own data structure and you want the ADS function to use that structure, you need to explicitly pass this data to the function. Let's look at an example in which the client has a state object such as .. literalinclude:: _code/_web/scope_client.js :language: js :linenos: And `loadSomething` is a proxy function from an `.ads.js` file: .. literalinclude:: _code/_web/scope_ads.js :language: js :linenos: At execution time, the `something` function will run within the server sandbox environment. There, the data argument will be a deep copy of the original data object from the client. It will be passed into the function through generated JavaScript code using its JSON serialization. (This means that any such argument must be serializable into self-contained JSON - make sure it does not contain cycles and also make sure you don't pass huge objects over the wire). In this case, the something function may modify its own local copy of the data object, yet the client's copy will of course remain unchanged. Later, when the data object gets returned from the server-side function, the client will again receive a new clone and TopBraid uses serialization via JSON to exchange the data between client and server. As a result, modifications to the object on the server will not affect the client-side object. Since we use JSON serialization to transport objects between client and server, extra care needs to be taken if the JavaScript objects are typed as `LiteralNode` or `NamedNode` or a subclass of that. By default, the type/class information will get lost. However, you can use the third argument of `TopBraid.asyncFunction` to specify the type(s) that each argument should have on the server. This approach had been illustrated in the :ref:`scripting_web_complex_example`, where the server-side function expects the argument to be an instance of the JavaScript class `skos_ConceptScheme`. For this mechanism to work, the argument values simply need to be objects with a `uri` field. In cases where the expected type is `LiteralNode`, the values may be simple JavaScript values of type boolean, number or string, or be objects with a `lex` field and an optional `lang` field and optional `datatype` (full datatype URI) or `dt` (local name of the datatype). This is the same format that is used by ADS literal nodes, but should also work for the literal representation within the TopBraid EDG user interface code. If the client-side code expects the result object of the async call to be of a certain JavaScript class, it needs to perform the appropriate type casting itself. The `async` function will only ever return plain JavaScript values or objects that fit into JSON serialization.