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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
## [Unreleased]

### Added
- Generate media stream paths (GET and PUT) for media entities and `Edm.Stream` properties
### Changed
### Deprecated
### Removed
Expand Down
2 changes: 1 addition & 1 deletion lib/compile/csdl.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const CDS_TERMS = Object.freeze({
'FilterRestrictions', 'IndexableByKey', 'InsertRestrictions', 'KeyAsSegmentSupported', 'NavigationRestrictions', 'OperationRestrictions',
'ReadRestrictions', 'SearchRestrictions', 'SelectSupport', 'SkipSupported', 'SortRestrictions', 'TopSupported', 'UpdateRestrictions'],
Core: ['AcceptableMediaTypes', 'Computed', 'ComputedDefaultValue', 'DefaultNamespace', 'Description', 'Example', 'Immutable', 'LongDescription',
'OptionalParameter', 'Permissions', 'SchemaVersion'],
'MediaType', 'OptionalParameter', 'Permissions', 'SchemaVersion'],
JSON: ['Schema'],
Validation: ['AllowedValues', 'Exclusive', 'Maximum', 'Minimum', 'Pattern']
})
Expand Down
86 changes: 86 additions & 0 deletions lib/compile/csdl2openapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
* @param {{ url?: string, servers?: object, odataVersion?: string, scheme?: string, host?: string, basePath?: string, diagram?: boolean, maxLevels?: number, shortActionPaths?: boolean }} options Optional parameters
* @return {object} OpenAPI description
*/
module.exports.csdl2openapi = function (

Check warning on line 93 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 24

Function has a complexity of 22. Maximum allowed is 15

Check warning on line 93 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 20

Function has a complexity of 22. Maximum allowed is 15

Check warning on line 93 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / lint

Function has a complexity of 22. Maximum allowed is 15
csdl,
{
url: serviceRoot,
Expand Down Expand Up @@ -653,6 +653,7 @@
level,
navigationPrefix: navigationPath
});
pathItemsForMediaStream({ paths, prefix: path, prefixParameters: parameters, type, name, sourceName });

