@sap/cds-dk 7.9.5 → 8.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2116 -0
- package/bin/add.js +18 -52
- package/bin/bind.js +2 -2
- package/bin/build.js +29 -4
- package/bin/cds.js +23 -11
- package/bin/compile/index.js +7 -6
- package/bin/completion/index.js +70 -46
- package/bin/completion/scripts/cds-bash.sh +4 -1
- package/bin/completion/scripts/cds-fish.fish +13 -0
- package/bin/completion/scripts/cds-ps.ps1 +2 -1
- package/bin/completion/scripts/cds-zsh.sh +2 -1
- package/bin/deploy/index.js +6 -5
- package/bin/deploy/to-sqlite.js +2 -2
- package/bin/extend.js +19 -50
- package/bin/help.js +2 -2
- package/bin/init.js +10 -77
- package/bin/login.js +13 -5
- package/bin/migrate.js +1 -1
- package/bin/pull.js +11 -0
- package/bin/repl.js +3 -0
- package/bin/test.js +5 -0
- package/bin/version.js +7 -7
- package/bin/watch.js +1 -10
- package/lib/bind/KubernetesApiAccessor.js +10 -9
- package/lib/build/buildTaskEngine.js +11 -7
- package/lib/build/buildTaskFactory.js +15 -43
- package/lib/build/buildTaskProviderFactory.js +16 -44
- package/lib/build/constants.js +3 -3
- package/lib/build/index.js +2 -2
- package/lib/build/{buildPlugin.js → plugin.js} +65 -46
- package/lib/{init/template/helm/files → build/plugins/helm/files/subcharts}/content-deployment/Chart.yaml +1 -1
- package/lib/{init/template/helm/files → build/plugins/helm/files/subcharts}/content-deployment/values.yaml +1 -1
- package/lib/{init/template/helm/files → build/plugins/helm/files/subcharts}/service-instance/Chart.yaml +1 -1
- package/lib/{init/template/helm/files → build/plugins/helm/files/subcharts}/service-instance/templates/_helpers.tpl +7 -0
- package/lib/build/plugins/helm/files/subcharts/service-instance/templates/servicebinding.yaml +32 -0
- package/lib/{init/template/helm/files → build/plugins/helm/files/subcharts}/service-instance/templates/serviceinstance.yaml +3 -0
- package/lib/{init/template/helm/files → build/plugins/helm/files/subcharts}/service-instance/values.schema.json +63 -0
- package/lib/{init/template/helm/files → build/plugins/helm/files/subcharts}/service-instance/values.yaml +23 -1
- package/lib/{init/template/helm/files → build/plugins/helm/files/subcharts}/web-application/Chart.yaml +1 -1
- package/lib/{init/template/helm/files → build/plugins/helm/files/subcharts}/web-application/templates/deployment.yaml +2 -0
- package/lib/build/plugins/helm/files/subcharts/web-application/templates/hpa.yaml +22 -0
- package/lib/{init/template/helm/files → build/plugins/helm/files/subcharts}/web-application/templates/pod-disruption-budget.yaml +1 -0
- package/lib/{init/template/helm/files → build/plugins/helm/files/subcharts}/web-application/values.schema.json +49 -0
- package/lib/{init/template/helm/files → build/plugins/helm/files/subcharts}/web-application/values.yaml +8 -2
- package/lib/{init/template/helm/files/chart → build/plugins/helm/files}/templates/_deployment_helpers.tpl +5 -0
- package/lib/build/plugins/helm/files/templates/_helpers.tpl +13 -0
- package/lib/build/plugins/helm/index.js +81 -0
- package/lib/build/plugins.js +12 -15
- package/lib/build/provider/featureToggleBuildPlugin.js +7 -39
- package/lib/build/provider/hana/2migration.js +168 -124
- package/lib/build/provider/hana/index.js +20 -25
- package/lib/build/provider/hana/template/package.json +4 -3
- package/lib/build/provider/internalBuildPlugin.js +32 -97
- package/lib/build/provider/internalBuildTaskProvider.js +10 -46
- package/lib/build/provider/java/index.js +2 -3
- package/lib/build/provider/mtx/index.js +5 -4
- package/lib/build/provider/mtx-extension/index.js +5 -2
- package/lib/build/provider/mtx-sidecar/index.js +2 -2
- package/lib/build/util.js +83 -12
- package/lib/compile/to-mermaid.js +108 -0
- package/lib/compile/to_asyncapi.js +7 -0
- package/lib/compile/to_openapi.js +7 -0
- package/lib/deploy/to-hana/hana.js +37 -34
- package/lib/deploy/to-hana/hdiDeployUtil.js +8 -6
- package/lib/env/schemas/cds-rc.js +1 -1
- package/lib/import/common.js +2 -2
- package/lib/import/odata/metadataconverter/MetadataConverter.js +4 -4
- package/lib/import/odata/v2parser.js +83 -197
- package/lib/import/odata/v4parser.js +1 -38
- package/lib/import/openapi/importOpenAPI.js +164 -42
- package/lib/index.js +10 -12
- package/lib/init/bas.js +91 -0
- package/lib/init/constants.js +16 -10
- package/lib/init/index.js +72 -150
- package/lib/init/merge.js +12 -20
- package/lib/init/mvn.js +103 -150
- package/lib/init/plugin.js +11 -7
- package/lib/init/projectReader.js +53 -19
- package/lib/init/registries/mta.js +51 -6
- package/lib/init/template/application-logging/files/package.json.hbs +7 -0
- package/lib/init/template/application-logging/index.js +13 -1
- package/lib/init/template/approuter/files/containerize.yaml.hbs +10 -0
- package/lib/init/template/approuter/files/mta.yaml.hbs +1 -2
- package/lib/init/template/approuter/files/values.yaml.hbs +19 -4
- package/lib/init/template/approuter/files/xs-app.json.hbs +12 -3
- package/lib/init/template/approuter/index.js +29 -25
- package/lib/init/template/attachments/files/mta.yaml.hbs +14 -0
- package/lib/init/template/attachments/files/package.json.hbs +5 -0
- package/lib/init/template/attachments/files/values.yaml +7 -0
- package/lib/init/template/attachments/index.js +53 -0
- package/lib/init/template/audit-logging/files/mta.yaml.hbs +14 -0
- package/lib/init/template/audit-logging/files/package.json.hbs +7 -0
- package/lib/init/template/audit-logging/index.js +66 -0
- package/lib/init/template/cf-manifest/index.js +4 -0
- package/lib/init/template/completion/completionSetup.js +54 -33
- package/lib/init/template/completion/index.js +7 -1
- package/lib/init/template/connectivity/index.js +4 -4
- package/lib/init/template/containerize/files/containerize.yaml.hbs +26 -0
- package/lib/init/template/containerize/index.js +24 -0
- package/lib/init/template/data/index.js +4 -0
- package/lib/init/template/destination/files/mta.yaml.hbs +25 -1
- package/lib/init/template/destination/index.js +12 -2
- package/lib/init/template/enterprise-messaging/index.js +6 -3
- package/lib/init/template/enterprise-messaging-shared/index.js +5 -1
- package/lib/init/template/extensibility/files/package.json.hbs +1 -1
- package/lib/init/template/extensibility/files/xs-security.json.hbs +2 -8
- package/lib/init/template/extensibility/index.js +3 -1
- package/lib/init/template/file-based-messaging/index.js +4 -0
- package/lib/init/template/h2/index.js +4 -0
- package/lib/init/template/hana/files/containerize.yaml.hbs +10 -0
- package/lib/init/template/hana/files/db/package.json +3 -2
- package/lib/init/template/hana/files/package.json.hbs +4 -7
- package/lib/init/template/hana/files/values.yaml.hbs +1 -2
- package/lib/init/template/hana/index.js +15 -7
- package/lib/init/template/helm/files/Chart.yaml.hbs +2 -3
- package/lib/init/template/helm/files/values.schema.json +2173 -0
- package/lib/init/template/helm/files/values.yaml.hbs +11 -6
- package/lib/init/template/helm/index.js +77 -18
- package/lib/init/template/helm-unified-runtime/index.js +19 -56
- package/lib/init/template/html5-repo/files/app-package.json.hbs +11 -7
- package/lib/init/template/html5-repo/files/manifest.json +13 -0
- package/lib/init/template/html5-repo/files/mta.yaml.hbs +41 -8
- package/lib/init/template/html5-repo/files/ui5-deploy.yaml.hbs +21 -0
- package/lib/init/template/html5-repo/files/ui5.yaml.hbs +2 -29
- package/lib/init/template/html5-repo/files/values.yaml.hbs +6 -2
- package/lib/init/template/html5-repo/files/xs-app.json.hbs +21 -8
- package/lib/init/template/html5-repo/index.js +33 -30
- package/lib/init/template/http/index.js +36 -12
- package/lib/init/template/java/index.js +18 -12
- package/lib/init/template/kafka/index.js +4 -0
- package/lib/init/template/lint/index.js +7 -2
- package/lib/init/template/liquibase/index.js +4 -0
- package/lib/init/template/local-messaging/index.js +4 -0
- package/lib/init/template/malware-scanner/files/mta.yaml.hbs +14 -0
- package/lib/init/template/malware-scanner/files/package.json.hbs +17 -0
- package/lib/init/template/malware-scanner/files/values.yaml +7 -0
- package/lib/init/template/malware-scanner/index.js +47 -0
- package/lib/init/template/mta/files/mta.yaml.hbs +3 -3
- package/lib/init/template/mta/files/package.json +1 -1
- package/lib/init/template/mta/index.js +4 -0
- package/lib/init/template/mtx/index.js +4 -0
- package/lib/init/template/multitenancy/files/containerize.yaml.hbs +10 -0
- package/lib/init/template/multitenancy/files/mta.yaml.hbs +0 -1
- package/lib/init/template/multitenancy/files/package.json.hbs +1 -1
- package/lib/init/template/multitenancy/files/package.sidecar.json.hbs +6 -5
- package/lib/init/template/multitenancy/files/values.yaml.hbs +14 -24
- package/lib/init/template/multitenancy/index.js +50 -28
- package/lib/init/template/nodejs/files/eslint.config.mjs +2 -0
- package/lib/init/template/nodejs/files/package.json.hbs +2 -3
- package/lib/init/template/nodejs/index.js +4 -0
- package/lib/init/template/notifications/files/mta.yaml.hbs +2 -2
- package/lib/init/template/notifications/index.js +4 -0
- package/lib/init/template/pipeline/files/.pipeline/config.yml +1 -1
- package/lib/init/template/pipeline/files/Jenkinsfile +1 -1
- package/lib/init/template/pipeline/index.js +4 -0
- package/lib/init/template/portal/files/mta.yaml.hbs +52 -0
- package/lib/init/template/portal/files/package.json.hbs +11 -0
- package/lib/init/template/portal/files/package.sidecar.json +7 -0
- package/lib/init/template/portal/files/portal-site/CommonDataModel.json.hbs +97 -0
- package/lib/init/template/portal/files/portal-site/i18n/catalog.properties +1 -0
- package/lib/init/template/portal/files/portal-site/i18n/catalog_de.properties +1 -0
- package/lib/init/template/portal/files/portal-site/i18n/group.properties +1 -0
- package/lib/init/template/portal/files/portal-site/i18n/group_de.properties +1 -0
- package/lib/init/template/portal/index.js +84 -0
- package/lib/init/template/postgres/index.js +5 -1
- package/lib/init/template/redis-messaging/index.js +4 -0
- package/lib/init/template/sample/files/app/admin-books/webapp/i18n/i18n_de.properties +3 -0
- package/lib/init/template/sample/files/app/admin-books/webapp/i18n/i18n_en.properties +3 -0
- package/lib/init/template/sample/files/app/admin-books/webapp/manifest.json +13 -1
- package/lib/init/template/sample/files/app/browse/webapp/i18n/i18n.properties +2 -2
- package/lib/init/template/sample/files/app/browse/webapp/i18n/i18n_de.properties +3 -0
- package/lib/init/template/sample/files/app/browse/webapp/i18n/i18n_en.properties +4 -0
- package/lib/init/template/sample/files/app/browse/webapp/manifest.json +3 -3
- package/lib/init/template/sample/files/app/index.html +1 -1
- package/lib/init/template/sample/files/db/data/sap.capire.bookshop-Authors.csv +5 -5
- package/lib/init/template/sample/files/db/data/sap.capire.bookshop-Books.csv +6 -6
- package/lib/init/template/sample/files/db/data/sap.capire.bookshop-Books_texts.csv +5 -5
- package/lib/init/template/sample/files/db/data/sap.capire.bookshop-Genres.csv +16 -16
- package/lib/init/template/sample/files/nodejs/cat-service.js +2 -2
- package/lib/init/template/sample/files/package.json.hbs +3 -0
- package/lib/init/template/sample/index.js +16 -9
- package/lib/init/template/sqlite/index.js +4 -0
- package/lib/init/template/tiny-sample/files/nodejs/db/data/my.bookshop-Books.csv +3 -3
- package/lib/init/template/tiny-sample/files/nodejs/srv/cat-service.cds.hbs +1 -1
- package/lib/init/template/tiny-sample/index.js +4 -0
- package/lib/init/template/toggles/files/package.json.hbs +1 -1
- package/lib/init/template/toggles/index.js +4 -0
- package/lib/init/template/typer/index.js +4 -0
- package/lib/init/template/typescript/files/package.json +12 -0
- package/lib/init/template/typescript/files/tsconfig.json +13 -0
- package/lib/init/template/typescript/index.js +39 -0
- package/lib/init/template/workzone/index.js +5 -0
- package/lib/init/template/workzone-standard/files/manifest.json.hbs +6 -0
- package/lib/init/template/workzone-standard/index.js +30 -0
- package/lib/init/template/xsuaa/files/package.json.hbs +1 -1
- package/lib/init/template/xsuaa/files/values.yaml.hbs +3 -2
- package/lib/init/template/xsuaa/index.js +11 -7
- package/lib/init/validate.js +27 -0
- package/lib/lint/generators.js +1 -1
- package/lib/lint/io.js +25 -7
- package/lib/mtx/auth_manager.js +64 -30
- package/lib/mtx/download_legacy.js +2 -2
- package/lib/mtx/index.js +0 -4
- package/lib/mtx/params.js +32 -38
- package/lib/mtx/pull.js +151 -43
- package/lib/mtx/push.js +3 -3
- package/lib/mtx/settings_manager.js +75 -35
- package/lib/mtx/tenant_action.js +1 -1
- package/lib/mtx/util/cf.js +1 -1
- package/lib/mtx/util/fs.js +26 -1
- package/lib/mtx/util/logging.js +7 -3
- package/lib/util/command.js +0 -2
- package/lib/util/fs.js +2 -3
- package/lib/util/question.js +28 -0
- package/lib/util/term.js +5 -3
- package/npm-shrinkwrap.json +226 -1087
- package/package.json +13 -8
- package/bin/activate.js +0 -77
- package/lib/compile/asyncapi/channels.js +0 -22
- package/lib/compile/asyncapi/components/messageTraits/index.js +0 -89
- package/lib/compile/asyncapi/components/messages/index.js +0 -173
- package/lib/compile/asyncapi/components/schemas/annotations.js +0 -33
- package/lib/compile/asyncapi/components/schemas/array.js +0 -8
- package/lib/compile/asyncapi/components/schemas/cache.js +0 -14
- package/lib/compile/asyncapi/components/schemas/definition.js +0 -187
- package/lib/compile/asyncapi/components/schemas/enum.js +0 -16
- package/lib/compile/asyncapi/components/schemas/examples.js +0 -10
- package/lib/compile/asyncapi/components/schemas/index.js +0 -14
- package/lib/compile/asyncapi/components/schemas/localized.js +0 -29
- package/lib/compile/asyncapi/components/schemas/primitiveElement.js +0 -41
- package/lib/compile/asyncapi/components/schemas/ref.js +0 -45
- package/lib/compile/asyncapi/components/schemas/utils.js +0 -108
- package/lib/compile/asyncapi/index.js +0 -247
- package/lib/compile/asyncapi/message.js +0 -13
- package/lib/compile/asyncapi/utils.js +0 -27
- package/lib/compile/openapi/csdl2openapi.js +0 -2448
- package/lib/compile/openapi/index.js +0 -68
- package/lib/init/template/helm/files/chart/templates/NOTES.txt +0 -14
- package/lib/init/template/helm/files/chart/templates/_helpers.tpl +0 -28
- package/lib/init/template/helm/files/chart/values.root.schema.json +0 -61
- package/lib/init/template/helm/files/chart/values.schema.json +0 -366
- package/lib/init/template/helm-unified-runtime/files/Chart.yaml.hbs +0 -10
- package/lib/init/template/helm-unified-runtime/files/templates/_deployment_helpers.tpl +0 -90
- package/lib/init/template/helm-unified-runtime/files/templates/_helpers.tpl +0 -28
- package/lib/init/template/helm-unified-runtime/files/values.root.schema.json +0 -61
- package/lib/init/template/helm-unified-runtime/files/values.schema.json +0 -366
- package/lib/init/template/helm-unified-runtime/files/values.yaml.hbs +0 -26
- package/lib/init/template/helm-unified-runtime/package.json +0 -4
- package/lib/init/template/multitenancy/files/mtxs-configmap-java-approuter.yaml +0 -20
- package/lib/init/template/multitenancy/files/mtxs-configmap-java.yaml +0 -18
- package/lib/init/template/multitenancy/files/mtxs-configmap-nodejs-approuter.yaml +0 -12
- package/lib/init/template/multitenancy/files/mtxs-configmap-nodejs.yaml +0 -12
- package/lib/init/template/multitenancy/files/saas-registry-secret.yaml +0 -9
- package/lib/init/template/nodejs/files/eslint.config.js +0 -13
- package/lib/init/template/notebook/index.js +0 -11
- package/lib/init/template/samples/index.js +0 -9
- package/lib/mtx/activate.js +0 -238
- package/lib/mtx/extend.js +0 -173
- package/lib/util/exec.d.ts +0 -54
- package/lib/util/exec.js +0 -211
- /package/lib/{init/template/approuter/files → build/plugins/helm/files/plugins/approuter}/approuter-configmap.yaml +0 -0
- /package/lib/{init/template/connectivity/files → build/plugins/helm/files/plugins/connectivity}/connectivity-proxy-info.yaml +0 -0
- /package/lib/{init/template/html5-repo/files → build/plugins/helm/files/plugins/html5-repo}/html5-apps-deployer-configmap.yaml +0 -0
- /package/lib/{init/template/helm/files → build/plugins/helm/files/subcharts}/content-deployment/templates/_helpers.tpl +0 -0
- /package/lib/{init/template/helm/files → build/plugins/helm/files/subcharts}/content-deployment/templates/job.yaml +0 -0
- /package/lib/{init/template/helm/files → build/plugins/helm/files/subcharts}/content-deployment/templates/secret.yaml +0 -0
- /package/lib/{init/template/helm/files → build/plugins/helm/files/subcharts}/content-deployment/templates/service-account.yaml +0 -0
- /package/lib/{init/template/helm/files → build/plugins/helm/files/subcharts}/content-deployment/templates/service-binding.yaml +0 -0
- /package/lib/{init/template/helm/files → build/plugins/helm/files/subcharts}/content-deployment/values.schema.json +0 -0
- /package/lib/{init/template/helm/files → build/plugins/helm/files/subcharts}/web-application/templates/NOTES.txt +0 -0
- /package/lib/{init/template/helm/files → build/plugins/helm/files/subcharts}/web-application/templates/_api.tpl +0 -0
- /package/lib/{init/template/helm/files → build/plugins/helm/files/subcharts}/web-application/templates/_helpers.tpl +0 -0
- /package/lib/{init/template/helm/files → build/plugins/helm/files/subcharts}/web-application/templates/api-rule.yaml +0 -0
- /package/lib/{init/template/helm/files → build/plugins/helm/files/subcharts}/web-application/templates/network-policy.yaml +0 -0
- /package/lib/{init/template/helm/files → build/plugins/helm/files/subcharts}/web-application/templates/rbac.yaml +0 -0
- /package/lib/{init/template/helm/files → build/plugins/helm/files/subcharts}/web-application/templates/secret.yaml +0 -0
- /package/lib/{init/template/helm/files → build/plugins/helm/files/subcharts}/web-application/templates/service-account.yaml +0 -0
- /package/lib/{init/template/helm/files → build/plugins/helm/files/subcharts}/web-application/templates/service-binding.yaml +0 -0
- /package/lib/{init/template/helm/files → build/plugins/helm/files/subcharts}/web-application/templates/service.yaml +0 -0
- /package/lib/{init/template/helm/files → build/plugins/helm/files/subcharts}/web-application/templates/virtual-service.yaml +0 -0
- /package/lib/{init/template/helm-unified-runtime → build/plugins/helm}/files/templates/NOTES.txt +0 -0
- /package/lib/init/template/{nodejs/files → java}/.cdsrc.json +0 -0
- /package/lib/init/template/tiny-sample/files/nodejs/db/{data-model.cds → schema.cds} +0 -0
|
@@ -1,2448 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Converts OData CSDL JSON to OpenAPI 3.0.2
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
var pluralize = require('pluralize')
|
|
6
|
-
|
|
7
|
-
//TODO
|
|
8
|
-
// - Core.Example for complex types
|
|
9
|
-
// - reduce number of loops over schemas
|
|
10
|
-
// - inject $$name into each model element to make parameter passing easier?
|
|
11
|
-
// - allow passing additional files for referenced documents
|
|
12
|
-
// - delta: headers Prefer and Preference-Applied
|
|
13
|
-
// - inline definitions for Edm.* to make OpenAPI documents self-contained
|
|
14
|
-
// - both "clickable" and freestyle $expand, $select, $orderby - does not work yet, open issue for OpenAPI UI
|
|
15
|
-
// - system query options for actions/functions/imports depending on $Collection
|
|
16
|
-
// - 200 response for PATCH
|
|
17
|
-
// - ETag for GET / If-Match for PATCH and DELETE depending on @Core.OptimisticConcurrency
|
|
18
|
-
// - CountRestrictions for GET collection-valued (containment) navigation - https://issues.oasis-open.org/browse/ODATA-1300
|
|
19
|
-
// - InsertRestrictions/NonInsertableProperties
|
|
20
|
-
// - InsertRestrictions/NonInsertableNavigationProperties
|
|
21
|
-
// - see //TODO comments below
|
|
22
|
-
|
|
23
|
-
const SUFFIX = {
|
|
24
|
-
read: "",
|
|
25
|
-
create: "-create",
|
|
26
|
-
update: "-update",
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const TITLE_SUFFIX = {
|
|
30
|
-
"": "",
|
|
31
|
-
"-create": " (for create)",
|
|
32
|
-
"-update": " (for update)",
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* ODM annotations in CDS that should be converted into OpenAPI.
|
|
37
|
-
*/
|
|
38
|
-
const ODM_ANNOTATIONS = Object.freeze(
|
|
39
|
-
{
|
|
40
|
-
'@ODM.entityName': 'x-sap-odm-entity-name',
|
|
41
|
-
'@ODM.oid': 'x-sap-odm-oid'
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Construct an OpenAPI description from a CSDL document
|
|
46
|
-
* @param {object} csdl CSDL document
|
|
47
|
-
* @param {object} options Optional parameters
|
|
48
|
-
* @return {object} OpenAPI description
|
|
49
|
-
*/
|
|
50
|
-
module.exports.csdl2openapi = function (
|
|
51
|
-
csdl,
|
|
52
|
-
{
|
|
53
|
-
url: serviceRoot,
|
|
54
|
-
servers: serversObject,
|
|
55
|
-
odataVersion: odataVersion,
|
|
56
|
-
scheme: scheme = 'https',
|
|
57
|
-
host: host = 'localhost',
|
|
58
|
-
basePath: basePath = '/service-root',
|
|
59
|
-
diagram: diagram = false,
|
|
60
|
-
maxLevels: maxLevels = 5
|
|
61
|
-
} = {}
|
|
62
|
-
) {
|
|
63
|
-
// as preProcess below mutates the csdl, copy it before, to avoid side-effects on the caller side
|
|
64
|
-
csdl = JSON.parse(JSON.stringify(csdl))
|
|
65
|
-
csdl.$Version = odataVersion ? odataVersion : '4.01'
|
|
66
|
-
serviceRoot = serviceRoot || (scheme + '://' + host + basePath);
|
|
67
|
-
const queryOptionPrefix = csdl.$Version <= '4.01' ? '$' : '';
|
|
68
|
-
const typesToInline = {}; // filled in schema() and used in inlineTypes()
|
|
69
|
-
const boundOverloads = {};
|
|
70
|
-
const derivedTypes = {};
|
|
71
|
-
const alias = {};
|
|
72
|
-
const namespace = { 'Edm': 'Edm' };
|
|
73
|
-
const namespaceUrl = {};
|
|
74
|
-
const voc = {};
|
|
75
|
-
const requiredSchemas = { list: [], used: {} };
|
|
76
|
-
|
|
77
|
-
preProcess(csdl, boundOverloads, derivedTypes, alias, namespace, namespaceUrl, voc);
|
|
78
|
-
|
|
79
|
-
const entityContainer = csdl.$EntityContainer ? modelElement(csdl.$EntityContainer) : {};
|
|
80
|
-
if (csdl.$EntityContainer) {
|
|
81
|
-
let serviceName = nameParts(csdl.$EntityContainer).qualifier;
|
|
82
|
-
Object.keys(entityContainer).forEach(element => {
|
|
83
|
-
if (entityContainer[element].$Type) {
|
|
84
|
-
let type = nameParts(entityContainer[element].$Type).name;
|
|
85
|
-
if (csdl[serviceName]?.[type]?.['@cds.autoexpose'] || csdl[serviceName]?.[type]?.['@cds.autoexposed'])
|
|
86
|
-
entityContainer[element]['$cds.autoexpose'] = true;
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const keyAsSegment = entityContainer ? entityContainer[voc.Capabilities.KeyAsSegmentSupported] : {};
|
|
92
|
-
|
|
93
|
-
const openapi = {
|
|
94
|
-
openapi: '3.0.2',
|
|
95
|
-
info: getInfo(csdl, entityContainer),
|
|
96
|
-
'x-sap-api-type': 'ODATAV4',
|
|
97
|
-
'x-odata-version': csdl.$Version,
|
|
98
|
-
'x-sap-shortText': getShortText(csdl, entityContainer),
|
|
99
|
-
servers: getServers(serviceRoot, serversObject),
|
|
100
|
-
tags: entityContainer ? getTags(entityContainer) : {},
|
|
101
|
-
paths: entityContainer ? getPaths(entityContainer) : {},
|
|
102
|
-
components: getComponents(csdl, entityContainer)
|
|
103
|
-
};
|
|
104
|
-
if (!csdl.$EntityContainer) {
|
|
105
|
-
delete openapi.servers;
|
|
106
|
-
delete openapi.tags;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
security(openapi, entityContainer);
|
|
110
|
-
|
|
111
|
-
return openapi;
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Collect model info for easier lookup
|
|
116
|
-
* @param {object} csdl CSDL document
|
|
117
|
-
* @param {object} boundOverloads Map of action/function names to bound overloads
|
|
118
|
-
* @param {object} derivedTypes Map of type names to derived types
|
|
119
|
-
* @param {object} alias Map of namespace or alias to alias
|
|
120
|
-
* @param {object} namespace Map of namespace or alias to namespace
|
|
121
|
-
* @param {object} namespaceUrl Map of namespace to reference URL
|
|
122
|
-
* @param {object} voc Map of vocabularies and terms
|
|
123
|
-
*/
|
|
124
|
-
function preProcess(csdl, boundOverloads, derivedTypes, alias, namespace, namespaceUrl, voc) {
|
|
125
|
-
Object.keys(csdl.$Reference || {}).forEach(url => {
|
|
126
|
-
const reference = csdl.$Reference[url];
|
|
127
|
-
(reference.$Include || []).forEach(include => {
|
|
128
|
-
const qualifier = include.$Alias || include.$Namespace;
|
|
129
|
-
alias[include.$Namespace] = qualifier;
|
|
130
|
-
namespace[qualifier] = include.$Namespace;
|
|
131
|
-
namespace[include.$Namespace] = include.$Namespace;
|
|
132
|
-
namespaceUrl[include.$Namespace] = url;
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
getVocabularies(voc, alias);
|
|
137
|
-
|
|
138
|
-
Object.keys(csdl).filter(name => isIdentifier(name)).forEach(name => {
|
|
139
|
-
const schema = csdl[name];
|
|
140
|
-
const qualifier = schema.$Alias || name;
|
|
141
|
-
const isDefaultNamespace = schema[voc.Core.DefaultNamespace];
|
|
142
|
-
|
|
143
|
-
alias[name] = qualifier;
|
|
144
|
-
namespace[qualifier] = name;
|
|
145
|
-
namespace[name] = name;
|
|
146
|
-
|
|
147
|
-
Object.keys(schema).filter(iName => isIdentifier(iName)).forEach(iName2 => {
|
|
148
|
-
const qualifiedName = qualifier + '.' + iName2;
|
|
149
|
-
const element = schema[iName2];
|
|
150
|
-
if (Array.isArray(element)) {
|
|
151
|
-
element.filter(overload => overload.$IsBound).forEach(overload => {
|
|
152
|
-
const type = overload.$Parameter[0].$Type + (overload.$Parameter[0].$Collection ? '-c' : '');
|
|
153
|
-
if (!boundOverloads[type]) boundOverloads[type] = [];
|
|
154
|
-
boundOverloads[type].push({ name: (isDefaultNamespace ? iName2 : qualifiedName), overload: overload });
|
|
155
|
-
});
|
|
156
|
-
} else if (element.$BaseType) {
|
|
157
|
-
const base = namespaceQualifiedName(element.$BaseType);
|
|
158
|
-
if (!derivedTypes[base]) derivedTypes[base] = [];
|
|
159
|
-
derivedTypes[base].push(qualifiedName);
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
Object.keys(schema.$Annotations || {}).forEach(target => {
|
|
164
|
-
const annotations = schema.$Annotations[target];
|
|
165
|
-
const segments = target.split('/');
|
|
166
|
-
const open = segments[0].indexOf('(');
|
|
167
|
-
let element;
|
|
168
|
-
if (open == -1) {
|
|
169
|
-
element = modelElement(segments[0]);
|
|
170
|
-
} else {
|
|
171
|
-
element = modelElement(segments[0].substring(0, open));
|
|
172
|
-
let args = segments[0].substring(open + 1, segments[0].length - 1);
|
|
173
|
-
element = element.find(
|
|
174
|
-
(overload) =>
|
|
175
|
-
(overload.$Kind == "Action" &&
|
|
176
|
-
overload.$IsBound != true &&
|
|
177
|
-
args == "") ||
|
|
178
|
-
(overload.$Kind == "Action" &&
|
|
179
|
-
args ==
|
|
180
|
-
(overload.$Parameter[0].$Collection
|
|
181
|
-
? `Collection(${overload.$Parameter[0].$Type})`
|
|
182
|
-
: overload.$Parameter[0].$Type || "")) ||
|
|
183
|
-
(overload.$Parameter || [])
|
|
184
|
-
.map((p) => {
|
|
185
|
-
const type = p.$Type || "Edm.String";
|
|
186
|
-
return p.$Collection ? `Collection(${type})` : type;
|
|
187
|
-
})
|
|
188
|
-
.join(",") == args
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
if (!element) {
|
|
192
|
-
console.warn(`Invalid annotation target '${target}'`);
|
|
193
|
-
} else if (Array.isArray(element)) {
|
|
194
|
-
//TODO: action or function:
|
|
195
|
-
//- loop over all overloads
|
|
196
|
-
//- if there are more segments, a parameter or the return type is targeted
|
|
197
|
-
} else {
|
|
198
|
-
switch (segments.length) {
|
|
199
|
-
case 1:
|
|
200
|
-
Object.assign(element, annotations);
|
|
201
|
-
break;
|
|
202
|
-
case 2:
|
|
203
|
-
if (['Action', 'Function'].includes(element.$Kind)) {
|
|
204
|
-
if (segments[1] == '$ReturnType') {
|
|
205
|
-
if (element.$ReturnType)
|
|
206
|
-
Object.assign(element.$ReturnType, annotations);
|
|
207
|
-
} else {
|
|
208
|
-
const parameter = element.$Parameter.find(p => p.$Name == segments[1]);
|
|
209
|
-
Object.assign(parameter, annotations);
|
|
210
|
-
}
|
|
211
|
-
} else {
|
|
212
|
-
if (element[segments[1]]) {
|
|
213
|
-
Object.assign(element[segments[1]], annotations);
|
|
214
|
-
} else {
|
|
215
|
-
// console.warn(`Invalid annotation target '${target}'`)
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
break;
|
|
219
|
-
default:
|
|
220
|
-
console.warn('More than two annotation target path segments');
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Construct map of qualified term names
|
|
229
|
-
* @param {object} voc Map of vocabularies and terms
|
|
230
|
-
* @param {object} alias Map of namespace or alias to alias
|
|
231
|
-
*/
|
|
232
|
-
function getVocabularies(voc, alias) {
|
|
233
|
-
const terms = {
|
|
234
|
-
Authorization: ['Authorizations', 'SecuritySchemes'],
|
|
235
|
-
Capabilities: ['BatchSupport', 'BatchSupported', 'ChangeTracking', 'CountRestrictions', 'DeleteRestrictions', 'DeepUpdateSupport', 'ExpandRestrictions',
|
|
236
|
-
'FilterRestrictions', 'IndexableByKey', 'InsertRestrictions', 'KeyAsSegmentSupported', 'NavigationRestrictions', 'OperationRestrictions',
|
|
237
|
-
'ReadRestrictions', 'SearchRestrictions', 'SelectSupport', 'SkipSupported', 'SortRestrictions', 'TopSupported', 'UpdateRestrictions'],
|
|
238
|
-
Core: ['AcceptableMediaTypes', 'Computed', 'DefaultNamespace', 'Description', 'Example', 'Immutable', 'LongDescription',
|
|
239
|
-
'OptionalParameter', 'Permissions', 'SchemaVersion'],
|
|
240
|
-
JSON: ['Schema'],
|
|
241
|
-
Validation: ['AllowedValues', 'Exclusive', 'Maximum', 'Minimum', 'Pattern']
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
Object.keys(terms).forEach(vocab => {
|
|
245
|
-
voc[vocab] = {};
|
|
246
|
-
terms[vocab].forEach(term => {
|
|
247
|
-
if (alias['Org.OData.' + vocab + '.V1'] != undefined)
|
|
248
|
-
voc[vocab][term] = '@' + alias['Org.OData.' + vocab + '.V1'] + '.' + term;
|
|
249
|
-
});
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
voc.Common = {
|
|
253
|
-
Label: `@${alias['com.sap.vocabularies.Common.v1']}.Label`
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Construct the Info Object
|
|
259
|
-
* @param {object} csdl CSDL document
|
|
260
|
-
* @param {object} entityContainer Entity Container object
|
|
261
|
-
* @return {object} Info Object
|
|
262
|
-
*/
|
|
263
|
-
function getInfo(csdl, entityContainer) {
|
|
264
|
-
const namespace = csdl.$EntityContainer ? nameParts(csdl.$EntityContainer).qualifier : null;
|
|
265
|
-
const containerSchema = csdl.$EntityContainer ? csdl[namespace] : {};
|
|
266
|
-
let description;
|
|
267
|
-
if (entityContainer && entityContainer[voc.Core.LongDescription]) {
|
|
268
|
-
description = entityContainer[voc.Core.LongDescription];
|
|
269
|
-
}
|
|
270
|
-
else if (containerSchema && containerSchema[voc.Core.LongDescription]) {
|
|
271
|
-
description = containerSchema[voc.Core.LongDescription];
|
|
272
|
-
}
|
|
273
|
-
else {
|
|
274
|
-
description = ('This service is located at [' + serviceRoot + '/]('
|
|
275
|
-
+ serviceRoot.replace(/\(/g, '%28').replace(/\)/g, '%29') + '/)');
|
|
276
|
-
}
|
|
277
|
-
description += (diagram ? getResourceDiagram(csdl, entityContainer) : '');
|
|
278
|
-
let title;
|
|
279
|
-
if (entityContainer && entityContainer[voc.Common.Label]) {
|
|
280
|
-
title = entityContainer[voc.Common.Label];
|
|
281
|
-
}
|
|
282
|
-
else if (entityContainer && entityContainer[voc.Core.Description]) {
|
|
283
|
-
title = entityContainer[voc.Core.Description];
|
|
284
|
-
}
|
|
285
|
-
else if (containerSchema && containerSchema[voc.Core.Description]) {
|
|
286
|
-
title = containerSchema[voc.Core.Description];
|
|
287
|
-
}
|
|
288
|
-
else if (csdl.$EntityContainer) {
|
|
289
|
-
title = 'Service for namespace ' + namespace;
|
|
290
|
-
}
|
|
291
|
-
else {
|
|
292
|
-
title = 'OData CSDL document';
|
|
293
|
-
}
|
|
294
|
-
return {
|
|
295
|
-
title: title,
|
|
296
|
-
description: csdl.$EntityContainer ? description : '',
|
|
297
|
-
version: containerSchema[voc.Core.SchemaVersion] || ''
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Construct resource diagram using web service at https://yuml.me
|
|
303
|
-
* @param {object} csdl CSDL document
|
|
304
|
-
* @param {object} entityContainer Entity Container object
|
|
305
|
-
* @return {string} resource diagram
|
|
306
|
-
*/
|
|
307
|
-
function getResourceDiagram(csdl, entityContainer) {
|
|
308
|
-
let diagram = '';
|
|
309
|
-
let comma = '';
|
|
310
|
-
//TODO: make colors configurable
|
|
311
|
-
let color = { resource: '{bg:lawngreen}', entityType: '{bg:lightslategray}', complexType: '', external: '{bg:whitesmoke}' }
|
|
312
|
-
|
|
313
|
-
Object.keys(csdl).filter(name => isIdentifier(name)).forEach(namespace => {
|
|
314
|
-
const schema = csdl[namespace];
|
|
315
|
-
Object.keys(schema).filter(name => isIdentifier(name) && ['EntityType', 'ComplexType'].includes(schema[name].$Kind))
|
|
316
|
-
.forEach(typeName => {
|
|
317
|
-
const type = schema[typeName];
|
|
318
|
-
diagram += comma
|
|
319
|
-
+ (type.$BaseType ? '[' + nameParts(type.$BaseType).name + ']^' : '')
|
|
320
|
-
+ '[' + typeName + (type.$Kind == 'EntityType' ? color.entityType : color.complexType) + ']';
|
|
321
|
-
Object.keys(type).filter(name => isIdentifier(name)).forEach(propertyName => {
|
|
322
|
-
const property = type[propertyName];
|
|
323
|
-
const targetNP = nameParts(property.$Type || 'Edm.String');
|
|
324
|
-
if (property.$Kind == 'NavigationProperty' || targetNP.qualifier != 'Edm') {
|
|
325
|
-
const target = modelElement(property.$Type);
|
|
326
|
-
const bidirectional = property.$Partner && target && target[property.$Partner] && target[property.$Partner].$Partner == propertyName;
|
|
327
|
-
// Note: if the partner has the same name then it will also be depicted
|
|
328
|
-
if (!bidirectional || propertyName <= property.$Partner) {
|
|
329
|
-
diagram += ',[' + typeName + ']'
|
|
330
|
-
+ ((property.$Kind != 'NavigationProperty' || property.$ContainsTarget) ? '++' : (bidirectional ? cardinality(target[property.$Partner]) : ''))
|
|
331
|
-
+ '-'
|
|
332
|
-
+ cardinality(property)
|
|
333
|
-
+ ((property.$Kind != 'NavigationProperty' || bidirectional) ? '' : '>')
|
|
334
|
-
+ '['
|
|
335
|
-
+ (target ? targetNP.name : property.$Type + color.external)
|
|
336
|
-
+ ']';
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
});
|
|
340
|
-
comma = ',';
|
|
341
|
-
});
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
Object.keys(entityContainer).filter(name => isIdentifier(name)).reverse().forEach(name => {
|
|
345
|
-
const resource = entityContainer[name];
|
|
346
|
-
if (resource.$Type) {
|
|
347
|
-
diagram += comma
|
|
348
|
-
+ '[' + name + '%20' + color.resource + ']' // additional space in case entity set and type have same name
|
|
349
|
-
+ '++-'
|
|
350
|
-
+ cardinality(resource)
|
|
351
|
-
+ '>[' + nameParts(resource.$Type).name + ']';
|
|
352
|
-
} else {
|
|
353
|
-
if (resource.$Action) {
|
|
354
|
-
diagram += comma
|
|
355
|
-
+ '[' + name + color.resource + ']';
|
|
356
|
-
const overload = modelElement(resource.$Action).find(pOverload => !pOverload.$IsBound);
|
|
357
|
-
diagram += overloadDiagram(name, overload);
|
|
358
|
-
} else if (resource.$Function) {
|
|
359
|
-
diagram += comma
|
|
360
|
-
+ '[' + name + color.resource + ']';
|
|
361
|
-
const overloads = modelElement(resource.$Function);
|
|
362
|
-
if (overloads) {
|
|
363
|
-
const unbound = overloads.filter(overload => !overload.$IsBound);
|
|
364
|
-
// TODO: loop over all overloads, add new source box after first arrow
|
|
365
|
-
diagram += overloadDiagram(name, unbound[0]);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
if (diagram != '') {
|
|
372
|
-
diagram = '\n\n## Entity Data Model\n\n\n### Legend\n';
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
return diagram;
|
|
380
|
-
|
|
381
|
-
/**
|
|
382
|
-
* Diagram representation of property cardinality
|
|
383
|
-
* @param {object} typedElement Typed model element, e.g. property
|
|
384
|
-
* @return {string} cardinality
|
|
385
|
-
*/
|
|
386
|
-
function cardinality(typedElement) {
|
|
387
|
-
return typedElement.$Collection ? '*' : (typedElement.$Nullable ? '0..1' : '');
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* Diagram representation of action or function overload
|
|
392
|
-
* @param {string} name Name of action or function import
|
|
393
|
-
* @param {object} overload Action or function overload
|
|
394
|
-
* @return {string} diagram part
|
|
395
|
-
*/
|
|
396
|
-
function overloadDiagram(name, overload) {
|
|
397
|
-
let diag = "";
|
|
398
|
-
if (overload.$ReturnType) {
|
|
399
|
-
const type = modelElement(overload.$ReturnType.$Type || "Edm.String");
|
|
400
|
-
if (type) {
|
|
401
|
-
diag += "-" + cardinality(overload.$ReturnType) + ">[" + nameParts(overload.$ReturnType.$Type).name + "]";
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
for (const param of overload.$Parameter || []) {
|
|
405
|
-
const type = modelElement(param.$Type || "Edm.String");
|
|
406
|
-
if (type) {
|
|
407
|
-
diag += comma + "[" + name + color.resource + "]in-" + cardinality(param.$Type) + ">[" + nameParts(param.$Type).name + "]";
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
return diag;
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Find model element by qualified name
|
|
416
|
-
* @param {string} qname Qualified name of model element
|
|
417
|
-
* @return {object} Model element
|
|
418
|
-
*/
|
|
419
|
-
function modelElement(qname) {
|
|
420
|
-
const q = nameParts(qname);
|
|
421
|
-
const schema = csdl[q.qualifier] || csdl[namespace[q.qualifier]];
|
|
422
|
-
return schema ? schema[q.name] : null;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
/**
|
|
426
|
-
* Construct the short text
|
|
427
|
-
* @param {object} csdl CSDL document
|
|
428
|
-
* @param {object} entityContainer Entity Container object
|
|
429
|
-
* @return {string} short text
|
|
430
|
-
*/
|
|
431
|
-
function getShortText(csdl, entityContainer) {
|
|
432
|
-
const namespace = csdl.$EntityContainer ? nameParts(csdl.$EntityContainer).qualifier : null;
|
|
433
|
-
const containerSchema = csdl.$EntityContainer ? csdl[namespace] : {};
|
|
434
|
-
let shortText;
|
|
435
|
-
if (entityContainer && entityContainer[voc.Core.Description]) {
|
|
436
|
-
shortText = entityContainer[voc.Core.Description];
|
|
437
|
-
}
|
|
438
|
-
else if (containerSchema && containerSchema[voc.Core.Description]) {
|
|
439
|
-
shortText = containerSchema[voc.Core.Description];
|
|
440
|
-
}
|
|
441
|
-
else if (csdl.$EntityContainer) {
|
|
442
|
-
shortText = 'Service for namespace ' + namespace;
|
|
443
|
-
}
|
|
444
|
-
else {
|
|
445
|
-
shortText = 'OData CSDL document';
|
|
446
|
-
}
|
|
447
|
-
return shortText;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* Construct an array of Server Objects
|
|
452
|
-
* @param {object} serviceRoot The service root
|
|
453
|
-
* @param {object} serversObject Input servers object
|
|
454
|
-
* @return {Array} The list of servers
|
|
455
|
-
*/
|
|
456
|
-
function getServers(serviceRoot, serversObject) {
|
|
457
|
-
let servers;
|
|
458
|
-
|
|
459
|
-
if (serversObject) {
|
|
460
|
-
try {
|
|
461
|
-
servers = JSON.parse(serversObject);
|
|
462
|
-
} catch (err) {
|
|
463
|
-
throw new Error(`The input server object is invalid.`);
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
if (!servers.length) {
|
|
467
|
-
throw new Error(`The input server object should be an array.`);
|
|
468
|
-
}
|
|
469
|
-
} else {
|
|
470
|
-
servers = [{ url: serviceRoot }];
|
|
471
|
-
}
|
|
472
|
-
return servers;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
/**
|
|
476
|
-
* Construct an array of Tag Objects from the entity container
|
|
477
|
-
* @param {object} container The entity container
|
|
478
|
-
* @return {Array} The list of tags
|
|
479
|
-
*/
|
|
480
|
-
function getTags(container) {
|
|
481
|
-
const tags = [];
|
|
482
|
-
// all entity sets and singletons
|
|
483
|
-
Object.keys(container).filter(name => isIdentifier(name) && container[name].$Type).forEach(child => {
|
|
484
|
-
const type = modelElement(container[child].$Type) || {};
|
|
485
|
-
const tag = {
|
|
486
|
-
name: type[voc.Common.Label] || child
|
|
487
|
-
};
|
|
488
|
-
const description = container[child][voc.Core.Description] || type[voc.Core.Description];
|
|
489
|
-
if (description) tag.description = description;
|
|
490
|
-
tags.push(tag);
|
|
491
|
-
})
|
|
492
|
-
return tags.sort(function (pre, next) { return pre.name.localeCompare(next.name) });
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
/**
|
|
496
|
-
* Construct the Paths Object from the entity container
|
|
497
|
-
* @param {object} container Entity container
|
|
498
|
-
* @return {object} Paths Object
|
|
499
|
-
*/
|
|
500
|
-
function getPaths(container) {
|
|
501
|
-
const paths = {};
|
|
502
|
-
const resources = Object.keys(container).filter(name => isIdentifier(name));
|
|
503
|
-
resources.forEach(name => {
|
|
504
|
-
let child = container[name];
|
|
505
|
-
if (child.$Type) {
|
|
506
|
-
const type = modelElement(child.$Type);
|
|
507
|
-
const sourceName = (type && type[voc.Common.Label]) || name;
|
|
508
|
-
// entity sets and singletons are almost containment navigation properties
|
|
509
|
-
child.$ContainsTarget = true;
|
|
510
|
-
pathItems(paths, '/' + name, [], child, child, sourceName, sourceName, child, 0, '');
|
|
511
|
-
} else if (child.$Action) {
|
|
512
|
-
pathItemActionImport(paths, name, child);
|
|
513
|
-
} else if (child.$Function) {
|
|
514
|
-
pathItemFunctionImport(paths, name, child);
|
|
515
|
-
} else {
|
|
516
|
-
console.warn('Unrecognized entity container child: ' + name);
|
|
517
|
-
}
|
|
518
|
-
})
|
|
519
|
-
if (resources.length > 0) pathItemBatch(paths, container);
|
|
520
|
-
return Object.keys(paths).sort().reduce((p, c) => (p[c] = paths[c], p), {});
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
/**
|
|
524
|
-
* Add path and Path Item Object for a navigation segment
|
|
525
|
-
* @param {object} paths Paths Object to augment
|
|
526
|
-
* @param {string} prefix Prefix for path
|
|
527
|
-
* @param {Array} prefixParameters Parameter Objects for prefix
|
|
528
|
-
* @param {object} element Model element of navigation segment
|
|
529
|
-
* @param {object} root Root model element
|
|
530
|
-
* @param {string} sourceName Name of path source
|
|
531
|
-
* @param {string} targetName Name of path target
|
|
532
|
-
* @param {string} target Target container child of path
|
|
533
|
-
* @param {integer} level Number of navigation segments so far
|
|
534
|
-
* @param {string} navigationPath Path for finding navigation restrictions
|
|
535
|
-
*/
|
|
536
|
-
function pathItems(paths, prefix, prefixParameters, element, root, sourceName, targetName, target, level, navigationPath) {
|
|
537
|
-
const name = prefix.substring(prefix.lastIndexOf('/') + 1);
|
|
538
|
-
const type = modelElement(element.$Type);
|
|
539
|
-
const pathItem = {};
|
|
540
|
-
const restrictions = navigationPropertyRestrictions(root, navigationPath);
|
|
541
|
-
const nonExpandable = nonExpandableProperties(root, navigationPath);
|
|
542
|
-
|
|
543
|
-
paths[prefix] = pathItem;
|
|
544
|
-
if (prefixParameters.length > 0) pathItem.parameters = prefixParameters;
|
|
545
|
-
|
|
546
|
-
operationRead(pathItem, element, name, sourceName, targetName, target, level, restrictions, false, nonExpandable);
|
|
547
|
-
if (!root['$cds.autoexpose'] && element.$Collection && (element.$ContainsTarget || level < 2 && target)) {
|
|
548
|
-
operationCreate(pathItem, element, name, sourceName, targetName, target, level, restrictions);
|
|
549
|
-
}
|
|
550
|
-
pathItemsForBoundOperations(paths, prefix, prefixParameters, element, sourceName);
|
|
551
|
-
|
|
552
|
-
if (element.$ContainsTarget) {
|
|
553
|
-
if (element.$Collection) {
|
|
554
|
-
if (level < maxLevels)
|
|
555
|
-
pathItemsWithKey(paths, prefix, prefixParameters, element, root, sourceName, targetName, target, level, navigationPath, restrictions, nonExpandable);
|
|
556
|
-
} else {
|
|
557
|
-
if (!root['$cds.autoexpose']) {
|
|
558
|
-
operationUpdate(pathItem, element, name, sourceName, target, level, restrictions);
|
|
559
|
-
if (element.$Nullable) {
|
|
560
|
-
operationDelete(pathItem, element, name, sourceName, target, level, restrictions);
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
pathItemsForBoundOperations(paths, prefix, prefixParameters, element, sourceName);
|
|
564
|
-
pathItemsWithNavigation(paths, prefix, prefixParameters, type, root, sourceName, level, navigationPath);
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
if (Object.keys(pathItem).filter((i) => i !== "parameters").length === 0)
|
|
569
|
-
delete paths[prefix];
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
/**
|
|
573
|
-
* Find navigation restrictions for a navigation path
|
|
574
|
-
* @param {object} root Root model element
|
|
575
|
-
* @param {string} navigationPath Path for finding navigation restrictions
|
|
576
|
-
* @return Navigation property restrictions of navigation segment
|
|
577
|
-
*/
|
|
578
|
-
function navigationPropertyRestrictions(root, navigationPath) {
|
|
579
|
-
const navigationRestrictions = root[voc.Capabilities.NavigationRestrictions] || {};
|
|
580
|
-
return (navigationRestrictions.RestrictedProperties || []).find(item => navigationPropertyPath(item.NavigationProperty) == navigationPath)
|
|
581
|
-
|| {};
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
/**
|
|
585
|
-
* Find non-expandable properties for a navigation path
|
|
586
|
-
* @param {object} root Root model element
|
|
587
|
-
* @param {string} navigationPath Path for finding navigation restrictions
|
|
588
|
-
* @return Navigation property restrictions of navigation segment
|
|
589
|
-
*/
|
|
590
|
-
function nonExpandableProperties(root, navigationPath) {
|
|
591
|
-
const expandRestrictions = root[voc.Capabilities.ExpandRestrictions] || {};
|
|
592
|
-
const prefix = navigationPath.length === 0 ? '' : navigationPath + '/'
|
|
593
|
-
const from = prefix.length
|
|
594
|
-
const nonExpandable = []
|
|
595
|
-
for (const path of (expandRestrictions.NonExpandableProperties || [])) {
|
|
596
|
-
if (path.startsWith(prefix)) {
|
|
597
|
-
nonExpandable.push(path.substring(from))
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
return nonExpandable;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
/**
|
|
604
|
-
* Add path and Path Item Object for a navigation segment with key
|
|
605
|
-
* @param {object} paths Paths Object to augment
|
|
606
|
-
* @param {string} prefix Prefix for path
|
|
607
|
-
* @param {Array} prefixParameters Parameter Objects for prefix
|
|
608
|
-
* @param {object} element Model element of navigation segment
|
|
609
|
-
* @param {object} root Root model element
|
|
610
|
-
* @param {string} sourceName Name of path source
|
|
611
|
-
* @param {string} targetName Name of path target
|
|
612
|
-
* @param {string} target Target container child of path
|
|
613
|
-
* @param {integer} level Number of navigation segments so far
|
|
614
|
-
* @param {string} navigationPath Path for finding navigation restrictions
|
|
615
|
-
* @param {object} restrictions Navigation property restrictions of navigation segment
|
|
616
|
-
* @param {array} nonExpandable Non-expandable navigation properties
|
|
617
|
-
*/
|
|
618
|
-
function pathItemsWithKey(paths, prefix, prefixParameters, element, root, sourceName, targetName, target, level, navigationPath, restrictions, nonExpandable) {
|
|
619
|
-
const targetIndexable = target == null || target[voc.Capabilities.IndexableByKey] != false;
|
|
620
|
-
if (restrictions.IndexableByKey == true || restrictions.IndexableByKey != false && targetIndexable) {
|
|
621
|
-
const name = prefix.substring(prefix.lastIndexOf('/') + 1);
|
|
622
|
-
const type = modelElement(element.$Type);
|
|
623
|
-
const key = entityKey(type, level);
|
|
624
|
-
if (key.parameters.length > 0) {
|
|
625
|
-
const path = prefix + key.segment;
|
|
626
|
-
const parameters = prefixParameters.concat(key.parameters);
|
|
627
|
-
const pathItem = { parameters: parameters };
|
|
628
|
-
paths[path] = pathItem;
|
|
629
|
-
|
|
630
|
-
operationRead(pathItem, element, name, sourceName, targetName, target, level, restrictions, true, nonExpandable);
|
|
631
|
-
if (!root['$cds.autoexpose']) {
|
|
632
|
-
operationUpdate(pathItem, element, name, sourceName, target, level, restrictions, true);
|
|
633
|
-
operationDelete(pathItem, element, name, sourceName, target, level, restrictions, true);
|
|
634
|
-
}
|
|
635
|
-
if (Object.keys(pathItem).filter((i) => i !== "parameters").length === 0)
|
|
636
|
-
delete paths[path];
|
|
637
|
-
|
|
638
|
-
pathItemsForBoundOperations(paths, path, parameters, element, sourceName, true);
|
|
639
|
-
pathItemsWithNavigation(paths, path, parameters, type, root, sourceName, level, navigationPath);
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
/**
|
|
645
|
-
* Construct Operation Object for create
|
|
646
|
-
* @param {object} pathItem Path Item Object to augment
|
|
647
|
-
* @param {object} element Model element of navigation segment
|
|
648
|
-
* @param {string} name Name of navigation segment
|
|
649
|
-
* @param {string} sourceName Name of path source
|
|
650
|
-
* @param {string} targetName Name of path target
|
|
651
|
-
* @param {string} target Target container child of path
|
|
652
|
-
* @param {integer} level Number of navigation segments so far
|
|
653
|
-
* @param {object} restrictions Navigation property restrictions of navigation segment
|
|
654
|
-
*/
|
|
655
|
-
function operationCreate(pathItem, element, name, sourceName, targetName, target, level, restrictions) {
|
|
656
|
-
const insertRestrictions = restrictions.InsertRestrictions || target && target[voc.Capabilities.InsertRestrictions] || {};
|
|
657
|
-
let countRestrictions = target && (target[voc.Capabilities.CountRestrictions]?.Countable === false); // count property will be added if CountRestrictions is false
|
|
658
|
-
if (insertRestrictions.Insertable !== false) {
|
|
659
|
-
const lname = pluralize.singular(splitName(name));
|
|
660
|
-
const type = modelElement(element.$Type);
|
|
661
|
-
pathItem.post = {
|
|
662
|
-
summary: insertRestrictions.Description || operationSummary('Creates', name, sourceName, level, true, true),
|
|
663
|
-
tags: [sourceName],
|
|
664
|
-
requestBody: {
|
|
665
|
-
description: type && type[voc.Core.Description] || 'New ' + lname,
|
|
666
|
-
required: true,
|
|
667
|
-
content: {
|
|
668
|
-
'application/json': {
|
|
669
|
-
schema: ref(element.$Type, SUFFIX.create),
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
},
|
|
673
|
-
responses: response(201, 'Created ' + lname, { $Type: element.$Type }, insertRestrictions.ErrorResponses, !countRestrictions),
|
|
674
|
-
};
|
|
675
|
-
if (insertRestrictions.LongDescription) pathItem.post.description = insertRestrictions.LongDescription;
|
|
676
|
-
if (targetName && sourceName != targetName) pathItem.post.tags.push(targetName);
|
|
677
|
-
customParameters(pathItem.post, insertRestrictions);
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
/**
|
|
682
|
-
* Split camel-cased name into words
|
|
683
|
-
* @param {string} name Name to split
|
|
684
|
-
* @return {string} Split name
|
|
685
|
-
*/
|
|
686
|
-
function splitName(name) {
|
|
687
|
-
return name.split(/(?=[A-Z])/g).join(' ').toLowerCase().replace(/ i d/g, ' id');
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
/**
|
|
691
|
-
* Construct operation summary
|
|
692
|
-
* @param {string} operation Operation (verb)
|
|
693
|
-
* @param {string} name Name of navigation segment
|
|
694
|
-
* @param {string} sourceName Name of path source
|
|
695
|
-
* @param {integer} level Number of navigation segments so far
|
|
696
|
-
* @param {boolean} collection Access a collection
|
|
697
|
-
* @param {boolean} byKey Access by key
|
|
698
|
-
* @return {string} Operation Text
|
|
699
|
-
*/
|
|
700
|
-
function operationSummary(operation, name, sourceName, level, collection, byKey) {
|
|
701
|
-
let lname = splitName(name);
|
|
702
|
-
let sname = splitName(sourceName);
|
|
703
|
-
|
|
704
|
-
return operation + ' '
|
|
705
|
-
+ (byKey ? 'a single ' : (collection ? 'a list of ' : ''))
|
|
706
|
-
+ (byKey ? pluralize.singular(lname) : lname)
|
|
707
|
-
//TODO: suppress "a" for all singletons
|
|
708
|
-
+ (level == 0 ? '' : (level == 1 && sname == 'me' ? ' of me' : ' of a ' + pluralize.singular(sname)))
|
|
709
|
-
+ '.'
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
/**
|
|
713
|
-
* Construct Operation Object for read
|
|
714
|
-
* @param {object} pathItem Path Item Object to augment
|
|
715
|
-
* @param {object} element Model element of navigation segment
|
|
716
|
-
* @param {string} name Name of navigation segment
|
|
717
|
-
* @param {string} sourceName Name of path source
|
|
718
|
-
* @param {string} targetName Name of path target
|
|
719
|
-
* @param {string} target Target container child of path
|
|
720
|
-
* @param {integer} level Number of navigation segments so far
|
|
721
|
-
* @param {object} restrictions Navigation property restrictions of navigation segment
|
|
722
|
-
* @param {boolean} byKey Read by key
|
|
723
|
-
* @param {array} nonExpandable Non-expandable navigation properties
|
|
724
|
-
*/
|
|
725
|
-
function operationRead(pathItem, element, name, sourceName, targetName, target, level, restrictions, byKey, nonExpandable) {
|
|
726
|
-
const targetRestrictions = target?.[voc.Capabilities.ReadRestrictions];
|
|
727
|
-
const readRestrictions = restrictions.ReadRestrictions || targetRestrictions || {};
|
|
728
|
-
const readByKeyRestrictions = readRestrictions.ReadByKeyRestrictions;
|
|
729
|
-
let readable = true;
|
|
730
|
-
let countRestrictions = target && (target[voc.Capabilities.CountRestrictions]?.Countable === false);
|
|
731
|
-
if (byKey && readByKeyRestrictions && readByKeyRestrictions.Readable !== undefined)
|
|
732
|
-
readable = readByKeyRestrictions.Readable;
|
|
733
|
-
else if (readRestrictions.Readable !== undefined)
|
|
734
|
-
readable = readRestrictions.Readable;
|
|
735
|
-
|
|
736
|
-
if (readable) {
|
|
737
|
-
let descriptions = (level == 0 ? targetRestrictions : restrictions.ReadRestrictions) || {};
|
|
738
|
-
if (byKey) descriptions = descriptions.ReadByKeyRestrictions || {};
|
|
739
|
-
const lname = splitName(name);
|
|
740
|
-
const collection = !byKey && element.$Collection;
|
|
741
|
-
const operation = {
|
|
742
|
-
summary: descriptions.Description || operationSummary('Retrieves', name, sourceName, level, element.$Collection, byKey),
|
|
743
|
-
tags: [sourceName],
|
|
744
|
-
parameters: [],
|
|
745
|
-
responses: response(200, 'Retrieved ' + (byKey ? pluralize.singular(lname) : lname), { $Type: element.$Type, $Collection: collection },
|
|
746
|
-
byKey ? readByKeyRestrictions?.ErrorResponses : readRestrictions?.ErrorResponses, !countRestrictions)
|
|
747
|
-
};
|
|
748
|
-
const deltaSupported = element[voc.Capabilities.ChangeTracking] && element[voc.Capabilities.ChangeTracking].Supported;
|
|
749
|
-
if (!byKey && deltaSupported) {
|
|
750
|
-
operation.responses[200].content['application/json'].schema.properties['@odata.deltaLink'] = {
|
|
751
|
-
type: 'string',
|
|
752
|
-
example: basePath + '/' + name + '?$deltatoken=opaque server-generated token for fetching the delta'
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
if (descriptions.LongDescription) operation.description = descriptions.LongDescription;
|
|
756
|
-
if (target && sourceName != targetName) operation.tags.push(targetName);
|
|
757
|
-
customParameters(operation, byKey ? readByKeyRestrictions || readRestrictions : readRestrictions);
|
|
758
|
-
|
|
759
|
-
if (collection) {
|
|
760
|
-
optionTop(operation.parameters, target, restrictions);
|
|
761
|
-
optionSkip(operation.parameters, target, restrictions);
|
|
762
|
-
if (csdl.$Version >= '4.0') optionSearch(operation.parameters, target, restrictions);
|
|
763
|
-
optionFilter(operation.parameters, target, restrictions);
|
|
764
|
-
optionCount(operation.parameters, target);
|
|
765
|
-
optionOrderBy(operation.parameters, element, target, restrictions);
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
optionSelect(operation.parameters, element, target, restrictions);
|
|
769
|
-
optionExpand(operation.parameters, element, target, nonExpandable);
|
|
770
|
-
|
|
771
|
-
pathItem.get = operation;
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
/**
|
|
776
|
-
* Add custom headers and query options
|
|
777
|
-
* @param {object} operation Operation object to augment
|
|
778
|
-
* @param {object} restrictions Restrictions for operation
|
|
779
|
-
*/
|
|
780
|
-
function customParameters(operation, restrictions) {
|
|
781
|
-
if (
|
|
782
|
-
!operation.parameters &&
|
|
783
|
-
(restrictions.CustomHeaders || restrictions.CustomQueryOptions)
|
|
784
|
-
)
|
|
785
|
-
operation.parameters = [];
|
|
786
|
-
for (const custom of restrictions.CustomHeaders || []) {
|
|
787
|
-
operation.parameters.push(customParameter(custom, "header"));
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
for (const custom of restrictions.CustomQueryOptions || []) {
|
|
791
|
-
operation.parameters.push(customParameter(custom, "query"));
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
/**
|
|
796
|
-
* Construct custom parameter
|
|
797
|
-
* @param {object} custom custom parameter in OData format
|
|
798
|
-
* @param {string} location "header" or "query"
|
|
799
|
-
*/
|
|
800
|
-
function customParameter(custom, location) {
|
|
801
|
-
return {
|
|
802
|
-
name: custom.Name,
|
|
803
|
-
in: location,
|
|
804
|
-
required: custom.Required || false,
|
|
805
|
-
...(custom.Description && { description: custom.Description }),
|
|
806
|
-
schema: {
|
|
807
|
-
type: "string",
|
|
808
|
-
...(custom.DocumentationURL && {
|
|
809
|
-
externalDocs: { url: custom.DocumentationURL },
|
|
810
|
-
}),
|
|
811
|
-
//TODO: Examples
|
|
812
|
-
},
|
|
813
|
-
};
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
/**
|
|
817
|
-
* Add parameter for query option $count
|
|
818
|
-
* @param {Array} parameters Array of parameters to augment
|
|
819
|
-
* @param {string} target Target container child of path
|
|
820
|
-
*/
|
|
821
|
-
function optionCount(parameters, target) {
|
|
822
|
-
const targetRestrictions = target && target[voc.Capabilities.CountRestrictions];
|
|
823
|
-
const targetCountable = target == null
|
|
824
|
-
|| targetRestrictions == null
|
|
825
|
-
|| targetRestrictions.Countable !== false;
|
|
826
|
-
|
|
827
|
-
if (targetCountable) {
|
|
828
|
-
parameters.push({
|
|
829
|
-
|
|
830
|
-
$ref: '#/components/parameters/count'
|
|
831
|
-
});
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
/**
|
|
836
|
-
* Add parameter for query option $expand
|
|
837
|
-
* @param {Array} parameters Array of parameters to augment
|
|
838
|
-
* @param {object} element Model element of navigation segment
|
|
839
|
-
* @param {string} target Target container child of path
|
|
840
|
-
* @param {array} nonExpandable Non-expandable navigation properties
|
|
841
|
-
*/
|
|
842
|
-
function optionExpand(parameters, element, target, nonExpandable) {
|
|
843
|
-
const targetRestrictions = target && target[voc.Capabilities.ExpandRestrictions];
|
|
844
|
-
const supported = targetRestrictions == null || targetRestrictions.Expandable != false;
|
|
845
|
-
if (supported) {
|
|
846
|
-
const expandItems = ['*'].concat(navigationPaths(element).filter(path => !nonExpandable.includes(path)));
|
|
847
|
-
if (expandItems.length > 1) {
|
|
848
|
-
parameters.push({
|
|
849
|
-
name: queryOptionPrefix + 'expand',
|
|
850
|
-
description: (targetRestrictions && targetRestrictions[voc.Core.Description])
|
|
851
|
-
|| 'The value of $expand query option is a comma-separated list of navigation property names, \
|
|
852
|
-
stream property names, or $value indicating the stream content of a media-entity. \
|
|
853
|
-
The corresponding related entities and stream values will be represented inline, \
|
|
854
|
-
see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionexpand)',
|
|
855
|
-
in: 'query',
|
|
856
|
-
explode: false,
|
|
857
|
-
schema: {
|
|
858
|
-
type: 'array',
|
|
859
|
-
uniqueItems: true,
|
|
860
|
-
items: {
|
|
861
|
-
type: 'string',
|
|
862
|
-
enum: expandItems
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
});
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
/**
|
|
871
|
-
* Collect navigation paths of a navigation segment and its potentially structured components
|
|
872
|
-
* @param {object} element Model element of navigation segment
|
|
873
|
-
* @param {string} prefix Navigation prefix
|
|
874
|
-
* @param {integer} level Number of navigation segments so far
|
|
875
|
-
* @return {Array} Array of navigation property paths
|
|
876
|
-
*/
|
|
877
|
-
function navigationPaths(element, prefix = '', level = 0) {
|
|
878
|
-
const paths = [];
|
|
879
|
-
const type = modelElement(element.$Type);
|
|
880
|
-
const properties = propertiesOfStructuredType(type);
|
|
881
|
-
Object.keys(properties).forEach(key => {
|
|
882
|
-
if (properties[key].$Kind == 'NavigationProperty') {
|
|
883
|
-
paths.push(prefix + key)
|
|
884
|
-
} else if (properties[key].$Type && level < maxLevels) {
|
|
885
|
-
paths.push(...navigationPaths(properties[key], prefix + key + '/', level + 1));
|
|
886
|
-
}
|
|
887
|
-
})
|
|
888
|
-
return paths;
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
/**
|
|
892
|
-
* Add parameter for query option $filter
|
|
893
|
-
* @param {Array} parameters Array of parameters to augment
|
|
894
|
-
* @param {string} target Target container child of path
|
|
895
|
-
* @param {object} restrictions Navigation property restrictions of navigation segment
|
|
896
|
-
*/
|
|
897
|
-
function optionFilter(parameters, target, restrictions) {
|
|
898
|
-
const filterRestrictions = restrictions.FilterRestrictions || target && target[voc.Capabilities.FilterRestrictions] || {};
|
|
899
|
-
|
|
900
|
-
if (filterRestrictions.Filterable !== false) {
|
|
901
|
-
const filter = {
|
|
902
|
-
name: queryOptionPrefix + 'filter',
|
|
903
|
-
description: filterRestrictions[voc.Core.Description]
|
|
904
|
-
|| 'Filter items by property values, see [Filtering](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionfilter)',
|
|
905
|
-
in: 'query',
|
|
906
|
-
schema: {
|
|
907
|
-
type: 'string'
|
|
908
|
-
}
|
|
909
|
-
};
|
|
910
|
-
if (filterRestrictions.RequiresFilter)
|
|
911
|
-
filter.required = true;
|
|
912
|
-
if (filterRestrictions.RequiredProperties) {
|
|
913
|
-
filter.description += '\n\nRequired filter properties:';
|
|
914
|
-
filterRestrictions.RequiredProperties.forEach(
|
|
915
|
-
item => filter.description += '\n- ' + propertyPath(item)
|
|
916
|
-
);
|
|
917
|
-
}
|
|
918
|
-
parameters.push(filter);
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
/**
|
|
923
|
-
* Add parameter for query option $orderby
|
|
924
|
-
* @param {Array} parameters Array of parameters to augment
|
|
925
|
-
* @param {object} element Model element of navigation segment
|
|
926
|
-
* @param {string} target Target container child of path
|
|
927
|
-
* @param {object} restrictions Navigation property restrictions of navigation segment
|
|
928
|
-
*/
|
|
929
|
-
function optionOrderBy(parameters, element, target, restrictions) {
|
|
930
|
-
const sortRestrictions = restrictions.SortRestrictions || target && target[voc.Capabilities.SortRestrictions] || {};
|
|
931
|
-
|
|
932
|
-
if (sortRestrictions.Sortable !== false) {
|
|
933
|
-
const nonSortable = {};
|
|
934
|
-
(sortRestrictions.NonSortableProperties || []).forEach(name => {
|
|
935
|
-
nonSortable[propertyPath(name)] = true;
|
|
936
|
-
});
|
|
937
|
-
const orderbyItems = [];
|
|
938
|
-
primitivePaths(element).filter(property => !nonSortable[property]).forEach(property => {
|
|
939
|
-
orderbyItems.push(property);
|
|
940
|
-
orderbyItems.push(property + ' desc');
|
|
941
|
-
});
|
|
942
|
-
if (orderbyItems.length > 0) {
|
|
943
|
-
parameters.push({
|
|
944
|
-
name: queryOptionPrefix + 'orderby',
|
|
945
|
-
description: sortRestrictions[voc.Core.Description]
|
|
946
|
-
|| 'Order items by property values, see [Sorting](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionorderby)',
|
|
947
|
-
in: 'query',
|
|
948
|
-
explode: false,
|
|
949
|
-
schema: {
|
|
950
|
-
type: 'array',
|
|
951
|
-
uniqueItems: true,
|
|
952
|
-
items: {
|
|
953
|
-
type: 'string',
|
|
954
|
-
enum: orderbyItems
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
});
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
/**
|
|
963
|
-
* Unpack EnumMember value if it uses CSDL JSON CS01 style, like CAP does
|
|
964
|
-
* @param {string or object} path Qualified name of referenced type
|
|
965
|
-
* @return {object} Reference Object
|
|
966
|
-
*/
|
|
967
|
-
function enumMember(member) {
|
|
968
|
-
if (typeof member == 'string')
|
|
969
|
-
return member;
|
|
970
|
-
else if (typeof member == 'object')
|
|
971
|
-
return member.$EnumMember;
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
/**
|
|
975
|
-
* Unpack NavigationPropertyPath value if it uses CSDL JSON CS01 style, like CAP does
|
|
976
|
-
* @param {string or object} path Qualified name of referenced type
|
|
977
|
-
* @return {object} Reference Object
|
|
978
|
-
*/
|
|
979
|
-
function navigationPropertyPath(path) {
|
|
980
|
-
if (typeof path == 'string')
|
|
981
|
-
return path;
|
|
982
|
-
else
|
|
983
|
-
return path.$NavigationPropertyPath;
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
/**
|
|
987
|
-
* Unpack PropertyPath value if it uses CSDL JSON CS01 style, like CAP does
|
|
988
|
-
* @param {string or object} path Qualified name of referenced type
|
|
989
|
-
* @return {object} Reference Object
|
|
990
|
-
*/
|
|
991
|
-
function propertyPath(path) {
|
|
992
|
-
if (typeof path == 'string')
|
|
993
|
-
return path;
|
|
994
|
-
else
|
|
995
|
-
return path.$PropertyPath;
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
/**
|
|
999
|
-
* Collect primitive paths of a navigation segment and its potentially structured components
|
|
1000
|
-
* @param {object} element Model element of navigation segment
|
|
1001
|
-
* @param {string} prefix Navigation prefix
|
|
1002
|
-
* @return {Array} Array of primitive property paths
|
|
1003
|
-
*/
|
|
1004
|
-
function primitivePaths(element, prefix = '') {
|
|
1005
|
-
const paths = [];
|
|
1006
|
-
const elementType = modelElement(element.$Type);
|
|
1007
|
-
|
|
1008
|
-
if (!elementType) {
|
|
1009
|
-
console.warn(`Unknown type for element: ${JSON.stringify(element)}`);
|
|
1010
|
-
return paths;
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
const propsOfType = propertiesOfStructuredType(elementType);
|
|
1014
|
-
const ignore = Object.entries(propsOfType)
|
|
1015
|
-
.filter(entry => entry[1].$Kind !== 'NavigationProperty')
|
|
1016
|
-
.filter(entry => entry[1].$Type)
|
|
1017
|
-
.filter(entry => nameParts(entry[1].$Type).qualifier !== 'Edm')
|
|
1018
|
-
.filter(entry => !modelElement(entry[1].$Type));
|
|
1019
|
-
|
|
1020
|
-
// Keep old logging
|
|
1021
|
-
ignore.forEach(entry => console.warn(`Unknown type for element: ${JSON.stringify(entry)}`));
|
|
1022
|
-
|
|
1023
|
-
const properties = Object.entries(propsOfType)
|
|
1024
|
-
.filter(entry => entry[1].$Kind !== 'NavigationProperty')
|
|
1025
|
-
.filter(entry => !ignore.includes(entry))
|
|
1026
|
-
.map(entryToProperty({ path: prefix, typeRefChain: [] }));
|
|
1027
|
-
|
|
1028
|
-
for (let i = 0; i < properties.length; i++) {
|
|
1029
|
-
const property = properties[i];
|
|
1030
|
-
if (!property.isComplex) {
|
|
1031
|
-
paths.push(property.path);
|
|
1032
|
-
continue;
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
const typeRefChainTail = property.typeRefChain[property.typeRefChain.length - 1];
|
|
1036
|
-
|
|
1037
|
-
// Allow full cycle to be shown (0) times
|
|
1038
|
-
if (property.typeRefChain.filter(_type => _type === typeRefChainTail).length > 1) {
|
|
1039
|
-
console.warn(`Cycle detected ${property.typeRefChain.join('->')}`);
|
|
1040
|
-
continue;
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
const expanded = Object.entries(property.properties)
|
|
1044
|
-
.filter(pProperty => pProperty[1].$Kind !== 'NavigationProperty')
|
|
1045
|
-
.map(entryToProperty(property))
|
|
1046
|
-
properties.splice(i + 1, 0, ...expanded);
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
return paths;
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
function entryToProperty(parent) {
|
|
1053
|
-
|
|
1054
|
-
return function (entry) {
|
|
1055
|
-
const key = entry[0];
|
|
1056
|
-
const property = entry[1];
|
|
1057
|
-
const propertyType = property.$Type && modelElement(property.$Type);
|
|
1058
|
-
|
|
1059
|
-
if (propertyType && propertyType.$Kind && propertyType.$Kind === 'ComplexType') {
|
|
1060
|
-
return {
|
|
1061
|
-
properties: propertiesOfStructuredType(propertyType),
|
|
1062
|
-
path: `${parent.path}${key}/`,
|
|
1063
|
-
typeRefChain: parent.typeRefChain.concat(property.$Type),
|
|
1064
|
-
isComplex: true
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
return {
|
|
1069
|
-
properties: {},
|
|
1070
|
-
path: `${parent.path}${key}`,
|
|
1071
|
-
typeRefChain: [],
|
|
1072
|
-
isComplex: false,
|
|
1073
|
-
}
|
|
1074
|
-
};
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
/**
|
|
1078
|
-
* Add parameter for query option $search
|
|
1079
|
-
* @param {Array} parameters Array of parameters to augment
|
|
1080
|
-
* @param {string} target Target container child of path
|
|
1081
|
-
* @param {object} restrictions Navigation property restrictions of navigation segment
|
|
1082
|
-
*/
|
|
1083
|
-
function optionSearch(parameters, target, restrictions) {
|
|
1084
|
-
const searchRestrictions = restrictions.SearchRestrictions || target && target[voc.Capabilities.SearchRestrictions] || {};
|
|
1085
|
-
|
|
1086
|
-
if (searchRestrictions.Searchable !== false) {
|
|
1087
|
-
if (searchRestrictions[voc.Core.Description]) {
|
|
1088
|
-
parameters.push({
|
|
1089
|
-
name: queryOptionPrefix + 'search',
|
|
1090
|
-
description: searchRestrictions[voc.Core.Description],
|
|
1091
|
-
in: 'query',
|
|
1092
|
-
schema: { type: 'string' }
|
|
1093
|
-
});
|
|
1094
|
-
} else {
|
|
1095
|
-
parameters.push({ $ref: '#/components/parameters/search' });
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
/**
|
|
1101
|
-
* Add parameter for query option $select
|
|
1102
|
-
* @param {Array} parameters Array of parameters to augment
|
|
1103
|
-
* @param {object} element Model element of navigation segment
|
|
1104
|
-
* @param {string} target Target container child of path
|
|
1105
|
-
* @param {object} restrictions Navigation property restrictions of navigation segment
|
|
1106
|
-
*/
|
|
1107
|
-
function optionSelect(parameters, element, target, restrictions) {
|
|
1108
|
-
const selectSupport = restrictions.SelectSupport || target && target[voc.Capabilities.SelectSupport] || {};
|
|
1109
|
-
|
|
1110
|
-
if (selectSupport.Supported !== false) {
|
|
1111
|
-
const type = modelElement(element.$Type) || {};
|
|
1112
|
-
const properties = propertiesOfStructuredType(type);
|
|
1113
|
-
const selectItems = [];
|
|
1114
|
-
Object.keys(properties).filter(key => properties[key].$Kind != 'NavigationProperty').forEach(
|
|
1115
|
-
key => selectItems.push(key)
|
|
1116
|
-
)
|
|
1117
|
-
if (selectItems.length > 0) {
|
|
1118
|
-
parameters.push({
|
|
1119
|
-
name: queryOptionPrefix + 'select',
|
|
1120
|
-
description: 'Select properties to be returned, see [Select](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionselect)',
|
|
1121
|
-
in: 'query',
|
|
1122
|
-
explode: false,
|
|
1123
|
-
schema: {
|
|
1124
|
-
type: 'array',
|
|
1125
|
-
uniqueItems: true,
|
|
1126
|
-
items: {
|
|
1127
|
-
type: 'string',
|
|
1128
|
-
enum: selectItems
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
});
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
/**
|
|
1137
|
-
* Add parameter for query option $skip
|
|
1138
|
-
* @param {Array} parameters Array of parameters to augment
|
|
1139
|
-
* @param {string} target Target container child of path
|
|
1140
|
-
* @param {object} restrictions Navigation property restrictions of navigation segment
|
|
1141
|
-
*/
|
|
1142
|
-
function optionSkip(parameters, target, restrictions) {
|
|
1143
|
-
const supported = restrictions.SkipSupported !== undefined
|
|
1144
|
-
? restrictions.SkipSupported
|
|
1145
|
-
: target == null || target[voc.Capabilities.SkipSupported] !== false;
|
|
1146
|
-
|
|
1147
|
-
if (supported) {
|
|
1148
|
-
parameters.push({
|
|
1149
|
-
$ref: '#/components/parameters/skip'
|
|
1150
|
-
});
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
/**
|
|
1155
|
-
* Add parameter for query option $top
|
|
1156
|
-
* @param {Array} parameters Array of parameters to augment
|
|
1157
|
-
* @param {string} target Target container child of path
|
|
1158
|
-
* @param {object} restrictions Navigation property restrictions of navigation segment
|
|
1159
|
-
*/
|
|
1160
|
-
function optionTop(parameters, target, restrictions) {
|
|
1161
|
-
const supported = restrictions.TopSupported !== undefined
|
|
1162
|
-
? restrictions.TopSupported
|
|
1163
|
-
: target == null || target[voc.Capabilities.TopSupported] !== false;
|
|
1164
|
-
|
|
1165
|
-
if (supported) {
|
|
1166
|
-
parameters.push({
|
|
1167
|
-
$ref: '#/components/parameters/top'
|
|
1168
|
-
});
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
/**
|
|
1173
|
-
* Construct Operation Object for update
|
|
1174
|
-
* @param {object} pathItem Path Item Object to augment
|
|
1175
|
-
* @param {object} element Model element of navigation segment
|
|
1176
|
-
* @param {string} name Name of navigation segment
|
|
1177
|
-
* @param {string} sourceName Name of path source
|
|
1178
|
-
* @param {string} target Target container child of path
|
|
1179
|
-
* @param {integer} level Number of navigation segments so far
|
|
1180
|
-
* @param {object} restrictions Navigation property restrictions of navigation segment
|
|
1181
|
-
* @param {boolean} byKey Update by key
|
|
1182
|
-
*/
|
|
1183
|
-
function operationUpdate(pathItem, element, name, sourceName, target, level, restrictions, byKey) {
|
|
1184
|
-
const updateRestrictions = restrictions.UpdateRestrictions || target && target[voc.Capabilities.UpdateRestrictions] || {};
|
|
1185
|
-
let countRestrictions = target && (target[voc.Capabilities.CountRestrictions]?.Countable === false);
|
|
1186
|
-
if (updateRestrictions.Updatable !== false) {
|
|
1187
|
-
const type = modelElement(element.$Type);
|
|
1188
|
-
const operation = {
|
|
1189
|
-
summary: updateRestrictions.Description || operationSummary('Changes', name, sourceName, level, element.$Collection, byKey),
|
|
1190
|
-
tags: [sourceName],
|
|
1191
|
-
requestBody: {
|
|
1192
|
-
description: type && type[voc.Core.Description] || 'New property values',
|
|
1193
|
-
required: true,
|
|
1194
|
-
content: {
|
|
1195
|
-
'application/json': {
|
|
1196
|
-
schema: ref(element.$Type, SUFFIX.update),
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
},
|
|
1200
|
-
responses: response(204, "Success", undefined, updateRestrictions.ErrorResponses, !countRestrictions),
|
|
1201
|
-
};
|
|
1202
|
-
if (updateRestrictions.LongDescription) operation.description = updateRestrictions.LongDescription;
|
|
1203
|
-
customParameters(operation, updateRestrictions);
|
|
1204
|
-
const updateMethod = updateRestrictions.UpdateMethod ? updateRestrictions.UpdateMethod.toLowerCase() : "patch";
|
|
1205
|
-
pathItem[updateMethod] = operation;
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
/**
|
|
1210
|
-
* Construct Operation Object for delete
|
|
1211
|
-
* @param {object} pathItem Path Item Object to augment
|
|
1212
|
-
* @param {object} element Model element of navigation segment
|
|
1213
|
-
* @param {string} name Name of navigation segment
|
|
1214
|
-
* @param {string} sourceName Name of path source
|
|
1215
|
-
* @param {string} target Target container child of path
|
|
1216
|
-
* @param {integer} level Number of navigation segments so far
|
|
1217
|
-
* @param {object} restrictions Navigation property restrictions of navigation segment
|
|
1218
|
-
* @param {boolean} byKey Delete by key
|
|
1219
|
-
*/
|
|
1220
|
-
function operationDelete(pathItem, element, name, sourceName, target, level, restrictions, byKey) {
|
|
1221
|
-
const deleteRestrictions = restrictions.DeleteRestrictions || target && target[voc.Capabilities.DeleteRestrictions] || {};
|
|
1222
|
-
let countRestrictions = target && (target[voc.Capabilities.CountRestrictions]?.Countable === false);
|
|
1223
|
-
if (deleteRestrictions.Deletable !== false) {
|
|
1224
|
-
pathItem.delete = {
|
|
1225
|
-
summary: deleteRestrictions.Description || operationSummary('Deletes', name, sourceName, level, element.$Collection, byKey),
|
|
1226
|
-
tags: [sourceName],
|
|
1227
|
-
responses: response(204, "Success", undefined, deleteRestrictions.ErrorResponses, !countRestrictions),
|
|
1228
|
-
};
|
|
1229
|
-
if (deleteRestrictions.LongDescription) pathItem.delete.description = deleteRestrictions.LongDescription;
|
|
1230
|
-
customParameters(pathItem.delete, deleteRestrictions);
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
/**
|
|
1235
|
-
* Add paths and Path Item Objects for navigation segments
|
|
1236
|
-
* @param {object} paths The Paths Object to augment
|
|
1237
|
-
* @param {string} prefix Prefix for path
|
|
1238
|
-
* @param {Array} prefixParameters Parameter Objects for prefix
|
|
1239
|
-
* @param {object} type Entity type object of navigation segment
|
|
1240
|
-
* @param {string} sourceName Name of path source
|
|
1241
|
-
* @param {integer} level Number of navigation segments so far
|
|
1242
|
-
* @param {string} navigationPrefix Path for finding navigation restrictions
|
|
1243
|
-
*/
|
|
1244
|
-
function pathItemsWithNavigation(paths, prefix, prefixParameters, type, root, sourceName, level, navigationPrefix) {
|
|
1245
|
-
const navigationRestrictions = root[voc.Capabilities.NavigationRestrictions] || {};
|
|
1246
|
-
const rootNavigable = level == 0 && enumMember(navigationRestrictions.Navigability) != 'None'
|
|
1247
|
-
|| level == 1 && enumMember(navigationRestrictions.Navigability) != 'Single'
|
|
1248
|
-
|| level > 1;
|
|
1249
|
-
|
|
1250
|
-
if (type && level < maxLevels) {
|
|
1251
|
-
const properties = navigationPathMap(type);
|
|
1252
|
-
Object.keys(properties).forEach(name => {
|
|
1253
|
-
const parentRestrictions = navigationPropertyRestrictions(root, navigationPrefix);
|
|
1254
|
-
if (enumMember(parentRestrictions.Navigability) == 'Single') return;
|
|
1255
|
-
|
|
1256
|
-
const navigationPath = navigationPrefix + (navigationPrefix.length > 0 ? '/' : '') + name;
|
|
1257
|
-
const restrictions = navigationPropertyRestrictions(root, navigationPath);
|
|
1258
|
-
if (['Recursive', 'Single'].includes(enumMember(restrictions.Navigability))
|
|
1259
|
-
|| restrictions.Navigability == null && rootNavigable) {
|
|
1260
|
-
const targetSetName = root.$NavigationPropertyBinding && root.$NavigationPropertyBinding[navigationPath];
|
|
1261
|
-
const target = entityContainer[targetSetName];
|
|
1262
|
-
const targetType = target && modelElement(target.$Type);
|
|
1263
|
-
const targetName = (targetType && targetType[voc.Common.Label]) || targetSetName;
|
|
1264
|
-
pathItems(paths, prefix + '/' + name, prefixParameters, properties[name], root, sourceName, targetName, target, level + 1, navigationPath);
|
|
1265
|
-
}
|
|
1266
|
-
});
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
/**
|
|
1271
|
-
* Collect navigation paths of a navigation segment and its potentially structured components
|
|
1272
|
-
* @param {object} type Structured type
|
|
1273
|
-
* @param {object} map Map of navigation property paths and their types
|
|
1274
|
-
* @param {string} prefix Navigation prefix
|
|
1275
|
-
* @param {integer} level Number of navigation segments so far
|
|
1276
|
-
* @return {object} Map of navigation property paths and their types
|
|
1277
|
-
*/
|
|
1278
|
-
function navigationPathMap(type, map = {}, prefix = '', level = 0) {
|
|
1279
|
-
const properties = propertiesOfStructuredType(type);
|
|
1280
|
-
Object.keys(properties).forEach(key => {
|
|
1281
|
-
if (properties[key].$Kind == 'NavigationProperty') {
|
|
1282
|
-
map[prefix + key] = properties[key];
|
|
1283
|
-
} else if (properties[key].$Type && !properties[key].$Collection && level < maxLevels) {
|
|
1284
|
-
navigationPathMap(modelElement(properties[key].$Type), map, prefix + key + '/', level + 1);
|
|
1285
|
-
}
|
|
1286
|
-
})
|
|
1287
|
-
return map;
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
/**
|
|
1291
|
-
* Construct map of key names for an entity type
|
|
1292
|
-
* @param {object} type Entity type object
|
|
1293
|
-
* @return {object} Map of key names
|
|
1294
|
-
*/
|
|
1295
|
-
function keyMap(type) {
|
|
1296
|
-
const map = {};
|
|
1297
|
-
if (type.$Kind == 'EntityType') {
|
|
1298
|
-
const keys = getKey(type) || [];
|
|
1299
|
-
keys.forEach(key => {
|
|
1300
|
-
if (typeof key == 'string')
|
|
1301
|
-
map[key] = true;
|
|
1302
|
-
});
|
|
1303
|
-
}
|
|
1304
|
-
return map;
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
/**
|
|
1308
|
-
* Key for path item
|
|
1309
|
-
* @param {object} entityType Entity Type object
|
|
1310
|
-
* @return {array} Key of entity type or null
|
|
1311
|
-
*/
|
|
1312
|
-
function getKey(entityType) {
|
|
1313
|
-
let type = entityType;
|
|
1314
|
-
let keys = null;
|
|
1315
|
-
while (type) {
|
|
1316
|
-
keys = type.$Key;
|
|
1317
|
-
if (keys || !type.$BaseType) break;
|
|
1318
|
-
type = modelElement(type.$BaseType);
|
|
1319
|
-
}
|
|
1320
|
-
return keys;
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
/**
|
|
1324
|
-
* Key for path item
|
|
1325
|
-
* @param {object} entityType Entity Type object
|
|
1326
|
-
* @param {integer} level Number of navigation segments so far
|
|
1327
|
-
* @return {object} key: Key segment, parameters: key parameters
|
|
1328
|
-
*/
|
|
1329
|
-
function entityKey(entityType, level) {
|
|
1330
|
-
let segment = '';
|
|
1331
|
-
const params = [];
|
|
1332
|
-
const keys = getKey(entityType) || [];
|
|
1333
|
-
const properties = propertiesOfStructuredType(entityType);
|
|
1334
|
-
|
|
1335
|
-
keys.forEach((key, index) => {
|
|
1336
|
-
const suffix = level > 0 ? '_' + level : '';
|
|
1337
|
-
if (keyAsSegment)
|
|
1338
|
-
segment += '/';
|
|
1339
|
-
else {
|
|
1340
|
-
if (index > 0) segment += ',';
|
|
1341
|
-
if (keys.length != 1) segment += key + '=';
|
|
1342
|
-
}
|
|
1343
|
-
let parameter;
|
|
1344
|
-
let property = {};
|
|
1345
|
-
if (typeof key == 'string') {
|
|
1346
|
-
parameter = key;
|
|
1347
|
-
property = properties[key];
|
|
1348
|
-
} else {
|
|
1349
|
-
parameter = Object.keys(key)[0];
|
|
1350
|
-
const segments = key[parameter].split('/');
|
|
1351
|
-
property = properties[segments[0]];
|
|
1352
|
-
for (let i = 1; i < segments.length; i++) {
|
|
1353
|
-
const complexType = modelElement(property.$Type);
|
|
1354
|
-
const properties = propertiesOfStructuredType(complexType);
|
|
1355
|
-
property = properties[segments[i]];
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
const propertyType = property.$Type;
|
|
1359
|
-
segment += pathValuePrefix(propertyType) + '{' + parameter + suffix + '}' + pathValueSuffix(propertyType);
|
|
1360
|
-
const param = {
|
|
1361
|
-
description: [property[voc.Core.Description], property[voc.Core.LongDescription]].filter(t => t).join(' \n')
|
|
1362
|
-
|| 'key: ' + parameter,
|
|
1363
|
-
in: 'path',
|
|
1364
|
-
name: parameter + suffix,
|
|
1365
|
-
required: true,
|
|
1366
|
-
schema: getSchema(property, '', true)
|
|
1367
|
-
};
|
|
1368
|
-
params.push(param);
|
|
1369
|
-
})
|
|
1370
|
-
return { segment: (keyAsSegment ? '' : '(') + segment + (keyAsSegment ? '' : ')'), parameters: params };
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
/**
|
|
1374
|
-
* Prefix for key value in key segment
|
|
1375
|
-
* @param {typename} Qualified name of key property type
|
|
1376
|
-
* @return {string} value prefix
|
|
1377
|
-
*/
|
|
1378
|
-
function pathValuePrefix(typename) {
|
|
1379
|
-
//TODO: handle other Edm types, enumeration types, and type definitions
|
|
1380
|
-
if (['Edm.Int64', 'Edm.Int32', 'Edm.Int16', 'Edm.SByte', 'Edm.Byte',
|
|
1381
|
-
'Edm.Double', 'Edm.Single', 'Edm.Date', 'Edm.DateTimeOffset', 'Edm.Guid'].includes(typename)) return '';
|
|
1382
|
-
if (keyAsSegment) return '';
|
|
1383
|
-
return `'`;
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
/**
|
|
1387
|
-
* Suffix for key value in key segment
|
|
1388
|
-
* @param {typename} Qualified name of key property type
|
|
1389
|
-
* @return {string} value prefix
|
|
1390
|
-
*/
|
|
1391
|
-
function pathValueSuffix(typename) {
|
|
1392
|
-
//TODO: handle other Edm types, enumeration types, and type definitions
|
|
1393
|
-
if (['Edm.Int64', 'Edm.Int32', 'Edm.Int16', 'Edm.SByte', 'Edm.Byte',
|
|
1394
|
-
'Edm.Double', 'Edm.Single', 'Edm.Date', 'Edm.DateTimeOffset', 'Edm.Guid'].includes(typename)) return '';
|
|
1395
|
-
if (keyAsSegment) return '';
|
|
1396
|
-
return `'`;
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
/**
|
|
1400
|
-
* Add path and Path Item Object for actions and functions bound to the element
|
|
1401
|
-
* @param {object} paths Paths Object to augment
|
|
1402
|
-
* @param {string} prefix Prefix for path
|
|
1403
|
-
* @param {Array} prefixParameters Parameter Objects for prefix
|
|
1404
|
-
* @param {object} element Model element the operations are bound to
|
|
1405
|
-
* @param {string} sourceName Name of path source
|
|
1406
|
-
* @param {boolean} byKey read by key
|
|
1407
|
-
*/
|
|
1408
|
-
function pathItemsForBoundOperations(paths, prefix, prefixParameters, element, sourceName, byKey = false) {
|
|
1409
|
-
const overloads = boundOverloads[element.$Type + (!byKey && element.$Collection ? '-c' : '')] || [];
|
|
1410
|
-
overloads.forEach(item => {
|
|
1411
|
-
if (item.overload.$Kind == 'Action')
|
|
1412
|
-
pathItemAction(paths, prefix + '/' + item.name, prefixParameters, item.name, item.overload, sourceName);
|
|
1413
|
-
else
|
|
1414
|
-
pathItemFunction(paths, prefix + '/' + item.name, prefixParameters, item.name, item.overload, sourceName);
|
|
1415
|
-
});
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
/**
|
|
1419
|
-
* Add path and Path Item Object for an action import
|
|
1420
|
-
* @param {object} paths Paths Object to augment
|
|
1421
|
-
* @param {string} name Name of action import
|
|
1422
|
-
* @param {object} child Action import object
|
|
1423
|
-
*/
|
|
1424
|
-
function pathItemActionImport(paths, name, child) {
|
|
1425
|
-
const overload = modelElement(child.$Action).find(pOverload => !pOverload.$IsBound);
|
|
1426
|
-
pathItemAction(paths, '/' + name, [], child.$Action, overload, child.$EntitySet, child);
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
/**
|
|
1430
|
-
* Add path and Path Item Object for action overload
|
|
1431
|
-
* @param {object} paths Paths Object to augment
|
|
1432
|
-
* @param {string} prefix Prefix for path
|
|
1433
|
-
* @param {Array} prefixParameters Parameter Objects for prefix
|
|
1434
|
-
* @param {string} actionName Qualified name of function
|
|
1435
|
-
* @param {object} overload Function overload
|
|
1436
|
-
* @param {string} sourceName Name of path source
|
|
1437
|
-
* @param {string} actionImport Action import
|
|
1438
|
-
*/
|
|
1439
|
-
function pathItemAction(paths, prefix, prefixParameters, actionName, overload, sourceName, actionImport = {}) {
|
|
1440
|
-
const name = actionName.indexOf('.') === -1 ? actionName : nameParts(actionName).name;
|
|
1441
|
-
const pathItem = {
|
|
1442
|
-
post: {
|
|
1443
|
-
summary: actionImport[voc.Core.Description] || overload[voc.Core.Description] || 'Invokes action ' + name,
|
|
1444
|
-
tags: [overload[voc.Common.Label] || sourceName || 'Service Operations'],
|
|
1445
|
-
responses: overload.$ReturnType ? response(200, "Success", overload.$ReturnType, overload[voc.Capabilities.OperationRestrictions]?.ErrorResponses)
|
|
1446
|
-
: response(204, "Success", undefined, overload[voc.Capabilities.OperationRestrictions]?.ErrorResponses),
|
|
1447
|
-
}
|
|
1448
|
-
};
|
|
1449
|
-
const description = actionImport[voc.Core.LongDescription] || overload[voc.Core.LongDescription];
|
|
1450
|
-
if (description) pathItem.post.description = description;
|
|
1451
|
-
if (prefixParameters.length > 0) pathItem.post.parameters = [...prefixParameters];
|
|
1452
|
-
let parameters = overload.$Parameter || [];
|
|
1453
|
-
if (overload.$IsBound) parameters = parameters.slice(1);
|
|
1454
|
-
if (parameters.length > 0) {
|
|
1455
|
-
const requestProperties = {};
|
|
1456
|
-
parameters.forEach(p => { requestProperties[p.$Name] = getSchema(p) });
|
|
1457
|
-
pathItem.post.requestBody = {
|
|
1458
|
-
description: 'Action parameters',
|
|
1459
|
-
content: {
|
|
1460
|
-
'application/json': {
|
|
1461
|
-
schema: {
|
|
1462
|
-
type: 'object',
|
|
1463
|
-
properties: requestProperties
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
|
-
}
|
|
1469
|
-
customParameters(pathItem.post, overload[voc.Capabilities.OperationRestrictions] || {});
|
|
1470
|
-
paths[prefix] = pathItem;
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
/**
|
|
1474
|
-
* Add path and Path Item Object for an action import
|
|
1475
|
-
* @param {object} paths Paths Object to augment
|
|
1476
|
-
* @param {string} name Name of function import
|
|
1477
|
-
* @param {object} child Function import object
|
|
1478
|
-
*/
|
|
1479
|
-
function pathItemFunctionImport(paths, name, child) {
|
|
1480
|
-
const overloads = modelElement(child.$Function);
|
|
1481
|
-
console.assert(overloads, 'Unknown function "' + child.$Function + '" in function import "' + name + '"');
|
|
1482
|
-
overloads && overloads.filter(overload => !overload.$IsBound).forEach(overload => pathItemFunction(paths, '/' + name, [], child.$Function, overload, child.$EntitySet, child));
|
|
1483
|
-
}
|
|
1484
|
-
|
|
1485
|
-
/**
|
|
1486
|
-
* Add path and Path Item Object for function overload
|
|
1487
|
-
* @param {object} paths Paths Object to augment
|
|
1488
|
-
* @param {string} prefix Prefix for path
|
|
1489
|
-
* @param {Array} prefixParameters Parameter Objects for prefix
|
|
1490
|
-
* @param {string} functionName Qualified name of function
|
|
1491
|
-
* @param {object} overload Function overload
|
|
1492
|
-
* @param {string} sourceName Name of path source
|
|
1493
|
-
* @param {object} functionImport Function Import
|
|
1494
|
-
*/
|
|
1495
|
-
function pathItemFunction(paths, prefix, prefixParameters, functionName, overload, sourceName, functionImport = {}) {
|
|
1496
|
-
const name = functionName.indexOf('.') === -1 ? functionName : nameParts(functionName).name;
|
|
1497
|
-
let parameters = overload.$Parameter || [];
|
|
1498
|
-
if (overload.$IsBound) parameters = parameters.slice(1);
|
|
1499
|
-
const pathSegments = [];
|
|
1500
|
-
const params = [];
|
|
1501
|
-
|
|
1502
|
-
const implicitAliases = csdl.$Version > '4.0' || parameters.some(p => p[voc.Core.OptionalParameter]);
|
|
1503
|
-
|
|
1504
|
-
parameters.forEach(p => {
|
|
1505
|
-
const param = {
|
|
1506
|
-
required: implicitAliases ? !p[voc.Core.OptionalParameter] : true
|
|
1507
|
-
};
|
|
1508
|
-
const description = [p[voc.Core.Description], p[voc.Core.LongDescription]].filter(t => t).join(' \n');
|
|
1509
|
-
if (description) param.description = description;
|
|
1510
|
-
const type = modelElement(p.$Type || 'Edm.String');
|
|
1511
|
-
// TODO: check whether parameter or type definition of Edm.Stream is annotated with JSON.Schema
|
|
1512
|
-
if (p.$Collection || p.$Type == 'Edm.Stream'
|
|
1513
|
-
|| type && ['ComplexType', 'EntityType'].includes(type.$Kind)
|
|
1514
|
-
|| type && type.$UnderlyingType == 'Edm.Stream') {
|
|
1515
|
-
param.in = 'query';
|
|
1516
|
-
if (implicitAliases) {
|
|
1517
|
-
param.name = p.$Name;
|
|
1518
|
-
} else {
|
|
1519
|
-
pathSegments.push(p.$Name + '=@' + p.$Name);
|
|
1520
|
-
param.name = '@' + p.$Name;
|
|
1521
|
-
}
|
|
1522
|
-
param.schema = { type: 'string' };
|
|
1523
|
-
if (description) param.description += ' \n'; else param.description = '';
|
|
1524
|
-
param.description += 'This is '
|
|
1525
|
-
+ (p.$Collection ? 'a ' : '')
|
|
1526
|
-
+ 'URL-encoded JSON '
|
|
1527
|
-
+ (p.$Collection ? 'array with items ' : '')
|
|
1528
|
-
+ 'of type '
|
|
1529
|
-
+ namespaceQualifiedName(p.$Type || 'Edm.String')
|
|
1530
|
-
+ ', see [Complex and Collection Literals](https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#sec_ComplexandCollectionLiterals)';
|
|
1531
|
-
param.example = p.$Collection ? '[]' : '{}';
|
|
1532
|
-
} else {
|
|
1533
|
-
if (implicitAliases) {
|
|
1534
|
-
param.in = 'query';
|
|
1535
|
-
} else {
|
|
1536
|
-
pathSegments.push(p.$Name + "={" + p.$Name + "}");
|
|
1537
|
-
param.in = 'path';
|
|
1538
|
-
}
|
|
1539
|
-
param.name = p.$Name;
|
|
1540
|
-
if (!p.$Type || p.$Type === "Edm.String" || (type && (!type.$Type || type.$Type === "Edm.String"))) {
|
|
1541
|
-
if (description) param.description += " \n";
|
|
1542
|
-
else param.description = "";
|
|
1543
|
-
param.description += "String value needs to be enclosed in single quotes";
|
|
1544
|
-
}
|
|
1545
|
-
param.schema = getSchema(p, '', true, true);
|
|
1546
|
-
}
|
|
1547
|
-
params.push(param);
|
|
1548
|
-
});
|
|
1549
|
-
|
|
1550
|
-
const pathParameters = implicitAliases ? '' : '(' + pathSegments.join(',') + ')';
|
|
1551
|
-
const pathItem = {
|
|
1552
|
-
get: {
|
|
1553
|
-
summary: functionImport[voc.Core.Description] || overload[voc.Core.Description] || 'Invokes function ' + name,
|
|
1554
|
-
tags: [overload[voc.Common.Label] || sourceName || 'Service Operations'],
|
|
1555
|
-
parameters: prefixParameters.concat(params),
|
|
1556
|
-
responses: response(200, "Success", overload.$ReturnType, overload[voc.Capabilities.OperationRestrictions]?.ErrorResponses),
|
|
1557
|
-
}
|
|
1558
|
-
};
|
|
1559
|
-
const iDescription = functionImport[voc.Core.LongDescription] || overload[voc.Core.LongDescription];
|
|
1560
|
-
if (iDescription) pathItem.get.description = iDescription;
|
|
1561
|
-
customParameters(pathItem.get, overload[voc.Capabilities.OperationRestrictions] || {});
|
|
1562
|
-
paths[prefix + pathParameters] = pathItem;
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
/**
|
|
1566
|
-
* Add path and Path Item Object for batch requests
|
|
1567
|
-
* @param {object} paths Paths Object to augment
|
|
1568
|
-
* @param {object} container Entity container
|
|
1569
|
-
*/
|
|
1570
|
-
function pathItemBatch(paths, container) {
|
|
1571
|
-
const batchSupport = container[voc.Capabilities.BatchSupport] || {};
|
|
1572
|
-
const supported = container[voc.Capabilities.BatchSupported] !== false && batchSupport.Supported !== false;
|
|
1573
|
-
if (supported) {
|
|
1574
|
-
const firstEntitySet = Object.keys(container).filter(child => isIdentifier(child) && container[child].$Collection)[0];
|
|
1575
|
-
paths['/$batch'] = {
|
|
1576
|
-
post: {
|
|
1577
|
-
summary: batchSupport[voc.Core.Description] || 'Sends a group of requests',
|
|
1578
|
-
description: (batchSupport[voc.Core.LongDescription] || 'Group multiple requests into a single request payload, see '
|
|
1579
|
-
+ '[Batch Requests](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_BatchRequests).')
|
|
1580
|
-
+ '\n\n*Please note that "Try it out" is not supported for this request.*',
|
|
1581
|
-
tags: ['Batch Requests'],
|
|
1582
|
-
requestBody: {
|
|
1583
|
-
required: true,
|
|
1584
|
-
description: 'Batch request',
|
|
1585
|
-
content: {
|
|
1586
|
-
'multipart/mixed;boundary=request-separator': {
|
|
1587
|
-
schema: {
|
|
1588
|
-
type: 'string'
|
|
1589
|
-
},
|
|
1590
|
-
example: '--request-separator\n'
|
|
1591
|
-
+ 'Content-Type: application/http\n'
|
|
1592
|
-
+ 'Content-Transfer-Encoding: binary\n\n'
|
|
1593
|
-
+ 'GET ' + firstEntitySet + ' HTTP/1.1\n'
|
|
1594
|
-
+ 'Accept: application/json\n\n'
|
|
1595
|
-
+ '\n--request-separator--'
|
|
1596
|
-
}
|
|
1597
|
-
}
|
|
1598
|
-
},
|
|
1599
|
-
responses: {
|
|
1600
|
-
'4XX': {
|
|
1601
|
-
$ref: '#/components/responses/error'
|
|
1602
|
-
}
|
|
1603
|
-
}
|
|
1604
|
-
}
|
|
1605
|
-
};
|
|
1606
|
-
paths['/$batch'].post.responses[csdl.$Version < '4.0' ? 202 : 200] = {
|
|
1607
|
-
description: 'Batch response',
|
|
1608
|
-
content: {
|
|
1609
|
-
'multipart/mixed': {
|
|
1610
|
-
schema: {
|
|
1611
|
-
type: 'string'
|
|
1612
|
-
},
|
|
1613
|
-
example: '--response-separator\n'
|
|
1614
|
-
+ 'Content-Type: application/http\n\n'
|
|
1615
|
-
+ 'HTTP/1.1 200 OK\n'
|
|
1616
|
-
+ 'Content-Type: application/json\n\n'
|
|
1617
|
-
+ '{...}'
|
|
1618
|
-
+ '\n--response-separator--'
|
|
1619
|
-
}
|
|
1620
|
-
}
|
|
1621
|
-
};
|
|
1622
|
-
}
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
/**
|
|
1626
|
-
* Construct Responses Object
|
|
1627
|
-
* @param {string} code HTTP response code
|
|
1628
|
-
* @param {string} description Description
|
|
1629
|
-
* @param {object} type Response type object
|
|
1630
|
-
* @param {array} errors Array of operation-specific status codes with descriptions
|
|
1631
|
-
*/
|
|
1632
|
-
function response(code, description, type, errors, isCount = true) {
|
|
1633
|
-
const r = {};
|
|
1634
|
-
r[code] = {
|
|
1635
|
-
description: description
|
|
1636
|
-
};
|
|
1637
|
-
let CountPropertyObj = { [csdl.$Version > '4.0' ? '@count' : '@odata.count']: ref('count') };
|
|
1638
|
-
if (code != 204) {
|
|
1639
|
-
const s = getSchema(type);
|
|
1640
|
-
r[code].content = {
|
|
1641
|
-
'application/json': {
|
|
1642
|
-
schema: type.$Collection
|
|
1643
|
-
? {
|
|
1644
|
-
type: 'object',
|
|
1645
|
-
title: 'Collection of ' + nameParts(type.$Type || 'Edm.String').name,
|
|
1646
|
-
properties: {
|
|
1647
|
-
...(isCount && CountPropertyObj),
|
|
1648
|
-
value: s
|
|
1649
|
-
}
|
|
1650
|
-
}
|
|
1651
|
-
: s
|
|
1652
|
-
}
|
|
1653
|
-
};
|
|
1654
|
-
}
|
|
1655
|
-
if (errors) {
|
|
1656
|
-
for (const e of errors) {
|
|
1657
|
-
r[e.StatusCode] = {
|
|
1658
|
-
description: e.Description,
|
|
1659
|
-
content: {
|
|
1660
|
-
"application/json": {
|
|
1661
|
-
schema: { $ref: "#/components/schemas/error" },
|
|
1662
|
-
},
|
|
1663
|
-
},
|
|
1664
|
-
};
|
|
1665
|
-
}
|
|
1666
|
-
} else {
|
|
1667
|
-
r["4XX"] = {
|
|
1668
|
-
$ref: "#/components/responses/error",
|
|
1669
|
-
};
|
|
1670
|
-
}
|
|
1671
|
-
return r;
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
|
-
/**
|
|
1675
|
-
* Construct the Components Object from the types of the CSDL document
|
|
1676
|
-
* @param {object} csdl CSDL document
|
|
1677
|
-
* @param {object} entityContainer Entity Container object
|
|
1678
|
-
* @return {object} Components Object
|
|
1679
|
-
*/
|
|
1680
|
-
function getComponents(csdl, entityContainer) {
|
|
1681
|
-
const c = {
|
|
1682
|
-
schemas: getSchemas(csdl)
|
|
1683
|
-
};
|
|
1684
|
-
|
|
1685
|
-
if (csdl.$EntityContainer) {
|
|
1686
|
-
c.parameters = getParameters();
|
|
1687
|
-
c.responses = {
|
|
1688
|
-
error: {
|
|
1689
|
-
description: 'Error',
|
|
1690
|
-
content: {
|
|
1691
|
-
'application/json': {
|
|
1692
|
-
schema: ref('error')
|
|
1693
|
-
}
|
|
1694
|
-
}
|
|
1695
|
-
}
|
|
1696
|
-
};
|
|
1697
|
-
}
|
|
1698
|
-
|
|
1699
|
-
getSecuritySchemes(c, entityContainer)
|
|
1700
|
-
|
|
1701
|
-
return c;
|
|
1702
|
-
}
|
|
1703
|
-
|
|
1704
|
-
/**
|
|
1705
|
-
* Construct Schema Objects from the types of the CSDL document
|
|
1706
|
-
* @param {object} csdl CSDL document
|
|
1707
|
-
* @return {object} Map of Schema Objects
|
|
1708
|
-
*/
|
|
1709
|
-
function getSchemas(csdl) {
|
|
1710
|
-
const unordered = {};
|
|
1711
|
-
|
|
1712
|
-
for (const r of requiredSchemas.list) {
|
|
1713
|
-
const type = modelElement(`${r.namespace}.${r.name}`);
|
|
1714
|
-
if (!type) continue;
|
|
1715
|
-
switch (type.$Kind) {
|
|
1716
|
-
case "ComplexType":
|
|
1717
|
-
case "EntityType":
|
|
1718
|
-
schemasForStructuredType(unordered, r.namespace, r.name, type, r.suffix);
|
|
1719
|
-
break;
|
|
1720
|
-
case "EnumType":
|
|
1721
|
-
schemaForEnumerationType(unordered, r.namespace, r.name, type);
|
|
1722
|
-
break;
|
|
1723
|
-
case "TypeDefinition":
|
|
1724
|
-
schemaForTypeDefinition(unordered, r.namespace, r.name, type);
|
|
1725
|
-
break;
|
|
1726
|
-
}
|
|
1727
|
-
}
|
|
1728
|
-
const ordered = {};
|
|
1729
|
-
for (const name of Object.keys(unordered).sort()) {
|
|
1730
|
-
ordered[name] = unordered[name];
|
|
1731
|
-
}
|
|
1732
|
-
|
|
1733
|
-
inlineTypes(ordered);
|
|
1734
|
-
|
|
1735
|
-
if (csdl.$EntityContainer) {
|
|
1736
|
-
ordered.count = count();
|
|
1737
|
-
ordered.error = error();
|
|
1738
|
-
}
|
|
1739
|
-
|
|
1740
|
-
return ordered;
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1743
|
-
/**
|
|
1744
|
-
* Construct Schema Objects from the types of the CSDL document
|
|
1745
|
-
* @param {object} schemas Map of Schema Objects to augment
|
|
1746
|
-
*/
|
|
1747
|
-
function inlineTypes(schemas) {
|
|
1748
|
-
if (typesToInline.geoPoint) {
|
|
1749
|
-
schemas.geoPoint = {
|
|
1750
|
-
type: 'object',
|
|
1751
|
-
properties: {
|
|
1752
|
-
coordinates: ref('geoPosition'),
|
|
1753
|
-
type: {
|
|
1754
|
-
type: 'string',
|
|
1755
|
-
enum: ['Point'],
|
|
1756
|
-
default: 'Point'
|
|
1757
|
-
}
|
|
1758
|
-
},
|
|
1759
|
-
required: ['type', 'coordinates']
|
|
1760
|
-
};
|
|
1761
|
-
schemas.geoPosition = {
|
|
1762
|
-
type: 'array',
|
|
1763
|
-
minItems: 2,
|
|
1764
|
-
items: {
|
|
1765
|
-
type: 'number'
|
|
1766
|
-
}
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
}
|
|
1770
|
-
|
|
1771
|
-
/**
|
|
1772
|
-
* Construct Schema Objects for an enumeration type
|
|
1773
|
-
* @param {object} schemas Map of Schema Objects to augment
|
|
1774
|
-
* @param {string} qualifier Qualifier for structured type
|
|
1775
|
-
* @param {string} name Simple name of structured type
|
|
1776
|
-
* @param {object} type Structured type
|
|
1777
|
-
* @return {object} Map of Schemas Objects
|
|
1778
|
-
*/
|
|
1779
|
-
function schemaForEnumerationType(schemas, qualifier, name, type) {
|
|
1780
|
-
const members = [];
|
|
1781
|
-
Object.keys(type).filter(iName => isIdentifier(iName)).forEach(iName2 => {
|
|
1782
|
-
members.push(iName2);
|
|
1783
|
-
});
|
|
1784
|
-
|
|
1785
|
-
const s = {
|
|
1786
|
-
type: 'string',
|
|
1787
|
-
title: name,
|
|
1788
|
-
enum: members
|
|
1789
|
-
};
|
|
1790
|
-
const description = type[voc.Core.LongDescription];
|
|
1791
|
-
if (description) s.description = description;
|
|
1792
|
-
schemas[qualifier + '.' + name] = s;
|
|
1793
|
-
}
|
|
1794
|
-
|
|
1795
|
-
/**
|
|
1796
|
-
* Construct Schema Objects for a type definition
|
|
1797
|
-
* @param {object} schemas Map of Schema Objects to augment
|
|
1798
|
-
* @param {string} qualifier Qualifier for structured type
|
|
1799
|
-
* @param {string} name Simple name of structured type
|
|
1800
|
-
* @param {object} type Structured type
|
|
1801
|
-
* @return {object} Map of Schemas Objects
|
|
1802
|
-
*/
|
|
1803
|
-
function schemaForTypeDefinition(schemas, qualifier, name, type) {
|
|
1804
|
-
const s = getSchema(Object.assign({ $Type: type.$UnderlyingType }, type));
|
|
1805
|
-
s.title = name;
|
|
1806
|
-
const description = type[voc.Core.LongDescription];
|
|
1807
|
-
if (description) s.description = description;
|
|
1808
|
-
schemas[qualifier + '.' + name] = s;
|
|
1809
|
-
}
|
|
1810
|
-
|
|
1811
|
-
/**
|
|
1812
|
-
* Construct Schema Objects for a structured type
|
|
1813
|
-
* @param {object} schemas Map of Schema Objects to augment
|
|
1814
|
-
* @param {string} qualifier Qualifier for structured type
|
|
1815
|
-
* @param {string} name Simple name of structured type
|
|
1816
|
-
* @param {string} suffix Suffix for read/create/update
|
|
1817
|
-
* @param {object} type Structured type
|
|
1818
|
-
* @return {object} Map of Schemas Objects
|
|
1819
|
-
*/
|
|
1820
|
-
function schemasForStructuredType(schemas, qualifier, name, type, suffix) {
|
|
1821
|
-
const schemaName = qualifier + "." + name + suffix;
|
|
1822
|
-
const baseName = qualifier + "." + name;
|
|
1823
|
-
const isKey = keyMap(type);
|
|
1824
|
-
const required = Object.keys(isKey);
|
|
1825
|
-
const schemaProperties = {};
|
|
1826
|
-
let isCount = true;
|
|
1827
|
-
if (csdl[qualifier]?.$Annotations) {
|
|
1828
|
-
const annotations = csdl[qualifier].$Annotations[`${qualifier}.EntityContainer/${name}`];
|
|
1829
|
-
if (annotations && annotations[voc.Capabilities.CountRestrictions] && annotations[voc.Capabilities.CountRestrictions]?.Countable === false) {
|
|
1830
|
-
isCount = false;
|
|
1831
|
-
}
|
|
1832
|
-
}
|
|
1833
|
-
const properties = propertiesOfStructuredType(type);
|
|
1834
|
-
Object.keys(properties).forEach(iName => {
|
|
1835
|
-
const property = properties[iName];
|
|
1836
|
-
if (suffix === SUFFIX.read) schemaProperties[iName] = getSchema(property);
|
|
1837
|
-
if ((Object.prototype.hasOwnProperty.call(property, '@Common.FieldControl')) && property['@Common.FieldControl'] === 'Mandatory') { required.push(iName) }
|
|
1838
|
-
if (property.$Kind == 'NavigationProperty') {
|
|
1839
|
-
if (property.$Collection && suffix === "" && isCount === true) {
|
|
1840
|
-
schemaProperties[`${iName}@${csdl.$Version === '4.0' ? 'odata.' : ''}count`] = ref('count');
|
|
1841
|
-
}
|
|
1842
|
-
if (property[voc.Core.Permissions] != "Read" && !property[voc.Core.Computed] && ( property.$ContainsTarget || property.$OnDelete === 'Cascade') ) {
|
|
1843
|
-
if (suffix === SUFFIX.create)
|
|
1844
|
-
schemaProperties[iName] = getSchema(property, SUFFIX.create);
|
|
1845
|
-
if (suffix === SUFFIX.update)
|
|
1846
|
-
schemaProperties[iName] = getSchema(property, SUFFIX.create);
|
|
1847
|
-
}
|
|
1848
|
-
} else {
|
|
1849
|
-
if (property[voc.Core.Permissions] === "Read" || property[voc.Core.Computed]) {
|
|
1850
|
-
let index = required.indexOf(iName);
|
|
1851
|
-
if (index != -1) required.splice(index, 1);
|
|
1852
|
-
} else {
|
|
1853
|
-
if (suffix === SUFFIX.create)
|
|
1854
|
-
schemaProperties[iName] = getSchema(property, SUFFIX.create);
|
|
1855
|
-
if (suffix === SUFFIX.update && !isKey[iName] && !property[voc.Core.Immutable])
|
|
1856
|
-
schemaProperties[iName] = getSchema(property, SUFFIX.update);
|
|
1857
|
-
}
|
|
1858
|
-
}
|
|
1859
|
-
});
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
schemas[schemaName] = {
|
|
1863
|
-
title: (type[voc.Core.Description] || name) + TITLE_SUFFIX[suffix],
|
|
1864
|
-
type: 'object'
|
|
1865
|
-
};
|
|
1866
|
-
if (Object.keys(schemaProperties).length > 0)
|
|
1867
|
-
schemas[schemaName].properties = schemaProperties;
|
|
1868
|
-
|
|
1869
|
-
if (suffix === SUFFIX.read && type["@ODM.root"]) schemas[schemaName]["x-sap-root-entity"] = type["@ODM.root"]
|
|
1870
|
-
odmExtensions(type, schemas[schemaName]);
|
|
1871
|
-
|
|
1872
|
-
if (suffix === SUFFIX.create && required.length > 0)
|
|
1873
|
-
schemas[schemaName].required = [...new Set(required)];
|
|
1874
|
-
|
|
1875
|
-
const description = type[voc.Core.LongDescription];
|
|
1876
|
-
if (description) {
|
|
1877
|
-
schemas[schemaName].description = description;
|
|
1878
|
-
}
|
|
1879
|
-
|
|
1880
|
-
if (derivedTypes[baseName]) {
|
|
1881
|
-
schemas[schemaName].anyOf = [];
|
|
1882
|
-
derivedTypes[baseName].forEach((derivedType) => {
|
|
1883
|
-
schemas[schemaName].anyOf.push(ref(derivedType, suffix));
|
|
1884
|
-
});
|
|
1885
|
-
if (!type.$Abstract) schemas[schemaName].anyOf.push({});
|
|
1886
|
-
}
|
|
1887
|
-
}
|
|
1888
|
-
|
|
1889
|
-
/**
|
|
1890
|
-
* Add ODM extensions to OpenAPI schema for a structured type
|
|
1891
|
-
* @param {object} type Structured type
|
|
1892
|
-
* @param {object} schema OpenAPI schema to augment
|
|
1893
|
-
*/
|
|
1894
|
-
function odmExtensions(type, schema) {
|
|
1895
|
-
for (const [annotation, openApiExtension] of Object.entries(ODM_ANNOTATIONS)) {
|
|
1896
|
-
if (type[annotation]) schema[openApiExtension] = type[annotation];
|
|
1897
|
-
}
|
|
1898
|
-
}
|
|
1899
|
-
|
|
1900
|
-
/**
|
|
1901
|
-
* Collect all properties of a structured type along the inheritance hierarchy
|
|
1902
|
-
* @param {object} type Structured type
|
|
1903
|
-
* @return {object} Map of properties
|
|
1904
|
-
*/
|
|
1905
|
-
function propertiesOfStructuredType(type) {
|
|
1906
|
-
const properties = (type && type.$BaseType) ? propertiesOfStructuredType(modelElement(type.$BaseType)) : {};
|
|
1907
|
-
if (type) {
|
|
1908
|
-
Object.keys(type).filter(name => isIdentifier(name)).forEach(name => {
|
|
1909
|
-
properties[name] = type[name];
|
|
1910
|
-
});
|
|
1911
|
-
}
|
|
1912
|
-
return properties;
|
|
1913
|
-
}
|
|
1914
|
-
|
|
1915
|
-
/**
|
|
1916
|
-
* Construct Parameter Objects for type-independent OData system query options
|
|
1917
|
-
* @return {object} Map of Parameter Objects
|
|
1918
|
-
*/
|
|
1919
|
-
function getParameters() {
|
|
1920
|
-
const param = {
|
|
1921
|
-
top: {
|
|
1922
|
-
name: queryOptionPrefix + 'top',
|
|
1923
|
-
in: 'query',
|
|
1924
|
-
description: 'Show only the first n items, see [Paging - Top](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptiontop)',
|
|
1925
|
-
schema: {
|
|
1926
|
-
type: 'integer',
|
|
1927
|
-
minimum: 0
|
|
1928
|
-
},
|
|
1929
|
-
example: 50
|
|
1930
|
-
},
|
|
1931
|
-
skip: {
|
|
1932
|
-
name: queryOptionPrefix + 'skip',
|
|
1933
|
-
in: 'query',
|
|
1934
|
-
description: 'Skip the first n items, see [Paging - Skip](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionskip)',
|
|
1935
|
-
schema: {
|
|
1936
|
-
type: 'integer',
|
|
1937
|
-
minimum: 0
|
|
1938
|
-
}
|
|
1939
|
-
},
|
|
1940
|
-
count: {
|
|
1941
|
-
name: queryOptionPrefix + 'count',
|
|
1942
|
-
in: 'query',
|
|
1943
|
-
description: 'Include count of items, see [Count](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptioncount)',
|
|
1944
|
-
schema: {
|
|
1945
|
-
type: 'boolean'
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
};
|
|
1949
|
-
|
|
1950
|
-
if (csdl.$Version >= '4.0') param.search = {
|
|
1951
|
-
name: queryOptionPrefix + 'search',
|
|
1952
|
-
in: 'query',
|
|
1953
|
-
description: 'Search items by search phrases, see [Searching](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionsearch)',
|
|
1954
|
-
schema: {
|
|
1955
|
-
type: 'string'
|
|
1956
|
-
}
|
|
1957
|
-
};
|
|
1958
|
-
|
|
1959
|
-
return param;
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
|
-
/**
|
|
1963
|
-
* Construct OData error response
|
|
1964
|
-
* @return {object} Error response schema
|
|
1965
|
-
*/
|
|
1966
|
-
function error() {
|
|
1967
|
-
const err = {
|
|
1968
|
-
type: 'object',
|
|
1969
|
-
required: ['error'],
|
|
1970
|
-
properties: {
|
|
1971
|
-
error: {
|
|
1972
|
-
type: 'object',
|
|
1973
|
-
required: ['code', 'message'],
|
|
1974
|
-
properties: {
|
|
1975
|
-
code: { type: 'string' },
|
|
1976
|
-
message: { type: 'string' },
|
|
1977
|
-
target: { type: 'string' },
|
|
1978
|
-
details: {
|
|
1979
|
-
type: 'array',
|
|
1980
|
-
items: {
|
|
1981
|
-
type: 'object',
|
|
1982
|
-
required: ['code', 'message'],
|
|
1983
|
-
properties: {
|
|
1984
|
-
code: { type: 'string' },
|
|
1985
|
-
message: { type: 'string' },
|
|
1986
|
-
target: { type: 'string' }
|
|
1987
|
-
}
|
|
1988
|
-
}
|
|
1989
|
-
},
|
|
1990
|
-
innererror: {
|
|
1991
|
-
type: 'object',
|
|
1992
|
-
description: 'The structure of this object is service-specific'
|
|
1993
|
-
}
|
|
1994
|
-
}
|
|
1995
|
-
}
|
|
1996
|
-
}
|
|
1997
|
-
};
|
|
1998
|
-
|
|
1999
|
-
if (csdl.$Version < '4.0') {
|
|
2000
|
-
err.properties.error.properties.message = {
|
|
2001
|
-
type: 'object',
|
|
2002
|
-
properties: {
|
|
2003
|
-
lang: { type: 'string' },
|
|
2004
|
-
value: { type: 'string' }
|
|
2005
|
-
},
|
|
2006
|
-
required: ['lang', 'value']
|
|
2007
|
-
};
|
|
2008
|
-
delete err.properties.error.properties.details;
|
|
2009
|
-
delete err.properties.error.properties.target;
|
|
2010
|
-
}
|
|
2011
|
-
|
|
2012
|
-
return err;
|
|
2013
|
-
}
|
|
2014
|
-
|
|
2015
|
-
/**
|
|
2016
|
-
* Construct OData count response
|
|
2017
|
-
* @return {object} Count response schema
|
|
2018
|
-
*/
|
|
2019
|
-
function count() {
|
|
2020
|
-
return {
|
|
2021
|
-
anyOf: [
|
|
2022
|
-
{ type: 'number' },
|
|
2023
|
-
{ type: 'string' }
|
|
2024
|
-
],
|
|
2025
|
-
description: 'The number of entities in the collection. Available when using the [$count](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptioncount) query option.',
|
|
2026
|
-
};
|
|
2027
|
-
}
|
|
2028
|
-
|
|
2029
|
-
/**
|
|
2030
|
-
* Construct Schema Object for model object referencing a type
|
|
2031
|
-
* @param {object} modelElement referencing a type
|
|
2032
|
-
* @return {object} Schema Object
|
|
2033
|
-
*/
|
|
2034
|
-
function getSchema(element, suffix = '', forParameter = false, forFunction = false) {
|
|
2035
|
-
let s = {};
|
|
2036
|
-
switch (element.$Type) {
|
|
2037
|
-
case 'Edm.AnnotationPath':
|
|
2038
|
-
case 'Edm.ModelElementPath':
|
|
2039
|
-
case 'Edm.NavigationPropertyPath':
|
|
2040
|
-
case 'Edm.PropertyPath':
|
|
2041
|
-
s.type = 'string';
|
|
2042
|
-
break;
|
|
2043
|
-
case 'Edm.Binary':
|
|
2044
|
-
s = {
|
|
2045
|
-
type: 'string',
|
|
2046
|
-
format: 'base64url'
|
|
2047
|
-
};
|
|
2048
|
-
if (element.$MaxLength) s.maxLength = Math.ceil(4 * element.$MaxLength / 3);
|
|
2049
|
-
break;
|
|
2050
|
-
case 'Edm.Boolean':
|
|
2051
|
-
s.type = 'boolean';
|
|
2052
|
-
break;
|
|
2053
|
-
case 'Edm.Byte':
|
|
2054
|
-
s = {
|
|
2055
|
-
type: 'integer',
|
|
2056
|
-
format: 'uint8'
|
|
2057
|
-
};
|
|
2058
|
-
break;
|
|
2059
|
-
case 'Edm.Date':
|
|
2060
|
-
s = {
|
|
2061
|
-
type: 'string',
|
|
2062
|
-
format: 'date',
|
|
2063
|
-
example: '2017-04-13'
|
|
2064
|
-
};
|
|
2065
|
-
break;
|
|
2066
|
-
case 'Edm.DateTime':
|
|
2067
|
-
case 'Edm.DateTimeOffset':
|
|
2068
|
-
s = {
|
|
2069
|
-
type: 'string',
|
|
2070
|
-
format: 'date-time',
|
|
2071
|
-
example: '2017-04-13T15:51:04' + (isNaN(element.$Precision) || element.$Precision === 0 ? '' : '.' + '0'.repeat(element.$Precision)) + 'Z'
|
|
2072
|
-
};
|
|
2073
|
-
break;
|
|
2074
|
-
case 'Edm.Decimal':
|
|
2075
|
-
s = {
|
|
2076
|
-
anyOf: [{ type: 'number', format: 'decimal' }, { type: 'string' }],
|
|
2077
|
-
example: 0
|
|
2078
|
-
};
|
|
2079
|
-
if (!isNaN(element.$Precision)) s['x-sap-precision'] = element.$Precision;
|
|
2080
|
-
if (!isNaN(element.$Scale)) s['x-sap-scale'] = element.$Scale;
|
|
2081
|
-
// eslint-disable-next-line no-case-declarations
|
|
2082
|
-
let scale = !isNaN(element.$Scale) ? element.$Scale : null;
|
|
2083
|
-
if (scale !== null) {
|
|
2084
|
-
// Node.js 12.13.0 has problems with negative exponents, 10 ** -5 --> 0.000009999999999999999
|
|
2085
|
-
if (scale <= 0)
|
|
2086
|
-
s.anyOf[0].multipleOf = 10 ** -scale;
|
|
2087
|
-
else
|
|
2088
|
-
s.anyOf[0].multipleOf = 1 / 10 ** scale;
|
|
2089
|
-
}
|
|
2090
|
-
if (element.$Precision < 16) {
|
|
2091
|
-
let limit = 10 ** (element.$Precision - scale);
|
|
2092
|
-
let delta = 10 ** -scale;
|
|
2093
|
-
s.anyOf[0].maximum = limit - delta;
|
|
2094
|
-
s.anyOf[0].minimum = -s.anyOf[0].maximum;
|
|
2095
|
-
}
|
|
2096
|
-
break;
|
|
2097
|
-
case 'Edm.Double':
|
|
2098
|
-
s = {
|
|
2099
|
-
anyOf: [{ type: 'number', format: 'double' }, { type: 'string' }],
|
|
2100
|
-
example: 3.14
|
|
2101
|
-
};
|
|
2102
|
-
break;
|
|
2103
|
-
case 'Edm.Duration':
|
|
2104
|
-
s = {
|
|
2105
|
-
type: 'string',
|
|
2106
|
-
format: 'duration',
|
|
2107
|
-
example: 'P4DT15H51M04S'
|
|
2108
|
-
};
|
|
2109
|
-
break;
|
|
2110
|
-
case 'Edm.GeographyPoint':
|
|
2111
|
-
case 'Edm.GeometryPoint':
|
|
2112
|
-
s = ref('geoPoint');
|
|
2113
|
-
typesToInline.geoPoint = true;
|
|
2114
|
-
break;
|
|
2115
|
-
case 'Edm.Guid':
|
|
2116
|
-
s = {
|
|
2117
|
-
type: 'string',
|
|
2118
|
-
format: 'uuid',
|
|
2119
|
-
example: '01234567-89ab-cdef-0123-456789abcdef'
|
|
2120
|
-
};
|
|
2121
|
-
break;
|
|
2122
|
-
case 'Edm.Int16':
|
|
2123
|
-
s = {
|
|
2124
|
-
type: 'integer',
|
|
2125
|
-
format: 'int16'
|
|
2126
|
-
};
|
|
2127
|
-
break;
|
|
2128
|
-
case 'Edm.Int32':
|
|
2129
|
-
s = {
|
|
2130
|
-
type: 'integer',
|
|
2131
|
-
format: 'int32'
|
|
2132
|
-
};
|
|
2133
|
-
break;
|
|
2134
|
-
case 'Edm.Int64':
|
|
2135
|
-
s = {
|
|
2136
|
-
anyOf: [{ type: 'integer', format: 'int64' }, { type: 'string' }],
|
|
2137
|
-
example: "42"
|
|
2138
|
-
};
|
|
2139
|
-
break;
|
|
2140
|
-
case 'Edm.PrimitiveType':
|
|
2141
|
-
s = {
|
|
2142
|
-
anyOf: [{ type: 'boolean' }, { type: 'number' }, { type: 'string' }]
|
|
2143
|
-
};
|
|
2144
|
-
break;
|
|
2145
|
-
case 'Edm.SByte':
|
|
2146
|
-
s = {
|
|
2147
|
-
type: 'integer',
|
|
2148
|
-
format: 'int8'
|
|
2149
|
-
};
|
|
2150
|
-
break;
|
|
2151
|
-
case 'Edm.Single':
|
|
2152
|
-
s = {
|
|
2153
|
-
anyOf: [{ type: 'number', format: 'float' }, { type: 'string' }],
|
|
2154
|
-
example: 3.14
|
|
2155
|
-
};
|
|
2156
|
-
break;
|
|
2157
|
-
case 'Edm.Stream':
|
|
2158
|
-
// eslint-disable-next-line no-case-declarations
|
|
2159
|
-
let jsonSchema = element[voc.JSON.Schema];
|
|
2160
|
-
if (jsonSchema) {
|
|
2161
|
-
if (typeof jsonSchema == 'string')
|
|
2162
|
-
s = JSON.parse(jsonSchema);
|
|
2163
|
-
else
|
|
2164
|
-
s = jsonSchema;
|
|
2165
|
-
} else {
|
|
2166
|
-
s = {
|
|
2167
|
-
type: 'string',
|
|
2168
|
-
format: 'base64url'
|
|
2169
|
-
};
|
|
2170
|
-
}
|
|
2171
|
-
break;
|
|
2172
|
-
case 'Edm.String':
|
|
2173
|
-
case undefined:
|
|
2174
|
-
s.type = 'string';
|
|
2175
|
-
if (element.$MaxLength) s.maxLength = element.$MaxLength;
|
|
2176
|
-
allowedValues(s, element);
|
|
2177
|
-
getPattern(s, element);
|
|
2178
|
-
break;
|
|
2179
|
-
case 'Edm.TimeOfDay':
|
|
2180
|
-
s = {
|
|
2181
|
-
type: 'string',
|
|
2182
|
-
format: 'time',
|
|
2183
|
-
example: '15:51:04'
|
|
2184
|
-
};
|
|
2185
|
-
break;
|
|
2186
|
-
default:
|
|
2187
|
-
if (element.$Type.startsWith('Edm.')) {
|
|
2188
|
-
console.warn('Unknown type: ' + element.$Type);
|
|
2189
|
-
} else {
|
|
2190
|
-
let type = modelElement(element.$Type);
|
|
2191
|
-
let isStructured = type && ['ComplexType', 'EntityType'].includes(type.$Kind);
|
|
2192
|
-
s = ref(element.$Type, (isStructured ? suffix : ''));
|
|
2193
|
-
if (element.$MaxLength) {
|
|
2194
|
-
s = {
|
|
2195
|
-
allOf: [s],
|
|
2196
|
-
maxLength: element.$MaxLength
|
|
2197
|
-
};
|
|
2198
|
-
}
|
|
2199
|
-
}
|
|
2200
|
-
}
|
|
2201
|
-
|
|
2202
|
-
if (element.$Nullable) {
|
|
2203
|
-
if (s.$ref) s = { allOf: [s] };
|
|
2204
|
-
s.nullable = true;
|
|
2205
|
-
}
|
|
2206
|
-
|
|
2207
|
-
if (element.$DefaultValue !== undefined) {
|
|
2208
|
-
if (s.$ref) s = { allOf: [s] };
|
|
2209
|
-
s.default = element.$DefaultValue;
|
|
2210
|
-
}
|
|
2211
|
-
|
|
2212
|
-
if (element[voc.Core.Example]) {
|
|
2213
|
-
if (s.$ref) s = { allOf: [s] };
|
|
2214
|
-
s.example = element[voc.Core.Example].Value;
|
|
2215
|
-
}
|
|
2216
|
-
|
|
2217
|
-
if (forFunction) {
|
|
2218
|
-
if (s.example && typeof s.example === "string") {
|
|
2219
|
-
s.example = `${pathValuePrefix(element.$Type)}${s.example
|
|
2220
|
-
}${pathValueSuffix(element.$Type)} `;
|
|
2221
|
-
}
|
|
2222
|
-
if (s.pattern) {
|
|
2223
|
-
const pre = pathValuePrefix(element.$Type);
|
|
2224
|
-
const suf = pathValueSuffix(element.$Type);
|
|
2225
|
-
s.pattern = s.pattern.replace(/^\^/, `^ ${pre} (`);
|
|
2226
|
-
s.pattern = s.pattern.replace(/\$$/, `)${suf} $`);
|
|
2227
|
-
} else if (!element.$Type || element.$Type === "Edm.String") {
|
|
2228
|
-
s.pattern = "^'([^']|'')*'$";
|
|
2229
|
-
}
|
|
2230
|
-
if (element.$Nullable) {
|
|
2231
|
-
if (!s.anyOf && !s.allOf) s.type = "string";
|
|
2232
|
-
if (s.format) s.format += ",null";
|
|
2233
|
-
if (s.anyOf && s.anyOf[0].format) s.anyOf[0].format += ",null";
|
|
2234
|
-
s.default = "null";
|
|
2235
|
-
if (s.pattern) {
|
|
2236
|
-
s.pattern = s.pattern.replace(/^\^/, "^(null|");
|
|
2237
|
-
s.pattern = s.pattern.replace(/\$$/, ")$");
|
|
2238
|
-
}
|
|
2239
|
-
}
|
|
2240
|
-
}
|
|
2241
|
-
|
|
2242
|
-
if (element[voc.Validation.Maximum] != undefined) {
|
|
2243
|
-
if (s.$ref) s = { allOf: [s] };
|
|
2244
|
-
if (s.anyOf) {
|
|
2245
|
-
s.anyOf[0].maximum = element[voc.Validation.Maximum];
|
|
2246
|
-
}
|
|
2247
|
-
if (element[voc.Validation.Maximum + voc.Validation.Exclusive]) s.exclusiveMaximum = true;
|
|
2248
|
-
}
|
|
2249
|
-
|
|
2250
|
-
if (element[voc.Validation.Minimum] != undefined) {
|
|
2251
|
-
if (s.$ref) s = { allOf: [s] };
|
|
2252
|
-
if (s.anyOf) {
|
|
2253
|
-
s.anyOf[0].minimum = element[voc.Validation.Minimum];
|
|
2254
|
-
}
|
|
2255
|
-
if (element[voc.Validation.Minimum + voc.Validation.Exclusive]) s.exclusiveMinimum = true;
|
|
2256
|
-
}
|
|
2257
|
-
|
|
2258
|
-
if (element.$Collection) {
|
|
2259
|
-
s = {
|
|
2260
|
-
type: 'array',
|
|
2261
|
-
items: s
|
|
2262
|
-
};
|
|
2263
|
-
}
|
|
2264
|
-
|
|
2265
|
-
if (!forParameter && element[voc.Core.LongDescription]) {
|
|
2266
|
-
if (s.$ref) s = { allOf: [s] };
|
|
2267
|
-
s.description = element[voc.Core.LongDescription];
|
|
2268
|
-
}
|
|
2269
|
-
|
|
2270
|
-
if (element['@ODM.oidReference']?.entityName) {
|
|
2271
|
-
s['x-sap-odm-oid-reference-entity-name'] = element['@ODM.oidReference'].entityName
|
|
2272
|
-
}
|
|
2273
|
-
|
|
2274
|
-
return s;
|
|
2275
|
-
}
|
|
2276
|
-
|
|
2277
|
-
/**
|
|
2278
|
-
* Add allowed values enum to Schema Object for string-like model element
|
|
2279
|
-
* @param {object} schema Schema Object to augment
|
|
2280
|
-
* @param {object} element Model element
|
|
2281
|
-
*/
|
|
2282
|
-
function allowedValues(schema, element) {
|
|
2283
|
-
const values = element[voc.Validation.AllowedValues];
|
|
2284
|
-
if (values) schema.enum = values.map(record => record.Value);
|
|
2285
|
-
}
|
|
2286
|
-
|
|
2287
|
-
/**
|
|
2288
|
-
* Add pattern to Schema Object for string-like model element
|
|
2289
|
-
* @param {object} schema Schema Object to augment
|
|
2290
|
-
* @param {object} element Model element
|
|
2291
|
-
*/
|
|
2292
|
-
function getPattern(schema, element) {
|
|
2293
|
-
const pattern = element[voc.Validation.Pattern];
|
|
2294
|
-
if (pattern) schema.pattern = pattern;
|
|
2295
|
-
}
|
|
2296
|
-
|
|
2297
|
-
/**
|
|
2298
|
-
* Construct Reference Object for a type
|
|
2299
|
-
* @param {string} typename Qualified name of referenced type
|
|
2300
|
-
* @param {string} suffix Optional suffix for referenced schema
|
|
2301
|
-
* @return {object} Reference Object
|
|
2302
|
-
*/
|
|
2303
|
-
function ref(typename, suffix = '') {
|
|
2304
|
-
let name = typename;
|
|
2305
|
-
let nsp = '';
|
|
2306
|
-
let url = '';
|
|
2307
|
-
if (typename.indexOf('.') != -1) {
|
|
2308
|
-
let parts = nameParts(typename);
|
|
2309
|
-
nsp = namespace[parts.qualifier];
|
|
2310
|
-
name = nsp + '.' + parts.name;
|
|
2311
|
-
url = namespaceUrl[nsp] || '';
|
|
2312
|
-
if (url === "" && !requiredSchemas.used[name + suffix]) {
|
|
2313
|
-
requiredSchemas.used[name + suffix] = true;
|
|
2314
|
-
requiredSchemas.list.push({ namespace: nsp, name: parts.name, suffix });
|
|
2315
|
-
}
|
|
2316
|
-
//TODO: introduce better way than guessing
|
|
2317
|
-
if (url.endsWith('.xml')) url = url.substring(0, url.length - 3) + "openapi3.json";
|
|
2318
|
-
}
|
|
2319
|
-
return {
|
|
2320
|
-
$ref: url + '#/components/schemas/' + name + suffix
|
|
2321
|
-
};
|
|
2322
|
-
}
|
|
2323
|
-
|
|
2324
|
-
/**
|
|
2325
|
-
* Augment Components Object with map of Security Scheme Objects
|
|
2326
|
-
* @param {object} components Components Object to augment
|
|
2327
|
-
* @param {object} entityContainer Entity Container object
|
|
2328
|
-
*/
|
|
2329
|
-
function getSecuritySchemes(components, entityContainer) {
|
|
2330
|
-
const authorizations = entityContainer && entityContainer[voc.Authorization.Authorizations] ? entityContainer[voc.Authorization.Authorizations] : [];
|
|
2331
|
-
const schemes = {};
|
|
2332
|
-
const location = { Header: 'header', QueryOption: 'query', Cookie: 'cookie' };
|
|
2333
|
-
authorizations.forEach(auth => {
|
|
2334
|
-
const scheme = {};
|
|
2335
|
-
const flow = {};
|
|
2336
|
-
if (auth.Description) scheme.description = auth.Description;
|
|
2337
|
-
const qualifiedType = auth['@type'] || auth['@odata.type']
|
|
2338
|
-
const type = qualifiedType.substring(qualifiedType.lastIndexOf(".") + 1);
|
|
2339
|
-
let unknown = false
|
|
2340
|
-
switch (type) {
|
|
2341
|
-
case 'ApiKey':
|
|
2342
|
-
scheme.type = 'apiKey';
|
|
2343
|
-
scheme.name = auth.KeyName;
|
|
2344
|
-
scheme.in = location[auth.Location];
|
|
2345
|
-
break;
|
|
2346
|
-
case 'Http':
|
|
2347
|
-
scheme.type = 'http';
|
|
2348
|
-
scheme.scheme = auth.Scheme;
|
|
2349
|
-
scheme.bearerFormat = auth.BearerFormat;
|
|
2350
|
-
break;
|
|
2351
|
-
case 'OAuth2AuthCode':
|
|
2352
|
-
scheme.type = 'oauth2';
|
|
2353
|
-
scheme.flows = { authorizationCode: flow };
|
|
2354
|
-
flow.authorizationUrl = auth.AuthorizationUrl;
|
|
2355
|
-
flow.tokenUrl = auth.TokenUrl;
|
|
2356
|
-
if (auth.RefreshUrl) flow.refreshUrl = auth.RefreshUrl;
|
|
2357
|
-
flow.scopes = getScopes(auth);
|
|
2358
|
-
break;
|
|
2359
|
-
case 'OAuth2ClientCredentials':
|
|
2360
|
-
scheme.type = 'oauth2';
|
|
2361
|
-
scheme.flows = { clientCredentials: flow };
|
|
2362
|
-
flow.tokenUrl = auth.TokenUrl;
|
|
2363
|
-
if (auth.RefreshUrl) flow.refreshUrl = auth.RefreshUrl;
|
|
2364
|
-
flow.scopes = getScopes(auth);
|
|
2365
|
-
break;
|
|
2366
|
-
case 'OAuth2Implicit':
|
|
2367
|
-
scheme.type = 'oauth2';
|
|
2368
|
-
scheme.flows = { implicit: flow };
|
|
2369
|
-
flow.authorizationUrl = auth.AuthorizationUrl;
|
|
2370
|
-
if (auth.RefreshUrl) flow.refreshUrl = auth.RefreshUrl;
|
|
2371
|
-
flow.scopes = getScopes(auth);
|
|
2372
|
-
break;
|
|
2373
|
-
case 'OAuth2Password':
|
|
2374
|
-
scheme.type = 'oauth2';
|
|
2375
|
-
scheme.flows = {};
|
|
2376
|
-
scheme.flows = { password: flow };
|
|
2377
|
-
flow.tokenUrl = auth.TokenUrl;
|
|
2378
|
-
if (auth.RefreshUrl) flow.refreshUrl = auth.RefreshUrl;
|
|
2379
|
-
flow.scopes = getScopes(auth);
|
|
2380
|
-
break;
|
|
2381
|
-
case 'OpenIDConnect':
|
|
2382
|
-
scheme.type = 'openIdConnect';
|
|
2383
|
-
scheme.openIdConnectUrl = auth.IssuerUrl;
|
|
2384
|
-
break;
|
|
2385
|
-
default:
|
|
2386
|
-
unknown = true
|
|
2387
|
-
console.warn('Unknown Authorization type ' + qualifiedType);
|
|
2388
|
-
}
|
|
2389
|
-
if (!unknown) schemes[auth.Name] = scheme;
|
|
2390
|
-
});
|
|
2391
|
-
if (Object.keys(schemes).length > 0) components.securitySchemes = schemes
|
|
2392
|
-
}
|
|
2393
|
-
|
|
2394
|
-
function getScopes(authorization) {
|
|
2395
|
-
const scopes = {};
|
|
2396
|
-
authorization.Scopes.forEach(scope => { scopes[scope.Scope] = scope.Description });
|
|
2397
|
-
return scopes;
|
|
2398
|
-
}
|
|
2399
|
-
|
|
2400
|
-
/**
|
|
2401
|
-
* Augment OpenAPI document with Security Requirements Object
|
|
2402
|
-
* @param {object} openapi OpenAPI document to augment
|
|
2403
|
-
* @param {object} entityContainer Entity Container object
|
|
2404
|
-
*/
|
|
2405
|
-
function security(openapi, entityContainer) {
|
|
2406
|
-
const securitySchemes = entityContainer && entityContainer[voc.Authorization.SecuritySchemes] ? entityContainer[voc.Authorization.SecuritySchemes] : [];
|
|
2407
|
-
if (securitySchemes.length > 0) openapi.security = [];
|
|
2408
|
-
securitySchemes.forEach(scheme => {
|
|
2409
|
-
const s = {};
|
|
2410
|
-
s[scheme.Authorization] = scheme.RequiredScopes || [];
|
|
2411
|
-
openapi.security.push(s);
|
|
2412
|
-
});
|
|
2413
|
-
}
|
|
2414
|
-
|
|
2415
|
-
/**
|
|
2416
|
-
* a qualified name consists of a namespace or alias, a dot, and a simple name
|
|
2417
|
-
* @param {string} qualifiedName
|
|
2418
|
-
* @return {string} namespace-qualified name
|
|
2419
|
-
*/
|
|
2420
|
-
function namespaceQualifiedName(qualifiedName) {
|
|
2421
|
-
let np = nameParts(qualifiedName);
|
|
2422
|
-
return namespace[np.qualifier] + '.' + np.name;
|
|
2423
|
-
}
|
|
2424
|
-
|
|
2425
|
-
/**
|
|
2426
|
-
* a qualified name consists of a namespace or alias, a dot, and a simple name
|
|
2427
|
-
* @param {string} qualifiedName
|
|
2428
|
-
* @return {object} with components qualifier and name
|
|
2429
|
-
*/
|
|
2430
|
-
function nameParts(qualifiedName) {
|
|
2431
|
-
const pos = qualifiedName.lastIndexOf('.');
|
|
2432
|
-
console.assert(pos > 0, 'Invalid qualified name ' + qualifiedName);
|
|
2433
|
-
return {
|
|
2434
|
-
qualifier: qualifiedName.substring(0, pos),
|
|
2435
|
-
name: qualifiedName.substring(pos + 1)
|
|
2436
|
-
};
|
|
2437
|
-
}
|
|
2438
|
-
|
|
2439
|
-
/**
|
|
2440
|
-
* an identifier does not start with $ and does not contain @
|
|
2441
|
-
* @param {string} name
|
|
2442
|
-
* @return {boolean} name is an identifier
|
|
2443
|
-
*/
|
|
2444
|
-
function isIdentifier(name) {
|
|
2445
|
-
return !name.startsWith('$') && !name.includes('@');
|
|
2446
|
-
}
|
|
2447
|
-
|
|
2448
|
-
};
|