OpenAPI API Configuration

OpenAPI API Configuration

The OpenAPI integration in SmartWeb works in two directions:

  • Inbound OpenAPI — SmartWeb exposes external HTTP endpoints described by an OpenAPI specification. Each operation is bound to a D2000 RPC procedure. Incoming HTTP requests are translated into D2000 RPC calls.

  • Outbound OpenAPI — D2000 ESL scripts can, through a connector running inside SmartWeb, call external HTTP services described by an OpenAPI specification. The connector serializes the RPC arguments into JSON, sends the request and returns the deserialized response back to D2000.

This page covers both directions. Generating the TypeScript client for D2000 RPC is no longer part of OpenAPI — see Generating a TypeScript Client for D2000 RPC.

Inbound OpenAPI

Inbound OpenAPI is exposed at /api/open and secured the same way as the server-to-server REST API (API key, JWT or Basic auth). On startup SmartWeb parses every configured specification file and publishes its POST operations at /api/open/ + contextPath + the path from the specification. Each operation is bound to a D2000 RPC procedure named by the x-d2-* extensions of the specification (see Nested structures); other HTTP methods are not bound, because an RPC call always carries a payload.

GET /api/open/<contextPath>/openapi.yaml returns the document that is really deployed — including the defaults SmartWeb filled in and any operations generated from D2000 metadata. For the example below that is GET /api/open/edc/openapi.yaml. The report of what SmartWeb made of the specifications and what it is missing in D2000 is at GET /api/open/status.md — see Generated specification documentation.

Configuration

smartweb: application: openApi: enabled: true processName: "SELF.SMARTWEB_OPENAPI" # only needed by a specification with a "mapping" block httpHeaders: [ ] # extra HTTP headers added to /api/open responses specifications: - specificationFilePath: edc_openapi_ofs.yaml # relative to the configuration directory specificationFormat: YAML # YAML | JSON specificationVersion: "3.0" contextPath: edc # URL segment under /api/open generateDocumentation: true # on by default, see the end of this page

Property

Description

Property

Description

enabled

Switches the inbound API on or off. A call to a disabled API is answered with 400.

processName

D2000 process the metadata session logs on as. Required only by a specification with a mapping block; without it, and without connectors.coreConnector.local.processName as a fallback, startup fails with No process name was specified!.

httpHeaders

Extra HTTP headers added to /api/open responses (format Name: value).

specifications[].specificationFilePath

Path to the specification file, relative to the configuration directory. The file is watched — a change reloads the context, and a file that fails to parse leaves the previously loaded version deployed.

specifications[].specificationFormat

YAML or JSON.

specifications[].contextPath

URL segment the operations are published under. Must not be empty — a specification without it is never mounted.

specifications[].generateDocumentation

true by default — the specification is part of the report at /api/open/status.md, see Generated specification documentation.

specifications[].mapping

Optional — generates operations from D2000 RPC metadata, see below.

[!NOTE] The URL is /api/open + contextPath + the path written in the specification. With contextPath: edc and a path /ofs/api/v1/contracts-changes the operation is called at POST /api/open/edc/ofs/api/v1/contracts-changes.

Request flow

  1. An external client sends POST /api/open/edc/ofs/api/v1/contracts-changes.

  2. SmartWeb resolves the URL to the specification and the operation bound to that path.

  3. The properties of the request body become the RPC input parameters, in the depth-first order of the payload schema; a property can also be filled from an HTTP header (see below).

  4. The output parameters are appended as empty values, so the ESL procedure receives the whole signature.

  5. The bound D2000 RPC procedure — x-d2-event-name + x-d2-event-rpc-name — is called, by default as a conversation (CONVERSATION_BEGIN_END), and SmartWeb waits for the ESL answer.

  6. The output parameters are serialized back into JSON according to the response schema and returned to the caller.

[!IMPORTANT] With x-d2-event-rpc-call-type: CONVERSATION_BEGIN_END (the default) the HTTP request is held until the ESL procedure answers with CALL [_hTC] Result(...) ASYNC TC_Ealso from its EXCEPTION_HANDLER. A procedure that does not answer leaves the caller waiting for its timeout.

Generating operations from D2000 metadata

A specification with a mapping block does not only publish what the file contains — SmartWeb opens a D2000 session, reads the RPC procedures of the ESL events and adds them to the document as further operations. The block also filters what is published:

- specificationFilePath: base.yaml contextPath: discover mapping: allowedD2RpcEventNames: [ "E.*" ] # ESL events to publish allowedD2RpcMethodNames: [ "*" ] # RPC procedures to publish allowedD2RpcCallModes: [ REPLY, CONVERSATION_BEGIN_END ]

The names are wildcard patterns, case-insensitive. specificationFilePath is optional here — without it the document is built purely from D2000 metadata. Changes in D2000 (a new RPC procedure, a changed structure definition) rebuild the document at runtime.

Values from HTTP headers and the response status code

Two extensions on components map payload properties onto the HTTP layer:

components: x-property-in-mappings: correlationId: $parameterRef: "#/components/parameters/CorrelationIdHeader" priority: PAYLOAD_THEN_PARAM # PAYLOAD_THEN_PARAM | PARAM_THEN_PAYLOAD | PARAM_ONLY x-property-out-mappings: result: pattern: "^(?<httpStatusCode>[0-9]{3})?:?(?<value>.*)$"
  • x-property-in-mappings — the named property may be filled from an HTTP header referenced through $parameterRef. priority decides what wins when both the payload and the header carry a value.

  • x-property-out-mappings — a regular expression applied to a text output parameter. The named group value becomes the value written into the JSON, the named group httpStatusCode sets the HTTP status code of the response. This is how an ESL procedure answers with something other than 200.

Nested structures

A D2000 structure is a flat table of typed columns — a cell cannot hold another structure. Arbitrarily deeply nested JSON is therefore decomposed relationally: the parent structure has a column holding the row's own key (rowId) and every nested structure has a column referencing the parent row's key (parentRowId). Each nested structure is passed as a separate RPC procedure parameter — the ESL procedure thus receives N flat structures and joins them through parentRowId == rowId.

Specification extensions

Attribute

Level

Meaning

Attribute

Level

Meaning

x-d2-event-name

operation (post)

D2000 ESL Event hosting the RPC procedure, e.g. E.API_OFS_Contracts

x-d2-interface-name

operation (post)

Optional ESL interface name for interface-implementing procedures

x-d2-event-rpc-name

operation (post)

RPC procedure name; defaults to the last path segment

x-d2-event-rpc-call-type

operation (post)

CONVERSATION_BEGIN_END (default) or REPLY

x-d2-structure-name

component schema, or an array-of-scalars property

Marks the component as a D2000 structure and names the target SD.*. Without it, a $ref nested inside a structure stays a scalar column (enumerations, scalar aliases). On a property it is only allowed for an array of scalars — on a property referencing a component it is an error

x-d2-structure-rowid-column

structure schema

Name of the integer column holding the row's own key — the link target for nested structures

x-d2-structure-parent-rowid-column

structure schema

Name of the integer column referencing the parent row's rowId

x-d2-name

property schema

Overrides the RPC parameter / column name derived from the JSON property name; used to resolve name collisions. On the items schema of an array of scalars it names that structure's single column

Example

components: schemas: EdcSyncPayload: # envelope — its properties are the RPC parameters type: object properties: subjects: type: array items: { $ref: "#/components/schemas/EdcSubject" } EdcSubject: type: object x-d2-structure-name: "SD.EdcSubject" x-d2-structure-rowid-column: "rowId" # transient integer key, generated by SmartWeb properties: id: { type: string } temporalData: type: array x-d2-name: "subjectTemporalData" # RPC parameter name (JSON property stays temporalData) items: { $ref: "#/components/schemas/EdcSubjectParametersTemporalDataItem" } EdcSubjectParametersTemporalDataItem: type: object x-d2-structure-name: "SD.EdcSubjectParametersTemporalDataItem" x-d2-structure-parent-rowid-column: "parentRowId" properties: id: { type: string } name: { type: string }

Arrays of scalars

An array whose elements are scalars has no matching D2000 value. Declaring x-d2-structure-name directly on the property maps such an array onto a flat SD.* structure with a single column — one row per element. The column name comes from x-d2-name on items, otherwise from the property name (x-d2-name on the property itself still names the RPC parameter). In the response such a structure is written back as an array of scalars. The same rules as for any other structure apply, so it can also be nested — it then needs x-d2-structure-parent-rowid-column.