if (Object.keys(pathItem).filter((i) => i !== "parameters").length === 0)
delete paths[path];
Expand Down Expand Up @@ -748,7 +749,7 @@
* @param {boolean} options.byKey Read by key
* @param {array} options.nonExpandable Non-expandable navigation properties
*/
function operationRead({ pathItem, element, name, sourceName, targetName, target, level, restrictions, byKey, nonExpandable }) {

Check warning on line 752 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 24

Function 'operationRead' has a complexity of 31. Maximum allowed is 15

Check warning on line 752 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 20

Function 'operationRead' has a complexity of 31. Maximum allowed is 15

Check warning on line 752 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / lint

Function 'operationRead' has a complexity of 31. Maximum allowed is 15
const targetRestrictions = target?.[meta.voc.Capabilities.ReadRestrictions];
const readRestrictions = restrictions.ReadRestrictions || targetRestrictions || {};
const readByKeyRestrictions = readRestrictions.ReadByKeyRestrictions;
Expand Down Expand Up @@ -1221,7 +1222,7 @@
* @param {object} options.restrictions Navigation property restrictions of navigation segment
* @param {boolean} [options.byKey=false] Update by key
*/
function operationUpdate({ pathItem, element, name, sourceName, target, level, restrictions, byKey = false }) {

Check warning on line 1225 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 24

Function 'operationUpdate' has a complexity of 16. Maximum allowed is 15

Check warning on line 1225 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 20

Function 'operationUpdate' has a complexity of 16. Maximum allowed is 15

Check warning on line 1225 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / lint

Function 'operationUpdate' has a complexity of 16. Maximum allowed is 15
const updateRestrictions = restrictions.UpdateRestrictions || target?.[meta.voc.Capabilities.UpdateRestrictions] || {};
const countRestrictions = target?.[meta.voc.Capabilities.CountRestrictions]?.Countable === false;
if (updateRestrictions.Updatable !== false && !element[meta.voc.Core.Immutable]) {
Expand Down Expand Up @@ -1300,6 +1301,91 @@
}
}

/**
* Add path and Path Item Object for media stream access ($value) and stream properties of an entity type
* @param {object} options
* @param {object} options.paths The Paths Object to augment
* @param {string} options.prefix Prefix for path (the key-qualified entity path)
* @param {Array} options.prefixParameters Parameter Objects for prefix
* @param {object} options.type Entity type object
* @param {string} options.name Name of the entity set
* @param {string} options.sourceName Name of path source
*/
function pathItemsForMediaStream({ paths, prefix, prefixParameters, type, name, sourceName }) {
if (type.$HasStream) {
const mediaTypes = type[meta.voc.Core.AcceptableMediaTypes]?.map(t => t['$EnumMember'] ?? t) ?? [];

Check warning on line 1316 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 24

["$EnumMember"] is better written in dot notation

Check warning on line 1316 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 20

["$EnumMember"] is better written in dot notation

Check warning on line 1316 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / lint

["$EnumMember"] is better written in dot notation
const contentTypes = mediaTypes.length > 0 ? mediaTypes : ['*/*'];
const mediaContent = Object.fromEntries(contentTypes.map(ct => [ct, { schema: { type: 'string', format: 'binary' } }]));
const lname = splitName(name);
const valuePath = `${prefix}/$value`;
const pathItem = prefixParameters.length > 0 ? { parameters: prefixParameters } : {};
paths[valuePath] = pathItem;

pathItem.get = {
summary: `Retrieves the media stream of a single ${pluralize.singular(lname)}.`,
tags: [normaliseTag(sourceName)],
responses: {
200: {
description: `Retrieved ${pluralize.singular(lname)}`,
content: mediaContent
},
'4XX': { $ref: '#/components/responses/error' }
}
};
pathItem.put = {
summary: `Changes the media stream of a single ${pluralize.singular(lname)}.`,
tags: [normaliseTag(sourceName)],
requestBody: {
description: `New media stream for ${pluralize.singular(lname)}`,
required: true,
content: mediaContent
},
responses: {
204: { description: 'Success' },
'4XX': { $ref: '#/components/responses/error' }
}
};
}

const properties = propertiesOfStructuredType(type);
Object.keys(properties).forEach(propName => {
const prop = properties[propName];
if (prop.$Kind === 'NavigationProperty' || prop.$Type !== 'Edm.Stream') return;
const mediaTypeAnnotation = prop[meta.voc.Core.MediaType];
const contentTypes = [typeof mediaTypeAnnotation === 'string' ? mediaTypeAnnotation : '*/*'];
const mediaContent = Object.fromEntries(contentTypes.map(ct => [ct, { schema: { type: 'string', format: 'binary' } }]));
const lname = splitName(name);
const propPath = `${prefix}/${propName}`;
const pathItem = prefixParameters.length > 0 ? { parameters: prefixParameters } : {};
paths[propPath] = pathItem;

pathItem.get = {
summary: `Retrieves ${splitName(propName)} of a single ${pluralize.singular(lname)}.`,
tags: [normaliseTag(sourceName)],
responses: {
200: {
description: `Retrieved ${splitName(propName)}`,
content: mediaContent
},
'4XX': { $ref: '#/components/responses/error' }
}
};
pathItem.put = {
summary: `Changes ${splitName(propName)} of a single ${pluralize.singular(lname)}.`,
tags: [normaliseTag(sourceName)],
requestBody: {
description: `New value for ${splitName(propName)}`,
required: true,
content: mediaContent
},
responses: {
204: { description: 'Success' },
'4XX': { $ref: '#/components/responses/error' }
}
};
});
}

/**
* Add paths and Path Item Objects for navigation segments
* @param {object} options
Expand Down Expand Up @@ -1557,7 +1643,7 @@
* @param {string} options.sourceName Name of path source
* @param {object} [options.actionImport={}] Action import
*/
function pathItemAction({ paths, prefix, prefixParameters, actionName, overload, sourceName, actionImport = {} }) {

Check warning on line 1646 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 24

Function 'pathItemAction' has a complexity of 18. Maximum allowed is 15

Check warning on line 1646 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 20

Function 'pathItemAction' has a complexity of 18. Maximum allowed is 15

Check warning on line 1646 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / lint

Function 'pathItemAction' has a complexity of 18. Maximum allowed is 15
const name = actionName.indexOf('.') === -1 ? actionName : nameParts(actionName).name;
const pathItem = {
post: {
Expand Down Expand Up @@ -1647,7 +1733,7 @@
* @param {string} options.sourceName Name of path source
* @param {object} [options.functionImport={}] Function Import
*/
function pathItemFunction({ paths, prefix, prefixParameters, functionName, overload, sourceName, functionImport = {} }) {

Check warning on line 1736 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 24

Function 'pathItemFunction' has a complexity of 16. Maximum allowed is 15

Check warning on line 1736 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 20

Function 'pathItemFunction' has a complexity of 16. Maximum allowed is 15

Check warning on line 1736 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / lint

Function 'pathItemFunction' has a complexity of 16. Maximum allowed is 15
const name = functionName.indexOf('.') === -1 ? functionName : nameParts(functionName).name;
let parameters = overload.$Parameter || [];
if (overload.$IsBound) parameters = parameters.slice(1);
Expand All @@ -1656,7 +1742,7 @@

const implicitAliases = csdl.$Version > '4.0' || parameters.some(p => p[meta.voc.Core.OptionalParameter]);

parameters.forEach(p => {

Check warning on line 1745 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 24

Arrow function has a complexity of 29. Maximum allowed is 15

Check warning on line 1745 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 20

Arrow function has a complexity of 29. Maximum allowed is 15

Check warning on line 1745 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / lint

Arrow function has a complexity of 29. Maximum allowed is 15
const description = getDescriptionWithFallback(p);
/** @type {Parameter} */
const param = {
Expand Down Expand Up @@ -2036,7 +2122,7 @@
* @param {string} options.suffix Suffix for read/create/update
* @return {object} Map of Schemas Objects
*/
function schemasForStructuredType({ schemas, qualifier, name, type, suffix }) {

Check warning on line 2125 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 24

Function 'schemasForStructuredType' has a complexity of 18. Maximum allowed is 15

Check warning on line 2125 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 20

Function 'schemasForStructuredType' has a complexity of 18. Maximum allowed is 15

Check warning on line 2125 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / lint

Function 'schemasForStructuredType' has a complexity of 18. Maximum allowed is 15
const schemaName = `${qualifier}.${name}${suffix}`;
const baseName = `${qualifier}.${name}`;
const isKey = keyMap(type);
Expand All @@ -2052,7 +2138,7 @@
const properties = propertiesOfStructuredType(type);
const expandRestrictions = type[meta.voc.Capabilities.ExpandRestrictions] ?? {};
const nonExpandableProperties = expandRestrictions.NonExpandableProperties ?? [];
Object.keys(properties).forEach(iName => {

Check warning on line 2141 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 24

Arrow function has a complexity of 27. Maximum allowed is 15

Check warning on line 2141 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 20

Arrow function has a complexity of 27. Maximum allowed is 15

Check warning on line 2141 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / lint

Arrow function has a complexity of 27. Maximum allowed is 15
if (nonExpandableProperties.includes(iName)) {
return;
}
Expand Down Expand Up @@ -2269,7 +2355,7 @@
* @param {object} element referencing a type
* @return {object} Schema Object
*/
function getSchema(element, suffix = '', forParameter = false, forFunction = false) {

Check warning on line 2358 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 24

Function 'getSchema' has a complexity of 84. Maximum allowed is 15

Check warning on line 2358 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / Node.js 20

Function 'getSchema' has a complexity of 84. Maximum allowed is 15

Check warning on line 2358 in lib/compile/csdl2openapi.js

View workflow job for this annotation

GitHub Actions / lint

Function 'getSchema' has a complexity of 84. Maximum allowed is 15

/** @type {Schema} */
let s = {};
Expand Down
149 changes: 149 additions & 0 deletions test/lib/compile/csdl2openapi.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2900,6 +2900,155 @@ it("Error Logging when name and title are missing", () => {
console.log("Error handling executed successfully");
});

describe("Media stream paths", () => {
it("generates /$value GET and PUT for media entity ($HasStream)", () => {
const csdl = {
$Version: "4.0",
$EntityContainer: "this.Container",
this: {
doc: {
$Kind: "EntityType",
$HasStream: true,
$Key: ["ID"],
ID: { $Type: "Edm.Guid" },
name: {},
},
Container: {
$Kind: "EntityContainer",
docs: { $Collection: true, $Type: "this.doc", $ContainsTarget: true },
},
},
};
const openapi = lib.csdl2openapi(csdl, {});
const valuePath = "/docs({ID})/$value";
assert.ok(openapi.paths[valuePath], `Expected path ${valuePath}`);
assert.ok(openapi.paths[valuePath].get, "Expected GET on $value");
assert.ok(openapi.paths[valuePath].put, "Expected PUT on $value");
assert.deepStrictEqual(
Object.keys(openapi.paths[valuePath].get.responses[200].content),
["*/*"],
"Default content type is */*"
);
});

it("uses @Core.AcceptableMediaTypes for content type in /$value", () => {
const csdl = {
$Version: "4.0",
$EntityContainer: "this.Container",
$Reference: {
dummy: { $Include: [{ $Namespace: "Org.OData.Core.V1", $Alias: "Core" }] },
},
this: {
doc: {
$Kind: "EntityType",
$HasStream: true,
"@Core.AcceptableMediaTypes": ["application/pdf", "image/png"],
$Key: ["ID"],
ID: { $Type: "Edm.Guid" },
},
Container: {
$Kind: "EntityContainer",
docs: { $Collection: true, $Type: "this.doc", $ContainsTarget: true },
},
},
};
const openapi = lib.csdl2openapi(csdl, {});
const valuePath = "/docs({ID})/$value";
assert.ok(openapi.paths[valuePath], `Expected path ${valuePath}`);
const contentKeys = Object.keys(openapi.paths[valuePath].get.responses[200].content);
assert.deepStrictEqual(contentKeys, ["application/pdf", "image/png"]);
});

it("generates GET and PUT for Edm.Stream property", () => {
const csdl = {
$Version: "4.0",
$EntityContainer: "this.Container",
this: {
doc: {
$Kind: "EntityType",
$Key: ["ID"],
ID: { $Type: "Edm.Guid" },
content: { $Type: "Edm.Stream" },
},
Container: {
$Kind: "EntityContainer",
docs: { $Collection: true, $Type: "this.doc", $ContainsTarget: true },
},
},
};
const openapi = lib.csdl2openapi(csdl, {});
const propPath = "/docs({ID})/content";
assert.ok(openapi.paths[propPath], `Expected path ${propPath}`);
assert.ok(openapi.paths[propPath].get, "Expected GET on stream property");
assert.ok(openapi.paths[propPath].put, "Expected PUT on stream property");
assert.deepStrictEqual(
Object.keys(openapi.paths[propPath].put.requestBody.content),
["*/*"],
"Default content type is */*"
);
});

it("uses @Core.MediaType annotation on Edm.Stream property", () => {
const csdl = {
$Version: "4.0",
$EntityContainer: "this.Container",
$Reference: {
dummy: { $Include: [{ $Namespace: "Org.OData.Core.V1", $Alias: "Core" }] },
},
this: {
doc: {
$Kind: "EntityType",
$Key: ["ID"],
ID: { $Type: "Edm.Guid" },
content: { $Type: "Edm.Stream", "@Core.MediaType": "application/pdf" },
},
Container: {
$Kind: "EntityContainer",
docs: { $Collection: true, $Type: "this.doc", $ContainsTarget: true },
},
},
};
const openapi = lib.csdl2openapi(csdl, {});
const propPath = "/docs({ID})/content";
assert.ok(openapi.paths[propPath], `Expected path ${propPath}`);
assert.deepStrictEqual(
Object.keys(openapi.paths[propPath].get.responses[200].content),
["application/pdf"]
);
});

it("falls back to */* when @Core.MediaType is a path expression on Edm.Stream property", () => {
const csdl = {
$Version: "4.0",
$EntityContainer: "this.Container",
$Reference: {
dummy: { $Include: [{ $Namespace: "Org.OData.Core.V1", $Alias: "Core" }] },
},
this: {
doc: {
$Kind: "EntityType",
$Key: ["ID"],
ID: { $Type: "Edm.Guid" },
content: { $Type: "Edm.Stream", "@Core.MediaType": { $Path: "mimeType" } },
mimeType: { default: "application/octet-stream" },
},
Container: {
$Kind: "EntityContainer",
docs: { $Collection: true, $Type: "this.doc", $ContainsTarget: true },
},
},
};
const openapi = lib.csdl2openapi(csdl, {});
const propPath = "/docs({ID})/content";
assert.ok(openapi.paths[propPath], `Expected path ${propPath}`);
assert.deepStrictEqual(
Object.keys(openapi.paths[propPath].get.responses[200].content),
["*/*"],
"Path expression MediaType falls back to */*"
);
});
});

describe("CAP / CS01", () => {
it("FilterRestrictions, NavigationRestrictions, and SortRestrictions", () => {
const csdl = {
Expand Down
70 changes: 70 additions & 0 deletions test/lib/compile/data/TripPin.openapi3.json
Original file line number Diff line number Diff line change
Expand Up @@ -3231,6 +3231,76 @@
}
}
},
"/Photos({Id})/$value": {
"parameters": [
{
"description": "key: Id",
"in": "path",
"name": "Id",
"required": true,
"schema": {
"anyOf": [
{
"type": "integer",
"format": "int64"
},
{
"type": "string"
}
],
"example": "42"
}
}
],
"get": {
"summary": "Retrieves the media stream of a single photo.",
"tags": [
"Photos"
],
"responses": {
"200": {
"description": "Retrieved photo",
"content": {
"image/jpeg": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
},
"4XX": {
"$ref": "#/components/responses/error"
}
}
},
"put": {
"summary": "Changes the media stream of a single photo.",
"tags": [
"Photos"
],
"requestBody": {
"description": "New media stream for photo",
"required": true,
"content": {
"image/jpeg": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
},
"responses": {
"204": {
"description": "Success"
},
"4XX": {
"$ref": "#/components/responses/error"
}
}
}
},
"/ResetDataSource": {
"post": {
"summary": "Invokes action ResetDataSource",
Expand Down
Loading
Loading