diegomvh / angular-odata

Client side OData typescript library for Angular
https://www.npmjs.com/package/angular-odata
MIT License
46 stars 14 forks source link

Updated - Clone? Filter? Select? #59

Closed johnhwright closed 2 years ago

johnhwright commented 2 years ago

Updated to the latest version and I see that some of the syntax has changed. Are there any good examples of how existing queries should be migrated?

I'm mainly curious about clone() and the new query() function. Should you always call clone when you query, what would be the case when you wouldn't call this? How do you select and expand with the new query while filtering?

We currently have uses like:

this.searchResults$ = this.odAdminSupportTicketService.supportTickets.filter({ or: [{ createdOn: { ge: fromDate, lt: toDate } }] })
      .select(['id', 'status', 'customer', 'messages',])
      .expand({
        customer: { select: ['email'] },
        messages: { select: ['message', 'createdOn', 'createdById'] },
      })
      .get()
      .pipe(...rxjs operators on data...)

this.odAdminSupportTicketService.supportTickets just maps to oDataServiceFactory.entitySet<ODSecureConversation>('support-tickets').entities() in a service.

So to upgrade would I'm guessing I maybe first clone, then use the new query func.

this.odAdminSupportTicketService.supportTickets.clone().query(q => q.filter({ or: [{ createdOn: { ge: fromDate, lt: toDate } }] }))
...

But I lose it after the query though because neither select nor expand exist on the ODataEntitySetResource anymore.

diegomvh commented 2 years ago

Hi @johnhwright

From version 0.80.0 to version 0.90.0 the most important change was that the resources become mutable by default, if another resource is required, that points to the same endpoint, it must be cloned. That is the function of the clone method.

In the previous version each call to select, expand, filter, etc internally generated new resources that were quickly discarded. The idea for the new version is to order and separate the handling of the path on the one hand and the querystring on the other hand.

In this way: To obtain the endpoint we move through the segments of the resources, alternating the path and generating new resources in each invocation of the methods.

var ticketsResource = this.odAdminSupportTicketService.supportTickets.clone();
var ticketResource = ticketsResource.entity(321);  // Create new resource from entitySet. The new resource point to the entity

On the other hand, the queryString is mutable for each resource, this implies that I can take the same resource and alter it in its subsequent invocations.

// I initially prepare a base case of the resource
ticketsResource.query(q => { 
  q.filter({ or: [{ createdOn: { ge: fromDate, lt: toDate } }] });
  q.select(['id', 'status', 'customer', 'messages',]);
  q.expand({
    customer: { select: ['email'] },
    messages: { select: ['message', 'createdOn', 'createdById'] },
  });
});
// Then in each successive call I can change the paging without modifying the base conditions of filtering, ordering, selection, etc.
ticketsResource.query(q => {
  q.top(100);
}).fetch().subscribe();
ticketsResource.query(q => {
  q.skip(100);
  q.top(100);
}).fetch().subscribe();
// Or simply
ticketsResource.query(q => {
  q.paging({skip:200, top: 100});
}).fetch().subscribe();
// In each call the initial select, filter and expand are preserved

I honestly liked the idea of ​​immutability for resources but was ambiguous about how to work with them. So I opted for this new approach. The segments are immutable and create new resources. On the other hand, handling the querystring mutates the resource.

The query method generates a context where it is possible to alter everything that corresponds to the queryString of the url. By removing the ambiguity I can focus on improving the possibilities of that query mutation context, today it is possible to use two approaches:

// OData v4 query builder approach
ticketsResource.query(q => { 
   q.filter({ or: [{ createdOn: { ge: fromDate, lt: toDate } }] });
});
// OData Filter Builder approach
ticketsResource.query(q => { 
   q.filter(({e, s}) => e('or').ge(s.createOn, fromDate).lt(s.createOn, toDate));
});

I apologize for the relatively abrupt change, I think there are several users of the tool who were confused by the change and I hope I have helped or clarified something with this brief explanation. Greetings and thank you very much for letting me know and giving me the opportunity to try to explain the change.

diegomvh commented 2 years ago

New versión of the example code:

this.searchResults$ = this.odAdminSupportTicketService.supportTickets
.clone()
.query(q => {
  q.filter({ or: [{ createdOn: { ge: fromDate, lt: toDate } }] });
  q.select(['id', 'status', 'customer', 'messages',]);
  q.expand({
     customer: { select: ['email'] },
     messages: { select: ['message', 'createdOn', 'createdById'] },
  });
})
.get()
.pipe(...rxjs operators on data...)

this.odAdminSupportTicketService.supportTickets not change

johnhwright commented 2 years ago

Oh wow. Not sure how I completely missed the part about being able to do multiple operations inside the query, assumed that the result actually have to be returned. Should have thought of that though, that's actually pretty awesome!

Great stuff, and thank you for the in depth response!

johnhwright commented 2 years ago

@diegomvh Just ran across a few more things to double check if you don't mind.

get needs to be replaced with fetch in pretty much all instances now yes?

I also use post and patch on the ODataEntitySetService class. - Looks like patch is now just modify (or maybe update not sure...`)?

And not sure what to do for my batch (though this one I might be able to simply work around with normal requests)?

  public batch(func: (batch: ODataBatchResource) => void, options?: HttpOptions): Observable<ODataResponse<any>> {
    return this.oData.batch('support-tickets').post(func, options);
  }
diegomvh commented 2 years ago

Hi @johnhwright Yes!, as you said, other changes are: In the resource API, the post, get, put, patch, delete methods are protected. The public methods depending on the resource are:

create: call the post method. update: call the put method modify: call the patch method destroy: calls the delete method fetch: call to get method and for example in an entity resource: fetchEntity: calls get and returns only the fetched entity without the metadata fetchModel: calls get and returns a model for the entity and the fetched metadata

batch call were redesigned, you must pass a function that returns an observable There are two ways to interact with them:

this.odata
      .batch("TripPin")
      .exec(() =>
        forkJoin({
          airports: airports.fetch(),
          people: people.fetch({ withCount: true }),
        })
      )
      .subscribe();
let batch = this.odata.batch("TripPin")
batch.add(airports.fetch()).subscribe();
batch.add(people.fetch()).subscribe();
batch.send().subscribe();

I hope I have helped best regards

johnhwright commented 2 years ago

Fantastic! We've managed to get everything up to the current version and it seems to be working great!

Thank you again for the very detailed responses!