angular-odata 0.160.0 → 0.162.0

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,16 +1,18 @@
1
1
  <h1 align="center">Angular OData</h1>
2
2
 
3
3
  <p align="center">
4
- <em>A fluent API for querying, creating, updating and deleting OData resources in Angular.</em>
4
+ <em>A typed, fluent client for OData services in Angular.</em>
5
5
  <br>
6
6
  </p>
7
7
 
8
8
  <p align="center">
9
- <a href="CONTRIBUTING.md">Contributing</a>
9
+ <a href="https://diegomvh.github.io/angular-odata/docs/api/additional-documentation/introduction.html">Handbook</a>
10
10
  ·
11
- <a href="./docs/api/index.html">Documentation</a>
11
+ <a href="https://diegomvh.github.io/angular-odata/docs/api/index.html">API reference</a>
12
12
  ·
13
13
  <a href="https://github.com/diegomvh/AngularODataEntity">Demo</a>
14
+ ·
15
+ <a href="https://github.com/diegomvh/angular-odata/blob/main/CONTRIBUTING.md">Contributing</a>
14
16
  <br>
15
17
  <br>
16
18
  </p>
@@ -26,14 +28,170 @@
26
28
 
27
29
  <hr>
28
30
 
29
- ## Installation
31
+ [OData](https://www.odata.org/) is a standard for REST APIs: every service describes its
32
+ data in a `$metadata` document and accepts the same query options (`$filter`, `$select`,
33
+ `$expand`, `$orderby`, …). **angular-odata** lets your Angular application talk to any OData
34
+ service without writing URLs by hand: you build requests with typed TypeScript code and get
35
+ back typed results.
36
+
37
+ ```typescript
38
+ // GET People?$select=UserName,FirstName&$filter=contains(UserName, 'russell')&$top=10
39
+ client
40
+ .entitySet<Person>('People')
41
+ .query((q) => {
42
+ q.filter(({ e, t }) => e().contains(t.UserName, 'russell'));
43
+ q.select(['UserName', 'FirstName']);
44
+ q.top(10);
45
+ })
46
+ .fetch()
47
+ .subscribe(({ entities }) => console.log(entities));
48
+ ```
49
+
50
+ ## Features
51
+
52
+ - **Typed query builder**: `$filter`, `$select`, `$expand`, `$orderby`, `$search`,
53
+ `$compute`, `$apply` and paging, with field names checked by the compiler.
54
+ - **Complete OData API**: entity sets, entities, navigation properties, functions,
55
+ actions, singletons, media and `$batch` requests.
56
+ - **Code generation**: one command creates interfaces, services and models from your
57
+ service's `$metadata`.
58
+ - **Models and collections**: change tracking, validation and `save()` / `destroy()`.
59
+ - **Caching**: in-memory, Web Storage or IndexedDB, with fetch policies such as
60
+ `cache-first` and `cache-and-network`.
61
+ - **Several APIs** in the same application, each with its own configuration.
30
62
 
31
- Install from npm:
63
+ ## Quick start
64
+
65
+ ### 1. Install
32
66
 
33
67
  ```bash
34
68
  npm i angular-odata
35
69
  ```
36
70
 
37
- ## OData Version
71
+ ### 2. Register the client
72
+
73
+ ```typescript
74
+ // app.config.ts
75
+ import { ApplicationConfig } from '@angular/core';
76
+ import { provideODataClient } from 'angular-odata';
77
+
78
+ export const appConfig: ApplicationConfig = {
79
+ providers: [
80
+ provideODataClient({
81
+ config: { serviceRootUrl: 'https://services.odata.org/V4/TripPinServiceRW/' },
82
+ }),
83
+ ],
84
+ };
85
+ ```
86
+
87
+ Applications that use NgModules can import `ODataModule.forRoot({ config })` instead.
88
+
89
+ ### 3. Query your service
90
+
91
+ ```typescript
92
+ import { Component, inject, signal } from '@angular/core';
93
+ import { ODataClient } from 'angular-odata';
94
+
95
+ interface Person {
96
+ UserName: string;
97
+ FirstName: string;
98
+ LastName: string;
99
+ }
100
+
101
+ @Component({
102
+ selector: 'app-people',
103
+ template: `
104
+ @for (person of people(); track person.UserName) {
105
+ <p>{{ person.FirstName }} {{ person.LastName }}</p>
106
+ }
107
+ `,
108
+ })
109
+ export class PeopleComponent {
110
+ private client = inject(ODataClient);
111
+ people = signal<Person[]>([]);
112
+
113
+ ngOnInit() {
114
+ this.client
115
+ .entitySet<Person>('People')
116
+ .query((q) => {
117
+ q.orderBy('LastName');
118
+ q.top(10);
119
+ })
120
+ .fetch()
121
+ .subscribe(({ entities }) => this.people.set(entities ?? []));
122
+ }
123
+ }
124
+ ```
125
+
126
+ ### 4. Optional: generate a typed API
127
+
128
+ Instead of writing interfaces and services by hand, generate them from the `$metadata` of
129
+ your service:
130
+
131
+ ```bash
132
+ ng generate angular-odata:apigen --name=TripPin --metadata='https://services.odata.org/V4/TripPinServiceRW/$metadata'
133
+ ```
134
+
135
+ ```typescript
136
+ import { PeopleService, TripPinConfig } from './trip-pin';
137
+
138
+ // app.config.ts
139
+ provideODataClient({ config: TripPinConfig });
140
+
141
+ // Any component or service
142
+ const people = inject(PeopleService);
143
+ people
144
+ .entity('russellwhyte')
145
+ .fetch()
146
+ .subscribe(({ entity }) => console.log(entity));
147
+ ```
148
+
149
+ ## A few more examples
150
+
151
+ ```typescript
152
+ const people = client.entitySet<Person>('People');
153
+
154
+ // Create, update and delete
155
+ people.create({ UserName: 'jdoe', FirstName: 'John', LastName: 'Doe' }).subscribe();
156
+ people.entity('jdoe').modify({ FirstName: 'Johnny' }).subscribe();
157
+ people.entity('jdoe').destroy().subscribe();
158
+
159
+ // Expand related entities: $expand=Trips($select=Name,Budget)
160
+ people
161
+ .entity('russellwhyte')
162
+ .query((q) =>
163
+ q.expand(({ e, t }) =>
164
+ e().field(t.Trips, (f) => f.select(({ e, t }) => e().field(t.Name).field(t.Budget))),
165
+ ),
166
+ )
167
+ .fetch()
168
+ .subscribe(({ entity }) => console.log(entity?.Trips));
169
+
170
+ // Follow the server's next links and get every entity
171
+ people.fetchAll().subscribe(({ entities }) => console.log(entities.length));
172
+ ```
173
+
174
+ ## Documentation
175
+
176
+ - **[Handbook](https://diegomvh.github.io/angular-odata/docs/api/additional-documentation/introduction.html)**:
177
+ guides for configuration, resources, the query builder, services, models, caching and code
178
+ generation.
179
+ - **[API reference](https://diegomvh.github.io/angular-odata/docs/api/index.html)**: every
180
+ class and method, generated from the source code.
181
+ - **[Demo application](https://github.com/diegomvh/AngularODataEntity)**: a complete Angular
182
+ application that uses the TripPin and Northwind sample services.
183
+
184
+ ## Compatibility
185
+
186
+ - **Angular**: 20 or later.
187
+ - **OData**: version 4 is fully supported; versions 3 and 2 have basic support.
188
+
189
+ ## Contributing
190
+
191
+ Bug reports and pull requests are welcome. Read the
192
+ [contributing guide](https://github.com/diegomvh/angular-odata/blob/main/CONTRIBUTING.md)
193
+ to get started.
194
+
195
+ ## License
38
196
 
39
- The library works mainly with OData Version 4, however, it incorporates basic support for versions 3 and 2.
197
+ [MIT](https://github.com/diegomvh/angular-odata/blob/main/LICENSE)
@@ -1593,7 +1593,7 @@ const Objects = {
1593
1593
  },
1594
1594
  resolveKey(key, { single = true } = {}) {
1595
1595
  const type = Types.rawType(key);
1596
- if (['number', 'string'].indexOf(type) !== -1)
1596
+ if (['Number', 'String'].indexOf(type) !== -1)
1597
1597
  return key;
1598
1598
  if (type !== 'Map' && type !== 'Object') {
1599
1599
  return undefined;
@@ -2153,9 +2153,9 @@ class ODataEnumTypeParser extends ODataAnnotatable {
2153
2153
  }
2154
2154
  return schema;
2155
2155
  }
2156
- validate(value, { method, navigation = false, } = {}) {
2156
+ validate(value, { method, navigation = false, }) {
2157
2157
  if (this.flags) {
2158
- let fields = this.fields(value);
2158
+ const fields = this.fields(value);
2159
2159
  return value && fields.length === 0 ? ['mismatch'] : undefined;
2160
2160
  }
2161
2161
  else {
@@ -2195,6 +2195,7 @@ class ODataStructuredTypeFieldParser extends ODataAnnotatable {
2195
2195
  navigation;
2196
2196
  nullable;
2197
2197
  default;
2198
+ optimisticConcurrency = false;
2198
2199
  maxLength;
2199
2200
  precision;
2200
2201
  scale;
@@ -2215,7 +2216,7 @@ class ODataStructuredTypeFieldParser extends ODataAnnotatable {
2215
2216
  this.precision = field.precision;
2216
2217
  this.scale = field.scale;
2217
2218
  }
2218
- validate(value, { method, navigation = false, } = {}) {
2219
+ validate(value, { method, navigation = false, }) {
2219
2220
  let errors;
2220
2221
  if (this.collection && Array.isArray(value)) {
2221
2222
  errors = value.map((v) => this.validate(v, { method, navigation }));
@@ -2231,12 +2232,8 @@ class ODataStructuredTypeFieldParser extends ODataAnnotatable {
2231
2232
  }
2232
2233
  else {
2233
2234
  // IsEdmType
2234
- const computed = this.annotatedValue(COMPUTED);
2235
2235
  errors = [];
2236
- if (!this.nullable &&
2237
- (value === null || (value === undefined && method !== 'modify')) && // Is null or undefined without patch?
2238
- !(computed && method === 'create') // Not (Is Computed field and create) ?
2239
- ) {
2236
+ if (method !== 'modify' && value == null && !(this.nullable || this.isComputed())) {
2240
2237
  errors.push(`required`);
2241
2238
  }
2242
2239
  if (this.maxLength !== undefined &&
@@ -2304,12 +2301,13 @@ class ODataStructuredTypeFieldParser extends ODataAnnotatable {
2304
2301
  });
2305
2302
  }
2306
2303
  //#endregion
2307
- configure({ options, parserForType, }) {
2304
+ configure({ options, optimisticConcurrency, parserForType, }) {
2308
2305
  this.parserOptions = options;
2309
2306
  this.parser = parserForType(this.type);
2310
- if (this.default !== undefined) {
2307
+ if (optimisticConcurrency !== undefined)
2308
+ this.optimisticConcurrency = optimisticConcurrency;
2309
+ if (this.default !== undefined)
2311
2310
  this.default = this.deserialize(this.default, options);
2312
- }
2313
2311
  }
2314
2312
  //#region Json Schema
2315
2313
  // https://json-schema.org/
@@ -2386,6 +2384,12 @@ class ODataStructuredTypeFieldParser extends ODataAnnotatable {
2386
2384
  isKey() {
2387
2385
  return this.structured.keys({ include_parents: true }).some((k) => k.name === this.name);
2388
2386
  }
2387
+ isComputed() {
2388
+ return this.annotatedValue(COMPUTED);
2389
+ }
2390
+ isOptimisticConcurrency() {
2391
+ return this.optimisticConcurrency;
2392
+ }
2389
2393
  hasReferentials() {
2390
2394
  return this.referentials.length !== 0;
2391
2395
  }
@@ -2518,14 +2522,18 @@ class ODataStructuredTypeParser extends ODataAnnotatable {
2518
2522
  const parserOptions = { ...this.parserOptions, ...options };
2519
2523
  return raw(JSON.stringify(this.serialize(value, parserOptions)));
2520
2524
  }
2521
- configure({ options, parserForType, }) {
2525
+ configure({ options, entitySet, parserForType, }) {
2522
2526
  this.parserOptions = options;
2523
2527
  if (this.base !== undefined) {
2524
2528
  this.parent = parserForType(this.base);
2525
2529
  if (this.parent !== undefined)
2526
2530
  this.parent.children.push(this);
2527
2531
  }
2528
- this._fields.forEach((f) => f.configure({ options, parserForType }));
2532
+ const optimisticConcurrencyFields = entitySet?.annotatedValue(OPTIMISTIC_CONCURRENCY) ?? [];
2533
+ this._fields.forEach((field) => {
2534
+ const optimisticConcurrency = optimisticConcurrencyFields.indexOf(field.name) !== -1;
2535
+ field.configure({ options, optimisticConcurrency, parserForType });
2536
+ });
2529
2537
  }
2530
2538
  /**
2531
2539
  * Returns all fields of the structured type.
@@ -2677,7 +2685,7 @@ class ODataStructuredTypeParser extends ODataAnnotatable {
2677
2685
  }
2678
2686
  return schema;
2679
2687
  }
2680
- validate(attrs, { method, navigation = false, } = {}) {
2688
+ validate(attrs, { method, navigation = false, }) {
2681
2689
  const errors = {};
2682
2690
  const fields = this.fields({
2683
2691
  include_navigation: true,
@@ -2995,8 +3003,10 @@ class ODataStructuredType extends ODataParserSchemaElement {
2995
3003
  if (this.parent !== undefined)
2996
3004
  this.parent.children.push(this);
2997
3005
  }
3006
+ const entitySet = this.api.findEntitySetForEntityType(this.type()) ?? this.api.findEntitySetForEntityType(this.type({ alias: true }));
2998
3007
  this.parser.configure({
2999
3008
  options,
3009
+ entitySet,
3000
3010
  parserForType: (t) => this.api.parserForType(t),
3001
3011
  });
3002
3012
  if (this.model !== undefined) {
@@ -3180,7 +3190,7 @@ class ODataStructuredType extends ODataParserSchemaElement {
3180
3190
  * @param method Method to use for the process validation
3181
3191
  * @returns Object with the errors
3182
3192
  */
3183
- validate(attrs, { method, navigation = false, } = {}) {
3193
+ validate(attrs, { method, navigation = false, }) {
3184
3194
  return this.parser.validate(attrs, { method, navigation });
3185
3195
  }
3186
3196
  }
@@ -3204,7 +3214,7 @@ class ODataSchema extends ODataAnnotatable {
3204
3214
  this.containers = (config.containers ?? []).map((config) => new ODataEntityContainer(config, this));
3205
3215
  }
3206
3216
  isNamespaceOf(type) {
3207
- return type.startsWith(this.namespace) ?? (this.alias && type.startsWith(this.alias));
3217
+ return type.startsWith(this.namespace) || (this.alias != null && type.startsWith(this.alias));
3208
3218
  }
3209
3219
  get entitySets() {
3210
3220
  return this.containers.reduce((acc, container) => [...acc, ...container.entitySets], []);
@@ -5988,8 +5998,7 @@ class ODataRequest {
5988
5998
  if (apiOptions.prefer?.maxPageSize !== undefined && ['GET'].indexOf(this._method) !== -1)
5989
5999
  prefer.push(`odata.maxpagesize=${apiOptions.prefer?.maxPageSize}`);
5990
6000
  // Annotations
5991
- if (apiOptions.prefer?.includeAnnotations !== undefined &&
5992
- ['GET'].indexOf(this._method) !== -1)
6001
+ if (apiOptions.prefer?.includeAnnotations !== undefined && ['GET'].indexOf(this._method) !== -1)
5993
6002
  prefer.push(`odata.include-annotations=${apiOptions.prefer?.includeAnnotations}`);
5994
6003
  // Omit Null Values
5995
6004
  if (apiOptions.prefer?.omitNullValues === true && ['GET'].indexOf(this._method) !== -1)
@@ -10750,7 +10759,7 @@ class ODataCollection {
10750
10759
  }
10751
10760
  create(attrs = {}, { silent = false, server = true } = {}) {
10752
10761
  const model = this.modelFactory(attrs);
10753
- return (model.isValid() && server ? model.save() : of(model)).pipe(switchMap((model) => this.add(model, { silent, server })), map(() => model));
10762
+ return (model.isValid({ method: 'create' }) && server ? model.save() : of(model)).pipe(switchMap((model) => this.add(model, { silent, server })), map(() => model));
10754
10763
  }
10755
10764
  set(path, value, {}) {
10756
10765
  const pathArray = (Types.isArray(path) ? path : path.match(/([^[.\]])+/g));
@@ -11204,10 +11213,10 @@ class ODataModel {
11204
11213
  }
11205
11214
  // Validation
11206
11215
  _errors;
11207
- validate({ method, navigation = false, } = {}) {
11216
+ validate({ method, navigation = false, }) {
11208
11217
  return this._meta.validate(this, { method, navigation });
11209
11218
  }
11210
- isValid({ method, navigation = false, } = {}) {
11219
+ isValid({ method, navigation = false, }) {
11211
11220
  this._errors = this.validate({ method, navigation });
11212
11221
  if (this._errors !== undefined)
11213
11222
  this.events$.trigger(ODataModelEventType.Invalid, {
@@ -11338,7 +11347,7 @@ class ODataModel {
11338
11347
  resource instanceof ODataNavigationPropertyResource))
11339
11348
  return throwError(() => new Error('save: Resource type ODataEntityResource/ODataNavigationPropertyResource needed'));
11340
11349
  // Resolve method and resource key
11341
- if (method === undefined && this.schema().isCompoundKey())
11350
+ if (method === undefined && this.isCompoundKey())
11342
11351
  return throwError(() => new Error('save: Composite key require a specific method, use create/update/modify'));
11343
11352
  method = method || (!resource.hasKey() ? 'create' : 'update');
11344
11353
  if (resource instanceof ODataEntityResource &&
@@ -11415,6 +11424,9 @@ class ODataModel {
11415
11424
  isNew() {
11416
11425
  return !this._meta.hasKey(this);
11417
11426
  }
11427
+ isCompoundKey() {
11428
+ return this.schema().isCompoundKey();
11429
+ }
11418
11430
  withResource(resource, ctx) {
11419
11431
  return this._meta.withResource(this, resource, ctx);
11420
11432
  }
@@ -11809,9 +11821,6 @@ class ODataModelField {
11809
11821
  parser;
11810
11822
  options;
11811
11823
  default;
11812
- required;
11813
- concurrency;
11814
- maxLength;
11815
11824
  minLength;
11816
11825
  min;
11817
11826
  max;
@@ -11823,9 +11832,6 @@ class ODataModelField {
11823
11832
  this.field = field;
11824
11833
  this.parser = parser;
11825
11834
  this.default = opts.default || parser.default;
11826
- this.required = Boolean(opts.required || !parser.nullable);
11827
- this.concurrency = Boolean(opts.concurrency);
11828
- this.maxLength = opts.maxLength || parser.maxLength;
11829
11835
  this.minLength = opts.minLength;
11830
11836
  this.min = opts.min;
11831
11837
  this.max = opts.max;
@@ -11843,16 +11849,20 @@ class ODataModelField {
11843
11849
  annotatedValue(term) {
11844
11850
  return this.parser.annotatedValue(term);
11845
11851
  }
11846
- configure({ concurrency, options }) {
11852
+ configure({ options }) {
11847
11853
  this.parserOptions = options;
11848
- if (concurrency)
11849
- this.concurrency = concurrency;
11850
11854
  if (this.default !== undefined)
11851
11855
  this.default = this.deserialize(this.default, options);
11852
11856
  }
11853
11857
  isKey() {
11854
11858
  return this.parser.isKey();
11855
11859
  }
11860
+ isComputed() {
11861
+ return this.parser.isComputed();
11862
+ }
11863
+ isOptimisticConcurrency() {
11864
+ return this.parser.isOptimisticConcurrency();
11865
+ }
11856
11866
  hasReferentials() {
11857
11867
  return this.parser.hasReferentials();
11858
11868
  }
@@ -11882,7 +11892,7 @@ class ODataModelField {
11882
11892
  throw new Error(`Could not find enum type for ${this.parser.type}`);
11883
11893
  return enumType;
11884
11894
  }
11885
- validate(value, { method, navigation = false, } = {}) {
11895
+ validate(value, { method, navigation = false, }) {
11886
11896
  if (ODataModelOptions.isModel(value)) {
11887
11897
  return !value.isValid({ method, navigation }) ? value._errors : undefined;
11888
11898
  }
@@ -11892,21 +11902,7 @@ class ODataModelField {
11892
11902
  : undefined;
11893
11903
  }
11894
11904
  else {
11895
- const computed = this.annotatedValue(COMPUTED);
11896
11905
  const errors = this.parser?.validate(value, { method, navigation }) ?? [];
11897
- if (this.required &&
11898
- (value === null || (value === undefined && method !== 'modify')) && // Is null or undefined without patch?
11899
- !(computed && method === 'create') // Not (Is Computed field and create) ?
11900
- ) {
11901
- if (errors.indexOf('required') === -1)
11902
- errors.push(`required`);
11903
- }
11904
- if (this.maxLength !== undefined &&
11905
- typeof value === 'string' &&
11906
- value.length > this.maxLength) {
11907
- if (errors.indexOf('maxlength') === -1)
11908
- errors.push(`maxlength`);
11909
- }
11910
11906
  if (this.minLength !== undefined &&
11911
11907
  typeof value === 'string' &&
11912
11908
  value.length < this.minLength) {
@@ -12010,11 +12006,11 @@ class ODataModelAttribute {
12010
12006
  get navigation() {
12011
12007
  return this._field.navigation;
12012
12008
  }
12013
- get computed() {
12014
- return this._field.annotatedValue(COMPUTED);
12009
+ isComputed() {
12010
+ return this._field.isComputed();
12015
12011
  }
12016
- get concurrency() {
12017
- return Boolean(this._field.concurrency);
12012
+ isOptimisticConcurrency() {
12013
+ return this._field.isOptimisticConcurrency();
12018
12014
  }
12019
12015
  get referentials() {
12020
12016
  return this._field.referentials;
@@ -12184,17 +12180,7 @@ class ODataModelOptions {
12184
12180
  this.parent.children.push(this);
12185
12181
  }
12186
12182
  this.entitySet = this.api.findEntitySetForEntityType(this.type());
12187
- let concurrencyFields = [];
12188
- if (this.entitySet !== undefined) {
12189
- concurrencyFields = this.entitySet.annotatedValue(OPTIMISTIC_CONCURRENCY) ?? [];
12190
- }
12191
- this._fields.forEach((field) => {
12192
- const concurrency = concurrencyFields.indexOf(field.field) !== -1;
12193
- field.configure({
12194
- concurrency,
12195
- options,
12196
- });
12197
- });
12183
+ this._fields.forEach((field) => field.configure({ options, }));
12198
12184
  }
12199
12185
  fields({ include_navigation, include_parents, }) {
12200
12186
  return [
@@ -12252,8 +12238,7 @@ class ODataModelOptions {
12252
12238
  parser: structuredFieldParser,
12253
12239
  });
12254
12240
  modelField.configure({
12255
- options: this.api.options,
12256
- concurrency: false,
12241
+ options: this.api.options
12257
12242
  });
12258
12243
  Object.defineProperty(self, modelField.name, {
12259
12244
  configurable: true,
@@ -12450,7 +12435,7 @@ class ODataModelOptions {
12450
12435
  return null;
12451
12436
  return resolve ? Objects.resolveKey(referenced, { single }) : Object.fromEntries(referenced);
12452
12437
  }
12453
- validate(self, { method, navigation = false, } = {}) {
12438
+ validate(self, { method, navigation = false, }) {
12454
12439
  const errors = this.fields({
12455
12440
  include_parents: true,
12456
12441
  include_navigation: navigation,
@@ -12517,9 +12502,9 @@ class ODataModelOptions {
12517
12502
  .reduce((acc, attr) => {
12518
12503
  const name = field_mapping ? attr.fieldName : attr.name;
12519
12504
  let value = attr.get();
12520
- const computed = attr.computed;
12505
+ const computed = attr.isComputed();
12521
12506
  const navigation = attr.navigation;
12522
- const concurrency = attr.concurrency;
12507
+ const concurrency = attr.isOptimisticConcurrency();
12523
12508
  if (ODataModelOptions.isModel(value)) {
12524
12509
  value = value.toEntity({
12525
12510
  client_id,
@@ -13036,7 +13021,7 @@ class ODataApi {
13036
13021
  });
13037
13022
  return true;
13038
13023
  }
13039
- populate({ schemas, references, models, collections }) {
13024
+ populate({ schemas, references, models, collections, }) {
13040
13025
  const odataSchemas = (schemas ?? []).map((schema) => new ODataSchema(schema, this));
13041
13026
  //TODO: Merge duplicates
13042
13027
  this.schemas = [...this.schemas, ...odataSchemas];