diff --git a/fern/api/definition/api.yml b/fern/api/definition/api.yml new file mode 100644 index 000000000..66147a50a --- /dev/null +++ b/fern/api/definition/api.yml @@ -0,0 +1 @@ +name: example \ No newline at end of file diff --git a/fern/api/definition/example.yaml b/fern/api/definition/example.yaml new file mode 100644 index 000000000..07ab06b83 --- /dev/null +++ b/fern/api/definition/example.yaml @@ -0,0 +1,471 @@ +service: + auth: false + base-path: /example + endpoints: + ReadAllApplication: + path: /application + method: GET + response: ApplicationResponse + request: + name: ReadallApplication + query-parameters: + $filter: optional + $select: optional + $expand: optional + $top: optional + $count: optional + examples: + - query-parameters: + $select: created_at,modified_at,id,name,note + response: + body: + d: + - created_at: 2022-06-19T23:22:46.962Z + modified_at: 2022-08-12T08:38:22.014Z + id: 71065 + name: xrkeejdkpljamasgfarw + note: eomyvjziulccgbqkfcly + - created_at: 2022-06-19T23:22:46.962Z + modified_at: 2022-08-12T08:38:22.014Z + id: 71065 + name: xrkeejdkpljamasgfarw + note: eomyvjziulccgbqkfcly + ReadApplication: + path: /application(id) + method: GET + response: Application + request: + name: ReadApplicationById + examples: + - response: + body: + created_at: 2022-06-19T23:22:46.962Z + modified_at: 2022-08-12T08:38:22.014Z + id: 71065 + name: xrkeejdkpljamasgfarw + note: eomyvjziulccgbqkfcly + CreateApplication: + path: /application(id) + method: POST + response: Application + request: + name: CreateApplicationById + examples: + - response: + body: + created_at: 2022-06-19T23:22:46.962Z + modified_at: 2022-08-12T08:38:22.014Z + id: 71065 + name: xrkeejdkpljamasgfarw + note: eomyvjziulccgbqkfcly + UpdateAllApplication: + path: /application + method: PATCH + request: + name: UpdateallApplication + query-parameters: + $filter: optional + $select: optional + $expand: optional + $top: optional + $count: optional + examples: + - query-parameters: + $select: created_at,modified_at,id,name,note + UpdateApplication: + path: /application(id) + method: PATCH + request: + name: UpdateApplicationById + examples: + - {} + DeleteAllApplication: + path: /application + method: DELETE + request: + name: DeleteallApplication + query-parameters: + $filter: optional + $select: optional + $expand: optional + $top: optional + $count: optional + examples: + - query-parameters: + $select: created_at,modified_at,id,name,note + DeleteApplication: + path: /application(id) + method: DELETE + request: + name: DeleteApplicationById + examples: + - {} + ReadAllDevice: + path: /device + method: GET + response: DeviceResponse + request: + name: ReadallDevice + query-parameters: + $filter: optional + $select: optional + $expand: optional + $top: optional + $count: optional + examples: + - query-parameters: + $select: created_at,modified_at,id,name,type,belongs_to__application + response: + body: + d: + - created_at: 2022-06-25T03:06:57.310Z + modified_at: 2022-12-24T06:08:24.699Z + id: 80097 + name: ewgdekqxythufswoytuc + type: mjlemqmluysjbhenlfuk + - created_at: 2022-06-25T03:06:57.310Z + modified_at: 2022-12-24T06:08:24.699Z + id: 80097 + name: ewgdekqxythufswoytuc + type: mjlemqmluysjbhenlfuk + ReadDevice: + path: /device(id) + method: GET + response: Device + request: + name: ReadDeviceById + examples: + - response: + body: + created_at: 2022-06-25T03:06:57.310Z + modified_at: 2022-12-24T06:08:24.699Z + id: 80097 + name: ewgdekqxythufswoytuc + type: mjlemqmluysjbhenlfuk + CreateDevice: + path: /device(id) + method: POST + response: Device + request: + name: CreateDeviceById + examples: + - response: + body: + created_at: 2022-06-25T03:06:57.310Z + modified_at: 2022-12-24T06:08:24.699Z + id: 80097 + name: ewgdekqxythufswoytuc + type: mjlemqmluysjbhenlfuk + UpdateAllDevice: + path: /device + method: PATCH + request: + name: UpdateallDevice + query-parameters: + $filter: optional + $select: optional + $expand: optional + $top: optional + $count: optional + examples: + - query-parameters: + $select: created_at,modified_at,id,name,type,belongs_to__application + UpdateDevice: + path: /device(id) + method: PATCH + request: + name: UpdateDeviceById + examples: + - {} + DeleteAllDevice: + path: /device + method: DELETE + request: + name: DeleteallDevice + query-parameters: + $filter: optional + $select: optional + $expand: optional + $top: optional + $count: optional + examples: + - query-parameters: + $select: created_at,modified_at,id,name,type,belongs_to__application + DeleteDevice: + path: /device(id) + method: DELETE + request: + name: DeleteDeviceById + examples: + - {} + ReadAllGateway: + path: /gateway + method: GET + response: GatewayResponse + request: + name: ReadallGateway + query-parameters: + $filter: optional + $select: optional + $expand: optional + $top: optional + $count: optional + examples: + - query-parameters: + $select: created_at,modified_at,id,name + response: + body: + d: + - created_at: 2023-03-11T21:19:54.812Z + modified_at: 2022-07-10T20:17:43.327Z + id: 68293 + name: lxakyatbhgluytsaeyvg + - created_at: 2023-03-11T21:19:54.812Z + modified_at: 2022-07-10T20:17:43.327Z + id: 68293 + name: lxakyatbhgluytsaeyvg + ReadGateway: + path: /gateway(id) + method: GET + response: Gateway + request: + name: ReadGatewayById + examples: + - response: + body: + created_at: 2023-03-11T21:19:54.812Z + modified_at: 2022-07-10T20:17:43.327Z + id: 68293 + name: lxakyatbhgluytsaeyvg + CreateGateway: + path: /gateway(id) + method: POST + response: Gateway + request: + name: CreateGatewayById + examples: + - response: + body: + created_at: 2023-03-11T21:19:54.812Z + modified_at: 2022-07-10T20:17:43.327Z + id: 68293 + name: lxakyatbhgluytsaeyvg + UpdateAllGateway: + path: /gateway + method: PATCH + request: + name: UpdateallGateway + query-parameters: + $filter: optional + $select: optional + $expand: optional + $top: optional + $count: optional + examples: + - query-parameters: + $select: created_at,modified_at,id,name + UpdateGateway: + path: /gateway(id) + method: PATCH + request: + name: UpdateGatewayById + examples: + - {} + DeleteAllGateway: + path: /gateway + method: DELETE + request: + name: DeleteallGateway + query-parameters: + $filter: optional + $select: optional + $expand: optional + $top: optional + $count: optional + examples: + - query-parameters: + $select: created_at,modified_at,id,name + DeleteGateway: + path: /gateway(id) + method: DELETE + request: + name: DeleteGatewayById + examples: + - {} + ReadAllGateway__connects__device: + path: /gateway__connects__device + method: GET + response: Gateway__connects__deviceResponse + request: + name: ReadallGateway__connects__device + query-parameters: + $filter: optional + $select: optional + $expand: optional + $top: optional + $count: optional + examples: + - query-parameters: + $select: created_at,modified_at,id,gateway,connects__device + response: + body: + d: + - created_at: 2023-03-12T20:55:03.789Z + modified_at: 2023-01-23T06:09:12.207Z + id: 49798 + - created_at: 2023-03-12T20:55:03.789Z + modified_at: 2023-01-23T06:09:12.207Z + id: 49798 + ReadGateway__connects__device: + path: /gateway__connects__device(id) + method: GET + response: Gateway__connects__device + request: + name: ReadGateway__connects__deviceById + examples: + - response: + body: + created_at: 2023-03-12T20:55:03.789Z + modified_at: 2023-01-23T06:09:12.207Z + id: 49798 + CreateGateway__connects__device: + path: /gateway__connects__device(id) + method: POST + response: Gateway__connects__device + request: + name: CreateGateway__connects__deviceById + examples: + - response: + body: + created_at: 2023-03-12T20:55:03.789Z + modified_at: 2023-01-23T06:09:12.207Z + id: 49798 + UpdateAllGateway__connects__device: + path: /gateway__connects__device + method: PATCH + request: + name: UpdateallGateway__connects__device + query-parameters: + $filter: optional + $select: optional + $expand: optional + $top: optional + $count: optional + examples: + - query-parameters: + $select: created_at,modified_at,id,gateway,connects__device + UpdateGateway__connects__device: + path: /gateway__connects__device(id) + method: PATCH + request: + name: UpdateGateway__connects__deviceById + examples: + - {} + DeleteAllGateway__connects__device: + path: /gateway__connects__device + method: DELETE + request: + name: DeleteallGateway__connects__device + query-parameters: + $filter: optional + $select: optional + $expand: optional + $top: optional + $count: optional + examples: + - query-parameters: + $select: created_at,modified_at,id,gateway,connects__device + DeleteGateway__connects__device: + path: /gateway__connects__device(id) + method: DELETE + request: + name: DeleteGateway__connects__deviceById + examples: + - {} +types: + Application: + properties: + created_at: + type: optional + modified_at: + type: optional + id: + type: long + docs: The unique identifier for a Application + name: + type: string + note: + type: string + examples: + - value: + created_at: 2022-06-19T23:22:46.962Z + modified_at: 2022-08-12T08:38:22.014Z + id: 71065 + name: xrkeejdkpljamasgfarw + note: eomyvjziulccgbqkfcly + ApplicationResponse: + properties: + d: list + Device: + properties: + created_at: + type: optional + modified_at: + type: optional + id: + type: long + docs: The unique identifier for a Device + name: + type: string + type: + type: optional + belongs_to__application: optional + examples: + - value: + created_at: 2022-06-25T03:06:57.310Z + modified_at: 2022-12-24T06:08:24.699Z + id: 80097 + name: ewgdekqxythufswoytuc + type: mjlemqmluysjbhenlfuk + DeviceResponse: + properties: + d: list + Gateway: + properties: + created_at: + type: optional + modified_at: + type: optional + id: + type: long + docs: The unique identifier for a Gateway + name: + type: optional + examples: + - value: + created_at: 2023-03-11T21:19:54.812Z + modified_at: 2022-07-10T20:17:43.327Z + id: 68293 + name: lxakyatbhgluytsaeyvg + GatewayResponse: + properties: + d: list + Gateway__connects__device: + properties: + created_at: + type: optional + modified_at: + type: optional + id: + type: long + docs: The unique identifier for a Gateway__connects__device + gateway: optional + connects__device: optional + examples: + - value: + created_at: 2023-03-12T20:55:03.789Z + modified_at: 2023-01-23T06:09:12.207Z + id: 49798 + Gateway__connects__deviceResponse: + properties: + d: list diff --git a/fern/api/generators.yml b/fern/api/generators.yml new file mode 100644 index 000000000..6e32c6ad3 --- /dev/null +++ b/fern/api/generators.yml @@ -0,0 +1,43 @@ +groups: + # # we run the FastAPI generator for server-side development + # server: + # generators: + # - name: fernapi/fern-fastapi-server + # version: 0.0.33 + # output: + # location: local-file-system + # path: ../../app/fern/server + # # on every commit into the main branch, we generate SDKs for internal use + # internal: + # generators: + # - name: fernapi/fern-typescript-sdk + # version: 0.0.249 + # output: + # location: npm + # package-name: "@fern-api/plantstore" + # token: ${NPM_TOKEN} + # - name: fernapi/fern-java-sdk + # version: 0.0.125 + # output: + # location: maven + # coordinate: io.github.fern-api:plantstore + # username: ${MAVEN_USERNAME} + # password: ${MAVEN_PASSWORD} + # when we release, we publish our external-facing SDKs + external: + generators: + - name: fernapi/fern-openapi + version: 0.0.26 + output: + location: local-file-system + path: ./open-api + # - name: fernapi/fern-typescript-sdk + # version: 0.5.6 + # output: + # location: local-file-system + # path: ./ts-sdk + # - name: fernapi/fern-python-sdk + # version: 0.1.2 + # output: + # location: local-file-system + # path: ./python-sdk \ No newline at end of file diff --git a/fern/api/open-api/openapi.yml b/fern/api/open-api/openapi.yml new file mode 100644 index 000000000..f1746a6a4 --- /dev/null +++ b/fern/api/open-api/openapi.yml @@ -0,0 +1,862 @@ +openapi: 3.0.1 +info: + title: example + version: '' +paths: + /example/application: + get: + operationId: example_ReadAllApplication + tags: + - Example + parameters: + - name: $filter + in: query + required: false + schema: + type: string + - name: $select + in: query + required: false + schema: + type: string + examples: + Example1: + value: created_at,modified_at,id,name,note + - name: $expand + in: query + required: false + schema: + type: string + - name: $top + in: query + required: false + schema: + type: integer + - name: $count + in: query + required: false + schema: + type: integer + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ApplicationResponse' + examples: + Example1: + value: + d: + - created_at: '2022-06-19T23:22:46.962Z' + modified_at: '2022-08-12T08:38:22.014Z' + id: 71065 + name: xrkeejdkpljamasgfarw + note: eomyvjziulccgbqkfcly + - created_at: '2022-06-19T23:22:46.962Z' + modified_at: '2022-08-12T08:38:22.014Z' + id: 71065 + name: xrkeejdkpljamasgfarw + note: eomyvjziulccgbqkfcly + patch: + operationId: example_UpdateAllApplication + tags: + - Example + parameters: + - name: $filter + in: query + required: false + schema: + type: string + - name: $select + in: query + required: false + schema: + type: string + examples: + Example1: + value: created_at,modified_at,id,name,note + - name: $expand + in: query + required: false + schema: + type: string + - name: $top + in: query + required: false + schema: + type: integer + - name: $count + in: query + required: false + schema: + type: integer + responses: + '204': + description: '' + delete: + operationId: example_DeleteAllApplication + tags: + - Example + parameters: + - name: $filter + in: query + required: false + schema: + type: string + - name: $select + in: query + required: false + schema: + type: string + examples: + Example1: + value: created_at,modified_at,id,name,note + - name: $expand + in: query + required: false + schema: + type: string + - name: $top + in: query + required: false + schema: + type: integer + - name: $count + in: query + required: false + schema: + type: integer + responses: + '204': + description: '' + /example/application(id): + get: + operationId: example_ReadApplication + tags: + - Example + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Application' + examples: + Example1: + value: + created_at: '2022-06-19T23:22:46.962Z' + modified_at: '2022-08-12T08:38:22.014Z' + id: 71065 + name: xrkeejdkpljamasgfarw + note: eomyvjziulccgbqkfcly + post: + operationId: example_CreateApplication + tags: + - Example + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Application' + examples: + Example1: + value: + created_at: '2022-06-19T23:22:46.962Z' + modified_at: '2022-08-12T08:38:22.014Z' + id: 71065 + name: xrkeejdkpljamasgfarw + note: eomyvjziulccgbqkfcly + patch: + operationId: example_UpdateApplication + tags: + - Example + parameters: [] + responses: + '204': + description: '' + delete: + operationId: example_DeleteApplication + tags: + - Example + parameters: [] + responses: + '204': + description: '' + /example/device: + get: + operationId: example_ReadAllDevice + tags: + - Example + parameters: + - name: $filter + in: query + required: false + schema: + type: string + - name: $select + in: query + required: false + schema: + type: string + examples: + Example1: + value: created_at,modified_at,id,name,type,belongs_to__application + - name: $expand + in: query + required: false + schema: + type: string + - name: $top + in: query + required: false + schema: + type: integer + - name: $count + in: query + required: false + schema: + type: integer + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/DeviceResponse' + examples: + Example1: + value: + d: + - created_at: '2022-06-25T03:06:57.310Z' + modified_at: '2022-12-24T06:08:24.699Z' + id: 80097 + name: ewgdekqxythufswoytuc + type: mjlemqmluysjbhenlfuk + - created_at: '2022-06-25T03:06:57.310Z' + modified_at: '2022-12-24T06:08:24.699Z' + id: 80097 + name: ewgdekqxythufswoytuc + type: mjlemqmluysjbhenlfuk + patch: + operationId: example_UpdateAllDevice + tags: + - Example + parameters: + - name: $filter + in: query + required: false + schema: + type: string + - name: $select + in: query + required: false + schema: + type: string + examples: + Example1: + value: created_at,modified_at,id,name,type,belongs_to__application + - name: $expand + in: query + required: false + schema: + type: string + - name: $top + in: query + required: false + schema: + type: integer + - name: $count + in: query + required: false + schema: + type: integer + responses: + '204': + description: '' + delete: + operationId: example_DeleteAllDevice + tags: + - Example + parameters: + - name: $filter + in: query + required: false + schema: + type: string + - name: $select + in: query + required: false + schema: + type: string + examples: + Example1: + value: created_at,modified_at,id,name,type,belongs_to__application + - name: $expand + in: query + required: false + schema: + type: string + - name: $top + in: query + required: false + schema: + type: integer + - name: $count + in: query + required: false + schema: + type: integer + responses: + '204': + description: '' + /example/device(id): + get: + operationId: example_ReadDevice + tags: + - Example + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Device' + examples: + Example1: + value: + created_at: '2022-06-25T03:06:57.310Z' + modified_at: '2022-12-24T06:08:24.699Z' + id: 80097 + name: ewgdekqxythufswoytuc + type: mjlemqmluysjbhenlfuk + post: + operationId: example_CreateDevice + tags: + - Example + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Device' + examples: + Example1: + value: + created_at: '2022-06-25T03:06:57.310Z' + modified_at: '2022-12-24T06:08:24.699Z' + id: 80097 + name: ewgdekqxythufswoytuc + type: mjlemqmluysjbhenlfuk + patch: + operationId: example_UpdateDevice + tags: + - Example + parameters: [] + responses: + '204': + description: '' + delete: + operationId: example_DeleteDevice + tags: + - Example + parameters: [] + responses: + '204': + description: '' + /example/gateway: + get: + operationId: example_ReadAllGateway + tags: + - Example + parameters: + - name: $filter + in: query + required: false + schema: + type: string + - name: $select + in: query + required: false + schema: + type: string + examples: + Example1: + value: created_at,modified_at,id,name + - name: $expand + in: query + required: false + schema: + type: string + - name: $top + in: query + required: false + schema: + type: integer + - name: $count + in: query + required: false + schema: + type: integer + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/GatewayResponse' + examples: + Example1: + value: + d: + - created_at: '2023-03-11T21:19:54.812Z' + modified_at: '2022-07-10T20:17:43.327Z' + id: 68293 + name: lxakyatbhgluytsaeyvg + - created_at: '2023-03-11T21:19:54.812Z' + modified_at: '2022-07-10T20:17:43.327Z' + id: 68293 + name: lxakyatbhgluytsaeyvg + patch: + operationId: example_UpdateAllGateway + tags: + - Example + parameters: + - name: $filter + in: query + required: false + schema: + type: string + - name: $select + in: query + required: false + schema: + type: string + examples: + Example1: + value: created_at,modified_at,id,name + - name: $expand + in: query + required: false + schema: + type: string + - name: $top + in: query + required: false + schema: + type: integer + - name: $count + in: query + required: false + schema: + type: integer + responses: + '204': + description: '' + delete: + operationId: example_DeleteAllGateway + tags: + - Example + parameters: + - name: $filter + in: query + required: false + schema: + type: string + - name: $select + in: query + required: false + schema: + type: string + examples: + Example1: + value: created_at,modified_at,id,name + - name: $expand + in: query + required: false + schema: + type: string + - name: $top + in: query + required: false + schema: + type: integer + - name: $count + in: query + required: false + schema: + type: integer + responses: + '204': + description: '' + /example/gateway(id): + get: + operationId: example_ReadGateway + tags: + - Example + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Gateway' + examples: + Example1: + value: + created_at: '2023-03-11T21:19:54.812Z' + modified_at: '2022-07-10T20:17:43.327Z' + id: 68293 + name: lxakyatbhgluytsaeyvg + post: + operationId: example_CreateGateway + tags: + - Example + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Gateway' + examples: + Example1: + value: + created_at: '2023-03-11T21:19:54.812Z' + modified_at: '2022-07-10T20:17:43.327Z' + id: 68293 + name: lxakyatbhgluytsaeyvg + patch: + operationId: example_UpdateGateway + tags: + - Example + parameters: [] + responses: + '204': + description: '' + delete: + operationId: example_DeleteGateway + tags: + - Example + parameters: [] + responses: + '204': + description: '' + /example/gateway__connects__device: + get: + operationId: example_ReadAllGateway__connects__device + tags: + - Example + parameters: + - name: $filter + in: query + required: false + schema: + type: string + - name: $select + in: query + required: false + schema: + type: string + examples: + Example1: + value: created_at,modified_at,id,gateway,connects__device + - name: $expand + in: query + required: false + schema: + type: string + - name: $top + in: query + required: false + schema: + type: integer + - name: $count + in: query + required: false + schema: + type: integer + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Gateway__connects__deviceResponse' + examples: + Example1: + value: + d: + - created_at: '2023-03-12T20:55:03.789Z' + modified_at: '2023-01-23T06:09:12.207Z' + id: 49798 + - created_at: '2023-03-12T20:55:03.789Z' + modified_at: '2023-01-23T06:09:12.207Z' + id: 49798 + patch: + operationId: example_UpdateAllGateway__connects__device + tags: + - Example + parameters: + - name: $filter + in: query + required: false + schema: + type: string + - name: $select + in: query + required: false + schema: + type: string + examples: + Example1: + value: created_at,modified_at,id,gateway,connects__device + - name: $expand + in: query + required: false + schema: + type: string + - name: $top + in: query + required: false + schema: + type: integer + - name: $count + in: query + required: false + schema: + type: integer + responses: + '204': + description: '' + delete: + operationId: example_DeleteAllGateway__connects__device + tags: + - Example + parameters: + - name: $filter + in: query + required: false + schema: + type: string + - name: $select + in: query + required: false + schema: + type: string + examples: + Example1: + value: created_at,modified_at,id,gateway,connects__device + - name: $expand + in: query + required: false + schema: + type: string + - name: $top + in: query + required: false + schema: + type: integer + - name: $count + in: query + required: false + schema: + type: integer + responses: + '204': + description: '' + /example/gateway__connects__device(id): + get: + operationId: example_ReadGateway__connects__device + tags: + - Example + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Gateway__connects__device' + examples: + Example1: + value: + created_at: '2023-03-12T20:55:03.789Z' + modified_at: '2023-01-23T06:09:12.207Z' + id: 49798 + post: + operationId: example_CreateGateway__connects__device + tags: + - Example + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Gateway__connects__device' + examples: + Example1: + value: + created_at: '2023-03-12T20:55:03.789Z' + modified_at: '2023-01-23T06:09:12.207Z' + id: 49798 + patch: + operationId: example_UpdateGateway__connects__device + tags: + - Example + parameters: [] + responses: + '204': + description: '' + delete: + operationId: example_DeleteGateway__connects__device + tags: + - Example + parameters: [] + responses: + '204': + description: '' +components: + schemas: + Application: + title: Application + type: object + properties: + created_at: + type: string + format: date-time + modified_at: + type: string + format: date-time + id: + type: integer + format: int64 + description: The unique identifier for a Application + example: 71065 + name: + type: string + example: xrkeejdkpljamasgfarw + note: + type: string + example: eomyvjziulccgbqkfcly + required: + - id + - name + - note + ApplicationResponse: + title: ApplicationResponse + type: object + properties: + d: + type: array + items: + $ref: '#/components/schemas/Application' + required: + - d + Device: + title: Device + type: object + properties: + created_at: + type: string + format: date-time + modified_at: + type: string + format: date-time + id: + type: integer + format: int64 + description: The unique identifier for a Device + example: 80097 + name: + type: string + example: ewgdekqxythufswoytuc + type: + type: string + belongs_to__application: + $ref: '#/components/schemas/Application' + required: + - id + - name + DeviceResponse: + title: DeviceResponse + type: object + properties: + d: + type: array + items: + $ref: '#/components/schemas/Device' + required: + - d + Gateway: + title: Gateway + type: object + properties: + created_at: + type: string + format: date-time + modified_at: + type: string + format: date-time + id: + type: integer + format: int64 + description: The unique identifier for a Gateway + example: 68293 + name: + type: string + required: + - id + GatewayResponse: + title: GatewayResponse + type: object + properties: + d: + type: array + items: + $ref: '#/components/schemas/Gateway' + required: + - d + Gateway__connects__device: + title: Gateway__connects__device + type: object + properties: + created_at: + type: string + format: date-time + modified_at: + type: string + format: date-time + id: + type: integer + format: int64 + description: The unique identifier for a Gateway__connects__device + example: 49798 + gateway: + $ref: '#/components/schemas/Gateway' + connects__device: + $ref: '#/components/schemas/Device' + required: + - id + Gateway__connects__deviceResponse: + title: Gateway__connects__deviceResponse + type: object + properties: + d: + type: array + items: + $ref: '#/components/schemas/Gateway__connects__device' + required: + - d + securitySchemes: {} diff --git a/fern/fern.config.json b/fern/fern.config.json new file mode 100644 index 000000000..42b6a5fa8 --- /dev/null +++ b/fern/fern.config.json @@ -0,0 +1,4 @@ +{ + "organization": "fern", + "version": "0.6.6" + } \ No newline at end of file diff --git a/package.json b/package.json index 197d6488a..dc50238d0 100644 --- a/package.json +++ b/package.json @@ -1,137 +1,143 @@ { - "name": "@balena/pinejs", - "version": "14.62.5", - "main": "out/server-glue/module", - "repository": "git@github.com:balena-io/pinejs.git", - "license": "Apache-2.0", - "bin": { - "abstract-sql-compiler": "./bin/abstract-sql-compiler.js", - "odata-compiler": "./bin/odata-compiler.js", - "sbvr-compiler": "./bin/sbvr-compiler.js" - }, - "scripts": { - "prepublish": "require-npm4-to-publish", - "prepare": "node -e \"try { require('husky').install() } catch (e) {if (e.code !== 'MODULE_NOT_FOUND') throw e}\" && npm run build", - "build": "grunt build", - "webpack-browser": "grunt browser", - "webpack-module": "grunt module", - "webpack-server": "grunt server", - "webpack-build": "npm run webpack-browser && npm run webpack-module && npm run webpack-server", - "lint": "balena-lint -e js -e ts src build typings Gruntfile.ts && npx tsc --project tsconfig.dev.json --noEmit", - "test": "npm run lint && npm run build && npm run webpack-build && npm run test:compose", - "test:compose": "trap 'docker-compose -f docker-compose.npm-test.yml down ; echo Stopped ; exit 0' SIGINT; docker-compose -f docker-compose.npm-test.yml up -d && sleep 2 && DATABASE_URL=postgres://docker:docker@localhost:5431/postgres npm run mocha", - "mocha": "TS_NODE_FILES=true mocha", - "prettify": "balena-lint -e js -e ts --fix src build typings Gruntfile.ts" - }, - "dependencies": { - "@balena/abstract-sql-compiler": "^8.0.0", - "@balena/abstract-sql-to-typescript": "^1.4.2", - "@balena/env-parsing": "^1.1.5", - "@balena/lf-to-abstract-sql": "^5.0.0", - "@balena/odata-parser": "^2.4.6", - "@balena/odata-to-abstract-sql": "^5.9.2", - "@balena/sbvr-parser": "^1.4.3", - "@balena/sbvr-types": "^3.4.18", - "@types/body-parser": "^1.19.2", - "@types/compression": "^1.7.2", - "@types/cookie-parser": "^1.4.3", - "@types/deep-freeze": "^0.1.2", - "@types/express": "^4.17.17", - "@types/express-session": "^1.17.6", - "@types/lodash": "^4.14.191", - "@types/memoizee": "^0.4.8", - "@types/method-override": "^0.0.32", - "@types/multer": "^1.4.7", - "@types/mysql": "^2.15.21", - "@types/node": "^18.14.1", - "@types/passport": "^1.0.12", - "@types/passport-local": "^1.0.35", - "@types/passport-strategy": "^0.2.35", - "@types/pg": "^8.6.6", - "@types/randomstring": "^1.1.8", - "@types/websql": "^0.0.27", - "commander": "^10.0.0", - "deep-freeze": "^0.0.1", - "eventemitter3": "^5.0.0", - "express-session": "^1.17.3", - "lodash": "^4.17.21", - "memoizee": "^0.4.15", - "pinejs-client-core": "^6.12.3", - "randomstring": "^1.2.3", - "typed-error": "^3.2.1" - }, - "devDependencies": { - "@balena/lint": "^6.2.1", - "@types/chai": "^4.3.4", - "@types/chai-as-promised": "^7.1.5", - "@types/grunt": "^0.4.27", - "@types/mocha": "^10.0.1", - "@types/supertest": "^2.0.12", - "@types/terser-webpack-plugin": "^5.2.0", - "@types/webpack": "^5.28.0", - "chai": "^4.3.7", - "grunt": "1.6.1", - "grunt-check-dependencies": "^1.0.0", - "grunt-cli": "^1.4.3", - "grunt-contrib-clean": "^2.0.1", - "grunt-contrib-concat": "^2.1.0", - "grunt-contrib-copy": "^1.0.0", - "grunt-contrib-rename": "^0.2.0", - "grunt-gitinfo": "^0.1.9", - "grunt-text-replace": "^0.4.0", - "grunt-ts": "^6.0.0-beta.22", - "grunt-webpack": "^5.0.0", - "husky": "^8.0.3", - "lint-staged": "^13.1.2", - "load-grunt-tasks": "^5.1.0", - "mocha": "^10.2.0", - "raw-loader": "^4.0.2", - "require-npm4-to-publish": "^1.0.0", - "supertest": "^6.3.3", - "terser-webpack-plugin": "^5.3.6", - "ts-loader": "^9.4.2", - "ts-node": "^10.9.1", - "typescript": "^4.9.5", - "webpack": "^5.75.0", - "webpack-dev-server": "^4.11.1" - }, - "optionalDependencies": { - "bcrypt": "^5.1.0", - "body-parser": "^1.20.2", - "compression": "^1.7.4", - "cookie-parser": "^1.4.6", - "express": "^4.18.2", - "method-override": "^3.0.0", - "multer": "1.4.5-lts.1", - "mysql": "^2.18.1", - "passport": "^0.6.0", - "passport-local": "^1.0.0", - "pg": "^8.9.0", - "pg-connection-string": "^2.5.0", - "serve-static": "^1.15.0" - }, - "engines": { - "node": ">=12.0.0", - "npm": ">=6.0.0" - }, - "lint-staged": { - "*.js": [ - "balena-lint --fix" - ], - "*.ts": [ - "balena-lint --fix" - ] - }, - "mocha": { - "extension": [ - ".test.ts" - ], - "require": "ts-node/register/transpile-only", - "exit": true, - "timeout": 60000, - "recursive": true - }, - "versionist": { - "publishedAt": "2023-03-23T11:14:59.649Z" - } + "name": "@balena/pinejs", + "version": "14.62.5", + "main": "out/server-glue/module", + "repository": "git@github.com:balena-io/pinejs.git", + "license": "Apache-2.0", + "bin": { + "abstract-sql-compiler": "./bin/abstract-sql-compiler.js", + "odata-compiler": "./bin/odata-compiler.js", + "sbvr-compiler": "./bin/sbvr-compiler.js" + }, + "scripts": { + "prepublish": "require-npm4-to-publish", + "prepare": "node -e \"try { require('husky').install() } catch (e) {if (e.code !== 'MODULE_NOT_FOUND') throw e}\" && npm run build", + "build": "grunt build", + "webpack-browser": "grunt browser", + "webpack-module": "grunt module", + "webpack-server": "grunt server", + "webpack-build": "npm run webpack-browser && npm run webpack-module && npm run webpack-server", + "lint": "balena-lint -e js -e ts src build typings Gruntfile.ts && npx tsc --project tsconfig.dev.json --noEmit", + "test": "npm run lint && npm run build && npm run webpack-build && npm run test:compose", + "test:compose": "trap 'docker-compose -f docker-compose.npm-test.yml down ; echo Stopped ; exit 0' SIGINT; docker-compose -f docker-compose.npm-test.yml up -d && sleep 2 && DATABASE_URL=postgres://docker:docker@localhost:5431/postgres npm run mocha", + "mocha": "TS_NODE_FILES=true mocha", + "prettify": "balena-lint -e js -e ts --fix src build typings Gruntfile.ts", + "fern:generate": "fern generate", + "fern:add": "fern add" + }, + "dependencies": { + "@balena/abstract-sql-compiler": "^8.0.0", + "@balena/abstract-sql-to-typescript": "^1.4.2", + "@balena/env-parsing": "^1.1.5", + "@balena/lf-to-abstract-sql": "^5.0.0", + "@balena/odata-parser": "^2.4.6", + "@balena/odata-to-abstract-sql": "^5.9.2", + "@balena/sbvr-parser": "^1.4.3", + "@balena/sbvr-types": "^3.4.18", + "@faker-js/faker": "^7.6.0", + "@types/body-parser": "^1.19.2", + "@types/compression": "^1.7.2", + "@types/cookie-parser": "^1.4.3", + "@types/deep-freeze": "^0.1.2", + "@types/express": "^4.17.17", + "@types/express-session": "^1.17.6", + "@types/lodash": "^4.14.191", + "@types/memoizee": "^0.4.8", + "@types/method-override": "^0.0.32", + "@types/multer": "^1.4.7", + "@types/mysql": "^2.15.21", + "@types/node": "^18.14.1", + "@types/passport": "^1.0.12", + "@types/passport-local": "^1.0.35", + "@types/passport-strategy": "^0.2.35", + "@types/pg": "^8.6.6", + "@types/randomstring": "^1.1.8", + "@types/websql": "^0.0.27", + "commander": "^10.0.0", + "deep-freeze": "^0.0.1", + "eventemitter3": "^5.0.0", + "express-session": "^1.17.3", + "fern-api": "^0.6.6", + "lodash": "^4.17.21", + "memoizee": "^0.4.15", + "odata-openapi": "^0.21.5", + "pinejs-client-core": "^6.12.3", + "randomstring": "^1.2.3", + "typed-error": "^3.2.1", + "yaml": "^2.2.1" + }, + "devDependencies": { + "@balena/lint": "^6.2.1", + "@types/chai": "^4.3.4", + "@types/chai-as-promised": "^7.1.5", + "@types/grunt": "^0.4.27", + "@types/mocha": "^10.0.1", + "@types/supertest": "^2.0.12", + "@types/terser-webpack-plugin": "^5.2.0", + "@types/webpack": "^5.28.0", + "chai": "^4.3.7", + "grunt": "1.6.1", + "grunt-check-dependencies": "^1.0.0", + "grunt-cli": "^1.4.3", + "grunt-contrib-clean": "^2.0.1", + "grunt-contrib-concat": "^2.1.0", + "grunt-contrib-copy": "^1.0.0", + "grunt-contrib-rename": "^0.2.0", + "grunt-gitinfo": "^0.1.9", + "grunt-text-replace": "^0.4.0", + "grunt-ts": "^6.0.0-beta.22", + "grunt-webpack": "^5.0.0", + "husky": "^8.0.3", + "lint-staged": "^13.1.2", + "load-grunt-tasks": "^5.1.0", + "mocha": "^10.2.0", + "raw-loader": "^4.0.2", + "require-npm4-to-publish": "^1.0.0", + "supertest": "^6.3.3", + "terser-webpack-plugin": "^5.3.6", + "ts-loader": "^9.4.2", + "ts-node": "^10.9.1", + "typescript": "^4.9.5", + "webpack": "^5.75.0", + "webpack-dev-server": "^4.11.1" + }, + "optionalDependencies": { + "bcrypt": "^5.1.0", + "body-parser": "^1.20.2", + "compression": "^1.7.4", + "cookie-parser": "^1.4.6", + "express": "^4.18.2", + "method-override": "^3.0.0", + "multer": "1.4.5-lts.1", + "mysql": "^2.18.1", + "passport": "^0.6.0", + "passport-local": "^1.0.0", + "pg": "^8.9.0", + "pg-connection-string": "^2.5.0", + "serve-static": "^1.15.0" + }, + "engines": { + "node": ">=12.0.0", + "npm": ">=6.0.0" + }, + "lint-staged": { + "*.js": [ + "balena-lint --fix" + ], + "*.ts": [ + "balena-lint --fix" + ] + }, + "mocha": { + "extension": [ + ".test.ts" + ], + "require": "ts-node/register/transpile-only", + "exit": true, + "timeout": 60000, + "recursive": true + }, + "versionist": { + "publishedAt": "2023-03-23T11:14:59.649Z" + } } diff --git a/src/odata-metadata/fern-metadatagenerator.ts b/src/odata-metadata/fern-metadatagenerator.ts new file mode 100644 index 000000000..4e6334237 --- /dev/null +++ b/src/odata-metadata/fern-metadatagenerator.ts @@ -0,0 +1,346 @@ +import type { + AbstractSqlModel, + AbstractSqlTable, +} from '@balena/abstract-sql-compiler'; + +import * as sbvrTypes from '@balena/sbvr-types'; +import { PermissionLookup } from '../sbvr-api/permissions'; +import { faker } from '@faker-js/faker'; + +// tslint:disable-next-line:no-var-requires +const { version }: { version: string } = require('../../package.json'); + +type PreparedPermissionsLookup = { + [vocabulary: string]: { + [resource: string]: { + read: boolean; + create: boolean; + update: boolean; + delete: boolean; + }; + }; +}; + +type PreparedAbstractModel = { + vocabulary: string; + abstractSqlModel: AbstractSqlModel; + preparedPermissionLookup: PreparedPermissionsLookup; +}; + +const getResourceName = (resourceName: string): string => + resourceName + .split('-') + .map((namePart) => namePart.split(' ').join('_')) + .join('__'); + +const forEachUniqueTable = ( + model: PreparedAbstractModel, + callback: ( + tableName: string, + table: AbstractSqlTable & { referenceScheme: string }, + ) => T, +): T[] => { + const usedTableNames: { [tableName: string]: true } = {}; + + const result = []; + + for (const key of Object.keys(model.abstractSqlModel.tables).sort()) { + const table = model.abstractSqlModel.tables[key] as AbstractSqlTable & { + referenceScheme: string; + }; + if ( + typeof table !== 'string' && + !table.primitive && + !usedTableNames[table.name] && + model.preparedPermissionLookup + ) { + usedTableNames[table.name] = true; + result.push(callback(key, table)); + } + } + return result; +}; + +/** + * parsing dictionary of vocabulary.resource.operation permissions string + * into dictionary of resource to operation for later lookup + */ + +const preparePermissionsLookup = ( + permissionLookup: PermissionLookup, +): PreparedPermissionsLookup => { + const resourcesAndOps: PreparedPermissionsLookup = {}; + + for (const resourceOpsAuths of Object.keys(permissionLookup)) { + const [vocabulary, resource, rule] = resourceOpsAuths.split('.'); + resourcesAndOps[vocabulary] ??= {}; + resourcesAndOps[vocabulary][resource] ??= { + ['read']: false, + ['create']: false, + ['update']: false, + ['delete']: false, + }; + + if (rule === 'all' || (resource === 'all' && rule === undefined)) { + resourcesAndOps[vocabulary][resource] = { + ['read']: true, + ['create']: true, + ['update']: true, + ['delete']: true, + }; + } else if ( + rule === 'read' || + rule === 'create' || + rule === 'update' || + rule === 'delete' + ) { + resourcesAndOps[vocabulary][resource][rule] = true; + } + } + return resourcesAndOps; +}; + +const capitalize = (str: string) => { + return str.charAt(0).toUpperCase() + str.slice(1); +}; + +export const generateFernMetadata = ( + vocabulary: string, + abstractSqlModel: AbstractSqlModel, + permissionsLookup?: PermissionLookup, +) => { + const complexTypes: { [fieldType: string]: string } = {}; + const resolveDataType = (fieldType: string): string => { + if (sbvrTypes[fieldType] == null) { + console.error('Could not resolve type', fieldType); + throw new Error('Could not resolve type' + fieldType); + } + const { complexType } = sbvrTypes[fieldType].types.odata; + if (complexType != null) { + complexTypes[fieldType] = complexType; + } + return sbvrTypes[fieldType].types.odata.name; + }; + + const prepPermissionsLookup = permissionsLookup + ? preparePermissionsLookup(permissionsLookup) + : {}; + + const model: PreparedAbstractModel = { + vocabulary, + abstractSqlModel, + preparedPermissionLookup: prepPermissionsLookup, + }; + + const ODataQueryParameters = { + $filter: 'optional', + $select: 'optional', + $expand: 'optional', + $top: 'optional', + $count: 'optional', + }; + + // type FernEndpoint = { + // path: string; + // 'path-parameters': { [key: string]: string }; + // method: string; + // request: { + // name: string; + // 'query-parameters': typeof ODataQueryParameters; + // auth?: boolean; + // docs?: string; + // }; + // }; + + const fernRootEndpoints: any = {}; + + const fernRootTypes: any = {}; + // let fernRootErrors: any = {}; + + const exampleFaker = ( + fieldName: string, + dataType?: any, + // referencedResource?: string, + ) => { + if (fieldName === 'id' || dataType === 'long' || dataType === 'integer') { + return faker.datatype.number(100000); + } else if (dataType === 'datetime') { + return faker.date.past(); + // return new Date().toISOString(); + } else if (dataType === 'string') { + return faker.random.alpha(20); + } + }; + + forEachUniqueTable(model, (_key, { name: resourceName, fields }) => { + resourceName = getResourceName(resourceName); + // no path nor entity when permissions not contain resource + const permissions: PreparedPermissionsLookup[string][string] = + model?.preparedPermissionLookup?.['resource']?.['all'] ?? + model?.preparedPermissionLookup?.[model.vocabulary]?.['all'] ?? + model?.preparedPermissionLookup?.[model.vocabulary]?.[resourceName]; + + if (!permissions) { + return; + } + + const uniqueTable: any = { + properties: {}, + }; + + const selectableFields: any = []; + const exampleForType: any = {}; + const exampleForEndpoint: any = {}; + + fields + .filter(({ dataType }) => dataType !== 'ForeignKey') + .map(({ dataType, fieldName, required }) => { + dataType = resolveDataType(dataType); + fieldName = getResourceName(fieldName); + + selectableFields.push(fieldName); + + const lookup: any = { int64: 'long' }; + + const dtName = dataType.replace('Edm.', '').toLowerCase(); + const dt = lookup[dtName] ? lookup[dtName] : dtName; + + if (fieldName !== 'id') { + uniqueTable.properties[fieldName] = { + type: required ? `optional<${dt}>` : dt, + }; + } else { + uniqueTable.properties[fieldName] = { + type: `long`, + docs: `The unique identifier for a ${capitalize(resourceName)}`, + }; + } + exampleForType[fieldName] = exampleForEndpoint[fieldName] = + exampleFaker(fieldName, dt); + }); + + fields + .filter( + ({ dataType, references }) => + dataType === 'ForeignKey' && references != null, + ) + .map(({ fieldName, references, required }) => { + const { resourceName: referencedResource } = references!; + const referencedResourceName = + model.abstractSqlModel.tables[referencedResource]?.name; + const typeReference = referencedResourceName || referencedResource; + + fieldName = getResourceName(fieldName); + + selectableFields.push(fieldName); + + const referenceResourceName = capitalize( + getResourceName(typeReference), + ); + + uniqueTable.properties[fieldName] = required + ? `optional<${referenceResourceName}>` + : referenceResourceName; + + // exampleForType[fieldName] = exampleFaker(fieldName, 'id'); + }); + + const capitalizedResourceName = capitalize(resourceName); + + uniqueTable.examples ??= [{ value: exampleForType }]; + + fernRootTypes[capitalizedResourceName] = uniqueTable; + fernRootTypes[capitalizedResourceName + 'Response'] = { + properties: { d: `list<${capitalizedResourceName}>` }, + }; + + for (const [resKey, resValue] of Object.entries(permissions) as Array< + [keyof PreparedPermissionsLookup[string][string], boolean] + >) { + const httpLookup: any = { + read: 'GET', + create: 'POST', + update: 'PATCH', + delete: 'DELETE', + }; + + const compileResponse: any = { + read: capitalizedResourceName + 'Response', + create: capitalizedResourceName + 'Response', + update: undefined, + delete: undefined, + }; + + const multiEndpoint: any = { + read: true, + create: false, + update: true, + delete: true, + }; + if (resValue) { + if (multiEndpoint[resKey]) { + fernRootEndpoints[ + capitalize(resKey) + 'All' + capitalizedResourceName + ] = { + path: `/${resourceName}`, + method: httpLookup[resKey], + response: compileResponse[resKey], + request: { + name: capitalize(resKey) + 'all' + capitalizedResourceName, + 'query-parameters': ODataQueryParameters, + }, + examples: [ + { + 'query-parameters': { + $select: selectableFields.join(','), + }, + response: compileResponse[resKey] + ? { + body: { + d: [exampleForEndpoint, exampleForEndpoint], + }, + } + : undefined, + }, + ], + }; + } + + fernRootEndpoints[capitalize(resKey) + capitalizedResourceName] = { + path: `/${resourceName}(id)`, + // 'path-parameters': { + // [`${resourceName}Id`]: 'long', + // }, + method: httpLookup[resKey], + response: compileResponse[resKey], + request: { + name: capitalize(resKey) + capitalizedResourceName + 'ById', + }, + examples: [ + { + response: compileResponse[resKey] + ? { + body: exampleForEndpoint, + } + : undefined, + }, + ], + }; + } + } + }); + + const fernRootApi = { + service: { + auth: false, + 'base-path': `/${vocabulary}`, + endpoints: fernRootEndpoints, + }, + types: fernRootTypes, + // errors: fernRootErrors, + }; + + return fernRootApi; +}; + +generateFernMetadata.version = version; diff --git a/src/odata-metadata/odata-metadata-generator.ts b/src/odata-metadata/odata-metadata-generator.ts index f36adfa2d..6b3d71a10 100644 --- a/src/odata-metadata/odata-metadata-generator.ts +++ b/src/odata-metadata/odata-metadata-generator.ts @@ -4,10 +4,199 @@ import type { } from '@balena/abstract-sql-compiler'; import * as sbvrTypes from '@balena/sbvr-types'; +import { PermissionLookup } from '../sbvr-api/permissions'; // tslint:disable-next-line:no-var-requires const { version }: { version: string } = require('../../package.json'); +// OData JSON v4 CSDL Vocabulary constants +// http://docs.oasis-open.org/odata/odata-vocabularies/v4.0/odata-vocabularies-v4.0.html +const odataVocabularyReferences: ODataCsdlV4References = { + 'https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Core.V1.json': + { + $Include: [ + { + $Namespace: 'Org.OData.Core.V1', + $Alias: 'Core', + '@Core.DefaultNamespace': true, + }, + ], + }, + 'https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Measures.V1.json': + { + $Include: [ + { + $Namespace: 'Org.OData.Measures.V1', + $Alias: 'Measures', + }, + ], + }, + 'https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Aggregation.V1.json': + { + $Include: [ + { + $Namespace: 'Org.OData.Aggregation.V1', + $Alias: 'Aggregation', + }, + ], + }, + 'https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Capabilities.V1.json': + { + $Include: [ + { + $Namespace: 'Org.OData.Capabilities.V1', + $Alias: 'Capabilities', + }, + ], + }, +}; + +/** + * Odata Common Schema Definition Language JSON format + * http://docs.oasis-open.org/odata/odata-json-format/v4.0/odata-json-format-v4.0.html + */ + +type ODataCsdlV4References = { + [URI: string]: { + $Include: Array<{ + $Namespace: string; + $Alias: string; + [annotation: string]: string | boolean; + }>; + }; +}; + +type ODataCsdlV4BaseProperty = { + [annotation: string]: string | boolean | undefined; + $Type?: string; + $Nullable?: boolean; +}; + +type ODataCsdlV4StructuralProperty = ODataCsdlV4BaseProperty & { + $Kind?: 'Property'; // This member SHOULD be omitted to reduce document size. +}; + +type ODataCsdlV4NavigationProperty = ODataCsdlV4BaseProperty & { + $Kind: 'NavigationProperty'; + $Partner?: string; +}; + +type ODataCsdlV4Property = + | ODataCsdlV4BaseProperty + | ODataCsdlV4StructuralProperty + | ODataCsdlV4NavigationProperty; + +type ODataCsdlV4EntityType = { + $Kind: 'EntityType'; + $Key: string[]; + [property: string]: + | true + | string[] + | string + | 'EntityType' + | ODataCsdlV4Property; +}; + +type ODataCsdlV4EntityContainerEntries = { + // $Collection: true; + $Type: string; + [property: string]: true | string | ODataCapabilitiesUDIRRestrictionsMethod; +}; + +type ODataCsdlV4Entities = { + [resource: string]: ODataCsdlV4EntityType; +}; + +type ODataCsdlV4EntityContainer = { + $Kind: 'EntityContainer'; + '@Capabilities.BatchSupported'?: boolean; + [resourceOrAnnotation: string]: + | 'EntityContainer' + | boolean + | string + | ODataCsdlV4EntityContainerEntries + | undefined; +}; + +type ODataCsdlV4Schema = { + $Alias: string; + '@Core.DefaultNamespace': true; + [resource: string]: + | string + | boolean + | ODataCsdlV4EntityContainer + | ODataCsdlV4EntityType; +}; + +type OdataCsdlV4 = { + $Version: string; + $Reference: ODataCsdlV4References; + $EntityContainer: string; + [schema: string]: string | ODataCsdlV4References | ODataCsdlV4Schema; +}; + +type PreparedPermissionsLookup = { + [vocabulary: string]: { + [resource: string]: { + read: boolean; + create: boolean; + update: boolean; + delete: boolean; + }; + }; +}; + +type PreparedAbstractModel = { + vocabulary: string; + abstractSqlModel: AbstractSqlModel; + preparedPermissionLookup: PreparedPermissionsLookup; +}; + +type ODataCapabilitiesUDIRRestrictionsMethod = + | { Updatable: boolean } + | { Deletable: boolean } + | { Insertable: boolean } + | { Readable: boolean }; + +const restrictionsLookup = ( + method: keyof PreparedPermissionsLookup[string][string] | 'all', + value: boolean, +) => { + const lookup = { + update: { + '@Capabilities.UpdateRestrictions': { + Updatable: value, + }, + }, + delete: { + '@Capabilities.DeleteRestrictions': { + Deletable: value, + }, + }, + create: { + '@Capabilities.InsertRestrictions': { + Insertable: value, + }, + }, + read: { + '@Capabilities.ReadRestrictions': { + Readable: value, + }, + }, + }; + + if (method === 'all') { + return { + ...lookup['update'], + ...lookup['delete'], + ...lookup['create'], + ...lookup['read'], + }; + } else { + return lookup[method] ?? {}; + } +}; + const getResourceName = (resourceName: string): string => resourceName .split('-') @@ -15,17 +204,25 @@ const getResourceName = (resourceName: string): string => .join('__'); const forEachUniqueTable = ( - model: AbstractSqlModel['tables'], - callback: (tableName: string, table: AbstractSqlTable) => T, + model: PreparedAbstractModel, + callback: ( + tableName: string, + table: AbstractSqlTable & { referenceScheme: string }, + ) => T, ): T[] => { const usedTableNames: { [tableName: string]: true } = {}; const result = []; - for (const [key, table] of Object.entries(model)) { + + for (const key of Object.keys(model.abstractSqlModel.tables).sort()) { + const table = model.abstractSqlModel.tables[key] as AbstractSqlTable & { + referenceScheme: string; + }; if ( typeof table !== 'string' && !table.primitive && - !usedTableNames[table.name] + !usedTableNames[table.name] && + model.preparedPermissionLookup ) { usedTableNames[table.name] = true; result.push(callback(key, table)); @@ -34,9 +231,49 @@ const forEachUniqueTable = ( return result; }; +/** + * parsing dictionary of vocabulary.resource.operation permissions string + * into dictionary of resource to operation for later lookup + */ + +const preparePermissionsLookup = ( + permissionLookup: PermissionLookup, +): PreparedPermissionsLookup => { + const resourcesAndOps: PreparedPermissionsLookup = {}; + + for (const resourceOpsAuths of Object.keys(permissionLookup)) { + const [vocabulary, resource, rule] = resourceOpsAuths.split('.'); + resourcesAndOps[vocabulary] ??= {}; + resourcesAndOps[vocabulary][resource] ??= { + ['read']: false, + ['create']: false, + ['update']: false, + ['delete']: false, + }; + + if (rule === 'all' || (resource === 'all' && rule === undefined)) { + resourcesAndOps[vocabulary][resource] = { + ['read']: true, + ['create']: true, + ['update']: true, + ['delete']: true, + }; + } else if ( + rule === 'read' || + rule === 'create' || + rule === 'update' || + rule === 'delete' + ) { + resourcesAndOps[vocabulary][resource][rule] = true; + } + } + return resourcesAndOps; +}; + export const generateODataMetadata = ( vocabulary: string, abstractSqlModel: AbstractSqlModel, + permissionsLookup?: PermissionLookup, ) => { const complexTypes: { [fieldType: string]: string } = {}; const resolveDataType = (fieldType: string): string => { @@ -51,132 +288,114 @@ export const generateODataMetadata = ( return sbvrTypes[fieldType].types.odata.name; }; - const model = abstractSqlModel.tables; - const associations: Array<{ - name: string; - ends: Array<{ - resourceName: string; - cardinality: '1' | '0..1' | '*'; - }>; - }> = []; - forEachUniqueTable(model, (_key, { name: resourceName, fields }) => { - resourceName = getResourceName(resourceName); - for (const { dataType, required, references } of fields) { - if (dataType === 'ForeignKey' && references != null) { - const { resourceName: referencedResource } = references; - associations.push({ - name: resourceName + referencedResource, - ends: [ - { resourceName, cardinality: required ? '1' : '0..1' }, - { resourceName: referencedResource, cardinality: '*' }, - ], + const prepPermissionsLookup = permissionsLookup + ? preparePermissionsLookup(permissionsLookup) + : {}; + + const model: PreparedAbstractModel = { + vocabulary, + abstractSqlModel, + preparedPermissionLookup: prepPermissionsLookup, + }; + + const metaBalenaEntries: ODataCsdlV4Entities = {}; + const entityContainer: ODataCsdlV4EntityContainer = { + $Kind: 'EntityContainer', + '@Capabilities.KeyAsSegmentSupported': false, + }; + + forEachUniqueTable( + model, + (_key, { idField, name: resourceName, fields, referenceScheme }) => { + resourceName = getResourceName(resourceName); + // no path nor entity when permissions not contain resource + const permissions: PreparedPermissionsLookup[string][string] = + model?.preparedPermissionLookup?.['resource']?.['all'] ?? + model?.preparedPermissionLookup?.[model.vocabulary]?.['all'] ?? + model?.preparedPermissionLookup?.[model.vocabulary]?.[resourceName]; + + if (!permissions) { + return; + } + + const uniqueTable: ODataCsdlV4EntityType = { + $Kind: 'EntityType', + $Key: [idField], + '@Core.LongDescription': + '{"x-internal-ref-scheme": ["' + referenceScheme + '"]}', + }; + + fields + .filter(({ dataType }) => dataType !== 'ForeignKey') + .map(({ dataType, fieldName, required }) => { + dataType = resolveDataType(dataType); + fieldName = getResourceName(fieldName); + + uniqueTable[fieldName] = { + $Type: dataType, + $Nullable: !required, + '@Core.Computed': + fieldName === 'created_at' || fieldName === 'modified_at' + ? true + : false, + }; + }); + + fields + .filter( + ({ dataType, references }) => + dataType === 'ForeignKey' && references != null, + ) + .map(({ fieldName, references, required }) => { + const { resourceName: referencedResource } = references!; + const referencedResourceName = + model.abstractSqlModel.tables[referencedResource]?.name; + const typeReference = referencedResourceName || referencedResource; + + fieldName = getResourceName(fieldName); + uniqueTable[fieldName] = { + $Kind: 'NavigationProperty', + $Partner: resourceName, + $Nullable: !required, + $Type: vocabulary + '.' + getResourceName(typeReference), + }; }); + + metaBalenaEntries[resourceName] = uniqueTable; + + let entityCon: ODataCsdlV4EntityContainerEntries = { + $Collection: true, + $Type: vocabulary + '.' + resourceName, + }; + for (const [resKey, resValue] of Object.entries(permissions) as Array< + [keyof PreparedPermissionsLookup[string][string], boolean] + >) { + entityCon = { ...entityCon, ...restrictionsLookup(resKey, resValue) }; } - } - }); - - return ( - ` - - - - - - ` + - forEachUniqueTable( - model, - (_key, { idField, name: resourceName, fields }) => { - resourceName = getResourceName(resourceName); - return ( - ` - - - - - - ` + - fields - .filter(({ dataType }) => dataType !== 'ForeignKey') - .map(({ dataType, fieldName, required }) => { - dataType = resolveDataType(dataType); - fieldName = getResourceName(fieldName); - return ``; - }) - .join('\n') + - '\n' + - fields - .filter( - ({ dataType, references }) => - dataType === 'ForeignKey' && references != null, - ) - .map(({ fieldName, references }) => { - const { resourceName: referencedResource } = references!; - fieldName = getResourceName(fieldName); - return ``; - }) - .join('\n') + - '\n' + - ` - ` - ); - }, - ).join('\n\n') + - associations - .map(({ name, ends }) => { - name = getResourceName(name); - return ( - `` + - '\n\t' + - ends - .map( - ({ resourceName, cardinality }) => - ``, - ) - .join('\n\t') + - '\n' + - `` - ); - }) - .join('\n') + - ` - - - ` + - forEachUniqueTable(model, (_key, { name: resourceName }) => { - resourceName = getResourceName(resourceName); - return ``; - }).join('\n') + - '\n' + - associations - .map(({ name, ends }) => { - name = getResourceName(name); - return ( - `` + - '\n\t' + - ends - .map( - ({ resourceName }) => - ``, - ) - .join('\n\t') + - ` - ` - ); - }) - .join('\n') + - ` - ` + - Object.values(complexTypes).join('\n') + - ` - - - ` + + entityContainer[resourceName] = entityCon; + }, ); + + const odataCsdl: OdataCsdlV4 = { + // needs to be === '4.0' as > '4.0' in csdl2openapi will switch to drop the `$` query parameter prefix for eg $top, $skip as it became optional in OData V4.01 + $Version: '3.0', + $EntityContainer: vocabulary + '.ODataApi', + $Reference: odataVocabularyReferences, + [vocabulary]: { + // schema + $Alias: vocabulary, + '@Core.DefaultNamespace': true, + '@Core.Description': `OpenAPI specification for PineJS served SBVR datamodel: ${vocabulary}`, + '@Core.LongDescription': + 'Auto-Genrated OpenAPI specification by utilizing OData CSDL to OpenAPI spec transformer.', + '@Core.SchemaVersion': version, + ...metaBalenaEntries, + ['ODataApi']: entityContainer, + }, + }; + + return odataCsdl; }; generateODataMetadata.version = version; diff --git a/src/odata-metadata/open-api-sepcification-generator.ts b/src/odata-metadata/open-api-sepcification-generator.ts new file mode 100644 index 000000000..034a5e78b --- /dev/null +++ b/src/odata-metadata/open-api-sepcification-generator.ts @@ -0,0 +1,77 @@ +import * as odataMetadata from 'odata-openapi'; +import { generateODataMetadata } from './odata-metadata-generator'; +// tslint:disable-next-line:no-var-requires + +export const generateODataMetadataAsOpenApi = ( + odataCsdl: ReturnType, + versionBasePathUrl: string = '', + hostname: string = '', +) => { + // console.log(`odataCsdl:${JSON.stringify(odataCsdl, null, 2)}`); + const openAPIJson: any = odataMetadata.csdl2openapi(odataCsdl, { + scheme: 'https', + host: hostname, + basePath: versionBasePathUrl, + diagram: false, + maxLevels: 5, + }); + + /** + * Manual rewriting OpenAPI specification to delete OData default functionality + * that is not implemented in Pinejs yet or is based on PineJs implements OData V3. + * + * Rewrite odata body response schema properties from `value: ` to `d: ` + * Currently pinejs is returning `d: ` + * https://www.odata.org/documentation/odata-version-2-0/json-format/ (6. Representing Collections of Entries) + * https://www.odata.org/documentation/odata-version-3-0/json-verbose-format/ (6.1 Response body) + * + * New v4 odata specifies the body response with `value: ` + * http://docs.oasis-open.org/odata/odata-json-format/v4.01/odata-json-format-v4.01.html#sec_IndividualPropertyorOperationRespons + * + * + * Currently pinejs does not implement a $count=true query parameter as this would return the count of all rows returned as an additional parameter. + * This was not part of OData V3 and is new for OData V4. As the odata-openapi converte is opionionated on V4 the parameter is put into the schema. + * Until this is in parity with OData V4 pinejs needs to cleanup the `odata.count` key from the response schema put in by `csdl2openapi` + * + * + * Used oasis translator generates openapi according to v4 spec (`value: `) + */ + + Object.keys(openAPIJson.paths).forEach((i) => { + // rewrite `value: ` to `d: ` + if ( + openAPIJson?.paths[i]?.get?.responses?.['200']?.content?.[ + 'application/json' + ]?.schema?.properties?.value + ) { + openAPIJson.paths[i].get.responses['200'].content[ + 'application/json' + ].schema.properties['d'] = + openAPIJson.paths[i].get.responses['200'].content[ + 'application/json' + ].schema.properties.value; + delete openAPIJson.paths[i].get.responses['200'].content[ + 'application/json' + ].schema.properties.value; + } + + // cleanup the `odata.count` key from the response schema + if ( + openAPIJson?.paths[i]?.get?.responses?.['200']?.content?.[ + 'application/json' + ]?.schema?.properties?.['@odata.count'] + ) { + delete openAPIJson.paths[i].get.responses['200'].content[ + 'application/json' + ].schema.properties['@odata.count']; + } + }); + + // cleanup $batch path as pinejs does not implement it. + // http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_BatchRequests + if (openAPIJson?.paths['/$batch']) { + delete openAPIJson.paths['/$batch']; + } + + return openAPIJson; +}; diff --git a/src/sbvr-api/permissions.ts b/src/sbvr-api/permissions.ts index 03d73d740..daa5dbe6c 100644 --- a/src/sbvr-api/permissions.ts +++ b/src/sbvr-api/permissions.ts @@ -312,7 +312,7 @@ const namespaceRelationships = ( }); }; -type PermissionLookup = Dictionary; +export type PermissionLookup = Dictionary; const getPermissionsLookup = env.createCache( 'permissionsLookup', @@ -1631,7 +1631,7 @@ const getGuestPermissions = memoize( { promise: true }, ); -const getReqPermissions = async ( +export const getReqPermissions = async ( req: PermissionReq, odataBinds: ODataBinds = [] as any as ODataBinds, ) => { diff --git a/src/sbvr-api/sbvr-utils.ts b/src/sbvr-api/sbvr-utils.ts index 7793b1c7d..7e241f7e7 100644 --- a/src/sbvr-api/sbvr-utils.ts +++ b/src/sbvr-api/sbvr-utils.ts @@ -36,7 +36,8 @@ import { ExtendedSBVRParser } from '../extended-sbvr-parser/extended-sbvr-parser import * as asyncMigrator from '../migrator/async'; import * as syncMigrator from '../migrator/sync'; -import { generateODataMetadata } from '../odata-metadata/odata-metadata-generator'; +import { generateODataMetadataAsOpenApi } from '../odata-metadata/open-api-sepcification-generator'; +import { generateFernMetadata } from '../odata-metadata/fern-metadatagenerator'; // tslint:disable-next-line:no-var-requires const devModel = require('./dev.sbvr'); @@ -95,6 +96,7 @@ export { resolveOdataBind } from './abstract-sql'; import * as odataResponse from './odata-response'; import { env } from '../server-glue/module'; import { translateAbstractSqlModel } from './translations'; +import { generateODataMetadata } from '../odata-metadata/odata-metadata-generator'; const LF2AbstractSQLTranslator = LF2AbstractSQL.createTranslator(sbvrTypes); const LF2AbstractSQLTranslatorVersion = `${LF2AbstractSQLVersion}+${sbvrTypesVersion}`; @@ -1710,10 +1712,49 @@ const respondGet = async ( return response; } else { if (request.resourceName === '$metadata') { + const permLookup = await permissions.getReqPermissions(req); + const spec = generateODataMetadata( + vocab, + models[vocab].abstractSql, + permLookup, + ); return { statusCode: 200, - body: models[vocab].odataMetadata, - headers: { 'content-type': 'xml' }, + body: spec, + headers: { 'content-type': 'application/json' }, + }; + } else if (request.resourceName === 'fern.json') { + // https://docs.oasis-open.org/odata/odata-openapi/v1.0/cn01/odata-openapi-v1.0-cn01.html#sec_ProvidingOASDocumentsforanODataServi + // Following the OASIS OData to openapi translation guide the openapi.json is an independent resource + const permLookup = await permissions.getReqPermissions(req); + const spec = generateODataMetadata( + vocab, + models[vocab].abstractSql, + permLookup, + ); + const openApispec = generateODataMetadataAsOpenApi( + spec, + req.originalUrl.replace('openapi.json', ''), + req.hostname, + ); + return { + statusCode: 200, + body: openApispec, + headers: { 'content-type': 'application/json' }, + }; + } else if (request.resourceName === 'openapi.json') { + // https://docs.oasis-open.org/odata/odata-openapi/v1.0/cn01/odata-openapi-v1.0-cn01.html#sec_ProvidingOASDocumentsforanODataServi + // Following the OASIS OData to openapi translation guide the openapi.json is an independent resource + const permLookup = await permissions.getReqPermissions(req); + const fernSpec = generateFernMetadata( + vocab, + models[vocab].abstractSql, + permLookup, + ); + return { + statusCode: 200, + body: fernSpec, + headers: { 'content-type': 'application/json' }, }; } else { // TODO: request.resourceName can be '$serviceroot' or a resource and we should return an odata xml document based on that @@ -1724,6 +1765,8 @@ const respondGet = async ( } }; +// paths./any/.get.responses.200.content.application/json.schema.d + const runPost = async ( _req: Express.Request, request: uriParser.ODataRequest, diff --git a/src/sbvr-api/uri-parser.ts b/src/sbvr-api/uri-parser.ts index 28ed74988..ff0772a4f 100644 --- a/src/sbvr-api/uri-parser.ts +++ b/src/sbvr-api/uri-parser.ts @@ -259,7 +259,7 @@ const memoizedOdata2AbstractSQL = (() => { }; })(); -export const metadataEndpoints = ['$metadata', '$serviceroot']; +export const metadataEndpoints = ['$metadata', '$serviceroot', 'openapi.json']; export async function parseOData( b: UnparsedRequest & { _isChangeSet?: false }, diff --git a/test/01-constrain.test.ts b/test/01-constrain.test.ts index 5ce42e3f3..3cc22e40d 100644 --- a/test/01-constrain.test.ts +++ b/test/01-constrain.test.ts @@ -31,7 +31,7 @@ describe('01 basic constrain tests', function () { }); it('create a student', async () => { - await supertest(testLocalServer) + const { body: student } = await supertest(testLocalServer) .post('/university/student') .send({ matrix_number: 1, @@ -41,6 +41,16 @@ describe('01 basic constrain tests', function () { semester_credits: 10, }) .expect(201); + + await supertest(testLocalServer) + .patch(`/university/student(${student.id})`) + .send({ + matrix_number: 1, + name: 'Johnny', + lastname: 'Doe', + birthday: new Date(), + semester_credits: 10, + }); }); it('should fail to create a student with same matrix number ', async () => { @@ -73,5 +83,22 @@ describe('01 basic constrain tests', function () { 'It is necessary that each student that has a semester credits, has a semester credits that is greater than or equal to 4 and is less than or equal to 16.', ); }); + + it('should create a student and delete it afterwards', async () => { + const { body: student } = await supertest(testLocalServer) + .post('/university/student') + .send({ + matrix_number: 3, + name: 'Mad', + lastname: 'Max', + birthday: new Date(), + semester_credits: 10, + }) + .expect(201); + + await supertest(testLocalServer) + .delete(`/university/student(${student.id})`) + .expect(200); + }); }); }); diff --git a/test/04-metadata.test.ts b/test/04-metadata.test.ts new file mode 100644 index 000000000..54cb01ffc --- /dev/null +++ b/test/04-metadata.test.ts @@ -0,0 +1,59 @@ +import * as fs from 'fs'; +import { expect } from 'chai'; +import * as supertest from 'supertest'; +import { testInit, testDeInit, testLocalServer } from './lib/test-init'; + +import { stringify } from 'yaml'; + +describe('04 metadata', function () { + describe('Full model access specification', async function () { + const fixturePath = __dirname + '/fixtures/04-metadata/config-full-access'; + let pineServer: Awaited>; + before(async () => { + pineServer = await testInit(fixturePath, true); + }); + + after(async () => { + await testDeInit(pineServer); + }); + + it('should send OData CSDL JSON on /$metadata', async () => { + const res = await supertest(testLocalServer) + .get('/example/$metadata') + .expect(200); + expect(res.body).to.be.an('object'); + }); + + it('should send OpenAPI spec JSON on /$metadata', async () => { + const res = await supertest(testLocalServer) + .get('/example/openapi.json') + .expect(200); + expect(res.body).to.be.an('object'); + }); + + it('should send fern spec JSON on /$metadata', async () => { + const res = await supertest(testLocalServer) + .get('/example/openapi.json') + .expect(200); + expect(res.body).to.be.an('object'); + }); + + it.only('OpenAPI spec should contain all paths and actions on resources', async () => { + // full CRUD access for device resource + const res = await supertest(testLocalServer) + .get('/example/openapi.json') + .expect(200); + expect(res.body).to.be.an('object'); + + const yamlString = stringify(res.body); + + console.log(`yamlString:${JSON.stringify(yamlString, null, 2)}`); + fs.writeFileSync('./fern/api/definition/example.yaml', yamlString); + + // for (const value of Object.values(res.body.paths)) { + // console.log(`value:${JSON.stringify(value, null, 2)}`); + // expect(value).to.have.keys(['get', 'patch', 'delete', 'post']); + // } + }); + }); +}); diff --git a/test/fixtures/04-metadata/config-full-access.ts b/test/fixtures/04-metadata/config-full-access.ts new file mode 100644 index 000000000..a68c04631 --- /dev/null +++ b/test/fixtures/04-metadata/config-full-access.ts @@ -0,0 +1,18 @@ +import type { ConfigLoader } from '../../../src/server-glue/module'; + +export default { + models: [ + { + apiRoot: 'example', + modelFile: __dirname + '/example.sbvr', + modelName: 'example', + }, + ], + users: [ + { + username: 'guest', + password: ' ', + permissions: ['resource.all'], + }, + ], +} as ConfigLoader.Config; diff --git a/test/fixtures/04-metadata/config-restricted-access.ts b/test/fixtures/04-metadata/config-restricted-access.ts new file mode 100644 index 000000000..0fcb74110 --- /dev/null +++ b/test/fixtures/04-metadata/config-restricted-access.ts @@ -0,0 +1,25 @@ +import type { ConfigLoader } from '../../../src/server-glue/module'; + +export default { + models: [ + { + apiRoot: 'example', + modelFile: __dirname + '/example.sbvr', + modelName: 'example', + }, + ], + users: [ + { + username: 'guest', + password: ' ', + permissions: [ + 'example.device.all', + 'example.application.create', + 'example.application.read', + 'example.application.update', + 'example.gateway.read', + 'example.gateway__connects__device.all', + ], + }, + ], +} as ConfigLoader.Config; diff --git a/test/fixtures/04-metadata/example.sbvr b/test/fixtures/04-metadata/example.sbvr new file mode 100644 index 000000000..581f36613 --- /dev/null +++ b/test/fixtures/04-metadata/example.sbvr @@ -0,0 +1,33 @@ +Vocabulary: example + +Term: name + Concept Type: Short Text (Type) +Term: note + Concept Type: Text (Type) +Term: type + Concept Type: Short Text (Type) + + +Term: application + +Fact Type: application has name + Necessity: each application has at most one name. +Fact Type: application has note + Necessity: each application has at most one note. + + +Term: device + +Fact Type: device has name + Necessity: each device has at most one name. +Fact Type: device has type + Necessity: each device has exactly one type. +Fact Type: device belongs to application + Necessity: each device belongs to exactly one application + + +Term: gateway + +Fact Type: gateway has name + Necessity: each gateway has exactly one name. +Fact Type: gateway connects device diff --git a/typings/odata-openapi.d.ts b/typings/odata-openapi.d.ts new file mode 100644 index 000000000..b91ee894d --- /dev/null +++ b/typings/odata-openapi.d.ts @@ -0,0 +1,6 @@ +declare module 'odata-openapi' { + export const csdl2openapi: ( + csdl, + { scheme, host, basePath, diagram, maxLevels } = {}, + ) => object; +}