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:
- Configure the service in
odata2ts.config.ts. - Generate the classes into
src/odata/. - Map the raw
Q*classes to plain types. - 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 configDTOโ the unique service key โ output foldersrc/odata/DTO.sourceUrlโ the OData base URL. Keep it in an env var (MONO_โฆ, see Environment / Rule 6) โ never hardcode it.sourceโ the localmetadata.xmlsnapshot 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 odata2tsIt 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 file | What it is |
|---|---|
QDefault | The query-object classes โ e.g. QDTO_Brand, QDTO_MasterUser. These are what you map in Step 3. |
DefaultModel | The DTO model interfaces + enums. |
DefaultService | OData service/entity-set classes. |
metadata.xml | The metadata snapshot odata2ts read from. |
There's also
pnpm odata:githubfor 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 path | Becomes |
|---|---|
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.