-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Return $metadata resource as odata + openapi spec
Returning odata and openapi specs in json format. Specs are scoped to the request permissions. Different users (roles) will receive different metadata endpoints and resources. Change-type: minor Signed-off-by: Harald Fischer <[email protected]>
- Loading branch information
Showing
10 changed files
with
669 additions
and
171 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import * as odataMetadata from 'odata-openapi'; | ||
import type { generateODataMetadata } from './odata-metadata-generator'; | ||
import _ = require('lodash'); | ||
// tslint:disable-next-line:no-var-requires | ||
|
||
export const generateODataMetadataAsOpenApi = ( | ||
odataCsdl: ReturnType<typeof generateODataMetadata>, | ||
versionBasePathUrl: string = '', | ||
hostname: string = '', | ||
) => { | ||
// console.log(`odataCsdl:${JSON.stringify(odataCsdl, null, 2)}`); | ||
const openAPIJson: any = odataMetadata.csdl2openapi(odataCsdl, { | ||
scheme: 'https', | ||
host: hostname, | ||
basePath: versionBasePathUrl, | ||
diagram: false, | ||
maxLevels: 5, | ||
}); | ||
|
||
/** | ||
* Manual rewriting OpenAPI specification to delete OData default functionality | ||
* that is not implemented in Pinejs yet or is based on PineJs implements OData V3. | ||
* | ||
* Rewrite odata body response schema properties from `value: ` to `d: ` | ||
* Currently pinejs is returning `d: ` | ||
* https://www.odata.org/documentation/odata-version-2-0/json-format/ (6. Representing Collections of Entries) | ||
* https://www.odata.org/documentation/odata-version-3-0/json-verbose-format/ (6.1 Response body) | ||
* | ||
* New v4 odata specifies the body response with `value: ` | ||
* http://docs.oasis-open.org/odata/odata-json-format/v4.01/odata-json-format-v4.01.html#sec_IndividualPropertyorOperationRespons | ||
* | ||
* | ||
* Currently pinejs does not implement a $count=true query parameter as this would return the count of all rows returned as an additional parameter. | ||
* This was not part of OData V3 and is new for OData V4. As the odata-openapi converte is opionionated on V4 the parameter is put into the schema. | ||
* Until this is in parity with OData V4 pinejs needs to cleanup the `odata.count` key from the response schema put in by `csdl2openapi` | ||
* | ||
* | ||
* Used oasis translator generates openapi according to v4 spec (`value: `) | ||
* | ||
* Unfortunantely odata-openapi does not export the genericFilter object. | ||
* Using hardcoded generic filter description as used in odata-openapi code. | ||
* Putting the genericFilter into the #/components/parameters/filter to reference it from paths | ||
* | ||
* */ | ||
const parameters = openAPIJson?.components?.parameters; | ||
parameters['filter'] = { | ||
name: '$filter', | ||
description: | ||
'Filter items by property values, see [Filtering](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionfilter)', | ||
in: 'query', | ||
schema: { | ||
type: 'string', | ||
}, | ||
}; | ||
|
||
for (const idx of Object.keys(openAPIJson.paths)) { | ||
// rewrite `value: ` to `d: ` | ||
const properties = | ||
openAPIJson?.paths[idx]?.get?.responses?.['200']?.content?.[ | ||
'application/json' | ||
]?.schema?.properties; | ||
if (properties?.value) { | ||
properties['d'] = properties.value; | ||
delete properties.value; | ||
} | ||
|
||
// cleanup the `odata.count` key from the response schema | ||
if (properties?.['@odata.count']) { | ||
delete properties['@odata.count']; | ||
} | ||
|
||
// copy over 'delete' and 'patch' action from single entiy path | ||
// odata-openAPI converter does not support collection delete and collection update. | ||
// pinejs support collection delete and update with $filter parameter | ||
const entityCollectionPath = openAPIJson?.paths[idx]; | ||
const singleEntityPath = openAPIJson?.paths[idx + '({id})']; | ||
if (entityCollectionPath != null && singleEntityPath != null) { | ||
const genericFilterParameterRef = { | ||
$ref: '#/components/parameters/filter', | ||
}; | ||
for (const action of ['delete', 'patch']) { | ||
entityCollectionPath[action] = _.clone(singleEntityPath?.[action]); | ||
if (entityCollectionPath[action]) { | ||
entityCollectionPath[action]['parameters'] = [ | ||
genericFilterParameterRef, | ||
]; | ||
} | ||
} | ||
} | ||
} | ||
|
||
// cleanup $batch path as pinejs does not implement it. | ||
// http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_BatchRequests | ||
if (openAPIJson?.paths['/$batch']) { | ||
delete openAPIJson.paths['/$batch']; | ||
} | ||
|
||
return openAPIJson; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { writeFileSync } from 'fs'; | ||
import { expect } from 'chai'; | ||
import supertest from 'supertest'; | ||
import { testInit, testDeInit, testLocalServer } from './lib/test-init'; | ||
|
||
import OpenAPIParser from '@readme/openapi-parser'; | ||
|
||
describe('08 metadata / openAPI spec', function () { | ||
describe('Full model access specification', async function () { | ||
const fixturePath = | ||
__dirname + '/fixtures/08-metadata/config-full-access.js'; | ||
let pineServer: Awaited<ReturnType<typeof testInit>>; | ||
before(async () => { | ||
pineServer = await testInit({ | ||
configPath: fixturePath, | ||
deleteDb: true, | ||
}); | ||
}); | ||
|
||
after(async () => { | ||
await testDeInit(pineServer); | ||
}); | ||
|
||
it('should send OData CSDL JSON on /$metadata', async () => { | ||
const res = await supertest(testLocalServer) | ||
.get('/example/$metadata') | ||
.expect(200); | ||
expect(res.body).to.be.an('object'); | ||
}); | ||
|
||
it('should send valid OpenAPI spec JSON on /$metadata', async () => { | ||
const { body } = await supertest(testLocalServer) | ||
.get('/example/openapi.json') | ||
.expect(200); | ||
expect(body).to.be.an('object'); | ||
|
||
const bodySpec = JSON.stringify(body, null, 2); | ||
await writeFileSync('openApiSpe-full.json', bodySpec); | ||
|
||
// validate the openAPI spec and expect no validator errors. | ||
try { | ||
const apiSpec = await OpenAPIParser.validate(JSON.parse(bodySpec)); | ||
expect(apiSpec).to.be.an('object'); | ||
} catch (err) { | ||
expect(err).to.be.undefined; | ||
} | ||
}); | ||
|
||
it('OpenAPI spec should contain all paths and actions on resources', async () => { | ||
// full CRUD access for device resource | ||
const res = await supertest(testLocalServer) | ||
.get('/example/openapi.json') | ||
.expect(200); | ||
expect(res.body).to.be.an('object'); | ||
|
||
// all collections should have get, patch, delete and post | ||
const singleIdPathRegEx = /\({id}\)/; | ||
for (const [path, value] of Object.entries(res.body.paths)) { | ||
if (!singleIdPathRegEx.exec(path)) { | ||
expect(value).to.have.keys(['get', 'patch', 'delete', 'post']); | ||
} | ||
} | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.