subjects: # RPC parameter "subjects" -> SD.EdcSubjectIdList x-d2-structure-name: "SD.EdcSubjectIdList" type: array items: { type: string, x-d2-name: "id" } # single column "id", one row per element

Dates

Both format: date-time and format: date map onto a time column. A date-time value is read and written as a full UTC ISO-8601 timestamp, a date value as a plain yyyy-MM-dd day in the server's local time zone. A string property without either format stays a text column.

Requirements on the D2000 side

[!IMPORTANT] The target SD.* structures must really exist in D2000 and their column names must match the names of the mapped properties (or x-d2-name). Cells are placed into the row by column name, so the property order in the specification does not have to match the column order of the structure and the link columns do not have to come first. Columns the specification does not map stay empty. If the structure does not exist, one of its mapped columns is missing, or a link column is not of type integer, the call fails with an error naming the structure and the column.

  • The link columns are transient — they are not JSON attributes and are not written to the response. The exception is a rowId mapped to an existing JSON attribute, which must be type: integer and required; the value is then taken from the request and is also written to the response. Mapping parentRowId to a JSON attribute is not allowed.

  • Transient rowId values are generated by SmartWeb from a single counter per message, incrementally from 1. A rowId is unique within the whole message, not within one structure — the counter runs on across all parent structures, so two parents never carry the same id and a parentRowId always identifies exactly one row. An ESL procedure filling these columns for a response has to keep the same rule. A rowId read from a payload attribute is not generated by SmartWeb, so keeping it unique is up to the caller.

  • The order of the ESL procedure's formal parameters follows the specification: depth-first (pre-order) traversal, i.e. a structure immediately followed by its nested structures, recursively; all input parameters first, then the output ones. Within one schema the parameters follow the declaration order of its properties in the specification file. SmartWeb neither generates nor adapts the procedure signature — the specification is the source of truth.

[!IMPORTANT] SmartWeb passes the parameters positionally and does not verify the procedure — it reads nothing about the signature from D2000. What happens with a procedure declared in a different order is decided on the D2000 side. Whenever the specification changes, re-check the parameter order of every affected procedure.

Specification errors

The following cases are a hard error when loading the specification: the error is logged and the change does not take effect (the previously loaded working version stays deployed).

  • recursive structure schema (direct or indirect $ref cycle)

  • RPC parameter name collision across the whole flattened tree, or column name collision inside one structure — resolve with the x-d2-name attribute

  • x-d2-structure-parent-rowid-column mapped to an existing JSON property

  • x-d2-structure-rowid-column mapped to a property that is not type: integer or not required

  • a structure with nested structures but without x-d2-structure-rowid-column

  • a nested structure without x-d2-structure-parent-rowid-column

  • x-d2-structure-name on a property referencing a component — it belongs on the component itself

  • x-d2-structure-rowid-column and x-d2-structure-parent-rowid-column naming the same column

Outbound OpenAPI connector

The outbound connector lets a D2000 ESL script call an external HTTP service. SmartWeb registers in D2000 as a process; the ESL script calls an RPC procedure on that process, the connector resolves it against the loaded OpenAPI specification, writes the RPC arguments as the JSON request body and sends the request. The answer is returned to D2000 as a second RPC call.

Both directions read the same x-d2-* extensions and the same nested-structure rules, so one specification file can serve the inbound API and the outbound connector at once.

Configuration

Every entry of openApiConnectors starts one independent connector — its own D2000 process, its own HTTP client, its own specifications.

smartweb: application: connectors: openApiConnectors: - local: processName: "SELF.OPENAPI_CONNECTOR" remote: connectionUrl: "https://external-api.example.com/base/" authType: "USERNAME_PASSWORD" # USERNAME_PASSWORD | API_KEY | OAUTH2 username: "user" password: "secret" # apiKey: "external-service-key" # authType: API_KEY # realm: { ... } # authType: OAUTH2 truststore: path: config/cert/truststore.p12 password: changeit connectTimeout: 5000 requestTimeout: 45000 idleTimeout: 30000 addressResolutionTimeout: 5000 openApi: enabled: true specifications: - specificationFilePath: external_api.yaml specificationFormat: "YAML" specificationVersion: "3.0" generateDocumentation: true

Property

Description

Property

Description

local.processName

Name of the D2000 process the connector logs on as. The ESL script addresses this process — the object has to exist in D2000.

remote.connectionUrl

Base URL of the external service. The operation path from the specification is resolved against it.

