Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
### Deprecated
### Removed
### Fixed
- Collection responses now use `@odata.count` and `@odata.nextLink` instead of the OData 4.01 short forms `@count` and `@nextLink`, matching what CAP actually returns at runtime. Navigation property expansion count is likewise corrected from `nav@count` to `nav@odata.count`.
- REST collection responses now accurately document both response shapes using `oneOf`: a bare array (returned without `$count`) and an object with `count` + `value` (returned with `$count=true`). Previously the spec always described an object wrapper that didn't match the bare-array response.
- REST collection responses now use `count` (not `@count`) as the count property name, matching the actual CAP REST runtime response.
- Navigation property expansion count is corrected from `nav@count` to `nav@odata.count` for both OData and REST protocols.
- OData collection responses now use `@odata.count` and `@odata.nextLink` instead of the OData 4.01 short forms `@count` and `@nextLink`, matching what CAP actually returns at runtime.
- Compositions of `@readonly` entities no longer advertise write operations (POST/PATCH/DELETE) on their navigation paths
### Security

Expand Down
46 changes: 34 additions & 12 deletions lib/compile/csdl2openapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -2099,9 +2099,10 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot
description
}
};
const countProperty = { '@odata.count': ref('count') };
const isRest = !isODataProtocol(protocol);
const countProperty = isRest ? { count: ref('count') } : { '@odata.count': ref('count') };
const nextLinkKey = '@odata.nextLink';
const nextLinkProperty = {
const nextLinkProperty = isRest ? {} : {
[nextLinkKey]: {
type: 'string',
format: 'uri',
Expand All @@ -2115,16 +2116,37 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot
};

if (type.$Collection) {
// @ts-ignore - not undefined, we set it above
r[code].content['application/json'].schema = {
type: 'object',
title: `Collection of ${nameParts(type.$Type ? type.$Type : 'Edm.String').name}`,
properties: {
...(isCount && countProperty),
value: s,
...nextLinkProperty
}
};
const title = `Collection of ${nameParts(type.$Type ? type.$Type : 'Edm.String').name}`;
if (isRest) {
// CAP REST returns two distinct shapes depending on whether $count=true is passed:
// a bare array without it, or {count, value} with it. OpenAPI cannot condition a
// response schema on a query parameter value, so oneOf is used to document both.
// @ts-ignore - not undefined, we set it above
r[code].content['application/json'].schema = {
title,
oneOf: [
s,
{
type: 'object',
properties: {
...(isCount && countProperty),
value: s,
}
}
]
};
} else {
// @ts-ignore - not undefined, we set it above
r[code].content['application/json'].schema = {
type: 'object',
title,
properties: {
...(isCount && countProperty),
value: s,
...nextLinkProperty
}
};
}
}

else if (
Expand Down
5 changes: 3 additions & 2 deletions lib/compile/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ type SingleSchema = (StringSchema | NumberSchema | BooleanSchema | ArraySchema |

type AnyOf = { anyOf: Schema[] } & Meta
type AllOf = { allOf: Schema[] } & Meta
type MultiSchema = AnyOf | AllOf
type OneOf = { oneOf: Schema[] } & Meta
type MultiSchema = AnyOf | AllOf | OneOf

export type Schema = (SingleSchema | MultiSchema)

Expand Down Expand Up @@ -113,7 +114,7 @@ type Response = {
required?: boolean,
description?: string,
content?: {[mime in MIMEType]?: {
schema?: SingleSchema & { title?: string },
schema?: Schema & { title?: string },
example?: string
}},
responses?: Record<string, { $ref: string }>,
Expand Down
60 changes: 60 additions & 0 deletions test/lib/compile/csdl2openapi.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3659,6 +3659,66 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot
});
});

describe("REST protocol", () => {
const csdl = {
$EntityContainer: "this.Container",
this: {
Book: {
$Kind: "EntityType",
$Key: ["ID"],
ID: { $Type: "Edm.Int32" },
title: { $Nullable: true },
authors: { $Kind: "NavigationProperty", $Type: "this.Author", $Collection: true },
},
Author: {
$Kind: "EntityType",
$Key: ["ID"],
ID: { $Type: "Edm.Int32" },
},
Container: {
Books: { $Type: "this.Book", $Collection: true },
},
},
};

test("REST collection response uses oneOf: bare array OR object with count+value", () => {
const openapi = lib.csdl2openapi(csdl, { protocol: "rest" });
const schema = openapi.paths["/Books"].get.responses[200].content["application/json"].schema;
assert.ok(Array.isArray(schema.oneOf), "REST collection response schema must use oneOf");
assert.strictEqual(schema.oneOf.length, 2, "oneOf must have exactly 2 variants");

const [arrayVariant, objectVariant] = schema.oneOf;
assert.strictEqual(arrayVariant.type, "array", "first variant must be bare array");

assert.strictEqual(objectVariant.type, "object", "second variant must be object");
assert.ok("value" in objectVariant.properties, "object variant must have value property");
assert.ok("count" in objectVariant.properties, "object variant must use 'count' (not '@count') for REST");
assert.ok(!("@count" in objectVariant.properties), "object variant must not use '@count'");
assert.ok(!("@odata.count" in objectVariant.properties), "object variant must not use '@odata.count'");
});

test("REST collection response has no @odata.nextLink", () => {
const openapi = lib.csdl2openapi(csdl, { protocol: "rest" });
const schema = openapi.paths["/Books"].get.responses[200].content["application/json"].schema;
const schemaStr = JSON.stringify(schema);
assert.ok(!schemaStr.includes("nextLink"), "REST collection response must not include nextLink");
});

test("REST nav-property expansion uses @odata.count", () => {
const openapi = lib.csdl2openapi(csdl, { protocol: "rest" });
const bookSchema = openapi.components.schemas["this.Book"].properties;
assert.ok("authors@odata.count" in bookSchema, "nav-property expansion must use authors@odata.count");
assert.ok(!("authors@count" in bookSchema), "must not emit bare authors@count");
});

test("OData collection response is unaffected by REST changes", () => {
const openapi = lib.csdl2openapi(csdl, {});
const schema = openapi.paths["/Books"].get.responses[200].content["application/json"].schema;
assert.ok(!schema.oneOf, "OData collection response must not use oneOf");
assert.strictEqual(schema.type, "object", "OData collection response must be an object");
});
});

function check(actual, expected) {
assert.deepStrictEqual(paths(actual), paths(expected), "Paths");
assert.deepStrictEqual(
Expand Down
Loading