jin-qu/jinqu-odata

Example of Expand + Select

Closed this issue · 20 comments

Submitted the pull request for updated docs, but one thing that's absent in the new docs is the section explaining how to combine expands with selects.

That section was:

// with selects
const result = query
    .expand(c => c.addresses)
        .thenExpand(a => a.city, a => a.country)
    .expand(c => c.addresses.$expand(a => a.city).country, c => c.name);
// $expand=addresses($expand=city($expand=country($select=name),$select=country),$select=city)

Not sure how to code that with the new API.

Current working expand demo is here:
https://stackblitz.com/edit/jinqu

This looks like a bug, we should use selected value as returned query's generic argument.

My mistake, return type don't change after $expand + $select. Only those properties will be filled. If we switch from lambda to string array for selection, we could use keyof. With lambdas, API seems a little flawed, you can access properties that you didn't select with $expand.

The answer to your question is:

// with selects
const result = query
    .expand(c => c.addresses)
        .thenExpand(a => a.city, a => a.country)
            .thenExpand(c => c.country, c => c.name)

This should create the desired result.

But of course we can ignore this and believe in the developer.

I wanted to fix the docs, but didn't want to add a random property to Author. Is there any navigation we can use on Author type? I checked the network on stackblitz, and it seems Author only have Id and Name.

The schema is:
https://www.solenya.org/odata/$metadata

It originally came from a Microsoft sample, but I've extended it to handle the more complex scenario of an author with multiple books. Can continue to extend it. If an author had an address, would that be useful to demonstrate the scenario?

Yeah, that would be great.

Digging into $select and $expand+$select, and I realized they don't allow expressions in selectors.
This is a game changer, I always believed we can use complex expressions, like:

query.select(c => ({ id, count: c.addresses.count() }))

It seems we can't. We can only list existing members. So we might have to limit the selections with keyof Entity.

The syntax will probably look like this:

// params keyof selection
query.select('id', 'addresses')

Implemented a test function like this:

select<K extends keyof T>(...names: K[]): Pick<T, K>

// usage
const c = service.companies().select('addresses', 'createDate')
// c only have 'addresses' and 'createDate' properties

I'm not sure if we should prefer this. It would force users to avoid invalid projection expression. Also still type safe.

OK, I'll add the Address field shortly.

There's now an "Address" property on Author. Note Address is a complex type, so does not have a key, but should be fine for the expand scenario.

I've also removed the "Location" (of type address) property from Book, as this didn't really seem to make sense (surely a Press or an Author would have an address, not a book).

The metadata:
https://www.solenya.org/odata/$metadata

I think Address being complex type causes problems with $expand.

Books?$expand=AuthorBooks($expand=Author($expand=Address))

// The query specified in the URI is not valid. Property 'Address' on type 'Default.Author' is not a navigation property or complex property. Only navigation properties can be expanded.

Will have a look

There's something weird going on; you should be able to expand complex types. Investigating.

As far as I can tell, the behaviour of ASP.NET's Core OData implementation, is that complex types are automatically returned, with the caveat that they're omitted if they exceed the MaxExpansionDepth value.

So will come up with an alternative to Address shortly.

It's weird indeed, but expected. A standard like this shouldn't change from implementation to implementation.

OK, Authors now have a Nationality:

https://www.solenya.org/odata/Books?$expand=AuthorBooks($expand=Author($expand=Nationality))

Regarding complex types, I did more testing, and MaxExpansionDepth doesn't actually have an affect on complex types. The reason the Address was showing up as null was due to some other bug. They should always be returned with the entity.

Where the type of Nationality is Country. Perhaps later to test additional scenarios can add a "Country" reference to Address; I think that's actually a legitimate OData schema.

Super, maybe we can replace test metadata with your schema. I will look into it.

All completed. Here is a sample (from unit tests)

const id = 42;
const query = service.companies()
    .expand('addresses', ['city'])
    .expand('addresses', a => a.id > id, { id })
        .thenExpand('city', c => c.name == 'Gotham')
            .thenExpand('country');

// $expand=addresses($filter=id gt 42;$expand=city($filter=name eq 'Gotham';$expand=country))

All type-safe, including 'addresses' and ['city'] part.

Updated docs accordingly:
#17