Skip to content

OData Types โ€‹

Every OData service publishes a $metadata document describing all of its entities and their fields. Instead of hand-writing TypeScript interfaces for those entities (tedious, and they drift out of sync with the backend), you generate them from $metadata with odata2ts.

There's one catch: odata2ts emits query-object classes (the Q* objects used to build type-safe OData queries), not plain field shapes. So the workflow has a small mapping step โ€” turn the raw Q* class into a normal type with EswOdataMapTypes from esw-utils. After that the type is safe to use anywhere in the app.

The four steps:

  1. Configure the service in odata2ts.config.ts.
  2. Generate the classes into src/odata/.
  3. Map the raw Q* classes to plain types.
  4. Use the mapped types across the app.

Step 1 โ€” Configure odata2ts.config.ts โ€‹

odata2ts.config.ts (at the app root) lists every OData source under services. Each key is unique and becomes the folder name generated under src/odata/<Key>.

ts
// odata2ts.config.ts
import type { ConfigFileOptions } from '@odata2ts/odata2ts'
import dotenv from 'dotenv'

dotenv.config()

const sourceUrl = `${String(process.env.MONO_VUE_ODATA_BASE_URL)}`

const config: ConfigFileOptions = {
  services: {
    DTO: {
      sourceUrl,                          // the OData root (from an env var)
      source: 'src/odata/DTO/metadata.xml', // local metadata snapshot
      output: 'src/odata/DTO',            // where generated files go
    },
  },
}

export default config
  • DTO โ€” the unique service key โ†’ output folder src/odata/DTO.
  • sourceUrl โ€” the OData base URL. Keep it in an env var (MONO_โ€ฆ, see Environment / Rule 6) โ€” never hardcode it.
  • source โ€” the local metadata.xml snapshot odata2ts reads/writes.
  • output โ€” the target folder for the generated code.

Many OData sources in one config โ€‹

Add another key for a second service โ€” it gets its own folder:

ts
services: {
  DTO: {
    sourceUrl: `${String(process.env.MONO_VUE_ODATA_BASE_URL)}`,
    source: 'src/odata/DTO/metadata.xml',
    output: 'src/odata/DTO',
  },
  Reporting: {                            // โ†’ src/odata/Reporting
    sourceUrl: `${String(process.env.MONO_VUE_REPORTING_ODATA_URL)}`,
    source: 'src/odata/Reporting/metadata.xml',
    output: 'src/odata/Reporting',
  },
},

Step 2 โ€” Generate the classes โ€‹

Run the generation script from package.json:

bash
pnpm odata:gen      # dotenv -e .env.dev odata2ts

It loads .env.dev (so MONO_VUE_ODATA_BASE_URL resolves), reads the metadata, and writes the typed output into each service's output folder. A src/odata/DTO/ folder ends up with:

Generated fileWhat it is
QDefaultThe query-object classes โ€” e.g. QDTO_Brand, QDTO_MasterUser. These are what you map in Step 3.
DefaultModelThe DTO model interfaces + enums.
DefaultServiceOData service/entity-set classes.
metadata.xmlThe metadata snapshot odata2ts read from.

There's also pnpm odata:github for pulling metadata from a GitHub source instead of a live URL.

Generated โ€” never hand-edit

Everything under src/odata/<Key>/ is regenerated on every run. Don't edit it by hand โ€” your changes vanish and you hide the real source (the metadata). This is Rule 4 in Template Rules. To change the types, fix the backend / regenerate.

Step 3 โ€” Map the raw Q* classes to normal types โ€‹

The generated Q* objects describe fields as query paths (QStringPath<string>, QNumberPath<number>, โ€ฆ), not as a plain shape you can put in a ref. EswOdataMapTypes (from esw-utils) unwraps them into a normal type. Do this once per entity in src/types/odata.d.ts:

ts
// src/types/odata.d.ts
import type {
  QDTO_Brand,
  QDTO_MasterUser,
} from '@mono-vue/odata/DTO/QDefault'
import type { EswOdataMapTypes } from 'esw-utils'

export type DTO_BrandTypes = EswOdataMapTypes<typeof QDTO_Brand>
export type DTO_MasterUserTypes = EswOdataMapTypes<typeof QDTO_MasterUser>

// hand-written shapes (e.g. a POST body) still live alongside as plain types
export type BrandPostTypes = {
  Nama: string
  NamaBudget: string
}

EswOdataMapTypes<typeof QDTO_Brand> reads the Q* class and unwraps each path to its real value type:

Generated pathBecomes
QStringPath<string>string
QNumberPath<number>number
QBooleanPath<boolean>boolean
QDateTimeOffsetPath<string>string
QEntityPath<QOther>the nested mapped object
QEntityCollectionPath<QOther>the nested mapped object as an array

So DTO_BrandTypes resolves to { Id: number; Nama: string; NamaBudget: string; Aktif: boolean; DibuatTanggal: string }. It also accepts an optional second generic to override specific fields with another mapped type when you need to.

Import through your app's own alias

The Q* objects are imported via the role-based resolve alias โ€” @mono-vue/... in a Remote, @mono-host/... in the Host. Each alias points at that app's own ./src (see Rule 1 / Rule 2 in Template Rules), so the generated types resolve correctly and stay safe to use across the ecosystem.

Step 4 โ€” Use the mapped types across the app โ€‹

The mapped types are now ordinary TypeScript โ€” use them in store state, as fetch generics, and to constrain table columns.

ts
// src/stores/use-master-user.ts
import type { DTO_MasterUserTypes } from '@mono-vue/types'
import { monoOdataFetch } from 'mono-utils/fetching'

export const useMasterUser = defineStore('master-user', () => {
  const detail = ref<DTO_MasterUserTypes | null>(null)

  async function fetchDetail(id: number) {
    const { data, error } = await monoOdataFetch<DTO_MasterUserTypes>({
      configBaseUrl: 'myOdata',
      url: `/DTO_MasterUser(${id})`,
      type: 'data',
      options: { select: ['Id', 'Username', 'NamaLengkap'], expand: ['Role'] },
    })
    if (!error) detail.value = data
  }

  return { detail, fetchDetail }
})

Because the type is a plain shape, keyof works too โ€” handy for typing table column definitions so a typo'd field name fails at compile time:

ts
// src/datas/tables/user.ts
import type { DTO_MasterUserTypes } from '@mono-vue/types'

export interface UserCol {
  field: keyof DTO_MasterUserTypes | string
  caption: string
  sortable?: boolean
}

const colUser: UserCol[] = [
  { field: 'Username', caption: 'Username', sortable: true },
  { field: 'NamaLengkap', caption: 'Nama Lengkap', sortable: true },
  { field: 'Nik', caption: 'NIK' },
]

Fetching itself still goes through mono-utils/fetching โ€” see Data Fetching and DataSource.

Recap

Generate (odata2ts) โ†’ map (EswOdataMapTypes) โ†’ use. The generated src/odata/<Key>/ folder is read-only (Rule 4); your only hand-written file in this flow is src/types/odata.d.ts. See Template Rules โ†’ Rule 11 for the short version.