@mkja/o-data 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,416 +1,444 @@
1
- ## o-data
2
-
3
- **o-data** is a TypeScript‑first OData 4.01 client and schema generator.
4
-
5
- It has two parts:
6
-
7
- - **Runtime library** – a strongly‑typed client for querying and mutating OData services.
8
- - **CLI parser / schema generator** – reads an OData CSDL XML document and generates a typed schema module the runtime can consume.
9
-
10
- ---
11
-
12
- ## Features
13
-
14
- - **Schema‑driven, fully typed client**
15
- - Describe your service once in a TypeScript schema; get strong types for queries, payloads, and responses.
16
- - **Fluent query builder**
17
- - `$select`, `$expand` (with nested options), `$filter`, `$orderby`, `$top`, `$skip`, `$count`.
18
- - `$filter` DSL with navigation, `any` / `all`, enums, dates, and string functions.
19
- - **Navigation‑aware create/update**
20
- - Supports `@odata.bind` for single and collection navigations, deep inserts, and batch references.
21
- - **Actions & functions**
22
- - Bound and unbound operations, with correct URL shapes and parameter serialization.
23
- - **Schema generator from CSDL**
24
- - CLI reads your OData metadata XML and emits a typed `schema({...})` module.
25
- - Powerful include/exclude/masking rules for keeping the generated surface small and relevant.
26
-
27
- ---
28
-
29
- ## Installation
30
-
31
- ```bash
32
- # with bun
33
- bun add o-data
34
-
35
- # or with npm
36
- npm install o-data
37
-
38
- # or with pnpm
39
- pnpm add o-data
40
-
41
- # or with yarn
42
- yarn add o-data
43
- ```
44
-
45
- The runtime expects a `fetch`‑compatible environment (`Request`, `Response`, `Headers`); it works in modern Node (with `fetch`) and browsers.
46
-
47
- ---
48
-
49
- ## Runtime: Getting started
50
-
51
- ### 1. Define or generate a schema
52
-
53
- You can either write a schema by hand:
54
-
55
- ```ts
56
- // schema.ts
57
- import { schema } from "o-data/schema";
58
-
59
- export const crmSchema = schema({
60
- namespace: "Microsoft.Dynamics.CRM",
61
- alias: "mscrm",
62
- enumtypes: {
63
- IncidentStatus: {
64
- isFlags: false,
65
- members: {
66
- Active: 0,
67
- Resolved: 1,
68
- Cancelled: 2,
69
- },
70
- },
71
- },
72
- entitytypes: {
73
- Incident: {
74
- properties: {
75
- id: { type: "Edm.Guid" },
76
- title: { type: "Edm.String" },
77
- status: { type: "enum", target: "IncidentStatus" },
78
- },
79
- },
80
- },
81
- entitysets: {
82
- incidents: { entitytype: "Incident" },
83
- },
84
- });
85
- ```
86
-
87
- …or generate one from a CSDL XML using the CLI (see **Schema generator (CLI)** below).
88
-
89
- ### 2. Create an `OdataClient`
90
-
91
- ```ts
92
- // client.ts
93
- import { OdataClient } from "o-data";
94
- import { crmSchema } from "./schema"; // or generated-o-data-schema
95
-
96
- const client = new OdataClient(crmSchema, {
97
- baseUrl: "https://example.com/api/data/v9.0/",
98
- transport: fetch, // any (req: Request) => Promise<Response>
99
- });
100
- ```
101
-
102
- ---
103
-
104
- ## Querying data
105
-
106
- ### Collection queries
107
-
108
- ```ts
109
- // GET /incidents?$select=title,status&$top=10&$orderby=title asc
110
- const response = await client.entitysets("incidents").query({
111
- select: ["title", "status"],
112
- top: 10,
113
- orderby: ["title", "asc"],
114
- });
115
-
116
- if (response.ok) {
117
- const incidents = response.result.data; // typed by schema + query
118
- }
119
- ```
120
-
121
- ### Expands and nested options
122
-
123
- ```ts
124
- // GET /incidents?$expand=incident_contact($select=name,email)
125
- const res = await client.entitysets("incidents").query({
126
- expand: {
127
- incident_contact: {
128
- select: ["name", "email"],
129
- },
130
- },
131
- });
132
- ```
133
-
134
- ### Filter builder
135
-
136
- Filters use a small builder DSL that respects your schema:
137
-
138
- ```ts
139
- // GET /incidents?$filter=status eq Namespace.IncidentStatus'Active'
140
- const res = await client.entitysets("incidents").query({
141
- filter: (h) => h.clause("status", "eq", "Active"),
142
- });
143
-
144
- // Navigation + logical operators
145
- const res2 = await client.entitysets("incidents").query({
146
- filter: (h) =>
147
- h
148
- .clause("title", "contains", "Support")
149
- .and(
150
- h.nav("incident_contact", (nh) =>
151
- nh.clause("email", "eq", "user@example.com"),
152
- ),
153
- ),
154
- });
155
- ```
156
-
157
- Supported operators include `eq`, `ne`, `gt`, `ge`, `lt`, `le`, `in`, `contains`, `startswith`, `endswith`.
158
- For enums, you can pass either the member name (`"Active"`) or the underlying numeric value (`1`); they are serialized as FQN enum literals.
159
-
160
- ### Single‑entity queries and navigation
161
-
162
- ```ts
163
- // GET /incidents(guid-123)?$select=title
164
- const incident = await client
165
- .entitysets("incidents")
166
- .key("guid-123")
167
- .query({ select: ["title"] });
168
-
169
- // GET /incidents(guid-123)/incident_contact
170
- const contact = await client
171
- .entitysets("incidents")
172
- .key("guid-123")
173
- .navigate("incident_contact")
174
- .query({});
175
- ```
176
-
177
- ---
178
-
179
- ## Creating and updating entities
180
-
181
- The library infers create/update shapes from your schema and takes care of `@odata.bind` and deep inserts.
182
-
183
- ### Create
184
-
185
- ```ts
186
- // Basic create
187
- const created = await client.entitysets("incidents").create({
188
- title: "New incident",
189
- description: "Description",
190
- });
191
-
192
- // Create with navigation bind (single‑valued)
193
- await client.entitysets("incidents").create({
194
- title: "Linked to contact",
195
- incident_contact: "guid-contact-id", // → "incident_contact@odata.bind": "/contacts(guid-contact-id)"
196
- });
197
-
198
- // Create with collection navigation bind
199
- await client.entitysets("contacts").create({
200
- name: "John",
201
- contact_incidents: ["incident-id-1", "incident-id-2"],
202
- // "contact_incidents@odata.bind": ["/incidents(incident-id-1)", "/incidents(incident-id-2)"]
203
- });
204
-
205
- // Deep insert
206
- await client.entitysets("incidents").create({
207
- title: "Deep insert example",
208
- incident_contact: {
209
- name: "Nested contact",
210
- email: "nested@example.com",
211
- },
212
- });
213
- ```
214
-
215
- You can control response shape via options:
216
-
217
- ```ts
218
- await client.entitysets("incidents").create(
219
- { title: "Return representation" },
220
- {
221
- select: ["title", "description"],
222
- prefer: { return_representation: true },
223
- },
224
- );
225
- ```
226
-
227
- ### Update
228
-
229
- ```ts
230
- // Simple PATCH
231
- await client.entitysets("incidents").key("guid-123").update({
232
- title: "Updated title",
233
- });
234
-
235
- // Repoint single navigation
236
- await client.entitysets("incidents").key("guid-123").update({
237
- incident_contact: "guid-new-contact",
238
- });
239
-
240
- // Collection navigation operations
241
- await client
242
- .entitysets("contacts")
243
- .key("guid-contact")
244
- .update({
245
- contact_incidents: {
246
- add: ["incident-id-3"],
247
- remove: ["incident-id-1"],
248
- },
249
- });
250
- ```
251
-
252
- Options for update mirror create: `select`, `prefer.return_representation`, custom headers.
253
-
254
- ---
255
-
256
- ## Actions and functions
257
-
258
- ### Bound actions
259
-
260
- ```ts
261
- // POST /incidents(guid-123)/Namespace.assignIncident
262
- const res = await client
263
- .entitysets("incidents")
264
- .key("guid-123")
265
- .action("assignIncident", {
266
- parameters: {
267
- assigneeId: "guid-user",
268
- priority: 1,
269
- },
270
- });
271
-
272
- if (res.ok) {
273
- const ok: boolean = res.result.data; // mapped from Edm.Boolean
274
- }
275
- ```
276
-
277
- ### Unbound actions (via imports)
278
-
279
- ```ts
280
- // POST /BulkCreate
281
- const res = await client.action("BulkCreate", {
282
- parameters: {
283
- entities: ["1", "2", "3"],
284
- },
285
- });
286
- ```
287
-
288
- ### Bound functions
289
-
290
- ```ts
291
- // GET /incidents(guid-123)/Namespace.getRelatedCount(relationType=@relationType)?@relationType='contact'
292
- const res = await client
293
- .entitysets("incidents")
294
- .key("guid-123")
295
- .function("getRelatedCount", {
296
- parameters: { relationType: "contact" },
297
- });
298
-
299
- if (res.ok) {
300
- const count: number = res.result.data;
301
- }
302
- ```
303
-
304
- ### Unbound functions (via imports)
305
-
306
- ```ts
307
- // GET /Search(query=@query,entityTypes=@entityTypes)?@query='test'&@entityTypes=...
308
- const res = await client.function("Search", {
309
- parameters: {
310
- query: "test",
311
- entityTypes: ["Incident", "Contact"],
312
- },
313
- });
314
- ```
315
-
316
- For navigation‑typed parameters (actions/functions), you can use the same patterns as for create/update: IDs, `[entityset, id]`, deep insert objects, or arrays thereof; the library converts them to `@odata.bind` or nested objects as needed.
317
-
318
- ---
319
-
320
- ## Schema generator (CLI)
321
-
322
- The CLI reads an OData CSDL XML document and generates a strongly‑typed schema module that plugs into the runtime.
323
-
324
- ### 1. Create a config file
325
-
326
- Create `odata-parser.config.js` (or `.ts`) in your project root:
327
-
328
- ```ts
329
- // odata-parser.config.ts
330
- import { defineConfig } from "o-data/parser";
331
-
332
- export default defineConfig({
333
- inputPath: "./metadata.xml",
334
- outputPath: "./src/schema",
335
- wantedEntities: "ALL", // or ['incidents', 'contacts']
336
- wantedUnboundActions: "ALL",
337
- wantedUnboundFunctions: "ALL",
338
- excludeFilters: {
339
- entities: [/^msdyn_/], // drop system sets
340
- properties: [/^adx_/], // drop noisy props
341
- },
342
- selectionMode: "additive", // or 'only' for strict whitelists
343
- // onlyEntities, onlyBoundActions, onlyUnboundActions, mask, ... are available
344
- });
345
- ```
346
-
347
- Running the generator will produce e.g. `src/schema/generated-o-data-schema.ts` that looks like:
348
-
349
- ```ts
350
- import { schema } from "o-data/schema";
351
-
352
- export const myservice_schema = schema({
353
- namespace: "My.Service",
354
- alias: "ms",
355
- enumtypes: { /* ... */ },
356
- complextypes: { /* ... */ },
357
- entitytypes: { /* ... */ },
358
- entitysets: { /* ... */ },
359
- actions: { /* ... */ },
360
- functions: { /* ... */ },
361
- actionImports: { /* ... */ },
362
- functionImports: { /* ... */ },
363
- });
364
- ```
365
-
366
- ### 2. Run the CLI
367
-
368
- From your project root:
369
-
370
- ```bash
371
- # using the global CLI name exposed by this package
372
- npx o-data path/to/odata-parser.config.js
373
-
374
- # or (when installed locally in a Node/Bun project)
375
- bun x o-data path/to/odata-parser.config.js
376
- ```
377
-
378
- If you omit the path, the CLI looks for `odata-parser.config.js` (and then `.ts`) in the current working directory.
379
-
380
- Then in your code:
381
-
382
- ```ts
383
- import { myservice_schema } from "./schema/generated-o-data-schema";
384
- import { OdataClient } from "o-data";
385
-
386
- const client = new OdataClient(myservice_schema, {
387
- baseUrl: "https://example.com/odata/",
388
- transport: fetch,
389
- });
390
- ```
391
-
392
- ---
393
-
394
- ## Status and limitations
395
-
396
- - The library is still **early (0.0.x)**; APIs may change.
397
- - Some operations are marked `TODO` in the runtime (e.g. delete support, collection‑bound actions/functions implementation, richer pagination).
398
- - The generator doesn’t yet handle OData operation overloading beyond keeping the first operation per name.
399
-
400
- ---
401
-
402
- ## Development
403
-
404
- - **Build**
405
-
406
- ```bash
407
- bun x tsc -p tsconfig.build.json
408
- ```
409
-
410
- - **Tests**
411
-
412
- ```bash
413
- bun test
414
- ```
415
-
416
- ---
1
+ ## o-data
2
+
3
+ **o-data** is a TypeScript‑first OData 4.01 client and schema generator.
4
+
5
+ It has two parts:
6
+
7
+ - **Runtime library** – a strongly‑typed client for querying and mutating OData services.
8
+ - **CLI parser / schema generator** – reads an OData CSDL XML document and generates a typed schema module the runtime can consume.
9
+
10
+ ---
11
+
12
+ ## Features
13
+
14
+ - **Schema‑driven, fully typed client**
15
+ - Describe your service once in a TypeScript schema; get strong types for queries, payloads, and responses.
16
+ - **Fluent query builder**
17
+ - `$select`, `$expand` (with nested options), `$filter`, `$orderby`, `$top`, `$skip`, `$count`.
18
+ - `$filter` DSL with navigation, `any` / `all`, enums, dates, and string functions.
19
+ - **Navigation‑aware create/update**
20
+ - Supports `@odata.bind` for single and collection navigations, deep inserts, and batch references.
21
+ - **Actions & functions**
22
+ - Bound and unbound operations, with correct URL shapes and parameter serialization.
23
+ - **Batch requests ($batch)**
24
+ - Same fluent API as the client; queue operations and send them in a single multipart request. GET/query/function outside changesets; create/update/delete/action inside changesets.
25
+ - **Schema generator from CSDL**
26
+ - CLI reads your OData metadata XML and emits a typed `schema({...})` module.
27
+ - Powerful include/exclude/masking rules for keeping the generated surface small and relevant.
28
+
29
+ ---
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ # with bun
35
+ bun add o-data
36
+
37
+ # or with npm
38
+ npm install o-data
39
+
40
+ # or with pnpm
41
+ pnpm add o-data
42
+
43
+ # or with yarn
44
+ yarn add o-data
45
+ ```
46
+
47
+ The runtime expects a `fetch`‑compatible environment (`Request`, `Response`, `Headers`); it works in modern Node (with `fetch`) and browsers.
48
+
49
+ ---
50
+
51
+ ## Runtime: Getting started
52
+
53
+ ### 1. Define or generate a schema
54
+
55
+ You can either write a schema by hand:
56
+
57
+ ```ts
58
+ // schema.ts
59
+ import { schema } from "o-data/schema";
60
+
61
+ export const crmSchema = schema({
62
+ namespace: "Microsoft.Dynamics.CRM",
63
+ alias: "mscrm",
64
+ enumtypes: {
65
+ IncidentStatus: {
66
+ isFlags: false,
67
+ members: {
68
+ Active: 0,
69
+ Resolved: 1,
70
+ Cancelled: 2,
71
+ },
72
+ },
73
+ },
74
+ entitytypes: {
75
+ Incident: {
76
+ properties: {
77
+ id: { type: "Edm.Guid" },
78
+ title: { type: "Edm.String" },
79
+ status: { type: "enum", target: "IncidentStatus" },
80
+ },
81
+ },
82
+ },
83
+ entitysets: {
84
+ incidents: { entitytype: "Incident" },
85
+ },
86
+ });
87
+ ```
88
+
89
+ …or generate one from a CSDL XML using the CLI (see **Schema generator (CLI)** below).
90
+
91
+ ### 2. Create an `OdataClient`
92
+
93
+ ```ts
94
+ // client.ts
95
+ import { OdataClient } from "o-data";
96
+ import { crmSchema } from "./schema"; // or generated-o-data-schema
97
+
98
+ const client = new OdataClient(crmSchema, {
99
+ baseUrl: "https://example.com/api/data/v9.0/",
100
+ transport: fetch, // any (req: Request) => Promise<Response>
101
+ });
102
+ ```
103
+
104
+ ---
105
+
106
+ ## Querying data
107
+
108
+ OData response shapes: **collections** use `result.value`; **single-entity, CRUD, actions, and functions** use a flat `result` (data at top level alongside `@odata.*` metadata).
109
+
110
+ ### Collection queries
111
+
112
+ ```ts
113
+ // GET /incidents?$select=title,status&$top=10&$orderby=title asc
114
+ const response = await client.entitysets("incidents").query({
115
+ select: ["title", "status"],
116
+ top: 10,
117
+ orderby: ["title", "asc"],
118
+ });
119
+
120
+ if (response.ok) {
121
+ const incidents = response.result.value; // typed by schema + query
122
+ }
123
+ ```
124
+
125
+ ### Expands and nested options
126
+
127
+ ```ts
128
+ // GET /incidents?$expand=incident_contact($select=name,email)
129
+ const res = await client.entitysets("incidents").query({
130
+ expand: {
131
+ incident_contact: {
132
+ select: ["name", "email"],
133
+ },
134
+ },
135
+ });
136
+ ```
137
+
138
+ ### Filter builder
139
+
140
+ Filters use a small builder DSL that respects your schema:
141
+
142
+ ```ts
143
+ // GET /incidents?$filter=status eq Namespace.IncidentStatus'Active'
144
+ const res = await client.entitysets("incidents").query({
145
+ filter: (h) => h.clause("status", "eq", "Active"),
146
+ });
147
+
148
+ // Navigation + logical operators
149
+ const res2 = await client.entitysets("incidents").query({
150
+ filter: (h) =>
151
+ h
152
+ .clause("title", "contains", "Support")
153
+ .and(
154
+ h.nav("incident_contact", (nh) =>
155
+ nh.clause("email", "eq", "user@example.com"),
156
+ ),
157
+ ),
158
+ });
159
+ ```
160
+
161
+ Supported operators include `eq`, `ne`, `gt`, `ge`, `lt`, `le`, `in`, `contains`, `startswith`, `endswith`.
162
+ For enums, you can pass either the member name (`"Active"`) or the underlying numeric value (`1`); they are serialized as FQN enum literals.
163
+
164
+ ### Single‑entity queries and navigation
165
+
166
+ ```ts
167
+ // GET /incidents(guid-123)?$select=title
168
+ const incident = await client
169
+ .entitysets("incidents")
170
+ .key("guid-123")
171
+ .query({ select: ["title"] });
172
+
173
+ // GET /incidents(guid-123)/incident_contact
174
+ const contact = await client
175
+ .entitysets("incidents")
176
+ .key("guid-123")
177
+ .navigate("incident_contact")
178
+ .query({});
179
+ ```
180
+
181
+ ---
182
+
183
+ ## Creating and updating entities
184
+
185
+ The library infers create/update shapes from your schema and takes care of `@odata.bind` and deep inserts.
186
+
187
+ ### Create
188
+
189
+ ```ts
190
+ // Basic create
191
+ const created = await client.entitysets("incidents").create({
192
+ title: "New incident",
193
+ description: "Description",
194
+ });
195
+
196
+ // Create with navigation bind (single‑valued)
197
+ await client.entitysets("incidents").create({
198
+ title: "Linked to contact",
199
+ incident_contact: "guid-contact-id", // → "incident_contact@odata.bind": "/contacts(guid-contact-id)"
200
+ });
201
+
202
+ // Create with collection navigation bind
203
+ await client.entitysets("contacts").create({
204
+ name: "John",
205
+ contact_incidents: ["incident-id-1", "incident-id-2"],
206
+ // → "contact_incidents@odata.bind": ["/incidents(incident-id-1)", "/incidents(incident-id-2)"]
207
+ });
208
+
209
+ // Deep insert
210
+ await client.entitysets("incidents").create({
211
+ title: "Deep insert example",
212
+ incident_contact: {
213
+ name: "Nested contact",
214
+ email: "nested@example.com",
215
+ },
216
+ });
217
+ ```
218
+
219
+ You can control response shape via options:
220
+
221
+ ```ts
222
+ await client.entitysets("incidents").create(
223
+ { title: "Return representation" },
224
+ {
225
+ select: ["title", "description"],
226
+ prefer: { return_representation: true },
227
+ },
228
+ );
229
+ ```
230
+
231
+ ### Update
232
+
233
+ ```ts
234
+ // Simple PATCH
235
+ await client.entitysets("incidents").key("guid-123").update({
236
+ title: "Updated title",
237
+ });
238
+
239
+ // Repoint single navigation
240
+ await client.entitysets("incidents").key("guid-123").update({
241
+ incident_contact: "guid-new-contact",
242
+ });
243
+
244
+ // Collection navigation operations
245
+ await client
246
+ .entitysets("contacts")
247
+ .key("guid-contact")
248
+ .update({
249
+ contact_incidents: {
250
+ add: ["incident-id-3"],
251
+ remove: ["incident-id-1"],
252
+ },
253
+ });
254
+ ```
255
+
256
+ Options for update mirror create: `select`, `prefer.return_representation`, custom headers.
257
+
258
+ ---
259
+
260
+ ## Batch requests
261
+
262
+ Use `client.batch()` to build a `$batch` request with the same fluent API. Operations are queued and sent in a single multipart request:
263
+
264
+ - **GET, query, function** – outside changesets (read-only)
265
+ - **Create, update, delete, action** – inside changesets (atomic)
266
+
267
+ ```ts
268
+ const batch = client.batch();
269
+
270
+ batch.entitysets("incidents").query({ select: ["title"], top: 10 });
271
+ batch.entitysets("incidents").create({ title: "New" });
272
+ batch.entitysets("incidents").key("guid-123").update({ title: "Updated" });
273
+ batch.entitysets("incidents").key("guid-456").delete();
274
+
275
+ const response = await batch.execute();
276
+ ```
277
+
278
+ `batch.execute()` returns the raw multipart `Response`; parsing individual operation responses is the application's responsibility. Use `batch.buildRequest()` to obtain the `Request` without sending it.
279
+
280
+ You can also use `.navigate(...)`, bound and unbound actions, and functions within a batch with the same API as the client.
281
+
282
+ ---
283
+
284
+ ## Actions and functions
285
+
286
+ ### Bound actions
287
+
288
+ ```ts
289
+ // POST /incidents(guid-123)/Namespace.assignIncident
290
+ const res = await client
291
+ .entitysets("incidents")
292
+ .key("guid-123")
293
+ .action("assignIncident", {
294
+ parameters: {
295
+ assigneeId: "guid-user",
296
+ priority: 1,
297
+ },
298
+ });
299
+
300
+ if (res.ok) {
301
+ const ok: boolean = res.result.value; // mapped from Edm.Boolean (flat at top level)
302
+ }
303
+ ```
304
+
305
+ ### Unbound actions (via imports)
306
+
307
+ ```ts
308
+ // POST /BulkCreate
309
+ const res = await client.action("BulkCreate", {
310
+ parameters: {
311
+ entities: ["1", "2", "3"],
312
+ },
313
+ });
314
+ ```
315
+
316
+ ### Bound functions
317
+
318
+ ```ts
319
+ // GET /incidents(guid-123)/Namespace.getRelatedCount(relationType=@relationType)?@relationType='contact'
320
+ const res = await client
321
+ .entitysets("incidents")
322
+ .key("guid-123")
323
+ .function("getRelatedCount", {
324
+ parameters: { relationType: "contact" },
325
+ });
326
+
327
+ if (res.ok) {
328
+ const count: number = res.result.value; // flat at top level
329
+ }
330
+ ```
331
+
332
+ ### Unbound functions (via imports)
333
+
334
+ ```ts
335
+ // GET /Search(query=@query,entityTypes=@entityTypes)?@query='test'&@entityTypes=...
336
+ const res = await client.function("Search", {
337
+ parameters: {
338
+ query: "test",
339
+ entityTypes: ["Incident", "Contact"],
340
+ },
341
+ });
342
+ ```
343
+
344
+ For navigation‑typed parameters (actions/functions), you can use the same patterns as for create/update: IDs, `[entityset, id]`, deep insert objects, or arrays thereof; the library converts them to `@odata.bind` or nested objects as needed.
345
+
346
+ ---
347
+
348
+ ## Schema generator (CLI)
349
+
350
+ The CLI reads an OData CSDL XML document and generates a strongly‑typed schema module that plugs into the runtime.
351
+
352
+ ### 1. Create a config file
353
+
354
+ Create `odata-parser.config.js` (or `.ts`) in your project root:
355
+
356
+ ```ts
357
+ // odata-parser.config.ts
358
+ import { defineConfig } from "o-data/parser";
359
+
360
+ export default defineConfig({
361
+ inputPath: "./metadata.xml",
362
+ outputPath: "./src/schema",
363
+ wantedEntities: "ALL", // or ['incidents', 'contacts']
364
+ wantedUnboundActions: "ALL",
365
+ wantedUnboundFunctions: "ALL",
366
+ excludeFilters: {
367
+ entities: [/^msdyn_/], // drop system sets
368
+ properties: [/^adx_/], // drop noisy props
369
+ },
370
+ selectionMode: "additive", // or 'only' for strict whitelists
371
+ // onlyEntities, onlyBoundActions, onlyUnboundActions, mask, ... are available
372
+ });
373
+ ```
374
+
375
+ Running the generator will produce e.g. `src/schema/generated-o-data-schema.ts` that looks like:
376
+
377
+ ```ts
378
+ import { schema } from "o-data/schema";
379
+
380
+ export const myservice_schema = schema({
381
+ namespace: "My.Service",
382
+ alias: "ms",
383
+ enumtypes: { /* ... */ },
384
+ complextypes: { /* ... */ },
385
+ entitytypes: { /* ... */ },
386
+ entitysets: { /* ... */ },
387
+ actions: { /* ... */ },
388
+ functions: { /* ... */ },
389
+ actionImports: { /* ... */ },
390
+ functionImports: { /* ... */ },
391
+ });
392
+ ```
393
+
394
+ ### 2. Run the CLI
395
+
396
+ From your project root:
397
+
398
+ ```bash
399
+ # using the global CLI name exposed by this package
400
+ npx o-data path/to/odata-parser.config.js
401
+
402
+ # or (when installed locally in a Node/Bun project)
403
+ bun x o-data path/to/odata-parser.config.js
404
+ ```
405
+
406
+ If you omit the path, the CLI looks for `odata-parser.config.js` (and then `.ts`) in the current working directory.
407
+
408
+ Then in your code:
409
+
410
+ ```ts
411
+ import { myservice_schema } from "./schema/generated-o-data-schema";
412
+ import { OdataClient } from "o-data";
413
+
414
+ const client = new OdataClient(myservice_schema, {
415
+ baseUrl: "https://example.com/odata/",
416
+ transport: fetch,
417
+ });
418
+ ```
419
+
420
+ ---
421
+
422
+ ## Status and limitations
423
+
424
+ - The library is still **early (0.0.x)**; APIs may change.
425
+ - Some operations are marked `TODO` in the runtime (e.g. delete support outside batch, collection‑bound actions/functions implementation, richer pagination).
426
+ - The generator doesn’t yet handle OData operation overloading beyond keeping the first operation per name.
427
+
428
+ ---
429
+
430
+ ## Development
431
+
432
+ - **Build**
433
+
434
+ ```bash
435
+ bun x tsc -p tsconfig.build.json
436
+ ```
437
+
438
+ - **Tests**
439
+
440
+ ```bash
441
+ bun test
442
+ ```
443
+
444
+ ---