Skip to main content

Generated Artefacts

Namespaces

Every OData schema declares a namespace, and every type in it is really addressed by its fully qualified name: Library.Catalog.Book, not just Book. One service may carry several schemas, and then the same simple name can legitimately appear more than once — a Branch in Library.Circulation and another in PublisherRegistry are two different types.

odata2ts works with the fully qualified name internally but generates from the plain one, because Library.Catalog.Book is no name for a TypeScript interface. The namespace survives in two places:

  • as a folder in the generated output, so the two Branch types never share a directory
  • in the index files, where a root barrel re-exports each namespace under its own name once there is more than one — see Index Files

Where that is not enough — because the bundled layout puts everything in one file, or because you simply want to tell them apart when reading — give one of them a name of its own, see name clashes.

Aliases

A schema may declare an alias next to its namespace, a shorthand that references within the metadata may use instead of the full one: Self.Book rather than Library.Catalog.Book.

odata2ts reads the alias and resolves references written either way, so an alias in your metadata needs no attention. It is also accepted where you address a type yourself: an entry in byTypeAndName may name the type by its simple name, its fully qualified name or its aliased one.

File Layout

By default the artefacts of one model live together in a folder of their own, one level below a folder per namespace:

src-generated/library/
├── index.ts ← root barrel
├── LibraryModel.ts ← parameter models of unbound operations
├── LibraryService.ts ← the main service
├── QLibrary.ts
├── library-catalog/ ← namespace "Library.Catalog"
│ ├── index.ts ← namespace barrel
│ ├── book/
│ │ ├── Book.ts
│ │ ├── BookService.ts
│ │ └── QBook.ts
│ └── ...
├── library-circulation/
└── publisher-registry/

Folder names derive from the namespace and the model name in kebab-case; the file names follow the naming configuration.

Index Files

Every folder holding generated files gets an index.ts, and so does the output directory itself. Import through those barrels rather than through individual files — that way your imports do not depend on where an artefact happens to end up, which matters because its location follows the model.

import { Book, EditableBook, QBook } from "../src-generated/library/library-catalog/index.js";

The root barrel re-exports the files on root level directly, but each namespace under its own name:

export * from "./LibraryModel.js";
export * from "./LibraryService.js";
export * from "./QLibrary.js";
export * as libraryCatalog from "./library-catalog/index.js";
export * as libraryCirculation from "./library-circulation/index.js";
export * as publisherRegistry from "./publisher-registry/index.js";

That is not decoration: OData allows the same type name in two namespaces, and a flat re-export would make both of them unreachable. Where a model has only one namespace the question does not arise, so there the root barrel stays flat and exports everything directly.

One File per Artefact Kind

Set bundledFileGeneration to true and the generation collapses into one file per kind of artefact instead:

src-generated/library/
├── index.ts
├── LibraryModel.ts ← all models
├── LibraryService.ts ← all services
└── QLibrary.ts ← all q-objects

Whether the folder layout leads to cyclic imports depends on your data model — a bidirectional link between two entities is enough, and those are common. Such cycles are perfectly valid within OData, and any common ESM or bundler setup resolves them. Some cannot: SAP UI5 in combination with the TS Babel plugin is the known case, as is any other bundler unable to handle cyclic dependencies. Bundling removes the cycles, so that is the setting to reach for there.

note

Up to version 0.41.0 bundledFileGeneration defaulted to true, i.e. the bundled layout was what you got. If your imports broke when upgrading, move them to the index files — or switch the option back on.

Artefact Listing

What you get depends on what the metadata declares. The names below follow the defaults, using the Trippin service as the example:

EDMX constructModelsQ-ObjectsServices
EntityType PersonPerson, EditablePerson, PersonIdQPerson + qPerson, QPersonIdPersonService, PersonCollectionService
ComplexType LocationLocation, EditableLocationQLocation + qLocationLocationService, LocationCollectionService
EnumType FeatureFeature
unbound operation GetNearestAirportGetNearestAirportParamsQGetNearestAirportmethod on the main service
bound operation Person/ShareTripPerson_ShareTripParamsPerson_QShareTripmethod on PersonService
EntitySet Peoplegetter on the main service
Singleton Megetter on the main service
the service itselfTrippinService, the entry point

The editable model is what create, update and patch take: managed properties are gone from it, and the navigation properties accept a related entity or a reference to an existing one. The id model is the key in its minimal form, and the id function (QPersonId) formats and parses the entity path built from it, e.g. People('russellwhyte').

Several options take artefacts away again:

OptionRemoves
mode: "models"all q-objects and services
mode: "qobjects"all services
skipEditableModelsthe editable models
skipIdModelsthe id models and id functions
skipOperationsthe parameter models and the q-functions / q-actions
skipCommentsthe doc comments on model properties

The three skip* options only take effect in models or qobjects mode - see fine-tuning artefact generation.

What that looks like in your code

import { FetchClient } from "@odata2ts/http-client-fetch";
// the editable model, for typing what you send
import type { EditablePerson } from "../src-generated/trippin/index.js";
// the q-object for filtering and ordering, and the main service as entry point
import { qPerson, TrippinService } from "../src-generated/trippin/index.js";

// the HTTP client does the actual requests - fetch here, axios and jQuery are available as well
const httpClient = new FetchClient();
const trippin = new TrippinService(httpClient, "https://services.odata.org/TripPinRESTierService");

See HTTP Clients for the other implementations and for configuring headers, authentication and the like.

Queryingselect and filter work off the q-object, so a typo is a compile error rather than a puzzling response. expanding goes one level deeper: it pulls in a related entity and lets you narrow what comes back with it.

const result = await trippin
.people()
.query((builder) =>
builder
.select("userName", "firstName")
.filter(qPerson.firstName.eq("Russell"))
.expanding("trips", (trip) => trip.select("tripId", "name").top(5)),
)
.execute();

// typed all the way down: `Person` with only the selected properties
result.data.value[0].firstName;

Creatingcreate takes the editable model, which is where the difference to Person shows: managed properties are absent, and a navigation property accepts either a new related entity or a reference to an existing one.

const newPerson: EditablePerson = {
userName: "russellwhyte",
firstName: "Russell",
lastName: "Whyte",
// a reference to an existing person, stated by its key
bestFriend: { "@id": "scottketchum" },
// ... or a new related entity, created along with this one
trips: [{ tripId: 42, name: "Trip to Berlin" }],
};

const created = await trippin.people().create(newPerson).execute();

Patching — the same editable model, but every property optional, so you send only what changes.

const person = trippin.people("russellwhyte");

await person.patch({ firstName: "Russ" }).execute();

// by default a patch answers 204 with no body; ask for the entity and the typing follows
const updated = await person
.patch<true>({ firstName: "Russell" })
.execute({ headers: { Prefer: "return=representation" } });
updated.data.firstName;