remote.authType

Authentication of the outbound request — see the table below. Without it no authentication header is sent.

remote.truststore.path / .password

Truststore for verifying the TLS certificate of an https target.

remote.connectTimeout / requestTimeout / idleTimeout / addressResolutionTimeout

HTTP client timeouts in milliseconds (defaults 5000 / 45000 / 30000 / 5000).

remote.openApi.specifications[]

Specifications of the external API — same properties as for the inbound direction; contextPath has no meaning here. The files are watched and reloaded on change.

[!IMPORTANT] connectionUrl has to end with a slash. The operation path is resolved against it as a relative URI, so https://host/base without the trailing slash loses the base segment and every call goes to https://host/….

Authentication types

authType

Header sent

Requires

authType

Header sent

Requires

(not set)

none

USERNAME_PASSWORD

Authorization: Basic …

username + password

API_KEY

X-API-Key: …

apiKey

OAUTH2

Authorization: Bearer … (token obtained per call, client-credentials grant)

realm

Calling it from an ESL script

INT _hConn _hConn := %StrToHBJ("SELF.OPENAPI_CONNECTOR.DCP") CALL [(0)] E.MyEvent.myOperation(_header, _rows, _nestedRows) ASYNC ON (_hConn) TC_BE _hTC
  • The connector is a JAPI process, so the call carries no object reference ([(0)]) and the process is addressed by HOBJ (ON (_hConn)). D2000 registers the connector's session under local.processName plus the .DCP extension — that is the object name %StrToHBJ resolves; the bare process name is not an object and returns an invalid value. The object exists once the connector has logged on. An RPC procedure that implements an ESL interface cannot be called this way.

  • The procedure name is x-d2-event-name + . + x-d2-event-rpc-name of the operation, written literally — D2000 object names may contain a full stop, so the name compiles. The connector splits it at the last full stop. An RPC name containing a hyphen cannot be written in ESL — use an underscore, the connector retries the lookup with hyphens.

  • The arguments are positional, in the same flattened depth-first order as for the inbound direction, and all of them have to be passed, empty structures included.

  • The answer is not a return value. The connector answers on the conversation with on<Rpc>Response(<output parameters>), or with on<Rpc>Error(errorCode, errorMessage). Both are declared in the calling script as RPC PROCEDURE [_hTC, TC_E] and close the conversation. If the procedure does not exist, the connector aborts the conversation instead.

RPC PROCEDURE [_hTC, TC_E] onMyOperationResponse(IN RECORD NOALIAS (SD.Header) _header) ; process the answer END onMyOperationResponse RPC PROCEDURE [_hTC, TC_E] onMyOperationError(IN TEXT _errorCode, IN TEXT _errorMessage) LOGEX _errorCode + " - " + _errorMessage PRIORITY _LOG_PRTY_ERROR END onMyOperationError

Error codes

errorCode is a mnemonic string, not an HTTP status code; some of them append the status of the remote answer.

errorCode

Cause

errorCode

Cause

ERROR_OPEN_API_MAPPING_MISSING

The event.rpc name has no operation in the loaded specification

ERROR_OPEN_API_REQUEST_VALIDATION

The RPC was called with more arguments than the operation has parameters

ERROR_OPEN_API_REQUEST_CREATION

The JSON request body could not be built

ERROR_OPEN_API_STRUCTURE_MAPPING

Mapping the answer onto D2000 structures failed in SmartWeb — a missing column or link column

ERROR_OPEN_API_RESPONSE_ERROR: <status>(<reason>)

The external service answered with a non-2xx status

ERROR_OPEN_API_RESPONSE_PARSING_ERROR: <status>(<reason>)

The answer could not be parsed

ERROR_OPEN_API_REQUEST_SEND_FAILED

The request could not be sent (connection, DNS, timeout)

ERROR_HTTP_CLIENT_NOT_RUNNING

The connector's HTTP client is not running

ERROR_OPEN_API_OAUTH_OBTAINING_ACCESS_TOKEN_FAILED

An OAuth2 access token could not be obtained

Generated client stubs

Generating the TypeScript client (structures.ts, events.ts) has moved out of OpenAPI — it is configured by the smartweb.application.generator.d2jsapi[] block and runs over the session of the core connector. The clientOutputDirectory and useLegacyApi keys of a specification were removed and leaving them in place has no effect. See Generating a TypeScript Client for D2000 RPC.

