Documentation / Exploitation / Guides pratiques / Configure terminology routing

Configure terminology routing

Coded ports ({ kind: "coded", valueSet }) are validated against a TerminologyPort. The instance's terminology: map routes each code system to the resolver that owns it — a plugin-contributed adapter (e.g. SNOMED CT via Hermes) — and lets every other system fall through to the built-in registry. This recipe adds one routing entry and verifies it resolves.

For what a coded type is and how value-sets fit the type system, see The type system. For adding a value-set or plugging a new resolver plugin, see Add vocabulary and types.

1. Identify the adapter to route to

Terminology resolvers are contributed by plugins as terminology adapters — the same mechanism as any other port adapter (libs/shared/src/plugin/manifest.ts, AdapterContribSchema). The bundled @posos/snomed plugin is the concrete example (external-plugins/snomed/plugin.yaml):

contributes:
  connectionTypes:
    - name: hermes
      label: Serveur Hermes
      auth: [none, basic, header, bearer]
      params:
        - { name: languageRefsetId, label: Jeu de références linguistique }
  adapters:
    - port: terminology
      name: hermes
      module: ./adapters/hermes.ts
      systems: [snomed-ct]   # informational only — not used for routing
      connection: { type: hermes }

The name (hermes) is what you reference from the instance manifest. systems documents which code systems the adapter is meant for, but it does not drive routing by itself — routing is entirely decided by the instance's terminology: keys (step 3). The adapter reaches the server through a connection of type hermes (step 2). Make sure the plugin is loaded first: see Declare and install plugins.

2. Declare the Hermes connection

Declare a connection of type hermes in the instance manifest, or create one in the console (Connexions), and make it the default connection of its type. See Manage connections for both origins.

connections:
  hermes-local:
    type: hermes
    label: Hermes local
    baseUrl: http://localhost:8081
    # languageRefsetId: "10031000315102"   # display language, see step 6
connectionDefaults:
  hermes: hermes-local

The connection carries the URL, the authentication (none for a bare Hermes; basic, header or bearer for a server behind a gateway) and the request timeout (timeoutMs, 30 s by default). Its parameter languageRefsetId selects the display language by language reference set (step 6). Write it as a string: a SNOMED CT identifier can exceed the precision of a JavaScript number.

3. Add a routing entry to the instance manifest

In instance.yaml (or the file pointed to by INSTANCE_CONFIG), add one entry per code system under terminology:, keyed by the code system identifier (the value-set id, e.g. snomed-ct) and pointing at an adapter name:

terminology:
  # routing by code system; systems not listed fall back to internal resolution.
  snomed-ct: { adapter: hermes }                            # default connection of type hermes
  # snomed-ct: { adapter: hermes, connection: hermes-fr }   # a named connection

buildTerminology (apps/api/src/instance.ts) instantiates the adapter with its config and, because the adapter declares connection: { type: hermes }, an accessor on the connection named by connection, else on the default connection of the type. The connection is resolved at each call: a connection edited in the console applies to the next calls, and the adapter empties its caches when the connection or its revision changes.

When no connection resolves, the Hermes adapter degrades as when the server is unreachable — codes accepted, subsumption refused, raw code as label — and logs [hermes] connexion indisponible (…) once. The adapter still reads its former configuration with a warning: config.baseUrl and config.languageRefsetId on the routing entry, then HERMES_BASE_URL and HERMES_LANGUAGE_REFSET_ID. There is no implicit URL.

If binding.adapter doesn't match any loaded adapter's name, the system is logged as a warning at boot and silently gets no dedicated resolver — it falls through to the same internal default as an unlisted system (see next step).

4. Leave every other system unrouted

You do not need an entry for systems that should resolve internally. The resulting RoutingTerminology (libs/shared/src/domain/terminology.ts) tries providers[system] first and falls back to InternalTerminology — the in-repo value-set registry (libs/shared/src/domain/value-sets.ts) — for anything not in the map:

private route(valueSet: string): TerminologyPort {
  return this.providers[valueSet] ?? this.fallback; // fallback = new InternalTerminology()
}

This fallback is fixed to the internal registry — there is no manifest option to point unrouted systems at a generic external server instead. If you need a system resolved externally, contribute (or select) a terminology adapter for it explicitly, as in step 1.

Be aware of one asymmetry: a value-set contributed with external: true (like snomed-ct itself — its codes aren't enumerated in-repo) has no concepts for InternalTerminology to check against, so its internal validateCode fails open (accepts any code) rather than rejecting it (libs/shared/src/domain/terminology.ts, InternalTerminology.validateCode). In practice this means: route every external value-set you actually want validated, and treat "unrouted + external" as unchecked, not as "denied".

5. Verify the routing

Run the instance-selection test, which asserts both branches — a routed system resolving through the adapter, and an unlisted system falling back internally (apps/api/tests/test-instance.ts):

cd apps/api
pnpm run test:instance

It exercises:

const okSnomed = await ports.terminology!.validateCode("snomed-ct", "24700007");
// → true, routed to Hermes
const okGender = await ports.terminology!.validateCode("administrative-gender", "male");
// → true, unlisted system, resolved by the internal fallback

Both tests declare a hermes connection on HERMES_BASE_URL (default http://localhost:8081). test:instance passes even without Hermes reachable — the adapter fails open and logs a warning (hermes.ts: "serveur injoignable … : validation SNOMED en mode fail-open"). To exercise a real Hermes round-trip (start it first), run the dedicated end-to-end test instead:

pnpm run test:snomed

6. The deployed instance

Staging runs Hermes in-cluster as applications/apps/meridian-hermes (posos-tech/vanilla-argonaute) — a StatefulSet with a PVC, reachable on ClusterIP only. The meridian-instance ConfigMap routes snomed-ct to it at http://hermes.ps-staging-apps.svc.cluster.local:8080; until it declares a hermes connection, that URL sits in the routing entry's former config.baseUrl, read with a warning. Two things differ from a Hermes you run locally:

  • The database is built on first boot, by two initContainers, from the SNOMED CT édition nationale française RF2 held in a private GCS bucket. That edition is a full edition, not a language extension — it carries the international content it depends on, so it is a single import. The ANS download is authenticated (SMT account + French NRC affiliate number), so the zip is uploaded by hand, once per annual (June) release.

  • French display is selected by the adapter, not by the server. Hermes resolves locales through a hardcoded table in which fr-fr means the international French language reference set, 722131000. The ANS national edition ships its own, 10031000315102, which that table does not know — so --locale fr makes the server refuse to start, and Hermes's documented escape hatch (a BCP 47 private-use tag, fr-x-10031000315102) fails too, because the match is then filtered against the recognised locales only. The server therefore runs on en-US, and the hermes connection carries the parameter languageRefsetId: "10031000315102" so the adapter picks the term preferred in the French refset off /extended.

    The translation is delivered incrementally, so display() returns French where translated and falls back to the server's preferred term elsewhere.

  • --bind-address 0.0.0.0 is required. Hermes binds to loopback by default, which is invisible to both the Service and the kubelet's readiness probe.

97 documents7 sectionssource : /docs · généré au build