Skip to content

@openhi/ours

@openhi/ours is the reference reading of the format in TypeScript: the resource shapes, a way to assemble an ontology from documents however you obtained them, and the cross-resource checks a publisher should pass before publishing.

Terminal window
npm install @openhi/ours

It works with bun, pnpm and yarn equally. The package is ESM, ships its own types, and depends only on zod.

import { assembleBundle, wellKnownOntologyUrl } from "@openhi/ours";
const get = (url: string) => fetch(url, { redirect: "follow" }).then((r) => r.json());
const ontology = await get(wellKnownOntologyUrl("example.org"));
const models = await get(ontology.models);
const vocabularies = await get(ontology.vocabularies);
const bundle = assembleBundle({ ontology, models: [models], vocabularies: [vocabularies] });
for (const model of bundle.models.values()) {
console.log(model.name, model.mapsTo?.map((m) => m.schema));
}

wellKnownOntologyUrl builds the well-known URI from a domain, so a caller that has only example.org needs nothing else. Follow redirects: a publisher who serves the ontology elsewhere redirects from there to its url.

assembleBundle returns maps keyed by URL:

interface OursBundle {
readonly ontology: Ontology;
readonly models: ReadonlyMap<string, Model>;
readonly vocabularies: ReadonlyMap<string, Vocabulary>;
readonly schemas: ReadonlyMap<string, JsonSchema>;
}

assembleBundle takes documents, not URLs or paths. The same ontology may be read from disk, fetched over HTTP, or built in memory by a generator, and all three should produce the same object. Fetching is yours to arrange, along with the retries, caching and authentication that go with it.

A document may be a single resource or a collection Bundle. resourcesIn unwraps either, so a reader never has to care which a publisher chose:

import { resourcesIn } from "@openhi/ours";
for (const resource of resourcesIn(document)) {
// resource is an Ontology, Model or Vocabulary, discriminated on resourceType
}

parseResource does the same for a document you already know is a single resource, and throws if it is not an OURS resource at all.

models, vocabularies and mappings are all required on an Ontology. A publisher with no vocabularies serves an empty collection at the URL rather than leaving the pointer out:

import { emptyBundle } from "@openhi/ours";
// GET https://ours.example.org/vocabularies.json
serve(emptyBundle());

An absent URL cannot be told apart from one that has not been published yet, so a consumer would have to guess whether to keep looking. An empty bundle at a live URL is a definite answer, and the point of the format is that nobody should have to ask.

toPublishedBundles renders an assembled bundle back into the collection Bundles a publisher serves, which is what a generator emitting an ontology from some other source of truth wants at the end:

import { toPublishedBundles } from "@openhi/ours";
const { models, vocabularies } = toPublishedBundles(bundle);
await Bun.write("public/models.json", JSON.stringify(models, null, 2));
await Bun.write("public/vocabularies.json", JSON.stringify(vocabularies, null, 2));

Every vocabulary also serialises to an ordinary JSON Schema enumeration, published beside it as .schema.json. A model binds a property to it with a plain $ref, so any off-the-shelf validator enforces the codes without knowing what OURS is.

import { vocabularySchemaFor, vocabularySchemaUrl } from "@openhi/ours";
for (const vocabulary of bundle.vocabularies.values()) {
await write(vocabularySchemaUrl(vocabulary), vocabularySchemaFor(vocabulary));
}

assembleBundle derives these for you and puts them in bundle.schemas, so a $ref to one resolves during validation without you registering anything.