Generated specification documentation

For every processed specification SmartWeb builds a Markdown report and serves it on the same path the document itself is served on:

URL

Answers with

URL

Answers with

GET /api/open/<contextPath>/openapi.yaml

the deployed specification of that context

GET /api/open/status.md

the report of every specification of the instance (text/markdown)

For example http://localhost:8098/smartweb/api/open/status.md.

[!NOTE] There is one report URL per instance, not per context: a report belongs to a specification file, and files do not map onto context paths — the specification of an outbound connector is published under none. When the same file is used in both directions it appears once, with a section per direction. No file is written to disk — the report is rendered when it is requested and checked against D2000 through the session of that same request, i.e. with the rights of whoever asked for it (API key, JWT or Basic auth). It is switched off with generateDocumentation: false on that specification.

The report is short — it is a report, not a description of the specification:

  • one sentence per direction: YAML correctly processed with the number of bound operations, or YAML was NOT processed with the mapping error verbatim,

  • a Structures on the D2000 side section — one section per SD.* the specification maps, whatever its state: it matches, it does not match (a missing column, a wrong type, a rowId / parentRowId that is not Integer), it is missing in D2000, or D2000 has it but does not publish it (see below, with its HOBJ). Every one of them ends with the whole column layout the specification maps onto that structure, so it can be created or corrected from the report alone,

  • an RPC procedures the D2000 side has to declare section — the ESL header of every bound RPC procedure (for the inbound direction including the CALL [_hTC] Result(…) ASYNC TC_E answer, for the outbound one the call and both callback procedures, on<Rpc>Response / on<Rpc>Error). These headers are generated from the specification; whether D2000 declares them is not checked — an RPC procedure lives in an ESL script. An output parameter that would repeat the name of an input one is renamed with an Out suffix (_headerOut), because ESL does not allow the same parameter name twice — the parameters are passed positionally,

  • a Recursion in the specification section when the file contains a recursive schema.

When nothing is missing the whole report is a few lines long. The parameters of working operations and the columns of matching structures are not listed — that is the content of the specification itself, which openapi.yaml serves.

[!NOTE] status.md answers even when the specification could not be processed at all — that report is the only place saying why.

Missing, or not published?

D2000 pushes structure definitions to a JAPI client as shared resources. An SD.* object can therefore exist in D2000 and the session still not know its definition — typically for a freshly created structure that no running D2000 side uses yet. SmartWeb therefore asks the kernel itself (getObjectInfo) whenever the definition is missing, and the report distinguishes two states:

Definition in the session

Object in the kernel

Reported as

Definition in the session

Object in the kernel

Reported as

present

the columns are checked

absent

does not exist

missing in D2000, with the columns to create

absent

exists

is not published by D2000, with its HOBJ and the advice to start / recompile the ESL script declaring it

Only the structures are checked this way — the RPC procedures are not checked at all.

[!IMPORTANT] As long as D2000 does not publish the definition of a structure, a call mapping it fails with 500 and the same explanation — it is not a problem of the specification but of the state of the D2000 side.

The report is always current

It is rendered per request, so there is nothing to invalidate:

  • the specification files are checked on disk on every request and whatever changed is re-read — editing a file and reloading the URL is enough, no waiting for the file watch,

  • the D2000 side is read at request time — creating the missing SD.* in CNF and reloading the URL is enough, no restart is needed,

  • the RPC procedures are not checked — their headers are what the specification expects of the D2000 side, and they are listed whether or not D2000 declares them.

When the same specification is used in both directions (published as an inbound API and called through an outbound connector), the report has a section per direction and one shared Structures on the D2000 side section.

[!NOTE] Checking the structures against D2000 requires a D2000 session — the one of the request is used. When none can be resolved the report is served as well, but the structure check is marked as not validated, with the reason. No extra session is opened for the report.

Recursion in the specification

A component that references itself has no equivalent in D2000 — a structure is a flat table and a cell cannot hold another structure — so the whole specification is rejected. The report then carries a Recursion in the specification section: the cycle as a path (OrgNode -> children -> OrgNode) and its unrolling to a fixed depth — one component per level (OrgNodeL1OrgNodeL3), all mapped onto the same SD.*, the root declaring only x-d2-structure-rowid-column, every level below it also x-d2-structure-parent-rowid-column, and the last one carrying no nested property any more.