From b1716816bbdbf420863030d8d677c1ddf7711aa8 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Tue, 1 Dec 2020 11:34:32 +0100 Subject: [PATCH] add odata nextlink --- src/lib/processor.ts | 30 ++++++++++++++++++++++++++++-- src/test/execute.spec.ts | 20 ++++++++++++++++---- src/test/server.spec.ts | 38 ++++++++++++++++++++++++++++++++++++++ src/test/test.model.ts | 15 +++++++++++++++ 4 files changed, 97 insertions(+), 6 deletions(-) diff --git a/src/lib/processor.ts b/src/lib/processor.ts index 6659c254..fe2d593d 100644 --- a/src/lib/processor.ts +++ b/src/lib/processor.ts @@ -450,6 +450,7 @@ export class ODataProcessor extends Transform { private streamObject = false; private streamEnd = false; private streamInlineCount: number; + private streamNextLink: string; private elementType: any; private resultCount = 0; @@ -530,6 +531,9 @@ export class ODataProcessor extends Transform { this.push("{"); if (this.options.metadata != ODataMetadataType.none) { this.push(`"@odata.context":"${this.odataContext}",`); + if (this.streamNextLink) { + this.push(`"@odata.nextLink":"${this.streamNextLink}",`); + } } this.push('"value":['); } @@ -546,6 +550,15 @@ export class ODataProcessor extends Transform { delete chunk.inlinecount; } } + if (chunk["@odata.nextLink"] || chunk.nextLink) { + this.streamNextLink = chunk["@odata.nextLink"] || chunk.nextLink; + if (Object.keys(chunk).length == 1) { + return typeof done == "function" ? done() : null; + } else { + delete chunk["@odata.nextLink"]; + delete chunk.nextLink; + } + } let entity = {}; let defer; if (this.ctrl) defer = this.__appendLinks(this.ctrl, this.elementType || this.ctrl.prototype.elementType, entity, chunk); @@ -592,6 +605,9 @@ export class ODataProcessor extends Transform { if (this.streamStart && typeof this.streamInlineCount == "number") { flushObject["@odata.count"] = this.streamInlineCount; } + if (this.options.metadata != ODataMetadataType.none && this.streamNextLink) { + flushObject["@odata.nextLink"] = this.streamNextLink; + } this.push(flushObject); } else { if (this.streamStart) { @@ -602,7 +618,9 @@ export class ODataProcessor extends Transform { if (this.options.metadata == ODataMetadataType.none) { this.push('{"value":[]}'); } else { - this.push(`{"@odata.context":"${this.odataContext}","value":[]}`); + let md = `{"@odata.context":"${this.odataContext}"`; + if (this.streamNextLink) { md = `${md},{"@odata.nextLink":"${this.streamNextLink}"`; } + this.push(`${md}","value":[]}`); } } } @@ -610,7 +628,9 @@ export class ODataProcessor extends Transform { if (this.options.metadata == ODataMetadataType.none) { this.push('{"value":[]}'); } else { - this.push(`{"@odata.context":"${this.odataContext}","value":[]}`); + let md = `{"@odata.context":"${this.odataContext}"`; + if (this.streamNextLink) { md = `${md},{"@odata.nextLink":"${this.streamNextLink}"`; } + this.push(`${md}","value":[]}`); } } this.streamEnd = true; @@ -796,10 +816,14 @@ export class ODataProcessor extends Transform { if (this.method == "get") { currentResult.then((value) => { try { + const nl = result.body["@odata.context"]; result.body = { "@odata.context": this.options.metadata != ODataMetadataType.none ? result.body["@odata.context"] : undefined, value: value }; + if (this.options.metadata != ODataMetadataType.none && nl) { + result.body["@odata.context"] = nl; + } let elementType = result.elementType; //if (value instanceof Object) result.elementType = Edm.isEnumType(result.elementType, part.name) @@ -1281,6 +1305,7 @@ export class ODataProcessor extends Transform { let elementType = result.elementType = jsPrimitiveTypes.indexOf(result.elementType) >= 0 || result.elementType == String || typeof result.elementType != "function" ? ctrlType : result.elementType; if (typeof result.body == "object" && result.body) { if (typeof result.body["@odata.count"] == "number") context["@odata.count"] = result.body["@odata.count"]; + if (result.body && result.body.value && result.body.value["nextLink"] && typeof result.body.value["nextLink"] == "string") context["@odata.nextLink"] = result.body.value["nextLink"]; if (!result.body["@odata.context"]) { let ctrl = this.ctrl && this.ctrl.prototype.elementType == ctrlType ? this.ctrl : this.serverType.getController(ctrlType); if (result.body.value && Array.isArray(result.body.value)) { @@ -1540,6 +1565,7 @@ export class ODataProcessor extends Transform { } if (isCollection && navigationResult.body.value && Array.isArray(navigationResult.body.value)) { if (typeof navigationResult.body["@odata.count"] == "number") context[prop + "@odata.count"] = navigationResult.body["@odata.count"]; + if (typeof navigationResult.body["@odata.nextLink"] == "string") context[prop + "@odata.nextLink"] = navigationResult.body["@odata.nextLink"]; context[prop] = navigationResult.body.value; } else if (navigationResult.body && Object.keys(navigationResult.body).length !== 0) { context[prop] = navigationResult.body; diff --git a/src/test/execute.spec.ts b/src/test/execute.spec.ts index 8764a269..88bcd18a 100644 --- a/src/test/execute.spec.ts +++ b/src/test/execute.spec.ts @@ -192,6 +192,18 @@ describe("OData execute", () => { }); }); + it("should return entity set result with nextLink", async () => { + const result = await TestServer.execute("/NextLinkEntitySet", "GET"); + + return expect(result.body["@odata.nextLink"]).to.equal("http://localhost/NextLinkEntitySet?$skip=1&$top=1"); + }); + + it("should return navigation property expanded with nextLink", async () => { + const result = await TestServer.execute("/GeneratorCategories?$expand=GeneratorProducts($top=2)&$top=1&$orderby=Name desc", "GET"); + + return expect(result.body.value[0]["GeneratorProducts@odata.nextLink"]).to.equal("http://localhost/GeneratorCategories('578f2baa12eaebabec4af28d')?$expand=GeneratorProducts($top=2&$skip=2)"); + }); + it("should create category reference on product", () => { return TestServer.execute("/Products('578f2b8c12eaebabec4af286')/Category/$ref", "POST", { "@odata.id": "http://localhost/Categories(categoryId='578f2baa12eaebabec4af28c')" @@ -430,10 +442,10 @@ describe("OData execute", () => { describe("Non existent entity", () => { it('should return cannot read property node error', () => { return TestServer.execute("/NonExistent", "GET") - .then((result) => {}) - .catch(err => { - expect(err.message).to.equal("Cannot read property 'node' of undefined"); - }); + .then((result) => { }) + .catch(err => { + expect(err.message).to.equal("Cannot read property 'node' of undefined"); + }); }); }); }); diff --git a/src/test/server.spec.ts b/src/test/server.spec.ts index 5d3a474b..d9115309 100644 --- a/src/test/server.spec.ts +++ b/src/test/server.spec.ts @@ -1177,6 +1177,44 @@ export function testFactory(createTest: any) { elementType: GeneratorCategory, contentType: "application/json" }); + createTest("should return GeneratorCategories expanded with GeneratorProducts@odata.nextLink when using $top in $expand subquery", + TestServer, "GET /GeneratorCategories?$expand=GeneratorProducts($top=2)&$top=1&$orderby=Name desc", { + statusCode: 200, + body: { + "@odata.context": "http://localhost/$metadata#GeneratorCategories", + "value": [ + { + "@odata.id": "http://localhost/GeneratorCategories('578f2baa12eaebabec4af28d')", + "Description": "Seaweed and fish", + "Name": "Seafood", + "_id": new ObjectID("578f2baa12eaebabec4af28d"), + "GeneratorProducts": [ + { + "@odata.id": "http://localhost/GeneratorProducts('578f2b8c12eaebabec4af242')", + "Discontinued": false, + "Name": "Ikura", + "QuantityPerUnit": "12 - 200 ml jars", + "UnitPrice": 31, + "_id": new ObjectID("578f2b8c12eaebabec4af242"), + "CategoryId": new ObjectID("578f2baa12eaebabec4af28d") + }, + { + "@odata.id": "http://localhost/GeneratorProducts('578f2b8c12eaebabec4af245')", + "Discontinued": false, + "Name": "Konbu", + "QuantityPerUnit": "2 kg box", + "UnitPrice": 6, + "_id": new ObjectID("578f2b8c12eaebabec4af245"), + "CategoryId": new ObjectID("578f2baa12eaebabec4af28d"), + } + ], + "GeneratorProducts@odata.nextLink": "http://localhost/GeneratorCategories('578f2baa12eaebabec4af28d')?$expand=GeneratorProducts($top=2&$skip=2)" + } + ] + }, + elementType: GeneratorCategory, + contentType: "application/json" + }); createTest("should return GeneratorCategories expanded with GeneratorProduct using $top,$filter,$expand subqueries", TestServer, "GET /GeneratorCategories?$expand=GeneratorProducts($orderby=Name desc)&$top=1&$orderby=Name desc", { diff --git a/src/test/test.model.ts b/src/test/test.model.ts index 9f9ebad3..48ad1053 100644 --- a/src/test/test.model.ts +++ b/src/test/test.model.ts @@ -211,6 +211,16 @@ export class InlineCountController extends ODataController { } } +@odata.type(Foobar) +export class NextLinkController extends ODataController { + @odata.GET + entitySet() { + let result = [{ id: 1, a: 1 }]; + (result).nextLink = "http://localhost/NextLinkEntitySet?$skip=1&$top=1"; + return result; + } +} + @odata.type(Foobar) export class BoundOperationController extends ODataController { @Edm.Action @@ -821,6 +831,10 @@ export class CategoriesAdvancedGeneratorController extends ODataController { response = yield doSkip(response, options); response = yield doTop(response, options); + if (query && query.raw && query.raw === "$top=2") { + (response).nextLink = "http://localhost/GeneratorCategories('578f2baa12eaebabec4af28d')?$expand=GeneratorProducts($top=2&$skip=2)"; + } + return response } } @@ -1017,6 +1031,7 @@ export class HiddenController extends ODataController { } @odata.controller(GeneratorTestController, "GeneratorEntitySet") @odata.controller(AsyncTestController, "AsyncEntitySet") @odata.controller(InlineCountController, "InlineCountEntitySet") +@odata.controller(NextLinkController, "NextLinkEntitySet") @odata.controller(BoundOperationController, "BoundOperationEntitySet") @odata.controller(ImagesController, "ImagesControllerEntitySet") @odata.controller(MusicController, "MusicControllerEntitySet")